diff --git a/src/NadekoBot.Tests/NadekoBot.Tests.csproj b/src/NadekoBot.Tests/NadekoBot.Tests.csproj index 06b68d4b1..81056fc9e 100644 --- a/src/NadekoBot.Tests/NadekoBot.Tests.csproj +++ b/src/NadekoBot.Tests/NadekoBot.Tests.csproj @@ -3,6 +3,7 @@ net6.0 10.0 + True false diff --git a/src/NadekoBot/Bot.cs b/src/NadekoBot/Bot.cs index 20cfd3391..32e1cd30d 100644 --- a/src/NadekoBot/Bot.cs +++ b/src/NadekoBot/Bot.cs @@ -6,11 +6,8 @@ using NadekoBot.Common; using NadekoBot.Services; using NadekoBot.Services.Database.Models; using NadekoBot.Extensions; -using System; -using System.Collections.Generic; using System.Collections.Immutable; using System.Diagnostics; -using System.Linq; using System.Net.Http; using System.Reflection; using System.Threading.Tasks; @@ -19,83 +16,81 @@ using NadekoBot.Common.ModuleBehaviors; using NadekoBot.Common.Configs; using NadekoBot.Db; using NadekoBot.Modules.Administration.Services; -using NadekoBot.Modules.Searches; -using Serilog; -namespace NadekoBot +namespace NadekoBot; + +public sealed class Bot { - public sealed class Bot + private readonly IBotCredentials _creds; + private readonly CommandService _commandService; + private readonly DbService _db; + private readonly IBotCredsProvider _credsProvider; + + public event Func JoinedGuild = delegate { return Task.CompletedTask; }; + + public DiscordSocketClient Client { get; } + public ImmutableArray AllGuildConfigs { get; private set; } + + private IServiceProvider Services { get; set; } + + public string Mention { get; private set; } + public bool IsReady { get; private set; } + + public Bot(int shardId, int? totalShards) { - private readonly IBotCredentials _creds; - private readonly CommandService _commandService; - private readonly DbService _db; - private readonly IBotCredsProvider _credsProvider; - - public event Func JoinedGuild = delegate { return Task.CompletedTask; }; - - public DiscordSocketClient Client { get; } - public ImmutableArray AllGuildConfigs { get; private set; } + if (shardId < 0) + throw new ArgumentOutOfRangeException(nameof(shardId)); - private IServiceProvider Services { get; set; } - - public string Mention { get; private set; } - public bool IsReady { get; private set; } - - public Bot(int shardId, int? totalShards) - { - if (shardId < 0) - throw new ArgumentOutOfRangeException(nameof(shardId)); - - _credsProvider = new BotCredsProvider(totalShards); - _creds = _credsProvider.GetCreds(); + _credsProvider = new BotCredsProvider(totalShards); + _creds = _credsProvider.GetCreds(); - _db = new DbService(_creds); + _db = new DbService(_creds); - if (shardId == 0) - { - _db.Setup(); - } + if (shardId == 0) + { + _db.Setup(); + } - Client = new DiscordSocketClient(new DiscordSocketConfig - { - MessageCacheSize = 50, - LogLevel = LogSeverity.Warning, - ConnectionTimeout = int.MaxValue, - TotalShards = _creds.TotalShards, - ShardId = shardId, - AlwaysDownloadUsers = false, - ExclusiveBulkDelete = true, - }); + Client = new DiscordSocketClient(new DiscordSocketConfig + { + MessageCacheSize = 50, + LogLevel = LogSeverity.Warning, + ConnectionTimeout = int.MaxValue, + TotalShards = _creds.TotalShards, + ShardId = shardId, + AlwaysDownloadUsers = false, + ExclusiveBulkDelete = true, + }); - _commandService = new CommandService(new CommandServiceConfig() - { - CaseSensitiveCommands = false, - DefaultRunMode = RunMode.Sync, - }); + _commandService = new CommandService(new CommandServiceConfig() + { + CaseSensitiveCommands = false, + DefaultRunMode = RunMode.Sync, + }); #if GLOBAL_NADEKO || DEBUG - Client.Log += Client_Log; + Client.Log += Client_Log; #endif - } + } - public List GetCurrentGuildIds() + public List GetCurrentGuildIds() + { + return Client.Guilds.Select(x => x.Id).ToList(); + } + + private void AddServices() + { + var startingGuildIdList = GetCurrentGuildIds(); + var sw = Stopwatch.StartNew(); + var _bot = Client.CurrentUser; + + using (var uow = _db.GetDbContext()) { - return Client.Guilds.Select(x => x.Id).ToList(); + uow.EnsureUserCreated(_bot.Id, _bot.Username, _bot.Discriminator, _bot.AvatarId); + AllGuildConfigs = uow.GuildConfigs.GetAllGuildConfigs(startingGuildIdList).ToImmutableArray(); } - - private void AddServices() - { - var startingGuildIdList = GetCurrentGuildIds(); - var sw = Stopwatch.StartNew(); - var _bot = Client.CurrentUser; - - using (var uow = _db.GetDbContext()) - { - uow.EnsureUserCreated(_bot.Id, _bot.Username, _bot.Discriminator, _bot.AvatarId); - AllGuildConfigs = uow.GuildConfigs.GetAllGuildConfigs(startingGuildIdList).ToImmutableArray(); - } - var svcs = new ServiceCollection() + var svcs = new ServiceCollection() .AddTransient(_ => _credsProvider.GetCreds()) // bot creds .AddSingleton(_credsProvider) .AddSingleton(_db) // database @@ -118,248 +113,247 @@ namespace NadekoBot #else .AddSingleton() #endif - ; + ; - svcs.AddHttpClient(); - svcs.AddHttpClient("memelist").ConfigurePrimaryHttpMessageHandler(() => new HttpClientHandler - { - AllowAutoRedirect = false - }); + svcs.AddHttpClient(); + svcs.AddHttpClient("memelist").ConfigurePrimaryHttpMessageHandler(() => new HttpClientHandler + { + AllowAutoRedirect = false + }); - if (Environment.GetEnvironmentVariable("NADEKOBOT_IS_COORDINATED") != "1") - { - svcs.AddSingleton(); - } - else - { - svcs.AddSingleton() - .AddSingleton(x => x.GetRequiredService()) - .AddSingleton(x => x.GetRequiredService()); - } + if (Environment.GetEnvironmentVariable("NADEKOBOT_IS_COORDINATED") != "1") + { + svcs.AddSingleton(); + } + else + { + svcs.AddSingleton() + .AddSingleton(x => x.GetRequiredService()) + .AddSingleton(x => x.GetRequiredService()); + } - svcs.AddSingleton() - .AddSingleton(x => x.GetRequiredService()) - .AddSingleton() - .AddSingleton(x => x.GetRequiredService()) - .AddSingleton(x => x.GetRequiredService()) - .AddSingleton(); + svcs.AddSingleton() + .AddSingleton(x => x.GetRequiredService()) + .AddSingleton() + .AddSingleton(x => x.GetRequiredService()) + .AddSingleton(x => x.GetRequiredService()) + .AddSingleton(); - svcs.Scan(scan => scan - .FromAssemblyOf() - .AddClasses(classes => classes - .AssignableToAny( - // services - typeof(INService), + svcs.Scan(scan => scan + .FromAssemblyOf() + .AddClasses(classes => classes + .AssignableToAny( + // services + typeof(INService), - // behaviours - typeof(IEarlyBehavior), - typeof(ILateBlocker), - typeof(IInputTransformer), - typeof(ILateExecutor)) + // behaviours + typeof(IEarlyBehavior), + typeof(ILateBlocker), + typeof(IInputTransformer), + typeof(ILateExecutor)) #if GLOBAL_NADEKO .WithoutAttribute() #endif - ) - .AsSelfWithInterfaces() - .WithSingletonLifetime() - ); + ) + .AsSelfWithInterfaces() + .WithSingletonLifetime() + ); - //initialize Services - Services = svcs.BuildServiceProvider(); - var exec = Services.GetRequiredService(); - exec.Initialize(); + //initialize Services + Services = svcs.BuildServiceProvider(); + var exec = Services.GetRequiredService(); + exec.Initialize(); - if (Client.ShardId == 0) - { - ApplyConfigMigrations(); - } + if (Client.ShardId == 0) + { + ApplyConfigMigrations(); + } - _ = LoadTypeReaders(typeof(Bot).Assembly); + _ = LoadTypeReaders(typeof(Bot).Assembly); - sw.Stop(); - Log.Information($"All services loaded in {sw.Elapsed.TotalSeconds:F2}s"); - } + sw.Stop(); + Log.Information($"All services loaded in {sw.Elapsed.TotalSeconds:F2}s"); + } - private void ApplyConfigMigrations() + private void ApplyConfigMigrations() + { + // execute all migrators + var migrators = Services.GetServices(); + foreach (var migrator in migrators) { - // execute all migrators - var migrators = Services.GetServices(); - foreach (var migrator in migrators) - { - migrator.EnsureMigrated(); - } - } - - private IEnumerable LoadTypeReaders(Assembly assembly) - { - Type[] allTypes; - try - { - allTypes = assembly.GetTypes(); - } - catch (ReflectionTypeLoadException ex) - { - Log.Warning(ex.LoaderExceptions[0], "Error getting types"); - return Enumerable.Empty(); - } - var filteredTypes = allTypes - .Where(x => x.IsSubclassOf(typeof(TypeReader)) - && x.BaseType.GetGenericArguments().Length > 0 - && !x.IsAbstract); - - var toReturn = new List(); - foreach (var ft in filteredTypes) - { - var x = (TypeReader)ActivatorUtilities.CreateInstance(Services, ft); - var baseType = ft.BaseType; - var typeArgs = baseType.GetGenericArguments(); - _commandService.AddTypeReader(typeArgs[0], x); - toReturn.Add(x); - } - - return toReturn; - } - - private async Task LoginAsync(string token) - { - var clientReady = new TaskCompletionSource(); - - Task SetClientReady() - { - var _ = Task.Run(async () => - { - clientReady.TrySetResult(true); - try - { - foreach (var chan in (await Client.GetDMChannelsAsync().ConfigureAwait(false))) - { - await chan.CloseAsync().ConfigureAwait(false); - } - } - catch - { - // ignored - } - }); - return Task.CompletedTask; - } - - //connect - Log.Information("Shard {ShardId} logging in ...", Client.ShardId); - try - { - await Client.LoginAsync(TokenType.Bot, token).ConfigureAwait(false); - await Client.StartAsync().ConfigureAwait(false); - } - catch (HttpException ex) - { - LoginErrorHandler.Handle(ex); - Helpers.ReadErrorAndExit(3); - } - catch (Exception ex) - { - LoginErrorHandler.Handle(ex); - Helpers.ReadErrorAndExit(4); - } - - Client.Ready += SetClientReady; - await clientReady.Task.ConfigureAwait(false); - Client.Ready -= SetClientReady; - - Client.JoinedGuild += Client_JoinedGuild; - Client.LeftGuild += Client_LeftGuild; - - Log.Information("Shard {0} logged in.", Client.ShardId); - } - - private Task Client_LeftGuild(SocketGuild arg) - { - Log.Information("Left server: {0} [{1}]", arg?.Name, arg?.Id); - return Task.CompletedTask; - } - - private Task Client_JoinedGuild(SocketGuild arg) - { - Log.Information($"Joined server: {0} [{1}]", arg.Name, arg.Id); - var _ = Task.Run(async () => - { - GuildConfig gc; - using (var uow = _db.GetDbContext()) - { - gc = uow.GuildConfigsForId(arg.Id); - } - await JoinedGuild.Invoke(gc).ConfigureAwait(false); - }); - return Task.CompletedTask; - } - - public async Task RunAsync() - { - var sw = Stopwatch.StartNew(); - - await LoginAsync(_creds.Token).ConfigureAwait(false); - - Mention = Client.CurrentUser.Mention; - Log.Information("Shard {ShardId} loading services...", Client.ShardId); - try - { - AddServices(); - } - catch (Exception ex) - { - Log.Error(ex, "Error adding services"); - Helpers.ReadErrorAndExit(9); - } - - sw.Stop(); - Log.Information("Shard {ShardId} connected in {Elapsed:F2}s", Client.ShardId, sw.Elapsed.TotalSeconds); - var commandHandler = Services.GetRequiredService(); - - // start handling messages received in commandhandler - await commandHandler.StartHandling().ConfigureAwait(false); - - await _commandService.AddModulesAsync(typeof(Bot).Assembly, Services); - - IsReady = true; - _ = Task.Run(ExecuteReadySubscriptions); - Log.Information("Shard {ShardId} ready", Client.ShardId); - } - - private Task ExecuteReadySubscriptions() - { - var readyExecutors = Services.GetServices(); - var tasks = readyExecutors.Select(async toExec => - { - try - { - await toExec.OnReadyAsync(); - } - catch (Exception ex) - { - Log.Error(ex, - "Failed running OnReadyAsync method on {Type} type: {Message}", - toExec.GetType().Name, - ex.Message); - } - }); - - return Task.WhenAll(tasks); - } - - private Task Client_Log(LogMessage arg) - { - if (arg.Exception != null) - Log.Warning(arg.Exception, arg.Source + " | " + arg.Message); - else - Log.Warning(arg.Source + " | " + arg.Message); - - return Task.CompletedTask; - } - - public async Task RunAndBlockAsync() - { - await RunAsync().ConfigureAwait(false); - await Task.Delay(-1).ConfigureAwait(false); + migrator.EnsureMigrated(); } } -} + + private IEnumerable LoadTypeReaders(Assembly assembly) + { + Type[] allTypes; + try + { + allTypes = assembly.GetTypes(); + } + catch (ReflectionTypeLoadException ex) + { + Log.Warning(ex.LoaderExceptions[0], "Error getting types"); + return Enumerable.Empty(); + } + var filteredTypes = allTypes + .Where(x => x.IsSubclassOf(typeof(TypeReader)) + && x.BaseType.GetGenericArguments().Length > 0 + && !x.IsAbstract); + + var toReturn = new List(); + foreach (var ft in filteredTypes) + { + var x = (TypeReader)ActivatorUtilities.CreateInstance(Services, ft); + var baseType = ft.BaseType; + var typeArgs = baseType.GetGenericArguments(); + _commandService.AddTypeReader(typeArgs[0], x); + toReturn.Add(x); + } + + return toReturn; + } + + private async Task LoginAsync(string token) + { + var clientReady = new TaskCompletionSource(); + + Task SetClientReady() + { + var _ = Task.Run(async () => + { + clientReady.TrySetResult(true); + try + { + foreach (var chan in (await Client.GetDMChannelsAsync().ConfigureAwait(false))) + { + await chan.CloseAsync().ConfigureAwait(false); + } + } + catch + { + // ignored + } + }); + return Task.CompletedTask; + } + + //connect + Log.Information("Shard {ShardId} logging in ...", Client.ShardId); + try + { + await Client.LoginAsync(TokenType.Bot, token).ConfigureAwait(false); + await Client.StartAsync().ConfigureAwait(false); + } + catch (HttpException ex) + { + LoginErrorHandler.Handle(ex); + Helpers.ReadErrorAndExit(3); + } + catch (Exception ex) + { + LoginErrorHandler.Handle(ex); + Helpers.ReadErrorAndExit(4); + } + + Client.Ready += SetClientReady; + await clientReady.Task.ConfigureAwait(false); + Client.Ready -= SetClientReady; + + Client.JoinedGuild += Client_JoinedGuild; + Client.LeftGuild += Client_LeftGuild; + + Log.Information("Shard {0} logged in.", Client.ShardId); + } + + private Task Client_LeftGuild(SocketGuild arg) + { + Log.Information("Left server: {0} [{1}]", arg?.Name, arg?.Id); + return Task.CompletedTask; + } + + private Task Client_JoinedGuild(SocketGuild arg) + { + Log.Information($"Joined server: {0} [{1}]", arg.Name, arg.Id); + var _ = Task.Run(async () => + { + GuildConfig gc; + using (var uow = _db.GetDbContext()) + { + gc = uow.GuildConfigsForId(arg.Id); + } + await JoinedGuild.Invoke(gc).ConfigureAwait(false); + }); + return Task.CompletedTask; + } + + public async Task RunAsync() + { + var sw = Stopwatch.StartNew(); + + await LoginAsync(_creds.Token).ConfigureAwait(false); + + Mention = Client.CurrentUser.Mention; + Log.Information("Shard {ShardId} loading services...", Client.ShardId); + try + { + AddServices(); + } + catch (Exception ex) + { + Log.Error(ex, "Error adding services"); + Helpers.ReadErrorAndExit(9); + } + + sw.Stop(); + Log.Information("Shard {ShardId} connected in {Elapsed:F2}s", Client.ShardId, sw.Elapsed.TotalSeconds); + var commandHandler = Services.GetRequiredService(); + + // start handling messages received in commandhandler + await commandHandler.StartHandling().ConfigureAwait(false); + + await _commandService.AddModulesAsync(typeof(Bot).Assembly, Services); + + IsReady = true; + _ = Task.Run(ExecuteReadySubscriptions); + Log.Information("Shard {ShardId} ready", Client.ShardId); + } + + private Task ExecuteReadySubscriptions() + { + var readyExecutors = Services.GetServices(); + var tasks = readyExecutors.Select(async toExec => + { + try + { + await toExec.OnReadyAsync(); + } + catch (Exception ex) + { + Log.Error(ex, + "Failed running OnReadyAsync method on {Type} type: {Message}", + toExec.GetType().Name, + ex.Message); + } + }); + + return Task.WhenAll(tasks); + } + + private Task Client_Log(LogMessage arg) + { + if (arg.Exception != null) + Log.Warning(arg.Exception, arg.Source + " | " + arg.Message); + else + Log.Warning(arg.Source + " | " + arg.Message); + + return Task.CompletedTask; + } + + public async Task RunAndBlockAsync() + { + await RunAsync().ConfigureAwait(false); + await Task.Delay(-1).ConfigureAwait(false); + } +} \ No newline at end of file diff --git a/src/NadekoBot/Common/AsyncLazy.cs b/src/NadekoBot/Common/AsyncLazy.cs index 25de32efc..d3a45812f 100644 --- a/src/NadekoBot/Common/AsyncLazy.cs +++ b/src/NadekoBot/Common/AsyncLazy.cs @@ -1,20 +1,17 @@ -using System; -using System.Runtime.CompilerServices; +using System.Runtime.CompilerServices; using System.Threading.Tasks; -namespace NadekoBot.Common +namespace NadekoBot.Common; + +public class AsyncLazy : Lazy> { - public class AsyncLazy : Lazy> - { - public AsyncLazy(Func valueFactory) : - base(() => Task.Run(valueFactory)) - { } + public AsyncLazy(Func valueFactory) : + base(() => Task.Run(valueFactory)) + { } - public AsyncLazy(Func> taskFactory) : - base(() => Task.Run(taskFactory)) - { } + public AsyncLazy(Func> taskFactory) : + base(() => Task.Run(taskFactory)) + { } - public TaskAwaiter GetAwaiter() { return Value.GetAwaiter(); } - } - -} + public TaskAwaiter GetAwaiter() { return Value.GetAwaiter(); } +} \ No newline at end of file diff --git a/src/NadekoBot/Common/Attributes/Aliases.cs b/src/NadekoBot/Common/Attributes/Aliases.cs index a7c9967f4..8d5c1be52 100644 --- a/src/NadekoBot/Common/Attributes/Aliases.cs +++ b/src/NadekoBot/Common/Attributes/Aliases.cs @@ -1,18 +1,13 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Runtime.CompilerServices; +using System.Runtime.CompilerServices; using Discord.Commands; -using NadekoBot.Services; -namespace NadekoBot.Common.Attributes + +namespace NadekoBot.Common.Attributes; + +[AttributeUsage(AttributeTargets.Method)] +public sealed class AliasesAttribute : AliasAttribute { - [AttributeUsage(AttributeTargets.Method)] - public sealed class AliasesAttribute : AliasAttribute + public AliasesAttribute([CallerMemberName] string memberName = "") + : base(CommandNameLoadHelper.GetAliasesFor(memberName)) { - public AliasesAttribute([CallerMemberName] string memberName = "") - : base(CommandNameLoadHelper.GetAliasesFor(memberName)) - { - } } -} +} \ No newline at end of file diff --git a/src/NadekoBot/Common/Attributes/BotPerm.cs b/src/NadekoBot/Common/Attributes/BotPerm.cs index 4804166a5..d1f3ffc7e 100644 --- a/src/NadekoBot/Common/Attributes/BotPerm.cs +++ b/src/NadekoBot/Common/Attributes/BotPerm.cs @@ -1,15 +1,14 @@ using Discord.Commands; -namespace Discord -{ - public class BotPermAttribute : RequireBotPermissionAttribute - { - public BotPermAttribute(GuildPerm permission) : base((GuildPermission)permission) - { - } +namespace Discord; - public BotPermAttribute(ChannelPerm permission) : base((ChannelPermission)permission) - { - } +public class BotPermAttribute : RequireBotPermissionAttribute +{ + public BotPermAttribute(GuildPerm permission) : base((GuildPermission)permission) + { } -} + + public BotPermAttribute(ChannelPerm permission) : base((ChannelPermission)permission) + { + } +} \ No newline at end of file diff --git a/src/NadekoBot/Common/Attributes/CommandNameLoadHelper.cs b/src/NadekoBot/Common/Attributes/CommandNameLoadHelper.cs index e85b7e765..bf4a5b067 100644 --- a/src/NadekoBot/Common/Attributes/CommandNameLoadHelper.cs +++ b/src/NadekoBot/Common/Attributes/CommandNameLoadHelper.cs @@ -1,36 +1,32 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; +using System.IO; -namespace NadekoBot.Common.Attributes +namespace NadekoBot.Common.Attributes; + +public static class CommandNameLoadHelper { - public static class CommandNameLoadHelper + + private static YamlDotNet.Serialization.IDeserializer _deserializer + = new YamlDotNet.Serialization.Deserializer(); + + public static Lazy> LazyCommandAliases + = new Lazy>(() => LoadCommandNames()); + public static Dictionary LoadCommandNames(string aliasesFilePath = "data/aliases.yml") { - - private static YamlDotNet.Serialization.IDeserializer _deserializer - = new YamlDotNet.Serialization.Deserializer(); - - public static Lazy> LazyCommandAliases - = new Lazy>(() => LoadCommandNames()); - public static Dictionary LoadCommandNames(string aliasesFilePath = "data/aliases.yml") - { - var text = File.ReadAllText(aliasesFilePath); - return _deserializer.Deserialize>(text); - } + var text = File.ReadAllText(aliasesFilePath); + return _deserializer.Deserialize>(text); + } - public static string[] GetAliasesFor(string methodName) - => LazyCommandAliases.Value.TryGetValue(methodName.ToLowerInvariant(), out var aliases) && aliases.Length > 1 - ? aliases.Skip(1).ToArray() - : Array.Empty(); + public static string[] GetAliasesFor(string methodName) + => LazyCommandAliases.Value.TryGetValue(methodName.ToLowerInvariant(), out var aliases) && aliases.Length > 1 + ? aliases.Skip(1).ToArray() + : Array.Empty(); - public static string GetCommandNameFor(string methodName) - { - methodName = methodName.ToLowerInvariant(); - var toReturn = LazyCommandAliases.Value.TryGetValue(methodName, out var aliases) && aliases.Length > 0 - ? aliases[0] - : methodName; - return toReturn; - } + public static string GetCommandNameFor(string methodName) + { + methodName = methodName.ToLowerInvariant(); + var toReturn = LazyCommandAliases.Value.TryGetValue(methodName, out var aliases) && aliases.Length > 0 + ? aliases[0] + : methodName; + return toReturn; } } \ No newline at end of file diff --git a/src/NadekoBot/Common/Attributes/Description.cs b/src/NadekoBot/Common/Attributes/Description.cs index 86719f505..44b6bd525 100644 --- a/src/NadekoBot/Common/Attributes/Description.cs +++ b/src/NadekoBot/Common/Attributes/Description.cs @@ -1,16 +1,12 @@ -using System; -using System.Runtime.CompilerServices; -using Discord.Commands; -using NadekoBot.Services; +using Discord.Commands; -namespace NadekoBot.Common.Attributes +namespace NadekoBot.Common.Attributes; + +[AttributeUsage(AttributeTargets.Method)] +public sealed class DescriptionAttribute : SummaryAttribute { - [AttributeUsage(AttributeTargets.Method)] - public sealed class DescriptionAttribute : SummaryAttribute + // Localization.LoadCommand(memberName.ToLowerInvariant()).Desc + public DescriptionAttribute(string text = "") : base(text) { - // Localization.LoadCommand(memberName.ToLowerInvariant()).Desc - public DescriptionAttribute(string text = "") : base(text) - { - } } -} +} \ No newline at end of file diff --git a/src/NadekoBot/Common/Attributes/LeftoverAttribute.cs b/src/NadekoBot/Common/Attributes/LeftoverAttribute.cs index 7f6dc21bb..61f99bd92 100644 --- a/src/NadekoBot/Common/Attributes/LeftoverAttribute.cs +++ b/src/NadekoBot/Common/Attributes/LeftoverAttribute.cs @@ -1,9 +1,8 @@ -namespace Discord.Commands +namespace Discord.Commands; + +public class LeftoverAttribute : RemainderAttribute { - public class LeftoverAttribute : RemainderAttribute + public LeftoverAttribute() { - public LeftoverAttribute() - { - } } -} +} \ No newline at end of file diff --git a/src/NadekoBot/Common/Attributes/NadekoCommand.cs b/src/NadekoBot/Common/Attributes/NadekoCommand.cs index cb8b1a6f1..a61059bf9 100644 --- a/src/NadekoBot/Common/Attributes/NadekoCommand.cs +++ b/src/NadekoBot/Common/Attributes/NadekoCommand.cs @@ -1,19 +1,16 @@ -using System; -using System.Runtime.CompilerServices; +using System.Runtime.CompilerServices; using Discord.Commands; -using NadekoBot.Services; -namespace NadekoBot.Common.Attributes +namespace NadekoBot.Common.Attributes; + +[AttributeUsage(AttributeTargets.Method)] +public sealed class NadekoCommandAttribute : CommandAttribute { - [AttributeUsage(AttributeTargets.Method)] - public sealed class NadekoCommandAttribute : CommandAttribute + public NadekoCommandAttribute([CallerMemberName] string memberName="") + : base(CommandNameLoadHelper.GetCommandNameFor(memberName)) { - public NadekoCommandAttribute([CallerMemberName] string memberName="") - : base(CommandNameLoadHelper.GetCommandNameFor(memberName)) - { - this.MethodName = memberName.ToLowerInvariant(); - } - - public string MethodName { get; } + this.MethodName = memberName.ToLowerInvariant(); } -} + + public string MethodName { get; } +} \ No newline at end of file diff --git a/src/NadekoBot/Common/Attributes/NadekoModuleAttribute.cs b/src/NadekoBot/Common/Attributes/NadekoModuleAttribute.cs index 31ba17309..f51815d68 100644 --- a/src/NadekoBot/Common/Attributes/NadekoModuleAttribute.cs +++ b/src/NadekoBot/Common/Attributes/NadekoModuleAttribute.cs @@ -1,14 +1,11 @@ -using System; -using Discord.Commands; +using Discord.Commands; -namespace NadekoBot.Common.Attributes +namespace NadekoBot.Common.Attributes; + +[AttributeUsage(AttributeTargets.Class)] +sealed class NadekoModuleAttribute : GroupAttribute { - [AttributeUsage(AttributeTargets.Class)] - sealed class NadekoModuleAttribute : GroupAttribute + public NadekoModuleAttribute(string moduleName) : base(moduleName) { - public NadekoModuleAttribute(string moduleName) : base(moduleName) - { - } } -} - +} \ No newline at end of file diff --git a/src/NadekoBot/Common/Attributes/NadekoOptions.cs b/src/NadekoBot/Common/Attributes/NadekoOptions.cs index 5b2ee3a09..ce221b409 100644 --- a/src/NadekoBot/Common/Attributes/NadekoOptions.cs +++ b/src/NadekoBot/Common/Attributes/NadekoOptions.cs @@ -1,15 +1,12 @@ -using System; +namespace NadekoBot.Common.Attributes; -namespace NadekoBot.Common.Attributes +[AttributeUsage(AttributeTargets.Method)] +public sealed class NadekoOptionsAttribute : Attribute { - [AttributeUsage(AttributeTargets.Method)] - public sealed class NadekoOptionsAttribute : Attribute - { - public Type OptionType { get; set; } + public Type OptionType { get; set; } - public NadekoOptionsAttribute(Type t) - { - this.OptionType = t; - } + public NadekoOptionsAttribute(Type t) + { + this.OptionType = t; } -} +} \ No newline at end of file diff --git a/src/NadekoBot/Common/Attributes/OwnerOnlyAttribute.cs b/src/NadekoBot/Common/Attributes/OwnerOnlyAttribute.cs index 7ccae8631..88c49877b 100644 --- a/src/NadekoBot/Common/Attributes/OwnerOnlyAttribute.cs +++ b/src/NadekoBot/Common/Attributes/OwnerOnlyAttribute.cs @@ -1,20 +1,18 @@ -using System; -using System.Threading.Tasks; +using System.Threading.Tasks; using Discord.Commands; using Microsoft.Extensions.DependencyInjection; using NadekoBot.Extensions; using NadekoBot.Services; -namespace NadekoBot.Common.Attributes -{ - [AttributeUsage(AttributeTargets.Method | AttributeTargets.Class)] - public sealed class OwnerOnlyAttribute : PreconditionAttribute - { - public override Task CheckPermissionsAsync(ICommandContext context, CommandInfo executingCommand, IServiceProvider services) - { - var creds = services.GetRequiredService().GetCreds(); +namespace NadekoBot.Common.Attributes; - return Task.FromResult((creds.IsOwner(context.User) || context.Client.CurrentUser.Id == context.User.Id ? PreconditionResult.FromSuccess() : PreconditionResult.FromError("Not owner"))); - } +[AttributeUsage(AttributeTargets.Method | AttributeTargets.Class)] +public sealed class OwnerOnlyAttribute : PreconditionAttribute +{ + public override Task CheckPermissionsAsync(ICommandContext context, CommandInfo executingCommand, IServiceProvider services) + { + var creds = services.GetRequiredService().GetCreds(); + + return Task.FromResult((creds.IsOwner(context.User) || context.Client.CurrentUser.Id == context.User.Id ? PreconditionResult.FromSuccess() : PreconditionResult.FromError("Not owner"))); } } \ No newline at end of file diff --git a/src/NadekoBot/Common/Attributes/Ratelimit.cs b/src/NadekoBot/Common/Attributes/Ratelimit.cs index 3a94c6ef7..5078e2959 100644 --- a/src/NadekoBot/Common/Attributes/Ratelimit.cs +++ b/src/NadekoBot/Common/Attributes/Ratelimit.cs @@ -1,38 +1,36 @@ using Discord.Commands; using NadekoBot.Services; -using System; using System.Threading.Tasks; using Microsoft.Extensions.DependencyInjection; -namespace NadekoBot.Common.Attributes +namespace NadekoBot.Common.Attributes; + +[AttributeUsage(AttributeTargets.Method)] +public sealed class RatelimitAttribute : PreconditionAttribute { - [AttributeUsage(AttributeTargets.Method)] - public sealed class RatelimitAttribute : PreconditionAttribute + public int Seconds { get; } + + public RatelimitAttribute(int seconds) { - public int Seconds { get; } + if (seconds <= 0) + throw new ArgumentOutOfRangeException(nameof(seconds)); - public RatelimitAttribute(int seconds) - { - if (seconds <= 0) - throw new ArgumentOutOfRangeException(nameof(seconds)); - - Seconds = seconds; - } - - public override Task CheckPermissionsAsync(ICommandContext context, CommandInfo command, IServiceProvider services) - { - if (Seconds == 0) - return Task.FromResult(PreconditionResult.FromSuccess()); - - var cache = services.GetRequiredService(); - var rem = cache.TryAddRatelimit(context.User.Id, command.Name, Seconds); - - if(rem is null) - return Task.FromResult(PreconditionResult.FromSuccess()); - - var msgContent = $"You can use this command again in {rem.Value.TotalSeconds:F1}s."; - - return Task.FromResult(PreconditionResult.FromError(msgContent)); - } + Seconds = seconds; } -} + + public override Task CheckPermissionsAsync(ICommandContext context, CommandInfo command, IServiceProvider services) + { + if (Seconds == 0) + return Task.FromResult(PreconditionResult.FromSuccess()); + + var cache = services.GetRequiredService(); + var rem = cache.TryAddRatelimit(context.User.Id, command.Name, Seconds); + + if(rem is null) + return Task.FromResult(PreconditionResult.FromSuccess()); + + var msgContent = $"You can use this command again in {rem.Value.TotalSeconds:F1}s."; + + return Task.FromResult(PreconditionResult.FromError(msgContent)); + } +} \ No newline at end of file diff --git a/src/NadekoBot/Common/Attributes/Usage.cs b/src/NadekoBot/Common/Attributes/Usage.cs index 42e10e7f4..7e3f24d4b 100644 --- a/src/NadekoBot/Common/Attributes/Usage.cs +++ b/src/NadekoBot/Common/Attributes/Usage.cs @@ -1,21 +1,16 @@ -using System; -using System.Runtime.CompilerServices; -using Discord.Commands; -using NadekoBot.Services; -using Newtonsoft.Json; +using Discord.Commands; -namespace NadekoBot.Common.Attributes +namespace NadekoBot.Common.Attributes; + +[AttributeUsage(AttributeTargets.Method)] +public sealed class UsageAttribute : RemarksAttribute { - [AttributeUsage(AttributeTargets.Method)] - public sealed class UsageAttribute : RemarksAttribute + // public static string GetUsage(string memberName) + // { + // var usage = Localization.LoadCommand(memberName.ToLowerInvariant()).Usage; + // return JsonConvert.SerializeObject(usage); + // } + public UsageAttribute(string text = "") : base(text) { - // public static string GetUsage(string memberName) - // { - // var usage = Localization.LoadCommand(memberName.ToLowerInvariant()).Usage; - // return JsonConvert.SerializeObject(usage); - // } - public UsageAttribute(string text = "") : base(text) - { - } } -} +} \ No newline at end of file diff --git a/src/NadekoBot/Common/Attributes/UserPerm.cs b/src/NadekoBot/Common/Attributes/UserPerm.cs index 4b462e078..e00af4ed5 100644 --- a/src/NadekoBot/Common/Attributes/UserPerm.cs +++ b/src/NadekoBot/Common/Attributes/UserPerm.cs @@ -1,33 +1,31 @@ -using System; -using System.Threading.Tasks; +using System.Threading.Tasks; using Discord.Commands; using Microsoft.Extensions.DependencyInjection; using NadekoBot.Modules.Administration.Services; -namespace Discord +namespace Discord; + +[AttributeUsage(AttributeTargets.Method, AllowMultiple = false)] +public class UserPermAttribute : PreconditionAttribute { - [AttributeUsage(AttributeTargets.Method, AllowMultiple = false)] - public class UserPermAttribute : PreconditionAttribute + public RequireUserPermissionAttribute UserPermissionAttribute { get; } + + public UserPermAttribute(GuildPerm permission) { - public RequireUserPermissionAttribute UserPermissionAttribute { get; } - - public UserPermAttribute(GuildPerm permission) - { - UserPermissionAttribute = new RequireUserPermissionAttribute((GuildPermission)permission); - } - - public UserPermAttribute(ChannelPerm permission) - { - UserPermissionAttribute = new RequireUserPermissionAttribute((ChannelPermission)permission); - } - - public override Task CheckPermissionsAsync(ICommandContext context, CommandInfo command, IServiceProvider services) - { - var permService = services.GetRequiredService(); - if (permService.TryGetOverrides(context.Guild?.Id ?? 0, command.Name.ToUpperInvariant(), out var _)) - return Task.FromResult(PreconditionResult.FromSuccess()); - - return UserPermissionAttribute.CheckPermissionsAsync(context, command, services); - } + UserPermissionAttribute = new RequireUserPermissionAttribute((GuildPermission)permission); } -} + + public UserPermAttribute(ChannelPerm permission) + { + UserPermissionAttribute = new RequireUserPermissionAttribute((ChannelPermission)permission); + } + + public override Task CheckPermissionsAsync(ICommandContext context, CommandInfo command, IServiceProvider services) + { + var permService = services.GetRequiredService(); + if (permService.TryGetOverrides(context.Guild?.Id ?? 0, command.Name.ToUpperInvariant(), out var _)) + return Task.FromResult(PreconditionResult.FromSuccess()); + + return UserPermissionAttribute.CheckPermissionsAsync(context, command, services); + } +} \ No newline at end of file diff --git a/src/NadekoBot/Common/CmdStrings.cs b/src/NadekoBot/Common/CmdStrings.cs index 2b051b978..4c05fe309 100644 --- a/src/NadekoBot/Common/CmdStrings.cs +++ b/src/NadekoBot/Common/CmdStrings.cs @@ -1,20 +1,19 @@ using Newtonsoft.Json; -namespace NadekoBot.Common -{ - public class CmdStrings - { - public string[] Usages { get; } - public string Description { get; } +namespace NadekoBot.Common; - [JsonConstructor] - public CmdStrings( - [JsonProperty("args")]string[] usages, - [JsonProperty("desc")]string description - ) - { - Usages = usages; - Description = description; - } +public class CmdStrings +{ + public string[] Usages { get; } + public string Description { get; } + + [JsonConstructor] + public CmdStrings( + [JsonProperty("args")]string[] usages, + [JsonProperty("desc")]string description + ) + { + Usages = usages; + Description = description; } } \ No newline at end of file diff --git a/src/NadekoBot/Common/Collections/ConcurrentHashSet.cs b/src/NadekoBot/Common/Collections/ConcurrentHashSet.cs index 99759f21b..444c55632 100644 --- a/src/NadekoBot/Common/Collections/ConcurrentHashSet.cs +++ b/src/NadekoBot/Common/Collections/ConcurrentHashSet.cs @@ -1,772 +1,768 @@ // License MIT // Source: https://github.com/i3arnon/ConcurrentHashSet -using System; using System.Collections; -using System.Collections.Generic; using System.Diagnostics; -using System.Linq; using System.Threading; -namespace NadekoBot.Common.Collections +namespace NadekoBot.Common.Collections; + +/// +/// Represents a thread-safe hash-based unique collection. +/// +/// The type of the items in the collection. +/// +/// All public members of are thread-safe and may be used +/// concurrently from multiple threads. +/// +[DebuggerDisplay("Count = {Count}")] +public sealed class ConcurrentHashSet : IReadOnlyCollection, ICollection { + private const int DefaultCapacity = 31; + private const int MaxLockNumber = 1024; + + private readonly IEqualityComparer _comparer; + private readonly bool _growLockArray; + + private int _budget; + private volatile Tables _tables; + + private static int DefaultConcurrencyLevel => PlatformHelper.ProcessorCount; + /// - /// Represents a thread-safe hash-based unique collection. + /// Gets the number of items contained in the . /// - /// The type of the items in the collection. - /// - /// All public members of are thread-safe and may be used - /// concurrently from multiple threads. - /// - [DebuggerDisplay("Count = {Count}")] - public sealed class ConcurrentHashSet : IReadOnlyCollection, ICollection + /// The number of items contained in the . + /// Count has snapshot semantics and represents the number of items in the + /// at the moment when Count was accessed. + public int Count { - private const int DefaultCapacity = 31; - private const int MaxLockNumber = 1024; - - private readonly IEqualityComparer _comparer; - private readonly bool _growLockArray; - - private int _budget; - private volatile Tables _tables; - - private static int DefaultConcurrencyLevel => PlatformHelper.ProcessorCount; - - /// - /// Gets the number of items contained in the . - /// - /// The number of items contained in the . - /// Count has snapshot semantics and represents the number of items in the - /// at the moment when Count was accessed. - public int Count + get { - get - { - var count = 0; - var acquiredLocks = 0; - try - { - AcquireAllLocks(ref acquiredLocks); - - for (var i = 0; i < _tables.CountPerLock.Length; i++) - { - count += _tables.CountPerLock[i]; - } - } - finally - { - ReleaseLocks(0, acquiredLocks); - } - - return count; - } - } - - /// - /// Gets a value that indicates whether the is empty. - /// - /// true if the is empty; otherwise, - /// false. - public bool IsEmpty - { - get - { - var acquiredLocks = 0; - try - { - AcquireAllLocks(ref acquiredLocks); - - for (var i = 0; i < _tables.CountPerLock.Length; i++) - { - if (_tables.CountPerLock[i] != 0) - { - return false; - } - } - } - finally - { - ReleaseLocks(0, acquiredLocks); - } - - return true; - } - } - - /// - /// Initializes a new instance of the - /// class that is empty, has the default concurrency level, has the default initial capacity, and - /// uses the default comparer for the item type. - /// - public ConcurrentHashSet() - : this(DefaultConcurrencyLevel, DefaultCapacity, true, EqualityComparer.Default) - { - } - - /// - /// Initializes a new instance of the - /// class that is empty, has the specified concurrency level and capacity, and uses the default - /// comparer for the item type. - /// - /// The estimated number of threads that will update the - /// concurrently. - /// The initial number of elements that the - /// can contain. - /// is - /// less than 1. - /// is less than - /// 0. - public ConcurrentHashSet(int concurrencyLevel, int capacity) - : this(concurrencyLevel, capacity, false, EqualityComparer.Default) - { - } - - /// - /// Initializes a new instance of the - /// class that contains elements copied from the specified , has the default concurrency - /// level, has the default initial capacity, and uses the default comparer for the item type. - /// - /// The whose elements are copied to - /// the new - /// . - /// is a null reference. - public ConcurrentHashSet(IEnumerable collection) - : this(collection, EqualityComparer.Default) - { - } - - /// - /// Initializes a new instance of the - /// class that is empty, has the specified concurrency level and capacity, and uses the specified - /// . - /// - /// The - /// implementation to use when comparing items. - /// is a null reference. - public ConcurrentHashSet(IEqualityComparer comparer) - : this(DefaultConcurrencyLevel, DefaultCapacity, true, comparer) - { - } - - /// - /// Initializes a new instance of the - /// class that contains elements copied from the specified , has the default concurrency level, has the default - /// initial capacity, and uses the specified - /// . - /// - /// The whose elements are copied to - /// the new - /// . - /// The - /// implementation to use when comparing items. - /// is a null reference - /// (Nothing in Visual Basic). -or- - /// is a null reference (Nothing in Visual Basic). - /// - public ConcurrentHashSet(IEnumerable collection, IEqualityComparer comparer) - : this(comparer) - { - if (collection is null) throw new ArgumentNullException(nameof(collection)); - - InitializeFromCollection(collection); - } - - - /// - /// Initializes a new instance of the - /// class that contains elements copied from the specified , - /// has the specified concurrency level, has the specified initial capacity, and uses the specified - /// . - /// - /// The estimated number of threads that will update the - /// concurrently. - /// The whose elements are copied to the new - /// . - /// The implementation to use - /// when comparing items. - /// - /// is a null reference. - /// -or- - /// is a null reference. - /// - /// - /// is less than 1. - /// - public ConcurrentHashSet(int concurrencyLevel, IEnumerable collection, IEqualityComparer comparer) - : this(concurrencyLevel, DefaultCapacity, false, comparer) - { - if (collection is null) throw new ArgumentNullException(nameof(collection)); - if (comparer is null) throw new ArgumentNullException(nameof(comparer)); - - InitializeFromCollection(collection); - } - - /// - /// Initializes a new instance of the - /// class that is empty, has the specified concurrency level, has the specified initial capacity, and - /// uses the specified . - /// - /// The estimated number of threads that will update the - /// concurrently. - /// The initial number of elements that the - /// can contain. - /// The - /// implementation to use when comparing items. - /// - /// is less than 1. -or- - /// is less than 0. - /// - /// is a null reference. - public ConcurrentHashSet(int concurrencyLevel, int capacity, IEqualityComparer comparer) - : this(concurrencyLevel, capacity, false, comparer) - { - } - - private ConcurrentHashSet(int concurrencyLevel, int capacity, bool growLockArray, IEqualityComparer comparer) - { - if (concurrencyLevel < 1) throw new ArgumentOutOfRangeException(nameof(concurrencyLevel)); - if (capacity < 0) throw new ArgumentOutOfRangeException(nameof(capacity)); - - // The capacity should be at least as large as the concurrency level. Otherwise, we would have locks that don't guard - // any buckets. - if (capacity < concurrencyLevel) - { - capacity = concurrencyLevel; - } - - var locks = new object[concurrencyLevel]; - for (var i = 0; i < locks.Length; i++) - { - locks[i] = new object(); - } - - var countPerLock = new int[locks.Length]; - var buckets = new Node[capacity]; - _tables = new Tables(buckets, locks, countPerLock); - - _growLockArray = growLockArray; - _budget = buckets.Length / locks.Length; - _comparer = comparer ?? throw new ArgumentNullException(nameof(comparer)); - } - - /// - /// Adds the specified item to the . - /// - /// The item to add. - /// true if the items was added to the - /// successfully; false if it already exists. - /// The - /// contains too many items. - public bool Add(T item) => - AddInternal(item, _comparer.GetHashCode(item), true); - - /// - /// Removes all items from the . - /// - public void Clear() - { - var locksAcquired = 0; + var count = 0; + var acquiredLocks = 0; try { - AcquireAllLocks(ref locksAcquired); + AcquireAllLocks(ref acquiredLocks); - var newTables = new Tables(new Node[DefaultCapacity], _tables.Locks, new int[_tables.CountPerLock.Length]); - _tables = newTables; - _budget = Math.Max(1, newTables.Buckets.Length / newTables.Locks.Length); + for (var i = 0; i < _tables.CountPerLock.Length; i++) + { + count += _tables.CountPerLock[i]; + } } finally { - ReleaseLocks(0, locksAcquired); + ReleaseLocks(0, acquiredLocks); } + + return count; + } + } + + /// + /// Gets a value that indicates whether the is empty. + /// + /// true if the is empty; otherwise, + /// false. + public bool IsEmpty + { + get + { + var acquiredLocks = 0; + try + { + AcquireAllLocks(ref acquiredLocks); + + for (var i = 0; i < _tables.CountPerLock.Length; i++) + { + if (_tables.CountPerLock[i] != 0) + { + return false; + } + } + } + finally + { + ReleaseLocks(0, acquiredLocks); + } + + return true; + } + } + + /// + /// Initializes a new instance of the + /// class that is empty, has the default concurrency level, has the default initial capacity, and + /// uses the default comparer for the item type. + /// + public ConcurrentHashSet() + : this(DefaultConcurrencyLevel, DefaultCapacity, true, EqualityComparer.Default) + { + } + + /// + /// Initializes a new instance of the + /// class that is empty, has the specified concurrency level and capacity, and uses the default + /// comparer for the item type. + /// + /// The estimated number of threads that will update the + /// concurrently. + /// The initial number of elements that the + /// can contain. + /// is + /// less than 1. + /// is less than + /// 0. + public ConcurrentHashSet(int concurrencyLevel, int capacity) + : this(concurrencyLevel, capacity, false, EqualityComparer.Default) + { + } + + /// + /// Initializes a new instance of the + /// class that contains elements copied from the specified , has the default concurrency + /// level, has the default initial capacity, and uses the default comparer for the item type. + /// + /// The whose elements are copied to + /// the new + /// . + /// is a null reference. + public ConcurrentHashSet(IEnumerable collection) + : this(collection, EqualityComparer.Default) + { + } + + /// + /// Initializes a new instance of the + /// class that is empty, has the specified concurrency level and capacity, and uses the specified + /// . + /// + /// The + /// implementation to use when comparing items. + /// is a null reference. + public ConcurrentHashSet(IEqualityComparer comparer) + : this(DefaultConcurrencyLevel, DefaultCapacity, true, comparer) + { + } + + /// + /// Initializes a new instance of the + /// class that contains elements copied from the specified , has the default concurrency level, has the default + /// initial capacity, and uses the specified + /// . + /// + /// The whose elements are copied to + /// the new + /// . + /// The + /// implementation to use when comparing items. + /// is a null reference + /// (Nothing in Visual Basic). -or- + /// is a null reference (Nothing in Visual Basic). + /// + public ConcurrentHashSet(IEnumerable collection, IEqualityComparer comparer) + : this(comparer) + { + if (collection is null) throw new ArgumentNullException(nameof(collection)); + + InitializeFromCollection(collection); + } + + + /// + /// Initializes a new instance of the + /// class that contains elements copied from the specified , + /// has the specified concurrency level, has the specified initial capacity, and uses the specified + /// . + /// + /// The estimated number of threads that will update the + /// concurrently. + /// The whose elements are copied to the new + /// . + /// The implementation to use + /// when comparing items. + /// + /// is a null reference. + /// -or- + /// is a null reference. + /// + /// + /// is less than 1. + /// + public ConcurrentHashSet(int concurrencyLevel, IEnumerable collection, IEqualityComparer comparer) + : this(concurrencyLevel, DefaultCapacity, false, comparer) + { + if (collection is null) throw new ArgumentNullException(nameof(collection)); + if (comparer is null) throw new ArgumentNullException(nameof(comparer)); + + InitializeFromCollection(collection); + } + + /// + /// Initializes a new instance of the + /// class that is empty, has the specified concurrency level, has the specified initial capacity, and + /// uses the specified . + /// + /// The estimated number of threads that will update the + /// concurrently. + /// The initial number of elements that the + /// can contain. + /// The + /// implementation to use when comparing items. + /// + /// is less than 1. -or- + /// is less than 0. + /// + /// is a null reference. + public ConcurrentHashSet(int concurrencyLevel, int capacity, IEqualityComparer comparer) + : this(concurrencyLevel, capacity, false, comparer) + { + } + + private ConcurrentHashSet(int concurrencyLevel, int capacity, bool growLockArray, IEqualityComparer comparer) + { + if (concurrencyLevel < 1) throw new ArgumentOutOfRangeException(nameof(concurrencyLevel)); + if (capacity < 0) throw new ArgumentOutOfRangeException(nameof(capacity)); + + // The capacity should be at least as large as the concurrency level. Otherwise, we would have locks that don't guard + // any buckets. + if (capacity < concurrencyLevel) + { + capacity = concurrencyLevel; } - /// - /// Determines whether the contains the specified - /// item. - /// - /// The item to locate in the . - /// true if the contains the item; otherwise, false. - public bool Contains(T item) + var locks = new object[concurrencyLevel]; + for (var i = 0; i < locks.Length; i++) { - var hashcode = _comparer.GetHashCode(item); + locks[i] = new object(); + } - // We must capture the _buckets field in a local variable. It is set to a new table on each table resize. + var countPerLock = new int[locks.Length]; + var buckets = new Node[capacity]; + _tables = new Tables(buckets, locks, countPerLock); + + _growLockArray = growLockArray; + _budget = buckets.Length / locks.Length; + _comparer = comparer ?? throw new ArgumentNullException(nameof(comparer)); + } + + /// + /// Adds the specified item to the . + /// + /// The item to add. + /// true if the items was added to the + /// successfully; false if it already exists. + /// The + /// contains too many items. + public bool Add(T item) => + AddInternal(item, _comparer.GetHashCode(item), true); + + /// + /// Removes all items from the . + /// + public void Clear() + { + var locksAcquired = 0; + try + { + AcquireAllLocks(ref locksAcquired); + + var newTables = new Tables(new Node[DefaultCapacity], _tables.Locks, new int[_tables.CountPerLock.Length]); + _tables = newTables; + _budget = Math.Max(1, newTables.Buckets.Length / newTables.Locks.Length); + } + finally + { + ReleaseLocks(0, locksAcquired); + } + } + + /// + /// Determines whether the contains the specified + /// item. + /// + /// The item to locate in the . + /// true if the contains the item; otherwise, false. + public bool Contains(T item) + { + var hashcode = _comparer.GetHashCode(item); + + // We must capture the _buckets field in a local variable. It is set to a new table on each table resize. + var tables = _tables; + + var bucketNo = GetBucket(hashcode, tables.Buckets.Length); + + // We can get away w/out a lock here. + // The Volatile.Read ensures that the load of the fields of 'n' doesn't move before the load from buckets[i]. + var current = Volatile.Read(ref tables.Buckets[bucketNo]); + + while (current != null) + { + if (hashcode == current.Hashcode && _comparer.Equals(current.Item, item)) + { + return true; + } + current = current.Next; + } + + return false; + } + + /// + /// Attempts to remove the item from the . + /// + /// The item to remove. + /// true if an item was removed successfully; otherwise, false. + public bool TryRemove(T item) + { + var hashcode = _comparer.GetHashCode(item); + while (true) + { var tables = _tables; - var bucketNo = GetBucket(hashcode, tables.Buckets.Length); + GetBucketAndLockNo(hashcode, out var bucketNo, out var lockNo, tables.Buckets.Length, tables.Locks.Length); - // We can get away w/out a lock here. - // The Volatile.Read ensures that the load of the fields of 'n' doesn't move before the load from buckets[i]. - var current = Volatile.Read(ref tables.Buckets[bucketNo]); - - while (current != null) + lock (tables.Locks[lockNo]) { - if (hashcode == current.Hashcode && _comparer.Equals(current.Item, item)) + // If the table just got resized, we may not be holding the right lock, and must retry. + // This should be a rare occurrence. + if (tables != _tables) { - return true; + continue; + } + + Node previous = null; + for (var current = tables.Buckets[bucketNo]; current != null; current = current.Next) + { + Debug.Assert((previous is null && current == tables.Buckets[bucketNo]) || previous.Next == current); + + if (hashcode == current.Hashcode && _comparer.Equals(current.Item, item)) + { + if (previous is null) + { + Volatile.Write(ref tables.Buckets[bucketNo], current.Next); + } + else + { + previous.Next = current.Next; + } + + tables.CountPerLock[lockNo]--; + return true; + } + previous = current; } - current = current.Next; } return false; } + } - /// - /// Attempts to remove the item from the . - /// - /// The item to remove. - /// true if an item was removed successfully; otherwise, false. - public bool TryRemove(T item) + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + + /// Returns an enumerator that iterates through the . + /// An enumerator for the . + /// + /// The enumerator returned from the collection is safe to use concurrently with + /// reads and writes to the collection, however it does not represent a moment-in-time snapshot + /// of the collection. The contents exposed through the enumerator may contain modifications + /// made to the collection after was called. + /// + public IEnumerator GetEnumerator() + { + var buckets = _tables.Buckets; + + for (var i = 0; i < buckets.Length; i++) { - var hashcode = _comparer.GetHashCode(item); - while (true) + // The Volatile.Read ensures that the load of the fields of 'current' doesn't move before the load from buckets[i]. + var current = Volatile.Read(ref buckets[i]); + + while (current != null) { - var tables = _tables; - - GetBucketAndLockNo(hashcode, out var bucketNo, out var lockNo, tables.Buckets.Length, tables.Locks.Length); - - lock (tables.Locks[lockNo]) - { - // If the table just got resized, we may not be holding the right lock, and must retry. - // This should be a rare occurrence. - if (tables != _tables) - { - continue; - } - - Node previous = null; - for (var current = tables.Buckets[bucketNo]; current != null; current = current.Next) - { - Debug.Assert((previous is null && current == tables.Buckets[bucketNo]) || previous.Next == current); - - if (hashcode == current.Hashcode && _comparer.Equals(current.Item, item)) - { - if (previous is null) - { - Volatile.Write(ref tables.Buckets[bucketNo], current.Next); - } - else - { - previous.Next = current.Next; - } - - tables.CountPerLock[lockNo]--; - return true; - } - previous = current; - } - } - - return false; - } - } - - IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); - - /// Returns an enumerator that iterates through the . - /// An enumerator for the . - /// - /// The enumerator returned from the collection is safe to use concurrently with - /// reads and writes to the collection, however it does not represent a moment-in-time snapshot - /// of the collection. The contents exposed through the enumerator may contain modifications - /// made to the collection after was called. - /// - public IEnumerator GetEnumerator() - { - var buckets = _tables.Buckets; - - for (var i = 0; i < buckets.Length; i++) - { - // The Volatile.Read ensures that the load of the fields of 'current' doesn't move before the load from buckets[i]. - var current = Volatile.Read(ref buckets[i]); - - while (current != null) - { - yield return current.Item; - current = current.Next; - } - } - } - - void ICollection.Add(T item) => Add(item); - - bool ICollection.IsReadOnly => false; - - void ICollection.CopyTo(T[] array, int arrayIndex) - { - if (array is null) throw new ArgumentNullException(nameof(array)); - if (arrayIndex < 0) throw new ArgumentOutOfRangeException(nameof(arrayIndex)); - - var locksAcquired = 0; - try - { - AcquireAllLocks(ref locksAcquired); - - var count = 0; - - for (var i = 0; i < _tables.Locks.Length && count >= 0; i++) - { - count += _tables.CountPerLock[i]; - } - - if (array.Length - count < arrayIndex || count < 0) //"count" itself or "count + arrayIndex" can overflow - { - throw new ArgumentException("The index is equal to or greater than the length of the array, or the number of elements in the set is greater than the available space from index to the end of the destination array."); - } - - CopyToItems(array, arrayIndex); - } - finally - { - ReleaseLocks(0, locksAcquired); - } - } - - bool ICollection.Remove(T item) => TryRemove(item); - - private void InitializeFromCollection(IEnumerable collection) - { - foreach (var item in collection) - { - AddInternal(item, _comparer.GetHashCode(item), false); - } - - if (_budget == 0) - { - _budget = _tables.Buckets.Length / _tables.Locks.Length; - } - } - - private bool AddInternal(T item, int hashcode, bool acquireLock) - { - while (true) - { - var tables = _tables; - GetBucketAndLockNo(hashcode, out var bucketNo, out var lockNo, tables.Buckets.Length, tables.Locks.Length); - - var resizeDesired = false; - var lockTaken = false; - try - { - if (acquireLock) - Monitor.Enter(tables.Locks[lockNo], ref lockTaken); - - // If the table just got resized, we may not be holding the right lock, and must retry. - // This should be a rare occurrence. - if (tables != _tables) - { - continue; - } - - // Try to find this item in the bucket - Node previous = null; - for (var current = tables.Buckets[bucketNo]; current != null; current = current.Next) - { - Debug.Assert((previous is null && current == tables.Buckets[bucketNo]) || previous.Next == current); - if (hashcode == current.Hashcode && _comparer.Equals(current.Item, item)) - { - return false; - } - previous = current; - } - - // The item was not found in the bucket. Insert the new item. - Volatile.Write(ref tables.Buckets[bucketNo], new Node(item, hashcode, tables.Buckets[bucketNo])); - checked - { - tables.CountPerLock[lockNo]++; - } - - // - // If the number of elements guarded by this lock has exceeded the budget, resize the bucket table. - // It is also possible that GrowTable will increase the budget but won't resize the bucket table. - // That happens if the bucket table is found to be poorly utilized due to a bad hash function. - // - if (tables.CountPerLock[lockNo] > _budget) - { - resizeDesired = true; - } - } - finally - { - if (lockTaken) - Monitor.Exit(tables.Locks[lockNo]); - } - - // - // The fact that we got here means that we just performed an insertion. If necessary, we will grow the table. - // - // Concurrency notes: - // - Notice that we are not holding any locks at when calling GrowTable. This is necessary to prevent deadlocks. - // - As a result, it is possible that GrowTable will be called unnecessarily. But, GrowTable will obtain lock 0 - // and then verify that the table we passed to it as the argument is still the current table. - // - if (resizeDesired) - { - GrowTable(tables); - } - - return true; - } - } - - private static int GetBucket(int hashcode, int bucketCount) - { - var bucketNo = (hashcode & 0x7fffffff) % bucketCount; - Debug.Assert(bucketNo >= 0 && bucketNo < bucketCount); - return bucketNo; - } - - private static void GetBucketAndLockNo(int hashcode, out int bucketNo, out int lockNo, int bucketCount, int lockCount) - { - bucketNo = (hashcode & 0x7fffffff) % bucketCount; - lockNo = bucketNo % lockCount; - - Debug.Assert(bucketNo >= 0 && bucketNo < bucketCount); - Debug.Assert(lockNo >= 0 && lockNo < lockCount); - } - - private void GrowTable(Tables tables) - { - const int maxArrayLength = 0X7FEFFFFF; - var locksAcquired = 0; - try - { - // The thread that first obtains _locks[0] will be the one doing the resize operation - AcquireLocks(0, 1, ref locksAcquired); - - // Make sure nobody resized the table while we were waiting for lock 0: - if (tables != _tables) - { - // We assume that since the table reference is different, it was already resized (or the budget - // was adjusted). If we ever decide to do table shrinking, or replace the table for other reasons, - // we will have to revisit this logic. - return; - } - - // Compute the (approx.) total size. Use an Int64 accumulation variable to avoid an overflow. - long approxCount = 0; - for (var i = 0; i < tables.CountPerLock.Length; i++) - { - approxCount += tables.CountPerLock[i]; - } - - // - // If the bucket array is too empty, double the budget instead of resizing the table - // - if (approxCount < tables.Buckets.Length / 4) - { - _budget = 2 * _budget; - if (_budget < 0) - { - _budget = int.MaxValue; - } - return; - } - - // Compute the new table size. We find the smallest integer larger than twice the previous table size, and not divisible by - // 2,3,5 or 7. We can consider a different table-sizing policy in the future. - var newLength = 0; - var maximizeTableSize = false; - try - { - checked - { - // Double the size of the buckets table and add one, so that we have an odd integer. - newLength = tables.Buckets.Length * 2 + 1; - - // Now, we only need to check odd integers, and find the first that is not divisible - // by 3, 5 or 7. - while (newLength % 3 == 0 || newLength % 5 == 0 || newLength % 7 == 0) - { - newLength += 2; - } - - Debug.Assert(newLength % 2 != 0); - - if (newLength > maxArrayLength) - { - maximizeTableSize = true; - } - } - } - catch (OverflowException) - { - maximizeTableSize = true; - } - - if (maximizeTableSize) - { - newLength = maxArrayLength; - - // We want to make sure that GrowTable will not be called again, since table is at the maximum size. - // To achieve that, we set the budget to int.MaxValue. - // - // (There is one special case that would allow GrowTable() to be called in the future: - // calling Clear() on the ConcurrentHashSet will shrink the table and lower the budget.) - _budget = int.MaxValue; - } - - // Now acquire all other locks for the table - AcquireLocks(1, tables.Locks.Length, ref locksAcquired); - - var newLocks = tables.Locks; - - // Add more locks - if (_growLockArray && tables.Locks.Length < MaxLockNumber) - { - newLocks = new object[tables.Locks.Length * 2]; - Array.Copy(tables.Locks, 0, newLocks, 0, tables.Locks.Length); - for (var i = tables.Locks.Length; i < newLocks.Length; i++) - { - newLocks[i] = new object(); - } - } - - var newBuckets = new Node[newLength]; - var newCountPerLock = new int[newLocks.Length]; - - // Copy all data into a new table, creating new nodes for all elements - for (var i = 0; i < tables.Buckets.Length; i++) - { - var current = tables.Buckets[i]; - while (current != null) - { - var next = current.Next; - GetBucketAndLockNo(current.Hashcode, out var newBucketNo, out var newLockNo, newBuckets.Length, newLocks.Length); - - newBuckets[newBucketNo] = new Node(current.Item, current.Hashcode, newBuckets[newBucketNo]); - - checked - { - newCountPerLock[newLockNo]++; - } - - current = next; - } - } - - // Adjust the budget - _budget = Math.Max(1, newBuckets.Length / newLocks.Length); - - // Replace tables with the new versions - _tables = new Tables(newBuckets, newLocks, newCountPerLock); - } - finally - { - // Release all locks that we took earlier - ReleaseLocks(0, locksAcquired); - } - } - - public int RemoveWhere(Func predicate) - { - var elems = this.Where(predicate); - var removed = 0; - foreach (var elem in elems) - { - if (this.TryRemove(elem)) - removed++; - } - return removed; - } - - private void AcquireAllLocks(ref int locksAcquired) - { - // First, acquire lock 0 - AcquireLocks(0, 1, ref locksAcquired); - - // Now that we have lock 0, the _locks array will not change (i.e., grow), - // and so we can safely read _locks.Length. - AcquireLocks(1, _tables.Locks.Length, ref locksAcquired); - Debug.Assert(locksAcquired == _tables.Locks.Length); - } - - private void AcquireLocks(int fromInclusive, int toExclusive, ref int locksAcquired) - { - Debug.Assert(fromInclusive <= toExclusive); - var locks = _tables.Locks; - - for (var i = fromInclusive; i < toExclusive; i++) - { - var lockTaken = false; - try - { - Monitor.Enter(locks[i], ref lockTaken); - } - finally - { - if (lockTaken) - { - locksAcquired++; - } - } - } - } - - private void ReleaseLocks(int fromInclusive, int toExclusive) - { - Debug.Assert(fromInclusive <= toExclusive); - - for (var i = fromInclusive; i < toExclusive; i++) - { - Monitor.Exit(_tables.Locks[i]); - } - } - - private void CopyToItems(T[] array, int index) - { - var buckets = _tables.Buckets; - for (var i = 0; i < buckets.Length; i++) - { - for (var current = buckets[i]; current != null; current = current.Next) - { - array[index] = current.Item; - index++; //this should never flow, CopyToItems is only called when there's no overflow risk - } - } - } - - private sealed class Tables - { - public readonly Node[] Buckets; - public readonly object[] Locks; - - public volatile int[] CountPerLock; - - public Tables(Node[] buckets, object[] locks, int[] countPerLock) - { - Buckets = buckets; - Locks = locks; - CountPerLock = countPerLock; - } - } - - private sealed class Node - { - public readonly T Item; - public readonly int Hashcode; - - public volatile Node Next; - - public Node(T item, int hashcode, Node next) - { - Item = item; - Hashcode = hashcode; - Next = next; + yield return current.Item; + current = current.Next; } } } + + void ICollection.Add(T item) => Add(item); + + bool ICollection.IsReadOnly => false; + + void ICollection.CopyTo(T[] array, int arrayIndex) + { + if (array is null) throw new ArgumentNullException(nameof(array)); + if (arrayIndex < 0) throw new ArgumentOutOfRangeException(nameof(arrayIndex)); + + var locksAcquired = 0; + try + { + AcquireAllLocks(ref locksAcquired); + + var count = 0; + + for (var i = 0; i < _tables.Locks.Length && count >= 0; i++) + { + count += _tables.CountPerLock[i]; + } + + if (array.Length - count < arrayIndex || count < 0) //"count" itself or "count + arrayIndex" can overflow + { + throw new ArgumentException("The index is equal to or greater than the length of the array, or the number of elements in the set is greater than the available space from index to the end of the destination array."); + } + + CopyToItems(array, arrayIndex); + } + finally + { + ReleaseLocks(0, locksAcquired); + } + } + + bool ICollection.Remove(T item) => TryRemove(item); + + private void InitializeFromCollection(IEnumerable collection) + { + foreach (var item in collection) + { + AddInternal(item, _comparer.GetHashCode(item), false); + } + + if (_budget == 0) + { + _budget = _tables.Buckets.Length / _tables.Locks.Length; + } + } + + private bool AddInternal(T item, int hashcode, bool acquireLock) + { + while (true) + { + var tables = _tables; + GetBucketAndLockNo(hashcode, out var bucketNo, out var lockNo, tables.Buckets.Length, tables.Locks.Length); + + var resizeDesired = false; + var lockTaken = false; + try + { + if (acquireLock) + Monitor.Enter(tables.Locks[lockNo], ref lockTaken); + + // If the table just got resized, we may not be holding the right lock, and must retry. + // This should be a rare occurrence. + if (tables != _tables) + { + continue; + } + + // Try to find this item in the bucket + Node previous = null; + for (var current = tables.Buckets[bucketNo]; current != null; current = current.Next) + { + Debug.Assert((previous is null && current == tables.Buckets[bucketNo]) || previous.Next == current); + if (hashcode == current.Hashcode && _comparer.Equals(current.Item, item)) + { + return false; + } + previous = current; + } + + // The item was not found in the bucket. Insert the new item. + Volatile.Write(ref tables.Buckets[bucketNo], new Node(item, hashcode, tables.Buckets[bucketNo])); + checked + { + tables.CountPerLock[lockNo]++; + } + + // + // If the number of elements guarded by this lock has exceeded the budget, resize the bucket table. + // It is also possible that GrowTable will increase the budget but won't resize the bucket table. + // That happens if the bucket table is found to be poorly utilized due to a bad hash function. + // + if (tables.CountPerLock[lockNo] > _budget) + { + resizeDesired = true; + } + } + finally + { + if (lockTaken) + Monitor.Exit(tables.Locks[lockNo]); + } + + // + // The fact that we got here means that we just performed an insertion. If necessary, we will grow the table. + // + // Concurrency notes: + // - Notice that we are not holding any locks at when calling GrowTable. This is necessary to prevent deadlocks. + // - As a result, it is possible that GrowTable will be called unnecessarily. But, GrowTable will obtain lock 0 + // and then verify that the table we passed to it as the argument is still the current table. + // + if (resizeDesired) + { + GrowTable(tables); + } + + return true; + } + } + + private static int GetBucket(int hashcode, int bucketCount) + { + var bucketNo = (hashcode & 0x7fffffff) % bucketCount; + Debug.Assert(bucketNo >= 0 && bucketNo < bucketCount); + return bucketNo; + } + + private static void GetBucketAndLockNo(int hashcode, out int bucketNo, out int lockNo, int bucketCount, int lockCount) + { + bucketNo = (hashcode & 0x7fffffff) % bucketCount; + lockNo = bucketNo % lockCount; + + Debug.Assert(bucketNo >= 0 && bucketNo < bucketCount); + Debug.Assert(lockNo >= 0 && lockNo < lockCount); + } + + private void GrowTable(Tables tables) + { + const int maxArrayLength = 0X7FEFFFFF; + var locksAcquired = 0; + try + { + // The thread that first obtains _locks[0] will be the one doing the resize operation + AcquireLocks(0, 1, ref locksAcquired); + + // Make sure nobody resized the table while we were waiting for lock 0: + if (tables != _tables) + { + // We assume that since the table reference is different, it was already resized (or the budget + // was adjusted). If we ever decide to do table shrinking, or replace the table for other reasons, + // we will have to revisit this logic. + return; + } + + // Compute the (approx.) total size. Use an Int64 accumulation variable to avoid an overflow. + long approxCount = 0; + for (var i = 0; i < tables.CountPerLock.Length; i++) + { + approxCount += tables.CountPerLock[i]; + } + + // + // If the bucket array is too empty, double the budget instead of resizing the table + // + if (approxCount < tables.Buckets.Length / 4) + { + _budget = 2 * _budget; + if (_budget < 0) + { + _budget = int.MaxValue; + } + return; + } + + // Compute the new table size. We find the smallest integer larger than twice the previous table size, and not divisible by + // 2,3,5 or 7. We can consider a different table-sizing policy in the future. + var newLength = 0; + var maximizeTableSize = false; + try + { + checked + { + // Double the size of the buckets table and add one, so that we have an odd integer. + newLength = tables.Buckets.Length * 2 + 1; + + // Now, we only need to check odd integers, and find the first that is not divisible + // by 3, 5 or 7. + while (newLength % 3 == 0 || newLength % 5 == 0 || newLength % 7 == 0) + { + newLength += 2; + } + + Debug.Assert(newLength % 2 != 0); + + if (newLength > maxArrayLength) + { + maximizeTableSize = true; + } + } + } + catch (OverflowException) + { + maximizeTableSize = true; + } + + if (maximizeTableSize) + { + newLength = maxArrayLength; + + // We want to make sure that GrowTable will not be called again, since table is at the maximum size. + // To achieve that, we set the budget to int.MaxValue. + // + // (There is one special case that would allow GrowTable() to be called in the future: + // calling Clear() on the ConcurrentHashSet will shrink the table and lower the budget.) + _budget = int.MaxValue; + } + + // Now acquire all other locks for the table + AcquireLocks(1, tables.Locks.Length, ref locksAcquired); + + var newLocks = tables.Locks; + + // Add more locks + if (_growLockArray && tables.Locks.Length < MaxLockNumber) + { + newLocks = new object[tables.Locks.Length * 2]; + Array.Copy(tables.Locks, 0, newLocks, 0, tables.Locks.Length); + for (var i = tables.Locks.Length; i < newLocks.Length; i++) + { + newLocks[i] = new object(); + } + } + + var newBuckets = new Node[newLength]; + var newCountPerLock = new int[newLocks.Length]; + + // Copy all data into a new table, creating new nodes for all elements + for (var i = 0; i < tables.Buckets.Length; i++) + { + var current = tables.Buckets[i]; + while (current != null) + { + var next = current.Next; + GetBucketAndLockNo(current.Hashcode, out var newBucketNo, out var newLockNo, newBuckets.Length, newLocks.Length); + + newBuckets[newBucketNo] = new Node(current.Item, current.Hashcode, newBuckets[newBucketNo]); + + checked + { + newCountPerLock[newLockNo]++; + } + + current = next; + } + } + + // Adjust the budget + _budget = Math.Max(1, newBuckets.Length / newLocks.Length); + + // Replace tables with the new versions + _tables = new Tables(newBuckets, newLocks, newCountPerLock); + } + finally + { + // Release all locks that we took earlier + ReleaseLocks(0, locksAcquired); + } + } + + public int RemoveWhere(Func predicate) + { + var elems = this.Where(predicate); + var removed = 0; + foreach (var elem in elems) + { + if (this.TryRemove(elem)) + removed++; + } + return removed; + } + + private void AcquireAllLocks(ref int locksAcquired) + { + // First, acquire lock 0 + AcquireLocks(0, 1, ref locksAcquired); + + // Now that we have lock 0, the _locks array will not change (i.e., grow), + // and so we can safely read _locks.Length. + AcquireLocks(1, _tables.Locks.Length, ref locksAcquired); + Debug.Assert(locksAcquired == _tables.Locks.Length); + } + + private void AcquireLocks(int fromInclusive, int toExclusive, ref int locksAcquired) + { + Debug.Assert(fromInclusive <= toExclusive); + var locks = _tables.Locks; + + for (var i = fromInclusive; i < toExclusive; i++) + { + var lockTaken = false; + try + { + Monitor.Enter(locks[i], ref lockTaken); + } + finally + { + if (lockTaken) + { + locksAcquired++; + } + } + } + } + + private void ReleaseLocks(int fromInclusive, int toExclusive) + { + Debug.Assert(fromInclusive <= toExclusive); + + for (var i = fromInclusive; i < toExclusive; i++) + { + Monitor.Exit(_tables.Locks[i]); + } + } + + private void CopyToItems(T[] array, int index) + { + var buckets = _tables.Buckets; + for (var i = 0; i < buckets.Length; i++) + { + for (var current = buckets[i]; current != null; current = current.Next) + { + array[index] = current.Item; + index++; //this should never flow, CopyToItems is only called when there's no overflow risk + } + } + } + + private sealed class Tables + { + public readonly Node[] Buckets; + public readonly object[] Locks; + + public volatile int[] CountPerLock; + + public Tables(Node[] buckets, object[] locks, int[] countPerLock) + { + Buckets = buckets; + Locks = locks; + CountPerLock = countPerLock; + } + } + + private sealed class Node + { + public readonly T Item; + public readonly int Hashcode; + + public volatile Node Next; + + public Node(T item, int hashcode, Node next) + { + Item = item; + Hashcode = hashcode; + Next = next; + } + } } \ No newline at end of file diff --git a/src/NadekoBot/Common/Collections/DisposableImutableList.cs b/src/NadekoBot/Common/Collections/DisposableImutableList.cs index 646b35594..2bb737f2c 100644 --- a/src/NadekoBot/Common/Collections/DisposableImutableList.cs +++ b/src/NadekoBot/Common/Collections/DisposableImutableList.cs @@ -1,77 +1,74 @@ -using System; -using System.Collections; -using System.Collections.Generic; +using System.Collections; -namespace NadekoBot.Common.Collections +namespace NadekoBot.Common.Collections; + +public static class DisposableReadOnlyListExtensions { - public static class DisposableReadOnlyListExtensions - { - public static IDisposableReadOnlyList AsDisposable(this IReadOnlyList arr) where T : IDisposable - => new DisposableReadOnlyList(arr); + public static IDisposableReadOnlyList AsDisposable(this IReadOnlyList arr) where T : IDisposable + => new DisposableReadOnlyList(arr); - public static IDisposableReadOnlyList> AsDisposable(this IReadOnlyList> arr) where TValue : IDisposable - => new DisposableReadOnlyList(arr); + public static IDisposableReadOnlyList> AsDisposable(this IReadOnlyList> arr) where TValue : IDisposable + => new DisposableReadOnlyList(arr); +} + +public interface IDisposableReadOnlyList : IReadOnlyList, IDisposable +{ +} + +public sealed class DisposableReadOnlyList : IDisposableReadOnlyList + where T : IDisposable +{ + private readonly IReadOnlyList _arr; + + public int Count => _arr.Count; + + public T this[int index] => _arr[index]; + + public DisposableReadOnlyList(IReadOnlyList arr) + { + this._arr = arr; } - public interface IDisposableReadOnlyList : IReadOnlyList, IDisposable + public IEnumerator GetEnumerator() + => _arr.GetEnumerator(); + + IEnumerator IEnumerable.GetEnumerator() + => _arr.GetEnumerator(); + + public void Dispose() { - } - - public sealed class DisposableReadOnlyList : IDisposableReadOnlyList - where T : IDisposable - { - private readonly IReadOnlyList _arr; - - public int Count => _arr.Count; - - public T this[int index] => _arr[index]; - - public DisposableReadOnlyList(IReadOnlyList arr) + foreach (var item in _arr) { - this._arr = arr; - } - - public IEnumerator GetEnumerator() - => _arr.GetEnumerator(); - - IEnumerator IEnumerable.GetEnumerator() - => _arr.GetEnumerator(); - - public void Dispose() - { - foreach (var item in _arr) - { - item.Dispose(); - } - } - } - - public sealed class DisposableReadOnlyList : IDisposableReadOnlyList> - where U : IDisposable - { - private readonly IReadOnlyList> _arr; - - public int Count => _arr.Count; - - KeyValuePair IReadOnlyList>.this[int index] => _arr[index]; - - public DisposableReadOnlyList(IReadOnlyList> arr) - { - this._arr = arr; - } - - public IEnumerator> GetEnumerator() => - _arr.GetEnumerator(); - - IEnumerator IEnumerable.GetEnumerator() => - _arr.GetEnumerator(); - - public void Dispose() - { - foreach (var item in _arr) - { - item.Value.Dispose(); - } + item.Dispose(); } } } + +public sealed class DisposableReadOnlyList : IDisposableReadOnlyList> + where U : IDisposable +{ + private readonly IReadOnlyList> _arr; + + public int Count => _arr.Count; + + KeyValuePair IReadOnlyList>.this[int index] => _arr[index]; + + public DisposableReadOnlyList(IReadOnlyList> arr) + { + this._arr = arr; + } + + public IEnumerator> GetEnumerator() => + _arr.GetEnumerator(); + + IEnumerator IEnumerable.GetEnumerator() => + _arr.GetEnumerator(); + + public void Dispose() + { + foreach (var item in _arr) + { + item.Value.Dispose(); + } + } +} \ No newline at end of file diff --git a/src/NadekoBot/Common/Collections/IndexedCollection.cs b/src/NadekoBot/Common/Collections/IndexedCollection.cs index f49dbfc06..3e2365af2 100644 --- a/src/NadekoBot/Common/Collections/IndexedCollection.cs +++ b/src/NadekoBot/Common/Collections/IndexedCollection.cs @@ -1,141 +1,138 @@ using System.Collections; -using System.Collections.Generic; -using System.Linq; using NadekoBot.Services.Database.Models; -namespace NadekoBot.Common.Collections +namespace NadekoBot.Common.Collections; + +public class IndexedCollection : IList where T : class, IIndexed { - public class IndexedCollection : IList where T : class, IIndexed + public List Source { get; } + private readonly object _locker = new object(); + + public int Count => Source.Count; + public bool IsReadOnly => false; + public int IndexOf(T item) => item.Index; + + public IndexedCollection() { - public List Source { get; } - private readonly object _locker = new object(); - - public int Count => Source.Count; - public bool IsReadOnly => false; - public int IndexOf(T item) => item.Index; - - public IndexedCollection() - { - Source = new List(); - } + Source = new List(); + } - public IndexedCollection(IEnumerable source) + public IndexedCollection(IEnumerable source) + { + lock (_locker) { - lock (_locker) + Source = source.OrderBy(x => x.Index).ToList(); + UpdateIndexes(); + } + } + + public void UpdateIndexes() + { + lock (_locker) + { + for (var i = 0; i < Source.Count; i++) { - Source = source.OrderBy(x => x.Index).ToList(); - UpdateIndexes(); + if (Source[i].Index != i) + Source[i].Index = i; } } + } - public void UpdateIndexes() + public static implicit operator List(IndexedCollection x) => + x.Source; + + public List ToList() => Source.ToList(); + + public IEnumerator GetEnumerator() => + Source.GetEnumerator(); + + IEnumerator IEnumerable.GetEnumerator() => + Source.GetEnumerator(); + + public void Add(T item) + { + lock (_locker) { - lock (_locker) + item.Index = Source.Count; + Source.Add(item); + } + } + + public virtual void Clear() + { + lock (_locker) + { + Source.Clear(); + } + } + + public bool Contains(T item) + { + lock (_locker) + { + return Source.Contains(item); + } + } + + public void CopyTo(T[] array, int arrayIndex) + { + lock (_locker) + { + Source.CopyTo(array, arrayIndex); + } + } + + public virtual bool Remove(T item) + { + bool removed; + lock (_locker) + { + if (removed = Source.Remove(item)) { - for (var i = 0; i < Source.Count; i++) + for (int i = 0; i < Source.Count; i++) { if (Source[i].Index != i) Source[i].Index = i; } } } + return removed; + } - public static implicit operator List(IndexedCollection x) => - x.Source; - - public List ToList() => Source.ToList(); - - public IEnumerator GetEnumerator() => - Source.GetEnumerator(); - - IEnumerator IEnumerable.GetEnumerator() => - Source.GetEnumerator(); - - public void Add(T item) + public virtual void Insert(int index, T item) + { + lock (_locker) { - lock (_locker) + Source.Insert(index, item); + for (int i = index; i < Source.Count; i++) { - item.Index = Source.Count; - Source.Add(item); - } - } - - public virtual void Clear() - { - lock (_locker) - { - Source.Clear(); - } - } - - public bool Contains(T item) - { - lock (_locker) - { - return Source.Contains(item); - } - } - - public void CopyTo(T[] array, int arrayIndex) - { - lock (_locker) - { - Source.CopyTo(array, arrayIndex); - } - } - - public virtual bool Remove(T item) - { - bool removed; - lock (_locker) - { - if (removed = Source.Remove(item)) - { - for (int i = 0; i < Source.Count; i++) - { - if (Source[i].Index != i) - Source[i].Index = i; - } - } - } - return removed; - } - - public virtual void Insert(int index, T item) - { - lock (_locker) - { - Source.Insert(index, item); - for (int i = index; i < Source.Count; i++) - { - Source[i].Index = i; - } - } - } - - public virtual void RemoveAt(int index) - { - lock (_locker) - { - Source.RemoveAt(index); - for (int i = index; i < Source.Count; i++) - { - Source[i].Index = i; - } - } - } - - public virtual T this[int index] - { - get { return Source[index]; } - set - { - lock (_locker) - { - value.Index = index; - Source[index] = value; - } + Source[i].Index = i; } } } -} + + public virtual void RemoveAt(int index) + { + lock (_locker) + { + Source.RemoveAt(index); + for (int i = index; i < Source.Count; i++) + { + Source[i].Index = i; + } + } + } + + public virtual T this[int index] + { + get { return Source[index]; } + set + { + lock (_locker) + { + value.Index = index; + Source[index] = value; + } + } + } +} \ No newline at end of file diff --git a/src/NadekoBot/Common/CommandData.cs b/src/NadekoBot/Common/CommandData.cs index b53ab2ead..c56e4374d 100644 --- a/src/NadekoBot/Common/CommandData.cs +++ b/src/NadekoBot/Common/CommandData.cs @@ -1,9 +1,8 @@ -namespace NadekoBot.Common +namespace NadekoBot.Common; + +public class CommandData { - public class CommandData - { - public string Cmd { get; set; } - public string Desc { get; set; } - public string[] Usage { get; set; } - } -} + public string Cmd { get; set; } + public string Desc { get; set; } + public string[] Usage { get; set; } +} \ No newline at end of file diff --git a/src/NadekoBot/Common/Configs/BotConfig.cs b/src/NadekoBot/Common/Configs/BotConfig.cs index 4a94bee3d..f89b7e6d8 100644 --- a/src/NadekoBot/Common/Configs/BotConfig.cs +++ b/src/NadekoBot/Common/Configs/BotConfig.cs @@ -1,69 +1,68 @@ -using System.Collections.Generic; -using System.Globalization; +using System.Globalization; using Cloneable; using NadekoBot.Common.Yml; using SixLabors.ImageSharp.PixelFormats; using YamlDotNet.Core; using YamlDotNet.Serialization; -namespace NadekoBot.Common.Configs -{ - [Cloneable] - public sealed partial class BotConfig : ICloneable - { - [Comment(@"DO NOT CHANGE")] - public int Version { get; set; } = 2; +namespace NadekoBot.Common.Configs; - [Comment(@"Most commands, when executed, have a small colored line +[Cloneable] +public sealed partial class BotConfig : ICloneable +{ + [Comment(@"DO NOT CHANGE")] + public int Version { get; set; } = 2; + + [Comment(@"Most commands, when executed, have a small colored line next to the response. The color depends whether the command is completed, errored or in progress (pending) Color settings below are for the color of those lines. To get color's hex, you can go here https://htmlcolorcodes.com/ and copy the hex code fo your selected color (marked as #)")] - public ColorConfig Color { get; set; } + public ColorConfig Color { get; set; } - [Comment("Default bot language. It has to be in the list of supported languages (.langli)")] - public CultureInfo DefaultLocale { get; set; } + [Comment("Default bot language. It has to be in the list of supported languages (.langli)")] + public CultureInfo DefaultLocale { get; set; } - [Comment(@"Style in which executed commands will show up in the console. + [Comment(@"Style in which executed commands will show up in the console. Allowed values: Simple, Normal, None")] - public ConsoleOutputType ConsoleOutputType { get; set; } + public ConsoleOutputType ConsoleOutputType { get; set; } // [Comment(@"For what kind of updates will the bot check. // Allowed values: Release, Commit, None")] // public UpdateCheckType CheckForUpdates { get; set; } - // [Comment(@"How often will the bot check for updates, in hours")] - // public int CheckUpdateInterval { get; set; } + // [Comment(@"How often will the bot check for updates, in hours")] + // public int CheckUpdateInterval { get; set; } - [Comment(@"Do you want any messages sent by users in Bot's DM to be forwarded to the owner(s)?")] - public bool ForwardMessages { get; set; } + [Comment(@"Do you want any messages sent by users in Bot's DM to be forwarded to the owner(s)?")] + public bool ForwardMessages { get; set; } - [Comment(@"Do you want the message to be forwarded only to the first owner specified in the list of owners (in creds.yml), + [Comment(@"Do you want the message to be forwarded only to the first owner specified in the list of owners (in creds.yml), or all owners? (this might cause the bot to lag if there's a lot of owners specified)")] - public bool ForwardToAllOwners { get; set; } + public bool ForwardToAllOwners { get; set; } - [Comment(@"When a user DMs the bot with a message which is not a command + [Comment(@"When a user DMs the bot with a message which is not a command they will receive this message. Leave empty for no response. The string which will be sent whenever someone DMs the bot. Supports embeds. How it looks: https://puu.sh/B0BLV.png")] - [YamlMember(ScalarStyle = ScalarStyle.Literal)] - public string DmHelpText { get; set; } + [YamlMember(ScalarStyle = ScalarStyle.Literal)] + public string DmHelpText { get; set; } - [Comment(@"Only users who send a DM to the bot containing one of the specified words will get a DmHelpText response. + [Comment(@"Only users who send a DM to the bot containing one of the specified words will get a DmHelpText response. Case insensitive. Leave empty to reply with DmHelpText to every DM.")] - public List DmHelpTextKeywords { get; set; } + public List DmHelpTextKeywords { get; set; } - [Comment(@"This is the response for the .h command")] - [YamlMember(ScalarStyle = ScalarStyle.Literal)] - public string HelpText { get; set; } - [Comment(@"List of modules and commands completely blocked on the bot")] - public BlockedConfig Blocked { get; set; } + [Comment(@"This is the response for the .h command")] + [YamlMember(ScalarStyle = ScalarStyle.Literal)] + public string HelpText { get; set; } + [Comment(@"List of modules and commands completely blocked on the bot")] + public BlockedConfig Blocked { get; set; } - [Comment(@"Which string will be used to recognize the commands")] - public string Prefix { get; set; } + [Comment(@"Which string will be used to recognize the commands")] + public string Prefix { get; set; } - [Comment(@"Toggles whether your bot will group greet/bye messages into a single message every 5 seconds. + [Comment(@"Toggles whether your bot will group greet/bye messages into a single message every 5 seconds. 1st user who joins will get greeted immediately If more users join within the next 5 seconds, they will be greeted in groups of 5. This will cause %user.mention% and other placeholders to be replaced with multiple users. @@ -72,12 +71,12 @@ it will become invalid, as it will resolve to a list of avatars of grouped users note: This setting is primarily used if you're afraid of raids, or you're running medium/large bots where some servers might get hundreds of people join at once. This is used to prevent the bot from getting ratelimited, and (slightly) reduce the greet spam in those servers.")] - public bool GroupGreets { get; set; } + public bool GroupGreets { get; set; } - [Comment(@"Whether the bot will rotate through all specified statuses. + [Comment(@"Whether the bot will rotate through all specified statuses. This setting can be changed via .rots command. See RotatingStatuses submodule in Administration.")] - public bool RotateStatuses { get; set; } + public bool RotateStatuses { get; set; } // [Comment(@"Whether the prefix will be a suffix, or prefix. // For example, if your prefix is ! you will run a command called 'cash' by typing either @@ -85,23 +84,23 @@ See RotatingStatuses submodule in Administration.")] // 'cash @Someone!' if your prefixIsSuffix: true")] // public bool PrefixIsSuffix { get; set; } - // public string Prefixed(string text) => PrefixIsSuffix - // ? text + Prefix - // : Prefix + text; + // public string Prefixed(string text) => PrefixIsSuffix + // ? text + Prefix + // : Prefix + text; - public string Prefixed(string text) - => Prefix + text; + public string Prefixed(string text) + => Prefix + text; - public BotConfig() - { - var color = new ColorConfig(); - Color = color; - DefaultLocale = new CultureInfo("en-US"); - ConsoleOutputType = ConsoleOutputType.Normal; - ForwardMessages = false; - ForwardToAllOwners = false; - DmHelpText = @"{""description"": ""Type `%prefix%h` for help.""}"; - HelpText = @"{ + public BotConfig() + { + var color = new ColorConfig(); + Color = color; + DefaultLocale = new CultureInfo("en-US"); + ConsoleOutputType = ConsoleOutputType.Normal; + ForwardMessages = false; + ForwardToAllOwners = false; + DmHelpText = @"{""description"": ""Type `%prefix%h` for help.""}"; + HelpText = @"{ ""title"": ""To invite me to your server, use this link"", ""description"": ""https://discordapp.com/oauth2/authorize?client_id={0}&scope=bot&permissions=66186303"", ""color"": 53380, @@ -126,59 +125,58 @@ See RotatingStatuses submodule in Administration.")] } ] }"; - var blocked = new BlockedConfig(); - Blocked = blocked; - Prefix = "."; - RotateStatuses = false; - GroupGreets = false; - DmHelpTextKeywords = new List() - { - "help", - "commands", - "cmds", - "module", - "can you do" - }; - } - } - - [Cloneable] - public sealed partial class BlockedConfig - { - public HashSet Commands { get; set; } - public HashSet Modules { get; set; } - - public BlockedConfig() + var blocked = new BlockedConfig(); + Blocked = blocked; + Prefix = "."; + RotateStatuses = false; + GroupGreets = false; + DmHelpTextKeywords = new List() { - Modules = new HashSet(); - Commands = new HashSet(); - } + "help", + "commands", + "cmds", + "module", + "can you do" + }; } +} - [Cloneable] - public partial class ColorConfig +[Cloneable] +public sealed partial class BlockedConfig +{ + public HashSet Commands { get; set; } + public HashSet Modules { get; set; } + + public BlockedConfig() { - [Comment(@"Color used for embed responses when command successfully executes")] - public Rgba32 Ok { get; set; } - - [Comment(@"Color used for embed responses when command has an error")] - public Rgba32 Error { get; set; } - - [Comment(@"Color used for embed responses while command is doing work or is in progress")] - public Rgba32 Pending { get; set; } - - public ColorConfig() - { - Ok = Rgba32.ParseHex("00e584"); - Error = Rgba32.ParseHex("ee281f"); - Pending = Rgba32.ParseHex("faa61a"); - } + Modules = new HashSet(); + Commands = new HashSet(); } +} + +[Cloneable] +public partial class ColorConfig +{ + [Comment(@"Color used for embed responses when command successfully executes")] + public Rgba32 Ok { get; set; } + + [Comment(@"Color used for embed responses when command has an error")] + public Rgba32 Error { get; set; } + + [Comment(@"Color used for embed responses while command is doing work or is in progress")] + public Rgba32 Pending { get; set; } + + public ColorConfig() + { + Ok = Rgba32.ParseHex("00e584"); + Error = Rgba32.ParseHex("ee281f"); + Pending = Rgba32.ParseHex("faa61a"); + } +} - public enum ConsoleOutputType - { - Normal = 0, - Simple = 1, - None = 2, - } +public enum ConsoleOutputType +{ + Normal = 0, + Simple = 1, + None = 2, } \ No newline at end of file diff --git a/src/NadekoBot/Common/Configs/IConfigSeria.cs b/src/NadekoBot/Common/Configs/IConfigSeria.cs index 3977e4c94..98254e635 100644 --- a/src/NadekoBot/Common/Configs/IConfigSeria.cs +++ b/src/NadekoBot/Common/Configs/IConfigSeria.cs @@ -1,18 +1,17 @@ -namespace NadekoBot.Common.Configs +namespace NadekoBot.Common.Configs; + +/// +/// Base interface for available config serializers +/// +public interface IConfigSeria { /// - /// Base interface for available config serializers + /// Serialize the object to string /// - public interface IConfigSeria - { - /// - /// Serialize the object to string - /// - public string Serialize(T obj); + public string Serialize(T obj); - /// - /// Deserialize string data into an object of the specified type - /// - public T Deserialize(string data); - } + /// + /// Deserialize string data into an object of the specified type + /// + public T Deserialize(string data); } \ No newline at end of file diff --git a/src/NadekoBot/Common/Creds.cs b/src/NadekoBot/Common/Creds.cs index 2ce2e06cf..988ab31b9 100644 --- a/src/NadekoBot/Common/Creds.cs +++ b/src/NadekoBot/Common/Creds.cs @@ -1,97 +1,95 @@ -using System.Collections.Generic; -using NadekoBot.Common.Yml; -using YamlDotNet.Serialization; +using NadekoBot.Common.Yml; -namespace NadekoBot.Common +namespace NadekoBot.Common; + +public sealed class Creds : IBotCredentials { - public sealed class Creds : IBotCredentials + public Creds() { - public Creds() + Version = 1; + Token = string.Empty; + OwnerIds = new List(); + TotalShards = 1; + GoogleApiKey = string.Empty; + Votes = new(string.Empty, string.Empty, string.Empty, string.Empty); + Patreon = new(string.Empty, string.Empty, string.Empty, string.Empty); + BotListToken = string.Empty; + CleverbotApiKey = string.Empty; + RedisOptions = "localhost:6379,syncTimeout=30000,responseTimeout=30000,allowAdmin=true,password="; + Db = new() { - Version = 1; - Token = string.Empty; - OwnerIds = new List(); - TotalShards = 1; - GoogleApiKey = string.Empty; - Votes = new(string.Empty, string.Empty, string.Empty, string.Empty); - Patreon = new(string.Empty, string.Empty, string.Empty, string.Empty); - BotListToken = string.Empty; - CleverbotApiKey = string.Empty; - RedisOptions = "localhost:6379,syncTimeout=30000,responseTimeout=30000,allowAdmin=true,password="; - Db = new() - { - Type = "sqlite", - ConnectionString = "Data Source=data/NadekoBot.db" - }; + Type = "sqlite", + ConnectionString = "Data Source=data/NadekoBot.db" + }; - CoordinatorUrl = "http://localhost:3442"; + CoordinatorUrl = "http://localhost:3442"; - RestartCommand = new() - { - }; - } + RestartCommand = new() + { + }; + } - [Comment(@"DO NOT CHANGE")] - public int Version { get; set; } + [Comment(@"DO NOT CHANGE")] + public int Version { get; set; } - [Comment(@"Bot token. Do not share with anyone ever -> https://discordapp.com/developers/applications/")] - public string Token { get; set; } + [Comment(@"Bot token. Do not share with anyone ever -> https://discordapp.com/developers/applications/")] + public string Token { get; set; } - [Comment(@"List of Ids of the users who have bot owner permissions + [Comment(@"List of Ids of the users who have bot owner permissions **DO NOT ADD PEOPLE YOU DON'T TRUST**")] - public ICollection OwnerIds { get; set; } + public ICollection OwnerIds { get; set; } - [Comment(@"The number of shards that the bot will running on. + [Comment(@"The number of shards that the bot will running on. Leave at 1 if you don't know what you're doing.")] - public int TotalShards { get; set; } + public int TotalShards { get; set; } - [Comment(@"Login to https://console.cloud.google.com, create a new project, go to APIs & Services -> Library -> YouTube Data API and enable it. + [Comment(@"Login to https://console.cloud.google.com, create a new project, go to APIs & Services -> Library -> YouTube Data API and enable it. Then, go to APIs and Services -> Credentials and click Create credentials -> API key. Used only for Youtube Data Api (at the moment).")] - public string GoogleApiKey { get; set; } + public string GoogleApiKey { get; set; } - [Comment(@"Settings for voting system for discordbots. Meant for use on global Nadeko.")] - public VotesSettings Votes { get; set; } + [Comment(@"Settings for voting system for discordbots. Meant for use on global Nadeko.")] + public VotesSettings Votes { get; set; } - [Comment(@"Patreon auto reward system settings. + [Comment(@"Patreon auto reward system settings. go to https://www.patreon.com/portal -> my clients -> create client")] - public PatreonSettings Patreon { get; set; } + public PatreonSettings Patreon { get; set; } - [Comment(@"Api key for sending stats to DiscordBotList.")] - public string BotListToken { get; set; } + [Comment(@"Api key for sending stats to DiscordBotList.")] + public string BotListToken { get; set; } - [Comment(@"Official cleverbot api key.")] - public string CleverbotApiKey { get; set; } + [Comment(@"Official cleverbot api key.")] + public string CleverbotApiKey { get; set; } - [Comment(@"Redis connection string. Don't change if you don't know what you're doing.")] - public string RedisOptions { get; set; } + [Comment(@"Redis connection string. Don't change if you don't know what you're doing.")] + public string RedisOptions { get; set; } - [Comment(@"Database options. Don't change if you don't know what you're doing. Leave null for default values")] - public DbOptions Db { get; set; } + [Comment(@"Database options. Don't change if you don't know what you're doing. Leave null for default values")] + public DbOptions Db { get; set; } - [Comment(@"Address and port of the coordinator endpoint. Leave empty for default. + [Comment(@"Address and port of the coordinator endpoint. Leave empty for default. Change only if you've changed the coordinator address or port.")] - public string CoordinatorUrl { get; set; } + public string CoordinatorUrl { get; set; } - [Comment(@"Api key obtained on https://rapidapi.com (go to MyApps -> Add New App -> Enter Name -> Application key)")] - public string RapidApiKey { get; set; } + [Comment(@"Api key obtained on https://rapidapi.com (go to MyApps -> Add New App -> Enter Name -> Application key)")] + public string RapidApiKey { get; set; } - [Comment(@"https://locationiq.com api key (register and you will receive the token in the email). + [Comment(@"https://locationiq.com api key (register and you will receive the token in the email). Used only for .time command.")] - public string LocationIqApiKey { get; set; } + public string LocationIqApiKey { get; set; } - [Comment(@"https://timezonedb.com api key (register and you will receive the token in the email). + [Comment(@"https://timezonedb.com api key (register and you will receive the token in the email). Used only for .time command")] - public string TimezoneDbApiKey { get; set; } + public string TimezoneDbApiKey { get; set; } - [Comment(@"https://pro.coinmarketcap.com/account/ api key. There is a free plan for personal use. + [Comment(@"https://pro.coinmarketcap.com/account/ api key. There is a free plan for personal use. Used for cryptocurrency related commands.")] - public string CoinmarketcapApiKey { get; set; } + public string CoinmarketcapApiKey { get; set; } - [Comment(@"Api key used for Osu related commands. Obtain this key at https://osu.ppy.sh/p/api")] - public string OsuApiKey { get; set; } + [Comment(@"Api key used for Osu related commands. Obtain this key at https://osu.ppy.sh/p/api")] + public string OsuApiKey { get; set; } - [Comment(@"Command and args which will be used to restart the bot. + [Comment(@"Command and args which will be used to restart the bot. Only used if bot is executed directly (NOT through the coordinator) placeholders: {0} -> shard id @@ -102,118 +100,117 @@ Linux default Windows default cmd: NadekoBot.exe args: {0}")] - public RestartConfig RestartCommand { get; set; } + public RestartConfig RestartCommand { get; set; } - public class DbOptions + public class DbOptions + { + [Comment(@"Database type. Only sqlite supported atm")] + public string Type { get; set; } + [Comment(@"Connection string. Will default to ""Data Source=data/NadekoBot.db""")] + public string ConnectionString { get; set; } + } + + // todo fixup patreon + public sealed record PatreonSettings + { + public string ClientId { get; set; } + public string AccessToken { get; set; } + public string RefreshToken { get; set; } + public string ClientSecret { get; set; } + + [Comment(@"Campaign ID of your patreon page. Go to your patreon page (make sure you're logged in) and type ""prompt('Campaign ID', window.patreon.bootstrap.creator.data.id);"" in the console. (ctrl + shift + i)")] + public string CampaignId { get; set; } + + public PatreonSettings(string accessToken, string refreshToken, string clientSecret, string campaignId) { - [Comment(@"Database type. Only sqlite supported atm")] - public string Type { get; set; } - [Comment(@"Connection string. Will default to ""Data Source=data/NadekoBot.db""")] - public string ConnectionString { get; set; } - } - - // todo fixup patreon - public sealed record PatreonSettings - { - public string ClientId { get; set; } - public string AccessToken { get; set; } - public string RefreshToken { get; set; } - public string ClientSecret { get; set; } - - [Comment(@"Campaign ID of your patreon page. Go to your patreon page (make sure you're logged in) and type ""prompt('Campaign ID', window.patreon.bootstrap.creator.data.id);"" in the console. (ctrl + shift + i)")] - public string CampaignId { get; set; } - - public PatreonSettings(string accessToken, string refreshToken, string clientSecret, string campaignId) - { - AccessToken = accessToken; - RefreshToken = refreshToken; - ClientSecret = clientSecret; - CampaignId = campaignId; - } - - public PatreonSettings() - { - - } + AccessToken = accessToken; + RefreshToken = refreshToken; + ClientSecret = clientSecret; + CampaignId = campaignId; } - public sealed record VotesSettings + public PatreonSettings() { - [Comment(@"top.gg votes service url -This is the url of your instance of the NadekoBot.Votes api -Example: https://votes.my.cool.bot.com")] - public string TopggServiceUrl { get; set; } - - [Comment(@"Authorization header value sent to the TopGG service url with each request -This should be equivalent to the TopggKey in your NadekoBot.Votes api appsettings.json file")] - public string TopggKey { get; set; } - - [Comment(@"discords.com votes service url -This is the url of your instance of the NadekoBot.Votes api -Example: https://votes.my.cool.bot.com")] - public string DiscordsServiceUrl { get; set; } - - [Comment(@"Authorization header value sent to the Discords service url with each request -This should be equivalent to the DiscordsKey in your NadekoBot.Votes api appsettings.json file")] - public string DiscordsKey { get; set; } - - public VotesSettings() - { - } - - public VotesSettings(string topggServiceUrl, string topggKey, string discordsServiceUrl, string discordsKey) - { - TopggServiceUrl = topggServiceUrl; - TopggKey = topggKey; - DiscordsServiceUrl = discordsServiceUrl; - DiscordsKey = discordsKey; - } - } - - public class Old - { - public string Token { get; set; } = string.Empty; - public ulong[] OwnerIds { get; set; } = new ulong[1]; - public string LoLApiKey { get; set; } = string.Empty; - public string GoogleApiKey { get; set; } = string.Empty; - public string MashapeKey { get; set; } = string.Empty; - public string OsuApiKey { get; set; } = string.Empty; - public string SoundCloudClientId { get; set; } = string.Empty; - public string CleverbotApiKey { get; set; } = string.Empty; - public string CarbonKey { get; set; } = string.Empty; - public int TotalShards { get; set; } = 1; - public string PatreonAccessToken { get; set; } = string.Empty; - public string PatreonCampaignId { get; set; } = "334038"; - public RestartConfig RestartCommand { get; set; } = null; - - public string ShardRunCommand { get; set; } = string.Empty; - public string ShardRunArguments { get; set; } = string.Empty; - public int? ShardRunPort { get; set; } = null; - public string MiningProxyUrl { get; set; } = string.Empty; - public string MiningProxyCreds { get; set; } = string.Empty; - - public string BotListToken { get; set; } = string.Empty; - public string TwitchClientId { get; set; } = string.Empty; - public string VotesToken { get; set; } = string.Empty; - public string VotesUrl { get; set; } = string.Empty; - public string RedisOptions { get; set; } = string.Empty; - public string LocationIqApiKey { get; set; } = string.Empty; - public string TimezoneDbApiKey { get; set; } = string.Empty; - public string CoinmarketcapApiKey { get; set; } = string.Empty; - - public class RestartConfig - { - public RestartConfig(string cmd, string args) - { - this.Cmd = cmd; - this.Args = args; - } - - public string Cmd { get; set; } - public string Args { get; set; } - } } } -} + + public sealed record VotesSettings + { + [Comment(@"top.gg votes service url +This is the url of your instance of the NadekoBot.Votes api +Example: https://votes.my.cool.bot.com")] + public string TopggServiceUrl { get; set; } + + [Comment(@"Authorization header value sent to the TopGG service url with each request +This should be equivalent to the TopggKey in your NadekoBot.Votes api appsettings.json file")] + public string TopggKey { get; set; } + + [Comment(@"discords.com votes service url +This is the url of your instance of the NadekoBot.Votes api +Example: https://votes.my.cool.bot.com")] + public string DiscordsServiceUrl { get; set; } + + [Comment(@"Authorization header value sent to the Discords service url with each request +This should be equivalent to the DiscordsKey in your NadekoBot.Votes api appsettings.json file")] + public string DiscordsKey { get; set; } + + public VotesSettings() + { + + } + + public VotesSettings(string topggServiceUrl, string topggKey, string discordsServiceUrl, string discordsKey) + { + TopggServiceUrl = topggServiceUrl; + TopggKey = topggKey; + DiscordsServiceUrl = discordsServiceUrl; + DiscordsKey = discordsKey; + } + } + + public class Old + { + public string Token { get; set; } = string.Empty; + public ulong[] OwnerIds { get; set; } = new ulong[1]; + public string LoLApiKey { get; set; } = string.Empty; + public string GoogleApiKey { get; set; } = string.Empty; + public string MashapeKey { get; set; } = string.Empty; + public string OsuApiKey { get; set; } = string.Empty; + public string SoundCloudClientId { get; set; } = string.Empty; + public string CleverbotApiKey { get; set; } = string.Empty; + public string CarbonKey { get; set; } = string.Empty; + public int TotalShards { get; set; } = 1; + public string PatreonAccessToken { get; set; } = string.Empty; + public string PatreonCampaignId { get; set; } = "334038"; + public RestartConfig RestartCommand { get; set; } = null; + + public string ShardRunCommand { get; set; } = string.Empty; + public string ShardRunArguments { get; set; } = string.Empty; + public int? ShardRunPort { get; set; } = null; + public string MiningProxyUrl { get; set; } = string.Empty; + public string MiningProxyCreds { get; set; } = string.Empty; + + public string BotListToken { get; set; } = string.Empty; + public string TwitchClientId { get; set; } = string.Empty; + public string VotesToken { get; set; } = string.Empty; + public string VotesUrl { get; set; } = string.Empty; + public string RedisOptions { get; set; } = string.Empty; + public string LocationIqApiKey { get; set; } = string.Empty; + public string TimezoneDbApiKey { get; set; } = string.Empty; + public string CoinmarketcapApiKey { get; set; } = string.Empty; + + public class RestartConfig + { + public RestartConfig(string cmd, string args) + { + this.Cmd = cmd; + this.Args = args; + } + + public string Cmd { get; set; } + public string Args { get; set; } + } + } +} \ No newline at end of file diff --git a/src/NadekoBot/Common/DownloadTracker.cs b/src/NadekoBot/Common/DownloadTracker.cs index a16f41156..08fba91b5 100644 --- a/src/NadekoBot/Common/DownloadTracker.cs +++ b/src/NadekoBot/Common/DownloadTracker.cs @@ -1,43 +1,41 @@ using NadekoBot.Services; -using System; using System.Collections.Concurrent; using System.Threading; using System.Threading.Tasks; using Discord; -namespace NadekoBot.Common +namespace NadekoBot.Common; + +public class DownloadTracker : INService { - public class DownloadTracker : INService + private ConcurrentDictionary LastDownloads { get; } = new ConcurrentDictionary(); + private SemaphoreSlim downloadUsersSemaphore = new SemaphoreSlim(1, 1); + + /// + /// Ensures all users on the specified guild were downloaded within the last hour. + /// + /// Guild to check and potentially download users from + /// Task representing download state + public async Task EnsureUsersDownloadedAsync(IGuild guild) { - private ConcurrentDictionary LastDownloads { get; } = new ConcurrentDictionary(); - private SemaphoreSlim downloadUsersSemaphore = new SemaphoreSlim(1, 1); - - /// - /// Ensures all users on the specified guild were downloaded within the last hour. - /// - /// Guild to check and potentially download users from - /// Task representing download state - public async Task EnsureUsersDownloadedAsync(IGuild guild) + await downloadUsersSemaphore.WaitAsync(); + try { - await downloadUsersSemaphore.WaitAsync(); - try - { - var now = DateTime.UtcNow; + var now = DateTime.UtcNow; - // download once per hour at most - var added = LastDownloads.AddOrUpdate( - guild.Id, - now, - (key, old) => (now - old) > TimeSpan.FromHours(1) ? now : old); + // download once per hour at most + var added = LastDownloads.AddOrUpdate( + guild.Id, + now, + (key, old) => (now - old) > TimeSpan.FromHours(1) ? now : old); - // means that this entry was just added - download the users - if (added == now) - await guild.DownloadUsersAsync(); - } - finally - { - downloadUsersSemaphore.Release(); - } + // means that this entry was just added - download the users + if (added == now) + await guild.DownloadUsersAsync(); + } + finally + { + downloadUsersSemaphore.Release(); } } -} +} \ No newline at end of file diff --git a/src/NadekoBot/Common/Extensions/IBotCredentialsExtensions.cs b/src/NadekoBot/Common/Extensions/IBotCredentialsExtensions.cs index a09b621a4..78ef98d1c 100644 --- a/src/NadekoBot/Common/Extensions/IBotCredentialsExtensions.cs +++ b/src/NadekoBot/Common/Extensions/IBotCredentialsExtensions.cs @@ -1,11 +1,9 @@ using Discord; -using NadekoBot.Common; -namespace NadekoBot.Extensions +namespace NadekoBot.Extensions; + +public static class BotCredentialsExtensions { - public static class BotCredentialsExtensions - { - public static bool IsOwner(this IBotCredentials creds, IUser user) - => creds.OwnerIds.Contains(user.Id); - } + public static bool IsOwner(this IBotCredentials creds, IUser user) + => creds.OwnerIds.Contains(user.Id); } \ No newline at end of file diff --git a/src/NadekoBot/Common/Extensions/ServiceCollectionExtensions.cs b/src/NadekoBot/Common/Extensions/ServiceCollectionExtensions.cs index f183fe5b4..bd465b30d 100644 --- a/src/NadekoBot/Common/Extensions/ServiceCollectionExtensions.cs +++ b/src/NadekoBot/Common/Extensions/ServiceCollectionExtensions.cs @@ -1,81 +1,77 @@ -using System; -using System.Linq; -using System.Reflection; +using System.Reflection; using Microsoft.Extensions.DependencyInjection; using NadekoBot.Common; using NadekoBot.Modules.Music; using NadekoBot.Services; -using NadekoBot.Modules.Administration.Services; using NadekoBot.Modules.Music.Resolvers; using NadekoBot.Modules.Music.Services; using StackExchange.Redis; -namespace NadekoBot.Extensions +namespace NadekoBot.Extensions; + +public static class ServiceCollectionExtensions { - public static class ServiceCollectionExtensions + public static IServiceCollection AddBotStringsServices(this IServiceCollection services, int totalShards) + => totalShards <= 1 + ? services + .AddSingleton() + .AddSingleton() + .AddSingleton() + : services.AddSingleton() + .AddSingleton() + .AddSingleton(); + + public static IServiceCollection AddConfigServices(this IServiceCollection services) { - public static IServiceCollection AddBotStringsServices(this IServiceCollection services, int totalShards) - => totalShards <= 1 - ? services - .AddSingleton() - .AddSingleton() - .AddSingleton() - : services.AddSingleton() - .AddSingleton() - .AddSingleton(); + var baseType = typeof(ConfigServiceBase<>); - public static IServiceCollection AddConfigServices(this IServiceCollection services) + foreach (var type in Assembly.GetCallingAssembly().ExportedTypes.Where(x => x.IsSealed)) { - var baseType = typeof(ConfigServiceBase<>); - - foreach (var type in Assembly.GetCallingAssembly().ExportedTypes.Where(x => x.IsSealed)) + if (type.BaseType?.IsGenericType == true && type.BaseType.GetGenericTypeDefinition() == baseType) { - if (type.BaseType?.IsGenericType == true && type.BaseType.GetGenericTypeDefinition() == baseType) - { - services.AddSingleton(type); - services.AddSingleton(x => (IConfigService)x.GetRequiredService(type)); - } + services.AddSingleton(type); + services.AddSingleton(x => (IConfigService)x.GetRequiredService(type)); } - - return services; } - public static IServiceCollection AddConfigMigrators(this IServiceCollection services) - => services.AddSealedSubclassesOf(typeof(IConfigMigrator)); + return services; + } - public static IServiceCollection AddMusic(this IServiceCollection services) - => services - .AddSingleton() - .AddSingleton() - .AddSingleton() - .AddSingleton() - .AddSingleton() - .AddSingleton() - .AddSingleton() - .AddSingleton() - .AddSingleton(svc => svc.GetRequiredService()); + public static IServiceCollection AddConfigMigrators(this IServiceCollection services) + => services.AddSealedSubclassesOf(typeof(IConfigMigrator)); + + public static IServiceCollection AddMusic(this IServiceCollection services) + => services + .AddSingleton() + .AddSingleton() + .AddSingleton() + .AddSingleton() + .AddSingleton() + .AddSingleton() + .AddSingleton() + .AddSingleton() + .AddSingleton(svc => svc.GetRequiredService()); - // consider using scrutor, because slightly different versions - // of this might be needed in several different places - public static IServiceCollection AddSealedSubclassesOf(this IServiceCollection services, Type baseType) + // consider using scrutor, because slightly different versions + // of this might be needed in several different places + public static IServiceCollection AddSealedSubclassesOf(this IServiceCollection services, Type baseType) + { + var subTypes = Assembly.GetCallingAssembly() + .ExportedTypes + .Where(type => type.IsSealed && baseType.IsAssignableFrom(type)); + + foreach (var subType in subTypes) { - var subTypes = Assembly.GetCallingAssembly() - .ExportedTypes - .Where(type => type.IsSealed && baseType.IsAssignableFrom(type)); - - foreach (var subType in subTypes) - { - services.AddSingleton(baseType, subType); - } - - return services; + services.AddSingleton(baseType, subType); } - public static IServiceCollection AddRedis(this IServiceCollection services, string redisOptions) - { - var conf = ConfigurationOptions.Parse(redisOptions); - services.AddSingleton(ConnectionMultiplexer.Connect(conf)); - return services; - } + return services; + } + + public static IServiceCollection AddRedis(this IServiceCollection services, string redisOptions) + { + var conf = ConfigurationOptions.Parse(redisOptions); + services.AddSingleton(ConnectionMultiplexer.Connect(conf)); + return services; } } \ No newline at end of file diff --git a/src/NadekoBot/Common/GuildPerm.cs b/src/NadekoBot/Common/GuildPerm.cs index e5e03f9be..9c4ba48fb 100644 --- a/src/NadekoBot/Common/GuildPerm.cs +++ b/src/NadekoBot/Common/GuildPerm.cs @@ -1,255 +1,250 @@ -using System; +namespace Discord; +// just a copy paste from discord.net in order to rename it, for compatibility iwth v3 which is gonna use custom lib -namespace Discord +// Summary: +// Defines the available permissions for a channel. +[Flags] +public enum GuildPerm : ulong { - // just a copy paste from discord.net in order to rename it, for compatibility iwth v3 which is gonna use custom lib - - - // Summary: - // Defines the available permissions for a channel. - [Flags] - public enum GuildPerm : ulong - { - // - // Summary: - // Allows creation of instant invites. - CreateInstantInvite = 1, - // - // Summary: - // Allows kicking members. - // - // Remarks: - // This permission requires the owner account to use two-factor authentication when - // used on a guild that has server-wide 2FA enabled. - KickMembers = 2, - // - // Summary: - // Allows banning members. - // - // Remarks: - // This permission requires the owner account to use two-factor authentication when - // used on a guild that has server-wide 2FA enabled. - BanMembers = 4, - // - // Summary: - // Allows all permissions and bypasses channel permission overwrites. - // - // Remarks: - // This permission requires the owner account to use two-factor authentication when - // used on a guild that has server-wide 2FA enabled. - Administrator = 8, - // - // Summary: - // Allows management and editing of channels. - // - // Remarks: - // This permission requires the owner account to use two-factor authentication when - // used on a guild that has server-wide 2FA enabled. - ManageChannels = 16, - // - // Summary: - // Allows management and editing of the guild. - // - // Remarks: - // This permission requires the owner account to use two-factor authentication when - // used on a guild that has server-wide 2FA enabled. - ManageGuild = 32, - // - // Summary: - // Allows for the addition of reactions to messages. - AddReactions = 64, - // - // Summary: - // Allows for viewing of audit logs. - ViewAuditLog = 128, - PrioritySpeaker = 256, - ReadMessages = 1024, - ViewChannel = 1024, - SendMessages = 2048, - // - // Summary: - // Allows for sending of text-to-speech messages. - SendTTSMessages = 4096, - // - // Summary: - // Allows for deletion of other users messages. - // - // Remarks: - // This permission requires the owner account to use two-factor authentication when - // used on a guild that has server-wide 2FA enabled. - ManageMessages = 8192, - // - // Summary: - // Allows links sent by users with this permission will be auto-embedded. - EmbedLinks = 16384, - // - // Summary: - // Allows for uploading images and files. - AttachFiles = 32768, - // - // Summary: - // Allows for reading of message history. - ReadMessageHistory = 65536, - // - // Summary: - // Allows for using the @everyone tag to notify all users in a channel, and the - // @here tag to notify all online users in a channel. - MentionEveryone = 131072, - // - // Summary: - // Allows the usage of custom emojis from other servers. - UseExternalEmojis = 262144, - // - // Summary: - // Allows for joining of a voice channel. - Connect = 1048576, - // - // Summary: - // Allows for speaking in a voice channel. - Speak = 2097152, - // - // Summary: - // Allows for muting members in a voice channel. - MuteMembers = 4194304, - // - // Summary: - // Allows for deafening of members in a voice channel. - DeafenMembers = 8388608, - // - // Summary: - // Allows for moving of members between voice channels. - MoveMembers = 16777216, - // - // Summary: - // Allows for using voice-activity-detection in a voice channel. - UseVAD = 33554432, - // - // Summary: - // Allows for modification of own nickname. - ChangeNickname = 67108864, - // - // Summary: - // Allows for modification of other users nicknames. - ManageNicknames = 134217728, - // - // Summary: - // Allows management and editing of roles. - // - // Remarks: - // This permission requires the owner account to use two-factor authentication when - // used on a guild that has server-wide 2FA enabled. - ManageRoles = 268435456, - // - // Summary: - // Allows management and editing of webhooks. - // - // Remarks: - // This permission requires the owner account to use two-factor authentication when - // used on a guild that has server-wide 2FA enabled. - ManageWebhooks = 536870912, - // - // Summary: - // Allows management and editing of emojis. - // - // Remarks: - // This permission requires the owner account to use two-factor authentication when - // used on a guild that has server-wide 2FA enabled. - ManageEmojis = 1073741824 - } - // // Summary: - // Defines the available permissions for a channel. - [Flags] - public enum ChannelPerm : ulong - { - // - // Summary: - // Allows creation of instant invites. - CreateInstantInvite = 1, - // - // Summary: - // Allows management and editing of channels. - ManageChannel = 16, - // - // Summary: - // Allows for the addition of reactions to messages. - AddReactions = 64, - PrioritySpeaker = 256, - // - // Summary: - // Allows for reading of messages. This flag is obsolete, use Discord.ChannelPermission.ViewChannel - // instead. - ReadMessages = 1024, - // - // Summary: - // Allows guild members to view a channel, which includes reading messages in text - // channels. - ViewChannel = 1024, - // - // Summary: - // Allows for sending messages in a channel. - SendMessages = 2048, - // - // Summary: - // Allows for sending of text-to-speech messages. - SendTTSMessages = 4096, - // - // Summary: - // Allows for deletion of other users messages. - ManageMessages = 8192, - // - // Summary: - // Allows links sent by users with this permission will be auto-embedded. - EmbedLinks = 16384, - // - // Summary: - // Allows for uploading images and files. - AttachFiles = 32768, - // - // Summary: - // Allows for reading of message history. - ReadMessageHistory = 65536, - // - // Summary: - // Allows for using the @everyone tag to notify all users in a channel, and the - // @here tag to notify all online users in a channel. - MentionEveryone = 131072, - // - // Summary: - // Allows the usage of custom emojis from other servers. - UseExternalEmojis = 262144, - // - // Summary: - // Allows for joining of a voice channel. - Connect = 1048576, - // - // Summary: - // Allows for speaking in a voice channel. - Speak = 2097152, - // - // Summary: - // Allows for muting members in a voice channel. - MuteMembers = 4194304, - // - // Summary: - // Allows for deafening of members in a voice channel. - DeafenMembers = 8388608, - // - // Summary: - // Allows for moving of members between voice channels. - MoveMembers = 16777216, - // - // Summary: - // Allows for using voice-activity-detection in a voice channel. - UseVAD = 33554432, - // - // Summary: - // Allows management and editing of roles. - ManageRoles = 268435456, - // - // Summary: - // Allows management and editing of webhooks. - ManageWebhooks = 536870912 - } + // Allows creation of instant invites. + CreateInstantInvite = 1, + // + // Summary: + // Allows kicking members. + // + // Remarks: + // This permission requires the owner account to use two-factor authentication when + // used on a guild that has server-wide 2FA enabled. + KickMembers = 2, + // + // Summary: + // Allows banning members. + // + // Remarks: + // This permission requires the owner account to use two-factor authentication when + // used on a guild that has server-wide 2FA enabled. + BanMembers = 4, + // + // Summary: + // Allows all permissions and bypasses channel permission overwrites. + // + // Remarks: + // This permission requires the owner account to use two-factor authentication when + // used on a guild that has server-wide 2FA enabled. + Administrator = 8, + // + // Summary: + // Allows management and editing of channels. + // + // Remarks: + // This permission requires the owner account to use two-factor authentication when + // used on a guild that has server-wide 2FA enabled. + ManageChannels = 16, + // + // Summary: + // Allows management and editing of the guild. + // + // Remarks: + // This permission requires the owner account to use two-factor authentication when + // used on a guild that has server-wide 2FA enabled. + ManageGuild = 32, + // + // Summary: + // Allows for the addition of reactions to messages. + AddReactions = 64, + // + // Summary: + // Allows for viewing of audit logs. + ViewAuditLog = 128, + PrioritySpeaker = 256, + ReadMessages = 1024, + ViewChannel = 1024, + SendMessages = 2048, + // + // Summary: + // Allows for sending of text-to-speech messages. + SendTTSMessages = 4096, + // + // Summary: + // Allows for deletion of other users messages. + // + // Remarks: + // This permission requires the owner account to use two-factor authentication when + // used on a guild that has server-wide 2FA enabled. + ManageMessages = 8192, + // + // Summary: + // Allows links sent by users with this permission will be auto-embedded. + EmbedLinks = 16384, + // + // Summary: + // Allows for uploading images and files. + AttachFiles = 32768, + // + // Summary: + // Allows for reading of message history. + ReadMessageHistory = 65536, + // + // Summary: + // Allows for using the @everyone tag to notify all users in a channel, and the + // @here tag to notify all online users in a channel. + MentionEveryone = 131072, + // + // Summary: + // Allows the usage of custom emojis from other servers. + UseExternalEmojis = 262144, + // + // Summary: + // Allows for joining of a voice channel. + Connect = 1048576, + // + // Summary: + // Allows for speaking in a voice channel. + Speak = 2097152, + // + // Summary: + // Allows for muting members in a voice channel. + MuteMembers = 4194304, + // + // Summary: + // Allows for deafening of members in a voice channel. + DeafenMembers = 8388608, + // + // Summary: + // Allows for moving of members between voice channels. + MoveMembers = 16777216, + // + // Summary: + // Allows for using voice-activity-detection in a voice channel. + UseVAD = 33554432, + // + // Summary: + // Allows for modification of own nickname. + ChangeNickname = 67108864, + // + // Summary: + // Allows for modification of other users nicknames. + ManageNicknames = 134217728, + // + // Summary: + // Allows management and editing of roles. + // + // Remarks: + // This permission requires the owner account to use two-factor authentication when + // used on a guild that has server-wide 2FA enabled. + ManageRoles = 268435456, + // + // Summary: + // Allows management and editing of webhooks. + // + // Remarks: + // This permission requires the owner account to use two-factor authentication when + // used on a guild that has server-wide 2FA enabled. + ManageWebhooks = 536870912, + // + // Summary: + // Allows management and editing of emojis. + // + // Remarks: + // This permission requires the owner account to use two-factor authentication when + // used on a guild that has server-wide 2FA enabled. + ManageEmojis = 1073741824 } + +// +// Summary: +// Defines the available permissions for a channel. +[Flags] +public enum ChannelPerm : ulong +{ + // + // Summary: + // Allows creation of instant invites. + CreateInstantInvite = 1, + // + // Summary: + // Allows management and editing of channels. + ManageChannel = 16, + // + // Summary: + // Allows for the addition of reactions to messages. + AddReactions = 64, + PrioritySpeaker = 256, + // + // Summary: + // Allows for reading of messages. This flag is obsolete, use Discord.ChannelPermission.ViewChannel + // instead. + ReadMessages = 1024, + // + // Summary: + // Allows guild members to view a channel, which includes reading messages in text + // channels. + ViewChannel = 1024, + // + // Summary: + // Allows for sending messages in a channel. + SendMessages = 2048, + // + // Summary: + // Allows for sending of text-to-speech messages. + SendTTSMessages = 4096, + // + // Summary: + // Allows for deletion of other users messages. + ManageMessages = 8192, + // + // Summary: + // Allows links sent by users with this permission will be auto-embedded. + EmbedLinks = 16384, + // + // Summary: + // Allows for uploading images and files. + AttachFiles = 32768, + // + // Summary: + // Allows for reading of message history. + ReadMessageHistory = 65536, + // + // Summary: + // Allows for using the @everyone tag to notify all users in a channel, and the + // @here tag to notify all online users in a channel. + MentionEveryone = 131072, + // + // Summary: + // Allows the usage of custom emojis from other servers. + UseExternalEmojis = 262144, + // + // Summary: + // Allows for joining of a voice channel. + Connect = 1048576, + // + // Summary: + // Allows for speaking in a voice channel. + Speak = 2097152, + // + // Summary: + // Allows for muting members in a voice channel. + MuteMembers = 4194304, + // + // Summary: + // Allows for deafening of members in a voice channel. + DeafenMembers = 8388608, + // + // Summary: + // Allows for moving of members between voice channels. + MoveMembers = 16777216, + // + // Summary: + // Allows for using voice-activity-detection in a voice channel. + UseVAD = 33554432, + // + // Summary: + // Allows management and editing of roles. + ManageRoles = 268435456, + // + // Summary: + // Allows management and editing of webhooks. + ManageWebhooks = 536870912 +} \ No newline at end of file diff --git a/src/NadekoBot/Common/Helpers.cs b/src/NadekoBot/Common/Helpers.cs index d003e13fb..35c455584 100644 --- a/src/NadekoBot/Common/Helpers.cs +++ b/src/NadekoBot/Common/Helpers.cs @@ -1,15 +1,12 @@ -using System; +namespace NadekoBot.Common; -namespace NadekoBot.Common +public static class Helpers { - public static class Helpers + public static void ReadErrorAndExit(int exitCode) { - public static void ReadErrorAndExit(int exitCode) - { - if (!Console.IsInputRedirected) - Console.ReadKey(); + if (!Console.IsInputRedirected) + Console.ReadKey(); - Environment.Exit(exitCode); - } + Environment.Exit(exitCode); } } \ No newline at end of file diff --git a/src/NadekoBot/Common/IBotCredentials.cs b/src/NadekoBot/Common/IBotCredentials.cs index 444abb73b..6621aa4cb 100644 --- a/src/NadekoBot/Common/IBotCredentials.cs +++ b/src/NadekoBot/Common/IBotCredentials.cs @@ -1,36 +1,31 @@ -using System.Collections.Generic; -using Discord; -using System.Collections.Immutable; -using System.Linq; -using NadekoBot.Common; +using NadekoBot.Common; -namespace NadekoBot +namespace NadekoBot; + +public interface IBotCredentials { - public interface IBotCredentials - { - string Token { get; } - string GoogleApiKey { get; } - ICollection OwnerIds { get; } - string RapidApiKey { get; } + string Token { get; } + string GoogleApiKey { get; } + ICollection OwnerIds { get; } + string RapidApiKey { get; } - Creds.DbOptions Db { get; } - string OsuApiKey { get; } - int TotalShards { get; } - Creds.PatreonSettings Patreon { get; } - string CleverbotApiKey { get; } - RestartConfig RestartCommand { get; } - Creds.VotesSettings Votes { get; } - string BotListToken { get; } - string RedisOptions { get; } - string LocationIqApiKey { get; } - string TimezoneDbApiKey { get; } - string CoinmarketcapApiKey { get; } - string CoordinatorUrl { get; set; } - } - - public class RestartConfig - { - public string Cmd { get; set; } - public string Args { get; set; } - } + Creds.DbOptions Db { get; } + string OsuApiKey { get; } + int TotalShards { get; } + Creds.PatreonSettings Patreon { get; } + string CleverbotApiKey { get; } + RestartConfig RestartCommand { get; } + Creds.VotesSettings Votes { get; } + string BotListToken { get; } + string RedisOptions { get; } + string LocationIqApiKey { get; } + string TimezoneDbApiKey { get; } + string CoinmarketcapApiKey { get; } + string CoordinatorUrl { get; set; } } + +public class RestartConfig +{ + public string Cmd { get; set; } + public string Args { get; set; } +} \ No newline at end of file diff --git a/src/NadekoBot/Common/ICloneable.cs b/src/NadekoBot/Common/ICloneable.cs index d5062325b..deddec15e 100644 --- a/src/NadekoBot/Common/ICloneable.cs +++ b/src/NadekoBot/Common/ICloneable.cs @@ -1,7 +1,6 @@ -namespace NadekoBot.Common +namespace NadekoBot.Common; + +public interface ICloneable where T : new() { - public interface ICloneable where T : new() - { - public T Clone(); - } + public T Clone(); } \ No newline at end of file diff --git a/src/NadekoBot/Common/IEmbedBuilder.cs b/src/NadekoBot/Common/IEmbedBuilder.cs index 7c65e2143..c9fd0f500 100644 --- a/src/NadekoBot/Common/IEmbedBuilder.cs +++ b/src/NadekoBot/Common/IEmbedBuilder.cs @@ -1,25 +1,24 @@ using Discord; -namespace NadekoBot -{ - public interface IEmbedBuilder - { - IEmbedBuilder WithDescription(string desc); - IEmbedBuilder WithTitle(string title); - IEmbedBuilder AddField(string title, object value, bool isInline = false); - IEmbedBuilder WithFooter(string text, string iconUrl = null); - IEmbedBuilder WithAuthor(string name, string iconUrl = null, string url = null); - IEmbedBuilder WithColor(EmbedColor color); - Embed Build(); - IEmbedBuilder WithUrl(string url); - IEmbedBuilder WithImageUrl(string url); - IEmbedBuilder WithThumbnailUrl(string url); - } +namespace NadekoBot; - public enum EmbedColor - { - Ok, - Pending, - Error, - } +public interface IEmbedBuilder +{ + IEmbedBuilder WithDescription(string desc); + IEmbedBuilder WithTitle(string title); + IEmbedBuilder AddField(string title, object value, bool isInline = false); + IEmbedBuilder WithFooter(string text, string iconUrl = null); + IEmbedBuilder WithAuthor(string name, string iconUrl = null, string url = null); + IEmbedBuilder WithColor(EmbedColor color); + Embed Build(); + IEmbedBuilder WithUrl(string url); + IEmbedBuilder WithImageUrl(string url); + IEmbedBuilder WithThumbnailUrl(string url); +} + +public enum EmbedColor +{ + Ok, + Pending, + Error, } \ No newline at end of file diff --git a/src/NadekoBot/Common/INadekoCommandOptions.cs b/src/NadekoBot/Common/INadekoCommandOptions.cs index 92e985b10..13c157554 100644 --- a/src/NadekoBot/Common/INadekoCommandOptions.cs +++ b/src/NadekoBot/Common/INadekoCommandOptions.cs @@ -1,7 +1,6 @@ -namespace NadekoBot.Common +namespace NadekoBot.Common; + +public interface INadekoCommandOptions { - public interface INadekoCommandOptions - { - void NormalizeOptions(); - } -} + void NormalizeOptions(); +} \ No newline at end of file diff --git a/src/NadekoBot/Common/IPlaceholderProvider.cs b/src/NadekoBot/Common/IPlaceholderProvider.cs index c9c5df438..cf9c94072 100644 --- a/src/NadekoBot/Common/IPlaceholderProvider.cs +++ b/src/NadekoBot/Common/IPlaceholderProvider.cs @@ -1,10 +1,6 @@ -using System; -using System.Collections.Generic; +namespace NadekoBot.Common; -namespace NadekoBot.Common +public interface IPlaceholderProvider { - public interface IPlaceholderProvider - { - public IEnumerable<(string Name, Func Func)> GetPlaceholders(); - } + public IEnumerable<(string Name, Func Func)> GetPlaceholders(); } \ No newline at end of file diff --git a/src/NadekoBot/Common/ImageUrls.cs b/src/NadekoBot/Common/ImageUrls.cs index 694d5207e..c4fdf4cda 100644 --- a/src/NadekoBot/Common/ImageUrls.cs +++ b/src/NadekoBot/Common/ImageUrls.cs @@ -1,50 +1,48 @@ -using System; -using NadekoBot.Common.Yml; +using NadekoBot.Common.Yml; -namespace NadekoBot.Common +namespace NadekoBot.Common; + +public class ImageUrls { - public class ImageUrls + [Comment("DO NOT CHANGE")] + public int Version { get; set; } = 3; + + public CoinData Coins { get; set; } + public Uri[] Currency { get; set; } + public Uri[] Dice { get; set; } + public RategirlData Rategirl { get; set; } + public XpData Xp { get; set; } + + //new + public RipData Rip { get; set; } + public SlotData Slots { get; set; } + + public class RipData { - [Comment("DO NOT CHANGE")] - public int Version { get; set; } = 3; - - public CoinData Coins { get; set; } - public Uri[] Currency { get; set; } - public Uri[] Dice { get; set; } - public RategirlData Rategirl { get; set; } - public XpData Xp { get; set; } - - //new - public RipData Rip { get; set; } - public SlotData Slots { get; set; } - - public class RipData - { - public Uri Bg { get; set; } - public Uri Overlay { get; set; } - } - - public class SlotData - { - public Uri[] Emojis { get; set; } - public Uri Bg { get; set; } - } - - public class CoinData - { - public Uri[] Heads { get; set; } - public Uri[] Tails { get; set; } - } - - public class RategirlData - { - public Uri Matrix { get; set; } - public Uri Dot { get; set; } - } - - public class XpData - { - public Uri Bg { get; set; } - } + public Uri Bg { get; set; } + public Uri Overlay { get; set; } } -} + + public class SlotData + { + public Uri[] Emojis { get; set; } + public Uri Bg { get; set; } + } + + public class CoinData + { + public Uri[] Heads { get; set; } + public Uri[] Tails { get; set; } + } + + public class RategirlData + { + public Uri Matrix { get; set; } + public Uri Dot { get; set; } + } + + public class XpData + { + public Uri Bg { get; set; } + } +} \ No newline at end of file diff --git a/src/NadekoBot/Common/JsonConverters/Rbga32Converter.cs b/src/NadekoBot/Common/JsonConverters/Rbga32Converter.cs index 638c2f7a4..207d3a4fe 100644 --- a/src/NadekoBot/Common/JsonConverters/Rbga32Converter.cs +++ b/src/NadekoBot/Common/JsonConverters/Rbga32Converter.cs @@ -1,34 +1,32 @@ -using System; -using System.Globalization; +using System.Globalization; using System.Text.Json; using System.Text.Json.Serialization; using SixLabors.ImageSharp.PixelFormats; -namespace NadekoBot.Common.JsonConverters +namespace NadekoBot.Common.JsonConverters; + +public class Rgba32Converter : JsonConverter { - public class Rgba32Converter : JsonConverter + public override Rgba32 Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { - public override Rgba32 Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) - { - return Rgba32.ParseHex(reader.GetString()); - } - - public override void Write(Utf8JsonWriter writer, Rgba32 value, JsonSerializerOptions options) - { - writer.WriteStringValue(value.ToHex()); - } + return Rgba32.ParseHex(reader.GetString()); } - - public class CultureInfoConverter : JsonConverter - { - public override CultureInfo Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) - { - return new CultureInfo(reader.GetString()); - } - public override void Write(Utf8JsonWriter writer, CultureInfo value, JsonSerializerOptions options) - { - writer.WriteStringValue(value.Name); - } + public override void Write(Utf8JsonWriter writer, Rgba32 value, JsonSerializerOptions options) + { + writer.WriteStringValue(value.ToHex()); + } +} + +public class CultureInfoConverter : JsonConverter +{ + public override CultureInfo Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + return new CultureInfo(reader.GetString()); + } + + public override void Write(Utf8JsonWriter writer, CultureInfo value, JsonSerializerOptions options) + { + writer.WriteStringValue(value.Name); } } \ No newline at end of file diff --git a/src/NadekoBot/Common/Kwum.cs b/src/NadekoBot/Common/Kwum.cs index b72550dc0..c13b4204f 100644 --- a/src/NadekoBot/Common/Kwum.cs +++ b/src/NadekoBot/Common/Kwum.cs @@ -1,98 +1,96 @@ -using System; -using System.Runtime.CompilerServices; +using System.Runtime.CompilerServices; -namespace NadekoBot.Common +namespace NadekoBot.Common; + +// needs proper invalid input check (character array input out of range) +// needs negative number support +public readonly struct kwum : IEquatable { - // needs proper invalid input check (character array input out of range) - // needs negative number support - public readonly struct kwum : IEquatable + private readonly int _value; + private const string ValidCharacters = "23456789abcdefghijkmnpqrstuvwxyz"; + + public kwum(int num) + => _value = num; + + public kwum(in char c) { - private readonly int _value; - private const string ValidCharacters = "23456789abcdefghijkmnpqrstuvwxyz"; + if (!IsValidChar(c)) + throw new ArgumentException("Character needs to be a valid kwum character.", nameof(c)); - public kwum(int num) - => _value = num; - - public kwum(in char c) + _value = InternalCharToValue(c); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static int InternalCharToValue(in char c) + => ValidCharacters.IndexOf(c); + + public kwum(in ReadOnlySpan input) + {; + _value = 0; + for (var index = 0; index < input.Length; index++) { + var c = input[index]; if (!IsValidChar(c)) - throw new ArgumentException("Character needs to be a valid kwum character.", nameof(c)); + throw new ArgumentException("All characters need to be a valid kwum characters.", nameof(input)); - _value = InternalCharToValue(c); - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static int InternalCharToValue(in char c) - => ValidCharacters.IndexOf(c); - - public kwum(in ReadOnlySpan input) - {; - _value = 0; - for (var index = 0; index < input.Length; index++) - { - var c = input[index]; - if (!IsValidChar(c)) - throw new ArgumentException("All characters need to be a valid kwum characters.", nameof(input)); - - _value += ValidCharacters.IndexOf(c) * (int)Math.Pow(ValidCharacters.Length, input.Length - index - 1); - } - } - - public static bool TryParse(in ReadOnlySpan input, out kwum value) - { - value = default; - foreach(var c in input) - if (!IsValidChar(c)) - return false; - - value = new kwum(input); - return true; - } - - public static kwum operator +(kwum left, kwum right) - => new kwum(left._value + right._value); - - public static bool operator ==(kwum left, kwum right) - => left._value == right._value; - - public static bool operator !=(kwum left, kwum right) - => !(left == right); - - public static implicit operator long(kwum kwum) - => kwum._value; - - public static implicit operator int(kwum kwum) - => kwum._value; - public static implicit operator kwum(int num) - => new kwum(num); - - public static bool IsValidChar(char c) - => ValidCharacters.Contains(c); - - public override string ToString() - { - var count = ValidCharacters.Length; - var localValue = _value; - var arrSize = (int)Math.Log(localValue, count) + 1; - Span chars = new char[arrSize]; - while (localValue > 0) - { - localValue = Math.DivRem(localValue, count, out var rem); - chars[--arrSize] = ValidCharacters[(int)rem]; - } - - return new string(chars); - } - - public override bool Equals(object obj) - => obj is kwum kw && kw == this; - - public bool Equals(kwum other) - => other == this; - - public override int GetHashCode() - { - return _value.GetHashCode(); + _value += ValidCharacters.IndexOf(c) * (int)Math.Pow(ValidCharacters.Length, input.Length - index - 1); } } + + public static bool TryParse(in ReadOnlySpan input, out kwum value) + { + value = default; + foreach(var c in input) + if (!IsValidChar(c)) + return false; + + value = new kwum(input); + return true; + } + + public static kwum operator +(kwum left, kwum right) + => new kwum(left._value + right._value); + + public static bool operator ==(kwum left, kwum right) + => left._value == right._value; + + public static bool operator !=(kwum left, kwum right) + => !(left == right); + + public static implicit operator long(kwum kwum) + => kwum._value; + + public static implicit operator int(kwum kwum) + => kwum._value; + public static implicit operator kwum(int num) + => new kwum(num); + + public static bool IsValidChar(char c) + => ValidCharacters.Contains(c); + + public override string ToString() + { + var count = ValidCharacters.Length; + var localValue = _value; + var arrSize = (int)Math.Log(localValue, count) + 1; + Span chars = new char[arrSize]; + while (localValue > 0) + { + localValue = Math.DivRem(localValue, count, out var rem); + chars[--arrSize] = ValidCharacters[(int)rem]; + } + + return new string(chars); + } + + public override bool Equals(object obj) + => obj is kwum kw && kw == this; + + public bool Equals(kwum other) + => other == this; + + public override int GetHashCode() + { + return _value.GetHashCode(); + } } \ No newline at end of file diff --git a/src/NadekoBot/Common/LbOpts.cs b/src/NadekoBot/Common/LbOpts.cs index bad634578..ec5d13a4f 100644 --- a/src/NadekoBot/Common/LbOpts.cs +++ b/src/NadekoBot/Common/LbOpts.cs @@ -1,14 +1,13 @@ using CommandLine; -namespace NadekoBot.Common -{ - public class LbOpts : INadekoCommandOptions - { - [Option('c', "clean", Default = false, HelpText = "Only show users who are on the server.")] - public bool Clean { get; set; } - public void NormalizeOptions() - { +namespace NadekoBot.Common; + +public class LbOpts : INadekoCommandOptions +{ + [Option('c', "clean", Default = false, HelpText = "Only show users who are on the server.")] + public bool Clean { get; set; } + public void NormalizeOptions() + { - } } -} +} \ No newline at end of file diff --git a/src/NadekoBot/Common/LoginErrorHandler.cs b/src/NadekoBot/Common/LoginErrorHandler.cs index 750d9ffb3..122a01c74 100644 --- a/src/NadekoBot/Common/LoginErrorHandler.cs +++ b/src/NadekoBot/Common/LoginErrorHandler.cs @@ -1,57 +1,54 @@ -using System; -using System.Net; +using System.Net; using System.Runtime.CompilerServices; using Discord.Net; -using Serilog; -namespace NadekoBot.Common +namespace NadekoBot.Common; + +public class LoginErrorHandler { - public class LoginErrorHandler + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void Handle(Exception ex) { - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void Handle(Exception ex) - { - Log.Fatal(ex, "A fatal error has occurred while attempting to connect to Discord"); - } + Log.Fatal(ex, "A fatal error has occurred while attempting to connect to Discord"); + } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void Handle(HttpException ex) + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void Handle(HttpException ex) + { + switch (ex.HttpCode) { - switch (ex.HttpCode) - { - case HttpStatusCode.Unauthorized: - Log.Error("Your bot token is wrong.\n" + - "You can find the bot token under the Bot tab in the developer page.\n" + - "Fix your token in the credentials file and restart the bot"); - break; + case HttpStatusCode.Unauthorized: + Log.Error("Your bot token is wrong.\n" + + "You can find the bot token under the Bot tab in the developer page.\n" + + "Fix your token in the credentials file and restart the bot"); + break; - case HttpStatusCode.BadRequest: - Log.Error("Something has been incorrectly formatted in your credentials file.\n" + - "Use the JSON Guide as reference to fix it and restart the bot."); - Log.Error("If you are on Linux, make sure Redis is installed and running"); - break; + case HttpStatusCode.BadRequest: + Log.Error("Something has been incorrectly formatted in your credentials file.\n" + + "Use the JSON Guide as reference to fix it and restart the bot."); + Log.Error("If you are on Linux, make sure Redis is installed and running"); + break; - case HttpStatusCode.RequestTimeout: - Log.Error("The request timed out. Make sure you have no external program blocking the bot " + - "from connecting to the internet"); - break; + case HttpStatusCode.RequestTimeout: + Log.Error("The request timed out. Make sure you have no external program blocking the bot " + + "from connecting to the internet"); + break; - case HttpStatusCode.ServiceUnavailable: - case HttpStatusCode.InternalServerError: - Log.Error("Discord is having internal issues. Please, try again later"); - break; + case HttpStatusCode.ServiceUnavailable: + case HttpStatusCode.InternalServerError: + Log.Error("Discord is having internal issues. Please, try again later"); + break; - case HttpStatusCode.TooManyRequests: - Log.Error("Your bot has been ratelimited by Discord. Please, try again later.\n" + - "Global ratelimits usually last for an hour"); - break; + case HttpStatusCode.TooManyRequests: + Log.Error("Your bot has been ratelimited by Discord. Please, try again later.\n" + + "Global ratelimits usually last for an hour"); + break; - default: - Log.Warning("An error occurred while attempting to connect to Discord"); - break; - } - - Log.Fatal(ex.ToString()); + default: + Log.Warning("An error occurred while attempting to connect to Discord"); + break; } + + Log.Fatal(ex.ToString()); } } \ No newline at end of file diff --git a/src/NadekoBot/Common/ModuleBehaviors/IEarlyBehavior.cs b/src/NadekoBot/Common/ModuleBehaviors/IEarlyBehavior.cs index 17989045b..032f47630 100644 --- a/src/NadekoBot/Common/ModuleBehaviors/IEarlyBehavior.cs +++ b/src/NadekoBot/Common/ModuleBehaviors/IEarlyBehavior.cs @@ -1,14 +1,13 @@ using System.Threading.Tasks; using Discord; -namespace NadekoBot.Common.ModuleBehaviors +namespace NadekoBot.Common.ModuleBehaviors; + +/// +/// Implemented by modules which block execution before anything is executed +/// +public interface IEarlyBehavior { - /// - /// Implemented by modules which block execution before anything is executed - /// - public interface IEarlyBehavior - { - int Priority { get; } - Task RunBehavior(IGuild guild, IUserMessage msg); - } -} + int Priority { get; } + Task RunBehavior(IGuild guild, IUserMessage msg); +} \ No newline at end of file diff --git a/src/NadekoBot/Common/ModuleBehaviors/IINputTransformer.cs b/src/NadekoBot/Common/ModuleBehaviors/IINputTransformer.cs index 8f4be4703..552eba1ec 100644 --- a/src/NadekoBot/Common/ModuleBehaviors/IINputTransformer.cs +++ b/src/NadekoBot/Common/ModuleBehaviors/IINputTransformer.cs @@ -1,10 +1,9 @@ using System.Threading.Tasks; using Discord; -namespace NadekoBot.Common.ModuleBehaviors +namespace NadekoBot.Common.ModuleBehaviors; + +public interface IInputTransformer { - public interface IInputTransformer - { - Task TransformInput(IGuild guild, IMessageChannel channel, IUser user, string input); - } -} + Task TransformInput(IGuild guild, IMessageChannel channel, IUser user, string input); +} \ No newline at end of file diff --git a/src/NadekoBot/Common/ModuleBehaviors/ILateBlocker.cs b/src/NadekoBot/Common/ModuleBehaviors/ILateBlocker.cs index 2a4a634ee..af9ad2a48 100644 --- a/src/NadekoBot/Common/ModuleBehaviors/ILateBlocker.cs +++ b/src/NadekoBot/Common/ModuleBehaviors/ILateBlocker.cs @@ -1,13 +1,11 @@ using System.Threading.Tasks; using Discord.Commands; -using Discord.WebSocket; -namespace NadekoBot.Common.ModuleBehaviors +namespace NadekoBot.Common.ModuleBehaviors; + +public interface ILateBlocker { - public interface ILateBlocker - { - public int Priority { get; } + public int Priority { get; } - Task TryBlockLate(ICommandContext context, string moduleName, CommandInfo command); - } -} + Task TryBlockLate(ICommandContext context, string moduleName, CommandInfo command); +} \ No newline at end of file diff --git a/src/NadekoBot/Common/ModuleBehaviors/ILateExecutor.cs b/src/NadekoBot/Common/ModuleBehaviors/ILateExecutor.cs index 66aff2477..a10688c03 100644 --- a/src/NadekoBot/Common/ModuleBehaviors/ILateExecutor.cs +++ b/src/NadekoBot/Common/ModuleBehaviors/ILateExecutor.cs @@ -1,13 +1,12 @@ using System.Threading.Tasks; using Discord; -namespace NadekoBot.Common.ModuleBehaviors +namespace NadekoBot.Common.ModuleBehaviors; + +/// +/// Last thing to be executed, won't stop further executions +/// +public interface ILateExecutor { - /// - /// Last thing to be executed, won't stop further executions - /// - public interface ILateExecutor - { - Task LateExecute(IGuild guild, IUserMessage msg); - } -} + Task LateExecute(IGuild guild, IUserMessage msg); +} \ No newline at end of file diff --git a/src/NadekoBot/Common/ModuleBehaviors/IModuleBehavior.cs b/src/NadekoBot/Common/ModuleBehaviors/IModuleBehavior.cs index b2623a923..7651a59f7 100644 --- a/src/NadekoBot/Common/ModuleBehaviors/IModuleBehavior.cs +++ b/src/NadekoBot/Common/ModuleBehaviors/IModuleBehavior.cs @@ -2,33 +2,32 @@ using Discord.WebSocket; using System.Threading.Tasks; -namespace NadekoBot.Common.ModuleBehaviors +namespace NadekoBot.Common.ModuleBehaviors; + +public struct ModuleBehaviorResult { - public struct ModuleBehaviorResult + public bool Blocked { get; set; } + public string NewInput { get; set; } + + public static ModuleBehaviorResult None() => new ModuleBehaviorResult { - public bool Blocked { get; set; } - public string NewInput { get; set; } + Blocked = false, + NewInput = null, + }; - public static ModuleBehaviorResult None() => new ModuleBehaviorResult - { - Blocked = false, - NewInput = null, - }; - - public static ModuleBehaviorResult FromBlocked(bool blocked) => new ModuleBehaviorResult - { - Blocked = blocked, - NewInput = null, - }; - } - - public interface IModuleBehavior + public static ModuleBehaviorResult FromBlocked(bool blocked) => new ModuleBehaviorResult { - /// - /// Negative priority means it will try to apply as early as possible - /// Positive priority menas it will try to apply as late as possible - /// - int Priority { get; } - Task ApplyBehavior(DiscordSocketClient client, IGuild guild, IUserMessage msg); - } + Blocked = blocked, + NewInput = null, + }; } + +public interface IModuleBehavior +{ + /// + /// Negative priority means it will try to apply as early as possible + /// Positive priority menas it will try to apply as late as possible + /// + int Priority { get; } + Task ApplyBehavior(DiscordSocketClient client, IGuild guild, IUserMessage msg); +} \ No newline at end of file diff --git a/src/NadekoBot/Common/ModuleBehaviors/IReadyExecutor.cs b/src/NadekoBot/Common/ModuleBehaviors/IReadyExecutor.cs index e6549bb7d..6c05c1f29 100644 --- a/src/NadekoBot/Common/ModuleBehaviors/IReadyExecutor.cs +++ b/src/NadekoBot/Common/ModuleBehaviors/IReadyExecutor.cs @@ -1,16 +1,15 @@ using System.Threading.Tasks; -namespace NadekoBot.Common.ModuleBehaviors +namespace NadekoBot.Common.ModuleBehaviors; + +/// +/// All services which need to execute something after +/// the bot is ready should implement this interface +/// +public interface IReadyExecutor { /// - /// All services which need to execute something after - /// the bot is ready should implement this interface + /// Executed when bot is ready /// - public interface IReadyExecutor - { - /// - /// Executed when bot is ready - /// - public Task OnReadyAsync(); - } + public Task OnReadyAsync(); } \ No newline at end of file diff --git a/src/NadekoBot/Common/NadekoModule.cs b/src/NadekoBot/Common/NadekoModule.cs index 7194eedd0..5c8d56a58 100644 --- a/src/NadekoBot/Common/NadekoModule.cs +++ b/src/NadekoBot/Common/NadekoModule.cs @@ -6,152 +6,151 @@ using NadekoBot.Extensions; using System.Globalization; using System.Threading.Tasks; -namespace NadekoBot.Modules +namespace NadekoBot.Modules; + +public abstract class NadekoModule : ModuleBase { - public abstract class NadekoModule : ModuleBase + protected CultureInfo _cultureInfo { get; set; } + public IBotStrings Strings { get; set; } + public CommandHandler CmdHandler { get; set; } + public ILocalization Localization { get; set; } + public IEmbedBuilderService _eb { get; set; } + + public string Prefix => CmdHandler.GetPrefix(ctx.Guild); + + protected ICommandContext ctx => Context; + + protected NadekoModule() { - protected CultureInfo _cultureInfo { get; set; } - public IBotStrings Strings { get; set; } - public CommandHandler CmdHandler { get; set; } - public ILocalization Localization { get; set; } - public IEmbedBuilderService _eb { get; set; } + } - public string Prefix => CmdHandler.GetPrefix(ctx.Guild); + protected override void BeforeExecute(CommandInfo cmd) + { + _cultureInfo = Localization.GetCultureInfo(ctx.Guild?.Id); + } + + protected string GetText(in LocStr data) => + Strings.GetText(data, _cultureInfo); - protected ICommandContext ctx => Context; + public Task SendErrorAsync(string error) + => ctx.Channel.SendErrorAsync(_eb, error); + + public Task SendErrorAsync(string title, string error, string url = null, string footer = null) + => ctx.Channel.SendErrorAsync(_eb, title, error, url, footer); + + public Task SendConfirmAsync(string text) + => ctx.Channel.SendConfirmAsync(_eb, text); + + public Task SendConfirmAsync(string title, string text, string url = null, string footer = null) + => ctx.Channel.SendConfirmAsync(_eb, title, text, url, footer); + + public Task SendPendingAsync(string text) + => ctx.Channel.SendPendingAsync(_eb, text); + + public Task ErrorLocalizedAsync(LocStr str) + => SendErrorAsync(GetText(str)); - protected NadekoModule() + public Task PendingLocalizedAsync(LocStr str) + => SendPendingAsync(GetText(str)); + + public Task ConfirmLocalizedAsync(LocStr str) + => SendConfirmAsync(GetText(str)); + + public Task ReplyErrorLocalizedAsync(LocStr str) + => SendErrorAsync($"{Format.Bold(ctx.User.ToString())} {GetText(str)}"); + + public Task ReplyPendingLocalizedAsync(LocStr str) + => SendPendingAsync($"{Format.Bold(ctx.User.ToString())} {GetText(str)}"); + + public Task ReplyConfirmLocalizedAsync(LocStr str) + => SendConfirmAsync($"{Format.Bold(ctx.User.ToString())} {GetText(str)}"); + + public async Task PromptUserConfirmAsync(IEmbedBuilder embed) + { + embed + .WithPendingColor() + .WithFooter("yes/no"); + + var msg = await ctx.Channel.EmbedAsync(embed).ConfigureAwait(false); + try { + var input = await GetUserInputAsync(ctx.User.Id, ctx.Channel.Id).ConfigureAwait(false); + input = input?.ToUpperInvariant(); + + if (input != "YES" && input != "Y") + { + return false; + } + + return true; + } + finally + { + var _ = Task.Run(() => msg.DeleteAsync()); + } + } + + // TypeConverter typeConverter = TypeDescriptor.GetConverter(propType); ? + public async Task GetUserInputAsync(ulong userId, ulong channelId) + { + var userInputTask = new TaskCompletionSource(); + var dsc = (DiscordSocketClient)ctx.Client; + try + { + dsc.MessageReceived += MessageReceived; + + if ((await Task.WhenAny(userInputTask.Task, Task.Delay(10000)).ConfigureAwait(false)) != userInputTask.Task) + { + return null; + } + + return await userInputTask.Task.ConfigureAwait(false); + } + finally + { + dsc.MessageReceived -= MessageReceived; } - protected override void BeforeExecute(CommandInfo cmd) + Task MessageReceived(SocketMessage arg) { - _cultureInfo = Localization.GetCultureInfo(ctx.Guild?.Id); - } - - protected string GetText(in LocStr data) => - Strings.GetText(data, _cultureInfo); - - public Task SendErrorAsync(string error) - => ctx.Channel.SendErrorAsync(_eb, error); - - public Task SendErrorAsync(string title, string error, string url = null, string footer = null) - => ctx.Channel.SendErrorAsync(_eb, title, error, url, footer); - - public Task SendConfirmAsync(string text) - => ctx.Channel.SendConfirmAsync(_eb, text); - - public Task SendConfirmAsync(string title, string text, string url = null, string footer = null) - => ctx.Channel.SendConfirmAsync(_eb, title, text, url, footer); - - public Task SendPendingAsync(string text) - => ctx.Channel.SendPendingAsync(_eb, text); - - public Task ErrorLocalizedAsync(LocStr str) - => SendErrorAsync(GetText(str)); - - public Task PendingLocalizedAsync(LocStr str) - => SendPendingAsync(GetText(str)); - - public Task ConfirmLocalizedAsync(LocStr str) - => SendConfirmAsync(GetText(str)); - - public Task ReplyErrorLocalizedAsync(LocStr str) - => SendErrorAsync($"{Format.Bold(ctx.User.ToString())} {GetText(str)}"); - - public Task ReplyPendingLocalizedAsync(LocStr str) - => SendPendingAsync($"{Format.Bold(ctx.User.ToString())} {GetText(str)}"); - - public Task ReplyConfirmLocalizedAsync(LocStr str) - => SendConfirmAsync($"{Format.Bold(ctx.User.ToString())} {GetText(str)}"); - - public async Task PromptUserConfirmAsync(IEmbedBuilder embed) - { - embed - .WithPendingColor() - .WithFooter("yes/no"); - - var msg = await ctx.Channel.EmbedAsync(embed).ConfigureAwait(false); - try + var _ = Task.Run(() => { - var input = await GetUserInputAsync(ctx.User.Id, ctx.Channel.Id).ConfigureAwait(false); - input = input?.ToUpperInvariant(); - - if (input != "YES" && input != "Y") + if (!(arg is SocketUserMessage userMsg) || + !(userMsg.Channel is ITextChannel chan) || + userMsg.Author.Id != userId || + userMsg.Channel.Id != channelId) { - return false; - } - - return true; - } - finally - { - var _ = Task.Run(() => msg.DeleteAsync()); - } - } - - // TypeConverter typeConverter = TypeDescriptor.GetConverter(propType); ? - public async Task GetUserInputAsync(ulong userId, ulong channelId) - { - var userInputTask = new TaskCompletionSource(); - var dsc = (DiscordSocketClient)ctx.Client; - try - { - dsc.MessageReceived += MessageReceived; - - if ((await Task.WhenAny(userInputTask.Task, Task.Delay(10000)).ConfigureAwait(false)) != userInputTask.Task) - { - return null; - } - - return await userInputTask.Task.ConfigureAwait(false); - } - finally - { - dsc.MessageReceived -= MessageReceived; - } - - Task MessageReceived(SocketMessage arg) - { - var _ = Task.Run(() => - { - if (!(arg is SocketUserMessage userMsg) || - !(userMsg.Channel is ITextChannel chan) || - userMsg.Author.Id != userId || - userMsg.Channel.Id != channelId) - { - return Task.CompletedTask; - } - - if (userInputTask.TrySetResult(arg.Content)) - { - userMsg.DeleteAfter(1); - } return Task.CompletedTask; - }); + } + + if (userInputTask.TrySetResult(arg.Content)) + { + userMsg.DeleteAfter(1); + } return Task.CompletedTask; - } + }); + return Task.CompletedTask; } } +} - public abstract class NadekoModule : NadekoModule +public abstract class NadekoModule : NadekoModule +{ + public TService _service { get; set; } + + protected NadekoModule() : base() { - public TService _service { get; set; } - - protected NadekoModule() : base() - { - } } +} - public abstract class NadekoSubmodule : NadekoModule - { - protected NadekoSubmodule() : base() { } - } +public abstract class NadekoSubmodule : NadekoModule +{ + protected NadekoSubmodule() : base() { } +} - public abstract class NadekoSubmodule : NadekoModule +public abstract class NadekoSubmodule : NadekoModule +{ + protected NadekoSubmodule() : base() { - protected NadekoSubmodule() : base() - { - } } } \ No newline at end of file diff --git a/src/NadekoBot/Common/NadekoModuleExtensions.cs b/src/NadekoBot/Common/NadekoModuleExtensions.cs index 6c53169c3..ceba46da0 100644 --- a/src/NadekoBot/Common/NadekoModuleExtensions.cs +++ b/src/NadekoBot/Common/NadekoModuleExtensions.cs @@ -1,7 +1,6 @@ -namespace NadekoBot.Modules +namespace NadekoBot.Modules; + +public static class NadekoModuleExtensions { - public static class NadekoModuleExtensions - { - } -} +} \ No newline at end of file diff --git a/src/NadekoBot/Common/NadekoRandom.cs b/src/NadekoBot/Common/NadekoRandom.cs index 12846964f..505b76605 100644 --- a/src/NadekoBot/Common/NadekoRandom.cs +++ b/src/NadekoBot/Common/NadekoRandom.cs @@ -1,74 +1,72 @@ -using System; -using System.Security.Cryptography; +using System.Security.Cryptography; -namespace NadekoBot.Common +namespace NadekoBot.Common; + +public class NadekoRandom : Random { - public class NadekoRandom : Random + readonly RandomNumberGenerator _rng; + + public NadekoRandom() : base() { - readonly RandomNumberGenerator _rng; - - public NadekoRandom() : base() - { - _rng = RandomNumberGenerator.Create(); - } - - public override int Next() - { - var bytes = new byte[sizeof(int)]; - _rng.GetBytes(bytes); - return Math.Abs(BitConverter.ToInt32(bytes, 0)); - } - - public override int Next(int maxValue) - { - if (maxValue <= 0) - throw new ArgumentOutOfRangeException(nameof(maxValue)); - var bytes = new byte[sizeof(int)]; - _rng.GetBytes(bytes); - return Math.Abs(BitConverter.ToInt32(bytes, 0)) % maxValue; - } - - public override int Next(int minValue, int maxValue) - { - if (minValue > maxValue) - throw new ArgumentOutOfRangeException(nameof(maxValue)); - if (minValue == maxValue) - return minValue; - var bytes = new byte[sizeof(int)]; - _rng.GetBytes(bytes); - var sign = Math.Sign(BitConverter.ToInt32(bytes, 0)); - return (sign * BitConverter.ToInt32(bytes, 0)) % (maxValue - minValue) + minValue; - } - - public long NextLong(long minValue, long maxValue) - { - if (minValue > maxValue) - throw new ArgumentOutOfRangeException(nameof(maxValue)); - if (minValue == maxValue) - return minValue; - var bytes = new byte[sizeof(long)]; - _rng.GetBytes(bytes); - var sign = Math.Sign(BitConverter.ToInt64(bytes, 0)); - return (sign * BitConverter.ToInt64(bytes, 0)) % (maxValue - minValue) + minValue; - } - - public override void NextBytes(byte[] buffer) - { - _rng.GetBytes(buffer); - } - - protected override double Sample() - { - var bytes = new byte[sizeof(double)]; - _rng.GetBytes(bytes); - return Math.Abs(BitConverter.ToDouble(bytes, 0) / double.MaxValue + 1); - } - - public override double NextDouble() - { - var bytes = new byte[sizeof(double)]; - _rng.GetBytes(bytes); - return BitConverter.ToDouble(bytes, 0); - } + _rng = RandomNumberGenerator.Create(); } -} + + public override int Next() + { + var bytes = new byte[sizeof(int)]; + _rng.GetBytes(bytes); + return Math.Abs(BitConverter.ToInt32(bytes, 0)); + } + + public override int Next(int maxValue) + { + if (maxValue <= 0) + throw new ArgumentOutOfRangeException(nameof(maxValue)); + var bytes = new byte[sizeof(int)]; + _rng.GetBytes(bytes); + return Math.Abs(BitConverter.ToInt32(bytes, 0)) % maxValue; + } + + public override int Next(int minValue, int maxValue) + { + if (minValue > maxValue) + throw new ArgumentOutOfRangeException(nameof(maxValue)); + if (minValue == maxValue) + return minValue; + var bytes = new byte[sizeof(int)]; + _rng.GetBytes(bytes); + var sign = Math.Sign(BitConverter.ToInt32(bytes, 0)); + return (sign * BitConverter.ToInt32(bytes, 0)) % (maxValue - minValue) + minValue; + } + + public long NextLong(long minValue, long maxValue) + { + if (minValue > maxValue) + throw new ArgumentOutOfRangeException(nameof(maxValue)); + if (minValue == maxValue) + return minValue; + var bytes = new byte[sizeof(long)]; + _rng.GetBytes(bytes); + var sign = Math.Sign(BitConverter.ToInt64(bytes, 0)); + return (sign * BitConverter.ToInt64(bytes, 0)) % (maxValue - minValue) + minValue; + } + + public override void NextBytes(byte[] buffer) + { + _rng.GetBytes(buffer); + } + + protected override double Sample() + { + var bytes = new byte[sizeof(double)]; + _rng.GetBytes(bytes); + return Math.Abs(BitConverter.ToDouble(bytes, 0) / double.MaxValue + 1); + } + + public override double NextDouble() + { + var bytes = new byte[sizeof(double)]; + _rng.GetBytes(bytes); + return BitConverter.ToDouble(bytes, 0); + } +} \ No newline at end of file diff --git a/src/NadekoBot/Common/NoPublicBotPrecondition.cs b/src/NadekoBot/Common/NoPublicBotPrecondition.cs index f17f885a1..bd479f5fc 100644 --- a/src/NadekoBot/Common/NoPublicBotPrecondition.cs +++ b/src/NadekoBot/Common/NoPublicBotPrecondition.cs @@ -1,19 +1,17 @@ -using System; -using System.Threading.Tasks; +using System.Threading.Tasks; using Discord.Commands; -namespace NadekoBot.Common +namespace NadekoBot.Common; + +[AttributeUsage(AttributeTargets.Method | AttributeTargets.Class)] +public sealed class NoPublicBotAttribute : PreconditionAttribute { - [AttributeUsage(AttributeTargets.Method | AttributeTargets.Class)] - public sealed class NoPublicBotAttribute : PreconditionAttribute + public override Task CheckPermissionsAsync(ICommandContext context, CommandInfo command, IServiceProvider services) { - public override Task CheckPermissionsAsync(ICommandContext context, CommandInfo command, IServiceProvider services) - { #if GLOBAL_NADEKO return Task.FromResult(PreconditionResult.FromError("Not available on the public bot. To learn how to selfhost a private bot, click [here](https://nadekobot.readthedocs.io/en/latest/).")); #else - return Task.FromResult(PreconditionResult.FromSuccess()); + return Task.FromResult(PreconditionResult.FromSuccess()); #endif - } } -} +} \ No newline at end of file diff --git a/src/NadekoBot/Common/OldImageUrls.cs b/src/NadekoBot/Common/OldImageUrls.cs index b98224df5..58f52319b 100644 --- a/src/NadekoBot/Common/OldImageUrls.cs +++ b/src/NadekoBot/Common/OldImageUrls.cs @@ -1,49 +1,46 @@ -using System; +namespace NadekoBot.Common; -namespace NadekoBot.Common +public class OldImageUrls { - public class OldImageUrls + public int Version { get; set; } = 2; + + public CoinData Coins { get; set; } + public Uri[] Currency { get; set; } + public Uri[] Dice { get; set; } + public RategirlData Rategirl { get; set; } + public XpData Xp { get; set; } + + //new + public RipData Rip { get; set; } + public SlotData Slots { get; set; } + + public class RipData { - public int Version { get; set; } = 2; + public Uri Bg { get; set; } + public Uri Overlay { get; set; } + } - public CoinData Coins { get; set; } - public Uri[] Currency { get; set; } - public Uri[] Dice { get; set; } - public RategirlData Rategirl { get; set; } - public XpData Xp { get; set; } + public class SlotData + { + public Uri[] Emojis { get; set; } + public Uri[] Numbers { get; set; } + public Uri Bg { get; set; } + } - //new - public RipData Rip { get; set; } - public SlotData Slots { get; set; } + public class CoinData + { + public Uri[] Heads { get; set; } + public Uri[] Tails { get; set; } + } - public class RipData - { - public Uri Bg { get; set; } - public Uri Overlay { get; set; } - } + public class RategirlData + { + public Uri Matrix { get; set; } + public Uri Dot { get; set; } + } - public class SlotData - { - public Uri[] Emojis { get; set; } - public Uri[] Numbers { get; set; } - public Uri Bg { get; set; } - } - - public class CoinData - { - public Uri[] Heads { get; set; } - public Uri[] Tails { get; set; } - } - - public class RategirlData - { - public Uri Matrix { get; set; } - public Uri Dot { get; set; } - } - - public class XpData - { - public Uri Bg { get; set; } - } + public class XpData + { + public Uri Bg { get; set; } } } \ No newline at end of file diff --git a/src/NadekoBot/Common/OptionsParser.cs b/src/NadekoBot/Common/OptionsParser.cs index 0f4aeea10..dd18e88da 100644 --- a/src/NadekoBot/Common/OptionsParser.cs +++ b/src/NadekoBot/Common/OptionsParser.cs @@ -1,24 +1,23 @@ using CommandLine; -namespace NadekoBot.Common -{ - public static class OptionsParser - { - public static T ParseFrom(string[] args) where T : INadekoCommandOptions, new() - => ParseFrom(new T(), args).Item1; +namespace NadekoBot.Common; - public static (T, bool) ParseFrom(T options, string[] args) where T : INadekoCommandOptions +public static class OptionsParser +{ + public static T ParseFrom(string[] args) where T : INadekoCommandOptions, new() + => ParseFrom(new T(), args).Item1; + + public static (T, bool) ParseFrom(T options, string[] args) where T : INadekoCommandOptions + { + using (var p = new Parser(x => + { + x.HelpWriter = null; + })) { - using (var p = new Parser(x => - { - x.HelpWriter = null; - })) - { - var res = p.ParseArguments(args); - options = res.MapResult(x => x, x => options); - options.NormalizeOptions(); - return (options, res.Tag == ParserResultType.Parsed); - } + var res = p.ParseArguments(args); + options = res.MapResult(x => x, x => options); + options.NormalizeOptions(); + return (options, res.Tag == ParserResultType.Parsed); } } -} +} \ No newline at end of file diff --git a/src/NadekoBot/Common/OsuMapData.cs b/src/NadekoBot/Common/OsuMapData.cs index 9d11614f3..8ea5aff34 100644 --- a/src/NadekoBot/Common/OsuMapData.cs +++ b/src/NadekoBot/Common/OsuMapData.cs @@ -1,9 +1,8 @@ -namespace NadekoBot.Common +namespace NadekoBot.Common; + +public class OsuMapData { - public class OsuMapData - { - public string Title { get; set; } - public string Artist { get; set; } - public string Version { get; set; } - } + public string Title { get; set; } + public string Artist { get; set; } + public string Version { get; set; } } \ No newline at end of file diff --git a/src/NadekoBot/Common/OsuUserBets.cs b/src/NadekoBot/Common/OsuUserBets.cs index f4e07a7ca..9954848a0 100644 --- a/src/NadekoBot/Common/OsuUserBets.cs +++ b/src/NadekoBot/Common/OsuUserBets.cs @@ -1,41 +1,40 @@ using Newtonsoft.Json; -namespace NadekoBot.Common +namespace NadekoBot.Common; + +public class OsuUserBests { - public class OsuUserBests - { - [JsonProperty("beatmap_id")] public string BeatmapId { get; set; } + [JsonProperty("beatmap_id")] public string BeatmapId { get; set; } - [JsonProperty("score_id")] public string ScoreId { get; set; } + [JsonProperty("score_id")] public string ScoreId { get; set; } - [JsonProperty("score")] public string Score { get; set; } + [JsonProperty("score")] public string Score { get; set; } - [JsonProperty("maxcombo")] public string Maxcombo { get; set; } + [JsonProperty("maxcombo")] public string Maxcombo { get; set; } - [JsonProperty("count50")] public double Count50 { get; set; } + [JsonProperty("count50")] public double Count50 { get; set; } - [JsonProperty("count100")] public double Count100 { get; set; } + [JsonProperty("count100")] public double Count100 { get; set; } - [JsonProperty("count300")] public double Count300 { get; set; } + [JsonProperty("count300")] public double Count300 { get; set; } - [JsonProperty("countmiss")] public int Countmiss { get; set; } + [JsonProperty("countmiss")] public int Countmiss { get; set; } - [JsonProperty("countkatu")] public double Countkatu { get; set; } + [JsonProperty("countkatu")] public double Countkatu { get; set; } - [JsonProperty("countgeki")] public double Countgeki { get; set; } + [JsonProperty("countgeki")] public double Countgeki { get; set; } - [JsonProperty("perfect")] public string Perfect { get; set; } + [JsonProperty("perfect")] public string Perfect { get; set; } - [JsonProperty("enabled_mods")] public int EnabledMods { get; set; } + [JsonProperty("enabled_mods")] public int EnabledMods { get; set; } - [JsonProperty("user_id")] public string UserId { get; set; } + [JsonProperty("user_id")] public string UserId { get; set; } - [JsonProperty("date")] public string Date { get; set; } + [JsonProperty("date")] public string Date { get; set; } - [JsonProperty("rank")] public string Rank { get; set; } + [JsonProperty("rank")] public string Rank { get; set; } - [JsonProperty("pp")] public double Pp { get; set; } + [JsonProperty("pp")] public double Pp { get; set; } - [JsonProperty("replay_available")] public string ReplayAvailable { get; set; } - } + [JsonProperty("replay_available")] public string ReplayAvailable { get; set; } } \ No newline at end of file diff --git a/src/NadekoBot/Common/PlatformHelper.cs b/src/NadekoBot/Common/PlatformHelper.cs index a8a53b0af..51dadda29 100644 --- a/src/NadekoBot/Common/PlatformHelper.cs +++ b/src/NadekoBot/Common/PlatformHelper.cs @@ -1,25 +1,22 @@ -using System; +namespace NadekoBot.Common; -namespace NadekoBot.Common +public static class PlatformHelper { - public static class PlatformHelper - { - private const int ProcessorCountRefreshIntervalMs = 30000; + private const int ProcessorCountRefreshIntervalMs = 30000; - private static volatile int _processorCount; - private static volatile int _lastProcessorCountRefreshTicks; + private static volatile int _processorCount; + private static volatile int _lastProcessorCountRefreshTicks; - public static int ProcessorCount { - get { - var now = Environment.TickCount; - if (_processorCount == 0 || (now - _lastProcessorCountRefreshTicks) >= ProcessorCountRefreshIntervalMs) - { - _processorCount = Environment.ProcessorCount; - _lastProcessorCountRefreshTicks = now; - } - - return _processorCount; + public static int ProcessorCount { + get { + var now = Environment.TickCount; + if (_processorCount == 0 || (now - _lastProcessorCountRefreshTicks) >= ProcessorCountRefreshIntervalMs) + { + _processorCount = Environment.ProcessorCount; + _lastProcessorCountRefreshTicks = now; } + + return _processorCount; } } } \ No newline at end of file diff --git a/src/NadekoBot/Common/Pokemon/PokemonNameId.cs b/src/NadekoBot/Common/Pokemon/PokemonNameId.cs index 44b4fd16b..58c3c71d6 100644 --- a/src/NadekoBot/Common/Pokemon/PokemonNameId.cs +++ b/src/NadekoBot/Common/Pokemon/PokemonNameId.cs @@ -1,8 +1,7 @@ -namespace NadekoBot.Common.Pokemon +namespace NadekoBot.Common.Pokemon; + +public class PokemonNameId { - public class PokemonNameId - { - public int Id { get; set; } - public string Name { get; set; } - } -} + public int Id { get; set; } + public string Name { get; set; } +} \ No newline at end of file diff --git a/src/NadekoBot/Common/Pokemon/SearchPokemon.cs b/src/NadekoBot/Common/Pokemon/SearchPokemon.cs index 859f914ee..ba57cabc0 100644 --- a/src/NadekoBot/Common/Pokemon/SearchPokemon.cs +++ b/src/NadekoBot/Common/Pokemon/SearchPokemon.cs @@ -1,40 +1,38 @@ using Newtonsoft.Json; -using System.Collections.Generic; -namespace NadekoBot.Common.Pokemon +namespace NadekoBot.Common.Pokemon; + +public class SearchPokemon { - public class SearchPokemon + public class GenderRatioClass { - public class GenderRatioClass - { - public float M { get; set; } - public float F { get; set; } - } - - public class BaseStatsClass - { - public int HP { get; set; } - public int ATK { get; set; } - public int DEF { get; set; } - public int SPA { get; set; } - public int SPD { get; set; } - public int SPE { get; set; } - - public override string ToString() => $@"💚**HP:** {HP,-4} ⚔**ATK:** {ATK,-4} 🛡**DEF:** {DEF,-4} -✨**SPA:** {SPA,-4} 🎇**SPD:** {SPD,-4} 💨**SPE:** {SPE,-4}"; - } - - [JsonProperty("num")] - public int Id { get; set; } - public string Species { get; set; } - public string[] Types { get; set; } - public GenderRatioClass GenderRatio { get; set; } - public BaseStatsClass BaseStats { get; set; } - public Dictionary Abilities { get; set; } - public float HeightM { get; set; } - public float WeightKg { get; set; } - public string Color { get; set; } - public string[] Evos { get; set; } - public string[] EggGroups { get; set; } + public float M { get; set; } + public float F { get; set; } } -} + + public class BaseStatsClass + { + public int HP { get; set; } + public int ATK { get; set; } + public int DEF { get; set; } + public int SPA { get; set; } + public int SPD { get; set; } + public int SPE { get; set; } + + public override string ToString() => $@"💚**HP:** {HP,-4} ⚔**ATK:** {ATK,-4} 🛡**DEF:** {DEF,-4} +✨**SPA:** {SPA,-4} 🎇**SPD:** {SPD,-4} 💨**SPE:** {SPE,-4}"; + } + + [JsonProperty("num")] + public int Id { get; set; } + public string Species { get; set; } + public string[] Types { get; set; } + public GenderRatioClass GenderRatio { get; set; } + public BaseStatsClass BaseStats { get; set; } + public Dictionary Abilities { get; set; } + public float HeightM { get; set; } + public float WeightKg { get; set; } + public string Color { get; set; } + public string[] Evos { get; set; } + public string[] EggGroups { get; set; } +} \ No newline at end of file diff --git a/src/NadekoBot/Common/Pokemon/SearchPokemonAbility.cs b/src/NadekoBot/Common/Pokemon/SearchPokemonAbility.cs index 9a2b6f1c7..ca48ff230 100644 --- a/src/NadekoBot/Common/Pokemon/SearchPokemonAbility.cs +++ b/src/NadekoBot/Common/Pokemon/SearchPokemonAbility.cs @@ -1,10 +1,9 @@ -namespace NadekoBot.Common.Pokemon +namespace NadekoBot.Common.Pokemon; + +public class SearchPokemonAbility { - public class SearchPokemonAbility - { - public string Desc { get; set; } - public string ShortDesc { get; set; } - public string Name { get; set; } - public float Rating { get; set; } - } -} + public string Desc { get; set; } + public string ShortDesc { get; set; } + public string Name { get; set; } + public float Rating { get; set; } +} \ No newline at end of file diff --git a/src/NadekoBot/Common/PubSub/EventPubSub.cs b/src/NadekoBot/Common/PubSub/EventPubSub.cs index b4b724f97..30bf6bc6d 100644 --- a/src/NadekoBot/Common/PubSub/EventPubSub.cs +++ b/src/NadekoBot/Common/PubSub/EventPubSub.cs @@ -1,95 +1,90 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; +using System.Threading.Tasks; -namespace NadekoBot.Common +namespace NadekoBot.Common; + +public class EventPubSub : IPubSub { - public class EventPubSub : IPubSub + private readonly Dictionary>>> _actions + = new Dictionary>>>(); + private readonly object locker = new object(); + + public Task Sub(in TypedKey key, Func action) { - private readonly Dictionary>>> _actions - = new Dictionary>>>(); - private readonly object locker = new object(); - - public Task Sub(in TypedKey key, Func action) + Func localAction = obj => action((TData) obj); + lock(locker) { - Func localAction = obj => action((TData) obj); - lock(locker) + Dictionary>> keyActions; + if (!_actions.TryGetValue(key.Key, out keyActions)) { - Dictionary>> keyActions; - if (!_actions.TryGetValue(key.Key, out keyActions)) - { - keyActions = new Dictionary>>(); - _actions[key.Key] = keyActions; - } - - List> sameActions; - if (!keyActions.TryGetValue(action, out sameActions)) - { - sameActions = new List>(); - keyActions[action] = sameActions; - } - - sameActions.Add(localAction); - - return Task.CompletedTask; + keyActions = new Dictionary>>(); + _actions[key.Key] = keyActions; } - } - - public Task Pub(in TypedKey key, TData data) - { - lock (locker) - { - if(_actions.TryGetValue(key.Key, out var actions)) - { - // if this class ever gets used, this needs to be properly implemented - // 1. ignore all valuetasks which are completed - // 2. return task.whenall all other tasks - return Task.WhenAll(actions - .SelectMany(kvp => kvp.Value) - .Select(action => action(data).AsTask())); - } - return Task.CompletedTask; + List> sameActions; + if (!keyActions.TryGetValue(action, out sameActions)) + { + sameActions = new List>(); + keyActions[action] = sameActions; } - } - public Task Unsub(in TypedKey key, Func action) + sameActions.Add(localAction); + + return Task.CompletedTask; + } + } + + public Task Pub(in TypedKey key, TData data) + { + lock (locker) { - lock (locker) + if(_actions.TryGetValue(key.Key, out var actions)) { - // get subscriptions for this action - if (_actions.TryGetValue(key.Key, out var actions)) + // if this class ever gets used, this needs to be properly implemented + // 1. ignore all valuetasks which are completed + // 2. return task.whenall all other tasks + return Task.WhenAll(actions + .SelectMany(kvp => kvp.Value) + .Select(action => action(data).AsTask())); + } + + return Task.CompletedTask; + } + } + + public Task Unsub(in TypedKey key, Func action) + { + lock (locker) + { + // get subscriptions for this action + if (_actions.TryGetValue(key.Key, out var actions)) + { + var hashCode = action.GetHashCode(); + // get subscriptions which have the same action hash code + // note: having this as a list allows for multiple subscriptions of + // the same insance's/static method + if (actions.TryGetValue(action, out var sameActions)) { - var hashCode = action.GetHashCode(); - // get subscriptions which have the same action hash code - // note: having this as a list allows for multiple subscriptions of - // the same insance's/static method - if (actions.TryGetValue(action, out var sameActions)) - { - // remove last subscription - sameActions.RemoveAt(sameActions.Count - 1); + // remove last subscription + sameActions.RemoveAt(sameActions.Count - 1); - // if the last subscription was the only subscription - // we can safely remove this action's dictionary entry - if (sameActions.Count == 0) - { - actions.Remove(action); + // if the last subscription was the only subscription + // we can safely remove this action's dictionary entry + if (sameActions.Count == 0) + { + actions.Remove(action); - // if our dictionary has no more elements after - // removing the entry - // it's safe to remove it from the key's subscriptions - if (actions.Count == 0) - { - _actions.Remove(key.Key); - } + // if our dictionary has no more elements after + // removing the entry + // it's safe to remove it from the key's subscriptions + if (actions.Count == 0) + { + _actions.Remove(key.Key); } } } - - return Task.CompletedTask; } + + return Task.CompletedTask; } } - } \ No newline at end of file diff --git a/src/NadekoBot/Common/PubSub/IPubSub.cs b/src/NadekoBot/Common/PubSub/IPubSub.cs index 496317cc1..cbdba3fcd 100644 --- a/src/NadekoBot/Common/PubSub/IPubSub.cs +++ b/src/NadekoBot/Common/PubSub/IPubSub.cs @@ -1,11 +1,9 @@ -using System; -using System.Threading.Tasks; +using System.Threading.Tasks; -namespace NadekoBot.Common +namespace NadekoBot.Common; + +public interface IPubSub { - public interface IPubSub - { - public Task Pub(in TypedKey key, TData data); - public Task Sub(in TypedKey key, Func action); - } + public Task Pub(in TypedKey key, TData data); + public Task Sub(in TypedKey key, Func action); } \ No newline at end of file diff --git a/src/NadekoBot/Common/PubSub/ISeria.cs b/src/NadekoBot/Common/PubSub/ISeria.cs index 964784dbf..5e48295b3 100644 --- a/src/NadekoBot/Common/PubSub/ISeria.cs +++ b/src/NadekoBot/Common/PubSub/ISeria.cs @@ -1,8 +1,7 @@ -namespace NadekoBot.Common +namespace NadekoBot.Common; + +public interface ISeria { - public interface ISeria - { - byte[] Serialize(T data); - T Deserialize(byte[] data); - } + byte[] Serialize(T data); + T Deserialize(byte[] data); } \ No newline at end of file diff --git a/src/NadekoBot/Common/PubSub/JsonSeria.cs b/src/NadekoBot/Common/PubSub/JsonSeria.cs index de39211d1..a1273023c 100644 --- a/src/NadekoBot/Common/PubSub/JsonSeria.cs +++ b/src/NadekoBot/Common/PubSub/JsonSeria.cs @@ -1,28 +1,27 @@ using System.Text.Json; using NadekoBot.Common.JsonConverters; -namespace NadekoBot.Common -{ - public class JsonSeria : ISeria - { - private JsonSerializerOptions serializerOptions = new JsonSerializerOptions() - { - Converters = - { - new Rgba32Converter(), - new CultureInfoConverter(), - } - }; - public byte[] Serialize(T data) - => JsonSerializer.SerializeToUtf8Bytes(data, serializerOptions); +namespace NadekoBot.Common; - public T Deserialize(byte[] data) +public class JsonSeria : ISeria +{ + private JsonSerializerOptions serializerOptions = new JsonSerializerOptions() + { + Converters = { - if (data is null) - return default; + new Rgba32Converter(), + new CultureInfoConverter(), + } + }; + public byte[] Serialize(T data) + => JsonSerializer.SerializeToUtf8Bytes(data, serializerOptions); + + public T Deserialize(byte[] data) + { + if (data is null) + return default; - return JsonSerializer.Deserialize(data, serializerOptions); - } + return JsonSerializer.Deserialize(data, serializerOptions); } } \ No newline at end of file diff --git a/src/NadekoBot/Common/PubSub/RedisPubSub.cs b/src/NadekoBot/Common/PubSub/RedisPubSub.cs index fb20c20a7..aabe1cef5 100644 --- a/src/NadekoBot/Common/PubSub/RedisPubSub.cs +++ b/src/NadekoBot/Common/PubSub/RedisPubSub.cs @@ -1,46 +1,42 @@ -using System; -using System.Threading.Tasks; -using NadekoBot.Services; +using System.Threading.Tasks; using NadekoBot.Extensions; -using Serilog; using StackExchange.Redis; -namespace NadekoBot.Common +namespace NadekoBot.Common; + +public sealed class RedisPubSub : IPubSub { - public sealed class RedisPubSub : IPubSub + private readonly ConnectionMultiplexer _multi; + private readonly ISeria _serializer; + private readonly IBotCredentials _creds; + + public RedisPubSub(ConnectionMultiplexer multi, ISeria serializer, IBotCredentials creds) { - private readonly ConnectionMultiplexer _multi; - private readonly ISeria _serializer; - private readonly IBotCredentials _creds; + _multi = multi; + _serializer = serializer; + _creds = creds; + } - public RedisPubSub(ConnectionMultiplexer multi, ISeria serializer, IBotCredentials creds) - { - _multi = multi; - _serializer = serializer; - _creds = creds; - } + public Task Pub(in TypedKey key, TData data) + { + var serialized = _serializer.Serialize(data); + return _multi.GetSubscriber().PublishAsync($"{_creds.RedisKey()}:{key.Key}", serialized, CommandFlags.FireAndForget); + } - public Task Pub(in TypedKey key, TData data) + public Task Sub(in TypedKey key, Func action) + { + var eventName = key.Key; + return _multi.GetSubscriber().SubscribeAsync($"{_creds.RedisKey()}:{eventName}", async (ch, data) => { - var serialized = _serializer.Serialize(data); - return _multi.GetSubscriber().PublishAsync($"{_creds.RedisKey()}:{key.Key}", serialized, CommandFlags.FireAndForget); - } - - public Task Sub(in TypedKey key, Func action) - { - var eventName = key.Key; - return _multi.GetSubscriber().SubscribeAsync($"{_creds.RedisKey()}:{eventName}", async (ch, data) => + try { - try - { - var dataObj = _serializer.Deserialize(data); - await action(dataObj); - } - catch (Exception ex) - { - Log.Error($"Error handling the event {eventName}: {ex.Message}"); - } - }); - } + var dataObj = _serializer.Deserialize(data); + await action(dataObj); + } + catch (Exception ex) + { + Log.Error($"Error handling the event {eventName}: {ex.Message}"); + } + }); } } \ No newline at end of file diff --git a/src/NadekoBot/Common/PubSub/TypedKey.cs b/src/NadekoBot/Common/PubSub/TypedKey.cs index 15746bb3a..6e768c0ad 100644 --- a/src/NadekoBot/Common/PubSub/TypedKey.cs +++ b/src/NadekoBot/Common/PubSub/TypedKey.cs @@ -1,29 +1,28 @@ -namespace NadekoBot.Common +namespace NadekoBot.Common; + +public readonly struct TypedKey { - public readonly struct TypedKey + public readonly string Key; + + public TypedKey(in string key) { - public readonly string Key; - - public TypedKey(in string key) - { - Key = key; - } - - public static implicit operator TypedKey(in string input) - => new TypedKey(input); - public static implicit operator string(in TypedKey input) - => input.Key; - - public static bool operator ==(in TypedKey left, in TypedKey right) - => left.Key == right.Key; - public static bool operator !=(in TypedKey left, in TypedKey right) - => !(left == right); - - public override bool Equals(object obj) - => obj is TypedKey o && o == this; - - public override int GetHashCode() => Key?.GetHashCode() ?? 0; - - public override string ToString() => Key; + Key = key; } + + public static implicit operator TypedKey(in string input) + => new TypedKey(input); + public static implicit operator string(in TypedKey input) + => input.Key; + + public static bool operator ==(in TypedKey left, in TypedKey right) + => left.Key == right.Key; + public static bool operator !=(in TypedKey left, in TypedKey right) + => !(left == right); + + public override bool Equals(object obj) + => obj is TypedKey o && o == this; + + public override int GetHashCode() => Key?.GetHashCode() ?? 0; + + public override string ToString() => Key; } \ No newline at end of file diff --git a/src/NadekoBot/Common/PubSub/YamlSeria.cs b/src/NadekoBot/Common/PubSub/YamlSeria.cs index 031f1fdcc..0a66754a3 100644 --- a/src/NadekoBot/Common/PubSub/YamlSeria.cs +++ b/src/NadekoBot/Common/PubSub/YamlSeria.cs @@ -3,36 +3,35 @@ using NadekoBot.Common.Yml; using NadekoBot.Common.Configs; using YamlDotNet.Serialization; -namespace NadekoBot.Common +namespace NadekoBot.Common; + +public class YamlSeria : IConfigSeria { - public class YamlSeria : IConfigSeria + private readonly ISerializer _serializer; + private readonly IDeserializer _deserializer; + + private static readonly Regex CodePointRegex + = new Regex(@"(\\U(?[a-zA-Z0-9]{8})|\\u(?[a-zA-Z0-9]{4})|\\x(?[a-zA-Z0-9]{2}))", + RegexOptions.Compiled); + + public YamlSeria() { - private readonly ISerializer _serializer; - private readonly IDeserializer _deserializer; - - private static readonly Regex CodePointRegex - = new Regex(@"(\\U(?[a-zA-Z0-9]{8})|\\u(?[a-zA-Z0-9]{4})|\\x(?[a-zA-Z0-9]{2}))", - RegexOptions.Compiled); - - public YamlSeria() - { - _serializer = Yaml.Serializer; - _deserializer = Yaml.Deserializer; - } - - public string Serialize(T obj) - { - var escapedOutput = _serializer.Serialize(obj); - var output = CodePointRegex.Replace(escapedOutput, me => - { - var str = me.Groups["code"].Value; - var newString = YamlHelper.UnescapeUnicodeCodePoint(str); - return newString; - }); - return output; - } - - public T Deserialize(string data) - => _deserializer.Deserialize(data); + _serializer = Yaml.Serializer; + _deserializer = Yaml.Deserializer; } + + public string Serialize(T obj) + { + var escapedOutput = _serializer.Serialize(obj); + var output = CodePointRegex.Replace(escapedOutput, me => + { + var str = me.Groups["code"].Value; + var newString = YamlHelper.UnescapeUnicodeCodePoint(str); + return newString; + }); + return output; + } + + public T Deserialize(string data) + => _deserializer.Deserialize(data); } \ No newline at end of file diff --git a/src/NadekoBot/Common/Replacements/ReplacementBuilder.cs b/src/NadekoBot/Common/Replacements/ReplacementBuilder.cs index 1be8d25cd..a58c90ba8 100644 --- a/src/NadekoBot/Common/Replacements/ReplacementBuilder.cs +++ b/src/NadekoBot/Common/Replacements/ReplacementBuilder.cs @@ -3,235 +3,229 @@ using Discord.Commands; using Discord.WebSocket; using NadekoBot.Extensions; using NadekoBot.Modules.Administration.Services; -using NadekoBot.Modules.Music.Services; -using System; using System.Collections.Concurrent; -using System.Collections.Generic; -using System.Linq; using System.Text.RegularExpressions; -using NadekoBot.Common; -namespace NadekoBot.Common.Replacements +namespace NadekoBot.Common.Replacements; + +public class ReplacementBuilder { - public class ReplacementBuilder + private static readonly Regex rngRegex = new Regex("%rng(?:(?(?:-)?\\d+)-(?(?:-)?\\d+))?%", RegexOptions.Compiled); + private ConcurrentDictionary> _reps = new ConcurrentDictionary>(); + private ConcurrentDictionary> _regex = new ConcurrentDictionary>(); + + public ReplacementBuilder() { - private static readonly Regex rngRegex = new Regex("%rng(?:(?(?:-)?\\d+)-(?(?:-)?\\d+))?%", RegexOptions.Compiled); - private ConcurrentDictionary> _reps = new ConcurrentDictionary>(); - private ConcurrentDictionary> _regex = new ConcurrentDictionary>(); + WithRngRegex(); + } - public ReplacementBuilder() + public ReplacementBuilder WithDefault(IUser usr, IMessageChannel ch, SocketGuild g, DiscordSocketClient client) + { + return this.WithUser(usr) + .WithChannel(ch) + .WithServer(client, g) + .WithClient(client); + } + + public ReplacementBuilder WithDefault(ICommandContext ctx) => + WithDefault(ctx.User, ctx.Channel, ctx.Guild as SocketGuild, (DiscordSocketClient)ctx.Client); + + public ReplacementBuilder WithMention(DiscordSocketClient client) + { + /*OBSOLETE*/ + _reps.TryAdd("%mention%", () => $"<@{client.CurrentUser.Id}>"); + /*NEW*/ + _reps.TryAdd("%bot.mention%", () => client.CurrentUser.Mention); + return this; + } + + public ReplacementBuilder WithClient(DiscordSocketClient client) + { + WithMention(client); + + /*OBSOLETE*/ + _reps.TryAdd("%shardid%", () => client.ShardId.ToString()); + _reps.TryAdd("%time%", () => DateTime.Now.ToString("HH:mm " + TimeZoneInfo.Local.StandardName.GetInitials())); + + /*NEW*/ + _reps.TryAdd("%bot.status%", () => client.Status.ToString()); + _reps.TryAdd("%bot.latency%", () => client.Latency.ToString()); + _reps.TryAdd("%bot.name%", () => client.CurrentUser.Username); + _reps.TryAdd("%bot.fullname%", () => client.CurrentUser.ToString()); + _reps.TryAdd("%bot.time%", () => DateTime.Now.ToString("HH:mm " + TimeZoneInfo.Local.StandardName.GetInitials())); + _reps.TryAdd("%bot.discrim%", () => client.CurrentUser.Discriminator); + _reps.TryAdd("%bot.id%", () => client.CurrentUser.Id.ToString()); + _reps.TryAdd("%bot.avatar%", () => client.CurrentUser.RealAvatarUrl()?.ToString()); + + WithStats(client); + return this; + } + + public ReplacementBuilder WithServer(DiscordSocketClient client, SocketGuild g) + { + /*OBSOLETE*/ + _reps.TryAdd("%sid%", () => g is null ? "DM" : g.Id.ToString()); + _reps.TryAdd("%server%", () => g is null ? "DM" : g.Name); + _reps.TryAdd("%members%", () => g != null && g is SocketGuild sg ? sg.MemberCount.ToString() : "?"); + _reps.TryAdd("%server_time%", () => { - WithRngRegex(); - } - - public ReplacementBuilder WithDefault(IUser usr, IMessageChannel ch, SocketGuild g, DiscordSocketClient client) - { - return this.WithUser(usr) - .WithChannel(ch) - .WithServer(client, g) - .WithClient(client); - } - - public ReplacementBuilder WithDefault(ICommandContext ctx) => - WithDefault(ctx.User, ctx.Channel, ctx.Guild as SocketGuild, (DiscordSocketClient)ctx.Client); - - public ReplacementBuilder WithMention(DiscordSocketClient client) - { - /*OBSOLETE*/ - _reps.TryAdd("%mention%", () => $"<@{client.CurrentUser.Id}>"); - /*NEW*/ - _reps.TryAdd("%bot.mention%", () => client.CurrentUser.Mention); - return this; - } - - public ReplacementBuilder WithClient(DiscordSocketClient client) - { - WithMention(client); - - /*OBSOLETE*/ - _reps.TryAdd("%shardid%", () => client.ShardId.ToString()); - _reps.TryAdd("%time%", () => DateTime.Now.ToString("HH:mm " + TimeZoneInfo.Local.StandardName.GetInitials())); - - /*NEW*/ - _reps.TryAdd("%bot.status%", () => client.Status.ToString()); - _reps.TryAdd("%bot.latency%", () => client.Latency.ToString()); - _reps.TryAdd("%bot.name%", () => client.CurrentUser.Username); - _reps.TryAdd("%bot.fullname%", () => client.CurrentUser.ToString()); - _reps.TryAdd("%bot.time%", () => DateTime.Now.ToString("HH:mm " + TimeZoneInfo.Local.StandardName.GetInitials())); - _reps.TryAdd("%bot.discrim%", () => client.CurrentUser.Discriminator); - _reps.TryAdd("%bot.id%", () => client.CurrentUser.Id.ToString()); - _reps.TryAdd("%bot.avatar%", () => client.CurrentUser.RealAvatarUrl()?.ToString()); - - WithStats(client); - return this; - } - - public ReplacementBuilder WithServer(DiscordSocketClient client, SocketGuild g) - { - /*OBSOLETE*/ - _reps.TryAdd("%sid%", () => g is null ? "DM" : g.Id.ToString()); - _reps.TryAdd("%server%", () => g is null ? "DM" : g.Name); - _reps.TryAdd("%members%", () => g != null && g is SocketGuild sg ? sg.MemberCount.ToString() : "?"); - _reps.TryAdd("%server_time%", () => + TimeZoneInfo to = TimeZoneInfo.Local; + if (g != null) { - TimeZoneInfo to = TimeZoneInfo.Local; - if (g != null) - { - if (GuildTimezoneService.AllServices.TryGetValue(client.CurrentUser.Id, out var tz)) - to = tz.GetTimeZoneOrDefault(g.Id) ?? TimeZoneInfo.Local; - } - - return TimeZoneInfo.ConvertTime(DateTime.UtcNow, - TimeZoneInfo.Utc, - to).ToString("HH:mm ") + to.StandardName.GetInitials(); - }); - /*NEW*/ - _reps.TryAdd("%server.id%", () => g is null ? "DM" : g.Id.ToString()); - _reps.TryAdd("%server.name%", () => g is null ? "DM" : g.Name); - _reps.TryAdd("%server.members%", () => g != null && g is SocketGuild sg ? sg.MemberCount.ToString() : "?"); - _reps.TryAdd("%server.boosters%", () => g.PremiumSubscriptionCount.ToString()); - _reps.TryAdd("%server.boost_level%", () => ((int)g.PremiumTier).ToString()); - _reps.TryAdd("%server.time%", () => - { - TimeZoneInfo to = TimeZoneInfo.Local; - if (g != null) - { - if (GuildTimezoneService.AllServices.TryGetValue(client.CurrentUser.Id, out var tz)) - to = tz.GetTimeZoneOrDefault(g.Id) ?? TimeZoneInfo.Local; - } - - return TimeZoneInfo.ConvertTime(DateTime.UtcNow, - TimeZoneInfo.Utc, - to).ToString("HH:mm ") + to.StandardName.GetInitials(); - }); - return this; - } - - public ReplacementBuilder WithChannel(IMessageChannel ch) - { - /*OBSOLETE*/ - _reps.TryAdd("%channel%", () => (ch as ITextChannel)?.Mention ?? "#" + ch.Name); - _reps.TryAdd("%chname%", () => ch.Name); - _reps.TryAdd("%cid%", () => ch?.Id.ToString()); - /*NEW*/ - _reps.TryAdd("%channel.mention%", () => (ch as ITextChannel)?.Mention ?? "#" + ch.Name); - _reps.TryAdd("%channel.name%", () => ch.Name); - _reps.TryAdd("%channel.id%", () => ch.Id.ToString()); - _reps.TryAdd("%channel.created%", () => ch.CreatedAt.ToString("HH:mm dd.MM.yyyy")); - _reps.TryAdd("%channel.nsfw%", () => (ch as ITextChannel)?.IsNsfw.ToString() ?? "-"); - _reps.TryAdd("%channel.topic%", () => (ch as ITextChannel)?.Topic ?? "-"); - return this; - } - - public ReplacementBuilder WithUser(IUser user) - { - // /*OBSOLETE*/ - // _reps.TryAdd("%user%", () => user.Mention); - // _reps.TryAdd("%userfull%", () => user.ToString()); - // _reps.TryAdd("%username%", () => user.Username); - // _reps.TryAdd("%userdiscrim%", () => user.Discriminator); - // _reps.TryAdd("%useravatar%", () => user.RealAvatarUrl()?.ToString()); - // _reps.TryAdd("%id%", () => user.Id.ToString()); - // _reps.TryAdd("%uid%", () => user.Id.ToString()); - // /*NEW*/ - // _reps.TryAdd("%user.mention%", () => user.Mention); - // _reps.TryAdd("%user.fullname%", () => user.ToString()); - // _reps.TryAdd("%user.name%", () => user.Username); - // _reps.TryAdd("%user.discrim%", () => user.Discriminator); - // _reps.TryAdd("%user.avatar%", () => user.RealAvatarUrl()?.ToString()); - // _reps.TryAdd("%user.id%", () => user.Id.ToString()); - // _reps.TryAdd("%user.created_time%", () => user.CreatedAt.ToString("HH:mm")); - // _reps.TryAdd("%user.created_date%", () => user.CreatedAt.ToString("dd.MM.yyyy")); - // _reps.TryAdd("%user.joined_time%", () => (user as IGuildUser)?.JoinedAt?.ToString("HH:mm") ?? "-"); - // _reps.TryAdd("%user.joined_date%", () => (user as IGuildUser)?.JoinedAt?.ToString("dd.MM.yyyy") ?? "-"); - WithManyUsers(new[] {user}); - return this; - } - - public ReplacementBuilder WithManyUsers(IEnumerable users) - { - /*OBSOLETE*/ - _reps.TryAdd("%user%", () => string.Join(" ", users.Select(user => user.Mention))); - _reps.TryAdd("%userfull%", () => string.Join(" ", users.Select(user => user.ToString()))); - _reps.TryAdd("%username%", () => string.Join(" ", users.Select(user => user.Username))); - _reps.TryAdd("%userdiscrim%", () => string.Join(" ", users.Select(user => user.Discriminator))); - _reps.TryAdd("%useravatar%", () => string.Join(" ", users.Select(user => user.RealAvatarUrl()?.ToString()))); - _reps.TryAdd("%id%", () => string.Join(" ", users.Select(user => user.Id.ToString()))); - _reps.TryAdd("%uid%", () => string.Join(" ", users.Select(user => user.Id.ToString()))); - /*NEW*/ - _reps.TryAdd("%user.mention%", () => string.Join(" ", users.Select(user => user.Mention))); - _reps.TryAdd("%user.fullname%", () => string.Join(" ", users.Select(user => user.ToString()))); - _reps.TryAdd("%user.name%", () => string.Join(" ", users.Select(user => user.Username))); - _reps.TryAdd("%user.discrim%", () => string.Join(" ", users.Select(user => user.Discriminator))); - _reps.TryAdd("%user.avatar%", () => string.Join(" ", users.Select(user => user.RealAvatarUrl()?.ToString()))); - _reps.TryAdd("%user.id%", () => string.Join(" ", users.Select(user => user.Id.ToString()))); - _reps.TryAdd("%user.created_time%", () => string.Join(" ", users.Select(user => user.CreatedAt.ToString("HH:mm")))); - _reps.TryAdd("%user.created_date%", () => string.Join(" ", users.Select(user => user.CreatedAt.ToString("dd.MM.yyyy")))); - _reps.TryAdd("%user.joined_time%", () => string.Join(" ", users.Select(user => (user as IGuildUser)?.JoinedAt?.ToString("HH:mm") ?? "-"))); - _reps.TryAdd("%user.joined_date%", () => string.Join(" ", users.Select(user => (user as IGuildUser)?.JoinedAt?.ToString("dd.MM.yyyy") ?? "-"))); - return this; - } - - private ReplacementBuilder WithStats(DiscordSocketClient c) - { - /*OBSOLETE*/ - _reps.TryAdd("%servers%", () => c.Guilds.Count.ToString()); -#if !GLOBAL_NADEKO - _reps.TryAdd("%users%", () => c.Guilds.Sum(g => g.MemberCount).ToString()); -#endif - - /*NEW*/ - _reps.TryAdd("%shard.servercount%", () => c.Guilds.Count.ToString()); -#if !GLOBAL_NADEKO - _reps.TryAdd("%shard.usercount%", () => c.Guilds.Sum(g => g.MemberCount).ToString()); -#endif - _reps.TryAdd("%shard.id%", () => c.ShardId.ToString()); - return this; - } - - public ReplacementBuilder WithRngRegex() - { - var rng = new NadekoRandom(); - _regex.TryAdd(rngRegex, (match) => - { - if (!int.TryParse(match.Groups["from"].ToString(), out var from)) - from = 0; - if (!int.TryParse(match.Groups["to"].ToString(), out var to)) - to = 0; - - if (from == 0 && to == 0) - return rng.Next(0, 11).ToString(); - - if (from >= to) - return string.Empty; - - return rng.Next(from, to + 1).ToString(); - }); - return this; - } - - public ReplacementBuilder WithOverride(string key, Func output) - { - _reps.AddOrUpdate(key, output, delegate { return output; }); - return this; - } - - public Replacer Build() - { - return new Replacer(_reps.Select(x => (x.Key, x.Value)).ToArray(), _regex.Select(x => (x.Key, x.Value)).ToArray()); - } - - public ReplacementBuilder WithProviders(IEnumerable phProviders) - { - foreach (var provider in phProviders) - { - foreach (var ovr in provider.GetPlaceholders()) - { - _reps.TryAdd(ovr.Name, ovr.Func); - } + if (GuildTimezoneService.AllServices.TryGetValue(client.CurrentUser.Id, out var tz)) + to = tz.GetTimeZoneOrDefault(g.Id) ?? TimeZoneInfo.Local; } - return this; - } + return TimeZoneInfo.ConvertTime(DateTime.UtcNow, + TimeZoneInfo.Utc, + to).ToString("HH:mm ") + to.StandardName.GetInitials(); + }); + /*NEW*/ + _reps.TryAdd("%server.id%", () => g is null ? "DM" : g.Id.ToString()); + _reps.TryAdd("%server.name%", () => g is null ? "DM" : g.Name); + _reps.TryAdd("%server.members%", () => g != null && g is SocketGuild sg ? sg.MemberCount.ToString() : "?"); + _reps.TryAdd("%server.boosters%", () => g.PremiumSubscriptionCount.ToString()); + _reps.TryAdd("%server.boost_level%", () => ((int)g.PremiumTier).ToString()); + _reps.TryAdd("%server.time%", () => + { + TimeZoneInfo to = TimeZoneInfo.Local; + if (g != null) + { + if (GuildTimezoneService.AllServices.TryGetValue(client.CurrentUser.Id, out var tz)) + to = tz.GetTimeZoneOrDefault(g.Id) ?? TimeZoneInfo.Local; + } + + return TimeZoneInfo.ConvertTime(DateTime.UtcNow, + TimeZoneInfo.Utc, + to).ToString("HH:mm ") + to.StandardName.GetInitials(); + }); + return this; } -} + + public ReplacementBuilder WithChannel(IMessageChannel ch) + { + /*OBSOLETE*/ + _reps.TryAdd("%channel%", () => (ch as ITextChannel)?.Mention ?? "#" + ch.Name); + _reps.TryAdd("%chname%", () => ch.Name); + _reps.TryAdd("%cid%", () => ch?.Id.ToString()); + /*NEW*/ + _reps.TryAdd("%channel.mention%", () => (ch as ITextChannel)?.Mention ?? "#" + ch.Name); + _reps.TryAdd("%channel.name%", () => ch.Name); + _reps.TryAdd("%channel.id%", () => ch.Id.ToString()); + _reps.TryAdd("%channel.created%", () => ch.CreatedAt.ToString("HH:mm dd.MM.yyyy")); + _reps.TryAdd("%channel.nsfw%", () => (ch as ITextChannel)?.IsNsfw.ToString() ?? "-"); + _reps.TryAdd("%channel.topic%", () => (ch as ITextChannel)?.Topic ?? "-"); + return this; + } + + public ReplacementBuilder WithUser(IUser user) + { + // /*OBSOLETE*/ + // _reps.TryAdd("%user%", () => user.Mention); + // _reps.TryAdd("%userfull%", () => user.ToString()); + // _reps.TryAdd("%username%", () => user.Username); + // _reps.TryAdd("%userdiscrim%", () => user.Discriminator); + // _reps.TryAdd("%useravatar%", () => user.RealAvatarUrl()?.ToString()); + // _reps.TryAdd("%id%", () => user.Id.ToString()); + // _reps.TryAdd("%uid%", () => user.Id.ToString()); + // /*NEW*/ + // _reps.TryAdd("%user.mention%", () => user.Mention); + // _reps.TryAdd("%user.fullname%", () => user.ToString()); + // _reps.TryAdd("%user.name%", () => user.Username); + // _reps.TryAdd("%user.discrim%", () => user.Discriminator); + // _reps.TryAdd("%user.avatar%", () => user.RealAvatarUrl()?.ToString()); + // _reps.TryAdd("%user.id%", () => user.Id.ToString()); + // _reps.TryAdd("%user.created_time%", () => user.CreatedAt.ToString("HH:mm")); + // _reps.TryAdd("%user.created_date%", () => user.CreatedAt.ToString("dd.MM.yyyy")); + // _reps.TryAdd("%user.joined_time%", () => (user as IGuildUser)?.JoinedAt?.ToString("HH:mm") ?? "-"); + // _reps.TryAdd("%user.joined_date%", () => (user as IGuildUser)?.JoinedAt?.ToString("dd.MM.yyyy") ?? "-"); + WithManyUsers(new[] {user}); + return this; + } + + public ReplacementBuilder WithManyUsers(IEnumerable users) + { + /*OBSOLETE*/ + _reps.TryAdd("%user%", () => string.Join(" ", users.Select(user => user.Mention))); + _reps.TryAdd("%userfull%", () => string.Join(" ", users.Select(user => user.ToString()))); + _reps.TryAdd("%username%", () => string.Join(" ", users.Select(user => user.Username))); + _reps.TryAdd("%userdiscrim%", () => string.Join(" ", users.Select(user => user.Discriminator))); + _reps.TryAdd("%useravatar%", () => string.Join(" ", users.Select(user => user.RealAvatarUrl()?.ToString()))); + _reps.TryAdd("%id%", () => string.Join(" ", users.Select(user => user.Id.ToString()))); + _reps.TryAdd("%uid%", () => string.Join(" ", users.Select(user => user.Id.ToString()))); + /*NEW*/ + _reps.TryAdd("%user.mention%", () => string.Join(" ", users.Select(user => user.Mention))); + _reps.TryAdd("%user.fullname%", () => string.Join(" ", users.Select(user => user.ToString()))); + _reps.TryAdd("%user.name%", () => string.Join(" ", users.Select(user => user.Username))); + _reps.TryAdd("%user.discrim%", () => string.Join(" ", users.Select(user => user.Discriminator))); + _reps.TryAdd("%user.avatar%", () => string.Join(" ", users.Select(user => user.RealAvatarUrl()?.ToString()))); + _reps.TryAdd("%user.id%", () => string.Join(" ", users.Select(user => user.Id.ToString()))); + _reps.TryAdd("%user.created_time%", () => string.Join(" ", users.Select(user => user.CreatedAt.ToString("HH:mm")))); + _reps.TryAdd("%user.created_date%", () => string.Join(" ", users.Select(user => user.CreatedAt.ToString("dd.MM.yyyy")))); + _reps.TryAdd("%user.joined_time%", () => string.Join(" ", users.Select(user => (user as IGuildUser)?.JoinedAt?.ToString("HH:mm") ?? "-"))); + _reps.TryAdd("%user.joined_date%", () => string.Join(" ", users.Select(user => (user as IGuildUser)?.JoinedAt?.ToString("dd.MM.yyyy") ?? "-"))); + return this; + } + + private ReplacementBuilder WithStats(DiscordSocketClient c) + { + /*OBSOLETE*/ + _reps.TryAdd("%servers%", () => c.Guilds.Count.ToString()); +#if !GLOBAL_NADEKO + _reps.TryAdd("%users%", () => c.Guilds.Sum(g => g.MemberCount).ToString()); +#endif + + /*NEW*/ + _reps.TryAdd("%shard.servercount%", () => c.Guilds.Count.ToString()); +#if !GLOBAL_NADEKO + _reps.TryAdd("%shard.usercount%", () => c.Guilds.Sum(g => g.MemberCount).ToString()); +#endif + _reps.TryAdd("%shard.id%", () => c.ShardId.ToString()); + return this; + } + + public ReplacementBuilder WithRngRegex() + { + var rng = new NadekoRandom(); + _regex.TryAdd(rngRegex, (match) => + { + if (!int.TryParse(match.Groups["from"].ToString(), out var from)) + from = 0; + if (!int.TryParse(match.Groups["to"].ToString(), out var to)) + to = 0; + + if (from == 0 && to == 0) + return rng.Next(0, 11).ToString(); + + if (from >= to) + return string.Empty; + + return rng.Next(from, to + 1).ToString(); + }); + return this; + } + + public ReplacementBuilder WithOverride(string key, Func output) + { + _reps.AddOrUpdate(key, output, delegate { return output; }); + return this; + } + + public Replacer Build() + { + return new Replacer(_reps.Select(x => (x.Key, x.Value)).ToArray(), _regex.Select(x => (x.Key, x.Value)).ToArray()); + } + + public ReplacementBuilder WithProviders(IEnumerable phProviders) + { + foreach (var provider in phProviders) + { + foreach (var ovr in provider.GetPlaceholders()) + { + _reps.TryAdd(ovr.Name, ovr.Func); + } + } + + return this; + } +} \ No newline at end of file diff --git a/src/NadekoBot/Common/Replacements/Replacer.cs b/src/NadekoBot/Common/Replacements/Replacer.cs index a3e35d4bc..930ac525b 100644 --- a/src/NadekoBot/Common/Replacements/Replacer.cs +++ b/src/NadekoBot/Common/Replacements/Replacer.cs @@ -1,91 +1,88 @@ -using System; -using System.Collections.Generic; -using System.Text.RegularExpressions; +using System.Text.RegularExpressions; -namespace NadekoBot.Common.Replacements +namespace NadekoBot.Common.Replacements; + +public class Replacer { - public class Replacer + private readonly IEnumerable<(string Key, Func Text)> _replacements; + private readonly IEnumerable<(Regex Regex, Func Replacement)> _regex; + + public Replacer(IEnumerable<(string, Func)> replacements, IEnumerable<(Regex, Func)> regex) { - private readonly IEnumerable<(string Key, Func Text)> _replacements; - private readonly IEnumerable<(Regex Regex, Func Replacement)> _regex; - - public Replacer(IEnumerable<(string, Func)> replacements, IEnumerable<(Regex, Func)> regex) - { - _replacements = replacements; - _regex = regex; - } - - public string Replace(string input) - { - if (string.IsNullOrWhiteSpace(input)) - return input; - - foreach (var (Key, Text) in _replacements) - { - if (input.Contains(Key)) - input = input.Replace(Key, Text(), StringComparison.InvariantCulture); - } - - foreach (var item in _regex) - { - input = item.Regex.Replace(input, (m) => item.Replacement(m)); - } - - return input; - } - - public SmartText Replace(SmartText data) - => data switch - { - SmartEmbedText embedData => Replace(embedData), - SmartPlainText plain => Replace(plain), - _ => throw new ArgumentOutOfRangeException(nameof(data), "Unsupported argument type") - }; - - public SmartPlainText Replace(SmartPlainText plainText) - => Replace(plainText.Text); - - public SmartEmbedText Replace(SmartEmbedText embedData) - { - var newEmbedData = new SmartEmbedText(); - newEmbedData.PlainText = Replace(embedData.PlainText); - newEmbedData.Description = Replace(embedData.Description); - newEmbedData.Title = Replace(embedData.Title); - newEmbedData.Thumbnail = Replace(embedData.Thumbnail); - newEmbedData.Image = Replace(embedData.Image); - newEmbedData.Url = Replace(embedData.Url); - if (embedData.Author != null) - { - newEmbedData.Author = new SmartTextEmbedAuthor(); - newEmbedData.Author.Name = Replace(embedData.Author.Name); - newEmbedData.Author.IconUrl = Replace(embedData.Author.IconUrl); - } - - if (embedData.Fields != null) - { - var fields = new List(); - foreach (var f in embedData.Fields) - { - var newF = new SmartTextEmbedField(); - newF.Name = Replace(f.Name); - newF.Value = Replace(f.Value); - newF.Inline = f.Inline; - fields.Add(newF); - } - - newEmbedData.Fields = fields.ToArray(); - } - - if (embedData.Footer != null) - { - newEmbedData.Footer = new SmartTextEmbedFooter(); - newEmbedData.Footer.Text = Replace(embedData.Footer.Text); - newEmbedData.Footer.IconUrl = Replace(embedData.Footer.IconUrl); - } - - newEmbedData.Color = embedData.Color; - - return newEmbedData; - } + _replacements = replacements; + _regex = regex; } -} + + public string Replace(string input) + { + if (string.IsNullOrWhiteSpace(input)) + return input; + + foreach (var (Key, Text) in _replacements) + { + if (input.Contains(Key)) + input = input.Replace(Key, Text(), StringComparison.InvariantCulture); + } + + foreach (var item in _regex) + { + input = item.Regex.Replace(input, (m) => item.Replacement(m)); + } + + return input; + } + + public SmartText Replace(SmartText data) + => data switch + { + SmartEmbedText embedData => Replace(embedData), + SmartPlainText plain => Replace(plain), + _ => throw new ArgumentOutOfRangeException(nameof(data), "Unsupported argument type") + }; + + public SmartPlainText Replace(SmartPlainText plainText) + => Replace(plainText.Text); + + public SmartEmbedText Replace(SmartEmbedText embedData) + { + var newEmbedData = new SmartEmbedText(); + newEmbedData.PlainText = Replace(embedData.PlainText); + newEmbedData.Description = Replace(embedData.Description); + newEmbedData.Title = Replace(embedData.Title); + newEmbedData.Thumbnail = Replace(embedData.Thumbnail); + newEmbedData.Image = Replace(embedData.Image); + newEmbedData.Url = Replace(embedData.Url); + if (embedData.Author != null) + { + newEmbedData.Author = new SmartTextEmbedAuthor(); + newEmbedData.Author.Name = Replace(embedData.Author.Name); + newEmbedData.Author.IconUrl = Replace(embedData.Author.IconUrl); + } + + if (embedData.Fields != null) + { + var fields = new List(); + foreach (var f in embedData.Fields) + { + var newF = new SmartTextEmbedField(); + newF.Name = Replace(f.Name); + newF.Value = Replace(f.Value); + newF.Inline = f.Inline; + fields.Add(newF); + } + + newEmbedData.Fields = fields.ToArray(); + } + + if (embedData.Footer != null) + { + newEmbedData.Footer = new SmartTextEmbedFooter(); + newEmbedData.Footer.Text = Replace(embedData.Footer.Text); + newEmbedData.Footer.IconUrl = Replace(embedData.Footer.IconUrl); + } + + newEmbedData.Color = embedData.Color; + + return newEmbedData; + } +} \ No newline at end of file diff --git a/src/NadekoBot/Common/RequireObjectPropertiesContractResolver.cs b/src/NadekoBot/Common/RequireObjectPropertiesContractResolver.cs index 001e84e01..c79b47404 100644 --- a/src/NadekoBot/Common/RequireObjectPropertiesContractResolver.cs +++ b/src/NadekoBot/Common/RequireObjectPropertiesContractResolver.cs @@ -1,16 +1,14 @@ using Newtonsoft.Json; using Newtonsoft.Json.Serialization; -using System; -namespace NadekoBot.Common +namespace NadekoBot.Common; + +public class RequireObjectPropertiesContractResolver : DefaultContractResolver { - public class RequireObjectPropertiesContractResolver : DefaultContractResolver + protected override JsonObjectContract CreateObjectContract(Type objectType) { - protected override JsonObjectContract CreateObjectContract(Type objectType) - { - var contract = base.CreateObjectContract(objectType); - contract.ItemRequired = Required.DisallowNull; - return contract; - } + var contract = base.CreateObjectContract(objectType); + contract.ItemRequired = Required.DisallowNull; + return contract; } -} +} \ No newline at end of file diff --git a/src/NadekoBot/Common/ShmartNumber.cs b/src/NadekoBot/Common/ShmartNumber.cs index 384b9bc07..0fcedd943 100644 --- a/src/NadekoBot/Common/ShmartNumber.cs +++ b/src/NadekoBot/Common/ShmartNumber.cs @@ -1,63 +1,60 @@ -using System; +namespace NadekoBot.Common; -namespace NadekoBot.Common +public struct ShmartNumber : IEquatable { - public struct ShmartNumber : IEquatable + public long Value { get; } + public string Input { get; } + + public ShmartNumber(long val, string input = null) { - public long Value { get; } - public string Input { get; } - - public ShmartNumber(long val, string input = null) - { - Value = val; - Input = input; - } - - public static implicit operator ShmartNumber(long num) - { - return new ShmartNumber(num); - } - - public static implicit operator long(ShmartNumber num) - { - return num.Value; - } - - public static implicit operator ShmartNumber(int num) - { - return new ShmartNumber(num); - } - - public override string ToString() - { - return Value.ToString(); - } - - public override bool Equals(object obj) - { - return obj is ShmartNumber sn - ? Equals(sn) - : false; - } - - public bool Equals(ShmartNumber other) - { - return other.Value == Value; - } - - public override int GetHashCode() - { - return Value.GetHashCode() ^ Input.GetHashCode(StringComparison.InvariantCulture); - } - - public static bool operator ==(ShmartNumber left, ShmartNumber right) - { - return left.Equals(right); - } - - public static bool operator !=(ShmartNumber left, ShmartNumber right) - { - return !(left == right); - } + Value = val; + Input = input; } -} + + public static implicit operator ShmartNumber(long num) + { + return new ShmartNumber(num); + } + + public static implicit operator long(ShmartNumber num) + { + return num.Value; + } + + public static implicit operator ShmartNumber(int num) + { + return new ShmartNumber(num); + } + + public override string ToString() + { + return Value.ToString(); + } + + public override bool Equals(object obj) + { + return obj is ShmartNumber sn + ? Equals(sn) + : false; + } + + public bool Equals(ShmartNumber other) + { + return other.Value == Value; + } + + public override int GetHashCode() + { + return Value.GetHashCode() ^ Input.GetHashCode(StringComparison.InvariantCulture); + } + + public static bool operator ==(ShmartNumber left, ShmartNumber right) + { + return left.Equals(right); + } + + public static bool operator !=(ShmartNumber left, ShmartNumber right) + { + return !(left == right); + } +} \ No newline at end of file diff --git a/src/NadekoBot/Common/SmartText/SmartEmbedText.cs b/src/NadekoBot/Common/SmartText/SmartEmbedText.cs index 8f7bc92a0..a5c1a712d 100644 --- a/src/NadekoBot/Common/SmartText/SmartEmbedText.cs +++ b/src/NadekoBot/Common/SmartText/SmartEmbedText.cs @@ -1,137 +1,133 @@ -using System; -using System.Linq; -using Discord; +using Discord; using NadekoBot.Extensions; -using NadekoBot.Services; -namespace NadekoBot +namespace NadekoBot; + +public sealed record SmartEmbedText : SmartText { - public sealed record SmartEmbedText : SmartText + public string PlainText { get; set; } + public string Title { get; set; } + public string Description { get; set; } + public string Url { get; set; } + public string Thumbnail { get; set; } + public string Image { get; set; } + + public SmartTextEmbedAuthor Author { get; set; } + public SmartTextEmbedFooter Footer { get; set; } + public SmartTextEmbedField[] Fields { get; set; } + + public uint Color { get; set; } = 7458112; + + public bool IsValid => + !string.IsNullOrWhiteSpace(Title) || + !string.IsNullOrWhiteSpace(Description) || + !string.IsNullOrWhiteSpace(Url) || + !string.IsNullOrWhiteSpace(Thumbnail) || + !string.IsNullOrWhiteSpace(Image) || + (Footer != null && (!string.IsNullOrWhiteSpace(Footer.Text) || !string.IsNullOrWhiteSpace(Footer.IconUrl))) || + (Fields != null && Fields.Length > 0); + + public static SmartEmbedText FromEmbed(IEmbed eb, string plainText = null) { - public string PlainText { get; set; } - public string Title { get; set; } - public string Description { get; set; } - public string Url { get; set; } - public string Thumbnail { get; set; } - public string Image { get; set; } - - public SmartTextEmbedAuthor Author { get; set; } - public SmartTextEmbedFooter Footer { get; set; } - public SmartTextEmbedField[] Fields { get; set; } - - public uint Color { get; set; } = 7458112; - - public bool IsValid => - !string.IsNullOrWhiteSpace(Title) || - !string.IsNullOrWhiteSpace(Description) || - !string.IsNullOrWhiteSpace(Url) || - !string.IsNullOrWhiteSpace(Thumbnail) || - !string.IsNullOrWhiteSpace(Image) || - (Footer != null && (!string.IsNullOrWhiteSpace(Footer.Text) || !string.IsNullOrWhiteSpace(Footer.IconUrl))) || - (Fields != null && Fields.Length > 0); - - public static SmartEmbedText FromEmbed(IEmbed eb, string plainText = null) - { - var set = new SmartEmbedText(); + var set = new SmartEmbedText(); - set.PlainText = plainText; - set.Title = eb.Title; - set.Description = eb.Description; - set.Url = eb.Url; - set.Thumbnail = eb.Thumbnail?.Url; - set.Image = eb.Image?.Url; - set.Author = eb.Author is EmbedAuthor ea - ? new() - { - Name = ea.Name, - Url = ea.Url, - IconUrl = ea.IconUrl - } - : null; - set.Footer = eb.Footer is EmbedFooter ef - ? new() - { - Text = ef.Text, - IconUrl = ef.IconUrl - } - : null; + set.PlainText = plainText; + set.Title = eb.Title; + set.Description = eb.Description; + set.Url = eb.Url; + set.Thumbnail = eb.Thumbnail?.Url; + set.Image = eb.Image?.Url; + set.Author = eb.Author is EmbedAuthor ea + ? new() + { + Name = ea.Name, + Url = ea.Url, + IconUrl = ea.IconUrl + } + : null; + set.Footer = eb.Footer is EmbedFooter ef + ? new() + { + Text = ef.Text, + IconUrl = ef.IconUrl + } + : null; - if (eb.Fields.Length > 0) - set.Fields = eb - .Fields - .Select(field => new SmartTextEmbedField() - { - Inline = field.Inline, - Name = field.Name, - Value = field.Value, - }) - .ToArray(); + if (eb.Fields.Length > 0) + set.Fields = eb + .Fields + .Select(field => new SmartTextEmbedField() + { + Inline = field.Inline, + Name = field.Name, + Value = field.Value, + }) + .ToArray(); - set.Color = eb.Color?.RawValue ?? 0; - return set; - } + set.Color = eb.Color?.RawValue ?? 0; + return set; + } - public EmbedBuilder GetEmbed() + public EmbedBuilder GetEmbed() + { + var embed = new EmbedBuilder() + .WithColor(Color); + + if (!string.IsNullOrWhiteSpace(Title)) + embed.WithTitle(Title); + + if (!string.IsNullOrWhiteSpace(Description)) + embed.WithDescription(Description); + + if (Url != null && Uri.IsWellFormedUriString(Url, UriKind.Absolute)) + embed.WithUrl(Url); + + if (Footer != null) { - var embed = new EmbedBuilder() - .WithColor(Color); - - if (!string.IsNullOrWhiteSpace(Title)) - embed.WithTitle(Title); - - if (!string.IsNullOrWhiteSpace(Description)) - embed.WithDescription(Description); - - if (Url != null && Uri.IsWellFormedUriString(Url, UriKind.Absolute)) - embed.WithUrl(Url); - - if (Footer != null) + embed.WithFooter(efb => { - embed.WithFooter(efb => - { - efb.WithText(Footer.Text); - if (Uri.IsWellFormedUriString(Footer.IconUrl, UriKind.Absolute)) - efb.WithIconUrl(Footer.IconUrl); - }); - } - - if (Thumbnail != null && Uri.IsWellFormedUriString(Thumbnail, UriKind.Absolute)) - embed.WithThumbnailUrl(Thumbnail); - - if (Image != null && Uri.IsWellFormedUriString(Image, UriKind.Absolute)) - embed.WithImageUrl(Image); - - if (Author != null && !string.IsNullOrWhiteSpace(Author.Name)) - { - if (!Uri.IsWellFormedUriString(Author.IconUrl, UriKind.Absolute)) - Author.IconUrl = null; - if (!Uri.IsWellFormedUriString(Author.Url, UriKind.Absolute)) - Author.Url = null; - - embed.WithAuthor(Author.Name, Author.IconUrl, Author.Url); - } - - if (Fields != null) - { - foreach (var f in Fields) - { - if (!string.IsNullOrWhiteSpace(f.Name) && !string.IsNullOrWhiteSpace(f.Value)) - embed.AddField(f.Name, f.Value, f.Inline); - } - } - - return embed; + efb.WithText(Footer.Text); + if (Uri.IsWellFormedUriString(Footer.IconUrl, UriKind.Absolute)) + efb.WithIconUrl(Footer.IconUrl); + }); } - public void NormalizeFields() + if (Thumbnail != null && Uri.IsWellFormedUriString(Thumbnail, UriKind.Absolute)) + embed.WithThumbnailUrl(Thumbnail); + + if (Image != null && Uri.IsWellFormedUriString(Image, UriKind.Absolute)) + embed.WithImageUrl(Image); + + if (Author != null && !string.IsNullOrWhiteSpace(Author.Name)) { - if (Fields != null && Fields.Length > 0) + if (!Uri.IsWellFormedUriString(Author.IconUrl, UriKind.Absolute)) + Author.IconUrl = null; + if (!Uri.IsWellFormedUriString(Author.Url, UriKind.Absolute)) + Author.Url = null; + + embed.WithAuthor(Author.Name, Author.IconUrl, Author.Url); + } + + if (Fields != null) + { + foreach (var f in Fields) { - foreach (var f in Fields) - { - f.Name = f.Name.TrimTo(256); - f.Value = f.Value.TrimTo(1024); - } + if (!string.IsNullOrWhiteSpace(f.Name) && !string.IsNullOrWhiteSpace(f.Value)) + embed.AddField(f.Name, f.Value, f.Inline); + } + } + + return embed; + } + + public void NormalizeFields() + { + if (Fields != null && Fields.Length > 0) + { + foreach (var f in Fields) + { + f.Name = f.Name.TrimTo(256); + f.Value = f.Value.TrimTo(1024); } } } diff --git a/src/NadekoBot/Common/SmartText/SmartPlainText.cs b/src/NadekoBot/Common/SmartText/SmartPlainText.cs index 2e87389d4..247febe82 100644 --- a/src/NadekoBot/Common/SmartText/SmartPlainText.cs +++ b/src/NadekoBot/Common/SmartText/SmartPlainText.cs @@ -1,23 +1,22 @@ -namespace NadekoBot +namespace NadekoBot; + +public sealed record SmartPlainText : SmartText { - public sealed record SmartPlainText : SmartText + public string Text { get; init; } + + public SmartPlainText(string text) { - public string Text { get; init; } + Text = text; + } - public SmartPlainText(string text) - { - Text = text; - } + public static implicit operator SmartPlainText(string input) + => new SmartPlainText(input); - public static implicit operator SmartPlainText(string input) - => new SmartPlainText(input); + public static implicit operator string(SmartPlainText input) + => input.Text; - public static implicit operator string(SmartPlainText input) - => input.Text; - - public override string ToString() - { - return Text; - } + public override string ToString() + { + return Text; } } \ No newline at end of file diff --git a/src/NadekoBot/Common/SmartText/SmartText.cs b/src/NadekoBot/Common/SmartText/SmartText.cs index 2eaea3bbd..e6957d48f 100644 --- a/src/NadekoBot/Common/SmartText/SmartText.cs +++ b/src/NadekoBot/Common/SmartText/SmartText.cs @@ -1,53 +1,51 @@ -using System; -using Newtonsoft.Json; +using Newtonsoft.Json; -namespace NadekoBot +namespace NadekoBot; + +public abstract record SmartText { - public abstract record SmartText - { - public bool IsEmbed => this is SmartEmbedText; - public bool IsPlainText => this is SmartPlainText; + public bool IsEmbed => this is SmartEmbedText; + public bool IsPlainText => this is SmartPlainText; - public static SmartText operator +(SmartText text, string input) - => text switch - { - SmartEmbedText set => set with { PlainText = set.PlainText + input }, - SmartPlainText spt => new SmartPlainText(spt.Text + input), - _ => throw new ArgumentOutOfRangeException(nameof(text)) - }; - - public static SmartText operator +(string input, SmartText text) - => text switch - { - SmartEmbedText set => set with { PlainText = input + set.PlainText }, - SmartPlainText spt => new SmartPlainText(input + spt.Text), - _ => throw new ArgumentOutOfRangeException(nameof(text)) - }; - - public static SmartText CreateFrom(string input) + public static SmartText operator +(SmartText text, string input) + => text switch { - if (string.IsNullOrWhiteSpace(input) || !input.TrimStart().StartsWith("{")) + SmartEmbedText set => set with { PlainText = set.PlainText + input }, + SmartPlainText spt => new SmartPlainText(spt.Text + input), + _ => throw new ArgumentOutOfRangeException(nameof(text)) + }; + + public static SmartText operator +(string input, SmartText text) + => text switch + { + SmartEmbedText set => set with { PlainText = input + set.PlainText }, + SmartPlainText spt => new SmartPlainText(input + spt.Text), + _ => throw new ArgumentOutOfRangeException(nameof(text)) + }; + + public static SmartText CreateFrom(string input) + { + if (string.IsNullOrWhiteSpace(input) || !input.TrimStart().StartsWith("{")) + { + return new SmartPlainText(input); + } + + try + { + var smartEmbedText = JsonConvert.DeserializeObject(input); + + smartEmbedText.NormalizeFields(); + + if (!smartEmbedText.IsValid) { return new SmartPlainText(input); } - try - { - var smartEmbedText = JsonConvert.DeserializeObject(input); - - smartEmbedText.NormalizeFields(); - - if (!smartEmbedText.IsValid) - { - return new SmartPlainText(input); - } - - return smartEmbedText; - } - catch - { - return new SmartPlainText(input); - } + return smartEmbedText; + } + catch + { + return new SmartPlainText(input); } } } \ No newline at end of file diff --git a/src/NadekoBot/Common/SmartText/SmartTextEmbedAuthor.cs b/src/NadekoBot/Common/SmartText/SmartTextEmbedAuthor.cs index c6d663580..ee0bbeb6b 100644 --- a/src/NadekoBot/Common/SmartText/SmartTextEmbedAuthor.cs +++ b/src/NadekoBot/Common/SmartText/SmartTextEmbedAuthor.cs @@ -1,13 +1,12 @@ using Newtonsoft.Json; -namespace NadekoBot +namespace NadekoBot; + +public class SmartTextEmbedAuthor { - public class SmartTextEmbedAuthor - { - public string Name { get; set; } - public string IconUrl { get; set; } - [JsonProperty("icon_url")] - private string Icon_Url { set => IconUrl = value; } - public string Url { get; set; } - } + public string Name { get; set; } + public string IconUrl { get; set; } + [JsonProperty("icon_url")] + private string Icon_Url { set => IconUrl = value; } + public string Url { get; set; } } \ No newline at end of file diff --git a/src/NadekoBot/Common/SmartText/SmartTextEmbedField.cs b/src/NadekoBot/Common/SmartText/SmartTextEmbedField.cs index 7450d90dd..5480479bd 100644 --- a/src/NadekoBot/Common/SmartText/SmartTextEmbedField.cs +++ b/src/NadekoBot/Common/SmartText/SmartTextEmbedField.cs @@ -1,9 +1,8 @@ -namespace NadekoBot +namespace NadekoBot; + +public class SmartTextEmbedField { - public class SmartTextEmbedField - { - public string Name { get; set; } - public string Value { get; set; } - public bool Inline { get; set; } - } + public string Name { get; set; } + public string Value { get; set; } + public bool Inline { get; set; } } \ No newline at end of file diff --git a/src/NadekoBot/Common/SmartText/SmartTextEmbedFooter.cs b/src/NadekoBot/Common/SmartText/SmartTextEmbedFooter.cs index a93a14868..1fe0cb445 100644 --- a/src/NadekoBot/Common/SmartText/SmartTextEmbedFooter.cs +++ b/src/NadekoBot/Common/SmartText/SmartTextEmbedFooter.cs @@ -1,12 +1,11 @@ using Newtonsoft.Json; -namespace NadekoBot +namespace NadekoBot; + +public class SmartTextEmbedFooter { - public class SmartTextEmbedFooter - { - public string Text { get; set; } - public string IconUrl { get; set; } - [JsonProperty("icon_url")] - private string Icon_Url { set => IconUrl = value; } - } + public string Text { get; set; } + public string IconUrl { get; set; } + [JsonProperty("icon_url")] + private string Icon_Url { set => IconUrl = value; } } \ No newline at end of file diff --git a/src/NadekoBot/Common/SocketMessageEventWrapper.cs b/src/NadekoBot/Common/SocketMessageEventWrapper.cs index 23bfa9e40..9d9ae4008 100644 --- a/src/NadekoBot/Common/SocketMessageEventWrapper.cs +++ b/src/NadekoBot/Common/SocketMessageEventWrapper.cs @@ -1,91 +1,89 @@ using Discord; using Discord.WebSocket; -using System; using System.Threading.Tasks; -namespace NadekoBot.Common +namespace NadekoBot.Common; + +public sealed class ReactionEventWrapper : IDisposable { - public sealed class ReactionEventWrapper : IDisposable + public IUserMessage Message { get; } + public event Action OnReactionAdded = delegate { }; + public event Action OnReactionRemoved = delegate { }; + public event Action OnReactionsCleared = delegate { }; + + public ReactionEventWrapper(DiscordSocketClient client, IUserMessage msg) { - public IUserMessage Message { get; } - public event Action OnReactionAdded = delegate { }; - public event Action OnReactionRemoved = delegate { }; - public event Action OnReactionsCleared = delegate { }; + Message = msg ?? throw new ArgumentNullException(nameof(msg)); + _client = client; - public ReactionEventWrapper(DiscordSocketClient client, IUserMessage msg) - { - Message = msg ?? throw new ArgumentNullException(nameof(msg)); - _client = client; - - _client.ReactionAdded += Discord_ReactionAdded; - _client.ReactionRemoved += Discord_ReactionRemoved; - _client.ReactionsCleared += Discord_ReactionsCleared; - } - - private Task Discord_ReactionsCleared(Cacheable msg, ISocketMessageChannel channel) - { - Task.Run(() => - { - try - { - if (msg.Id == Message.Id) - OnReactionsCleared?.Invoke(); - } - catch { } - }); - - return Task.CompletedTask; - } - - private Task Discord_ReactionRemoved(Cacheable msg, ISocketMessageChannel channel, SocketReaction reaction) - { - Task.Run(() => - { - try - { - if (msg.Id == Message.Id) - OnReactionRemoved?.Invoke(reaction); - } - catch { } - }); - - return Task.CompletedTask; - } - - private Task Discord_ReactionAdded(Cacheable msg, ISocketMessageChannel channel, SocketReaction reaction) - { - Task.Run(() => - { - try - { - if (msg.Id == Message.Id) - OnReactionAdded?.Invoke(reaction); - } - catch { } - }); - - return Task.CompletedTask; - } - - public void UnsubAll() - { - _client.ReactionAdded -= Discord_ReactionAdded; - _client.ReactionRemoved -= Discord_ReactionRemoved; - _client.ReactionsCleared -= Discord_ReactionsCleared; - OnReactionAdded = null; - OnReactionRemoved = null; - OnReactionsCleared = null; - } - - private bool disposing = false; - private readonly DiscordSocketClient _client; - - public void Dispose() - { - if (disposing) - return; - disposing = true; - UnsubAll(); - } + _client.ReactionAdded += Discord_ReactionAdded; + _client.ReactionRemoved += Discord_ReactionRemoved; + _client.ReactionsCleared += Discord_ReactionsCleared; } -} + + private Task Discord_ReactionsCleared(Cacheable msg, ISocketMessageChannel channel) + { + Task.Run(() => + { + try + { + if (msg.Id == Message.Id) + OnReactionsCleared?.Invoke(); + } + catch { } + }); + + return Task.CompletedTask; + } + + private Task Discord_ReactionRemoved(Cacheable msg, ISocketMessageChannel channel, SocketReaction reaction) + { + Task.Run(() => + { + try + { + if (msg.Id == Message.Id) + OnReactionRemoved?.Invoke(reaction); + } + catch { } + }); + + return Task.CompletedTask; + } + + private Task Discord_ReactionAdded(Cacheable msg, ISocketMessageChannel channel, SocketReaction reaction) + { + Task.Run(() => + { + try + { + if (msg.Id == Message.Id) + OnReactionAdded?.Invoke(reaction); + } + catch { } + }); + + return Task.CompletedTask; + } + + public void UnsubAll() + { + _client.ReactionAdded -= Discord_ReactionAdded; + _client.ReactionRemoved -= Discord_ReactionRemoved; + _client.ReactionsCleared -= Discord_ReactionsCleared; + OnReactionAdded = null; + OnReactionRemoved = null; + OnReactionsCleared = null; + } + + private bool disposing = false; + private readonly DiscordSocketClient _client; + + public void Dispose() + { + if (disposing) + return; + disposing = true; + UnsubAll(); + } +} \ No newline at end of file diff --git a/src/NadekoBot/Common/TypeReaders/AddRemove.cs b/src/NadekoBot/Common/TypeReaders/AddRemove.cs index 3cab4751c..3f6fdc62b 100644 --- a/src/NadekoBot/Common/TypeReaders/AddRemove.cs +++ b/src/NadekoBot/Common/TypeReaders/AddRemove.cs @@ -1,9 +1,8 @@ -namespace NadekoBot.Common.TypeReaders +namespace NadekoBot.Common.TypeReaders; + +public enum AddRemove { - public enum AddRemove - { - Add = int.MinValue, - Rem = int.MinValue + 1, - Rm = int.MinValue + 1, - } -} + Add = int.MinValue, + Rem = int.MinValue + 1, + Rm = int.MinValue + 1, +} \ No newline at end of file diff --git a/src/NadekoBot/Common/TypeReaders/BotCommandTypeReader.cs b/src/NadekoBot/Common/TypeReaders/BotCommandTypeReader.cs index fc9e8e422..c81f46452 100644 --- a/src/NadekoBot/Common/TypeReaders/BotCommandTypeReader.cs +++ b/src/NadekoBot/Common/TypeReaders/BotCommandTypeReader.cs @@ -1,90 +1,87 @@ -using System; -using System.Linq; -using System.Threading.Tasks; +using System.Threading.Tasks; using Discord.Commands; using NadekoBot.Services; using NadekoBot.Modules.CustomReactions.Services; -namespace NadekoBot.Common.TypeReaders +namespace NadekoBot.Common.TypeReaders; + +public sealed class CommandTypeReader : NadekoTypeReader { - public sealed class CommandTypeReader : NadekoTypeReader + private readonly CommandHandler _handler; + private readonly CommandService _cmds; + + public CommandTypeReader(CommandHandler handler, CommandService cmds) { - private readonly CommandHandler _handler; - private readonly CommandService _cmds; - - public CommandTypeReader(CommandHandler handler, CommandService cmds) - { - _handler = handler; - _cmds = cmds; - } - - public override Task ReadAsync(ICommandContext context, string input) - { - input = input.ToUpperInvariant(); - var prefix = _handler.GetPrefix(context.Guild); - if (!input.StartsWith(prefix.ToUpperInvariant(), StringComparison.InvariantCulture)) - return Task.FromResult(TypeReaderResult.FromError(CommandError.ParseFailed, "No such command found.")); - - input = input.Substring(prefix.Length); - - var cmd = _cmds.Commands.FirstOrDefault(c => c.Aliases.Select(a => a.ToUpperInvariant()).Contains(input)); - if (cmd is null) - return Task.FromResult(TypeReaderResult.FromError(CommandError.ParseFailed, "No such command found.")); - - return Task.FromResult(TypeReaderResult.FromSuccess(cmd)); - } + _handler = handler; + _cmds = cmds; } - public sealed class CommandOrCrTypeReader : NadekoTypeReader + public override Task ReadAsync(ICommandContext context, string input) { - private readonly CommandService _cmds; - private readonly CustomReactionsService _crs; - private readonly CommandHandler _commandHandler; + input = input.ToUpperInvariant(); + var prefix = _handler.GetPrefix(context.Guild); + if (!input.StartsWith(prefix.ToUpperInvariant(), StringComparison.InvariantCulture)) + return Task.FromResult(TypeReaderResult.FromError(CommandError.ParseFailed, "No such command found.")); - public CommandOrCrTypeReader( - CommandService cmds, - CustomReactionsService crs, - CommandHandler commandHandler) - { - _cmds = cmds; - _crs = crs; - _commandHandler = commandHandler; - } + input = input.Substring(prefix.Length); - public override async Task ReadAsync(ICommandContext context, string input) - { - input = input.ToUpperInvariant(); + var cmd = _cmds.Commands.FirstOrDefault(c => c.Aliases.Select(a => a.ToUpperInvariant()).Contains(input)); + if (cmd is null) + return Task.FromResult(TypeReaderResult.FromError(CommandError.ParseFailed, "No such command found.")); - if (_crs.ReactionExists(context.Guild?.Id, input)) - { - return TypeReaderResult.FromSuccess(new CommandOrCrInfo(input, CommandOrCrInfo.Type.Custom)); - } - - var cmd = await new CommandTypeReader(_commandHandler, _cmds).ReadAsync(context, input).ConfigureAwait(false); - if (cmd.IsSuccess) - { - return TypeReaderResult.FromSuccess(new CommandOrCrInfo(((CommandInfo)cmd.Values.First().Value).Name, CommandOrCrInfo.Type.Normal)); - } - return TypeReaderResult.FromError(CommandError.ParseFailed, "No such command or cr found."); - } - } - - public class CommandOrCrInfo - { - public enum Type - { - Normal, - Custom, - } - - public string Name { get; set; } - public Type CmdType { get; set; } - public bool IsCustom => CmdType == Type.Custom; - - public CommandOrCrInfo(string input, Type type) - { - this.Name = input; - this.CmdType = type; - } + return Task.FromResult(TypeReaderResult.FromSuccess(cmd)); } } + +public sealed class CommandOrCrTypeReader : NadekoTypeReader +{ + private readonly CommandService _cmds; + private readonly CustomReactionsService _crs; + private readonly CommandHandler _commandHandler; + + public CommandOrCrTypeReader( + CommandService cmds, + CustomReactionsService crs, + CommandHandler commandHandler) + { + _cmds = cmds; + _crs = crs; + _commandHandler = commandHandler; + } + + public override async Task ReadAsync(ICommandContext context, string input) + { + input = input.ToUpperInvariant(); + + if (_crs.ReactionExists(context.Guild?.Id, input)) + { + return TypeReaderResult.FromSuccess(new CommandOrCrInfo(input, CommandOrCrInfo.Type.Custom)); + } + + var cmd = await new CommandTypeReader(_commandHandler, _cmds).ReadAsync(context, input).ConfigureAwait(false); + if (cmd.IsSuccess) + { + return TypeReaderResult.FromSuccess(new CommandOrCrInfo(((CommandInfo)cmd.Values.First().Value).Name, CommandOrCrInfo.Type.Normal)); + } + return TypeReaderResult.FromError(CommandError.ParseFailed, "No such command or cr found."); + } +} + +public class CommandOrCrInfo +{ + public enum Type + { + Normal, + Custom, + } + + public string Name { get; set; } + public Type CmdType { get; set; } + public bool IsCustom => CmdType == Type.Custom; + + public CommandOrCrInfo(string input, Type type) + { + this.Name = input; + this.CmdType = type; + } +} \ No newline at end of file diff --git a/src/NadekoBot/Common/TypeReaders/EmoteTypeReader.cs b/src/NadekoBot/Common/TypeReaders/EmoteTypeReader.cs index bb45e641a..19c357f06 100644 --- a/src/NadekoBot/Common/TypeReaders/EmoteTypeReader.cs +++ b/src/NadekoBot/Common/TypeReaders/EmoteTypeReader.cs @@ -2,16 +2,15 @@ using Discord; using Discord.Commands; -namespace NadekoBot.Common.TypeReaders -{ - public sealed class EmoteTypeReader : NadekoTypeReader - { - public override Task ReadAsync(ICommandContext ctx, string input) - { - if (!Emote.TryParse(input, out var emote)) - return Task.FromResult(TypeReaderResult.FromError(CommandError.ParseFailed, "Input is not a valid emote")); +namespace NadekoBot.Common.TypeReaders; - return Task.FromResult(TypeReaderResult.FromSuccess(emote)); - } +public sealed class EmoteTypeReader : NadekoTypeReader +{ + public override Task ReadAsync(ICommandContext ctx, string input) + { + if (!Emote.TryParse(input, out var emote)) + return Task.FromResult(TypeReaderResult.FromError(CommandError.ParseFailed, "Input is not a valid emote")); + + return Task.FromResult(TypeReaderResult.FromSuccess(emote)); } } \ No newline at end of file diff --git a/src/NadekoBot/Common/TypeReaders/GuildDateTimeTypeReader.cs b/src/NadekoBot/Common/TypeReaders/GuildDateTimeTypeReader.cs index ee71b031a..6abe79bbc 100644 --- a/src/NadekoBot/Common/TypeReaders/GuildDateTimeTypeReader.cs +++ b/src/NadekoBot/Common/TypeReaders/GuildDateTimeTypeReader.cs @@ -1,53 +1,51 @@ -using System; -using System.Threading.Tasks; +using System.Threading.Tasks; using Discord.Commands; using NadekoBot.Modules.Administration.Services; -namespace NadekoBot.Common.TypeReaders +namespace NadekoBot.Common.TypeReaders; + +public sealed class GuildDateTimeTypeReader : NadekoTypeReader { - public sealed class GuildDateTimeTypeReader : NadekoTypeReader + private readonly GuildTimezoneService _gts; + + public GuildDateTimeTypeReader(GuildTimezoneService gts) { - private readonly GuildTimezoneService _gts; - - public GuildDateTimeTypeReader(GuildTimezoneService gts) - { - _gts = gts; - } + _gts = gts; + } - public override Task ReadAsync(ICommandContext context, string input) - { - var gdt = Parse(context.Guild.Id, input); - if(gdt is null) - return Task.FromResult(TypeReaderResult.FromError(CommandError.ParseFailed, "Input string is in an incorrect format.")); + public override Task ReadAsync(ICommandContext context, string input) + { + var gdt = Parse(context.Guild.Id, input); + if(gdt is null) + return Task.FromResult(TypeReaderResult.FromError(CommandError.ParseFailed, "Input string is in an incorrect format.")); - return Task.FromResult(TypeReaderResult.FromSuccess(gdt)); - } - - private GuildDateTime Parse(ulong guildId, string input) - { - if (!DateTime.TryParse(input, out var dt)) - return null; - - var tz = _gts.GetTimeZoneOrUtc(guildId); - - return new(tz, dt); - } + return Task.FromResult(TypeReaderResult.FromSuccess(gdt)); } - public class GuildDateTime + private GuildDateTime Parse(ulong guildId, string input) { - public TimeZoneInfo Timezone { get; } - public DateTime CurrentGuildTime { get; } - public DateTime InputTime { get; } - public DateTime InputTimeUtc { get; } + if (!DateTime.TryParse(input, out var dt)) + return null; - public GuildDateTime(TimeZoneInfo guildTimezone, DateTime inputTime) - { - var now = DateTime.UtcNow; - Timezone = guildTimezone; - CurrentGuildTime = TimeZoneInfo.ConvertTime(now, TimeZoneInfo.Utc, Timezone); - InputTime = inputTime; - InputTimeUtc = TimeZoneInfo.ConvertTime(inputTime, Timezone, TimeZoneInfo.Utc); - } + var tz = _gts.GetTimeZoneOrUtc(guildId); + + return new(tz, dt); } } + +public class GuildDateTime +{ + public TimeZoneInfo Timezone { get; } + public DateTime CurrentGuildTime { get; } + public DateTime InputTime { get; } + public DateTime InputTimeUtc { get; } + + public GuildDateTime(TimeZoneInfo guildTimezone, DateTime inputTime) + { + var now = DateTime.UtcNow; + Timezone = guildTimezone; + CurrentGuildTime = TimeZoneInfo.ConvertTime(now, TimeZoneInfo.Utc, Timezone); + InputTime = inputTime; + InputTimeUtc = TimeZoneInfo.ConvertTime(inputTime, Timezone, TimeZoneInfo.Utc); + } +} \ No newline at end of file diff --git a/src/NadekoBot/Common/TypeReaders/GuildTypeReader.cs b/src/NadekoBot/Common/TypeReaders/GuildTypeReader.cs index ab8434484..b715a7eca 100644 --- a/src/NadekoBot/Common/TypeReaders/GuildTypeReader.cs +++ b/src/NadekoBot/Common/TypeReaders/GuildTypeReader.cs @@ -1,31 +1,29 @@ -using System.Linq; -using System.Threading.Tasks; +using System.Threading.Tasks; using Discord.Commands; using Discord.WebSocket; using Discord; -namespace NadekoBot.Common.TypeReaders +namespace NadekoBot.Common.TypeReaders; + +public sealed class GuildTypeReader : NadekoTypeReader { - public sealed class GuildTypeReader : NadekoTypeReader + private readonly DiscordSocketClient _client; + + public GuildTypeReader(DiscordSocketClient client) { - private readonly DiscordSocketClient _client; - - public GuildTypeReader(DiscordSocketClient client) - { - _client = client; - } - - public override Task ReadAsync(ICommandContext context, string input) - { - input = input.Trim().ToUpperInvariant(); - var guilds = _client.Guilds; - var guild = guilds.FirstOrDefault(g => g.Id.ToString().Trim().ToUpperInvariant() == input) ?? //by id - guilds.FirstOrDefault(g => g.Name.Trim().ToUpperInvariant() == input); //by name - - if (guild != null) - return Task.FromResult(TypeReaderResult.FromSuccess(guild)); - - return Task.FromResult(TypeReaderResult.FromError(CommandError.ParseFailed, "No guild by that name or Id found")); - } + _client = client; } -} + + public override Task ReadAsync(ICommandContext context, string input) + { + input = input.Trim().ToUpperInvariant(); + var guilds = _client.Guilds; + var guild = guilds.FirstOrDefault(g => g.Id.ToString().Trim().ToUpperInvariant() == input) ?? //by id + guilds.FirstOrDefault(g => g.Name.Trim().ToUpperInvariant() == input); //by name + + if (guild != null) + return Task.FromResult(TypeReaderResult.FromSuccess(guild)); + + return Task.FromResult(TypeReaderResult.FromError(CommandError.ParseFailed, "No guild by that name or Id found")); + } +} \ No newline at end of file diff --git a/src/NadekoBot/Common/TypeReaders/KwumTypeReader.cs b/src/NadekoBot/Common/TypeReaders/KwumTypeReader.cs index 2a6bde307..9c67700bd 100644 --- a/src/NadekoBot/Common/TypeReaders/KwumTypeReader.cs +++ b/src/NadekoBot/Common/TypeReaders/KwumTypeReader.cs @@ -1,24 +1,23 @@ using System.Threading.Tasks; using Discord.Commands; -namespace NadekoBot.Common.TypeReaders +namespace NadekoBot.Common.TypeReaders; + +public sealed class KwumTypeReader : NadekoTypeReader { - public sealed class KwumTypeReader : NadekoTypeReader + public override Task ReadAsync(ICommandContext context, string input) { - public override Task ReadAsync(ICommandContext context, string input) - { - if (kwum.TryParse(input, out var val)) - return Task.FromResult(TypeReaderResult.FromSuccess(val)); + if (kwum.TryParse(input, out var val)) + return Task.FromResult(TypeReaderResult.FromSuccess(val)); - return Task.FromResult(TypeReaderResult.FromError(CommandError.ParseFailed, "Input is not a valid kwum")); - } + return Task.FromResult(TypeReaderResult.FromError(CommandError.ParseFailed, "Input is not a valid kwum")); } +} - public sealed class SmartTextTypeReader : NadekoTypeReader +public sealed class SmartTextTypeReader : NadekoTypeReader +{ + public override Task ReadAsync(ICommandContext ctx, string input) { - public override Task ReadAsync(ICommandContext ctx, string input) - { - return Task.FromResult(TypeReaderResult.FromSuccess(SmartText.CreateFrom(input))); - } + return Task.FromResult(TypeReaderResult.FromSuccess(SmartText.CreateFrom(input))); } } \ No newline at end of file diff --git a/src/NadekoBot/Common/TypeReaders/Models/PermissionAction.cs b/src/NadekoBot/Common/TypeReaders/Models/PermissionAction.cs index 12dbb1dfe..2e787fd75 100644 --- a/src/NadekoBot/Common/TypeReaders/Models/PermissionAction.cs +++ b/src/NadekoBot/Common/TypeReaders/Models/PermissionAction.cs @@ -1,27 +1,26 @@ -namespace NadekoBot.Common.TypeReaders.Models +namespace NadekoBot.Common.TypeReaders.Models; + +public class PermissionAction { - public class PermissionAction + public static PermissionAction Enable => new PermissionAction(true); + public static PermissionAction Disable => new PermissionAction(false); + + public bool Value { get; } + + public PermissionAction(bool value) { - public static PermissionAction Enable => new PermissionAction(true); - public static PermissionAction Disable => new PermissionAction(false); - - public bool Value { get; } - - public PermissionAction(bool value) - { - this.Value = value; - } - - public override bool Equals(object obj) - { - if (obj is null || GetType() != obj.GetType()) - { - return false; - } - - return this.Value == ((PermissionAction)obj).Value; - } - - public override int GetHashCode() => Value.GetHashCode(); + this.Value = value; } -} + + public override bool Equals(object obj) + { + if (obj is null || GetType() != obj.GetType()) + { + return false; + } + + return this.Value == ((PermissionAction)obj).Value; + } + + public override int GetHashCode() => Value.GetHashCode(); +} \ No newline at end of file diff --git a/src/NadekoBot/Common/TypeReaders/Models/StoopidTime.cs b/src/NadekoBot/Common/TypeReaders/Models/StoopidTime.cs index df8188bdf..3767fccc9 100644 --- a/src/NadekoBot/Common/TypeReaders/Models/StoopidTime.cs +++ b/src/NadekoBot/Common/TypeReaders/Models/StoopidTime.cs @@ -1,65 +1,62 @@ -using System; -using System.Collections.Generic; -using System.Text.RegularExpressions; +using System.Text.RegularExpressions; -namespace NadekoBot.Common.TypeReaders.Models +namespace NadekoBot.Common.TypeReaders.Models; + +public class StoopidTime { - public class StoopidTime + public string Input { get; set; } + public TimeSpan Time { get; set; } + + private static readonly Regex _regex = new Regex( + @"^(?:(?\d)mo)?(?:(?\d{1,2})w)?(?:(?\d{1,2})d)?(?:(?\d{1,4})h)?(?:(?\d{1,5})m)?(?:(?\d{1,6})s)?$", + RegexOptions.Compiled | RegexOptions.Multiline); + + private StoopidTime() { } + + public static StoopidTime FromInput(string input) { - public string Input { get; set; } - public TimeSpan Time { get; set; } + var m = _regex.Match(input); - private static readonly Regex _regex = new Regex( -@"^(?:(?\d)mo)?(?:(?\d{1,2})w)?(?:(?\d{1,2})d)?(?:(?\d{1,4})h)?(?:(?\d{1,5})m)?(?:(?\d{1,6})s)?$", - RegexOptions.Compiled | RegexOptions.Multiline); - - private StoopidTime() { } - - public static StoopidTime FromInput(string input) + if (m.Length == 0) { - var m = _regex.Match(input); - - if (m.Length == 0) - { - throw new ArgumentException("Invalid string input format."); - } - - string output = ""; - var namesAndValues = new Dictionary(); - - foreach (var groupName in _regex.GetGroupNames()) - { - if (groupName == "0") continue; - if (!int.TryParse(m.Groups[groupName].Value, out var value)) - { - namesAndValues[groupName] = 0; - continue; - } - - if (value < 1) - { - throw new ArgumentException($"Invalid {groupName} value."); - } - - namesAndValues[groupName] = value; - output += m.Groups[groupName].Value + " " + groupName + " "; - } - var ts = new TimeSpan(30 * namesAndValues["months"] + - 7 * namesAndValues["weeks"] + - namesAndValues["days"], - namesAndValues["hours"], - namesAndValues["minutes"], - namesAndValues["seconds"]); - if (ts > TimeSpan.FromDays(90)) - { - throw new ArgumentException("Time is too long."); - } - - return new StoopidTime() - { - Input = input, - Time = ts, - }; + throw new ArgumentException("Invalid string input format."); } + + string output = ""; + var namesAndValues = new Dictionary(); + + foreach (var groupName in _regex.GetGroupNames()) + { + if (groupName == "0") continue; + if (!int.TryParse(m.Groups[groupName].Value, out var value)) + { + namesAndValues[groupName] = 0; + continue; + } + + if (value < 1) + { + throw new ArgumentException($"Invalid {groupName} value."); + } + + namesAndValues[groupName] = value; + output += m.Groups[groupName].Value + " " + groupName + " "; + } + var ts = new TimeSpan(30 * namesAndValues["months"] + + 7 * namesAndValues["weeks"] + + namesAndValues["days"], + namesAndValues["hours"], + namesAndValues["minutes"], + namesAndValues["seconds"]); + if (ts > TimeSpan.FromDays(90)) + { + throw new ArgumentException("Time is too long."); + } + + return new StoopidTime() + { + Input = input, + Time = ts, + }; } -} +} \ No newline at end of file diff --git a/src/NadekoBot/Common/TypeReaders/ModuleTypeReader.cs b/src/NadekoBot/Common/TypeReaders/ModuleTypeReader.cs index 99cd3f679..9f868d239 100644 --- a/src/NadekoBot/Common/TypeReaders/ModuleTypeReader.cs +++ b/src/NadekoBot/Common/TypeReaders/ModuleTypeReader.cs @@ -1,55 +1,53 @@ -using System.Linq; -using System.Threading.Tasks; +using System.Threading.Tasks; using Discord.Commands; using NadekoBot.Extensions; -namespace NadekoBot.Common.TypeReaders +namespace NadekoBot.Common.TypeReaders; + +public sealed class ModuleTypeReader : NadekoTypeReader { - public sealed class ModuleTypeReader : NadekoTypeReader + private readonly CommandService _cmds; + + public ModuleTypeReader(CommandService cmds) { - private readonly CommandService _cmds; - - public ModuleTypeReader(CommandService cmds) - { - _cmds = cmds; - } - - public override Task ReadAsync(ICommandContext context, string input) - { - input = input.ToUpperInvariant(); - var module = _cmds.Modules.GroupBy(m => m.GetTopLevelModule()).FirstOrDefault(m => m.Key.Name.ToUpperInvariant() == input)?.Key; - if (module is null) - return Task.FromResult(TypeReaderResult.FromError(CommandError.ParseFailed, "No such module found.")); - - return Task.FromResult(TypeReaderResult.FromSuccess(module)); - } + _cmds = cmds; } - public sealed class ModuleOrCrTypeReader : NadekoTypeReader + public override Task ReadAsync(ICommandContext context, string input) { - private readonly CommandService _cmds; + input = input.ToUpperInvariant(); + var module = _cmds.Modules.GroupBy(m => m.GetTopLevelModule()).FirstOrDefault(m => m.Key.Name.ToUpperInvariant() == input)?.Key; + if (module is null) + return Task.FromResult(TypeReaderResult.FromError(CommandError.ParseFailed, "No such module found.")); - public ModuleOrCrTypeReader(CommandService cmds) - { - _cmds = cmds; - } - - public override Task ReadAsync(ICommandContext context, string input) - { - input = input.ToUpperInvariant(); - var module = _cmds.Modules.GroupBy(m => m.GetTopLevelModule()).FirstOrDefault(m => m.Key.Name.ToUpperInvariant() == input)?.Key; - if (module is null && input != "ACTUALCUSTOMREACTIONS") - return Task.FromResult(TypeReaderResult.FromError(CommandError.ParseFailed, "No such module found.")); - - return Task.FromResult(TypeReaderResult.FromSuccess(new ModuleOrCrInfo - { - Name = input, - })); - } - } - - public sealed class ModuleOrCrInfo - { - public string Name { get; set; } + return Task.FromResult(TypeReaderResult.FromSuccess(module)); } } + +public sealed class ModuleOrCrTypeReader : NadekoTypeReader +{ + private readonly CommandService _cmds; + + public ModuleOrCrTypeReader(CommandService cmds) + { + _cmds = cmds; + } + + public override Task ReadAsync(ICommandContext context, string input) + { + input = input.ToUpperInvariant(); + var module = _cmds.Modules.GroupBy(m => m.GetTopLevelModule()).FirstOrDefault(m => m.Key.Name.ToUpperInvariant() == input)?.Key; + if (module is null && input != "ACTUALCUSTOMREACTIONS") + return Task.FromResult(TypeReaderResult.FromError(CommandError.ParseFailed, "No such module found.")); + + return Task.FromResult(TypeReaderResult.FromSuccess(new ModuleOrCrInfo + { + Name = input, + })); + } +} + +public sealed class ModuleOrCrInfo +{ + public string Name { get; set; } +} \ No newline at end of file diff --git a/src/NadekoBot/Common/TypeReaders/NadekoTypeReader.cs b/src/NadekoBot/Common/TypeReaders/NadekoTypeReader.cs index 972d55450..3cfeb6199 100644 --- a/src/NadekoBot/Common/TypeReaders/NadekoTypeReader.cs +++ b/src/NadekoBot/Common/TypeReaders/NadekoTypeReader.cs @@ -1,14 +1,12 @@ -using System; -using System.Threading.Tasks; +using System.Threading.Tasks; using Discord.Commands; -namespace NadekoBot.Common.TypeReaders -{ - public abstract class NadekoTypeReader : TypeReader - { - public abstract Task ReadAsync(ICommandContext ctx, string input); +namespace NadekoBot.Common.TypeReaders; - public override Task ReadAsync(ICommandContext ctx, string input, IServiceProvider services) - => ReadAsync(ctx, input); - } -} +public abstract class NadekoTypeReader : TypeReader +{ + public abstract Task ReadAsync(ICommandContext ctx, string input); + + public override Task ReadAsync(ICommandContext ctx, string input, IServiceProvider services) + => ReadAsync(ctx, input); +} \ No newline at end of file diff --git a/src/NadekoBot/Common/TypeReaders/PermissionActionTypeReader.cs b/src/NadekoBot/Common/TypeReaders/PermissionActionTypeReader.cs index 6ab50b3a7..a3f068d31 100644 --- a/src/NadekoBot/Common/TypeReaders/PermissionActionTypeReader.cs +++ b/src/NadekoBot/Common/TypeReaders/PermissionActionTypeReader.cs @@ -2,39 +2,38 @@ using Discord.Commands; using NadekoBot.Common.TypeReaders.Models; -namespace NadekoBot.Common.TypeReaders +namespace NadekoBot.Common.TypeReaders; + +/// +/// Used instead of bool for more flexible keywords for true/false only in the permission module +/// +public sealed class PermissionActionTypeReader : NadekoTypeReader { - /// - /// Used instead of bool for more flexible keywords for true/false only in the permission module - /// - public sealed class PermissionActionTypeReader : NadekoTypeReader + public override Task ReadAsync(ICommandContext context, string input) { - public override Task ReadAsync(ICommandContext context, string input) + input = input.ToUpperInvariant(); + switch (input) { - input = input.ToUpperInvariant(); - switch (input) - { - case "1": - case "T": - case "TRUE": - case "ENABLE": - case "ENABLED": - case "ALLOW": - case "PERMIT": - case "UNBAN": - return Task.FromResult(TypeReaderResult.FromSuccess(PermissionAction.Enable)); - case "0": - case "F": - case "FALSE": - case "DENY": - case "DISABLE": - case "DISABLED": - case "DISALLOW": - case "BAN": - return Task.FromResult(TypeReaderResult.FromSuccess(PermissionAction.Disable)); - default: - return Task.FromResult(TypeReaderResult.FromError(CommandError.ParseFailed, "Did not receive a valid boolean value")); - } + case "1": + case "T": + case "TRUE": + case "ENABLE": + case "ENABLED": + case "ALLOW": + case "PERMIT": + case "UNBAN": + return Task.FromResult(TypeReaderResult.FromSuccess(PermissionAction.Enable)); + case "0": + case "F": + case "FALSE": + case "DENY": + case "DISABLE": + case "DISABLED": + case "DISALLOW": + case "BAN": + return Task.FromResult(TypeReaderResult.FromSuccess(PermissionAction.Disable)); + default: + return Task.FromResult(TypeReaderResult.FromError(CommandError.ParseFailed, "Did not receive a valid boolean value")); } } -} +} \ No newline at end of file diff --git a/src/NadekoBot/Common/TypeReaders/Rgba32TypeReader.cs b/src/NadekoBot/Common/TypeReaders/Rgba32TypeReader.cs index 65b1bdd1f..8bdd599dc 100644 --- a/src/NadekoBot/Common/TypeReaders/Rgba32TypeReader.cs +++ b/src/NadekoBot/Common/TypeReaders/Rgba32TypeReader.cs @@ -1,25 +1,23 @@ -using System; -using System.Threading.Tasks; +using System.Threading.Tasks; using Discord.Commands; using SixLabors.ImageSharp; -namespace NadekoBot.Common.TypeReaders -{ - public sealed class Rgba32TypeReader : NadekoTypeReader - { - public override async Task ReadAsync(ICommandContext context, string input) - { - await Task.Yield(); +namespace NadekoBot.Common.TypeReaders; - input = input.Replace("#", "", StringComparison.InvariantCulture); - try - { - return TypeReaderResult.FromSuccess(Color.ParseHex(input)); - } - catch - { - return TypeReaderResult.FromError(CommandError.ParseFailed, "Parameter is not a valid color hex."); - } +public sealed class Rgba32TypeReader : NadekoTypeReader +{ + public override async Task ReadAsync(ICommandContext context, string input) + { + await Task.Yield(); + + input = input.Replace("#", "", StringComparison.InvariantCulture); + try + { + return TypeReaderResult.FromSuccess(Color.ParseHex(input)); + } + catch + { + return TypeReaderResult.FromError(CommandError.ParseFailed, "Parameter is not a valid color hex."); } } -} +} \ No newline at end of file diff --git a/src/NadekoBot/Common/TypeReaders/ShmartNumberTypeReader.cs b/src/NadekoBot/Common/TypeReaders/ShmartNumberTypeReader.cs index 7c8333bad..07c710e55 100644 --- a/src/NadekoBot/Common/TypeReaders/ShmartNumberTypeReader.cs +++ b/src/NadekoBot/Common/TypeReaders/ShmartNumberTypeReader.cs @@ -1,107 +1,105 @@ using Discord.Commands; -using System; using System.Text.RegularExpressions; using System.Threading.Tasks; using NadekoBot.Db; using NadekoBot.Modules.Gambling.Services; using NadekoBot.Services; -namespace NadekoBot.Common.TypeReaders +namespace NadekoBot.Common.TypeReaders; + +public sealed class ShmartNumberTypeReader : NadekoTypeReader { - public sealed class ShmartNumberTypeReader : NadekoTypeReader + private readonly DbService _db; + private readonly GamblingConfigService _gambling; + + public ShmartNumberTypeReader(DbService db, GamblingConfigService gambling) { - private readonly DbService _db; - private readonly GamblingConfigService _gambling; + _db = db; + _gambling = gambling; + } - public ShmartNumberTypeReader(DbService db, GamblingConfigService gambling) + public override async Task ReadAsync(ICommandContext context, string input) + { + await Task.Yield(); + + if (string.IsNullOrWhiteSpace(input)) + return TypeReaderResult.FromError(CommandError.ParseFailed, "Input is empty."); + + var i = input.Trim().ToUpperInvariant(); + + i = i.Replace("K", "000"); + + //can't add m because it will conflict with max atm + + if (TryHandlePercentage(context, i, out var num)) + return TypeReaderResult.FromSuccess(new ShmartNumber(num, i)); + try { - _db = db; - _gambling = gambling; + var expr = new NCalc.Expression(i, NCalc.EvaluateOptions.IgnoreCase); + expr.EvaluateParameter += (str, ev) => EvaluateParam(str, ev, context); + var lon = (long)(decimal.Parse(expr.Evaluate().ToString())); + return TypeReaderResult.FromSuccess(new ShmartNumber(lon, input)); } - - public override async Task ReadAsync(ICommandContext context, string input) + catch (Exception) { - await Task.Yield(); - - if (string.IsNullOrWhiteSpace(input)) - return TypeReaderResult.FromError(CommandError.ParseFailed, "Input is empty."); - - var i = input.Trim().ToUpperInvariant(); - - i = i.Replace("K", "000"); - - //can't add m because it will conflict with max atm - - if (TryHandlePercentage(context, i, out var num)) - return TypeReaderResult.FromSuccess(new ShmartNumber(num, i)); - try - { - var expr = new NCalc.Expression(i, NCalc.EvaluateOptions.IgnoreCase); - expr.EvaluateParameter += (str, ev) => EvaluateParam(str, ev, context); - var lon = (long)(decimal.Parse(expr.Evaluate().ToString())); - return TypeReaderResult.FromSuccess(new ShmartNumber(lon, input)); - } - catch (Exception) - { - return TypeReaderResult.FromError(CommandError.ParseFailed, $"Invalid input: {input}"); - } - } - - private void EvaluateParam(string name, NCalc.ParameterArgs args, ICommandContext ctx) - { - switch (name.ToUpperInvariant()) - { - case "PI": - args.Result = Math.PI; - break; - case "E": - args.Result = Math.E; - break; - case "ALL": - case "ALLIN": - args.Result = Cur(ctx); - break; - case "HALF": - args.Result = Cur(ctx) / 2; - break; - case "MAX": - args.Result = Max(ctx); - break; - default: - break; - } - } - - private static readonly Regex percentRegex = new Regex(@"^((?100|\d{1,2})%)$", RegexOptions.Compiled); - - private long Cur(ICommandContext ctx) - { - using var uow = _db.GetDbContext(); - return uow.DiscordUser.GetUserCurrency(ctx.User.Id); - } - - private long Max(ICommandContext ctx) - { - var settings = _gambling.Data; - var max = settings.MaxBet; - return max == 0 - ? Cur(ctx) - : max; - } - - private bool TryHandlePercentage(ICommandContext ctx, string input, out long num) - { - num = 0; - var m = percentRegex.Match(input); - if (m.Captures.Count != 0) - { - if (!long.TryParse(m.Groups["num"].ToString(), out var percent)) - return false; - - num = (long)(Cur(ctx) * (percent / 100.0f)); - return true; - } - return false; + return TypeReaderResult.FromError(CommandError.ParseFailed, $"Invalid input: {input}"); } } -} + + private void EvaluateParam(string name, NCalc.ParameterArgs args, ICommandContext ctx) + { + switch (name.ToUpperInvariant()) + { + case "PI": + args.Result = Math.PI; + break; + case "E": + args.Result = Math.E; + break; + case "ALL": + case "ALLIN": + args.Result = Cur(ctx); + break; + case "HALF": + args.Result = Cur(ctx) / 2; + break; + case "MAX": + args.Result = Max(ctx); + break; + default: + break; + } + } + + private static readonly Regex percentRegex = new Regex(@"^((?100|\d{1,2})%)$", RegexOptions.Compiled); + + private long Cur(ICommandContext ctx) + { + using var uow = _db.GetDbContext(); + return uow.DiscordUser.GetUserCurrency(ctx.User.Id); + } + + private long Max(ICommandContext ctx) + { + var settings = _gambling.Data; + var max = settings.MaxBet; + return max == 0 + ? Cur(ctx) + : max; + } + + private bool TryHandlePercentage(ICommandContext ctx, string input, out long num) + { + num = 0; + var m = percentRegex.Match(input); + if (m.Captures.Count != 0) + { + if (!long.TryParse(m.Groups["num"].ToString(), out var percent)) + return false; + + num = (long)(Cur(ctx) * (percent / 100.0f)); + return true; + } + return false; + } +} \ No newline at end of file diff --git a/src/NadekoBot/Common/TypeReaders/StoopidTimeTypeReader.cs b/src/NadekoBot/Common/TypeReaders/StoopidTimeTypeReader.cs index 71eb5066b..c63111930 100644 --- a/src/NadekoBot/Common/TypeReaders/StoopidTimeTypeReader.cs +++ b/src/NadekoBot/Common/TypeReaders/StoopidTimeTypeReader.cs @@ -1,25 +1,23 @@ using Discord.Commands; using NadekoBot.Common.TypeReaders.Models; -using System; using System.Threading.Tasks; -namespace NadekoBot.Common.TypeReaders +namespace NadekoBot.Common.TypeReaders; + +public sealed class StoopidTimeTypeReader : NadekoTypeReader { - public sealed class StoopidTimeTypeReader : NadekoTypeReader + public override Task ReadAsync(ICommandContext context, string input) { - public override Task ReadAsync(ICommandContext context, string input) + if (string.IsNullOrWhiteSpace(input)) + return Task.FromResult(TypeReaderResult.FromError(CommandError.Unsuccessful, "Input is empty.")); + try { - if (string.IsNullOrWhiteSpace(input)) - return Task.FromResult(TypeReaderResult.FromError(CommandError.Unsuccessful, "Input is empty.")); - try - { - var time = StoopidTime.FromInput(input); - return Task.FromResult(TypeReaderResult.FromSuccess(time)); - } - catch (Exception ex) - { - return Task.FromResult(TypeReaderResult.FromError(CommandError.Exception, ex.Message)); - } + var time = StoopidTime.FromInput(input); + return Task.FromResult(TypeReaderResult.FromSuccess(time)); + } + catch (Exception ex) + { + return Task.FromResult(TypeReaderResult.FromError(CommandError.Exception, ex.Message)); } } -} +} \ No newline at end of file diff --git a/src/NadekoBot/Common/Yml/CommentAttribute.cs b/src/NadekoBot/Common/Yml/CommentAttribute.cs index 1bf92bebc..0be600ffc 100644 --- a/src/NadekoBot/Common/Yml/CommentAttribute.cs +++ b/src/NadekoBot/Common/Yml/CommentAttribute.cs @@ -1,14 +1,11 @@ -using System; +namespace NadekoBot.Common.Yml; -namespace NadekoBot.Common.Yml +public class CommentAttribute : Attribute { - public class CommentAttribute : Attribute - { - public string Comment { get; } + public string Comment { get; } - public CommentAttribute(string comment) - { - Comment = comment; - } + public CommentAttribute(string comment) + { + Comment = comment; } } \ No newline at end of file diff --git a/src/NadekoBot/Common/Yml/CommentGatheringTypeInspector.cs b/src/NadekoBot/Common/Yml/CommentGatheringTypeInspector.cs index 74b76c3c7..cfb47d3be 100644 --- a/src/NadekoBot/Common/Yml/CommentGatheringTypeInspector.cs +++ b/src/NadekoBot/Common/Yml/CommentGatheringTypeInspector.cs @@ -1,73 +1,69 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using YamlDotNet.Core; +using YamlDotNet.Core; using YamlDotNet.Serialization; using YamlDotNet.Serialization.TypeInspectors; -namespace NadekoBot.Common.Yml +namespace NadekoBot.Common.Yml; + +public class CommentGatheringTypeInspector : TypeInspectorSkeleton { - public class CommentGatheringTypeInspector : TypeInspectorSkeleton + private readonly ITypeInspector innerTypeDescriptor; + + public CommentGatheringTypeInspector(ITypeInspector innerTypeDescriptor) { - private readonly ITypeInspector innerTypeDescriptor; + this.innerTypeDescriptor = innerTypeDescriptor ?? throw new ArgumentNullException("innerTypeDescriptor"); + } - public CommentGatheringTypeInspector(ITypeInspector innerTypeDescriptor) + public override IEnumerable GetProperties(Type type, object container) + { + return innerTypeDescriptor + .GetProperties(type, container) + .Select(d => new CommentsPropertyDescriptor(d)); + } + + private sealed class CommentsPropertyDescriptor : IPropertyDescriptor + { + private readonly IPropertyDescriptor baseDescriptor; + + public CommentsPropertyDescriptor(IPropertyDescriptor baseDescriptor) { - this.innerTypeDescriptor = innerTypeDescriptor ?? throw new ArgumentNullException("innerTypeDescriptor"); + this.baseDescriptor = baseDescriptor; + Name = baseDescriptor.Name; } - public override IEnumerable GetProperties(Type type, object container) - { - return innerTypeDescriptor - .GetProperties(type, container) - .Select(d => new CommentsPropertyDescriptor(d)); + public string Name { get; set; } + + public Type Type { get { return baseDescriptor.Type; } } + + public Type TypeOverride { + get { return baseDescriptor.TypeOverride; } + set { baseDescriptor.TypeOverride = value; } } - private sealed class CommentsPropertyDescriptor : IPropertyDescriptor + public int Order { get; set; } + + public ScalarStyle ScalarStyle { + get { return baseDescriptor.ScalarStyle; } + set { baseDescriptor.ScalarStyle = value; } + } + + public bool CanWrite { get { return baseDescriptor.CanWrite; } } + + public void Write(object target, object value) { - private readonly IPropertyDescriptor baseDescriptor; + baseDescriptor.Write(target, value); + } - public CommentsPropertyDescriptor(IPropertyDescriptor baseDescriptor) - { - this.baseDescriptor = baseDescriptor; - Name = baseDescriptor.Name; - } + public T GetCustomAttribute() where T : Attribute + { + return baseDescriptor.GetCustomAttribute(); + } - public string Name { get; set; } - - public Type Type { get { return baseDescriptor.Type; } } - - public Type TypeOverride { - get { return baseDescriptor.TypeOverride; } - set { baseDescriptor.TypeOverride = value; } - } - - public int Order { get; set; } - - public ScalarStyle ScalarStyle { - get { return baseDescriptor.ScalarStyle; } - set { baseDescriptor.ScalarStyle = value; } - } - - public bool CanWrite { get { return baseDescriptor.CanWrite; } } - - public void Write(object target, object value) - { - baseDescriptor.Write(target, value); - } - - public T GetCustomAttribute() where T : Attribute - { - return baseDescriptor.GetCustomAttribute(); - } - - public IObjectDescriptor Read(object target) - { - var comment = baseDescriptor.GetCustomAttribute(); - return comment != null - ? new CommentsObjectDescriptor(baseDescriptor.Read(target), comment.Comment) - : baseDescriptor.Read(target); - } + public IObjectDescriptor Read(object target) + { + var comment = baseDescriptor.GetCustomAttribute(); + return comment != null + ? new CommentsObjectDescriptor(baseDescriptor.Read(target), comment.Comment) + : baseDescriptor.Read(target); } } -} +} \ No newline at end of file diff --git a/src/NadekoBot/Common/Yml/CommentsObjectDescriptor.cs b/src/NadekoBot/Common/Yml/CommentsObjectDescriptor.cs index e6aae857a..f5600ee9e 100644 --- a/src/NadekoBot/Common/Yml/CommentsObjectDescriptor.cs +++ b/src/NadekoBot/Common/Yml/CommentsObjectDescriptor.cs @@ -1,24 +1,22 @@ -using System; -using YamlDotNet.Core; +using YamlDotNet.Core; using YamlDotNet.Serialization; -namespace NadekoBot.Common.Yml +namespace NadekoBot.Common.Yml; + +public sealed class CommentsObjectDescriptor : IObjectDescriptor { - public sealed class CommentsObjectDescriptor : IObjectDescriptor + private readonly IObjectDescriptor innerDescriptor; + + public CommentsObjectDescriptor(IObjectDescriptor innerDescriptor, string comment) { - private readonly IObjectDescriptor innerDescriptor; - - public CommentsObjectDescriptor(IObjectDescriptor innerDescriptor, string comment) - { - this.innerDescriptor = innerDescriptor; - this.Comment = comment; - } - - public string Comment { get; private set; } - - public object Value { get { return innerDescriptor.Value; } } - public Type Type { get { return innerDescriptor.Type; } } - public Type StaticType { get { return innerDescriptor.StaticType; } } - public ScalarStyle ScalarStyle { get { return innerDescriptor.ScalarStyle; } } + this.innerDescriptor = innerDescriptor; + this.Comment = comment; } -} + + public string Comment { get; private set; } + + public object Value { get { return innerDescriptor.Value; } } + public Type Type { get { return innerDescriptor.Type; } } + public Type StaticType { get { return innerDescriptor.StaticType; } } + public ScalarStyle ScalarStyle { get { return innerDescriptor.ScalarStyle; } } +} \ No newline at end of file diff --git a/src/NadekoBot/Common/Yml/CommentsObjectGraphVisitor.cs b/src/NadekoBot/Common/Yml/CommentsObjectGraphVisitor.cs index 20a344af2..ce6538c05 100644 --- a/src/NadekoBot/Common/Yml/CommentsObjectGraphVisitor.cs +++ b/src/NadekoBot/Common/Yml/CommentsObjectGraphVisitor.cs @@ -3,24 +3,23 @@ using YamlDotNet.Core.Events; using YamlDotNet.Serialization; using YamlDotNet.Serialization.ObjectGraphVisitors; -namespace NadekoBot.Common.Yml +namespace NadekoBot.Common.Yml; + +public class CommentsObjectGraphVisitor : ChainedObjectGraphVisitor { - public class CommentsObjectGraphVisitor : ChainedObjectGraphVisitor + public CommentsObjectGraphVisitor(IObjectGraphVisitor nextVisitor) + : base(nextVisitor) { - public CommentsObjectGraphVisitor(IObjectGraphVisitor nextVisitor) - : base(nextVisitor) - { - } - - public override bool EnterMapping(IPropertyDescriptor key, IObjectDescriptor value, IEmitter context) - { - var commentsDescriptor = value as CommentsObjectDescriptor; - if (commentsDescriptor != null && !string.IsNullOrWhiteSpace(commentsDescriptor.Comment)) - { - context.Emit(new Comment(commentsDescriptor.Comment.Replace("\n", "\n# "), false)); - } - - return base.EnterMapping(key, value, context); - } } -} + + public override bool EnterMapping(IPropertyDescriptor key, IObjectDescriptor value, IEmitter context) + { + var commentsDescriptor = value as CommentsObjectDescriptor; + if (commentsDescriptor != null && !string.IsNullOrWhiteSpace(commentsDescriptor.Comment)) + { + context.Emit(new Comment(commentsDescriptor.Comment.Replace("\n", "\n# "), false)); + } + + return base.EnterMapping(key, value, context); + } +} \ No newline at end of file diff --git a/src/NadekoBot/Common/Yml/MultilineScalarFlowStyleEmitter.cs b/src/NadekoBot/Common/Yml/MultilineScalarFlowStyleEmitter.cs index bade22207..058c8a06f 100644 --- a/src/NadekoBot/Common/Yml/MultilineScalarFlowStyleEmitter.cs +++ b/src/NadekoBot/Common/Yml/MultilineScalarFlowStyleEmitter.cs @@ -2,31 +2,30 @@ using YamlDotNet.Serialization; using YamlDotNet.Serialization.EventEmitters; -namespace NadekoBot.Common.Yml +namespace NadekoBot.Common.Yml; + +public class MultilineScalarFlowStyleEmitter : ChainedEventEmitter { - public class MultilineScalarFlowStyleEmitter : ChainedEventEmitter + public MultilineScalarFlowStyleEmitter(IEventEmitter nextEmitter) + : base(nextEmitter) { } + + public override void Emit(ScalarEventInfo eventInfo, IEmitter emitter) { - public MultilineScalarFlowStyleEmitter(IEventEmitter nextEmitter) - : base(nextEmitter) { } - public override void Emit(ScalarEventInfo eventInfo, IEmitter emitter) + if (typeof(string).IsAssignableFrom(eventInfo.Source.Type)) { - - if (typeof(string).IsAssignableFrom(eventInfo.Source.Type)) + string value = eventInfo.Source.Value as string; + if (!string.IsNullOrEmpty(value)) { - string value = eventInfo.Source.Value as string; - if (!string.IsNullOrEmpty(value)) - { - bool isMultiLine = value.IndexOfAny(new char[] { '\r', '\n', '\x85', '\x2028', '\x2029' }) >= 0; - if (isMultiLine) - eventInfo = new ScalarEventInfo(eventInfo.Source) - { - Style = ScalarStyle.Literal, - }; - } + bool isMultiLine = value.IndexOfAny(new char[] { '\r', '\n', '\x85', '\x2028', '\x2029' }) >= 0; + if (isMultiLine) + eventInfo = new ScalarEventInfo(eventInfo.Source) + { + Style = ScalarStyle.Literal, + }; } - - nextEmitter.Emit(eventInfo, emitter); } + + nextEmitter.Emit(eventInfo, emitter); } } \ No newline at end of file diff --git a/src/NadekoBot/Common/Yml/Rgba32Converter.cs b/src/NadekoBot/Common/Yml/Rgba32Converter.cs index 5f6e6bffe..fd4551ad7 100644 --- a/src/NadekoBot/Common/Yml/Rgba32Converter.cs +++ b/src/NadekoBot/Common/Yml/Rgba32Converter.cs @@ -1,52 +1,50 @@ -using System; -using System.Globalization; +using System.Globalization; using SixLabors.ImageSharp.PixelFormats; using YamlDotNet.Core; using YamlDotNet.Core.Events; using YamlDotNet.Serialization; -namespace NadekoBot.Common.Yml +namespace NadekoBot.Common.Yml; + +public class Rgba32Converter : IYamlTypeConverter { - public class Rgba32Converter : IYamlTypeConverter + public bool Accepts(Type type) { - public bool Accepts(Type type) - { - return type == typeof(Rgba32); - } - - public object ReadYaml(IParser parser, Type type) - { - var scalar = parser.Consume(); - var result = Rgba32.ParseHex(scalar.Value); - return result; - } - - public void WriteYaml(IEmitter emitter, object value, Type type) - { - var color = (Rgba32)value; - var val = (uint) (color.B << 0 | color.G << 8 | color.R << 16); - emitter.Emit(new Scalar(val.ToString("X6").ToLower())); - } + return type == typeof(Rgba32); } - - public class CultureInfoConverter : IYamlTypeConverter + + public object ReadYaml(IParser parser, Type type) { - public bool Accepts(Type type) - { - return type == typeof(CultureInfo); - } + var scalar = parser.Consume(); + var result = Rgba32.ParseHex(scalar.Value); + return result; + } - public object ReadYaml(IParser parser, Type type) - { - var scalar = parser.Consume(); - var result = new CultureInfo(scalar.Value); - return result; - } + public void WriteYaml(IEmitter emitter, object value, Type type) + { + var color = (Rgba32)value; + var val = (uint) (color.B << 0 | color.G << 8 | color.R << 16); + emitter.Emit(new Scalar(val.ToString("X6").ToLower())); + } +} + +public class CultureInfoConverter : IYamlTypeConverter +{ + public bool Accepts(Type type) + { + return type == typeof(CultureInfo); + } - public void WriteYaml(IEmitter emitter, object value, Type type) - { - var ci = (CultureInfo)value; - emitter.Emit(new Scalar(ci.Name)); - } + public object ReadYaml(IParser parser, Type type) + { + var scalar = parser.Consume(); + var result = new CultureInfo(scalar.Value); + return result; + } + + public void WriteYaml(IEmitter emitter, object value, Type type) + { + var ci = (CultureInfo)value; + emitter.Emit(new Scalar(ci.Name)); } } \ No newline at end of file diff --git a/src/NadekoBot/Common/Yml/UriConverter.cs b/src/NadekoBot/Common/Yml/UriConverter.cs index 2c9e50185..f02990c97 100644 --- a/src/NadekoBot/Common/Yml/UriConverter.cs +++ b/src/NadekoBot/Common/Yml/UriConverter.cs @@ -1,28 +1,26 @@ -using System; -using YamlDotNet.Core; +using YamlDotNet.Core; using YamlDotNet.Core.Events; using YamlDotNet.Serialization; -namespace NadekoBot.Common.Yml +namespace NadekoBot.Common.Yml; + +public class UriConverter : IYamlTypeConverter { - public class UriConverter : IYamlTypeConverter + public bool Accepts(Type type) { - public bool Accepts(Type type) - { - return type == typeof(Uri); - } + return type == typeof(Uri); + } - public object ReadYaml(IParser parser, Type type) - { - var scalar = parser.Consume(); - var result = new Uri(scalar.Value); - return result; - } + public object ReadYaml(IParser parser, Type type) + { + var scalar = parser.Consume(); + var result = new Uri(scalar.Value); + return result; + } - public void WriteYaml(IEmitter emitter, object value, Type type) - { - var uri = (Uri)value; - emitter.Emit(new Scalar(uri.ToString())); - } + public void WriteYaml(IEmitter emitter, object value, Type type) + { + var uri = (Uri)value; + emitter.Emit(new Scalar(uri.ToString())); } } \ No newline at end of file diff --git a/src/NadekoBot/Common/Yml/Yaml.cs b/src/NadekoBot/Common/Yml/Yaml.cs index 86ecd974d..ce16e3150 100644 --- a/src/NadekoBot/Common/Yml/Yaml.cs +++ b/src/NadekoBot/Common/Yml/Yaml.cs @@ -1,26 +1,25 @@ using YamlDotNet.Serialization; -namespace NadekoBot.Common.Yml -{ - public class Yaml - { - public static ISerializer Serializer => new SerializerBuilder() - .WithTypeInspector(inner => new CommentGatheringTypeInspector(inner)) - .WithEmissionPhaseObjectGraphVisitor(args => new CommentsObjectGraphVisitor(args.InnerVisitor)) - .WithEventEmitter(args => new MultilineScalarFlowStyleEmitter(args)) - .WithNamingConvention(YamlDotNet.Serialization.NamingConventions.CamelCaseNamingConvention.Instance) - .WithIndentedSequences() - .WithTypeConverter(new Rgba32Converter()) - .WithTypeConverter(new CultureInfoConverter()) - .WithTypeConverter(new UriConverter()) - .Build(); +namespace NadekoBot.Common.Yml; - public static IDeserializer Deserializer => new DeserializerBuilder() - .WithNamingConvention(YamlDotNet.Serialization.NamingConventions.CamelCaseNamingConvention.Instance) - .WithTypeConverter(new Rgba32Converter()) - .WithTypeConverter(new CultureInfoConverter()) - .WithTypeConverter(new UriConverter()) - .IgnoreUnmatchedProperties() - .Build(); - } +public class Yaml +{ + public static ISerializer Serializer => new SerializerBuilder() + .WithTypeInspector(inner => new CommentGatheringTypeInspector(inner)) + .WithEmissionPhaseObjectGraphVisitor(args => new CommentsObjectGraphVisitor(args.InnerVisitor)) + .WithEventEmitter(args => new MultilineScalarFlowStyleEmitter(args)) + .WithNamingConvention(YamlDotNet.Serialization.NamingConventions.CamelCaseNamingConvention.Instance) + .WithIndentedSequences() + .WithTypeConverter(new Rgba32Converter()) + .WithTypeConverter(new CultureInfoConverter()) + .WithTypeConverter(new UriConverter()) + .Build(); + + public static IDeserializer Deserializer => new DeserializerBuilder() + .WithNamingConvention(YamlDotNet.Serialization.NamingConventions.CamelCaseNamingConvention.Instance) + .WithTypeConverter(new Rgba32Converter()) + .WithTypeConverter(new CultureInfoConverter()) + .WithTypeConverter(new UriConverter()) + .IgnoreUnmatchedProperties() + .Build(); } \ No newline at end of file diff --git a/src/NadekoBot/Common/Yml/YamlHelper.cs b/src/NadekoBot/Common/Yml/YamlHelper.cs index 865edc169..a8a403e55 100644 --- a/src/NadekoBot/Common/Yml/YamlHelper.cs +++ b/src/NadekoBot/Common/Yml/YamlHelper.cs @@ -1,58 +1,57 @@ -namespace NadekoBot.Common.Yml +namespace NadekoBot.Common.Yml; + +public class YamlHelper { - public class YamlHelper + // https://github.com/aaubry/YamlDotNet/blob/0f4cc205e8b2dd8ef6589d96de32bf608a687c6f/YamlDotNet/Core/Scanner.cs#L1687 + /// + /// This is modified code from yamldotnet's repo which handles parsing unicode code points + /// it is needed as yamldotnet doesn't support unescaped unicode characters + /// + /// Unicode code point + /// Actual character + public static string UnescapeUnicodeCodePoint(string point) { - // https://github.com/aaubry/YamlDotNet/blob/0f4cc205e8b2dd8ef6589d96de32bf608a687c6f/YamlDotNet/Core/Scanner.cs#L1687 - /// - /// This is modified code from yamldotnet's repo which handles parsing unicode code points - /// it is needed as yamldotnet doesn't support unescaped unicode characters - /// - /// Unicode code point - /// Actual character - public static string UnescapeUnicodeCodePoint(string point) + var character = 0; + + // Scan the character value. + + foreach(var c in point) { - var character = 0; - - // Scan the character value. - - foreach(var c in point) - { - if (!IsHex(c)) - { - return point; - } - character = (character << 4) + AsHex(c); - } - - // Check the value and write the character. - - if (character >= 0xD800 && character <= 0xDFFF || character > 0x10FFFF) + if (!IsHex(c)) { return point; } + character = (character << 4) + AsHex(c); + } - return char.ConvertFromUtf32(character); - } - - public static bool IsHex(char c) + // Check the value and write the character. + + if (character >= 0xD800 && character <= 0xDFFF || character > 0x10FFFF) { - return - (c >= '0' && c <= '9') || - (c >= 'A' && c <= 'F') || - (c >= 'a' && c <= 'f'); + return point; } + + return char.ConvertFromUtf32(character); + } - public static int AsHex(char c) + public static bool IsHex(char c) + { + return + (c >= '0' && c <= '9') || + (c >= 'A' && c <= 'F') || + (c >= 'a' && c <= 'f'); + } + + public static int AsHex(char c) + { + if (c <= '9') { - if (c <= '9') - { - return c - '0'; - } - if (c <= 'F') - { - return c - 'A' + 10; - } - return c - 'a' + 10; + return c - '0'; } + if (c <= 'F') + { + return c - 'A' + 10; + } + return c - 'a' + 10; } } \ No newline at end of file diff --git a/src/NadekoBot/Db/Extensions/ClubExtensions.cs b/src/NadekoBot/Db/Extensions/ClubExtensions.cs index 4e1c760e6..f3c8fdc96 100644 --- a/src/NadekoBot/Db/Extensions/ClubExtensions.cs +++ b/src/NadekoBot/Db/Extensions/ClubExtensions.cs @@ -1,48 +1,45 @@ -using System.Linq; -using Microsoft.EntityFrameworkCore; -using System.Collections.Generic; +using Microsoft.EntityFrameworkCore; using NadekoBot.Db.Models; -namespace NadekoBot.Db +namespace NadekoBot.Db; + +public static class ClubExtensions { - public static class ClubExtensions - { - private static IQueryable Include(this DbSet clubs) - => clubs.Include(x => x.Owner) - .Include(x => x.Applicants) - .ThenInclude(x => x.User) - .Include(x => x.Bans) - .ThenInclude(x => x.User) - .Include(x => x.Users) - .AsQueryable(); - public static ClubInfo GetByOwner(this DbSet clubs, ulong userId) - => Include(clubs).FirstOrDefault(c => c.Owner.UserId == userId); + private static IQueryable Include(this DbSet clubs) + => clubs.Include(x => x.Owner) + .Include(x => x.Applicants) + .ThenInclude(x => x.User) + .Include(x => x.Bans) + .ThenInclude(x => x.User) + .Include(x => x.Users) + .AsQueryable(); + public static ClubInfo GetByOwner(this DbSet clubs, ulong userId) + => Include(clubs).FirstOrDefault(c => c.Owner.UserId == userId); - public static ClubInfo GetByOwnerOrAdmin(this DbSet clubs, ulong userId) - => Include(clubs).FirstOrDefault(c => c.Owner.UserId == userId - || c.Users.Any(u => u.UserId == userId && u.IsClubAdmin)); + public static ClubInfo GetByOwnerOrAdmin(this DbSet clubs, ulong userId) + => Include(clubs).FirstOrDefault(c => c.Owner.UserId == userId + || c.Users.Any(u => u.UserId == userId && u.IsClubAdmin)); - public static ClubInfo GetByMember(this DbSet clubs, ulong userId) - => Include(clubs).FirstOrDefault(c => c.Users.Any(u => u.UserId == userId)); + public static ClubInfo GetByMember(this DbSet clubs, ulong userId) + => Include(clubs).FirstOrDefault(c => c.Users.Any(u => u.UserId == userId)); - public static ClubInfo GetByName(this DbSet clubs, string name, int discrim) - => Include(clubs).FirstOrDefault(c => c.Name.ToUpper() == name.ToUpper() && c.Discrim == discrim); + public static ClubInfo GetByName(this DbSet clubs, string name, int discrim) + => Include(clubs).FirstOrDefault(c => c.Name.ToUpper() == name.ToUpper() && c.Discrim == discrim); - public static int GetNextDiscrim(this DbSet clubs, string name) - => Include(clubs) - .Where(x => x.Name.ToUpper() == name.ToUpper()) - .Select(x => x.Discrim) - .DefaultIfEmpty() - .Max() + 1; + public static int GetNextDiscrim(this DbSet clubs, string name) + => Include(clubs) + .Where(x => x.Name.ToUpper() == name.ToUpper()) + .Select(x => x.Discrim) + .DefaultIfEmpty() + .Max() + 1; - public static List GetClubLeaderboardPage(this DbSet clubs, int page) - { - return clubs - .AsNoTracking() - .OrderByDescending(x => x.Xp) - .Skip(page * 9) - .Take(9) - .ToList(); - } + public static List GetClubLeaderboardPage(this DbSet clubs, int page) + { + return clubs + .AsNoTracking() + .OrderByDescending(x => x.Xp) + .Skip(page * 9) + .Take(9) + .ToList(); } } \ No newline at end of file diff --git a/src/NadekoBot/Db/Extensions/CurrencyTransactionExtensions.cs b/src/NadekoBot/Db/Extensions/CurrencyTransactionExtensions.cs index fea1a6689..080bf4ec8 100644 --- a/src/NadekoBot/Db/Extensions/CurrencyTransactionExtensions.cs +++ b/src/NadekoBot/Db/Extensions/CurrencyTransactionExtensions.cs @@ -1,21 +1,18 @@ -using System.Collections.Generic; -using System.Linq; -using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore; using NadekoBot.Services.Database.Models; -namespace NadekoBot.Db +namespace NadekoBot.Db; + +public static class CurrencyTransactionExtensions { - public static class CurrencyTransactionExtensions + public static List GetPageFor(this DbSet set, ulong userId, int page) { - public static List GetPageFor(this DbSet set, ulong userId, int page) - { - return set.AsQueryable() - .AsNoTracking() - .Where(x => x.UserId == userId) - .OrderByDescending(x => x.DateAdded) - .Skip(15 * page) - .Take(15) - .ToList(); - } + return set.AsQueryable() + .AsNoTracking() + .Where(x => x.UserId == userId) + .OrderByDescending(x => x.DateAdded) + .Skip(15 * page) + .Take(15) + .ToList(); } } \ No newline at end of file diff --git a/src/NadekoBot/Db/Extensions/CustomReactionsExtensions.cs b/src/NadekoBot/Db/Extensions/CustomReactionsExtensions.cs index 16e36705a..30718dad3 100644 --- a/src/NadekoBot/Db/Extensions/CustomReactionsExtensions.cs +++ b/src/NadekoBot/Db/Extensions/CustomReactionsExtensions.cs @@ -1,30 +1,27 @@ using Microsoft.EntityFrameworkCore; -using System.Collections.Generic; -using System.Linq; using LinqToDB; using NadekoBot.Services.Database.Models; -namespace NadekoBot.Db +namespace NadekoBot.Db; + +public static class CustomReactionsExtensions { - public static class CustomReactionsExtensions + public static int ClearFromGuild(this DbSet crs, ulong guildId) { - public static int ClearFromGuild(this DbSet crs, ulong guildId) - { - return crs.Delete(x => x.GuildId == guildId); - } - - public static IEnumerable ForId(this DbSet crs, ulong id) - { - return crs - .AsNoTracking() - .AsQueryable() - .Where(x => x.GuildId == id) - .ToArray(); - } - - public static CustomReaction GetByGuildIdAndInput(this DbSet crs, ulong? guildId, string input) - { - return crs.FirstOrDefault(x => x.GuildId == guildId && x.Trigger.ToUpper() == input); - } + return crs.Delete(x => x.GuildId == guildId); } -} + + public static IEnumerable ForId(this DbSet crs, ulong id) + { + return crs + .AsNoTracking() + .AsQueryable() + .Where(x => x.GuildId == id) + .ToArray(); + } + + public static CustomReaction GetByGuildIdAndInput(this DbSet crs, ulong? guildId, string input) + { + return crs.FirstOrDefault(x => x.GuildId == guildId && x.Trigger.ToUpper() == input); + } +} \ No newline at end of file diff --git a/src/NadekoBot/Db/Extensions/DbExtensions.cs b/src/NadekoBot/Db/Extensions/DbExtensions.cs index dc251cb50..9ed6a45cc 100644 --- a/src/NadekoBot/Db/Extensions/DbExtensions.cs +++ b/src/NadekoBot/Db/Extensions/DbExtensions.cs @@ -1,12 +1,10 @@ -using System.Linq; -using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore; using NadekoBot.Services.Database.Models; -namespace NadekoBot.Db +namespace NadekoBot.Db; + +public static class DbExtensions { - public static class DbExtensions - { - public static T GetById(this DbSet set, int id) where T: DbEntity - => set.FirstOrDefault(x => x.Id == id); - } + public static T GetById(this DbSet set, int id) where T: DbEntity + => set.FirstOrDefault(x => x.Id == id); } \ No newline at end of file diff --git a/src/NadekoBot/Db/Extensions/DiscordUserExtensions.cs b/src/NadekoBot/Db/Extensions/DiscordUserExtensions.cs index eb7574d16..65a29a1e6 100644 --- a/src/NadekoBot/Db/Extensions/DiscordUserExtensions.cs +++ b/src/NadekoBot/Db/Extensions/DiscordUserExtensions.cs @@ -1,145 +1,142 @@ -using System; -using NadekoBot.Db.Models; -using System.Linq; +using NadekoBot.Db.Models; using Microsoft.EntityFrameworkCore; using Discord; -using System.Collections.Generic; using LinqToDB; using LinqToDB.EntityFrameworkCore; using NadekoBot.Services.Database; -namespace NadekoBot.Db +namespace NadekoBot.Db; + +public static class DiscordUserExtensions { - public static class DiscordUserExtensions + public static void EnsureUserCreated(this NadekoContext ctx, ulong userId, string username, string discrim, string avatarId) { - public static void EnsureUserCreated(this NadekoContext ctx, ulong userId, string username, string discrim, string avatarId) + ctx.DiscordUser + .ToLinqToDBTable() + .InsertOrUpdate(() => new() + { + UserId = userId, + Username = username, + Discriminator = discrim, + AvatarId = avatarId, + TotalXp = 0, + CurrencyAmount = 0 + }, + old => new() + { + Username = username, + Discriminator = discrim, + AvatarId = avatarId, + }, () => new() + { + UserId = userId + }); + } + + //temp is only used in updatecurrencystate, so that i don't overwrite real usernames/discrims with Unknown + public static DiscordUser GetOrCreateUser(this NadekoContext ctx, ulong userId, string username, string discrim, string avatarId) + { + ctx.EnsureUserCreated(userId, username, discrim, avatarId); + return ctx.DiscordUser + .Include(x => x.Club) + .First(u => u.UserId == userId); + } + + public static DiscordUser GetOrCreateUser(this NadekoContext ctx, IUser original) + => ctx.GetOrCreateUser(original.Id, original.Username, original.Discriminator, original.AvatarId); + + public static int GetUserGlobalRank(this DbSet users, ulong id) + { + return users.AsQueryable() + .Where(x => x.TotalXp > (users + .AsQueryable() + .Where(y => y.UserId == id) + .Select(y => y.TotalXp) + .FirstOrDefault())) + .Count() + 1; + } + + public static DiscordUser[] GetUsersXpLeaderboardFor(this DbSet users, int page) + { + return users.AsQueryable() + .OrderByDescending(x => x.TotalXp) + .Skip(page * 9) + .Take(9) + .AsEnumerable() + .ToArray(); + } + + public static List GetTopRichest(this DbSet users, ulong botId, int count, int page = 0) + { + return users.AsQueryable() + .Where(c => c.CurrencyAmount > 0 && botId != c.UserId) + .OrderByDescending(c => c.CurrencyAmount) + .Skip(page * 9) + .Take(count) + .ToList(); + } + + public static List GetTopRichest(this DbSet users, ulong botId, int count) + { + return users.AsQueryable() + .Where(c => c.CurrencyAmount > 0 && botId != c.UserId) + .OrderByDescending(c => c.CurrencyAmount) + .Take(count) + .ToList(); + } + + public static long GetUserCurrency(this DbSet users, ulong userId) => + users.AsNoTracking() + .FirstOrDefault(x => x.UserId == userId) + ?.CurrencyAmount ?? 0; + + public static void RemoveFromMany(this DbSet users, IEnumerable ids) + { + var items = users.AsQueryable().Where(x => ids.Contains(x.UserId)); + foreach (var item in items) { - ctx.DiscordUser - .ToLinqToDBTable() - .InsertOrUpdate(() => new() - { - UserId = userId, - Username = username, - Discriminator = discrim, - AvatarId = avatarId, - TotalXp = 0, - CurrencyAmount = 0 - }, - old => new() - { - Username = username, - Discriminator = discrim, - AvatarId = avatarId, - }, () => new() - { - UserId = userId - }); + item.CurrencyAmount = 0; } + } - //temp is only used in updatecurrencystate, so that i don't overwrite real usernames/discrims with Unknown - public static DiscordUser GetOrCreateUser(this NadekoContext ctx, ulong userId, string username, string discrim, string avatarId) + public static bool TryUpdateCurrencyState(this NadekoContext ctx, ulong userId, string name, string discrim, string avatarId, long amount, bool allowNegative = false) + { + if (amount == 0) + return true; + + // if remove - try to remove if he has more or equal than the amount + // and return number of rows > 0 (was there a change) + if (amount < 0 && !allowNegative) { - ctx.EnsureUserCreated(userId, username, discrim, avatarId); - return ctx.DiscordUser - .Include(x => x.Club) - .First(u => u.UserId == userId); - } - - public static DiscordUser GetOrCreateUser(this NadekoContext ctx, IUser original) - => ctx.GetOrCreateUser(original.Id, original.Username, original.Discriminator, original.AvatarId); - - public static int GetUserGlobalRank(this DbSet users, ulong id) - { - return users.AsQueryable() - .Where(x => x.TotalXp > (users - .AsQueryable() - .Where(y => y.UserId == id) - .Select(y => y.TotalXp) - .FirstOrDefault())) - .Count() + 1; - } - - public static DiscordUser[] GetUsersXpLeaderboardFor(this DbSet users, int page) - { - return users.AsQueryable() - .OrderByDescending(x => x.TotalXp) - .Skip(page * 9) - .Take(9) - .AsEnumerable() - .ToArray(); - } - - public static List GetTopRichest(this DbSet users, ulong botId, int count, int page = 0) - { - return users.AsQueryable() - .Where(c => c.CurrencyAmount > 0 && botId != c.UserId) - .OrderByDescending(c => c.CurrencyAmount) - .Skip(page * 9) - .Take(count) - .ToList(); - } - - public static List GetTopRichest(this DbSet users, ulong botId, int count) - { - return users.AsQueryable() - .Where(c => c.CurrencyAmount > 0 && botId != c.UserId) - .OrderByDescending(c => c.CurrencyAmount) - .Take(count) - .ToList(); - } - - public static long GetUserCurrency(this DbSet users, ulong userId) => - users.AsNoTracking() - .FirstOrDefault(x => x.UserId == userId) - ?.CurrencyAmount ?? 0; - - public static void RemoveFromMany(this DbSet users, IEnumerable ids) - { - var items = users.AsQueryable().Where(x => ids.Contains(x.UserId)); - foreach (var item in items) - { - item.CurrencyAmount = 0; - } - } - - public static bool TryUpdateCurrencyState(this NadekoContext ctx, ulong userId, string name, string discrim, string avatarId, long amount, bool allowNegative = false) - { - if (amount == 0) - return true; - - // if remove - try to remove if he has more or equal than the amount - // and return number of rows > 0 (was there a change) - if (amount < 0 && !allowNegative) - { - var rows = ctx.Database.ExecuteSqlInterpolated($@" + var rows = ctx.Database.ExecuteSqlInterpolated($@" UPDATE DiscordUser SET CurrencyAmount=CurrencyAmount+{amount} WHERE UserId={userId} AND CurrencyAmount>={-amount};"); - return rows > 0; - } + return rows > 0; + } - // if remove and negative is allowed, just remove without any condition - if (amount < 0 && allowNegative) - { - var rows = ctx.Database.ExecuteSqlInterpolated($@" + // if remove and negative is allowed, just remove without any condition + if (amount < 0 && allowNegative) + { + var rows = ctx.Database.ExecuteSqlInterpolated($@" UPDATE DiscordUser SET CurrencyAmount=CurrencyAmount+{amount} WHERE UserId={userId};"); - return rows > 0; - } + return rows > 0; + } - // if add - create a new user with default values if it doesn't exist - // if it exists, sum current amount with the new one, if it doesn't - // he just has the new amount - var updatedUserData = !string.IsNullOrWhiteSpace(name); - name = name ?? "Unknown"; - discrim = discrim ?? "????"; - avatarId = avatarId ?? ""; + // if add - create a new user with default values if it doesn't exist + // if it exists, sum current amount with the new one, if it doesn't + // he just has the new amount + var updatedUserData = !string.IsNullOrWhiteSpace(name); + name = name ?? "Unknown"; + discrim = discrim ?? "????"; + avatarId = avatarId ?? ""; - // just update the amount, there is no new user data - if (!updatedUserData) - { - var rows = ctx.Database.ExecuteSqlInterpolated($@" + // just update the amount, there is no new user data + if (!updatedUserData) + { + var rows = ctx.Database.ExecuteSqlInterpolated($@" UPDATE OR IGNORE DiscordUser SET CurrencyAmount=CurrencyAmount+{amount} WHERE UserId={userId}; @@ -148,10 +145,10 @@ INSERT OR IGNORE INTO DiscordUser (UserId, Username, Discriminator, AvatarId, Cu VALUES ({userId}, {name}, {discrim}, {avatarId}, {amount}, 0); "); - } - else - { - ctx.Database.ExecuteSqlInterpolated($@" + } + else + { + ctx.Database.ExecuteSqlInterpolated($@" UPDATE OR IGNORE DiscordUser SET CurrencyAmount=CurrencyAmount+{amount}, Username={name}, @@ -162,23 +159,22 @@ WHERE UserId={userId}; INSERT OR IGNORE INTO DiscordUser (UserId, Username, Discriminator, AvatarId, CurrencyAmount, TotalXp) VALUES ({userId}, {name}, {discrim}, {avatarId}, {amount}, 0); "); - } - return true; - } - - public static decimal GetTotalCurrency(this DbSet users) - { - return users - .Sum((Func)(x => x.CurrencyAmount)); - } - - public static decimal GetTopOnePercentCurrency(this DbSet users, ulong botId) - { - return users.AsQueryable() - .Where(x => x.UserId != botId) - .OrderByDescending(x => x.CurrencyAmount) - .Take(users.Count() / 100 == 0 ? 1 : users.Count() / 100) - .Sum(x => x.CurrencyAmount); } + return true; } -} + + public static decimal GetTotalCurrency(this DbSet users) + { + return users + .Sum((Func)(x => x.CurrencyAmount)); + } + + public static decimal GetTopOnePercentCurrency(this DbSet users, ulong botId) + { + return users.AsQueryable() + .Where(x => x.UserId != botId) + .OrderByDescending(x => x.CurrencyAmount) + .Take(users.Count() / 100 == 0 ? 1 : users.Count() / 100) + .Sum(x => x.CurrencyAmount); + } +} \ No newline at end of file diff --git a/src/NadekoBot/Db/Extensions/GuildConfigExtensions.cs b/src/NadekoBot/Db/Extensions/GuildConfigExtensions.cs index d8979e032..7084db33b 100644 --- a/src/NadekoBot/Db/Extensions/GuildConfigExtensions.cs +++ b/src/NadekoBot/Db/Extensions/GuildConfigExtensions.cs @@ -1,231 +1,227 @@ using NadekoBot.Services.Database.Models; -using System.Collections.Generic; -using System.Linq; using Microsoft.EntityFrameworkCore; -using System; using NadekoBot.Services.Database; using NadekoBot.Db.Models; -namespace NadekoBot.Db +namespace NadekoBot.Db; + +public static class GuildConfigExtensions { - public static class GuildConfigExtensions + public class GeneratingChannel { - public class GeneratingChannel - { - public ulong GuildId { get; set; } - public ulong ChannelId { get; set; } - } - - /// - /// Gets full stream role settings for the guild with the specified id. - /// - /// Db Context - /// Id of the guild to get stream role settings for. - /// Guild'p stream role settings - public static StreamRoleSettings GetStreamRoleSettings(this NadekoContext ctx, ulong guildId) - { - var conf = ctx.GuildConfigsForId(guildId, set => set.Include(y => y.StreamRole) - .Include(y => y.StreamRole.Whitelist) - .Include(y => y.StreamRole.Blacklist)); - - if (conf.StreamRole is null) - conf.StreamRole = new StreamRoleSettings(); - - return conf.StreamRole; - } - - private static List DefaultWarnPunishments => - new List() { - new WarningPunishment() { - Count = 3, - Punishment = PunishmentAction.Kick - }, - new WarningPunishment() { - Count = 5, - Punishment = PunishmentAction.Ban - } - }; - - private static IQueryable IncludeEverything(this DbSet configs) - { - // todo split query - return configs - .AsQueryable() - .Include(gc => gc.CommandCooldowns) - .Include(gc => gc.FollowedStreams) - .Include(gc => gc.StreamRole) - .Include(gc => gc.XpSettings) - .ThenInclude(x => x.ExclusionList) - .Include(gc => gc.DelMsgOnCmdChannels) - .Include(gc => gc.ReactionRoleMessages) - .ThenInclude(x => x.ReactionRoles) - ; - } - - public static IEnumerable GetAllGuildConfigs(this DbSet configs, List availableGuilds) - => configs - .IncludeEverything() - .AsNoTracking() - .Where(x => availableGuilds.Contains(x.GuildId)) - .ToList(); - - /// - /// Gets and creates if it doesn't exist a config for a guild. - /// - /// For which guild - /// Use to manipulate the set however you want - /// Config for the guild - public static GuildConfig GuildConfigsForId(this NadekoContext ctx, ulong guildId, Func, IQueryable> includes = null) - { - GuildConfig config; - - if (includes is null) - { - config = ctx - .GuildConfigs - .IncludeEverything() - .FirstOrDefault(c => c.GuildId == guildId); - } - else - { - var set = includes(ctx.GuildConfigs); - config = set.FirstOrDefault(c => c.GuildId == guildId); - } - - if (config is null) - { - ctx.GuildConfigs.Add((config = new GuildConfig - { - GuildId = guildId, - Permissions = Permissionv2.GetDefaultPermlist, - WarningsInitialized = true, - WarnPunishments = DefaultWarnPunishments, - })); - ctx.SaveChanges(); - } - - if (!config.WarningsInitialized) - { - config.WarningsInitialized = true; - config.WarnPunishments = DefaultWarnPunishments; - } - - return config; - } - - public static LogSetting LogSettingsFor(this NadekoContext ctx, ulong guildId) - { - var logSetting = ctx.LogSettings - .AsQueryable() - .Include(x => x.LogIgnores) - .Where(x => x.GuildId == guildId) - .FirstOrDefault(); - - if (logSetting is null) - { - ctx.LogSettings.Add(logSetting = new () - { - GuildId = guildId - }); - ctx.SaveChanges(); - } - - return logSetting; - } - - public static IEnumerable Permissionsv2ForAll(this DbSet configs, List include) - { - var query = configs.AsQueryable() - .Where(x => include.Contains(x.GuildId)) - .Include(gc => gc.Permissions); - - return query.ToList(); - } - - public static GuildConfig GcWithPermissionsv2For(this NadekoContext ctx, ulong guildId) - { - var config = ctx - .GuildConfigs - .AsQueryable() - .Where(gc => gc.GuildId == guildId) - .Include(gc => gc.Permissions) - .FirstOrDefault(); - - if (config is null) // if there is no guildconfig, create new one - { - ctx.GuildConfigs.Add((config = new GuildConfig - { - GuildId = guildId, - Permissions = Permissionv2.GetDefaultPermlist - })); - ctx.SaveChanges(); - } - else if (config.Permissions is null || !config.Permissions.Any()) // if no perms, add default ones - { - config.Permissions = Permissionv2.GetDefaultPermlist; - ctx.SaveChanges(); - } - - return config; - } - - public static IEnumerable GetFollowedStreams(this DbSet configs) - { - return configs - .AsQueryable() - .Include(x => x.FollowedStreams) - .SelectMany(gc => gc.FollowedStreams) - .ToArray(); - } - - public static IEnumerable GetFollowedStreams(this DbSet configs, List included) - { - return configs.AsQueryable() - .Where(gc => included.Contains(gc.GuildId)) - .Include(gc => gc.FollowedStreams) - .SelectMany(gc => gc.FollowedStreams) - .ToList(); - } - - public static void SetCleverbotEnabled(this DbSet configs, ulong id, bool cleverbotEnabled) - { - var conf = configs.FirstOrDefault(gc => gc.GuildId == id); - - if (conf is null) - return; - - conf.CleverbotEnabled = cleverbotEnabled; - } - - public static XpSettings XpSettingsFor(this NadekoContext ctx, ulong guildId) - { - var gc = ctx.GuildConfigsForId(guildId, - set => set.Include(x => x.XpSettings) - .ThenInclude(x => x.RoleRewards) - .Include(x => x.XpSettings) - .ThenInclude(x => x.CurrencyRewards) - .Include(x => x.XpSettings) - .ThenInclude(x => x.ExclusionList)); - - if (gc.XpSettings is null) - gc.XpSettings = new XpSettings(); - - return gc.XpSettings; - } - - public static IEnumerable GetGeneratingChannels(this DbSet configs) - { - return configs - .AsQueryable() - .Include(x => x.GenerateCurrencyChannelIds) - .Where(x => x.GenerateCurrencyChannelIds.Any()) - .SelectMany(x => x.GenerateCurrencyChannelIds) - .Select(x => new GeneratingChannel() - { - ChannelId = x.ChannelId, - GuildId = x.GuildConfig.GuildId - }) - .ToArray(); - } + public ulong GuildId { get; set; } + public ulong ChannelId { get; set; } } -} + + /// + /// Gets full stream role settings for the guild with the specified id. + /// + /// Db Context + /// Id of the guild to get stream role settings for. + /// Guild'p stream role settings + public static StreamRoleSettings GetStreamRoleSettings(this NadekoContext ctx, ulong guildId) + { + var conf = ctx.GuildConfigsForId(guildId, set => set.Include(y => y.StreamRole) + .Include(y => y.StreamRole.Whitelist) + .Include(y => y.StreamRole.Blacklist)); + + if (conf.StreamRole is null) + conf.StreamRole = new StreamRoleSettings(); + + return conf.StreamRole; + } + + private static List DefaultWarnPunishments => + new List() { + new WarningPunishment() { + Count = 3, + Punishment = PunishmentAction.Kick + }, + new WarningPunishment() { + Count = 5, + Punishment = PunishmentAction.Ban + } + }; + + private static IQueryable IncludeEverything(this DbSet configs) + { + // todo split query + return configs + .AsQueryable() + .Include(gc => gc.CommandCooldowns) + .Include(gc => gc.FollowedStreams) + .Include(gc => gc.StreamRole) + .Include(gc => gc.XpSettings) + .ThenInclude(x => x.ExclusionList) + .Include(gc => gc.DelMsgOnCmdChannels) + .Include(gc => gc.ReactionRoleMessages) + .ThenInclude(x => x.ReactionRoles) + ; + } + + public static IEnumerable GetAllGuildConfigs(this DbSet configs, List availableGuilds) + => configs + .IncludeEverything() + .AsNoTracking() + .Where(x => availableGuilds.Contains(x.GuildId)) + .ToList(); + + /// + /// Gets and creates if it doesn't exist a config for a guild. + /// + /// For which guild + /// Use to manipulate the set however you want + /// Config for the guild + public static GuildConfig GuildConfigsForId(this NadekoContext ctx, ulong guildId, Func, IQueryable> includes = null) + { + GuildConfig config; + + if (includes is null) + { + config = ctx + .GuildConfigs + .IncludeEverything() + .FirstOrDefault(c => c.GuildId == guildId); + } + else + { + var set = includes(ctx.GuildConfigs); + config = set.FirstOrDefault(c => c.GuildId == guildId); + } + + if (config is null) + { + ctx.GuildConfigs.Add((config = new GuildConfig + { + GuildId = guildId, + Permissions = Permissionv2.GetDefaultPermlist, + WarningsInitialized = true, + WarnPunishments = DefaultWarnPunishments, + })); + ctx.SaveChanges(); + } + + if (!config.WarningsInitialized) + { + config.WarningsInitialized = true; + config.WarnPunishments = DefaultWarnPunishments; + } + + return config; + } + + public static LogSetting LogSettingsFor(this NadekoContext ctx, ulong guildId) + { + var logSetting = ctx.LogSettings + .AsQueryable() + .Include(x => x.LogIgnores) + .Where(x => x.GuildId == guildId) + .FirstOrDefault(); + + if (logSetting is null) + { + ctx.LogSettings.Add(logSetting = new () + { + GuildId = guildId + }); + ctx.SaveChanges(); + } + + return logSetting; + } + + public static IEnumerable Permissionsv2ForAll(this DbSet configs, List include) + { + var query = configs.AsQueryable() + .Where(x => include.Contains(x.GuildId)) + .Include(gc => gc.Permissions); + + return query.ToList(); + } + + public static GuildConfig GcWithPermissionsv2For(this NadekoContext ctx, ulong guildId) + { + var config = ctx + .GuildConfigs + .AsQueryable() + .Where(gc => gc.GuildId == guildId) + .Include(gc => gc.Permissions) + .FirstOrDefault(); + + if (config is null) // if there is no guildconfig, create new one + { + ctx.GuildConfigs.Add((config = new GuildConfig + { + GuildId = guildId, + Permissions = Permissionv2.GetDefaultPermlist + })); + ctx.SaveChanges(); + } + else if (config.Permissions is null || !config.Permissions.Any()) // if no perms, add default ones + { + config.Permissions = Permissionv2.GetDefaultPermlist; + ctx.SaveChanges(); + } + + return config; + } + + public static IEnumerable GetFollowedStreams(this DbSet configs) + { + return configs + .AsQueryable() + .Include(x => x.FollowedStreams) + .SelectMany(gc => gc.FollowedStreams) + .ToArray(); + } + + public static IEnumerable GetFollowedStreams(this DbSet configs, List included) + { + return configs.AsQueryable() + .Where(gc => included.Contains(gc.GuildId)) + .Include(gc => gc.FollowedStreams) + .SelectMany(gc => gc.FollowedStreams) + .ToList(); + } + + public static void SetCleverbotEnabled(this DbSet configs, ulong id, bool cleverbotEnabled) + { + var conf = configs.FirstOrDefault(gc => gc.GuildId == id); + + if (conf is null) + return; + + conf.CleverbotEnabled = cleverbotEnabled; + } + + public static XpSettings XpSettingsFor(this NadekoContext ctx, ulong guildId) + { + var gc = ctx.GuildConfigsForId(guildId, + set => set.Include(x => x.XpSettings) + .ThenInclude(x => x.RoleRewards) + .Include(x => x.XpSettings) + .ThenInclude(x => x.CurrencyRewards) + .Include(x => x.XpSettings) + .ThenInclude(x => x.ExclusionList)); + + if (gc.XpSettings is null) + gc.XpSettings = new XpSettings(); + + return gc.XpSettings; + } + + public static IEnumerable GetGeneratingChannels(this DbSet configs) + { + return configs + .AsQueryable() + .Include(x => x.GenerateCurrencyChannelIds) + .Where(x => x.GenerateCurrencyChannelIds.Any()) + .SelectMany(x => x.GenerateCurrencyChannelIds) + .Select(x => new GeneratingChannel() + { + ChannelId = x.ChannelId, + GuildId = x.GuildConfig.GuildId + }) + .ToArray(); + } +} \ No newline at end of file diff --git a/src/NadekoBot/Db/Extensions/MusicPlayerSettingsExtensions.cs b/src/NadekoBot/Db/Extensions/MusicPlayerSettingsExtensions.cs index cb343bd35..d111f53d0 100644 --- a/src/NadekoBot/Db/Extensions/MusicPlayerSettingsExtensions.cs +++ b/src/NadekoBot/Db/Extensions/MusicPlayerSettingsExtensions.cs @@ -2,29 +2,28 @@ using Microsoft.EntityFrameworkCore; using NadekoBot.Services.Database.Models; -namespace NadekoBot.Db +namespace NadekoBot.Db; + +public static class MusicPlayerSettingsExtensions { - public static class MusicPlayerSettingsExtensions + public static async Task ForGuildAsync(this DbSet settings, ulong guildId) { - public static async Task ForGuildAsync(this DbSet settings, ulong guildId) + var toReturn = await settings + .AsQueryable() + .FirstOrDefaultAsync(x => x.GuildId == guildId); + + if (toReturn is null) { - var toReturn = await settings - .AsQueryable() - .FirstOrDefaultAsync(x => x.GuildId == guildId); - - if (toReturn is null) + var newSettings = new MusicPlayerSettings() { - var newSettings = new MusicPlayerSettings() - { - GuildId = guildId, - PlayerRepeat = PlayerRepeatType.Queue - }; + GuildId = guildId, + PlayerRepeat = PlayerRepeatType.Queue + }; - await settings.AddAsync(newSettings); - return newSettings; - } - - return toReturn; + await settings.AddAsync(newSettings); + return newSettings; } + + return toReturn; } } \ No newline at end of file diff --git a/src/NadekoBot/Db/Extensions/MusicPlaylistExtensions.cs b/src/NadekoBot/Db/Extensions/MusicPlaylistExtensions.cs index 587075e58..b9b9b8158 100644 --- a/src/NadekoBot/Db/Extensions/MusicPlaylistExtensions.cs +++ b/src/NadekoBot/Db/Extensions/MusicPlaylistExtensions.cs @@ -1,29 +1,25 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore; using NadekoBot.Services.Database.Models; -namespace NadekoBot.Db +namespace NadekoBot.Db; + +public static class MusicPlaylistExtensions { - public static class MusicPlaylistExtensions + public static List GetPlaylistsOnPage(this DbSet playlists, int num) { - public static List GetPlaylistsOnPage(this DbSet playlists, int num) - { - if (num < 1) - throw new IndexOutOfRangeException(); + if (num < 1) + throw new IndexOutOfRangeException(); - return playlists - .AsQueryable() - .Skip((num - 1) * 20) - .Take(20) - .Include(pl => pl.Songs) - .ToList(); - } - - public static MusicPlaylist GetWithSongs(this DbSet playlists, int id) => - playlists - .Include(mpl => mpl.Songs) - .FirstOrDefault(mpl => mpl.Id == id); + return playlists + .AsQueryable() + .Skip((num - 1) * 20) + .Take(20) + .Include(pl => pl.Songs) + .ToList(); } -} + + public static MusicPlaylist GetWithSongs(this DbSet playlists, int id) => + playlists + .Include(mpl => mpl.Songs) + .FirstOrDefault(mpl => mpl.Id == id); +} \ No newline at end of file diff --git a/src/NadekoBot/Db/Extensions/PollExtensions.cs b/src/NadekoBot/Db/Extensions/PollExtensions.cs index 81c727bdb..c719333ab 100644 --- a/src/NadekoBot/Db/Extensions/PollExtensions.cs +++ b/src/NadekoBot/Db/Extensions/PollExtensions.cs @@ -1,46 +1,41 @@ -using NadekoBot.Db.Models; -using System.Collections.Generic; -using System.Linq; -using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore; using NadekoBot.Services.Database; using NadekoBot.Services.Database.Models; -using NadekoBot.Db; -namespace NadekoBot.Db +namespace NadekoBot.Db; + +public static class PollExtensions { - public static class PollExtensions + public static IEnumerable GetAllPolls(this DbSet polls) { - public static IEnumerable GetAllPolls(this DbSet polls) - { - return polls.Include(x => x.Answers) - .Include(x => x.Votes) - .ToArray(); - } - - public static void RemovePoll(this NadekoContext ctx, int id) - { - var p = ctx - .Poll - .Include(x => x.Answers) - .Include(x => x.Votes) - .FirstOrDefault(x => x.Id == id); - - if (p is null) - return; - - if (p.Votes != null) - { - ctx.RemoveRange(p.Votes); - p.Votes.Clear(); - } - - if (p.Answers != null) - { - ctx.RemoveRange(p.Answers); - p.Answers.Clear(); - } - - ctx.Poll.Remove(p); - } + return polls.Include(x => x.Answers) + .Include(x => x.Votes) + .ToArray(); } -} + + public static void RemovePoll(this NadekoContext ctx, int id) + { + var p = ctx + .Poll + .Include(x => x.Answers) + .Include(x => x.Votes) + .FirstOrDefault(x => x.Id == id); + + if (p is null) + return; + + if (p.Votes != null) + { + ctx.RemoveRange(p.Votes); + p.Votes.Clear(); + } + + if (p.Answers != null) + { + ctx.RemoveRange(p.Answers); + p.Answers.Clear(); + } + + ctx.Poll.Remove(p); + } +} \ No newline at end of file diff --git a/src/NadekoBot/Db/Extensions/QuoteExtensions.cs b/src/NadekoBot/Db/Extensions/QuoteExtensions.cs index cdcb56d83..03e006fc6 100644 --- a/src/NadekoBot/Db/Extensions/QuoteExtensions.cs +++ b/src/NadekoBot/Db/Extensions/QuoteExtensions.cs @@ -1,58 +1,55 @@ using NadekoBot.Services.Database.Models; -using System.Collections.Generic; -using System.Linq; using System.Threading.Tasks; using Microsoft.EntityFrameworkCore; using NadekoBot.Common; -namespace NadekoBot.Db +namespace NadekoBot.Db; + +public static class QuoteExtensions { - public static class QuoteExtensions + public static IEnumerable GetForGuild(this DbSet quotes, ulong guildId) { - public static IEnumerable GetForGuild(this DbSet quotes, ulong guildId) - { - return quotes.AsQueryable().Where(x => x.GuildId == guildId); - } + return quotes.AsQueryable().Where(x => x.GuildId == guildId); + } - public static IEnumerable GetGroup(this DbSet quotes, ulong guildId, int page, OrderType order) - { - var q = quotes.AsQueryable().Where(x => x.GuildId == guildId); - if (order == OrderType.Keyword) - q = q.OrderBy(x => x.Keyword); - else - q = q.OrderBy(x => x.Id); + public static IEnumerable GetGroup(this DbSet quotes, ulong guildId, int page, OrderType order) + { + var q = quotes.AsQueryable().Where(x => x.GuildId == guildId); + if (order == OrderType.Keyword) + q = q.OrderBy(x => x.Keyword); + else + q = q.OrderBy(x => x.Id); - return q.Skip(15 * page).Take(15).ToArray(); - } + return q.Skip(15 * page).Take(15).ToArray(); + } - public static async Task GetRandomQuoteByKeywordAsync(this DbSet quotes, ulong guildId, string keyword) - { - var rng = new NadekoRandom(); - return (await quotes.AsQueryable() + public static async Task GetRandomQuoteByKeywordAsync(this DbSet quotes, ulong guildId, string keyword) + { + var rng = new NadekoRandom(); + return (await quotes.AsQueryable() .Where(q => q.GuildId == guildId && q.Keyword == keyword) .ToListAsync()) - .OrderBy(q => rng.Next()) - .FirstOrDefault(); - } + .OrderBy(q => rng.Next()) + .FirstOrDefault(); + } - public static async Task SearchQuoteKeywordTextAsync(this DbSet quotes, ulong guildId, string keyword, string text) - { - var rngk = new NadekoRandom(); - return (await quotes.AsQueryable() + public static async Task SearchQuoteKeywordTextAsync(this DbSet quotes, ulong guildId, string keyword, string text) + { + var rngk = new NadekoRandom(); + return (await quotes.AsQueryable() .Where(q => q.GuildId == guildId && q.Keyword == keyword && EF.Functions.Like(q.Text.ToUpper(), $"%{text.ToUpper()}%") - // && q.Text.Contains(text, StringComparison.OrdinalIgnoreCase) - ) + // && q.Text.Contains(text, StringComparison.OrdinalIgnoreCase) + ) .ToListAsync()) - .OrderBy(q => rngk.Next()) - .FirstOrDefault(); - } - - public static void RemoveAllByKeyword(this DbSet quotes, ulong guildId, string keyword) - { - quotes.RemoveRange(quotes.AsQueryable().Where(x => x.GuildId == guildId && x.Keyword.ToUpper() == keyword)); - } - + .OrderBy(q => rngk.Next()) + .FirstOrDefault(); } -} + + public static void RemoveAllByKeyword(this DbSet quotes, ulong guildId, string keyword) + { + quotes.RemoveRange(quotes.AsQueryable().Where(x => x.GuildId == guildId && x.Keyword.ToUpper() == keyword)); + } + +} \ No newline at end of file diff --git a/src/NadekoBot/Db/Extensions/ReminderExtensions.cs b/src/NadekoBot/Db/Extensions/ReminderExtensions.cs index 6cc60eaa9..d26028b79 100644 --- a/src/NadekoBot/Db/Extensions/ReminderExtensions.cs +++ b/src/NadekoBot/Db/Extensions/ReminderExtensions.cs @@ -1,29 +1,26 @@ using NadekoBot.Services.Database.Models; using Microsoft.EntityFrameworkCore; -using System.Collections.Generic; -using System.Linq; -namespace NadekoBot.Db +namespace NadekoBot.Db; + +public static class ReminderExtensions { - public static class ReminderExtensions - { - public static IEnumerable GetIncludedReminders(this DbSet reminders, IEnumerable guildIds) - => reminders.AsQueryable() - .Where(x => guildIds.Contains(x.ServerId) || x.ServerId == 0) - .ToList(); + public static IEnumerable GetIncludedReminders(this DbSet reminders, IEnumerable guildIds) + => reminders.AsQueryable() + .Where(x => guildIds.Contains(x.ServerId) || x.ServerId == 0) + .ToList(); - public static IEnumerable RemindersFor(this DbSet reminders, ulong userId, int page) - => reminders.AsQueryable() - .Where(x => x.UserId == userId) - .OrderBy(x => x.DateAdded) - .Skip(page * 10) - .Take(10); + public static IEnumerable RemindersFor(this DbSet reminders, ulong userId, int page) + => reminders.AsQueryable() + .Where(x => x.UserId == userId) + .OrderBy(x => x.DateAdded) + .Skip(page * 10) + .Take(10); - public static IEnumerable RemindersForServer(this DbSet reminders, ulong serverId, int page) - => reminders.AsQueryable() - .Where(x => x.ServerId == serverId) - .OrderBy(x => x.DateAdded) - .Skip(page * 10) - .Take(10); - } -} + public static IEnumerable RemindersForServer(this DbSet reminders, ulong serverId, int page) + => reminders.AsQueryable() + .Where(x => x.ServerId == serverId) + .OrderBy(x => x.DateAdded) + .Skip(page * 10) + .Take(10); +} \ No newline at end of file diff --git a/src/NadekoBot/Db/Extensions/SelfAssignableRolesExtensions.cs b/src/NadekoBot/Db/Extensions/SelfAssignableRolesExtensions.cs index 15e5b8e0d..9fa5eada0 100644 --- a/src/NadekoBot/Db/Extensions/SelfAssignableRolesExtensions.cs +++ b/src/NadekoBot/Db/Extensions/SelfAssignableRolesExtensions.cs @@ -1,26 +1,23 @@ using NadekoBot.Services.Database.Models; -using System.Collections.Generic; -using System.Linq; using Microsoft.EntityFrameworkCore; -namespace NadekoBot.Db +namespace NadekoBot.Db; + +public static class SelfAssignableRolesExtensions { - public static class SelfAssignableRolesExtensions + public static bool DeleteByGuildAndRoleId(this DbSet roles, ulong guildId, ulong roleId) { - public static bool DeleteByGuildAndRoleId(this DbSet roles, ulong guildId, ulong roleId) - { - var role = roles.FirstOrDefault(s => s.GuildId == guildId && s.RoleId == roleId); + var role = roles.FirstOrDefault(s => s.GuildId == guildId && s.RoleId == roleId); - if (role is null) - return false; + if (role is null) + return false; - roles.Remove(role); - return true; - } - - public static IEnumerable GetFromGuild(this DbSet roles, ulong guildId) - => roles.AsQueryable() - .Where(s => s.GuildId == guildId) - .ToArray(); + roles.Remove(role); + return true; } -} + + public static IEnumerable GetFromGuild(this DbSet roles, ulong guildId) + => roles.AsQueryable() + .Where(s => s.GuildId == guildId) + .ToArray(); +} \ No newline at end of file diff --git a/src/NadekoBot/Db/Extensions/UserXpExtensions.cs b/src/NadekoBot/Db/Extensions/UserXpExtensions.cs index 8efcf48a5..2500bd819 100644 --- a/src/NadekoBot/Db/Extensions/UserXpExtensions.cs +++ b/src/NadekoBot/Db/Extensions/UserXpExtensions.cs @@ -1,84 +1,81 @@ -using System.Linq; -using Microsoft.EntityFrameworkCore; -using System.Collections.Generic; +using Microsoft.EntityFrameworkCore; using LinqToDB; using NadekoBot.Services.Database; using NadekoBot.Services.Database.Models; -namespace NadekoBot.Db +namespace NadekoBot.Db; + +public static class UserXpExtensions { - public static class UserXpExtensions + public static UserXpStats GetOrCreateUserXpStats(this NadekoContext ctx, ulong guildId, ulong userId) { - public static UserXpStats GetOrCreateUserXpStats(this NadekoContext ctx, ulong guildId, ulong userId) - { - var usr = ctx.UserXpStats.FirstOrDefault(x => x.UserId == userId && x.GuildId == guildId); + var usr = ctx.UserXpStats.FirstOrDefault(x => x.UserId == userId && x.GuildId == guildId); - if (usr is null) + if (usr is null) + { + ctx.Add(usr = new UserXpStats() { - ctx.Add(usr = new UserXpStats() - { - Xp = 0, - UserId = userId, - NotifyOnLevelUp = XpNotificationLocation.None, - GuildId = guildId, - }); - } - - return usr; + Xp = 0, + UserId = userId, + NotifyOnLevelUp = XpNotificationLocation.None, + GuildId = guildId, + }); } - public static List GetUsersFor(this DbSet xps, ulong guildId, int page) - { - return xps - .AsQueryable() - .AsNoTracking() - .Where(x => x.GuildId == guildId) - .OrderByDescending(x => x.Xp + x.AwardedXp) - .Skip(page * 9) - .Take(9) - .ToList(); - } - - public static List GetTopUserXps(this DbSet xps, ulong guildId, int count) - { - return xps - .AsQueryable() - .AsNoTracking() - .Where(x => x.GuildId == guildId) - .OrderByDescending(x => x.Xp + x.AwardedXp) - .Take(count) - .ToList(); - } - - public static int GetUserGuildRanking(this DbSet xps, ulong userId, ulong guildId) - { - // @"SELECT COUNT(*) + 1 - //FROM UserXpStats - //WHERE GuildId = @p1 AND ((Xp + AwardedXp) > (SELECT Xp + AwardedXp - // FROM UserXpStats - // WHERE UserId = @p2 AND GuildId = @p1 - // LIMIT 1));"; - - return xps - .AsQueryable() - .AsNoTracking() - .Where(x => x.GuildId == guildId && ((x.Xp + x.AwardedXp) > - (xps.AsQueryable() - .Where(y => y.UserId == userId && y.GuildId == guildId) - .Select(y => y.Xp + y.AwardedXp) - .FirstOrDefault()) - )) - .Count() + 1; - } - - public static void ResetGuildUserXp(this DbSet xps, ulong userId, ulong guildId) - { - xps.Delete(x => x.UserId == userId && x.GuildId == guildId); - } - - public static void ResetGuildXp(this DbSet xps, ulong guildId) - { - xps.Delete(x => x.GuildId == guildId); - } + return usr; } -} + + public static List GetUsersFor(this DbSet xps, ulong guildId, int page) + { + return xps + .AsQueryable() + .AsNoTracking() + .Where(x => x.GuildId == guildId) + .OrderByDescending(x => x.Xp + x.AwardedXp) + .Skip(page * 9) + .Take(9) + .ToList(); + } + + public static List GetTopUserXps(this DbSet xps, ulong guildId, int count) + { + return xps + .AsQueryable() + .AsNoTracking() + .Where(x => x.GuildId == guildId) + .OrderByDescending(x => x.Xp + x.AwardedXp) + .Take(count) + .ToList(); + } + + public static int GetUserGuildRanking(this DbSet xps, ulong userId, ulong guildId) + { + // @"SELECT COUNT(*) + 1 + //FROM UserXpStats + //WHERE GuildId = @p1 AND ((Xp + AwardedXp) > (SELECT Xp + AwardedXp + // FROM UserXpStats + // WHERE UserId = @p2 AND GuildId = @p1 + // LIMIT 1));"; + + return xps + .AsQueryable() + .AsNoTracking() + .Where(x => x.GuildId == guildId && ((x.Xp + x.AwardedXp) > + (xps.AsQueryable() + .Where(y => y.UserId == userId && y.GuildId == guildId) + .Select(y => y.Xp + y.AwardedXp) + .FirstOrDefault()) + )) + .Count() + 1; + } + + public static void ResetGuildUserXp(this DbSet xps, ulong userId, ulong guildId) + { + xps.Delete(x => x.UserId == userId && x.GuildId == guildId); + } + + public static void ResetGuildXp(this DbSet xps, ulong guildId) + { + xps.Delete(x => x.GuildId == guildId); + } +} \ No newline at end of file diff --git a/src/NadekoBot/Db/Extensions/WaifuExtensions.cs b/src/NadekoBot/Db/Extensions/WaifuExtensions.cs index 6c98993ce..7b7a409b7 100644 --- a/src/NadekoBot/Db/Extensions/WaifuExtensions.cs +++ b/src/NadekoBot/Db/Extensions/WaifuExtensions.cs @@ -1,164 +1,160 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore; using NadekoBot.Services.Database; using NadekoBot.Services.Database.Models; using NadekoBot.Db.Models; -namespace NadekoBot.Db -{ - public class WaifuInfoStats - { - public string FullName { get; set; } - public int Price { get; set; } - public string ClaimerName { get; set; } - public string AffinityName { get; set; } - public int AffinityCount { get; set; } - public int DivorceCount { get; set; } - public int ClaimCount { get; set; } - public List Items { get; set; } - public List Claims { get; set; } - public List Fans { get; set; } - } - - public static class WaifuExtensions - { - public static WaifuInfo ByWaifuUserId(this DbSet waifus, ulong userId, Func, IQueryable> includes = null) - { - if (includes is null) - { - return waifus.Include(wi => wi.Waifu) - .Include(wi => wi.Affinity) - .Include(wi => wi.Claimer) - .Include(wi => wi.Items) - .FirstOrDefault(wi => wi.Waifu.UserId == userId); - } +namespace NadekoBot.Db; - return includes(waifus) - .AsQueryable() +public class WaifuInfoStats +{ + public string FullName { get; set; } + public int Price { get; set; } + public string ClaimerName { get; set; } + public string AffinityName { get; set; } + public int AffinityCount { get; set; } + public int DivorceCount { get; set; } + public int ClaimCount { get; set; } + public List Items { get; set; } + public List Claims { get; set; } + public List Fans { get; set; } +} + +public static class WaifuExtensions +{ + public static WaifuInfo ByWaifuUserId(this DbSet waifus, ulong userId, Func, IQueryable> includes = null) + { + if (includes is null) + { + return waifus.Include(wi => wi.Waifu) + .Include(wi => wi.Affinity) + .Include(wi => wi.Claimer) + .Include(wi => wi.Items) .FirstOrDefault(wi => wi.Waifu.UserId == userId); } - public static IEnumerable GetTop(this DbSet waifus, int count, int skip = 0) - { - if (count < 0) - throw new ArgumentOutOfRangeException(nameof(count)); - if (count == 0) - return new List(); + return includes(waifus) + .AsQueryable() + .FirstOrDefault(wi => wi.Waifu.UserId == userId); + } - return waifus.Include(wi => wi.Waifu) - .Include(wi => wi.Affinity) - .Include(wi => wi.Claimer) - .OrderByDescending(wi => wi.Price) - .Skip(skip) - .Take(count) - .Select(x => new WaifuLbResult - { - Affinity = x.Affinity == null ? null : x.Affinity.Username, - AffinityDiscrim = x.Affinity == null ? null : x.Affinity.Discriminator, - Claimer = x.Claimer == null ? null : x.Claimer.Username, - ClaimerDiscrim = x.Claimer == null ? null : x.Claimer.Discriminator, - Username = x.Waifu.Username, - Discrim = x.Waifu.Discriminator, - Price = x.Price, - }) - .ToList(); + public static IEnumerable GetTop(this DbSet waifus, int count, int skip = 0) + { + if (count < 0) + throw new ArgumentOutOfRangeException(nameof(count)); + if (count == 0) + return new List(); - } + return waifus.Include(wi => wi.Waifu) + .Include(wi => wi.Affinity) + .Include(wi => wi.Claimer) + .OrderByDescending(wi => wi.Price) + .Skip(skip) + .Take(count) + .Select(x => new WaifuLbResult + { + Affinity = x.Affinity == null ? null : x.Affinity.Username, + AffinityDiscrim = x.Affinity == null ? null : x.Affinity.Discriminator, + Claimer = x.Claimer == null ? null : x.Claimer.Username, + ClaimerDiscrim = x.Claimer == null ? null : x.Claimer.Discriminator, + Username = x.Waifu.Username, + Discrim = x.Waifu.Discriminator, + Price = x.Price, + }) + .ToList(); - public static decimal GetTotalValue(this DbSet waifus) - { - return waifus - .AsQueryable() - .Where(x => x.ClaimerId != null) - .Sum(x => x.Price); - } + } - public static ulong GetWaifuUserId(this DbSet waifus, ulong ownerId, string name) - { - return waifus - .AsQueryable() - .AsNoTracking() - .Where(x => x.Claimer.UserId == ownerId - && x.Waifu.Username + "#" + x.Waifu.Discriminator == name) - .Select(x => x.Waifu.UserId) - .FirstOrDefault(); - } + public static decimal GetTotalValue(this DbSet waifus) + { + return waifus + .AsQueryable() + .Where(x => x.ClaimerId != null) + .Sum(x => x.Price); + } + + public static ulong GetWaifuUserId(this DbSet waifus, ulong ownerId, string name) + { + return waifus + .AsQueryable() + .AsNoTracking() + .Where(x => x.Claimer.UserId == ownerId + && x.Waifu.Username + "#" + x.Waifu.Discriminator == name) + .Select(x => x.Waifu.UserId) + .FirstOrDefault(); + } - public static WaifuInfoStats GetWaifuInfo(this NadekoContext ctx, ulong userId) - { - ctx.Database.ExecuteSqlInterpolated($@" + public static WaifuInfoStats GetWaifuInfo(this NadekoContext ctx, ulong userId) + { + ctx.Database.ExecuteSqlInterpolated($@" INSERT OR IGNORE INTO WaifuInfo (AffinityId, ClaimerId, Price, WaifuId) VALUES ({null}, {null}, {1}, (SELECT Id FROM DiscordUser WHERE UserId={userId}));"); - var toReturn = ctx.WaifuInfo + var toReturn = ctx.WaifuInfo + .AsQueryable() + .Where(w => w.WaifuId == ctx.Set() .AsQueryable() - .Where(w => w.WaifuId == ctx.Set() + .Where(u => u.UserId == userId) + .Select(u => u.Id).FirstOrDefault()) + .Select(w => new WaifuInfoStats + { + FullName = ctx.Set() .AsQueryable() .Where(u => u.UserId == userId) - .Select(u => u.Id).FirstOrDefault()) - .Select(w => new WaifuInfoStats - { - FullName = ctx.Set() - .AsQueryable() - .Where(u => u.UserId == userId) - .Select(u => u.Username + "#" + u.Discriminator) - .FirstOrDefault(), + .Select(u => u.Username + "#" + u.Discriminator) + .FirstOrDefault(), - AffinityCount = ctx.Set() - .AsQueryable() - .Count(x => x.UserId == w.WaifuId && - x.UpdateType == WaifuUpdateType.AffinityChanged && - x.NewId != null), + AffinityCount = ctx.Set() + .AsQueryable() + .Count(x => x.UserId == w.WaifuId && + x.UpdateType == WaifuUpdateType.AffinityChanged && + x.NewId != null), - AffinityName = ctx.Set() - .AsQueryable() - .Where(u => u.Id == w.AffinityId) - .Select(u => u.Username + "#" + u.Discriminator) - .FirstOrDefault(), + AffinityName = ctx.Set() + .AsQueryable() + .Where(u => u.Id == w.AffinityId) + .Select(u => u.Username + "#" + u.Discriminator) + .FirstOrDefault(), - ClaimCount = ctx.WaifuInfo - .AsQueryable() - .Count(x => x.ClaimerId == w.WaifuId), + ClaimCount = ctx.WaifuInfo + .AsQueryable() + .Count(x => x.ClaimerId == w.WaifuId), - ClaimerName = ctx.Set() - .AsQueryable() - .Where(u => u.Id == w.ClaimerId) - .Select(u => u.Username + "#" + u.Discriminator) - .FirstOrDefault(), + ClaimerName = ctx.Set() + .AsQueryable() + .Where(u => u.Id == w.ClaimerId) + .Select(u => u.Username + "#" + u.Discriminator) + .FirstOrDefault(), - DivorceCount = ctx - .Set() - .AsQueryable() - .Count(x => x.OldId == w.WaifuId && - x.NewId == null && - x.UpdateType == WaifuUpdateType.Claimed), + DivorceCount = ctx + .Set() + .AsQueryable() + .Count(x => x.OldId == w.WaifuId && + x.NewId == null && + x.UpdateType == WaifuUpdateType.Claimed), - Price = w.Price, + Price = w.Price, - Claims = ctx.WaifuInfo - .AsQueryable() - .Include(x => x.Waifu) - .Where(x => x.ClaimerId == w.WaifuId) - .Select(x => x.Waifu.Username + "#" + x.Waifu.Discriminator) - .ToList(), + Claims = ctx.WaifuInfo + .AsQueryable() + .Include(x => x.Waifu) + .Where(x => x.ClaimerId == w.WaifuId) + .Select(x => x.Waifu.Username + "#" + x.Waifu.Discriminator) + .ToList(), - Fans = ctx.WaifuInfo - .AsQueryable() - .Include(x => x.Waifu) - .Where(x => x.AffinityId == w.WaifuId) - .Select(x => x.Waifu.Username + "#" + x.Waifu.Discriminator) - .ToList(), + Fans = ctx.WaifuInfo + .AsQueryable() + .Include(x => x.Waifu) + .Where(x => x.AffinityId == w.WaifuId) + .Select(x => x.Waifu.Username + "#" + x.Waifu.Discriminator) + .ToList(), - Items = w.Items, - }) + Items = w.Items, + }) .FirstOrDefault(); - if (toReturn is null) - return null; + if (toReturn is null) + return null; - return toReturn; - } + return toReturn; } } \ No newline at end of file diff --git a/src/NadekoBot/Db/Extensions/WarningExtensions.cs b/src/NadekoBot/Db/Extensions/WarningExtensions.cs index d69a83d35..f66369a92 100644 --- a/src/NadekoBot/Db/Extensions/WarningExtensions.cs +++ b/src/NadekoBot/Db/Extensions/WarningExtensions.cs @@ -1,56 +1,53 @@ using NadekoBot.Services.Database.Models; using Microsoft.EntityFrameworkCore; -using System.Linq; using System.Threading.Tasks; -using System; -namespace NadekoBot.Db +namespace NadekoBot.Db; + +public static class WarningExtensions { - public static class WarningExtensions + public static Warning[] ForId(this DbSet warnings, ulong guildId, ulong userId) { - public static Warning[] ForId(this DbSet warnings, ulong guildId, ulong userId) - { - var query = warnings.AsQueryable() - .Where(x => x.GuildId == guildId && x.UserId == userId) - .OrderByDescending(x => x.DateAdded); + var query = warnings.AsQueryable() + .Where(x => x.GuildId == guildId && x.UserId == userId) + .OrderByDescending(x => x.DateAdded); - return query.ToArray(); - } - - public static bool Forgive(this DbSet warnings, ulong guildId, ulong userId, string mod, int index) - { - if (index < 0) - throw new ArgumentOutOfRangeException(nameof(index)); - - var warn = warnings.AsQueryable().Where(x => x.GuildId == guildId && x.UserId == userId) - .OrderByDescending(x => x.DateAdded) - .Skip(index) - .FirstOrDefault(); - - if (warn is null || warn.Forgiven) - return false; - - warn.Forgiven = true; - warn.ForgivenBy = mod; - return true; - } - - public static async Task ForgiveAll(this DbSet warnings, ulong guildId, ulong userId, string mod) - { - await warnings.AsQueryable().Where(x => x.GuildId == guildId && x.UserId == userId) - .ForEachAsync(x => - { - if (x.Forgiven != true) - { - x.Forgiven = true; - x.ForgivenBy = mod; - } - }); - } - - public static Warning[] GetForGuild(this DbSet warnings, ulong id) - { - return warnings.AsQueryable().Where(x => x.GuildId == id).ToArray(); - } + return query.ToArray(); } -} + + public static bool Forgive(this DbSet warnings, ulong guildId, ulong userId, string mod, int index) + { + if (index < 0) + throw new ArgumentOutOfRangeException(nameof(index)); + + var warn = warnings.AsQueryable().Where(x => x.GuildId == guildId && x.UserId == userId) + .OrderByDescending(x => x.DateAdded) + .Skip(index) + .FirstOrDefault(); + + if (warn is null || warn.Forgiven) + return false; + + warn.Forgiven = true; + warn.ForgivenBy = mod; + return true; + } + + public static async Task ForgiveAll(this DbSet warnings, ulong guildId, ulong userId, string mod) + { + await warnings.AsQueryable().Where(x => x.GuildId == guildId && x.UserId == userId) + .ForEachAsync(x => + { + if (x.Forgiven != true) + { + x.Forgiven = true; + x.ForgivenBy = mod; + } + }); + } + + public static Warning[] GetForGuild(this DbSet warnings, ulong id) + { + return warnings.AsQueryable().Where(x => x.GuildId == id).ToArray(); + } +} \ No newline at end of file diff --git a/src/NadekoBot/Db/Models/AntiProtection.cs b/src/NadekoBot/Db/Models/AntiProtection.cs index 106028003..d03a37cca 100644 --- a/src/NadekoBot/Db/Models/AntiProtection.cs +++ b/src/NadekoBot/Db/Models/AntiProtection.cs @@ -1,68 +1,65 @@ -using System; -using System.Collections.Generic; -namespace NadekoBot.Services.Database.Models +namespace NadekoBot.Services.Database.Models; + +public class AntiRaidSetting : DbEntity { - public class AntiRaidSetting : DbEntity - { - public int GuildConfigId { get; set; } - public GuildConfig GuildConfig { get; set; } + public int GuildConfigId { get; set; } + public GuildConfig GuildConfig { get; set; } - public int UserThreshold { get; set; } - public int Seconds { get; set; } - public PunishmentAction Action { get; set; } + public int UserThreshold { get; set; } + public int Seconds { get; set; } + public PunishmentAction Action { get; set; } - /// - /// Duration of the punishment, in minutes. This works only for supported Actions, like: - /// Mute, Chatmute, Voicemute, etc... - /// - public int PunishDuration { get; set; } - } - - public class AntiSpamSetting : DbEntity - { - public int GuildConfigId { get; set; } - public GuildConfig GuildConfig { get; set; } - - public PunishmentAction Action { get; set; } - public int MessageThreshold { get; set; } = 3; - public int MuteTime { get; set; } = 0; - public ulong? RoleId { get; set; } - public HashSet IgnoredChannels { get; set; } = new HashSet(); - } - - public class AntiAltSetting - { - public int Id { get; set; } - public int GuildConfigId { get; set; } - public TimeSpan MinAge { get; set; } - public PunishmentAction Action { get; set; } - public int ActionDurationMinutes { get; set; } - public ulong? RoleId { get; set; } - } - - public enum PunishmentAction - { - Mute, - Kick, - Ban, - Softban, - RemoveRoles, - ChatMute, - VoiceMute, - AddRole - } - - public class AntiSpamIgnore : DbEntity - { - public ulong ChannelId { get; set; } - - public override int GetHashCode() => ChannelId.GetHashCode(); - - public override bool Equals(object obj) - { - return obj is AntiSpamIgnore inst - ? inst.ChannelId == ChannelId - : false; - } - } + /// + /// Duration of the punishment, in minutes. This works only for supported Actions, like: + /// Mute, Chatmute, Voicemute, etc... + /// + public int PunishDuration { get; set; } } + +public class AntiSpamSetting : DbEntity +{ + public int GuildConfigId { get; set; } + public GuildConfig GuildConfig { get; set; } + + public PunishmentAction Action { get; set; } + public int MessageThreshold { get; set; } = 3; + public int MuteTime { get; set; } = 0; + public ulong? RoleId { get; set; } + public HashSet IgnoredChannels { get; set; } = new HashSet(); +} + +public class AntiAltSetting +{ + public int Id { get; set; } + public int GuildConfigId { get; set; } + public TimeSpan MinAge { get; set; } + public PunishmentAction Action { get; set; } + public int ActionDurationMinutes { get; set; } + public ulong? RoleId { get; set; } +} + +public enum PunishmentAction +{ + Mute, + Kick, + Ban, + Softban, + RemoveRoles, + ChatMute, + VoiceMute, + AddRole +} + +public class AntiSpamIgnore : DbEntity +{ + public ulong ChannelId { get; set; } + + public override int GetHashCode() => ChannelId.GetHashCode(); + + public override bool Equals(object obj) + { + return obj is AntiSpamIgnore inst + ? inst.ChannelId == ChannelId + : false; + } +} \ No newline at end of file diff --git a/src/NadekoBot/Db/Models/AutoCommand.cs b/src/NadekoBot/Db/Models/AutoCommand.cs index 304988ef8..c527e3215 100644 --- a/src/NadekoBot/Db/Models/AutoCommand.cs +++ b/src/NadekoBot/Db/Models/AutoCommand.cs @@ -1,14 +1,13 @@ -namespace NadekoBot.Services.Database.Models +namespace NadekoBot.Services.Database.Models; + +public class AutoCommand : DbEntity { - public class AutoCommand : DbEntity - { - public string CommandText { get; set; } - public ulong ChannelId { get; set; } - public string ChannelName { get; set; } - public ulong? GuildId { get; set; } - public string GuildName { get; set; } - public ulong? VoiceChannelId {get; set; } - public string VoiceChannelName { get; set; } - public int Interval { get; set; } - } + public string CommandText { get; set; } + public ulong ChannelId { get; set; } + public string ChannelName { get; set; } + public ulong? GuildId { get; set; } + public string GuildName { get; set; } + public ulong? VoiceChannelId {get; set; } + public string VoiceChannelName { get; set; } + public int Interval { get; set; } } \ No newline at end of file diff --git a/src/NadekoBot/Db/Models/AutoTranslateChannel.cs b/src/NadekoBot/Db/Models/AutoTranslateChannel.cs index d131c6f96..8b97d6454 100644 --- a/src/NadekoBot/Db/Models/AutoTranslateChannel.cs +++ b/src/NadekoBot/Db/Models/AutoTranslateChannel.cs @@ -1,12 +1,9 @@ -using System.Collections.Generic; +namespace NadekoBot.Services.Database.Models; -namespace NadekoBot.Services.Database.Models +public class AutoTranslateChannel : DbEntity { - public class AutoTranslateChannel : DbEntity - { - public ulong GuildId { get; set; } - public ulong ChannelId { get; set; } - public bool AutoDelete { get; set; } - public IList Users { get; set; } = new List(); - } + public ulong GuildId { get; set; } + public ulong ChannelId { get; set; } + public bool AutoDelete { get; set; } + public IList Users { get; set; } = new List(); } \ No newline at end of file diff --git a/src/NadekoBot/Db/Models/AutoTranslateUser.cs b/src/NadekoBot/Db/Models/AutoTranslateUser.cs index 7e6102c03..7b3db6aef 100644 --- a/src/NadekoBot/Db/Models/AutoTranslateUser.cs +++ b/src/NadekoBot/Db/Models/AutoTranslateUser.cs @@ -1,11 +1,10 @@ -namespace NadekoBot.Services.Database.Models +namespace NadekoBot.Services.Database.Models; + +public class AutoTranslateUser : DbEntity { - public class AutoTranslateUser : DbEntity - { - public int ChannelId { get; set; } - public AutoTranslateChannel Channel { get; set; } - public ulong UserId { get; set; } - public string Source { get; set; } - public string Target { get; set; } - } + public int ChannelId { get; set; } + public AutoTranslateChannel Channel { get; set; } + public ulong UserId { get; set; } + public string Source { get; set; } + public string Target { get; set; } } \ No newline at end of file diff --git a/src/NadekoBot/Db/Models/BanTemplate.cs b/src/NadekoBot/Db/Models/BanTemplate.cs index c8235db05..19d7d60e0 100644 --- a/src/NadekoBot/Db/Models/BanTemplate.cs +++ b/src/NadekoBot/Db/Models/BanTemplate.cs @@ -1,8 +1,7 @@ -namespace NadekoBot.Services.Database.Models +namespace NadekoBot.Services.Database.Models; + +public class BanTemplate : DbEntity { - public class BanTemplate : DbEntity - { - public ulong GuildId { get; set; } - public string Text { get; set; } - } + public ulong GuildId { get; set; } + public string Text { get; set; } } \ No newline at end of file diff --git a/src/NadekoBot/Db/Models/BlacklistEntry.cs b/src/NadekoBot/Db/Models/BlacklistEntry.cs index dbf0bbfd8..80e6ac5ea 100644 --- a/src/NadekoBot/Db/Models/BlacklistEntry.cs +++ b/src/NadekoBot/Db/Models/BlacklistEntry.cs @@ -1,18 +1,14 @@ -using System; -using System.Collections.Generic; +namespace NadekoBot.Services.Database.Models; -namespace NadekoBot.Services.Database.Models +public class BlacklistEntry : DbEntity { - public class BlacklistEntry : DbEntity - { - public ulong ItemId { get; set; } - public BlacklistType Type { get; set; } - } + public ulong ItemId { get; set; } + public BlacklistType Type { get; set; } +} - public enum BlacklistType - { - Server, - Channel, - User - } +public enum BlacklistType +{ + Server, + Channel, + User } \ No newline at end of file diff --git a/src/NadekoBot/Db/Models/ClubInfo.cs b/src/NadekoBot/Db/Models/ClubInfo.cs index a9ea52732..8a3b2a220 100644 --- a/src/NadekoBot/Db/Models/ClubInfo.cs +++ b/src/NadekoBot/Db/Models/ClubInfo.cs @@ -1,49 +1,47 @@ -using System.Collections.Generic; -using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations; using NadekoBot.Services.Database.Models; -namespace NadekoBot.Db.Models +namespace NadekoBot.Db.Models; + +public class ClubInfo : DbEntity { - public class ClubInfo : DbEntity - { - [MaxLength(20)] - public string Name { get; set; } - public int Discrim { get; set; } + [MaxLength(20)] + public string Name { get; set; } + public int Discrim { get; set; } - public string ImageUrl { get; set; } = ""; - public int MinimumLevelReq { get; set; } = 5; - public int Xp { get; set; } = 0; + public string ImageUrl { get; set; } = ""; + public int MinimumLevelReq { get; set; } = 5; + public int Xp { get; set; } = 0; - public int OwnerId { get; set; } - public DiscordUser Owner { get; set; } + public int OwnerId { get; set; } + public DiscordUser Owner { get; set; } - public List Users { get; set; } = new List(); + public List Users { get; set; } = new List(); - public List Applicants { get; set; } = new List(); - public List Bans { get; set; } = new List(); - public string Description { get; set; } + public List Applicants { get; set; } = new List(); + public List Bans { get; set; } = new List(); + public string Description { get; set; } - public override string ToString() - { - return Name + "#" + Discrim; - } - } - - public class ClubApplicants + public override string ToString() { - public int ClubId { get; set; } - public ClubInfo Club { get; set; } - - public int UserId { get; set; } - public DiscordUser User { get; set; } - } - - public class ClubBans - { - public int ClubId { get; set; } - public ClubInfo Club { get; set; } - - public int UserId { get; set; } - public DiscordUser User { get; set; } + return Name + "#" + Discrim; } } + +public class ClubApplicants +{ + public int ClubId { get; set; } + public ClubInfo Club { get; set; } + + public int UserId { get; set; } + public DiscordUser User { get; set; } +} + +public class ClubBans +{ + public int ClubId { get; set; } + public ClubInfo Club { get; set; } + + public int UserId { get; set; } + public DiscordUser User { get; set; } +} \ No newline at end of file diff --git a/src/NadekoBot/Db/Models/CommandAlias.cs b/src/NadekoBot/Db/Models/CommandAlias.cs index 7823d6f01..89ed8943c 100644 --- a/src/NadekoBot/Db/Models/CommandAlias.cs +++ b/src/NadekoBot/Db/Models/CommandAlias.cs @@ -1,8 +1,7 @@ -namespace NadekoBot.Services.Database.Models +namespace NadekoBot.Services.Database.Models; + +public class CommandAlias : DbEntity { - public class CommandAlias : DbEntity - { - public string Trigger { get; set; } - public string Mapping { get; set; } - } -} + public string Trigger { get; set; } + public string Mapping { get; set; } +} \ No newline at end of file diff --git a/src/NadekoBot/Db/Models/CommandCooldown.cs b/src/NadekoBot/Db/Models/CommandCooldown.cs index 6adcf2adb..eaf603526 100644 --- a/src/NadekoBot/Db/Models/CommandCooldown.cs +++ b/src/NadekoBot/Db/Models/CommandCooldown.cs @@ -1,8 +1,7 @@ -namespace NadekoBot.Services.Database.Models +namespace NadekoBot.Services.Database.Models; + +public class CommandCooldown : DbEntity { - public class CommandCooldown : DbEntity - { - public int Seconds { get; set; } - public string CommandName { get; set; } - } -} + public int Seconds { get; set; } + public string CommandName { get; set; } +} \ No newline at end of file diff --git a/src/NadekoBot/Db/Models/CurrencyTransaction.cs b/src/NadekoBot/Db/Models/CurrencyTransaction.cs index 816f1c5f7..a9ec396a8 100644 --- a/src/NadekoBot/Db/Models/CurrencyTransaction.cs +++ b/src/NadekoBot/Db/Models/CurrencyTransaction.cs @@ -1,16 +1,15 @@ -namespace NadekoBot.Services.Database.Models -{ - public class CurrencyTransaction : DbEntity - { - public long Amount { get; set; } - public string Reason { get; set; } - public ulong UserId { get; set; } +namespace NadekoBot.Services.Database.Models; - public CurrencyTransaction Clone() => new CurrencyTransaction - { - Amount = Amount, - Reason = Reason, - UserId = UserId, - }; - } -} +public class CurrencyTransaction : DbEntity +{ + public long Amount { get; set; } + public string Reason { get; set; } + public ulong UserId { get; set; } + + public CurrencyTransaction Clone() => new CurrencyTransaction + { + Amount = Amount, + Reason = Reason, + UserId = UserId, + }; +} \ No newline at end of file diff --git a/src/NadekoBot/Db/Models/CustomReaction.cs b/src/NadekoBot/Db/Models/CustomReaction.cs index 4842fd955..eb72526f4 100644 --- a/src/NadekoBot/Db/Models/CustomReaction.cs +++ b/src/NadekoBot/Db/Models/CustomReaction.cs @@ -1,33 +1,27 @@ -using Newtonsoft.Json; -using System; -using System.ComponentModel.DataAnnotations.Schema; -using System.Text.RegularExpressions; +namespace NadekoBot.Services.Database.Models; -namespace NadekoBot.Services.Database.Models +public class CustomReaction : DbEntity { - public class CustomReaction : DbEntity - { - public ulong? GuildId { get; set; } - public string Response { get; set; } - public string Trigger { get; set; } + public ulong? GuildId { get; set; } + public string Response { get; set; } + public string Trigger { get; set; } - public bool AutoDeleteTrigger { get; set; } - public bool DmResponse { get; set; } - public bool ContainsAnywhere { get; set; } - public bool AllowTarget { get; set; } - public string Reactions { get; set; } + public bool AutoDeleteTrigger { get; set; } + public bool DmResponse { get; set; } + public bool ContainsAnywhere { get; set; } + public bool AllowTarget { get; set; } + public string Reactions { get; set; } - public string[] GetReactions() => - string.IsNullOrWhiteSpace(Reactions) - ? Array.Empty() - : Reactions.Split("@@@"); + public string[] GetReactions() => + string.IsNullOrWhiteSpace(Reactions) + ? Array.Empty() + : Reactions.Split("@@@"); - public bool IsGlobal() => GuildId is null || GuildId == 0; - } - - public class ReactionResponse : DbEntity - { - public bool OwnerOnly { get; set; } - public string Text { get; set; } - } + public bool IsGlobal() => GuildId is null || GuildId == 0; } + +public class ReactionResponse : DbEntity +{ + public bool OwnerOnly { get; set; } + public string Text { get; set; } +} \ No newline at end of file diff --git a/src/NadekoBot/Db/Models/DbEntity.cs b/src/NadekoBot/Db/Models/DbEntity.cs index e727851c9..04221b89c 100644 --- a/src/NadekoBot/Db/Models/DbEntity.cs +++ b/src/NadekoBot/Db/Models/DbEntity.cs @@ -1,12 +1,10 @@ -using System; -using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations; -namespace NadekoBot.Services.Database.Models +namespace NadekoBot.Services.Database.Models; + +public class DbEntity { - public class DbEntity - { - [Key] - public int Id { get; set; } - public DateTime? DateAdded { get; set; } = DateTime.UtcNow; - } -} + [Key] + public int Id { get; set; } + public DateTime? DateAdded { get; set; } = DateTime.UtcNow; +} \ No newline at end of file diff --git a/src/NadekoBot/Db/Models/DelMsgOnCmdChannel.cs b/src/NadekoBot/Db/Models/DelMsgOnCmdChannel.cs index 6aaf9ae0a..a7b8c28c0 100644 --- a/src/NadekoBot/Db/Models/DelMsgOnCmdChannel.cs +++ b/src/NadekoBot/Db/Models/DelMsgOnCmdChannel.cs @@ -1,19 +1,18 @@ -namespace NadekoBot.Services.Database.Models +namespace NadekoBot.Services.Database.Models; + +public class DelMsgOnCmdChannel : DbEntity { - public class DelMsgOnCmdChannel : DbEntity + public ulong ChannelId { get; set; } + public bool State { get; set; } + + public override int GetHashCode() { - public ulong ChannelId { get; set; } - public bool State { get; set; } - - public override int GetHashCode() - { - return ChannelId.GetHashCode(); - } - - public override bool Equals(object obj) - { - return obj is DelMsgOnCmdChannel x - && x.ChannelId == ChannelId; - } + return ChannelId.GetHashCode(); } -} + + public override bool Equals(object obj) + { + return obj is DelMsgOnCmdChannel x + && x.ChannelId == ChannelId; + } +} \ No newline at end of file diff --git a/src/NadekoBot/Db/Models/DiscordPemOverride.cs b/src/NadekoBot/Db/Models/DiscordPemOverride.cs index 76340b170..55d102742 100644 --- a/src/NadekoBot/Db/Models/DiscordPemOverride.cs +++ b/src/NadekoBot/Db/Models/DiscordPemOverride.cs @@ -1,12 +1,11 @@ using Discord; -namespace NadekoBot.Services.Database.Models +namespace NadekoBot.Services.Database.Models; + +public class DiscordPermOverride : DbEntity { - public class DiscordPermOverride : DbEntity - { - public GuildPerm Perm { get; set; } + public GuildPerm Perm { get; set; } - public ulong? GuildId { get; set; } - public string Command { get; set; } - } + public ulong? GuildId { get; set; } + public string Command { get; set; } } \ No newline at end of file diff --git a/src/NadekoBot/Db/Models/DiscordUser.cs b/src/NadekoBot/Db/Models/DiscordUser.cs index 934123ad5..60b3db891 100644 --- a/src/NadekoBot/Db/Models/DiscordUser.cs +++ b/src/NadekoBot/Db/Models/DiscordUser.cs @@ -1,38 +1,36 @@ -using System; -using NadekoBot.Services.Database.Models; +using NadekoBot.Services.Database.Models; -namespace NadekoBot.Db.Models +namespace NadekoBot.Db.Models; + +public class DiscordUser : DbEntity { - public class DiscordUser : DbEntity + public ulong UserId { get; set; } + public string Username { get; set; } + public string Discriminator { get; set; } + public string AvatarId { get; set; } + + public ClubInfo Club { get; set; } + public bool IsClubAdmin { get; set; } + + public int TotalXp { get; set; } + public DateTime LastLevelUp { get; set; } = DateTime.UtcNow; + public DateTime LastXpGain { get; set; } = DateTime.MinValue; + public XpNotificationLocation NotifyOnLevelUp { get; set; } + + public long CurrencyAmount { get; set; } + + public override bool Equals(object obj) { - public ulong UserId { get; set; } - public string Username { get; set; } - public string Discriminator { get; set; } - public string AvatarId { get; set; } - - public ClubInfo Club { get; set; } - public bool IsClubAdmin { get; set; } - - public int TotalXp { get; set; } - public DateTime LastLevelUp { get; set; } = DateTime.UtcNow; - public DateTime LastXpGain { get; set; } = DateTime.MinValue; - public XpNotificationLocation NotifyOnLevelUp { get; set; } - - public long CurrencyAmount { get; set; } - - public override bool Equals(object obj) - { - return obj is DiscordUser du - ? du.UserId == UserId - : false; - } - - public override int GetHashCode() - { - return UserId.GetHashCode(); - } - - public override string ToString() => - Username + "#" + Discriminator; + return obj is DiscordUser du + ? du.UserId == UserId + : false; } -} + + public override int GetHashCode() + { + return UserId.GetHashCode(); + } + + public override string ToString() => + Username + "#" + Discriminator; +} \ No newline at end of file diff --git a/src/NadekoBot/Db/Models/Event.cs b/src/NadekoBot/Db/Models/Event.cs index 86e7c0aed..126892e10 100644 --- a/src/NadekoBot/Db/Models/Event.cs +++ b/src/NadekoBot/Db/Models/Event.cs @@ -1,47 +1,44 @@ -using System.Collections.Generic; +namespace NadekoBot.Services.Database.Models; -namespace NadekoBot.Services.Database.Models +public class CurrencyEvent { - public class CurrencyEvent + public enum Type { - public enum Type - { - Reaction, - GameStatus, - //NotRaid, - } - - public ulong ServerId { get; set; } - public ulong ChannelId { get; set; } - public ulong MessageId { get; set; } - public Type EventType { get; set; } - - /// - /// Amount of currency that the user will be rewarded. - /// - public long Amount { get; set; } - /// - /// Maximum amount of currency that can be handed out. - /// - public long PotSize { get; set; } - public List AwardedUsers { get; set; } - - /// - /// Used as extra data storage for events which need it. - /// - public ulong ExtraId { get; set; } - /// - /// May be used for some future event. - /// - public ulong ExtraId2 { get; set; } - /// - /// May be used for some future event. - /// - public string ExtraString { get; set; } + Reaction, + GameStatus, + //NotRaid, } - public class AwardedUser - { + public ulong ServerId { get; set; } + public ulong ChannelId { get; set; } + public ulong MessageId { get; set; } + public Type EventType { get; set; } - } + /// + /// Amount of currency that the user will be rewarded. + /// + public long Amount { get; set; } + /// + /// Maximum amount of currency that can be handed out. + /// + public long PotSize { get; set; } + public List AwardedUsers { get; set; } + + /// + /// Used as extra data storage for events which need it. + /// + public ulong ExtraId { get; set; } + /// + /// May be used for some future event. + /// + public ulong ExtraId2 { get; set; } + /// + /// May be used for some future event. + /// + public string ExtraString { get; set; } } + +public class AwardedUser +{ + +} \ No newline at end of file diff --git a/src/NadekoBot/Db/Models/FeedSub.cs b/src/NadekoBot/Db/Models/FeedSub.cs index d783a863f..d21a04e15 100644 --- a/src/NadekoBot/Db/Models/FeedSub.cs +++ b/src/NadekoBot/Db/Models/FeedSub.cs @@ -1,25 +1,22 @@ -using System; +namespace NadekoBot.Services.Database.Models; -namespace NadekoBot.Services.Database.Models +public class FeedSub : DbEntity { - public class FeedSub : DbEntity + public int GuildConfigId { get; set; } + public GuildConfig GuildConfig { get; set; } + + public ulong ChannelId { get; set; } + public string Url { get; set; } + + public override int GetHashCode() { - public int GuildConfigId { get; set; } - public GuildConfig GuildConfig { get; set; } - - public ulong ChannelId { get; set; } - public string Url { get; set; } - - public override int GetHashCode() - { - return Url.GetHashCode(StringComparison.InvariantCulture) ^ GuildConfigId.GetHashCode(); - } - - public override bool Equals(object obj) - { - return obj is FeedSub s - && s.Url.ToLower() == Url.ToLower() - && s.GuildConfigId == GuildConfigId; - } + return Url.GetHashCode(StringComparison.InvariantCulture) ^ GuildConfigId.GetHashCode(); } -} + + public override bool Equals(object obj) + { + return obj is FeedSub s + && s.Url.ToLower() == Url.ToLower() + && s.GuildConfigId == GuildConfigId; + } +} \ No newline at end of file diff --git a/src/NadekoBot/Db/Models/FilterChannelId.cs b/src/NadekoBot/Db/Models/FilterChannelId.cs index a1e2da4c9..7a9468f91 100644 --- a/src/NadekoBot/Db/Models/FilterChannelId.cs +++ b/src/NadekoBot/Db/Models/FilterChannelId.cs @@ -1,19 +1,18 @@ -namespace NadekoBot.Services.Database.Models +namespace NadekoBot.Services.Database.Models; + +public class FilterChannelId : DbEntity { - public class FilterChannelId : DbEntity + public ulong ChannelId { get; set; } + + public override bool Equals(object obj) { - public ulong ChannelId { get; set; } - - public override bool Equals(object obj) - { - return obj is FilterChannelId f - ? f.ChannelId == ChannelId - : false; - } - - public override int GetHashCode() - { - return ChannelId.GetHashCode(); - } + return obj is FilterChannelId f + ? f.ChannelId == ChannelId + : false; } -} + + public override int GetHashCode() + { + return ChannelId.GetHashCode(); + } +} \ No newline at end of file diff --git a/src/NadekoBot/Db/Models/FilterLinksChannelId.cs b/src/NadekoBot/Db/Models/FilterLinksChannelId.cs index 0bbeacf13..afbcf7bec 100644 --- a/src/NadekoBot/Db/Models/FilterLinksChannelId.cs +++ b/src/NadekoBot/Db/Models/FilterLinksChannelId.cs @@ -1,19 +1,18 @@ -namespace NadekoBot.Services.Database.Models +namespace NadekoBot.Services.Database.Models; + +public class FilterLinksChannelId : DbEntity { - public class FilterLinksChannelId : DbEntity + public ulong ChannelId { get; set; } + + public override bool Equals(object obj) { - public ulong ChannelId { get; set; } - - public override bool Equals(object obj) - { - return obj is FilterLinksChannelId f - ? f.ChannelId == ChannelId - : false; - } - - public override int GetHashCode() - { - return ChannelId.GetHashCode(); - } + return obj is FilterLinksChannelId f + ? f.ChannelId == ChannelId + : false; } -} + + public override int GetHashCode() + { + return ChannelId.GetHashCode(); + } +} \ No newline at end of file diff --git a/src/NadekoBot/Db/Models/FilteredWord.cs b/src/NadekoBot/Db/Models/FilteredWord.cs index ec46f1d90..5e2bbd6bc 100644 --- a/src/NadekoBot/Db/Models/FilteredWord.cs +++ b/src/NadekoBot/Db/Models/FilteredWord.cs @@ -1,7 +1,6 @@ -namespace NadekoBot.Services.Database.Models +namespace NadekoBot.Services.Database.Models; + +public class FilteredWord : DbEntity { - public class FilteredWord : DbEntity - { - public string Word { get; set; } - } -} + public string Word { get; set; } +} \ No newline at end of file diff --git a/src/NadekoBot/Db/Models/FollowedStream.cs b/src/NadekoBot/Db/Models/FollowedStream.cs index dae613429..960c67100 100644 --- a/src/NadekoBot/Db/Models/FollowedStream.cs +++ b/src/NadekoBot/Db/Models/FollowedStream.cs @@ -1,50 +1,48 @@ -using System; -using NadekoBot.Services.Database.Models; +using NadekoBot.Services.Database.Models; using NadekoBot.Modules.Searches.Common; -namespace NadekoBot.Db.Models +namespace NadekoBot.Db.Models; + +public class FollowedStream : DbEntity { - public class FollowedStream : DbEntity + public ulong GuildId { get; set; } + public ulong ChannelId { get; set; } + public string Username { get; set; } + public FType Type { get; set; } + public string Message { get; set; } + + public enum FType { - public ulong GuildId { get; set; } - public ulong ChannelId { get; set; } - public string Username { get; set; } - public FType Type { get; set; } - public string Message { get; set; } - - public enum FType - { - Twitch = 0, - [Obsolete("No longer supported.")] - Smashcast = 1, - [Obsolete("No longer supported.")] - Mixer = 2, - Picarto = 3, - Youtube = 4, - Facebook = 5, - } - - protected bool Equals(FollowedStream other) - { - return ChannelId == other.ChannelId - && Username.Trim().ToUpperInvariant() == other.Username.Trim().ToUpperInvariant() - && Type == other.Type; - } - - public override int GetHashCode() - { - return HashCode.Combine(ChannelId, Username, (int) Type); - } - - public override bool Equals(object obj) - { - if (ReferenceEquals(null, obj)) return false; - if (ReferenceEquals(this, obj)) return true; - if (obj.GetType() != this.GetType()) return false; - return Equals((FollowedStream) obj); - } - - public StreamDataKey CreateKey() => - new StreamDataKey(Type, Username.ToLower()); + Twitch = 0, + [Obsolete("No longer supported.")] + Smashcast = 1, + [Obsolete("No longer supported.")] + Mixer = 2, + Picarto = 3, + Youtube = 4, + Facebook = 5, } + + protected bool Equals(FollowedStream other) + { + return ChannelId == other.ChannelId + && Username.Trim().ToUpperInvariant() == other.Username.Trim().ToUpperInvariant() + && Type == other.Type; + } + + public override int GetHashCode() + { + return HashCode.Combine(ChannelId, Username, (int) Type); + } + + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) return false; + if (ReferenceEquals(this, obj)) return true; + if (obj.GetType() != this.GetType()) return false; + return Equals((FollowedStream) obj); + } + + public StreamDataKey CreateKey() => + new StreamDataKey(Type, Username.ToLower()); } \ No newline at end of file diff --git a/src/NadekoBot/Db/Models/GCChannelId.cs b/src/NadekoBot/Db/Models/GCChannelId.cs index 8ce1e6780..34edd8a6d 100644 --- a/src/NadekoBot/Db/Models/GCChannelId.cs +++ b/src/NadekoBot/Db/Models/GCChannelId.cs @@ -1,18 +1,17 @@ -namespace NadekoBot.Services.Database.Models +namespace NadekoBot.Services.Database.Models; + +public class GCChannelId : DbEntity { - public class GCChannelId : DbEntity + public GuildConfig GuildConfig { get; set; } + public ulong ChannelId { get; set; } + + public override bool Equals(object obj) { - public GuildConfig GuildConfig { get; set; } - public ulong ChannelId { get; set; } - - public override bool Equals(object obj) - { - return obj is GCChannelId gc - ? gc.ChannelId == ChannelId - : false; - } - - public override int GetHashCode() => - this.ChannelId.GetHashCode(); + return obj is GCChannelId gc + ? gc.ChannelId == ChannelId + : false; } -} + + public override int GetHashCode() => + this.ChannelId.GetHashCode(); +} \ No newline at end of file diff --git a/src/NadekoBot/Db/Models/GroupName.cs b/src/NadekoBot/Db/Models/GroupName.cs index 88fe68b85..a04ce6b9a 100644 --- a/src/NadekoBot/Db/Models/GroupName.cs +++ b/src/NadekoBot/Db/Models/GroupName.cs @@ -1,11 +1,10 @@ -namespace NadekoBot.Services.Database.Models -{ - public class GroupName : DbEntity - { - public int GuildConfigId { get; set; } - public GuildConfig GuildConfig { get; set; } +namespace NadekoBot.Services.Database.Models; - public int Number { get; set; } - public string Name { get; set; } - } -} +public class GroupName : DbEntity +{ + public int GuildConfigId { get; set; } + public GuildConfig GuildConfig { get; set; } + + public int Number { get; set; } + public string Name { get; set; } +} \ No newline at end of file diff --git a/src/NadekoBot/Db/Models/GuildConfig.cs b/src/NadekoBot/Db/Models/GuildConfig.cs index 5b0c38cba..8cebcbfee 100644 --- a/src/NadekoBot/Db/Models/GuildConfig.cs +++ b/src/NadekoBot/Db/Models/GuildConfig.cs @@ -1,109 +1,106 @@ -using System; -using NadekoBot.Common.Collections; -using System.Collections.Generic; +using NadekoBot.Common.Collections; using NadekoBot.Db.Models; -namespace NadekoBot.Services.Database.Models +namespace NadekoBot.Services.Database.Models; + +public class GuildConfig : DbEntity { - public class GuildConfig : DbEntity - { - public ulong GuildId { get; set; } + public ulong GuildId { get; set; } - public string Prefix { get; set; } = null; + public string Prefix { get; set; } = null; - public bool DeleteMessageOnCommand { get; set; } - public HashSet DelMsgOnCmdChannels { get; set; } = new HashSet(); - public string AutoAssignRoleIds { get; set; } - //greet stuff - public bool AutoDeleteGreetMessages { get; set; } //unused - public bool AutoDeleteByeMessages { get; set; } // unused - public int AutoDeleteGreetMessagesTimer { get; set; } = 30; - public int AutoDeleteByeMessagesTimer { get; set; } = 30; + public bool DeleteMessageOnCommand { get; set; } + public HashSet DelMsgOnCmdChannels { get; set; } = new HashSet(); + public string AutoAssignRoleIds { get; set; } + //greet stuff + public bool AutoDeleteGreetMessages { get; set; } //unused + public bool AutoDeleteByeMessages { get; set; } // unused + public int AutoDeleteGreetMessagesTimer { get; set; } = 30; + public int AutoDeleteByeMessagesTimer { get; set; } = 30; - public ulong GreetMessageChannelId { get; set; } - public ulong ByeMessageChannelId { get; set; } + public ulong GreetMessageChannelId { get; set; } + public ulong ByeMessageChannelId { get; set; } - public bool SendDmGreetMessage { get; set; } - public string DmGreetMessageText { get; set; } = "Welcome to the %server% server, %user%!"; + public bool SendDmGreetMessage { get; set; } + public string DmGreetMessageText { get; set; } = "Welcome to the %server% server, %user%!"; - public bool SendChannelGreetMessage { get; set; } - public string ChannelGreetMessageText { get; set; } = "Welcome to the %server% server, %user%!"; + public bool SendChannelGreetMessage { get; set; } + public string ChannelGreetMessageText { get; set; } = "Welcome to the %server% server, %user%!"; - #region Boost Message + #region Boost Message - public bool SendBoostMessage { get; set; } - public string BoostMessage { get; set; } = "%user% just boosted this server!"; - public ulong BoostMessageChannelId { get; set; } - public int BoostMessageDeleteAfter { get; set; } + public bool SendBoostMessage { get; set; } + public string BoostMessage { get; set; } = "%user% just boosted this server!"; + public ulong BoostMessageChannelId { get; set; } + public int BoostMessageDeleteAfter { get; set; } - #endregion + #endregion - public bool SendChannelByeMessage { get; set; } - public string ChannelByeMessageText { get; set; } = "%user% has left!"; + public bool SendChannelByeMessage { get; set; } + public string ChannelByeMessageText { get; set; } = "%user% has left!"; - //self assignable roles - public bool ExclusiveSelfAssignedRoles { get; set; } - public bool AutoDeleteSelfAssignedRoleMessages { get; set; } + //self assignable roles + public bool ExclusiveSelfAssignedRoles { get; set; } + public bool AutoDeleteSelfAssignedRoleMessages { get; set; } - //stream notifications - public HashSet FollowedStreams { get; set; } = new HashSet(); + //stream notifications + public HashSet FollowedStreams { get; set; } = new HashSet(); - //currencyGeneration - public HashSet GenerateCurrencyChannelIds { get; set; } = new HashSet(); + //currencyGeneration + public HashSet GenerateCurrencyChannelIds { get; set; } = new HashSet(); - public List Permissions { get; set; } - public bool VerbosePermissions { get; set; } = true; - public string PermissionRole { get; set; } = null; + public List Permissions { get; set; } + public bool VerbosePermissions { get; set; } = true; + public string PermissionRole { get; set; } = null; - public HashSet CommandCooldowns { get; set; } = new HashSet(); + public HashSet CommandCooldowns { get; set; } = new HashSet(); - //filtering - public bool FilterInvites { get; set; } - public bool FilterLinks { get; set; } - public HashSet FilterInvitesChannelIds { get; set; } = new HashSet(); - public HashSet FilterLinksChannelIds { get; set; } = new HashSet(); + //filtering + public bool FilterInvites { get; set; } + public bool FilterLinks { get; set; } + public HashSet FilterInvitesChannelIds { get; set; } = new HashSet(); + public HashSet FilterLinksChannelIds { get; set; } = new HashSet(); - //public bool FilterLinks { get; set; } - //public HashSet FilterLinksChannels { get; set; } = new HashSet(); + //public bool FilterLinks { get; set; } + //public HashSet FilterLinksChannels { get; set; } = new HashSet(); - public bool FilterWords { get; set; } - public HashSet FilteredWords { get; set; } = new HashSet(); - public HashSet FilterWordsChannelIds { get; set; } = new HashSet(); + public bool FilterWords { get; set; } + public HashSet FilteredWords { get; set; } = new HashSet(); + public HashSet FilterWordsChannelIds { get; set; } = new HashSet(); - public HashSet MutedUsers { get; set; } = new HashSet(); + public HashSet MutedUsers { get; set; } = new HashSet(); - public string MuteRoleName { get; set; } - public bool CleverbotEnabled { get; set; } + public string MuteRoleName { get; set; } + public bool CleverbotEnabled { get; set; } - public AntiRaidSetting AntiRaidSetting { get; set; } - public AntiSpamSetting AntiSpamSetting { get; set; } - public AntiAltSetting AntiAltSetting { get; set; } + public AntiRaidSetting AntiRaidSetting { get; set; } + public AntiSpamSetting AntiSpamSetting { get; set; } + public AntiAltSetting AntiAltSetting { get; set; } - public string Locale { get; set; } = null; - public string TimeZoneId { get; set; } = null; + public string Locale { get; set; } = null; + public string TimeZoneId { get; set; } = null; - public HashSet UnmuteTimers { get; set; } = new HashSet(); - public HashSet UnbanTimer { get; set; } = new HashSet(); - public HashSet UnroleTimer { get; set; } = new HashSet(); - public HashSet VcRoleInfos { get; set; } - public HashSet CommandAliases { get; set; } = new HashSet(); - public List WarnPunishments { get; set; } = new List(); - public bool WarningsInitialized { get; set; } - public HashSet SlowmodeIgnoredUsers { get; set; } - public HashSet SlowmodeIgnoredRoles { get; set; } + public HashSet UnmuteTimers { get; set; } = new HashSet(); + public HashSet UnbanTimer { get; set; } = new HashSet(); + public HashSet UnroleTimer { get; set; } = new HashSet(); + public HashSet VcRoleInfos { get; set; } + public HashSet CommandAliases { get; set; } = new HashSet(); + public List WarnPunishments { get; set; } = new List(); + public bool WarningsInitialized { get; set; } + public HashSet SlowmodeIgnoredUsers { get; set; } + public HashSet SlowmodeIgnoredRoles { get; set; } - public List ShopEntries { get; set; } - public ulong? GameVoiceChannel { get; set; } = null; - public bool VerboseErrors { get; set; } = false; + public List ShopEntries { get; set; } + public ulong? GameVoiceChannel { get; set; } = null; + public bool VerboseErrors { get; set; } = false; - public StreamRoleSettings StreamRole { get; set; } + public StreamRoleSettings StreamRole { get; set; } - public XpSettings XpSettings { get; set; } - public List FeedSubs { get; set; } = new List(); - public IndexedCollection ReactionRoleMessages { get; set; } = new IndexedCollection(); - public bool NotifyStreamOffline { get; set; } - public List SelfAssignableRoleGroupNames { get; set; } - public int WarnExpireHours { get; set; } = 0; - public WarnExpireAction WarnExpireAction { get; set; } = WarnExpireAction.Clear; - } -} + public XpSettings XpSettings { get; set; } + public List FeedSubs { get; set; } = new List(); + public IndexedCollection ReactionRoleMessages { get; set; } = new IndexedCollection(); + public bool NotifyStreamOffline { get; set; } + public List SelfAssignableRoleGroupNames { get; set; } + public int WarnExpireHours { get; set; } = 0; + public WarnExpireAction WarnExpireAction { get; set; } = WarnExpireAction.Clear; +} \ No newline at end of file diff --git a/src/NadekoBot/Db/Models/IgnoredLogItem.cs b/src/NadekoBot/Db/Models/IgnoredLogItem.cs index 598e7c316..b9b721bb8 100644 --- a/src/NadekoBot/Db/Models/IgnoredLogItem.cs +++ b/src/NadekoBot/Db/Models/IgnoredLogItem.cs @@ -1,16 +1,15 @@ -namespace NadekoBot.Services.Database.Models -{ - public class IgnoredLogItem : DbEntity - { - public int LogSettingId { get; set; } - public LogSetting LogSetting { get; set; } - public ulong LogItemId { get; set; } - public IgnoredItemType ItemType { get; set; } - } +namespace NadekoBot.Services.Database.Models; - public enum IgnoredItemType - { - Channel, - User, - } +public class IgnoredLogItem : DbEntity +{ + public int LogSettingId { get; set; } + public LogSetting LogSetting { get; set; } + public ulong LogItemId { get; set; } + public IgnoredItemType ItemType { get; set; } } + +public enum IgnoredItemType +{ + Channel, + User, +} \ No newline at end of file diff --git a/src/NadekoBot/Db/Models/IgnoredVoicePresenceChannel.cs b/src/NadekoBot/Db/Models/IgnoredVoicePresenceChannel.cs index 75bced37a..c733e19a6 100644 --- a/src/NadekoBot/Db/Models/IgnoredVoicePresenceChannel.cs +++ b/src/NadekoBot/Db/Models/IgnoredVoicePresenceChannel.cs @@ -1,8 +1,7 @@ -namespace NadekoBot.Services.Database.Models +namespace NadekoBot.Services.Database.Models; + +public class IgnoredVoicePresenceChannel : DbEntity { - public class IgnoredVoicePresenceChannel : DbEntity - { - public LogSetting LogSetting { get; set; } - public ulong ChannelId { get; set; } - } -} + public LogSetting LogSetting { get; set; } + public ulong ChannelId { get; set; } +} \ No newline at end of file diff --git a/src/NadekoBot/Db/Models/ImageOnlyChannel.cs b/src/NadekoBot/Db/Models/ImageOnlyChannel.cs index 8f0cda0a3..6155685af 100644 --- a/src/NadekoBot/Db/Models/ImageOnlyChannel.cs +++ b/src/NadekoBot/Db/Models/ImageOnlyChannel.cs @@ -1,8 +1,7 @@ -namespace NadekoBot.Services.Database.Models +namespace NadekoBot.Services.Database.Models; + +public class ImageOnlyChannel : DbEntity { - public class ImageOnlyChannel : DbEntity - { - public ulong GuildId { get; set; } - public ulong ChannelId { get; set; } - } + public ulong GuildId { get; set; } + public ulong ChannelId { get; set; } } \ No newline at end of file diff --git a/src/NadekoBot/Db/Models/LogSetting.cs b/src/NadekoBot/Db/Models/LogSetting.cs index 1b0c78bcf..b7a7dd8d2 100644 --- a/src/NadekoBot/Db/Models/LogSetting.cs +++ b/src/NadekoBot/Db/Models/LogSetting.cs @@ -1,34 +1,31 @@ -using System.Collections.Generic; +namespace NadekoBot.Services.Database.Models; -namespace NadekoBot.Services.Database.Models +public class LogSetting : DbEntity { - public class LogSetting : DbEntity - { - public List LogIgnores { get; set; } = new List(); + public List LogIgnores { get; set; } = new List(); - public ulong GuildId { get; set; } - public ulong? LogOtherId { get; set; } - public ulong? MessageUpdatedId { get; set; } - public ulong? MessageDeletedId { get; set; } + public ulong GuildId { get; set; } + public ulong? LogOtherId { get; set; } + public ulong? MessageUpdatedId { get; set; } + public ulong? MessageDeletedId { get; set; } - public ulong? UserJoinedId { get; set; } - public ulong? UserLeftId { get; set; } - public ulong? UserBannedId { get; set; } - public ulong? UserUnbannedId { get; set; } - public ulong? UserUpdatedId { get; set; } + public ulong? UserJoinedId { get; set; } + public ulong? UserLeftId { get; set; } + public ulong? UserBannedId { get; set; } + public ulong? UserUnbannedId { get; set; } + public ulong? UserUpdatedId { get; set; } - public ulong? ChannelCreatedId { get; set; } - public ulong? ChannelDestroyedId { get; set; } - public ulong? ChannelUpdatedId { get; set; } + public ulong? ChannelCreatedId { get; set; } + public ulong? ChannelDestroyedId { get; set; } + public ulong? ChannelUpdatedId { get; set; } - public ulong? UserMutedId { get; set; } + public ulong? UserMutedId { get; set; } - //userpresence - public ulong? LogUserPresenceId { get; set; } + //userpresence + public ulong? LogUserPresenceId { get; set; } - //voicepresence + //voicepresence - public ulong? LogVoicePresenceId { get; set; } - public ulong? LogVoicePresenceTTSId { get; set; } - } -} + public ulong? LogVoicePresenceId { get; set; } + public ulong? LogVoicePresenceTTSId { get; set; } +} \ No newline at end of file diff --git a/src/NadekoBot/Db/Models/MusicPlaylist.cs b/src/NadekoBot/Db/Models/MusicPlaylist.cs index 9788f033c..21c8f8330 100644 --- a/src/NadekoBot/Db/Models/MusicPlaylist.cs +++ b/src/NadekoBot/Db/Models/MusicPlaylist.cs @@ -1,12 +1,9 @@ -using System.Collections.Generic; +namespace NadekoBot.Services.Database.Models; -namespace NadekoBot.Services.Database.Models +public class MusicPlaylist : DbEntity { - public class MusicPlaylist : DbEntity - { - public string Name { get; set; } - public string Author { get; set; } - public ulong AuthorId { get; set; } - public List Songs { get; set; } = new List(); - } -} + public string Name { get; set; } + public string Author { get; set; } + public ulong AuthorId { get; set; } + public List Songs { get; set; } = new List(); +} \ No newline at end of file diff --git a/src/NadekoBot/Db/Models/MusicSettings.cs b/src/NadekoBot/Db/Models/MusicSettings.cs index f2eb302ba..6a22899cb 100644 --- a/src/NadekoBot/Db/Models/MusicSettings.cs +++ b/src/NadekoBot/Db/Models/MusicSettings.cs @@ -1,56 +1,55 @@ -namespace NadekoBot.Services.Database.Models +namespace NadekoBot.Services.Database.Models; + +public class MusicPlayerSettings { - public class MusicPlayerSettings - { - /// - /// Auto generated Id - /// - public int Id { get; set; } + /// + /// Auto generated Id + /// + public int Id { get; set; } - /// - /// Id of the guild - /// - public ulong GuildId { get; set; } + /// + /// Id of the guild + /// + public ulong GuildId { get; set; } - /// - /// Queue repeat type - /// - public PlayerRepeatType PlayerRepeat { get; set; } = PlayerRepeatType.Queue; + /// + /// Queue repeat type + /// + public PlayerRepeatType PlayerRepeat { get; set; } = PlayerRepeatType.Queue; - /// - /// Channel id the bot will always try to send track related messages to - /// - public ulong? MusicChannelId { get; set; } = null; + /// + /// Channel id the bot will always try to send track related messages to + /// + public ulong? MusicChannelId { get; set; } = null; - /// - /// Default volume player will be created with - /// - public int Volume { get; set; } = 100; + /// + /// Default volume player will be created with + /// + public int Volume { get; set; } = 100; - /// - /// Whether the bot should auto disconnect from the voice channel once the queue is done - /// This only has effect if - /// - public bool AutoDisconnect { get; set; } = false; + /// + /// Whether the bot should auto disconnect from the voice channel once the queue is done + /// This only has effect if + /// + public bool AutoDisconnect { get; set; } = false; - /// - /// Selected quality preset for the music player - /// - public QualityPreset QualityPreset { get; set; } - } - - public enum QualityPreset - { - Highest, - High, - Medium, - Low - } - - public enum PlayerRepeatType - { - None, - Track, - Queue - } + /// + /// Selected quality preset for the music player + /// + public QualityPreset QualityPreset { get; set; } } + +public enum QualityPreset +{ + Highest, + High, + Medium, + Low +} + +public enum PlayerRepeatType +{ + None, + Track, + Queue +} \ No newline at end of file diff --git a/src/NadekoBot/Db/Models/MutedUserId.cs b/src/NadekoBot/Db/Models/MutedUserId.cs index 9d6e1fb1b..812efd25a 100644 --- a/src/NadekoBot/Db/Models/MutedUserId.cs +++ b/src/NadekoBot/Db/Models/MutedUserId.cs @@ -1,19 +1,18 @@ -namespace NadekoBot.Services.Database.Models +namespace NadekoBot.Services.Database.Models; + +public class MutedUserId : DbEntity { - public class MutedUserId : DbEntity + public ulong UserId { get; set; } + + public override int GetHashCode() { - public ulong UserId { get; set; } - - public override int GetHashCode() - { - return UserId.GetHashCode(); - } - - public override bool Equals(object obj) - { - return obj is MutedUserId mui - ? mui.UserId == UserId - : false; - } + return UserId.GetHashCode(); } -} + + public override bool Equals(object obj) + { + return obj is MutedUserId mui + ? mui.UserId == UserId + : false; + } +} \ No newline at end of file diff --git a/src/NadekoBot/Db/Models/NsfwBlacklistedTag.cs b/src/NadekoBot/Db/Models/NsfwBlacklistedTag.cs index 04582cb48..29a4f83ec 100644 --- a/src/NadekoBot/Db/Models/NsfwBlacklistedTag.cs +++ b/src/NadekoBot/Db/Models/NsfwBlacklistedTag.cs @@ -1,16 +1,13 @@ -using System; +namespace NadekoBot.Services.Database.Models; -namespace NadekoBot.Services.Database.Models +public class NsfwBlacklistedTag : DbEntity { - public class NsfwBlacklistedTag : DbEntity - { - public ulong GuildId { get; set; } - public string Tag { get; set; } + public ulong GuildId { get; set; } + public string Tag { get; set; } - public override int GetHashCode() - => Tag.GetHashCode(StringComparison.InvariantCulture); + public override int GetHashCode() + => Tag.GetHashCode(StringComparison.InvariantCulture); - public override bool Equals(object obj) - => obj is NsfwBlacklistedTag x && x.Tag == Tag; - } -} + public override bool Equals(object obj) + => obj is NsfwBlacklistedTag x && x.Tag == Tag; +} \ No newline at end of file diff --git a/src/NadekoBot/Db/Models/Permission.cs b/src/NadekoBot/Db/Models/Permission.cs index 672fc692b..5fb40c8bf 100644 --- a/src/NadekoBot/Db/Models/Permission.cs +++ b/src/NadekoBot/Db/Models/Permission.cs @@ -1,60 +1,58 @@ -using System.Collections.Generic; -using System.ComponentModel.DataAnnotations.Schema; +using System.ComponentModel.DataAnnotations.Schema; using System.Diagnostics; -namespace NadekoBot.Services.Database.Models +namespace NadekoBot.Services.Database.Models; + +public interface IIndexed { - public interface IIndexed - { - int Index { get; set; } - } - - [DebuggerDisplay("{PrimaryTarget}{SecondaryTarget} {SecondaryTargetName} {State} {PrimaryTargetId}")] - public class Permissionv2 : DbEntity, IIndexed - { - public int? GuildConfigId { get; set; } - public int Index { get; set; } - - public PrimaryPermissionType PrimaryTarget { get; set; } - public ulong PrimaryTargetId { get; set; } - - public SecondaryPermissionType SecondaryTarget { get; set; } - public string SecondaryTargetName { get; set; } - - public bool IsCustomCommand { get; set; } - - public bool State { get; set; } - - [NotMapped] - public static Permissionv2 AllowAllPerm => new Permissionv2() - { - PrimaryTarget = PrimaryPermissionType.Server, - PrimaryTargetId = 0, - SecondaryTarget = SecondaryPermissionType.AllModules, - SecondaryTargetName = "*", - State = true, - Index = 0, - }; - - public static List GetDefaultPermlist => - new List - { - AllowAllPerm - }; - } - - public enum PrimaryPermissionType - { - User, - Channel, - Role, - Server - } - - public enum SecondaryPermissionType - { - Module, - Command, - AllModules - } + int Index { get; set; } } + +[DebuggerDisplay("{PrimaryTarget}{SecondaryTarget} {SecondaryTargetName} {State} {PrimaryTargetId}")] +public class Permissionv2 : DbEntity, IIndexed +{ + public int? GuildConfigId { get; set; } + public int Index { get; set; } + + public PrimaryPermissionType PrimaryTarget { get; set; } + public ulong PrimaryTargetId { get; set; } + + public SecondaryPermissionType SecondaryTarget { get; set; } + public string SecondaryTargetName { get; set; } + + public bool IsCustomCommand { get; set; } + + public bool State { get; set; } + + [NotMapped] + public static Permissionv2 AllowAllPerm => new Permissionv2() + { + PrimaryTarget = PrimaryPermissionType.Server, + PrimaryTargetId = 0, + SecondaryTarget = SecondaryPermissionType.AllModules, + SecondaryTargetName = "*", + State = true, + Index = 0, + }; + + public static List GetDefaultPermlist => + new List + { + AllowAllPerm + }; +} + +public enum PrimaryPermissionType +{ + User, + Channel, + Role, + Server +} + +public enum SecondaryPermissionType +{ + Module, + Command, + AllModules +} \ No newline at end of file diff --git a/src/NadekoBot/Db/Models/PlantedCurrency.cs b/src/NadekoBot/Db/Models/PlantedCurrency.cs index ab4447e2b..d7f07ba4e 100644 --- a/src/NadekoBot/Db/Models/PlantedCurrency.cs +++ b/src/NadekoBot/Db/Models/PlantedCurrency.cs @@ -1,12 +1,11 @@ -namespace NadekoBot.Services.Database.Models +namespace NadekoBot.Services.Database.Models; + +public class PlantedCurrency : DbEntity { - public class PlantedCurrency : DbEntity - { - public long Amount { get; set; } - public string Password { get; set; } - public ulong GuildId { get; set; } - public ulong ChannelId { get; set; } - public ulong UserId { get; set; } - public ulong MessageId { get; set; } - } -} + public long Amount { get; set; } + public string Password { get; set; } + public ulong GuildId { get; set; } + public ulong ChannelId { get; set; } + public ulong UserId { get; set; } + public ulong MessageId { get; set; } +} \ No newline at end of file diff --git a/src/NadekoBot/Db/Models/PlaylistSong.cs b/src/NadekoBot/Db/Models/PlaylistSong.cs index f938d2427..5495b402c 100644 --- a/src/NadekoBot/Db/Models/PlaylistSong.cs +++ b/src/NadekoBot/Db/Models/PlaylistSong.cs @@ -1,19 +1,18 @@ -namespace NadekoBot.Services.Database.Models -{ - public class PlaylistSong : DbEntity - { - public string Provider { get; set; } - public MusicType ProviderType { get; set; } - public string Title { get; set; } - public string Uri { get; set; } - public string Query { get; set; } - } +namespace NadekoBot.Services.Database.Models; - public enum MusicType - { - Radio, - YouTube, - Local, - Soundcloud - } +public class PlaylistSong : DbEntity +{ + public string Provider { get; set; } + public MusicType ProviderType { get; set; } + public string Title { get; set; } + public string Uri { get; set; } + public string Query { get; set; } } + +public enum MusicType +{ + Radio, + YouTube, + Local, + Soundcloud +} \ No newline at end of file diff --git a/src/NadekoBot/Db/Models/Poll.cs b/src/NadekoBot/Db/Models/Poll.cs index 8dc368df2..8981e5866 100644 --- a/src/NadekoBot/Db/Models/Poll.cs +++ b/src/NadekoBot/Db/Models/Poll.cs @@ -1,20 +1,18 @@ using NadekoBot.Common.Collections; -using System.Collections.Generic; -namespace NadekoBot.Services.Database.Models +namespace NadekoBot.Services.Database.Models; + +public class Poll : DbEntity { - public class Poll : DbEntity - { - public ulong GuildId { get; set; } - public ulong ChannelId { get; set; } - public string Question { get; set; } - public IndexedCollection Answers { get; set; } - public HashSet Votes { get; set; } = new HashSet(); - } - - public class PollAnswer : DbEntity, IIndexed - { - public int Index { get; set; } - public string Text { get; set; } - } + public ulong GuildId { get; set; } + public ulong ChannelId { get; set; } + public string Question { get; set; } + public IndexedCollection Answers { get; set; } + public HashSet Votes { get; set; } = new HashSet(); } + +public class PollAnswer : DbEntity, IIndexed +{ + public int Index { get; set; } + public string Text { get; set; } +} \ No newline at end of file diff --git a/src/NadekoBot/Db/Models/PollVote.cs b/src/NadekoBot/Db/Models/PollVote.cs index 38de53d5f..3d5c12ed6 100644 --- a/src/NadekoBot/Db/Models/PollVote.cs +++ b/src/NadekoBot/Db/Models/PollVote.cs @@ -1,20 +1,19 @@ -namespace NadekoBot.Services.Database.Models +namespace NadekoBot.Services.Database.Models; + +public class PollVote : DbEntity { - public class PollVote : DbEntity + public ulong UserId { get; set; } + public int VoteIndex { get; set; } + + public override int GetHashCode() { - public ulong UserId { get; set; } - public int VoteIndex { get; set; } - - public override int GetHashCode() - { - return UserId.GetHashCode(); - } - - public override bool Equals(object obj) - { - return obj is PollVote p - ? p.UserId == UserId - : false; - } + return UserId.GetHashCode(); } -} + + public override bool Equals(object obj) + { + return obj is PollVote p + ? p.UserId == UserId + : false; + } +} \ No newline at end of file diff --git a/src/NadekoBot/Db/Models/Quote.cs b/src/NadekoBot/Db/Models/Quote.cs index 211b6029a..47396b5ce 100644 --- a/src/NadekoBot/Db/Models/Quote.cs +++ b/src/NadekoBot/Db/Models/Quote.cs @@ -1,23 +1,22 @@ using System.ComponentModel.DataAnnotations; -namespace NadekoBot.Services.Database.Models +namespace NadekoBot.Services.Database.Models; + +public class Quote : DbEntity { - public class Quote : DbEntity - { - public ulong GuildId { get; set; } - [Required] - public string Keyword { get; set; } - [Required] - public string AuthorName { get; set; } - public ulong AuthorId { get; set; } - [Required] - public string Text { get; set; } - } - - - public enum OrderType - { - Id = -1, - Keyword = -2 - } + public ulong GuildId { get; set; } + [Required] + public string Keyword { get; set; } + [Required] + public string AuthorName { get; set; } + public ulong AuthorId { get; set; } + [Required] + public string Text { get; set; } } + + +public enum OrderType +{ + Id = -1, + Keyword = -2 +} \ No newline at end of file diff --git a/src/NadekoBot/Db/Models/ReactionRole.cs b/src/NadekoBot/Db/Models/ReactionRole.cs index 18dcf58d9..0406cf98b 100644 --- a/src/NadekoBot/Db/Models/ReactionRole.cs +++ b/src/NadekoBot/Db/Models/ReactionRole.cs @@ -1,24 +1,21 @@ -using System.Collections.Generic; +namespace NadekoBot.Services.Database.Models; -namespace NadekoBot.Services.Database.Models +public class ReactionRoleMessage : DbEntity, IIndexed { - public class ReactionRoleMessage : DbEntity, IIndexed - { - public int Index { get; set; } + public int Index { get; set; } - public int GuildConfigId { get; set; } - public GuildConfig GuildConfig { get; set; } + public int GuildConfigId { get; set; } + public GuildConfig GuildConfig { get; set; } - public ulong ChannelId { get; set; } - public ulong MessageId { get; set; } + public ulong ChannelId { get; set; } + public ulong MessageId { get; set; } - public List ReactionRoles { get; set; } - public bool Exclusive { get; set; } - } - - public class ReactionRole : DbEntity - { - public string EmoteName { get; set; } - public ulong RoleId { get; set; } - } + public List ReactionRoles { get; set; } + public bool Exclusive { get; set; } } + +public class ReactionRole : DbEntity +{ + public string EmoteName { get; set; } + public ulong RoleId { get; set; } +} \ No newline at end of file diff --git a/src/NadekoBot/Db/Models/Reminder.cs b/src/NadekoBot/Db/Models/Reminder.cs index 63600de92..32bd90e6c 100644 --- a/src/NadekoBot/Db/Models/Reminder.cs +++ b/src/NadekoBot/Db/Models/Reminder.cs @@ -1,14 +1,11 @@ -using System; +namespace NadekoBot.Services.Database.Models; -namespace NadekoBot.Services.Database.Models +public class Reminder : DbEntity { - public class Reminder : DbEntity - { - public DateTime When { get; set; } - public ulong ChannelId { get; set; } - public ulong ServerId { get; set; } - public ulong UserId { get; set; } - public string Message { get; set; } - public bool IsPrivate { get; set; } - } -} + public DateTime When { get; set; } + public ulong ChannelId { get; set; } + public ulong ServerId { get; set; } + public ulong UserId { get; set; } + public string Message { get; set; } + public bool IsPrivate { get; set; } +} \ No newline at end of file diff --git a/src/NadekoBot/Db/Models/Repeater.cs b/src/NadekoBot/Db/Models/Repeater.cs index ddda51974..8f7fb9daf 100644 --- a/src/NadekoBot/Db/Models/Repeater.cs +++ b/src/NadekoBot/Db/Models/Repeater.cs @@ -1,17 +1,14 @@ -using System; +namespace NadekoBot.Services.Database.Models; -namespace NadekoBot.Services.Database.Models +public class Repeater { - public class Repeater - { - public int Id { get; set; } - public ulong GuildId { get; set; } - public ulong ChannelId { get; set; } - public ulong? LastMessageId { get; set; } - public string Message { get; set; } - public TimeSpan Interval { get; set; } - public TimeSpan? StartTimeOfDay { get; set; } - public bool NoRedundant { get; set; } - public DateTime DateAdded { get; set; } - } -} + public int Id { get; set; } + public ulong GuildId { get; set; } + public ulong ChannelId { get; set; } + public ulong? LastMessageId { get; set; } + public string Message { get; set; } + public TimeSpan Interval { get; set; } + public TimeSpan? StartTimeOfDay { get; set; } + public bool NoRedundant { get; set; } + public DateTime DateAdded { get; set; } +} \ No newline at end of file diff --git a/src/NadekoBot/Db/Models/RewardedUser.cs b/src/NadekoBot/Db/Models/RewardedUser.cs index f40e083fd..7241284d6 100644 --- a/src/NadekoBot/Db/Models/RewardedUser.cs +++ b/src/NadekoBot/Db/Models/RewardedUser.cs @@ -1,12 +1,9 @@ -using System; +namespace NadekoBot.Services.Database.Models; -namespace NadekoBot.Services.Database.Models +public class RewardedUser : DbEntity { - public class RewardedUser : DbEntity - { - public ulong UserId { get; set; } - public string PatreonUserId { get; set; } - public int AmountRewardedThisMonth { get; set; } - public DateTime LastReward { get; set; } - } -} + public ulong UserId { get; set; } + public string PatreonUserId { get; set; } + public int AmountRewardedThisMonth { get; set; } + public DateTime LastReward { get; set; } +} \ No newline at end of file diff --git a/src/NadekoBot/Db/Models/RotatingPlayingStatus.cs b/src/NadekoBot/Db/Models/RotatingPlayingStatus.cs index ef80052e5..460e0bc76 100644 --- a/src/NadekoBot/Db/Models/RotatingPlayingStatus.cs +++ b/src/NadekoBot/Db/Models/RotatingPlayingStatus.cs @@ -1,10 +1,9 @@ using Discord; -namespace NadekoBot.Services.Database.Models +namespace NadekoBot.Services.Database.Models; + +public class RotatingPlayingStatus : DbEntity { - public class RotatingPlayingStatus : DbEntity - { - public string Status { get; set; } - public ActivityType Type { get; set; } - } + public string Status { get; set; } + public ActivityType Type { get; set; } } \ No newline at end of file diff --git a/src/NadekoBot/Db/Models/SelfAssignableRole.cs b/src/NadekoBot/Db/Models/SelfAssignableRole.cs index 974274d83..99b9ca279 100644 --- a/src/NadekoBot/Db/Models/SelfAssignableRole.cs +++ b/src/NadekoBot/Db/Models/SelfAssignableRole.cs @@ -1,11 +1,10 @@ -namespace NadekoBot.Services.Database.Models +namespace NadekoBot.Services.Database.Models; + +public class SelfAssignedRole : DbEntity { - public class SelfAssignedRole : DbEntity - { - public ulong GuildId { get; set; } - public ulong RoleId { get; set; } + public ulong GuildId { get; set; } + public ulong RoleId { get; set; } - public int Group { get; set; } - public int LevelRequirement { get; set; } - } -} + public int Group { get; set; } + public int LevelRequirement { get; set; } +} \ No newline at end of file diff --git a/src/NadekoBot/Db/Models/ShopEntry.cs b/src/NadekoBot/Db/Models/ShopEntry.cs index 042c60b07..ef5bc2048 100644 --- a/src/NadekoBot/Db/Models/ShopEntry.cs +++ b/src/NadekoBot/Db/Models/ShopEntry.cs @@ -1,46 +1,42 @@ -using System; -using System.Collections.Generic; +namespace NadekoBot.Services.Database.Models; -namespace NadekoBot.Services.Database.Models +public enum ShopEntryType { - public enum ShopEntryType - { - Role, - List, - //Infinite_List, - } - - public class ShopEntry : DbEntity, IIndexed - { - public int Index { get; set; } - public int Price { get; set; } - public string Name { get; set; } - public ulong AuthorId { get; set; } - - public ShopEntryType Type { get; set; } - - //role - public string RoleName { get; set; } - public ulong RoleId { get; set; } - - //list - public HashSet Items { get; set; } = new HashSet(); - } - - public class ShopEntryItem : DbEntity - { - public string Text { get; set; } - - public override bool Equals(object obj) - { - if (obj is null || GetType() != obj.GetType()) - { - return false; - } - return ((ShopEntryItem)obj).Text == Text; - } - - public override int GetHashCode() => - Text.GetHashCode(StringComparison.InvariantCulture); - } + Role, + List, + //Infinite_List, } + +public class ShopEntry : DbEntity, IIndexed +{ + public int Index { get; set; } + public int Price { get; set; } + public string Name { get; set; } + public ulong AuthorId { get; set; } + + public ShopEntryType Type { get; set; } + + //role + public string RoleName { get; set; } + public ulong RoleId { get; set; } + + //list + public HashSet Items { get; set; } = new HashSet(); +} + +public class ShopEntryItem : DbEntity +{ + public string Text { get; set; } + + public override bool Equals(object obj) + { + if (obj is null || GetType() != obj.GetType()) + { + return false; + } + return ((ShopEntryItem)obj).Text == Text; + } + + public override int GetHashCode() => + Text.GetHashCode(StringComparison.InvariantCulture); +} \ No newline at end of file diff --git a/src/NadekoBot/Db/Models/SlowmodeIgnoredRole.cs b/src/NadekoBot/Db/Models/SlowmodeIgnoredRole.cs index eef11c4a6..8e2ccc902 100644 --- a/src/NadekoBot/Db/Models/SlowmodeIgnoredRole.cs +++ b/src/NadekoBot/Db/Models/SlowmodeIgnoredRole.cs @@ -1,24 +1,23 @@ -namespace NadekoBot.Services.Database.Models +namespace NadekoBot.Services.Database.Models; + +public class SlowmodeIgnoredRole : DbEntity { - public class SlowmodeIgnoredRole : DbEntity + public ulong RoleId { get; set; } + + // override object.Equals + public override bool Equals(object obj) { - public ulong RoleId { get; set; } - - // override object.Equals - public override bool Equals(object obj) + if (obj is null || GetType() != obj.GetType()) { - if (obj is null || GetType() != obj.GetType()) - { - return false; - } - - return ((SlowmodeIgnoredRole)obj).RoleId == RoleId; + return false; } - // override object.GetHashCode - public override int GetHashCode() - { - return RoleId.GetHashCode(); - } + return ((SlowmodeIgnoredRole)obj).RoleId == RoleId; } -} + + // override object.GetHashCode + public override int GetHashCode() + { + return RoleId.GetHashCode(); + } +} \ No newline at end of file diff --git a/src/NadekoBot/Db/Models/SlowmodeIgnoredUser.cs b/src/NadekoBot/Db/Models/SlowmodeIgnoredUser.cs index 2096b898e..cca19bd76 100644 --- a/src/NadekoBot/Db/Models/SlowmodeIgnoredUser.cs +++ b/src/NadekoBot/Db/Models/SlowmodeIgnoredUser.cs @@ -1,24 +1,23 @@ -namespace NadekoBot.Services.Database.Models +namespace NadekoBot.Services.Database.Models; + +public class SlowmodeIgnoredUser : DbEntity { - public class SlowmodeIgnoredUser : DbEntity + public ulong UserId { get; set; } + + // override object.Equals + public override bool Equals(object obj) { - public ulong UserId { get; set; } - - // override object.Equals - public override bool Equals(object obj) + if (obj is null || GetType() != obj.GetType()) { - if (obj is null || GetType() != obj.GetType()) - { - return false; - } - - return ((SlowmodeIgnoredUser)obj).UserId == UserId; + return false; } - // override object.GetHashCode - public override int GetHashCode() - { - return UserId.GetHashCode(); - } + return ((SlowmodeIgnoredUser)obj).UserId == UserId; } -} + + // override object.GetHashCode + public override int GetHashCode() + { + return UserId.GetHashCode(); + } +} \ No newline at end of file diff --git a/src/NadekoBot/Db/Models/StreamRoleSettings.cs b/src/NadekoBot/Db/Models/StreamRoleSettings.cs index 5aa408c23..0bd5b564f 100644 --- a/src/NadekoBot/Db/Models/StreamRoleSettings.cs +++ b/src/NadekoBot/Db/Models/StreamRoleSettings.cs @@ -1,78 +1,75 @@ -using System.Collections.Generic; +namespace NadekoBot.Services.Database.Models; -namespace NadekoBot.Services.Database.Models +public class StreamRoleSettings : DbEntity { - public class StreamRoleSettings : DbEntity + public int GuildConfigId { get; set; } + public GuildConfig GuildConfig { get; set; } + + /// + /// Whether the feature is enabled in the guild. + /// + public bool Enabled { get; set; } + + /// + /// Id of the role to give to the users in the role 'FromRole' when they start streaming + /// + public ulong AddRoleId { get; set; } + + /// + /// Id of the role whose users are eligible to get the 'AddRole' + /// + public ulong FromRoleId { get; set; } + + /// + /// If set, feature will only apply to users who have this keyword in their streaming status. + /// + public string Keyword { get; set; } + + /// + /// A collection of whitelisted users' IDs. Whitelisted users don't require 'keyword' in + /// order to get the stream role. + /// + public HashSet Whitelist { get; set; } = new HashSet(); + + /// + /// A collection of blacklisted users' IDs. Blacklisted useres will never get the stream role. + /// + public HashSet Blacklist { get; set; } = new HashSet(); +} + +public class StreamRoleBlacklistedUser : DbEntity +{ + public ulong UserId { get; set; } + public string Username { get; set; } + + public override bool Equals(object obj) { - public int GuildConfigId { get; set; } - public GuildConfig GuildConfig { get; set; } + if (!(obj is StreamRoleBlacklistedUser x)) + return false; - /// - /// Whether the feature is enabled in the guild. - /// - public bool Enabled { get; set; } - - /// - /// Id of the role to give to the users in the role 'FromRole' when they start streaming - /// - public ulong AddRoleId { get; set; } - - /// - /// Id of the role whose users are eligible to get the 'AddRole' - /// - public ulong FromRoleId { get; set; } - - /// - /// If set, feature will only apply to users who have this keyword in their streaming status. - /// - public string Keyword { get; set; } - - /// - /// A collection of whitelisted users' IDs. Whitelisted users don't require 'keyword' in - /// order to get the stream role. - /// - public HashSet Whitelist { get; set; } = new HashSet(); - - /// - /// A collection of blacklisted users' IDs. Blacklisted useres will never get the stream role. - /// - public HashSet Blacklist { get; set; } = new HashSet(); + return x.UserId == UserId; } - public class StreamRoleBlacklistedUser : DbEntity + public override int GetHashCode() { - public ulong UserId { get; set; } - public string Username { get; set; } - - public override bool Equals(object obj) - { - if (!(obj is StreamRoleBlacklistedUser x)) - return false; - - return x.UserId == UserId; - } - - public override int GetHashCode() - { - return UserId.GetHashCode(); - } - } - - public class StreamRoleWhitelistedUser : DbEntity - { - public ulong UserId { get; set; } - public string Username { get; set; } - - public override bool Equals(object obj) - { - return obj is StreamRoleWhitelistedUser x - ? x.UserId == UserId - : false; - } - - public override int GetHashCode() - { - return UserId.GetHashCode(); - } + return UserId.GetHashCode(); } } + +public class StreamRoleWhitelistedUser : DbEntity +{ + public ulong UserId { get; set; } + public string Username { get; set; } + + public override bool Equals(object obj) + { + return obj is StreamRoleWhitelistedUser x + ? x.UserId == UserId + : false; + } + + public override int GetHashCode() + { + return UserId.GetHashCode(); + } +} \ No newline at end of file diff --git a/src/NadekoBot/Db/Models/UnbanTimer.cs b/src/NadekoBot/Db/Models/UnbanTimer.cs index ef771a1fd..99b171b5c 100644 --- a/src/NadekoBot/Db/Models/UnbanTimer.cs +++ b/src/NadekoBot/Db/Models/UnbanTimer.cs @@ -1,20 +1,17 @@ -using System; +namespace NadekoBot.Services.Database.Models; -namespace NadekoBot.Services.Database.Models +public class UnbanTimer : DbEntity { - public class UnbanTimer : DbEntity + public ulong UserId { get; set; } + public DateTime UnbanAt { get; set; } + + public override int GetHashCode() => + UserId.GetHashCode(); + + public override bool Equals(object obj) { - public ulong UserId { get; set; } - public DateTime UnbanAt { get; set; } - - public override int GetHashCode() => - UserId.GetHashCode(); - - public override bool Equals(object obj) - { - return obj is UnbanTimer ut - ? ut.UserId == UserId - : false; - } + return obj is UnbanTimer ut + ? ut.UserId == UserId + : false; } -} +} \ No newline at end of file diff --git a/src/NadekoBot/Db/Models/UnmuteTimer.cs b/src/NadekoBot/Db/Models/UnmuteTimer.cs index a26bb6348..29c4dd5f1 100644 --- a/src/NadekoBot/Db/Models/UnmuteTimer.cs +++ b/src/NadekoBot/Db/Models/UnmuteTimer.cs @@ -1,20 +1,17 @@ -using System; +namespace NadekoBot.Services.Database.Models; -namespace NadekoBot.Services.Database.Models +public class UnmuteTimer : DbEntity { - public class UnmuteTimer : DbEntity + public ulong UserId { get; set; } + public DateTime UnmuteAt { get; set; } + + public override int GetHashCode() => + UserId.GetHashCode(); + + public override bool Equals(object obj) { - public ulong UserId { get; set; } - public DateTime UnmuteAt { get; set; } - - public override int GetHashCode() => - UserId.GetHashCode(); - - public override bool Equals(object obj) - { - return obj is UnmuteTimer ut - ? ut.UserId == UserId - : false; - } + return obj is UnmuteTimer ut + ? ut.UserId == UserId + : false; } -} +} \ No newline at end of file diff --git a/src/NadekoBot/Db/Models/UnroleTimer.cs b/src/NadekoBot/Db/Models/UnroleTimer.cs index 47fb3a5c7..1a16a5c6b 100644 --- a/src/NadekoBot/Db/Models/UnroleTimer.cs +++ b/src/NadekoBot/Db/Models/UnroleTimer.cs @@ -1,21 +1,18 @@ -using System; +namespace NadekoBot.Services.Database.Models; -namespace NadekoBot.Services.Database.Models +public class UnroleTimer : DbEntity { - public class UnroleTimer : DbEntity + public ulong UserId { get; set; } + public ulong RoleId { get; set; } + public DateTime UnbanAt { get; set; } + + public override int GetHashCode() => + UserId.GetHashCode() ^ RoleId.GetHashCode(); + + public override bool Equals(object obj) { - public ulong UserId { get; set; } - public ulong RoleId { get; set; } - public DateTime UnbanAt { get; set; } - - public override int GetHashCode() => - UserId.GetHashCode() ^ RoleId.GetHashCode(); - - public override bool Equals(object obj) - { - return obj is UnroleTimer ut - ? ut.UserId == UserId && ut.RoleId == RoleId - : false; - } + return obj is UnroleTimer ut + ? ut.UserId == UserId && ut.RoleId == RoleId + : false; } -} +} \ No newline at end of file diff --git a/src/NadekoBot/Db/Models/UserXpStats.cs b/src/NadekoBot/Db/Models/UserXpStats.cs index c37489ca1..3f5654bfe 100644 --- a/src/NadekoBot/Db/Models/UserXpStats.cs +++ b/src/NadekoBot/Db/Models/UserXpStats.cs @@ -1,16 +1,13 @@ -using System; +namespace NadekoBot.Services.Database.Models; -namespace NadekoBot.Services.Database.Models +public class UserXpStats : DbEntity { - public class UserXpStats : DbEntity - { - public ulong UserId { get; set; } - public ulong GuildId { get; set; } - public int Xp { get; set; } - public int AwardedXp { get; set; } - public XpNotificationLocation NotifyOnLevelUp { get; set; } - public DateTime LastLevelUp { get; set; } = DateTime.UtcNow; - } - - public enum XpNotificationLocation { None, Dm, Channel } + public ulong UserId { get; set; } + public ulong GuildId { get; set; } + public int Xp { get; set; } + public int AwardedXp { get; set; } + public XpNotificationLocation NotifyOnLevelUp { get; set; } + public DateTime LastLevelUp { get; set; } = DateTime.UtcNow; } + +public enum XpNotificationLocation { None, Dm, Channel } \ No newline at end of file diff --git a/src/NadekoBot/Db/Models/VcRoleInfo.cs b/src/NadekoBot/Db/Models/VcRoleInfo.cs index 6560b1bb1..4ff3d23a9 100644 --- a/src/NadekoBot/Db/Models/VcRoleInfo.cs +++ b/src/NadekoBot/Db/Models/VcRoleInfo.cs @@ -1,8 +1,7 @@ -namespace NadekoBot.Services.Database.Models +namespace NadekoBot.Services.Database.Models; + +public class VcRoleInfo : DbEntity { - public class VcRoleInfo : DbEntity - { - public ulong VoiceChannelId { get; set; } - public ulong RoleId { get; set; } - } -} + public ulong VoiceChannelId { get; set; } + public ulong RoleId { get; set; } +} \ No newline at end of file diff --git a/src/NadekoBot/Db/Models/Waifu.cs b/src/NadekoBot/Db/Models/Waifu.cs index 87d6d1266..59c730bb4 100644 --- a/src/NadekoBot/Db/Models/Waifu.cs +++ b/src/NadekoBot/Db/Models/Waifu.cs @@ -1,90 +1,87 @@ using NadekoBot.Extensions; -using System.Collections.Generic; -using NadekoBot.Modules.Gambling.Common; using NadekoBot.Db.Models; -namespace NadekoBot.Services.Database.Models +namespace NadekoBot.Services.Database.Models; + +public class WaifuInfo : DbEntity { - public class WaifuInfo : DbEntity + public int WaifuId { get; set; } + public DiscordUser Waifu { get; set; } + + public int? ClaimerId { get; set; } + public DiscordUser Claimer { get; set; } + + public int? AffinityId { get; set; } + public DiscordUser Affinity { get; set; } + + public int Price { get; set; } + public List Items { get; set; } = new List(); + + public override string ToString() { - public int WaifuId { get; set; } - public DiscordUser Waifu { get; set; } + var claimer = "no one"; + var status = ""; - public int? ClaimerId { get; set; } - public DiscordUser Claimer { get; set; } + var waifuUsername = Waifu.Username.TrimTo(20); + var claimerUsername = Claimer?.Username.TrimTo(20); - public int? AffinityId { get; set; } - public DiscordUser Affinity { get; set; } - - public int Price { get; set; } - public List Items { get; set; } = new List(); - - public override string ToString() + if (ClaimerId != null) { - var claimer = "no one"; - var status = ""; - - var waifuUsername = Waifu.Username.TrimTo(20); - var claimerUsername = Claimer?.Username.TrimTo(20); - - if (ClaimerId != null) - { - claimer = $"{ claimerUsername }#{Claimer.Discriminator}"; - } - if (AffinityId is null) - { - status = $"... but {waifuUsername}'s heart is empty"; - } - else if (AffinityId == ClaimerId) - { - status = $"... and {waifuUsername} likes {claimerUsername} too <3"; - } - else - { - status = $"... but {waifuUsername}'s heart belongs to {Affinity.Username.TrimTo(20)}#{Affinity.Discriminator}"; - } - return $"**{waifuUsername}#{Waifu.Discriminator}** - claimed by **{claimer}**\n\t{status}"; + claimer = $"{ claimerUsername }#{Claimer.Discriminator}"; } + if (AffinityId is null) + { + status = $"... but {waifuUsername}'s heart is empty"; + } + else if (AffinityId == ClaimerId) + { + status = $"... and {waifuUsername} likes {claimerUsername} too <3"; + } + else + { + status = $"... but {waifuUsername}'s heart belongs to {Affinity.Username.TrimTo(20)}#{Affinity.Discriminator}"; + } + return $"**{waifuUsername}#{Waifu.Discriminator}** - claimed by **{claimer}**\n\t{status}"; } +} - public class WaifuLbResult +public class WaifuLbResult +{ + public string Username { get; set; } + public string Discrim { get; set; } + + public string Claimer { get; set; } + public string ClaimerDiscrim { get; set; } + + public string Affinity { get; set; } + public string AffinityDiscrim { get; set; } + + public int Price { get; set; } + + public override string ToString() { - public string Username { get; set; } - public string Discrim { get; set; } + var claimer = "no one"; + var status = ""; - public string Claimer { get; set; } - public string ClaimerDiscrim { get; set; } + var waifuUsername = Username.TrimTo(20); + var claimerUsername = Claimer?.TrimTo(20); - public string Affinity { get; set; } - public string AffinityDiscrim { get; set; } - - public int Price { get; set; } - - public override string ToString() + if (Claimer != null) { - var claimer = "no one"; - var status = ""; - - var waifuUsername = Username.TrimTo(20); - var claimerUsername = Claimer?.TrimTo(20); - - if (Claimer != null) - { - claimer = $"{ claimerUsername }#{ClaimerDiscrim}"; - } - if (Affinity is null) - { - status = $"... but {waifuUsername}'s heart is empty"; - } - else if (Affinity + AffinityDiscrim == Claimer + ClaimerDiscrim) - { - status = $"... and {waifuUsername} likes {claimerUsername} too <3"; - } - else - { - status = $"... but {waifuUsername}'s heart belongs to {Affinity.TrimTo(20)}#{AffinityDiscrim}"; - } - return $"**{waifuUsername}#{Discrim}** - claimed by **{claimer}**\n\t{status}"; + claimer = $"{ claimerUsername }#{ClaimerDiscrim}"; } + if (Affinity is null) + { + status = $"... but {waifuUsername}'s heart is empty"; + } + else if (Affinity + AffinityDiscrim == Claimer + ClaimerDiscrim) + { + status = $"... and {waifuUsername} likes {claimerUsername} too <3"; + } + else + { + status = $"... but {waifuUsername}'s heart belongs to {Affinity.TrimTo(20)}#{AffinityDiscrim}"; + } + return $"**{waifuUsername}#{Discrim}** - claimed by **{claimer}**\n\t{status}"; } } \ No newline at end of file diff --git a/src/NadekoBot/Db/Models/WaifuItem.cs b/src/NadekoBot/Db/Models/WaifuItem.cs index efca795d5..2e37ff42f 100644 --- a/src/NadekoBot/Db/Models/WaifuItem.cs +++ b/src/NadekoBot/Db/Models/WaifuItem.cs @@ -1,11 +1,8 @@ -using System; +namespace NadekoBot.Services.Database.Models; -namespace NadekoBot.Services.Database.Models +public class WaifuItem : DbEntity { - public class WaifuItem : DbEntity - { - public int? WaifuInfoId { get; set; } - public string ItemEmoji { get; set; } - public string Name { get; set; } - } + public int? WaifuInfoId { get; set; } + public string ItemEmoji { get; set; } + public string Name { get; set; } } \ No newline at end of file diff --git a/src/NadekoBot/Db/Models/WaifuUpdate.cs b/src/NadekoBot/Db/Models/WaifuUpdate.cs index b0e2f2b68..4e66453f1 100644 --- a/src/NadekoBot/Db/Models/WaifuUpdate.cs +++ b/src/NadekoBot/Db/Models/WaifuUpdate.cs @@ -1,23 +1,22 @@ using NadekoBot.Db.Models; -namespace NadekoBot.Services.Database.Models +namespace NadekoBot.Services.Database.Models; + +public class WaifuUpdate : DbEntity { - public class WaifuUpdate : DbEntity - { - public int UserId { get; set; } - public DiscordUser User { get; set; } - public WaifuUpdateType UpdateType { get; set; } + public int UserId { get; set; } + public DiscordUser User { get; set; } + public WaifuUpdateType UpdateType { get; set; } - public int? OldId { get; set; } - public DiscordUser Old { get; set; } + public int? OldId { get; set; } + public DiscordUser Old { get; set; } - public int? NewId { get; set; } - public DiscordUser New { get; set; } - } - - public enum WaifuUpdateType - { - AffinityChanged, - Claimed - } + public int? NewId { get; set; } + public DiscordUser New { get; set; } } + +public enum WaifuUpdateType +{ + AffinityChanged, + Claimed +} \ No newline at end of file diff --git a/src/NadekoBot/Db/Models/WarnExpireAction.cs b/src/NadekoBot/Db/Models/WarnExpireAction.cs index 20f079f6f..282c8afe9 100644 --- a/src/NadekoBot/Db/Models/WarnExpireAction.cs +++ b/src/NadekoBot/Db/Models/WarnExpireAction.cs @@ -1,8 +1,7 @@ -namespace NadekoBot.Services.Database.Models +namespace NadekoBot.Services.Database.Models; + +public enum WarnExpireAction { - public enum WarnExpireAction - { - Clear, - Delete - } -} + Clear, + Delete +} \ No newline at end of file diff --git a/src/NadekoBot/Db/Models/Warning.cs b/src/NadekoBot/Db/Models/Warning.cs index 0935b08a2..2bfb6ee2b 100644 --- a/src/NadekoBot/Db/Models/Warning.cs +++ b/src/NadekoBot/Db/Models/Warning.cs @@ -1,13 +1,12 @@ -namespace NadekoBot.Services.Database.Models +namespace NadekoBot.Services.Database.Models; + +public class Warning : DbEntity { - public class Warning : DbEntity - { - public ulong GuildId { get; set; } - public ulong UserId { get; set; } - public string Reason { get; set; } - public bool Forgiven { get; set; } - public string ForgivenBy { get; set; } - public string Moderator { get; set; } - public int Weight { get; set; } - } -} + public ulong GuildId { get; set; } + public ulong UserId { get; set; } + public string Reason { get; set; } + public bool Forgiven { get; set; } + public string ForgivenBy { get; set; } + public string Moderator { get; set; } + public int Weight { get; set; } +} \ No newline at end of file diff --git a/src/NadekoBot/Db/Models/WarningPunishment.cs b/src/NadekoBot/Db/Models/WarningPunishment.cs index 00d0187a6..6cb1035fa 100644 --- a/src/NadekoBot/Db/Models/WarningPunishment.cs +++ b/src/NadekoBot/Db/Models/WarningPunishment.cs @@ -1,10 +1,9 @@ -namespace NadekoBot.Services.Database.Models +namespace NadekoBot.Services.Database.Models; + +public class WarningPunishment : DbEntity { - public class WarningPunishment : DbEntity - { - public int Count { get; set; } - public PunishmentAction Punishment { get; set; } - public int Time { get; set; } - public ulong? RoleId { get; set; } - } -} + public int Count { get; set; } + public PunishmentAction Punishment { get; set; } + public int Time { get; set; } + public ulong? RoleId { get; set; } +} \ No newline at end of file diff --git a/src/NadekoBot/Db/Models/XpSettings.cs b/src/NadekoBot/Db/Models/XpSettings.cs index abd16e84a..9d5ceb16d 100644 --- a/src/NadekoBot/Db/Models/XpSettings.cs +++ b/src/NadekoBot/Db/Models/XpSettings.cs @@ -1,76 +1,73 @@ -using System.Collections.Generic; +namespace NadekoBot.Services.Database.Models; -namespace NadekoBot.Services.Database.Models +public class XpSettings : DbEntity { - public class XpSettings : DbEntity - { - public int GuildConfigId { get; set; } - public GuildConfig GuildConfig { get; set; } + public int GuildConfigId { get; set; } + public GuildConfig GuildConfig { get; set; } - public HashSet RoleRewards { get; set; } = new HashSet(); - public HashSet CurrencyRewards { get; set; } = new HashSet(); - public HashSet ExclusionList { get; set; } = new HashSet(); - public bool ServerExcluded { get; set; } - } + public HashSet RoleRewards { get; set; } = new HashSet(); + public HashSet CurrencyRewards { get; set; } = new HashSet(); + public HashSet ExclusionList { get; set; } = new HashSet(); + public bool ServerExcluded { get; set; } +} - public enum ExcludedItemType { Channel, Role } +public enum ExcludedItemType { Channel, Role } - public class XpRoleReward : DbEntity - { - public int XpSettingsId { get; set; } - public XpSettings XpSettings { get; set; } +public class XpRoleReward : DbEntity +{ + public int XpSettingsId { get; set; } + public XpSettings XpSettings { get; set; } - public int Level { get; set; } - public ulong RoleId { get; set; } + public int Level { get; set; } + public ulong RoleId { get; set; } - /// - /// Whether the role should be removed (true) or added (false) - /// - public bool Remove { get; set; } + /// + /// Whether the role should be removed (true) or added (false) + /// + public bool Remove { get; set; } - public override int GetHashCode() - { - return Level.GetHashCode() ^ XpSettingsId.GetHashCode(); - } - - public override bool Equals(object obj) - { - return obj is XpRoleReward xrr && xrr.Level == Level && xrr.XpSettingsId == XpSettingsId; - } + public override int GetHashCode() + { + return Level.GetHashCode() ^ XpSettingsId.GetHashCode(); } - public class XpCurrencyReward : DbEntity + public override bool Equals(object obj) { - public int XpSettingsId { get; set; } - public XpSettings XpSettings { get; set; } - - public int Level { get; set; } - public int Amount { get; set; } - - public override int GetHashCode() - { - return Level.GetHashCode() ^ XpSettingsId.GetHashCode(); - } - - public override bool Equals(object obj) - { - return obj is XpCurrencyReward xrr && xrr.Level == Level && xrr.XpSettingsId == XpSettingsId; - } - } - - public class ExcludedItem : DbEntity - { - public ulong ItemId { get; set; } - public ExcludedItemType ItemType { get; set; } - - public override int GetHashCode() - { - return ItemId.GetHashCode() ^ ItemType.GetHashCode(); - } - - public override bool Equals(object obj) - { - return obj is ExcludedItem ei && ei.ItemId == ItemId && ei.ItemType == ItemType; - } + return obj is XpRoleReward xrr && xrr.Level == Level && xrr.XpSettingsId == XpSettingsId; } } + +public class XpCurrencyReward : DbEntity +{ + public int XpSettingsId { get; set; } + public XpSettings XpSettings { get; set; } + + public int Level { get; set; } + public int Amount { get; set; } + + public override int GetHashCode() + { + return Level.GetHashCode() ^ XpSettingsId.GetHashCode(); + } + + public override bool Equals(object obj) + { + return obj is XpCurrencyReward xrr && xrr.Level == Level && xrr.XpSettingsId == XpSettingsId; + } +} + +public class ExcludedItem : DbEntity +{ + public ulong ItemId { get; set; } + public ExcludedItemType ItemType { get; set; } + + public override int GetHashCode() + { + return ItemId.GetHashCode() ^ ItemType.GetHashCode(); + } + + public override bool Equals(object obj) + { + return obj is ExcludedItem ei && ei.ItemId == ItemId && ei.ItemType == ItemType; + } +} \ No newline at end of file diff --git a/src/NadekoBot/Db/NadekoContext.cs b/src/NadekoBot/Db/NadekoContext.cs index 46ef96d06..113202322 100644 --- a/src/NadekoBot/Db/NadekoContext.cs +++ b/src/NadekoBot/Db/NadekoContext.cs @@ -2,389 +2,387 @@ using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Design; using NadekoBot.Services.Database.Models; -using System; using System.IO; using Microsoft.Extensions.Logging; using NadekoBot.Db.Models; -namespace NadekoBot.Services.Database +namespace NadekoBot.Services.Database; + +public class NadekoContextFactory : IDesignTimeDbContextFactory { - public class NadekoContextFactory : IDesignTimeDbContextFactory + public NadekoContext CreateDbContext(string[] args) { - public NadekoContext CreateDbContext(string[] args) - { - LogSetup.SetupLogger(-2); - var optionsBuilder = new DbContextOptionsBuilder(); - IBotCredentials creds = new BotCredsProvider().GetCreds(); - var builder = new SqliteConnectionStringBuilder(creds.Db.ConnectionString); - builder.DataSource = Path.Combine(AppContext.BaseDirectory, builder.DataSource); - optionsBuilder.UseSqlite(builder.ToString()); - var ctx = new NadekoContext(optionsBuilder.Options); - ctx.Database.SetCommandTimeout(60); - return ctx; - } - } - - public class NadekoContext : DbContext - { - public DbSet GuildConfigs { get; set; } - - public DbSet Quotes { get; set; } - public DbSet Reminders { get; set; } - public DbSet SelfAssignableRoles { get; set; } - public DbSet MusicPlaylists { get; set; } - public DbSet CustomReactions { get; set; } - public DbSet CurrencyTransactions { get; set; } - public DbSet WaifuUpdates { get; set; } - public DbSet Warnings { get; set; } - public DbSet UserXpStats { get; set; } - public DbSet Clubs { get; set; } - - //logging - public DbSet LogSettings { get; set; } - public DbSet IgnoredVoicePresenceCHannels { get; set; } - public DbSet IgnoredLogChannels { get; set; } - - public DbSet RotatingStatus { get; set; } - public DbSet Blacklist { get; set; } - public DbSet AutoCommands { get; set; } - - public DbSet RewardedUsers { get; set; } - public DbSet PlantedCurrency { get; set; } - public DbSet BanTemplates { get; set; } - public DbSet DiscordPermOverrides { get; set; } - public DbSet DiscordUser { get; set; } - public DbSet MusicPlayerSettings { get; set; } - public DbSet Repeaters { get; set; } - public DbSet Poll { get; set; } - public DbSet WaifuInfo { get; set; } - public DbSet ImageOnlyChannels { get; set; } - public DbSet NsfwBlacklistedTags { get; set; } - public DbSet AutoTranslateChannels { get; set; } - public DbSet AutoTranslateUsers { get; set; } - - public NadekoContext(DbContextOptions options) : base(options) - { - } - -#if DEBUG - public static readonly ILoggerFactory _debugLoggerFactory = - LoggerFactory.Create(x => x.AddConsole()); - protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) - { - optionsBuilder.UseLoggerFactory(_debugLoggerFactory); - } -#endif - - protected override void OnModelCreating(ModelBuilder modelBuilder) - { - #region QUOTES - - var quoteEntity = modelBuilder.Entity(); - quoteEntity.HasIndex(x => x.GuildId); - quoteEntity.HasIndex(x => x.Keyword); - - #endregion - - #region GuildConfig - - var configEntity = modelBuilder.Entity(); - configEntity - .HasIndex(c => c.GuildId) - .IsUnique(); - - modelBuilder.Entity() - .HasOne(x => x.GuildConfig) - .WithOne(x => x.AntiSpamSetting); - - modelBuilder.Entity() - .HasOne(x => x.GuildConfig) - .WithOne(x => x.AntiRaidSetting); - - modelBuilder.Entity() - .HasOne(x => x.AntiAltSetting) - .WithOne() - .HasForeignKey(x => x.GuildConfigId) - .OnDelete(DeleteBehavior.Cascade); - - modelBuilder.Entity() - .HasAlternateKey(x => new { x.GuildConfigId, x.Url }); - - modelBuilder.Entity() - .HasIndex(x => x.MessageId) - .IsUnique(); - - modelBuilder.Entity() - .HasIndex(x => x.ChannelId); - - configEntity.HasIndex(x => x.WarnExpireHours) - .IsUnique(false); - - #endregion - - #region streamrole - modelBuilder.Entity() - .HasOne(x => x.GuildConfig) - .WithOne(x => x.StreamRole); - #endregion - - #region Self Assignable Roles - - var selfassignableRolesEntity = modelBuilder.Entity(); - - selfassignableRolesEntity - .HasIndex(s => new { s.GuildId, s.RoleId }) - .IsUnique(); - - selfassignableRolesEntity - .Property(x => x.Group) - .HasDefaultValue(0); - - #endregion - - #region MusicPlaylists - var musicPlaylistEntity = modelBuilder.Entity(); - - musicPlaylistEntity - .HasMany(p => p.Songs) - .WithOne() - .OnDelete(DeleteBehavior.Cascade); - - - #endregion - - #region Waifus - - var wi = modelBuilder.Entity(); - wi.HasOne(x => x.Waifu) - .WithOne(); - - wi.HasIndex(x => x.Price); - wi.HasIndex(x => x.ClaimerId); - // wi.HasMany(x => x.Items) - // .WithOne() - // .OnDelete(DeleteBehavior.Cascade); - - var wu = modelBuilder.Entity(); - #endregion - - #region DiscordUser - - modelBuilder.Entity(du => - { - du.Property(x => x.IsClubAdmin) - .HasDefaultValue(false); - - du.Property(x => x.NotifyOnLevelUp) - .HasDefaultValue(XpNotificationLocation.None); - - du.Property(x => x.LastXpGain) - .HasDefaultValueSql("datetime('now', '-1 years')"); - - du.Property(x => x.LastLevelUp) - .HasDefaultValueSql("datetime('now')"); - - du.HasAlternateKey(w => w.UserId); - du.HasOne(x => x.Club) - .WithMany(x => x.Users) - .IsRequired(false); - - du.HasIndex(x => x.TotalXp); - du.HasIndex(x => x.CurrencyAmount); - du.HasIndex(x => x.UserId); - }); - - - #endregion - - #region Warnings - - modelBuilder.Entity(warn => - { - warn.HasIndex(x => x.GuildId); - warn.HasIndex(x => x.UserId); - warn.HasIndex(x => x.DateAdded); - warn.Property(x => x.Weight) - .HasDefaultValue(1); - }); - - #endregion - - #region PatreonRewards - var pr = modelBuilder.Entity(); - pr.HasIndex(x => x.PatreonUserId) - .IsUnique(); - #endregion - - #region XpStats - var xps = modelBuilder.Entity(); - xps - .HasIndex(x => new { x.UserId, x.GuildId }) - .IsUnique(); - - xps - .Property(x => x.LastLevelUp) - .HasDefaultValue(new DateTime(2017, 9, 21, 20, 53, 13, 307, DateTimeKind.Local)); - - xps.HasIndex(x => x.UserId); - xps.HasIndex(x => x.GuildId); - xps.HasIndex(x => x.Xp); - xps.HasIndex(x => x.AwardedXp); - - #endregion - - #region XpSettings - modelBuilder.Entity() - .HasOne(x => x.GuildConfig) - .WithOne(x => x.XpSettings); - #endregion - - #region XpRoleReward - modelBuilder.Entity() - .HasIndex(x => new { x.XpSettingsId, x.Level }) - .IsUnique(); - #endregion - - #region Club - var ci = modelBuilder.Entity(); - ci.HasOne(x => x.Owner) - .WithOne() - .HasForeignKey(x => x.OwnerId); - - - ci.HasAlternateKey(x => new { x.Name, x.Discrim }); - #endregion - - #region ClubManytoMany - - modelBuilder.Entity() - .HasKey(t => new { t.ClubId, t.UserId }); - - modelBuilder.Entity() - .HasOne(pt => pt.User) - .WithMany(); - - modelBuilder.Entity() - .HasOne(pt => pt.Club) - .WithMany(x => x.Applicants); - - modelBuilder.Entity() - .HasKey(t => new { t.ClubId, t.UserId }); - - modelBuilder.Entity() - .HasOne(pt => pt.User) - .WithMany(); - - modelBuilder.Entity() - .HasOne(pt => pt.Club) - .WithMany(x => x.Bans); - - #endregion - - #region Polls - modelBuilder.Entity() - .HasIndex(x => x.GuildId) - .IsUnique(); - #endregion - - #region CurrencyTransactions - modelBuilder.Entity() - .HasIndex(x => x.UserId) - .IsUnique(false); - #endregion - - #region Reminders - modelBuilder.Entity() - .HasIndex(x => x.When); - #endregion - - #region GroupName - modelBuilder.Entity() - .HasIndex(x => new { x.GuildConfigId, x.Number }) - .IsUnique(); - - modelBuilder.Entity() - .HasOne(x => x.GuildConfig) - .WithMany(x => x.SelfAssignableRoleGroupNames) - .IsRequired(); - #endregion - - #region BanTemplate - - modelBuilder.Entity() - .HasIndex(x => x.GuildId) - .IsUnique(); - - #endregion - - #region Perm Override - - modelBuilder.Entity() - .HasIndex(x => new {x.GuildId, x.Command}) - .IsUnique(); - - #endregion - - #region Music - - modelBuilder.Entity() - .HasIndex(x => x.GuildId) - .IsUnique(); - - modelBuilder.Entity() - .Property(x => x.Volume) - .HasDefaultValue(100); - - #endregion - - #region Reaction roles - - modelBuilder.Entity(rrm => rrm - .HasMany(x => x.ReactionRoles) - .WithOne() - .OnDelete(DeleteBehavior.Cascade)); - - #endregion - - #region LogSettings - - modelBuilder.Entity(ls => ls - .HasIndex(x => x.GuildId) - .IsUnique()); - - modelBuilder.Entity(ls => ls - .HasMany(x => x.LogIgnores) - .WithOne(x => x.LogSetting) - .OnDelete(DeleteBehavior.Cascade)); - - modelBuilder.Entity(ili => ili - .HasIndex(x => new { x.LogSettingId, x.LogItemId, x.ItemType }) - .IsUnique()); - - #endregion - - modelBuilder.Entity(ioc => ioc - .HasIndex(x => x.ChannelId) - .IsUnique()); - - modelBuilder.Entity(nbt => nbt - .HasIndex(x => x.GuildId) - .IsUnique(false)); - - var atch = modelBuilder.Entity(); - atch.HasIndex(x => x.GuildId) - .IsUnique(false); - - atch.HasIndex(x => x.ChannelId) - .IsUnique(); - - atch - .HasMany(x => x.Users) - .WithOne(x => x.Channel) - .OnDelete(DeleteBehavior.Cascade); - - modelBuilder.Entity(atu => atu - .HasAlternateKey(x => new { x.ChannelId, x.UserId })); - } + LogSetup.SetupLogger(-2); + var optionsBuilder = new DbContextOptionsBuilder(); + IBotCredentials creds = new BotCredsProvider().GetCreds(); + var builder = new SqliteConnectionStringBuilder(creds.Db.ConnectionString); + builder.DataSource = Path.Combine(AppContext.BaseDirectory, builder.DataSource); + optionsBuilder.UseSqlite(builder.ToString()); + var ctx = new NadekoContext(optionsBuilder.Options); + ctx.Database.SetCommandTimeout(60); + return ctx; } } + +public class NadekoContext : DbContext +{ + public DbSet GuildConfigs { get; set; } + + public DbSet Quotes { get; set; } + public DbSet Reminders { get; set; } + public DbSet SelfAssignableRoles { get; set; } + public DbSet MusicPlaylists { get; set; } + public DbSet CustomReactions { get; set; } + public DbSet CurrencyTransactions { get; set; } + public DbSet WaifuUpdates { get; set; } + public DbSet Warnings { get; set; } + public DbSet UserXpStats { get; set; } + public DbSet Clubs { get; set; } + + //logging + public DbSet LogSettings { get; set; } + public DbSet IgnoredVoicePresenceCHannels { get; set; } + public DbSet IgnoredLogChannels { get; set; } + + public DbSet RotatingStatus { get; set; } + public DbSet Blacklist { get; set; } + public DbSet AutoCommands { get; set; } + + public DbSet RewardedUsers { get; set; } + public DbSet PlantedCurrency { get; set; } + public DbSet BanTemplates { get; set; } + public DbSet DiscordPermOverrides { get; set; } + public DbSet DiscordUser { get; set; } + public DbSet MusicPlayerSettings { get; set; } + public DbSet Repeaters { get; set; } + public DbSet Poll { get; set; } + public DbSet WaifuInfo { get; set; } + public DbSet ImageOnlyChannels { get; set; } + public DbSet NsfwBlacklistedTags { get; set; } + public DbSet AutoTranslateChannels { get; set; } + public DbSet AutoTranslateUsers { get; set; } + + public NadekoContext(DbContextOptions options) : base(options) + { + } + +#if DEBUG + public static readonly ILoggerFactory _debugLoggerFactory = + LoggerFactory.Create(x => x.AddConsole()); + protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) + { + optionsBuilder.UseLoggerFactory(_debugLoggerFactory); + } +#endif + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + #region QUOTES + + var quoteEntity = modelBuilder.Entity(); + quoteEntity.HasIndex(x => x.GuildId); + quoteEntity.HasIndex(x => x.Keyword); + + #endregion + + #region GuildConfig + + var configEntity = modelBuilder.Entity(); + configEntity + .HasIndex(c => c.GuildId) + .IsUnique(); + + modelBuilder.Entity() + .HasOne(x => x.GuildConfig) + .WithOne(x => x.AntiSpamSetting); + + modelBuilder.Entity() + .HasOne(x => x.GuildConfig) + .WithOne(x => x.AntiRaidSetting); + + modelBuilder.Entity() + .HasOne(x => x.AntiAltSetting) + .WithOne() + .HasForeignKey(x => x.GuildConfigId) + .OnDelete(DeleteBehavior.Cascade); + + modelBuilder.Entity() + .HasAlternateKey(x => new { x.GuildConfigId, x.Url }); + + modelBuilder.Entity() + .HasIndex(x => x.MessageId) + .IsUnique(); + + modelBuilder.Entity() + .HasIndex(x => x.ChannelId); + + configEntity.HasIndex(x => x.WarnExpireHours) + .IsUnique(false); + + #endregion + + #region streamrole + modelBuilder.Entity() + .HasOne(x => x.GuildConfig) + .WithOne(x => x.StreamRole); + #endregion + + #region Self Assignable Roles + + var selfassignableRolesEntity = modelBuilder.Entity(); + + selfassignableRolesEntity + .HasIndex(s => new { s.GuildId, s.RoleId }) + .IsUnique(); + + selfassignableRolesEntity + .Property(x => x.Group) + .HasDefaultValue(0); + + #endregion + + #region MusicPlaylists + var musicPlaylistEntity = modelBuilder.Entity(); + + musicPlaylistEntity + .HasMany(p => p.Songs) + .WithOne() + .OnDelete(DeleteBehavior.Cascade); + + + #endregion + + #region Waifus + + var wi = modelBuilder.Entity(); + wi.HasOne(x => x.Waifu) + .WithOne(); + + wi.HasIndex(x => x.Price); + wi.HasIndex(x => x.ClaimerId); + // wi.HasMany(x => x.Items) + // .WithOne() + // .OnDelete(DeleteBehavior.Cascade); + + var wu = modelBuilder.Entity(); + #endregion + + #region DiscordUser + + modelBuilder.Entity(du => + { + du.Property(x => x.IsClubAdmin) + .HasDefaultValue(false); + + du.Property(x => x.NotifyOnLevelUp) + .HasDefaultValue(XpNotificationLocation.None); + + du.Property(x => x.LastXpGain) + .HasDefaultValueSql("datetime('now', '-1 years')"); + + du.Property(x => x.LastLevelUp) + .HasDefaultValueSql("datetime('now')"); + + du.HasAlternateKey(w => w.UserId); + du.HasOne(x => x.Club) + .WithMany(x => x.Users) + .IsRequired(false); + + du.HasIndex(x => x.TotalXp); + du.HasIndex(x => x.CurrencyAmount); + du.HasIndex(x => x.UserId); + }); + + + #endregion + + #region Warnings + + modelBuilder.Entity(warn => + { + warn.HasIndex(x => x.GuildId); + warn.HasIndex(x => x.UserId); + warn.HasIndex(x => x.DateAdded); + warn.Property(x => x.Weight) + .HasDefaultValue(1); + }); + + #endregion + + #region PatreonRewards + var pr = modelBuilder.Entity(); + pr.HasIndex(x => x.PatreonUserId) + .IsUnique(); + #endregion + + #region XpStats + var xps = modelBuilder.Entity(); + xps + .HasIndex(x => new { x.UserId, x.GuildId }) + .IsUnique(); + + xps + .Property(x => x.LastLevelUp) + .HasDefaultValue(new DateTime(2017, 9, 21, 20, 53, 13, 307, DateTimeKind.Local)); + + xps.HasIndex(x => x.UserId); + xps.HasIndex(x => x.GuildId); + xps.HasIndex(x => x.Xp); + xps.HasIndex(x => x.AwardedXp); + + #endregion + + #region XpSettings + modelBuilder.Entity() + .HasOne(x => x.GuildConfig) + .WithOne(x => x.XpSettings); + #endregion + + #region XpRoleReward + modelBuilder.Entity() + .HasIndex(x => new { x.XpSettingsId, x.Level }) + .IsUnique(); + #endregion + + #region Club + var ci = modelBuilder.Entity(); + ci.HasOne(x => x.Owner) + .WithOne() + .HasForeignKey(x => x.OwnerId); + + + ci.HasAlternateKey(x => new { x.Name, x.Discrim }); + #endregion + + #region ClubManytoMany + + modelBuilder.Entity() + .HasKey(t => new { t.ClubId, t.UserId }); + + modelBuilder.Entity() + .HasOne(pt => pt.User) + .WithMany(); + + modelBuilder.Entity() + .HasOne(pt => pt.Club) + .WithMany(x => x.Applicants); + + modelBuilder.Entity() + .HasKey(t => new { t.ClubId, t.UserId }); + + modelBuilder.Entity() + .HasOne(pt => pt.User) + .WithMany(); + + modelBuilder.Entity() + .HasOne(pt => pt.Club) + .WithMany(x => x.Bans); + + #endregion + + #region Polls + modelBuilder.Entity() + .HasIndex(x => x.GuildId) + .IsUnique(); + #endregion + + #region CurrencyTransactions + modelBuilder.Entity() + .HasIndex(x => x.UserId) + .IsUnique(false); + #endregion + + #region Reminders + modelBuilder.Entity() + .HasIndex(x => x.When); + #endregion + + #region GroupName + modelBuilder.Entity() + .HasIndex(x => new { x.GuildConfigId, x.Number }) + .IsUnique(); + + modelBuilder.Entity() + .HasOne(x => x.GuildConfig) + .WithMany(x => x.SelfAssignableRoleGroupNames) + .IsRequired(); + #endregion + + #region BanTemplate + + modelBuilder.Entity() + .HasIndex(x => x.GuildId) + .IsUnique(); + + #endregion + + #region Perm Override + + modelBuilder.Entity() + .HasIndex(x => new {x.GuildId, x.Command}) + .IsUnique(); + + #endregion + + #region Music + + modelBuilder.Entity() + .HasIndex(x => x.GuildId) + .IsUnique(); + + modelBuilder.Entity() + .Property(x => x.Volume) + .HasDefaultValue(100); + + #endregion + + #region Reaction roles + + modelBuilder.Entity(rrm => rrm + .HasMany(x => x.ReactionRoles) + .WithOne() + .OnDelete(DeleteBehavior.Cascade)); + + #endregion + + #region LogSettings + + modelBuilder.Entity(ls => ls + .HasIndex(x => x.GuildId) + .IsUnique()); + + modelBuilder.Entity(ls => ls + .HasMany(x => x.LogIgnores) + .WithOne(x => x.LogSetting) + .OnDelete(DeleteBehavior.Cascade)); + + modelBuilder.Entity(ili => ili + .HasIndex(x => new { x.LogSettingId, x.LogItemId, x.ItemType }) + .IsUnique()); + + #endregion + + modelBuilder.Entity(ioc => ioc + .HasIndex(x => x.ChannelId) + .IsUnique()); + + modelBuilder.Entity(nbt => nbt + .HasIndex(x => x.GuildId) + .IsUnique(false)); + + var atch = modelBuilder.Entity(); + atch.HasIndex(x => x.GuildId) + .IsUnique(false); + + atch.HasIndex(x => x.ChannelId) + .IsUnique(); + + atch + .HasMany(x => x.Users) + .WithOne(x => x.Channel) + .OnDelete(DeleteBehavior.Cascade); + + modelBuilder.Entity(atu => atu + .HasAlternateKey(x => new { x.ChannelId, x.UserId })); + } +} \ No newline at end of file diff --git a/src/NadekoBot/Modules/Administration/Administration.cs b/src/NadekoBot/Modules/Administration/Administration.cs index 733f4b05b..22260382f 100644 --- a/src/NadekoBot/Modules/Administration/Administration.cs +++ b/src/NadekoBot/Modules/Administration/Administration.cs @@ -5,349 +5,346 @@ using NadekoBot.Common.Attributes; using NadekoBot.Common.TypeReaders.Models; using NadekoBot.Extensions; using NadekoBot.Modules.Administration.Services; -using System; -using System.Linq; using System.Threading.Tasks; -namespace NadekoBot.Modules.Administration +namespace NadekoBot.Modules.Administration; + +public partial class Administration : NadekoModule { - public partial class Administration : NadekoModule + private readonly ImageOnlyChannelService _imageOnly; + + public Administration(ImageOnlyChannelService imageOnly) { - private readonly ImageOnlyChannelService _imageOnly; - - public Administration(ImageOnlyChannelService imageOnly) - { - _imageOnly = imageOnly; - } + _imageOnly = imageOnly; + } - public enum List - { - List = 0, - Ls = 0 - } + public enum List + { + List = 0, + Ls = 0 + } - [NadekoCommand, Aliases] - [RequireContext(ContextType.Guild)] - [UserPerm(GuildPerm.Administrator)] - [BotPerm(GuildPerm.Administrator)] - public async Task ImageOnlyChannel(StoopidTime time = null) - { - var newValue = _imageOnly.ToggleImageOnlyChannel(ctx.Guild.Id, ctx.Channel.Id); - if (newValue) - await ReplyConfirmLocalizedAsync(strs.imageonly_enable); - else - await ReplyPendingLocalizedAsync(strs.imageonly_disable); - } + [NadekoCommand, Aliases] + [RequireContext(ContextType.Guild)] + [UserPerm(GuildPerm.Administrator)] + [BotPerm(GuildPerm.Administrator)] + public async Task ImageOnlyChannel(StoopidTime time = null) + { + var newValue = _imageOnly.ToggleImageOnlyChannel(ctx.Guild.Id, ctx.Channel.Id); + if (newValue) + await ReplyConfirmLocalizedAsync(strs.imageonly_enable); + else + await ReplyPendingLocalizedAsync(strs.imageonly_disable); + } - [NadekoCommand, Aliases] - [RequireContext(ContextType.Guild)] - [UserPerm(ChannelPerm.ManageChannel)] - [BotPerm(ChannelPerm.ManageChannel)] - public async Task Slowmode(StoopidTime time = null) - { - var seconds = (int?)time?.Time.TotalSeconds ?? 0; - if (time is not null && (time.Time < TimeSpan.FromSeconds(0) || time.Time > TimeSpan.FromHours(6))) - return; + [NadekoCommand, Aliases] + [RequireContext(ContextType.Guild)] + [UserPerm(ChannelPerm.ManageChannel)] + [BotPerm(ChannelPerm.ManageChannel)] + public async Task Slowmode(StoopidTime time = null) + { + var seconds = (int?)time?.Time.TotalSeconds ?? 0; + if (time is not null && (time.Time < TimeSpan.FromSeconds(0) || time.Time > TimeSpan.FromHours(6))) + return; - await ((ITextChannel) ctx.Channel).ModifyAsync(tcp => + await ((ITextChannel) ctx.Channel).ModifyAsync(tcp => + { + tcp.SlowModeInterval = seconds; + }); + + await ctx.OkAsync(); + } + + [NadekoCommand, Aliases] + [RequireContext(ContextType.Guild)] + [UserPerm(GuildPerm.Administrator)] + [BotPerm(GuildPerm.ManageMessages)] + [Priority(2)] + public async Task Delmsgoncmd(List _) + { + var guild = (SocketGuild) ctx.Guild; + var (enabled, channels) = _service.GetDelMsgOnCmdData(ctx.Guild.Id); + + var embed = _eb.Create() + .WithOkColor() + .WithTitle(GetText(strs.server_delmsgoncmd)) + .WithDescription(enabled ? "✅" : "❌"); + + var str = string.Join("\n", channels + .Select(x => { - tcp.SlowModeInterval = seconds; - }); + var ch = guild.GetChannel(x.ChannelId)?.ToString() + ?? x.ChannelId.ToString(); + var prefix = x.State ? "✅ " : "❌ "; + return prefix + ch; + })); - await ctx.OkAsync(); - } + if (string.IsNullOrWhiteSpace(str)) + str = "-"; - [NadekoCommand, Aliases] - [RequireContext(ContextType.Guild)] - [UserPerm(GuildPerm.Administrator)] - [BotPerm(GuildPerm.ManageMessages)] - [Priority(2)] - public async Task Delmsgoncmd(List _) + embed.AddField(GetText(strs.channel_delmsgoncmd), str); + + await ctx.Channel.EmbedAsync(embed).ConfigureAwait(false); + } + + public enum Server + { + Server + } + + [NadekoCommand, Aliases] + [RequireContext(ContextType.Guild)] + [UserPerm(GuildPerm.Administrator)] + [BotPerm(GuildPerm.ManageMessages)] + [Priority(1)] + public async Task Delmsgoncmd(Server _ = Server.Server) + { + if (_service.ToggleDeleteMessageOnCommand(ctx.Guild.Id)) { - var guild = (SocketGuild) ctx.Guild; - var (enabled, channels) = _service.GetDelMsgOnCmdData(ctx.Guild.Id); - - var embed = _eb.Create() - .WithOkColor() - .WithTitle(GetText(strs.server_delmsgoncmd)) - .WithDescription(enabled ? "✅" : "❌"); - - var str = string.Join("\n", channels - .Select(x => - { - var ch = guild.GetChannel(x.ChannelId)?.ToString() - ?? x.ChannelId.ToString(); - var prefix = x.State ? "✅ " : "❌ "; - return prefix + ch; - })); - - if (string.IsNullOrWhiteSpace(str)) - str = "-"; - - embed.AddField(GetText(strs.channel_delmsgoncmd), str); - - await ctx.Channel.EmbedAsync(embed).ConfigureAwait(false); + _service.DeleteMessagesOnCommand.Add(ctx.Guild.Id); + await ReplyConfirmLocalizedAsync(strs.delmsg_on).ConfigureAwait(false); } - - public enum Server + else { - Server - } - - [NadekoCommand, Aliases] - [RequireContext(ContextType.Guild)] - [UserPerm(GuildPerm.Administrator)] - [BotPerm(GuildPerm.ManageMessages)] - [Priority(1)] - public async Task Delmsgoncmd(Server _ = Server.Server) - { - if (_service.ToggleDeleteMessageOnCommand(ctx.Guild.Id)) - { - _service.DeleteMessagesOnCommand.Add(ctx.Guild.Id); - await ReplyConfirmLocalizedAsync(strs.delmsg_on).ConfigureAwait(false); - } - else - { - _service.DeleteMessagesOnCommand.TryRemove(ctx.Guild.Id); - await ReplyConfirmLocalizedAsync(strs.delmsg_off).ConfigureAwait(false); - } - } - - public enum Channel - { - Channel, - Ch, - Chnl, - Chan - } - - public enum State - { - Enable, - Disable, - Inherit - } - - [NadekoCommand, Aliases] - [RequireContext(ContextType.Guild)] - [UserPerm(GuildPerm.Administrator)] - [BotPerm(GuildPerm.ManageMessages)] - [Priority(0)] - public Task Delmsgoncmd(Channel _, State s, ITextChannel ch) - => Delmsgoncmd(_, s, ch.Id); - - [NadekoCommand, Aliases] - [RequireContext(ContextType.Guild)] - [UserPerm(GuildPerm.Administrator)] - [BotPerm(GuildPerm.ManageMessages)] - [Priority(1)] - public async Task Delmsgoncmd(Channel _, State s, ulong? chId = null) - { - var actualChId = chId ?? ctx.Channel.Id; - await _service.SetDelMsgOnCmdState(ctx.Guild.Id, actualChId, s).ConfigureAwait(false); - - if (s == State.Disable) - { - await ReplyConfirmLocalizedAsync(strs.delmsg_channel_off).ConfigureAwait(false); - } - else if (s == State.Enable) - { - await ReplyConfirmLocalizedAsync(strs.delmsg_channel_on).ConfigureAwait(false); - } - else - { - await ReplyConfirmLocalizedAsync(strs.delmsg_channel_inherit).ConfigureAwait(false); - } - } - - [NadekoCommand, Aliases] - [RequireContext(ContextType.Guild)] - [UserPerm(GuildPerm.DeafenMembers)] - [BotPerm(GuildPerm.DeafenMembers)] - public async Task Deafen(params IGuildUser[] users) - { - await _service.DeafenUsers(true, users).ConfigureAwait(false); - await ReplyConfirmLocalizedAsync(strs.deafen).ConfigureAwait(false); - } - - [NadekoCommand, Aliases] - [RequireContext(ContextType.Guild)] - [UserPerm(GuildPerm.DeafenMembers)] - [BotPerm(GuildPerm.DeafenMembers)] - public async Task UnDeafen(params IGuildUser[] users) - { - await _service.DeafenUsers(false, users).ConfigureAwait(false); - await ReplyConfirmLocalizedAsync(strs.undeafen).ConfigureAwait(false); - } - - [NadekoCommand, Aliases] - [RequireContext(ContextType.Guild)] - [UserPerm(GuildPerm.ManageChannels)] - [BotPerm(GuildPerm.ManageChannels)] - public async Task DelVoiChanl([Leftover] IVoiceChannel voiceChannel) - { - await voiceChannel.DeleteAsync().ConfigureAwait(false); - await ReplyConfirmLocalizedAsync(strs.delvoich(Format.Bold(voiceChannel.Name))).ConfigureAwait(false); - } - - [NadekoCommand, Aliases] - [RequireContext(ContextType.Guild)] - [UserPerm(GuildPerm.ManageChannels)] - [BotPerm(GuildPerm.ManageChannels)] - public async Task CreatVoiChanl([Leftover] string channelName) - { - var ch = await ctx.Guild.CreateVoiceChannelAsync(channelName).ConfigureAwait(false); - await ReplyConfirmLocalizedAsync(strs.createvoich(Format.Bold(ch.Name))).ConfigureAwait(false); - } - - [NadekoCommand, Aliases] - [RequireContext(ContextType.Guild)] - [UserPerm(GuildPerm.ManageChannels)] - [BotPerm(GuildPerm.ManageChannels)] - public async Task DelTxtChanl([Leftover] ITextChannel toDelete) - { - await toDelete.DeleteAsync().ConfigureAwait(false); - await ReplyConfirmLocalizedAsync(strs.deltextchan(Format.Bold(toDelete.Name))).ConfigureAwait(false); - } - - [NadekoCommand, Aliases] - [RequireContext(ContextType.Guild)] - [UserPerm(GuildPerm.ManageChannels)] - [BotPerm(GuildPerm.ManageChannels)] - public async Task CreaTxtChanl([Leftover] string channelName) - { - var txtCh = await ctx.Guild.CreateTextChannelAsync(channelName).ConfigureAwait(false); - await ReplyConfirmLocalizedAsync(strs.createtextchan(Format.Bold(txtCh.Name))).ConfigureAwait(false); - } - - [NadekoCommand, Aliases] - [RequireContext(ContextType.Guild)] - [UserPerm(GuildPerm.ManageChannels)] - [BotPerm(GuildPerm.ManageChannels)] - public async Task SetTopic([Leftover] string topic = null) - { - var channel = (ITextChannel) ctx.Channel; - topic = topic ?? ""; - await channel.ModifyAsync(c => c.Topic = topic).ConfigureAwait(false); - await ReplyConfirmLocalizedAsync(strs.set_topic).ConfigureAwait(false); - } - - [NadekoCommand, Aliases] - [RequireContext(ContextType.Guild)] - [UserPerm(GuildPerm.ManageChannels)] - [BotPerm(GuildPerm.ManageChannels)] - public async Task SetChanlName([Leftover] string name) - { - var channel = (ITextChannel) ctx.Channel; - await channel.ModifyAsync(c => c.Name = name).ConfigureAwait(false); - await ReplyConfirmLocalizedAsync(strs.set_channel_name).ConfigureAwait(false); - } - - [NadekoCommand, Aliases] - [RequireContext(ContextType.Guild)] - [UserPerm(GuildPerm.ManageChannels)] - [BotPerm(GuildPerm.ManageChannels)] - public async Task NsfwToggle() - { - var channel = (ITextChannel) ctx.Channel; - var isEnabled = channel.IsNsfw; - - await channel.ModifyAsync(c => c.IsNsfw = !isEnabled).ConfigureAwait(false); - - if (isEnabled) - await ReplyConfirmLocalizedAsync(strs.nsfw_set_false).ConfigureAwait(false); - else - await ReplyConfirmLocalizedAsync(strs.nsfw_set_true).ConfigureAwait(false); - } - - [NadekoCommand, Aliases] - [RequireContext(ContextType.Guild)] - [UserPerm(ChannelPerm.ManageMessages)] - [Priority(0)] - public Task Edit(ulong messageId, [Leftover] string text) - => Edit((ITextChannel) ctx.Channel, messageId, text); - - [NadekoCommand, Aliases] - [RequireContext(ContextType.Guild)] - [Priority(1)] - public async Task Edit(ITextChannel channel, ulong messageId, [Leftover] string text) - { - var userPerms = ((SocketGuildUser) ctx.User).GetPermissions(channel); - var botPerms = ((SocketGuild) ctx.Guild).CurrentUser.GetPermissions(channel); - if (!userPerms.Has(ChannelPermission.ManageMessages)) - { - await ReplyErrorLocalizedAsync(strs.insuf_perms_u).ConfigureAwait(false); - return; - } - - if (!botPerms.Has(ChannelPermission.ViewChannel)) - { - await ReplyErrorLocalizedAsync(strs.insuf_perms_i).ConfigureAwait(false); - return; - } - - await _service.EditMessage(ctx, channel, messageId, text); - } - - [NadekoCommand, Aliases] - [RequireContext(ContextType.Guild)] - [UserPerm(ChannelPerm.ManageMessages)] - [BotPerm(ChannelPerm.ManageMessages)] - public Task Delete(ulong messageId, StoopidTime time = null) - => Delete((ITextChannel) ctx.Channel, messageId, time); - - [NadekoCommand, Aliases] - [RequireContext(ContextType.Guild)] - public async Task Delete(ITextChannel channel, ulong messageId, StoopidTime time = null) - { - await InternalMessageAction(channel, messageId, time, (msg) => msg.DeleteAsync()); - } - - private async Task InternalMessageAction(ITextChannel channel, ulong messageId, StoopidTime time, - Func func) - { - var userPerms = ((SocketGuildUser) ctx.User).GetPermissions(channel); - var botPerms = ((SocketGuild) ctx.Guild).CurrentUser.GetPermissions(channel); - if (!userPerms.Has(ChannelPermission.ManageMessages)) - { - await ReplyErrorLocalizedAsync(strs.insuf_perms_u).ConfigureAwait(false); - return; - } - - if (!botPerms.Has(ChannelPermission.ManageMessages)) - { - await ReplyErrorLocalizedAsync(strs.insuf_perms_i).ConfigureAwait(false); - return; - } - - - var msg = await channel.GetMessageAsync(messageId).ConfigureAwait(false); - if (msg is null) - { - await ReplyErrorLocalizedAsync(strs.msg_not_found).ConfigureAwait(false); - return; - } - - if (time is null) - { - await msg.DeleteAsync().ConfigureAwait(false); - } - else if (time.Time <= TimeSpan.FromDays(7)) - { - var _ = Task.Run(async () => - { - await Task.Delay(time.Time).ConfigureAwait(false); - await msg.DeleteAsync().ConfigureAwait(false); - }); - } - else - { - await ReplyErrorLocalizedAsync(strs.time_too_long).ConfigureAwait(false); - return; - } - - await ctx.OkAsync(); + _service.DeleteMessagesOnCommand.TryRemove(ctx.Guild.Id); + await ReplyConfirmLocalizedAsync(strs.delmsg_off).ConfigureAwait(false); } } + + public enum Channel + { + Channel, + Ch, + Chnl, + Chan + } + + public enum State + { + Enable, + Disable, + Inherit + } + + [NadekoCommand, Aliases] + [RequireContext(ContextType.Guild)] + [UserPerm(GuildPerm.Administrator)] + [BotPerm(GuildPerm.ManageMessages)] + [Priority(0)] + public Task Delmsgoncmd(Channel _, State s, ITextChannel ch) + => Delmsgoncmd(_, s, ch.Id); + + [NadekoCommand, Aliases] + [RequireContext(ContextType.Guild)] + [UserPerm(GuildPerm.Administrator)] + [BotPerm(GuildPerm.ManageMessages)] + [Priority(1)] + public async Task Delmsgoncmd(Channel _, State s, ulong? chId = null) + { + var actualChId = chId ?? ctx.Channel.Id; + await _service.SetDelMsgOnCmdState(ctx.Guild.Id, actualChId, s).ConfigureAwait(false); + + if (s == State.Disable) + { + await ReplyConfirmLocalizedAsync(strs.delmsg_channel_off).ConfigureAwait(false); + } + else if (s == State.Enable) + { + await ReplyConfirmLocalizedAsync(strs.delmsg_channel_on).ConfigureAwait(false); + } + else + { + await ReplyConfirmLocalizedAsync(strs.delmsg_channel_inherit).ConfigureAwait(false); + } + } + + [NadekoCommand, Aliases] + [RequireContext(ContextType.Guild)] + [UserPerm(GuildPerm.DeafenMembers)] + [BotPerm(GuildPerm.DeafenMembers)] + public async Task Deafen(params IGuildUser[] users) + { + await _service.DeafenUsers(true, users).ConfigureAwait(false); + await ReplyConfirmLocalizedAsync(strs.deafen).ConfigureAwait(false); + } + + [NadekoCommand, Aliases] + [RequireContext(ContextType.Guild)] + [UserPerm(GuildPerm.DeafenMembers)] + [BotPerm(GuildPerm.DeafenMembers)] + public async Task UnDeafen(params IGuildUser[] users) + { + await _service.DeafenUsers(false, users).ConfigureAwait(false); + await ReplyConfirmLocalizedAsync(strs.undeafen).ConfigureAwait(false); + } + + [NadekoCommand, Aliases] + [RequireContext(ContextType.Guild)] + [UserPerm(GuildPerm.ManageChannels)] + [BotPerm(GuildPerm.ManageChannels)] + public async Task DelVoiChanl([Leftover] IVoiceChannel voiceChannel) + { + await voiceChannel.DeleteAsync().ConfigureAwait(false); + await ReplyConfirmLocalizedAsync(strs.delvoich(Format.Bold(voiceChannel.Name))).ConfigureAwait(false); + } + + [NadekoCommand, Aliases] + [RequireContext(ContextType.Guild)] + [UserPerm(GuildPerm.ManageChannels)] + [BotPerm(GuildPerm.ManageChannels)] + public async Task CreatVoiChanl([Leftover] string channelName) + { + var ch = await ctx.Guild.CreateVoiceChannelAsync(channelName).ConfigureAwait(false); + await ReplyConfirmLocalizedAsync(strs.createvoich(Format.Bold(ch.Name))).ConfigureAwait(false); + } + + [NadekoCommand, Aliases] + [RequireContext(ContextType.Guild)] + [UserPerm(GuildPerm.ManageChannels)] + [BotPerm(GuildPerm.ManageChannels)] + public async Task DelTxtChanl([Leftover] ITextChannel toDelete) + { + await toDelete.DeleteAsync().ConfigureAwait(false); + await ReplyConfirmLocalizedAsync(strs.deltextchan(Format.Bold(toDelete.Name))).ConfigureAwait(false); + } + + [NadekoCommand, Aliases] + [RequireContext(ContextType.Guild)] + [UserPerm(GuildPerm.ManageChannels)] + [BotPerm(GuildPerm.ManageChannels)] + public async Task CreaTxtChanl([Leftover] string channelName) + { + var txtCh = await ctx.Guild.CreateTextChannelAsync(channelName).ConfigureAwait(false); + await ReplyConfirmLocalizedAsync(strs.createtextchan(Format.Bold(txtCh.Name))).ConfigureAwait(false); + } + + [NadekoCommand, Aliases] + [RequireContext(ContextType.Guild)] + [UserPerm(GuildPerm.ManageChannels)] + [BotPerm(GuildPerm.ManageChannels)] + public async Task SetTopic([Leftover] string topic = null) + { + var channel = (ITextChannel) ctx.Channel; + topic = topic ?? ""; + await channel.ModifyAsync(c => c.Topic = topic).ConfigureAwait(false); + await ReplyConfirmLocalizedAsync(strs.set_topic).ConfigureAwait(false); + } + + [NadekoCommand, Aliases] + [RequireContext(ContextType.Guild)] + [UserPerm(GuildPerm.ManageChannels)] + [BotPerm(GuildPerm.ManageChannels)] + public async Task SetChanlName([Leftover] string name) + { + var channel = (ITextChannel) ctx.Channel; + await channel.ModifyAsync(c => c.Name = name).ConfigureAwait(false); + await ReplyConfirmLocalizedAsync(strs.set_channel_name).ConfigureAwait(false); + } + + [NadekoCommand, Aliases] + [RequireContext(ContextType.Guild)] + [UserPerm(GuildPerm.ManageChannels)] + [BotPerm(GuildPerm.ManageChannels)] + public async Task NsfwToggle() + { + var channel = (ITextChannel) ctx.Channel; + var isEnabled = channel.IsNsfw; + + await channel.ModifyAsync(c => c.IsNsfw = !isEnabled).ConfigureAwait(false); + + if (isEnabled) + await ReplyConfirmLocalizedAsync(strs.nsfw_set_false).ConfigureAwait(false); + else + await ReplyConfirmLocalizedAsync(strs.nsfw_set_true).ConfigureAwait(false); + } + + [NadekoCommand, Aliases] + [RequireContext(ContextType.Guild)] + [UserPerm(ChannelPerm.ManageMessages)] + [Priority(0)] + public Task Edit(ulong messageId, [Leftover] string text) + => Edit((ITextChannel) ctx.Channel, messageId, text); + + [NadekoCommand, Aliases] + [RequireContext(ContextType.Guild)] + [Priority(1)] + public async Task Edit(ITextChannel channel, ulong messageId, [Leftover] string text) + { + var userPerms = ((SocketGuildUser) ctx.User).GetPermissions(channel); + var botPerms = ((SocketGuild) ctx.Guild).CurrentUser.GetPermissions(channel); + if (!userPerms.Has(ChannelPermission.ManageMessages)) + { + await ReplyErrorLocalizedAsync(strs.insuf_perms_u).ConfigureAwait(false); + return; + } + + if (!botPerms.Has(ChannelPermission.ViewChannel)) + { + await ReplyErrorLocalizedAsync(strs.insuf_perms_i).ConfigureAwait(false); + return; + } + + await _service.EditMessage(ctx, channel, messageId, text); + } + + [NadekoCommand, Aliases] + [RequireContext(ContextType.Guild)] + [UserPerm(ChannelPerm.ManageMessages)] + [BotPerm(ChannelPerm.ManageMessages)] + public Task Delete(ulong messageId, StoopidTime time = null) + => Delete((ITextChannel) ctx.Channel, messageId, time); + + [NadekoCommand, Aliases] + [RequireContext(ContextType.Guild)] + public async Task Delete(ITextChannel channel, ulong messageId, StoopidTime time = null) + { + await InternalMessageAction(channel, messageId, time, (msg) => msg.DeleteAsync()); + } + + private async Task InternalMessageAction(ITextChannel channel, ulong messageId, StoopidTime time, + Func func) + { + var userPerms = ((SocketGuildUser) ctx.User).GetPermissions(channel); + var botPerms = ((SocketGuild) ctx.Guild).CurrentUser.GetPermissions(channel); + if (!userPerms.Has(ChannelPermission.ManageMessages)) + { + await ReplyErrorLocalizedAsync(strs.insuf_perms_u).ConfigureAwait(false); + return; + } + + if (!botPerms.Has(ChannelPermission.ManageMessages)) + { + await ReplyErrorLocalizedAsync(strs.insuf_perms_i).ConfigureAwait(false); + return; + } + + + var msg = await channel.GetMessageAsync(messageId).ConfigureAwait(false); + if (msg is null) + { + await ReplyErrorLocalizedAsync(strs.msg_not_found).ConfigureAwait(false); + return; + } + + if (time is null) + { + await msg.DeleteAsync().ConfigureAwait(false); + } + else if (time.Time <= TimeSpan.FromDays(7)) + { + var _ = Task.Run(async () => + { + await Task.Delay(time.Time).ConfigureAwait(false); + await msg.DeleteAsync().ConfigureAwait(false); + }); + } + else + { + await ReplyErrorLocalizedAsync(strs.time_too_long).ConfigureAwait(false); + return; + } + + await ctx.OkAsync(); + } } \ No newline at end of file diff --git a/src/NadekoBot/Modules/Administration/AutoAssignRoleCommands.cs b/src/NadekoBot/Modules/Administration/AutoAssignRoleCommands.cs index 3b22690f9..4652aa315 100644 --- a/src/NadekoBot/Modules/Administration/AutoAssignRoleCommands.cs +++ b/src/NadekoBot/Modules/Administration/AutoAssignRoleCommands.cs @@ -1,74 +1,72 @@ using Discord; using Discord.Commands; using NadekoBot.Extensions; -using System.Linq; using System.Threading.Tasks; using NadekoBot.Common.Attributes; using NadekoBot.Modules.Administration.Services; -namespace NadekoBot.Modules.Administration +namespace NadekoBot.Modules.Administration; + +public partial class Administration { - public partial class Administration + [Group] + public class AutoAssignRoleCommands : NadekoSubmodule { - [Group] - public class AutoAssignRoleCommands : NadekoSubmodule + [NadekoCommand, Aliases] + [RequireContext(ContextType.Guild)] + [UserPerm(GuildPerm.ManageRoles)] + [BotPerm(GuildPerm.ManageRoles)] + public async Task AutoAssignRole([Leftover] IRole role) { - [NadekoCommand, Aliases] - [RequireContext(ContextType.Guild)] - [UserPerm(GuildPerm.ManageRoles)] - [BotPerm(GuildPerm.ManageRoles)] - public async Task AutoAssignRole([Leftover] IRole role) + var guser = (IGuildUser) ctx.User; + if (role.Id == ctx.Guild.EveryoneRole.Id) + return; + + // the user can't aar the role which is higher or equal to his highest role + if (ctx.User.Id != guser.Guild.OwnerId && guser.GetRoles().Max(x => x.Position) <= role.Position) { - var guser = (IGuildUser) ctx.User; - if (role.Id == ctx.Guild.EveryoneRole.Id) - return; - - // the user can't aar the role which is higher or equal to his highest role - if (ctx.User.Id != guser.Guild.OwnerId && guser.GetRoles().Max(x => x.Position) <= role.Position) - { - await ReplyErrorLocalizedAsync(strs.hierarchy); - return; - } - - var roles = await _service.ToggleAarAsync(ctx.Guild.Id, role.Id); - if (roles.Count == 0) - { - await ReplyConfirmLocalizedAsync(strs.aar_disabled); - } - else if (roles.Contains(role.Id)) - { - await AutoAssignRole(); - } - else - { - await ReplyConfirmLocalizedAsync(strs.aar_role_removed(Format.Bold(role.ToString()))); - } + await ReplyErrorLocalizedAsync(strs.hierarchy); + return; } + + var roles = await _service.ToggleAarAsync(ctx.Guild.Id, role.Id); + if (roles.Count == 0) + { + await ReplyConfirmLocalizedAsync(strs.aar_disabled); + } + else if (roles.Contains(role.Id)) + { + await AutoAssignRole(); + } + else + { + await ReplyConfirmLocalizedAsync(strs.aar_role_removed(Format.Bold(role.ToString()))); + } + } - [NadekoCommand, Aliases] - [RequireContext(ContextType.Guild)] - [UserPerm(GuildPerm.ManageRoles)] - [BotPerm(GuildPerm.ManageRoles)] - public async Task AutoAssignRole() + [NadekoCommand, Aliases] + [RequireContext(ContextType.Guild)] + [UserPerm(GuildPerm.ManageRoles)] + [BotPerm(GuildPerm.ManageRoles)] + public async Task AutoAssignRole() + { + if (!_service.TryGetRoles(ctx.Guild.Id, out var roles)) { - if (!_service.TryGetRoles(ctx.Guild.Id, out var roles)) - { - await ReplyConfirmLocalizedAsync(strs.aar_none); - return; - } - - var existing = roles.Select(rid => ctx.Guild.GetRole(rid)).Where(r => r is not null) - .ToList(); - - if (existing.Count != roles.Count) - { - await _service.SetAarRolesAsync(ctx.Guild.Id, existing.Select(x => x.Id)); - } - - await ReplyConfirmLocalizedAsync(strs.aar_roles( - '\n' + existing.Select(x => Format.Bold(x.ToString())) - .JoinWith(",\n"))); + await ReplyConfirmLocalizedAsync(strs.aar_none); + return; } + + var existing = roles.Select(rid => ctx.Guild.GetRole(rid)).Where(r => r is not null) + .ToList(); + + if (existing.Count != roles.Count) + { + await _service.SetAarRolesAsync(ctx.Guild.Id, existing.Select(x => x.Id)); + } + + await ReplyConfirmLocalizedAsync(strs.aar_roles( + '\n' + existing.Select(x => Format.Bold(x.ToString())) + .JoinWith(",\n"))); } } } \ No newline at end of file diff --git a/src/NadekoBot/Modules/Administration/Common/LogType.cs b/src/NadekoBot/Modules/Administration/Common/LogType.cs index 405365cc6..459d38d8d 100644 --- a/src/NadekoBot/Modules/Administration/Common/LogType.cs +++ b/src/NadekoBot/Modules/Administration/Common/LogType.cs @@ -1,21 +1,20 @@ -namespace NadekoBot.Modules.Administration +namespace NadekoBot.Modules.Administration; + +public enum LogType { - public enum LogType - { - Other, - MessageUpdated, - MessageDeleted, - UserJoined, - UserLeft, - UserBanned, - UserUnbanned, - UserUpdated, - ChannelCreated, - ChannelDestroyed, - ChannelUpdated, - UserPresence, - VoicePresence, - VoicePresenceTTS, - UserMuted - } + Other, + MessageUpdated, + MessageDeleted, + UserJoined, + UserLeft, + UserBanned, + UserUnbanned, + UserUpdated, + ChannelCreated, + ChannelDestroyed, + ChannelUpdated, + UserPresence, + VoicePresence, + VoicePresenceTTS, + UserMuted } \ No newline at end of file diff --git a/src/NadekoBot/Modules/Administration/Common/ProtectionStats.cs b/src/NadekoBot/Modules/Administration/Common/ProtectionStats.cs index 7cb81a18d..16bf9dae8 100644 --- a/src/NadekoBot/Modules/Administration/Common/ProtectionStats.cs +++ b/src/NadekoBot/Modules/Administration/Common/ProtectionStats.cs @@ -1,50 +1,48 @@ -using System; -using System.Collections.Concurrent; +using System.Collections.Concurrent; using System.Threading; using Discord; using NadekoBot.Common.Collections; using NadekoBot.Services.Database.Models; -namespace NadekoBot.Modules.Administration.Common +namespace NadekoBot.Modules.Administration.Common; + +public enum ProtectionType { - public enum ProtectionType - { - Raiding, - Spamming, - Alting - } - - public class AntiRaidStats - { - public AntiRaidSetting AntiRaidSettings { get; set; } - public int UsersCount { get; set; } - public ConcurrentHashSet RaidUsers { get; set; } = new ConcurrentHashSet(); - } - - public class AntiSpamStats - { - public AntiSpamSetting AntiSpamSettings { get; set; } - public ConcurrentDictionary UserStats { get; set; } - = new ConcurrentDictionary(); - } - - public class AntiAltStats - { - private readonly AntiAltSetting _setting; - public PunishmentAction Action => _setting.Action; - public int ActionDurationMinutes => _setting.ActionDurationMinutes; - public ulong? RoleId => _setting.RoleId; - public TimeSpan MinAge => _setting.MinAge; - - private int _counter = 0; - public int Counter => _counter; - - public AntiAltStats(AntiAltSetting setting) - { - _setting = setting; - } - - public void Increment() => Interlocked.Increment(ref _counter); - - } + Raiding, + Spamming, + Alting } + +public class AntiRaidStats +{ + public AntiRaidSetting AntiRaidSettings { get; set; } + public int UsersCount { get; set; } + public ConcurrentHashSet RaidUsers { get; set; } = new ConcurrentHashSet(); +} + +public class AntiSpamStats +{ + public AntiSpamSetting AntiSpamSettings { get; set; } + public ConcurrentDictionary UserStats { get; set; } + = new ConcurrentDictionary(); +} + +public class AntiAltStats +{ + private readonly AntiAltSetting _setting; + public PunishmentAction Action => _setting.Action; + public int ActionDurationMinutes => _setting.ActionDurationMinutes; + public ulong? RoleId => _setting.RoleId; + public TimeSpan MinAge => _setting.MinAge; + + private int _counter = 0; + public int Counter => _counter; + + public AntiAltStats(AntiAltSetting setting) + { + _setting = setting; + } + + public void Increment() => Interlocked.Increment(ref _counter); + +} \ No newline at end of file diff --git a/src/NadekoBot/Modules/Administration/Common/PunishQueueItem.cs b/src/NadekoBot/Modules/Administration/Common/PunishQueueItem.cs index 79ae4a054..370b202c4 100644 --- a/src/NadekoBot/Modules/Administration/Common/PunishQueueItem.cs +++ b/src/NadekoBot/Modules/Administration/Common/PunishQueueItem.cs @@ -1,14 +1,13 @@ using Discord; using NadekoBot.Services.Database.Models; -namespace NadekoBot.Modules.Administration.Common +namespace NadekoBot.Modules.Administration.Common; + +public class PunishQueueItem { - public class PunishQueueItem - { - public PunishmentAction Action { get; set; } - public ProtectionType Type { get; set; } - public int MuteTime { get; set; } - public ulong? RoleId { get; set; } - public IGuildUser User { get; set; } - } + public PunishmentAction Action { get; set; } + public ProtectionType Type { get; set; } + public int MuteTime { get; set; } + public ulong? RoleId { get; set; } + public IGuildUser User { get; set; } } \ No newline at end of file diff --git a/src/NadekoBot/Modules/Administration/Common/UserSpamStats.cs b/src/NadekoBot/Modules/Administration/Common/UserSpamStats.cs index 90f0c25e9..c37f21d2e 100644 --- a/src/NadekoBot/Modules/Administration/Common/UserSpamStats.cs +++ b/src/NadekoBot/Modules/Administration/Common/UserSpamStats.cs @@ -1,50 +1,47 @@ -using System; -using System.Collections.Concurrent; -using System.Linq; +using System.Collections.Concurrent; using System.Threading; using Discord; -namespace NadekoBot.Modules.Administration.Common +namespace NadekoBot.Modules.Administration.Common; + +public sealed class UserSpamStats : IDisposable { - public sealed class UserSpamStats : IDisposable + public int Count => timers.Count; + public string LastMessage { get; set; } + + private ConcurrentQueue timers { get; } + + public UserSpamStats(IUserMessage msg) { - public int Count => timers.Count; - public string LastMessage { get; set; } + LastMessage = msg.Content.ToUpperInvariant(); + timers = new ConcurrentQueue(); - private ConcurrentQueue timers { get; } + ApplyNextMessage(msg); + } - public UserSpamStats(IUserMessage msg) + private readonly object applyLock = new object(); + public void ApplyNextMessage(IUserMessage message) + { + lock (applyLock) { - LastMessage = msg.Content.ToUpperInvariant(); - timers = new ConcurrentQueue(); - - ApplyNextMessage(msg); - } - - private readonly object applyLock = new object(); - public void ApplyNextMessage(IUserMessage message) - { - lock (applyLock) + var upperMsg = message.Content.ToUpperInvariant(); + if (upperMsg != LastMessage || (string.IsNullOrWhiteSpace(upperMsg) && message.Attachments.Any())) { - var upperMsg = message.Content.ToUpperInvariant(); - if (upperMsg != LastMessage || (string.IsNullOrWhiteSpace(upperMsg) && message.Attachments.Any())) - { - LastMessage = upperMsg; - while (timers.TryDequeue(out var old)) - old.Change(Timeout.Infinite, Timeout.Infinite); - } - var t = new Timer((_) => { - if (timers.TryDequeue(out var old)) - old.Change(Timeout.Infinite, Timeout.Infinite); - }, null, TimeSpan.FromMinutes(30), TimeSpan.FromMinutes(30)); - timers.Enqueue(t); + LastMessage = upperMsg; + while (timers.TryDequeue(out var old)) + old.Change(Timeout.Infinite, Timeout.Infinite); } - } - - public void Dispose() - { - while (timers.TryDequeue(out var old)) - old.Change(Timeout.Infinite, Timeout.Infinite); + var t = new Timer((_) => { + if (timers.TryDequeue(out var old)) + old.Change(Timeout.Infinite, Timeout.Infinite); + }, null, TimeSpan.FromMinutes(30), TimeSpan.FromMinutes(30)); + timers.Enqueue(t); } } -} + + public void Dispose() + { + while (timers.TryDequeue(out var old)) + old.Change(Timeout.Infinite, Timeout.Infinite); + } +} \ No newline at end of file diff --git a/src/NadekoBot/Modules/Administration/DangerousCommands.cs b/src/NadekoBot/Modules/Administration/DangerousCommands.cs index f19f012c0..e01b01141 100644 --- a/src/NadekoBot/Modules/Administration/DangerousCommands.cs +++ b/src/NadekoBot/Modules/Administration/DangerousCommands.cs @@ -1,11 +1,9 @@ using Discord.Commands; using NadekoBot.Common.Attributes; using NadekoBot.Extensions; -using System; using System.Threading.Tasks; using Discord; using NadekoBot.Modules.Administration.Services; -using System.Linq; #if !GLOBAL_NADEKO namespace NadekoBot.Modules.Administration diff --git a/src/NadekoBot/Modules/Administration/DiscordPermOverrideCommands.cs b/src/NadekoBot/Modules/Administration/DiscordPermOverrideCommands.cs index 71622c74c..42c444356 100644 --- a/src/NadekoBot/Modules/Administration/DiscordPermOverrideCommands.cs +++ b/src/NadekoBot/Modules/Administration/DiscordPermOverrideCommands.cs @@ -1,5 +1,4 @@ -using System.Linq; -using System.Threading.Tasks; +using System.Threading.Tasks; using Discord; using Discord.Commands; using NadekoBot.Common.Attributes; @@ -7,81 +6,80 @@ using NadekoBot.Common.TypeReaders; using NadekoBot.Extensions; using NadekoBot.Modules.Administration.Services; -namespace NadekoBot.Modules.Administration +namespace NadekoBot.Modules.Administration; + +public partial class Administration { - public partial class Administration + [Group] + public class DiscordPermOverrideCommands : NadekoSubmodule { - [Group] - public class DiscordPermOverrideCommands : NadekoSubmodule + // override stats, it should require that the user has managessages guild permission + // .po 'stats' add user guild managemessages + [NadekoCommand, Aliases] + [RequireContext(ContextType.Guild)] + [UserPerm(GuildPerm.Administrator)] + public async Task DiscordPermOverride(CommandOrCrInfo cmd, params GuildPerm[] perms) { - // override stats, it should require that the user has managessages guild permission - // .po 'stats' add user guild managemessages - [NadekoCommand, Aliases] - [RequireContext(ContextType.Guild)] - [UserPerm(GuildPerm.Administrator)] - public async Task DiscordPermOverride(CommandOrCrInfo cmd, params GuildPerm[] perms) + if (perms is null || perms.Length == 0) { - if (perms is null || perms.Length == 0) - { - await _service.RemoveOverride(ctx.Guild.Id, cmd.Name); - await ReplyConfirmLocalizedAsync(strs.perm_override_reset); - return; - } - - var aggregatePerms = perms.Aggregate((acc, seed) => seed | acc); - await _service.AddOverride(ctx.Guild.Id, cmd.Name, aggregatePerms); - - await ReplyConfirmLocalizedAsync(strs.perm_override( - Format.Bold(aggregatePerms.ToString()), - Format.Code(cmd.Name))); + await _service.RemoveOverride(ctx.Guild.Id, cmd.Name); + await ReplyConfirmLocalizedAsync(strs.perm_override_reset); + return; } + + var aggregatePerms = perms.Aggregate((acc, seed) => seed | acc); + await _service.AddOverride(ctx.Guild.Id, cmd.Name, aggregatePerms); + + await ReplyConfirmLocalizedAsync(strs.perm_override( + Format.Bold(aggregatePerms.ToString()), + Format.Code(cmd.Name))); + } - [NadekoCommand, Aliases] - [RequireContext(ContextType.Guild)] - [UserPerm(GuildPerm.Administrator)] - public async Task DiscordPermOverrideReset() - { - var result = await PromptUserConfirmAsync(_eb.Create() - .WithOkColor() - .WithDescription(GetText(strs.perm_override_all_confirm))); + [NadekoCommand, Aliases] + [RequireContext(ContextType.Guild)] + [UserPerm(GuildPerm.Administrator)] + public async Task DiscordPermOverrideReset() + { + var result = await PromptUserConfirmAsync(_eb.Create() + .WithOkColor() + .WithDescription(GetText(strs.perm_override_all_confirm))); - if (!result) - return; - await _service.ClearAllOverrides(ctx.Guild.Id); + if (!result) + return; + await _service.ClearAllOverrides(ctx.Guild.Id); - await ReplyConfirmLocalizedAsync(strs.perm_override_all); - } + await ReplyConfirmLocalizedAsync(strs.perm_override_all); + } - [NadekoCommand, Aliases] - [RequireContext(ContextType.Guild)] - [UserPerm(GuildPerm.Administrator)] - public async Task DiscordPermOverrideList(int page = 1) - { - if (--page < 0) - return; + [NadekoCommand, Aliases] + [RequireContext(ContextType.Guild)] + [UserPerm(GuildPerm.Administrator)] + public async Task DiscordPermOverrideList(int page = 1) + { + if (--page < 0) + return; - var overrides = await _service.GetAllOverrides(ctx.Guild.Id); + var overrides = await _service.GetAllOverrides(ctx.Guild.Id); - await ctx.SendPaginatedConfirmAsync(page, curPage => - { - var eb = _eb.Create() - .WithTitle(GetText(strs.perm_overrides)) - .WithOkColor(); + await ctx.SendPaginatedConfirmAsync(page, curPage => + { + var eb = _eb.Create() + .WithTitle(GetText(strs.perm_overrides)) + .WithOkColor(); - var thisPageOverrides = overrides - .Skip(9 * curPage) - .Take(9) - .ToList(); + var thisPageOverrides = overrides + .Skip(9 * curPage) + .Take(9) + .ToList(); - if (thisPageOverrides.Count == 0) - eb.WithDescription(GetText(strs.perm_override_page_none)); - else - eb.WithDescription(string.Join("\n", - thisPageOverrides.Select(ov => $"{ov.Command} => {ov.Perm.ToString()}"))); + if (thisPageOverrides.Count == 0) + eb.WithDescription(GetText(strs.perm_override_page_none)); + else + eb.WithDescription(string.Join("\n", + thisPageOverrides.Select(ov => $"{ov.Command} => {ov.Perm.ToString()}"))); - return eb; - }, overrides.Count, 9, true); - } + return eb; + }, overrides.Count, 9, true); } } } \ No newline at end of file diff --git a/src/NadekoBot/Modules/Administration/GameChannelCommands.cs b/src/NadekoBot/Modules/Administration/GameChannelCommands.cs index e1a8dc2dd..fbe28def3 100644 --- a/src/NadekoBot/Modules/Administration/GameChannelCommands.cs +++ b/src/NadekoBot/Modules/Administration/GameChannelCommands.cs @@ -4,38 +4,37 @@ using System.Threading.Tasks; using NadekoBot.Common.Attributes; using NadekoBot.Modules.Administration.Services; -namespace NadekoBot.Modules.Administration +namespace NadekoBot.Modules.Administration; + +public partial class Administration { - public partial class Administration + [Group] + public class GameChannelCommands : NadekoSubmodule { - [Group] - public class GameChannelCommands : NadekoSubmodule + [NadekoCommand, Aliases] + [RequireContext(ContextType.Guild)] + [UserPerm(GuildPerm.Administrator)] + [BotPerm(GuildPerm.MoveMembers)] + public async Task GameVoiceChannel() { - [NadekoCommand, Aliases] - [RequireContext(ContextType.Guild)] - [UserPerm(GuildPerm.Administrator)] - [BotPerm(GuildPerm.MoveMembers)] - public async Task GameVoiceChannel() + var vch = ((IGuildUser)ctx.User).VoiceChannel; + + if (vch is null) { - var vch = ((IGuildUser)ctx.User).VoiceChannel; + await ReplyErrorLocalizedAsync(strs.not_in_voice).ConfigureAwait(false); + return; + } + var id = _service.ToggleGameVoiceChannel(ctx.Guild.Id, vch.Id); - if (vch is null) - { - await ReplyErrorLocalizedAsync(strs.not_in_voice).ConfigureAwait(false); - return; - } - var id = _service.ToggleGameVoiceChannel(ctx.Guild.Id, vch.Id); - - if (id is null) - { - await ReplyConfirmLocalizedAsync(strs.gvc_disabled).ConfigureAwait(false); - } - else - { - _service.GameVoiceChannels.Add(vch.Id); - await ReplyConfirmLocalizedAsync(strs.gvc_enabled(Format.Bold(vch.Name))).ConfigureAwait(false); - } + if (id is null) + { + await ReplyConfirmLocalizedAsync(strs.gvc_disabled).ConfigureAwait(false); + } + else + { + _service.GameVoiceChannels.Add(vch.Id); + await ReplyConfirmLocalizedAsync(strs.gvc_enabled(Format.Bold(vch.Name))).ConfigureAwait(false); } } } -} +} \ No newline at end of file diff --git a/src/NadekoBot/Modules/Administration/LocalizationCommands.cs b/src/NadekoBot/Modules/Administration/LocalizationCommands.cs index 31b37c32c..8b1d0b7f7 100644 --- a/src/NadekoBot/Modules/Administration/LocalizationCommands.cs +++ b/src/NadekoBot/Modules/Administration/LocalizationCommands.cs @@ -1,132 +1,128 @@ using Discord; using Discord.Commands; using NadekoBot.Extensions; -using System; -using System.Collections.Generic; using System.Globalization; -using System.Linq; using System.Threading.Tasks; using NadekoBot.Common.Attributes; -namespace NadekoBot.Modules.Administration +namespace NadekoBot.Modules.Administration; + +public partial class Administration { - public partial class Administration + [Group] + public class LocalizationCommands : NadekoSubmodule { - [Group] - public class LocalizationCommands : NadekoSubmodule + private static readonly IReadOnlyDictionary supportedLocales = + new Dictionary() + { + {"ar", "العربية"}, + {"zh-TW", "繁體中文, 台灣"}, + {"zh-CN", "简体中文, 中华人民共和国"}, + {"nl-NL", "Nederlands, Nederland"}, + {"en-US", "English, United States"}, + {"fr-FR", "Français, France"}, + {"cs-CZ", "Čeština, Česká republika"}, + {"da-DK", "Dansk, Danmark"}, + {"de-DE", "Deutsch, Deutschland"}, + {"he-IL", "עברית, ישראל"}, + {"hu-HU", "Magyar, Magyarország"}, + {"id-ID", "Bahasa Indonesia, Indonesia"}, + {"it-IT", "Italiano, Italia"}, + {"ja-JP", "日本語, 日本"}, + {"ko-KR", "한국어, 대한민국"}, + {"nb-NO", "Norsk, Norge"}, + {"pl-PL", "Polski, Polska"}, + {"pt-BR", "Português Brasileiro, Brasil"}, + {"ro-RO", "Română, România"}, + {"ru-RU", "Русский, Россия"}, + {"sr-Cyrl-RS", "Српски, Србија"}, + {"es-ES", "Español, España"}, + {"sv-SE", "Svenska, Sverige"}, + {"tr-TR", "Türkçe, Türkiye"}, + {"ts-TS", "Tsundere, You Baka"}, + {"uk-UA", "Українська, Україна"} + }; + + [NadekoCommand, Aliases] + [RequireContext(ContextType.Guild)] + [Priority(0)] + public async Task LanguageSet() { - private static readonly IReadOnlyDictionary supportedLocales = - new Dictionary() - { - {"ar", "العربية"}, - {"zh-TW", "繁體中文, 台灣"}, - {"zh-CN", "简体中文, 中华人民共和国"}, - {"nl-NL", "Nederlands, Nederland"}, - {"en-US", "English, United States"}, - {"fr-FR", "Français, France"}, - {"cs-CZ", "Čeština, Česká republika"}, - {"da-DK", "Dansk, Danmark"}, - {"de-DE", "Deutsch, Deutschland"}, - {"he-IL", "עברית, ישראל"}, - {"hu-HU", "Magyar, Magyarország"}, - {"id-ID", "Bahasa Indonesia, Indonesia"}, - {"it-IT", "Italiano, Italia"}, - {"ja-JP", "日本語, 日本"}, - {"ko-KR", "한국어, 대한민국"}, - {"nb-NO", "Norsk, Norge"}, - {"pl-PL", "Polski, Polska"}, - {"pt-BR", "Português Brasileiro, Brasil"}, - {"ro-RO", "Română, România"}, - {"ru-RU", "Русский, Россия"}, - {"sr-Cyrl-RS", "Српски, Србија"}, - {"es-ES", "Español, España"}, - {"sv-SE", "Svenska, Sverige"}, - {"tr-TR", "Türkçe, Türkiye"}, - {"ts-TS", "Tsundere, You Baka"}, - {"uk-UA", "Українська, Україна"} - }; + await ReplyConfirmLocalizedAsync(strs.lang_set_show( + Format.Bold(_cultureInfo.ToString()), + Format.Bold(_cultureInfo.NativeName))); + } - [NadekoCommand, Aliases] - [RequireContext(ContextType.Guild)] - [Priority(0)] - public async Task LanguageSet() + [NadekoCommand, Aliases] + [RequireContext(ContextType.Guild)] + [UserPerm(GuildPerm.Administrator)] + [Priority(1)] + public async Task LanguageSet(string name) + { + try { - await ReplyConfirmLocalizedAsync(strs.lang_set_show( - Format.Bold(_cultureInfo.ToString()), - Format.Bold(_cultureInfo.NativeName))); - } - - [NadekoCommand, Aliases] - [RequireContext(ContextType.Guild)] - [UserPerm(GuildPerm.Administrator)] - [Priority(1)] - public async Task LanguageSet(string name) - { - try + CultureInfo ci; + if (name.Trim().ToLowerInvariant() == "default") { - CultureInfo ci; - if (name.Trim().ToLowerInvariant() == "default") - { - Localization.RemoveGuildCulture(ctx.Guild); - ci = Localization.DefaultCultureInfo; - } - else - { - ci = new CultureInfo(name); - Localization.SetGuildCulture(ctx.Guild, ci); - } - - await ReplyConfirmLocalizedAsync(strs.lang_set(Format.Bold(ci.ToString()), - Format.Bold(ci.NativeName))); + Localization.RemoveGuildCulture(ctx.Guild); + ci = Localization.DefaultCultureInfo; } - catch (Exception) + else { - await ReplyErrorLocalizedAsync(strs.lang_set_fail).ConfigureAwait(false); + ci = new CultureInfo(name); + Localization.SetGuildCulture(ctx.Guild, ci); } - } - [NadekoCommand, Aliases] - public async Task LanguageSetDefault() - { - var cul = Localization.DefaultCultureInfo; - await ReplyErrorLocalizedAsync(strs.lang_set_bot_show(cul, cul.NativeName)); + await ReplyConfirmLocalizedAsync(strs.lang_set(Format.Bold(ci.ToString()), + Format.Bold(ci.NativeName))); } - - [NadekoCommand, Aliases] - [OwnerOnly] - public async Task LanguageSetDefault(string name) + catch (Exception) { - try + await ReplyErrorLocalizedAsync(strs.lang_set_fail).ConfigureAwait(false); + } + } + + [NadekoCommand, Aliases] + public async Task LanguageSetDefault() + { + var cul = Localization.DefaultCultureInfo; + await ReplyErrorLocalizedAsync(strs.lang_set_bot_show(cul, cul.NativeName)); + } + + [NadekoCommand, Aliases] + [OwnerOnly] + public async Task LanguageSetDefault(string name) + { + try + { + CultureInfo ci; + if (name.Trim().ToLowerInvariant() == "default") { - CultureInfo ci; - if (name.Trim().ToLowerInvariant() == "default") - { - Localization.ResetDefaultCulture(); - ci = Localization.DefaultCultureInfo; - } - else - { - ci = new CultureInfo(name); - Localization.SetDefaultCulture(ci); - } - - await ReplyConfirmLocalizedAsync(strs.lang_set_bot(Format.Bold(ci.ToString()), - Format.Bold(ci.NativeName))); + Localization.ResetDefaultCulture(); + ci = Localization.DefaultCultureInfo; } - catch (Exception) + else { - await ReplyErrorLocalizedAsync(strs.lang_set_fail).ConfigureAwait(false); + ci = new CultureInfo(name); + Localization.SetDefaultCulture(ci); } - } - [NadekoCommand, Aliases] - public async Task LanguagesList() - { - await ctx.Channel.EmbedAsync(_eb.Create().WithOkColor() - .WithTitle(GetText(strs.lang_list)) - .WithDescription(string.Join("\n", - supportedLocales.Select(x => $"{Format.Code(x.Key),-10} => {x.Value}")))).ConfigureAwait(false); + await ReplyConfirmLocalizedAsync(strs.lang_set_bot(Format.Bold(ci.ToString()), + Format.Bold(ci.NativeName))); } + catch (Exception) + { + await ReplyErrorLocalizedAsync(strs.lang_set_fail).ConfigureAwait(false); + } + } + + [NadekoCommand, Aliases] + public async Task LanguagesList() + { + await ctx.Channel.EmbedAsync(_eb.Create().WithOkColor() + .WithTitle(GetText(strs.lang_list)) + .WithDescription(string.Join("\n", + supportedLocales.Select(x => $"{Format.Code(x.Key),-10} => {x.Value}")))).ConfigureAwait(false); } } } diff --git a/src/NadekoBot/Modules/Administration/LogCommands.cs b/src/NadekoBot/Modules/Administration/LogCommands.cs index 1c939a231..cc949b226 100644 --- a/src/NadekoBot/Modules/Administration/LogCommands.cs +++ b/src/NadekoBot/Modules/Administration/LogCommands.cs @@ -6,158 +6,154 @@ using NadekoBot.Common.TypeReaders.Models; using NadekoBot.Services.Database.Models; using NadekoBot.Extensions; using NadekoBot.Modules.Administration.Services; -using System; -using System.Collections.Generic; -using System.Linq; using System.Threading.Tasks; -namespace NadekoBot.Modules.Administration +namespace NadekoBot.Modules.Administration; + +public partial class Administration { - public partial class Administration + [Group] + [NoPublicBot] + public class LogCommands : NadekoSubmodule { - [Group] - [NoPublicBot] - public class LogCommands : NadekoSubmodule + [NadekoCommand, Aliases] + [RequireContext(ContextType.Guild)] + [UserPerm(GuildPerm.Administrator)] + [OwnerOnly] + public async Task LogServer(PermissionAction action) { - [NadekoCommand, Aliases] - [RequireContext(ContextType.Guild)] - [UserPerm(GuildPerm.Administrator)] - [OwnerOnly] - public async Task LogServer(PermissionAction action) - { - await _service.LogServer(ctx.Guild.Id, ctx.Channel.Id, action.Value).ConfigureAwait(false); - if (action.Value) - await ReplyConfirmLocalizedAsync(strs.log_all).ConfigureAwait(false); - else - await ReplyConfirmLocalizedAsync(strs.log_disabled).ConfigureAwait(false); - } + await _service.LogServer(ctx.Guild.Id, ctx.Channel.Id, action.Value).ConfigureAwait(false); + if (action.Value) + await ReplyConfirmLocalizedAsync(strs.log_all).ConfigureAwait(false); + else + await ReplyConfirmLocalizedAsync(strs.log_disabled).ConfigureAwait(false); + } - [NadekoCommand, Aliases] - [RequireContext(ContextType.Guild)] - [UserPerm(GuildPerm.Administrator)] - [OwnerOnly] - public async Task LogIgnore() - { - var settings = _service.GetGuildLogSettings(ctx.Guild.Id); + [NadekoCommand, Aliases] + [RequireContext(ContextType.Guild)] + [UserPerm(GuildPerm.Administrator)] + [OwnerOnly] + public async Task LogIgnore() + { + var settings = _service.GetGuildLogSettings(ctx.Guild.Id); - var chs = settings?.LogIgnores.Where(x => x.ItemType == IgnoredItemType.Channel).ToList() - ?? new List(); - var usrs = settings?.LogIgnores.Where(x => x.ItemType == IgnoredItemType.User).ToList() - ?? new List(); + var chs = settings?.LogIgnores.Where(x => x.ItemType == IgnoredItemType.Channel).ToList() + ?? new List(); + var usrs = settings?.LogIgnores.Where(x => x.ItemType == IgnoredItemType.User).ToList() + ?? new List(); - var eb = _eb.Create(ctx) - .WithOkColor() - .AddField(GetText(strs.log_ignored_channels), - chs.Count == 0 ? "-" : string.Join('\n', chs.Select(x => $"{x.LogItemId} | <#{x.LogItemId}>"))) - .AddField(GetText(strs.log_ignored_users), - usrs.Count == 0 ? "-" : string.Join('\n', usrs.Select(x => $"{x.LogItemId} | <@{x.LogItemId}>"))); + var eb = _eb.Create(ctx) + .WithOkColor() + .AddField(GetText(strs.log_ignored_channels), + chs.Count == 0 ? "-" : string.Join('\n', chs.Select(x => $"{x.LogItemId} | <#{x.LogItemId}>"))) + .AddField(GetText(strs.log_ignored_users), + usrs.Count == 0 ? "-" : string.Join('\n', usrs.Select(x => $"{x.LogItemId} | <@{x.LogItemId}>"))); - await ctx.Channel.EmbedAsync(eb); - } + await ctx.Channel.EmbedAsync(eb); + } - [NadekoCommand, Aliases] - [RequireContext(ContextType.Guild)] - [UserPerm(GuildPerm.Administrator)] - [OwnerOnly] - public async Task LogIgnore([Leftover]ITextChannel target) - { - target ??= (ITextChannel)ctx.Channel; + [NadekoCommand, Aliases] + [RequireContext(ContextType.Guild)] + [UserPerm(GuildPerm.Administrator)] + [OwnerOnly] + public async Task LogIgnore([Leftover]ITextChannel target) + { + target ??= (ITextChannel)ctx.Channel; - var removed = _service.LogIgnore(ctx.Guild.Id, target.Id, IgnoredItemType.Channel); + var removed = _service.LogIgnore(ctx.Guild.Id, target.Id, IgnoredItemType.Channel); - if (!removed) - await ReplyConfirmLocalizedAsync(strs.log_ignore_chan(Format.Bold(target.Mention + "(" + target.Id + ")"))).ConfigureAwait(false); - else - await ReplyConfirmLocalizedAsync(strs.log_not_ignore_chan(Format.Bold(target.Mention + "(" + target.Id + ")"))).ConfigureAwait(false); - } + if (!removed) + await ReplyConfirmLocalizedAsync(strs.log_ignore_chan(Format.Bold(target.Mention + "(" + target.Id + ")"))).ConfigureAwait(false); + else + await ReplyConfirmLocalizedAsync(strs.log_not_ignore_chan(Format.Bold(target.Mention + "(" + target.Id + ")"))).ConfigureAwait(false); + } - [NadekoCommand, Aliases] - [RequireContext(ContextType.Guild)] - [UserPerm(GuildPerm.Administrator)] - [OwnerOnly] - public async Task LogIgnore([Leftover]IUser target) - { - var removed = _service.LogIgnore(ctx.Guild.Id, target.Id, IgnoredItemType.User); + [NadekoCommand, Aliases] + [RequireContext(ContextType.Guild)] + [UserPerm(GuildPerm.Administrator)] + [OwnerOnly] + public async Task LogIgnore([Leftover]IUser target) + { + var removed = _service.LogIgnore(ctx.Guild.Id, target.Id, IgnoredItemType.User); - if (!removed) - await ReplyConfirmLocalizedAsync(strs.log_ignore_user(Format.Bold(target.Mention + "(" + target.Id + ")"))).ConfigureAwait(false); - else - await ReplyConfirmLocalizedAsync(strs.log_not_ignore_user(Format.Bold(target.Mention + "(" + target.Id + ")"))).ConfigureAwait(false); - } + if (!removed) + await ReplyConfirmLocalizedAsync(strs.log_ignore_user(Format.Bold(target.Mention + "(" + target.Id + ")"))).ConfigureAwait(false); + else + await ReplyConfirmLocalizedAsync(strs.log_not_ignore_user(Format.Bold(target.Mention + "(" + target.Id + ")"))).ConfigureAwait(false); + } - [NadekoCommand, Aliases] - [RequireContext(ContextType.Guild)] - [UserPerm(GuildPerm.Administrator)] - [OwnerOnly] - public async Task LogEvents() - { - var logSetting = _service.GetGuildLogSettings(ctx.Guild.Id); - var str = string.Join("\n", Enum.GetNames(typeof(LogType)) - .Select(x => - { - var val = logSetting is null ? null : GetLogProperty(logSetting, Enum.Parse(x)); - if (val != null) - return $"{Format.Bold(x)} <#{val}>"; - return Format.Bold(x); - })); - - await SendConfirmAsync(Format.Bold(GetText(strs.log_events)) + "\n" + - str) - .ConfigureAwait(false); - } - - private static ulong? GetLogProperty(LogSetting l, LogType type) - { - switch (type) + [NadekoCommand, Aliases] + [RequireContext(ContextType.Guild)] + [UserPerm(GuildPerm.Administrator)] + [OwnerOnly] + public async Task LogEvents() + { + var logSetting = _service.GetGuildLogSettings(ctx.Guild.Id); + var str = string.Join("\n", Enum.GetNames(typeof(LogType)) + .Select(x => { - case LogType.Other: - return l.LogOtherId; - case LogType.MessageUpdated: - return l.MessageUpdatedId; - case LogType.MessageDeleted: - return l.MessageDeletedId; - case LogType.UserJoined: - return l.UserJoinedId; - case LogType.UserLeft: - return l.UserLeftId; - case LogType.UserBanned: - return l.UserBannedId; - case LogType.UserUnbanned: - return l.UserUnbannedId; - case LogType.UserUpdated: - return l.UserUpdatedId; - case LogType.ChannelCreated: - return l.ChannelCreatedId; - case LogType.ChannelDestroyed: - return l.ChannelDestroyedId; - case LogType.ChannelUpdated: - return l.ChannelUpdatedId; - case LogType.UserPresence: - return l.LogUserPresenceId; - case LogType.VoicePresence: - return l.LogVoicePresenceId; - case LogType.VoicePresenceTTS: - return l.LogVoicePresenceTTSId; - case LogType.UserMuted: - return l.UserMutedId; - default: - return null; - } - } + var val = logSetting is null ? null : GetLogProperty(logSetting, Enum.Parse(x)); + if (val != null) + return $"{Format.Bold(x)} <#{val}>"; + return Format.Bold(x); + })); - [NadekoCommand, Aliases] - [RequireContext(ContextType.Guild)] - [UserPerm(GuildPerm.Administrator)] - [OwnerOnly] - public async Task Log(LogType type) + await SendConfirmAsync(Format.Bold(GetText(strs.log_events)) + "\n" + + str) + .ConfigureAwait(false); + } + + private static ulong? GetLogProperty(LogSetting l, LogType type) + { + switch (type) { - var val = _service.Log(ctx.Guild.Id, ctx.Channel.Id, type); - - if (val) - await ReplyConfirmLocalizedAsync(strs.log(Format.Bold(type.ToString()))).ConfigureAwait(false); - else - await ReplyConfirmLocalizedAsync(strs.log_stop(Format.Bold(type.ToString()))).ConfigureAwait(false); + case LogType.Other: + return l.LogOtherId; + case LogType.MessageUpdated: + return l.MessageUpdatedId; + case LogType.MessageDeleted: + return l.MessageDeletedId; + case LogType.UserJoined: + return l.UserJoinedId; + case LogType.UserLeft: + return l.UserLeftId; + case LogType.UserBanned: + return l.UserBannedId; + case LogType.UserUnbanned: + return l.UserUnbannedId; + case LogType.UserUpdated: + return l.UserUpdatedId; + case LogType.ChannelCreated: + return l.ChannelCreatedId; + case LogType.ChannelDestroyed: + return l.ChannelDestroyedId; + case LogType.ChannelUpdated: + return l.ChannelUpdatedId; + case LogType.UserPresence: + return l.LogUserPresenceId; + case LogType.VoicePresence: + return l.LogVoicePresenceId; + case LogType.VoicePresenceTTS: + return l.LogVoicePresenceTTSId; + case LogType.UserMuted: + return l.UserMutedId; + default: + return null; } } + + [NadekoCommand, Aliases] + [RequireContext(ContextType.Guild)] + [UserPerm(GuildPerm.Administrator)] + [OwnerOnly] + public async Task Log(LogType type) + { + var val = _service.Log(ctx.Guild.Id, ctx.Channel.Id, type); + + if (val) + await ReplyConfirmLocalizedAsync(strs.log(Format.Bold(type.ToString()))).ConfigureAwait(false); + else + await ReplyConfirmLocalizedAsync(strs.log_stop(Format.Bold(type.ToString()))).ConfigureAwait(false); + } } -} +} \ No newline at end of file diff --git a/src/NadekoBot/Modules/Administration/MuteCommands.cs b/src/NadekoBot/Modules/Administration/MuteCommands.cs index 6360689c0..e876520b4 100644 --- a/src/NadekoBot/Modules/Administration/MuteCommands.cs +++ b/src/NadekoBot/Modules/Administration/MuteCommands.cs @@ -3,235 +3,231 @@ using Discord.Commands; using NadekoBot.Common.Attributes; using NadekoBot.Common.TypeReaders.Models; using NadekoBot.Modules.Administration.Services; -using System; -using System.Linq; using System.Threading.Tasks; using Discord.WebSocket; using NadekoBot.Extensions; -using Serilog; -namespace NadekoBot.Modules.Administration +namespace NadekoBot.Modules.Administration; + +public partial class Administration { - public partial class Administration + [Group] + public class MuteCommands : NadekoSubmodule { - [Group] - public class MuteCommands : NadekoSubmodule + private async Task VerifyMutePermissions(IGuildUser runnerUser, IGuildUser targetUser) { - private async Task VerifyMutePermissions(IGuildUser runnerUser, IGuildUser targetUser) + var runnerUserRoles = runnerUser.GetRoles(); + var targetUserRoles = targetUser.GetRoles(); + if (runnerUser.Id != ctx.Guild.OwnerId && + runnerUserRoles.Max(x => x.Position) <= targetUserRoles.Max(x => x.Position)) { - var runnerUserRoles = runnerUser.GetRoles(); - var targetUserRoles = targetUser.GetRoles(); - if (runnerUser.Id != ctx.Guild.OwnerId && - runnerUserRoles.Max(x => x.Position) <= targetUserRoles.Max(x => x.Position)) - { - await ReplyErrorLocalizedAsync(strs.mute_perms).ConfigureAwait(false); - return false; - } - - return true; + await ReplyErrorLocalizedAsync(strs.mute_perms).ConfigureAwait(false); + return false; } - [NadekoCommand, Aliases] - [RequireContext(ContextType.Guild)] - [UserPerm(GuildPerm.ManageRoles)] - public async Task MuteRole([Leftover] IRole role = null) + return true; + } + + [NadekoCommand, Aliases] + [RequireContext(ContextType.Guild)] + [UserPerm(GuildPerm.ManageRoles)] + public async Task MuteRole([Leftover] IRole role = null) + { + if (role is null) { - if (role is null) - { - var muteRole = await _service.GetMuteRole(ctx.Guild).ConfigureAwait(false); - await ReplyConfirmLocalizedAsync(strs.mute_role(Format.Code(muteRole.Name))).ConfigureAwait(false); - return; - } + var muteRole = await _service.GetMuteRole(ctx.Guild).ConfigureAwait(false); + await ReplyConfirmLocalizedAsync(strs.mute_role(Format.Code(muteRole.Name))).ConfigureAwait(false); + return; + } - if (ctx.User.Id != ctx.Guild.OwnerId && - role.Position >= ((SocketGuildUser) ctx.User).Roles.Max(x => x.Position)) - { - await ReplyErrorLocalizedAsync(strs.insuf_perms_u).ConfigureAwait(false); - return; - } + if (ctx.User.Id != ctx.Guild.OwnerId && + role.Position >= ((SocketGuildUser) ctx.User).Roles.Max(x => x.Position)) + { + await ReplyErrorLocalizedAsync(strs.insuf_perms_u).ConfigureAwait(false); + return; + } - await _service.SetMuteRoleAsync(ctx.Guild.Id, role.Name).ConfigureAwait(false); + await _service.SetMuteRoleAsync(ctx.Guild.Id, role.Name).ConfigureAwait(false); - await ReplyConfirmLocalizedAsync(strs.mute_role_set).ConfigureAwait(false); - } + await ReplyConfirmLocalizedAsync(strs.mute_role_set).ConfigureAwait(false); + } - [NadekoCommand, Aliases] - [RequireContext(ContextType.Guild)] - [UserPerm(GuildPerm.ManageRoles | GuildPerm.MuteMembers)] - [Priority(0)] - public async Task Mute(IGuildUser target, [Leftover] string reason = "") + [NadekoCommand, Aliases] + [RequireContext(ContextType.Guild)] + [UserPerm(GuildPerm.ManageRoles | GuildPerm.MuteMembers)] + [Priority(0)] + public async Task Mute(IGuildUser target, [Leftover] string reason = "") + { + try { - try - { - if (!await VerifyMutePermissions((IGuildUser)ctx.User, target)) - return; - - await _service.MuteUser(target, ctx.User, reason: reason).ConfigureAwait(false); - await ReplyConfirmLocalizedAsync(strs.user_muted(Format.Bold(target.ToString()))).ConfigureAwait(false); - } - catch (Exception ex) - { - Log.Warning(ex.ToString()); - await ReplyErrorLocalizedAsync(strs.mute_error).ConfigureAwait(false); - } - } - - [NadekoCommand, Aliases] - [RequireContext(ContextType.Guild)] - [UserPerm(GuildPerm.ManageRoles | GuildPerm.MuteMembers)] - [Priority(1)] - public async Task Mute(StoopidTime time, IGuildUser user, [Leftover] string reason = "") - { - if (time.Time < TimeSpan.FromMinutes(1) || time.Time > TimeSpan.FromDays(49)) + if (!await VerifyMutePermissions((IGuildUser)ctx.User, target)) return; - try - { - if (!await VerifyMutePermissions((IGuildUser)ctx.User, user)) - return; - await _service.TimedMute(user, ctx.User, time.Time, reason: reason).ConfigureAwait(false); - await ReplyConfirmLocalizedAsync(strs.user_muted_time(Format.Bold(user.ToString()), (int)time.Time.TotalMinutes)); - } - catch (Exception ex) - { - Log.Warning(ex, "Error in mute command"); - await ReplyErrorLocalizedAsync(strs.mute_error).ConfigureAwait(false); - } + await _service.MuteUser(target, ctx.User, reason: reason).ConfigureAwait(false); + await ReplyConfirmLocalizedAsync(strs.user_muted(Format.Bold(target.ToString()))).ConfigureAwait(false); } - - [NadekoCommand, Aliases] - [RequireContext(ContextType.Guild)] - [UserPerm(GuildPerm.ManageRoles | GuildPerm.MuteMembers)] - public async Task Unmute(IGuildUser user, [Leftover] string reason = "") + catch (Exception ex) { - try - { - await _service.UnmuteUser(user.GuildId, user.Id, ctx.User, reason: reason).ConfigureAwait(false); - await ReplyConfirmLocalizedAsync(strs.user_unmuted(Format.Bold(user.ToString()))).ConfigureAwait(false); - } - catch - { - await ReplyErrorLocalizedAsync(strs.mute_error).ConfigureAwait(false); - } + Log.Warning(ex.ToString()); + await ReplyErrorLocalizedAsync(strs.mute_error).ConfigureAwait(false); } + } - [NadekoCommand, Aliases] - [RequireContext(ContextType.Guild)] - [UserPerm(GuildPerm.ManageRoles)] - [Priority(0)] - public async Task ChatMute(IGuildUser user, [Leftover] string reason = "") + [NadekoCommand, Aliases] + [RequireContext(ContextType.Guild)] + [UserPerm(GuildPerm.ManageRoles | GuildPerm.MuteMembers)] + [Priority(1)] + public async Task Mute(StoopidTime time, IGuildUser user, [Leftover] string reason = "") + { + if (time.Time < TimeSpan.FromMinutes(1) || time.Time > TimeSpan.FromDays(49)) + return; + try { - try - { - if (!await VerifyMutePermissions((IGuildUser)ctx.User, user)) - return; - - await _service.MuteUser(user, ctx.User, MuteType.Chat, reason: reason).ConfigureAwait(false); - await ReplyConfirmLocalizedAsync(strs.user_chat_mute(Format.Bold(user.ToString()))).ConfigureAwait(false); - } - catch (Exception ex) - { - Log.Warning(ex.ToString()); - await ReplyErrorLocalizedAsync(strs.mute_error).ConfigureAwait(false); - } - } - - [NadekoCommand, Aliases] - [RequireContext(ContextType.Guild)] - [UserPerm(GuildPerm.ManageRoles)] - [Priority(1)] - public async Task ChatMute(StoopidTime time, IGuildUser user, [Leftover] string reason = "") - { - if (time.Time < TimeSpan.FromMinutes(1) || time.Time > TimeSpan.FromDays(49)) + if (!await VerifyMutePermissions((IGuildUser)ctx.User, user)) return; - try - { - if (!await VerifyMutePermissions((IGuildUser)ctx.User, user)) - return; - await _service.TimedMute(user, ctx.User, time.Time, MuteType.Chat, reason: reason).ConfigureAwait(false); - await ReplyConfirmLocalizedAsync(strs.user_chat_mute_time(Format.Bold(user.ToString()), (int)time.Time.TotalMinutes)); - } - catch (Exception ex) - { - Log.Warning(ex.ToString()); - await ReplyErrorLocalizedAsync(strs.mute_error).ConfigureAwait(false); - } + await _service.TimedMute(user, ctx.User, time.Time, reason: reason).ConfigureAwait(false); + await ReplyConfirmLocalizedAsync(strs.user_muted_time(Format.Bold(user.ToString()), (int)time.Time.TotalMinutes)); } - - [NadekoCommand, Aliases] - [RequireContext(ContextType.Guild)] - [UserPerm(GuildPerm.ManageRoles)] - public async Task ChatUnmute(IGuildUser user, [Leftover] string reason = "") + catch (Exception ex) { - try - { - await _service.UnmuteUser(user.Guild.Id, user.Id, ctx.User, MuteType.Chat, reason: reason).ConfigureAwait(false); - await ReplyConfirmLocalizedAsync(strs.user_chat_unmute(Format.Bold(user.ToString()))).ConfigureAwait(false); - } - catch - { - await ReplyErrorLocalizedAsync(strs.mute_error).ConfigureAwait(false); - } + Log.Warning(ex, "Error in mute command"); + await ReplyErrorLocalizedAsync(strs.mute_error).ConfigureAwait(false); } + } - [NadekoCommand, Aliases] - [RequireContext(ContextType.Guild)] - [UserPerm(GuildPerm.MuteMembers)] - [Priority(0)] - public async Task VoiceMute(IGuildUser user, [Leftover] string reason = "") + [NadekoCommand, Aliases] + [RequireContext(ContextType.Guild)] + [UserPerm(GuildPerm.ManageRoles | GuildPerm.MuteMembers)] + public async Task Unmute(IGuildUser user, [Leftover] string reason = "") + { + try { - try - { - if (!await VerifyMutePermissions((IGuildUser)ctx.User, user)) - return; - - await _service.MuteUser(user, ctx.User, MuteType.Voice, reason: reason).ConfigureAwait(false); - await ReplyConfirmLocalizedAsync(strs.user_voice_mute(Format.Bold(user.ToString()))).ConfigureAwait(false); - } - catch - { - await ReplyErrorLocalizedAsync(strs.mute_error).ConfigureAwait(false); - } + await _service.UnmuteUser(user.GuildId, user.Id, ctx.User, reason: reason).ConfigureAwait(false); + await ReplyConfirmLocalizedAsync(strs.user_unmuted(Format.Bold(user.ToString()))).ConfigureAwait(false); } - - [NadekoCommand, Aliases] - [RequireContext(ContextType.Guild)] - [UserPerm(GuildPerm.MuteMembers)] - [Priority(1)] - public async Task VoiceMute(StoopidTime time,IGuildUser user, [Leftover] string reason = "") + catch { - if (time.Time < TimeSpan.FromMinutes(1) || time.Time > TimeSpan.FromDays(49)) + await ReplyErrorLocalizedAsync(strs.mute_error).ConfigureAwait(false); + } + } + + [NadekoCommand, Aliases] + [RequireContext(ContextType.Guild)] + [UserPerm(GuildPerm.ManageRoles)] + [Priority(0)] + public async Task ChatMute(IGuildUser user, [Leftover] string reason = "") + { + try + { + if (!await VerifyMutePermissions((IGuildUser)ctx.User, user)) return; - try - { - if (!await VerifyMutePermissions((IGuildUser)ctx.User, user)) - return; - await _service.TimedMute(user, ctx.User, time.Time, MuteType.Voice, reason: reason).ConfigureAwait(false); - await ReplyConfirmLocalizedAsync(strs.user_voice_mute_time(Format.Bold(user.ToString()), (int)time.Time.TotalMinutes)); - } - catch - { - await ReplyErrorLocalizedAsync(strs.mute_error).ConfigureAwait(false); - } + await _service.MuteUser(user, ctx.User, MuteType.Chat, reason: reason).ConfigureAwait(false); + await ReplyConfirmLocalizedAsync(strs.user_chat_mute(Format.Bold(user.ToString()))).ConfigureAwait(false); } - - [NadekoCommand, Aliases] - [RequireContext(ContextType.Guild)] - [UserPerm(GuildPerm.MuteMembers)] - public async Task VoiceUnmute(IGuildUser user, [Leftover] string reason = "") + catch (Exception ex) { - try - { - await _service.UnmuteUser(user.GuildId, user.Id, ctx.User, MuteType.Voice, reason: reason).ConfigureAwait(false); - await ReplyConfirmLocalizedAsync(strs.user_voice_unmute(Format.Bold(user.ToString()))).ConfigureAwait(false); - } - catch - { - await ReplyErrorLocalizedAsync(strs.mute_error).ConfigureAwait(false); - } + Log.Warning(ex.ToString()); + await ReplyErrorLocalizedAsync(strs.mute_error).ConfigureAwait(false); + } + } + + [NadekoCommand, Aliases] + [RequireContext(ContextType.Guild)] + [UserPerm(GuildPerm.ManageRoles)] + [Priority(1)] + public async Task ChatMute(StoopidTime time, IGuildUser user, [Leftover] string reason = "") + { + if (time.Time < TimeSpan.FromMinutes(1) || time.Time > TimeSpan.FromDays(49)) + return; + try + { + if (!await VerifyMutePermissions((IGuildUser)ctx.User, user)) + return; + + await _service.TimedMute(user, ctx.User, time.Time, MuteType.Chat, reason: reason).ConfigureAwait(false); + await ReplyConfirmLocalizedAsync(strs.user_chat_mute_time(Format.Bold(user.ToString()), (int)time.Time.TotalMinutes)); + } + catch (Exception ex) + { + Log.Warning(ex.ToString()); + await ReplyErrorLocalizedAsync(strs.mute_error).ConfigureAwait(false); + } + } + + [NadekoCommand, Aliases] + [RequireContext(ContextType.Guild)] + [UserPerm(GuildPerm.ManageRoles)] + public async Task ChatUnmute(IGuildUser user, [Leftover] string reason = "") + { + try + { + await _service.UnmuteUser(user.Guild.Id, user.Id, ctx.User, MuteType.Chat, reason: reason).ConfigureAwait(false); + await ReplyConfirmLocalizedAsync(strs.user_chat_unmute(Format.Bold(user.ToString()))).ConfigureAwait(false); + } + catch + { + await ReplyErrorLocalizedAsync(strs.mute_error).ConfigureAwait(false); + } + } + + [NadekoCommand, Aliases] + [RequireContext(ContextType.Guild)] + [UserPerm(GuildPerm.MuteMembers)] + [Priority(0)] + public async Task VoiceMute(IGuildUser user, [Leftover] string reason = "") + { + try + { + if (!await VerifyMutePermissions((IGuildUser)ctx.User, user)) + return; + + await _service.MuteUser(user, ctx.User, MuteType.Voice, reason: reason).ConfigureAwait(false); + await ReplyConfirmLocalizedAsync(strs.user_voice_mute(Format.Bold(user.ToString()))).ConfigureAwait(false); + } + catch + { + await ReplyErrorLocalizedAsync(strs.mute_error).ConfigureAwait(false); + } + } + + [NadekoCommand, Aliases] + [RequireContext(ContextType.Guild)] + [UserPerm(GuildPerm.MuteMembers)] + [Priority(1)] + public async Task VoiceMute(StoopidTime time,IGuildUser user, [Leftover] string reason = "") + { + if (time.Time < TimeSpan.FromMinutes(1) || time.Time > TimeSpan.FromDays(49)) + return; + try + { + if (!await VerifyMutePermissions((IGuildUser)ctx.User, user)) + return; + + await _service.TimedMute(user, ctx.User, time.Time, MuteType.Voice, reason: reason).ConfigureAwait(false); + await ReplyConfirmLocalizedAsync(strs.user_voice_mute_time(Format.Bold(user.ToString()), (int)time.Time.TotalMinutes)); + } + catch + { + await ReplyErrorLocalizedAsync(strs.mute_error).ConfigureAwait(false); + } + } + + [NadekoCommand, Aliases] + [RequireContext(ContextType.Guild)] + [UserPerm(GuildPerm.MuteMembers)] + public async Task VoiceUnmute(IGuildUser user, [Leftover] string reason = "") + { + try + { + await _service.UnmuteUser(user.GuildId, user.Id, ctx.User, MuteType.Voice, reason: reason).ConfigureAwait(false); + await ReplyConfirmLocalizedAsync(strs.user_voice_unmute(Format.Bold(user.ToString()))).ConfigureAwait(false); + } + catch + { + await ReplyErrorLocalizedAsync(strs.mute_error).ConfigureAwait(false); } } } -} +} \ No newline at end of file diff --git a/src/NadekoBot/Modules/Administration/PlayingRotateCommands.cs b/src/NadekoBot/Modules/Administration/PlayingRotateCommands.cs index 4348f0f88..799498896 100644 --- a/src/NadekoBot/Modules/Administration/PlayingRotateCommands.cs +++ b/src/NadekoBot/Modules/Administration/PlayingRotateCommands.cs @@ -1,68 +1,66 @@ using Discord.Commands; -using System.Linq; using System.Threading.Tasks; using NadekoBot.Common.Attributes; using NadekoBot.Modules.Administration.Services; using Discord; -namespace NadekoBot.Modules.Administration +namespace NadekoBot.Modules.Administration; + +public partial class Administration { - public partial class Administration + [Group] + public class PlayingRotateCommands : NadekoSubmodule { - [Group] - public class PlayingRotateCommands : NadekoSubmodule + [NadekoCommand, Aliases] + [OwnerOnly] + public async Task RotatePlaying() { - [NadekoCommand, Aliases] - [OwnerOnly] - public async Task RotatePlaying() + if (_service.ToggleRotatePlaying()) + await ReplyConfirmLocalizedAsync(strs.ropl_enabled).ConfigureAwait(false); + else + await ReplyConfirmLocalizedAsync(strs.ropl_disabled).ConfigureAwait(false); + } + + [NadekoCommand, Aliases] + [OwnerOnly] + public async Task AddPlaying(ActivityType t, [Leftover] string status) + { + await _service.AddPlaying(t, status).ConfigureAwait(false); + + await ReplyConfirmLocalizedAsync(strs.ropl_added).ConfigureAwait(false); + } + + [NadekoCommand, Aliases] + [OwnerOnly] + public async Task ListPlaying() + { + var statuses = _service.GetRotatingStatuses(); + + if (!statuses.Any()) { - if (_service.ToggleRotatePlaying()) - await ReplyConfirmLocalizedAsync(strs.ropl_enabled).ConfigureAwait(false); - else - await ReplyConfirmLocalizedAsync(strs.ropl_disabled).ConfigureAwait(false); + await ReplyErrorLocalizedAsync(strs.ropl_not_set).ConfigureAwait(false); + } + else + { + var i = 1; + await ReplyConfirmLocalizedAsync(strs.ropl_list( + string.Join("\n\t", statuses.Select(rs => $"`{i++}.` *{rs.Type}* {rs.Status}")))); } - [NadekoCommand, Aliases] - [OwnerOnly] - public async Task AddPlaying(ActivityType t, [Leftover] string status) - { - await _service.AddPlaying(t, status).ConfigureAwait(false); + } - await ReplyConfirmLocalizedAsync(strs.ropl_added).ConfigureAwait(false); - } + [NadekoCommand, Aliases] + [OwnerOnly] + public async Task RemovePlaying(int index) + { + index -= 1; - [NadekoCommand, Aliases] - [OwnerOnly] - public async Task ListPlaying() - { - var statuses = _service.GetRotatingStatuses(); + var msg = await _service.RemovePlayingAsync(index).ConfigureAwait(false); - if (!statuses.Any()) - { - await ReplyErrorLocalizedAsync(strs.ropl_not_set).ConfigureAwait(false); - } - else - { - var i = 1; - await ReplyConfirmLocalizedAsync(strs.ropl_list( - string.Join("\n\t", statuses.Select(rs => $"`{i++}.` *{rs.Type}* {rs.Status}")))); - } + if (msg is null) + return; - } - - [NadekoCommand, Aliases] - [OwnerOnly] - public async Task RemovePlaying(int index) - { - index -= 1; - - var msg = await _service.RemovePlayingAsync(index).ConfigureAwait(false); - - if (msg is null) - return; - - await ReplyConfirmLocalizedAsync(strs.reprm(msg)); - } + await ReplyConfirmLocalizedAsync(strs.reprm(msg)); } } } \ No newline at end of file diff --git a/src/NadekoBot/Modules/Administration/PrefixCommands.cs b/src/NadekoBot/Modules/Administration/PrefixCommands.cs index 7bc41c691..2509e17eb 100644 --- a/src/NadekoBot/Modules/Administration/PrefixCommands.cs +++ b/src/NadekoBot/Modules/Administration/PrefixCommands.cs @@ -3,62 +3,61 @@ using Discord.Commands; using System.Threading.Tasks; using NadekoBot.Common.Attributes; -namespace NadekoBot.Modules.Administration +namespace NadekoBot.Modules.Administration; + +public partial class Administration { - public partial class Administration + [Group] + public class PrefixCommands : NadekoSubmodule { - [Group] - public class PrefixCommands : NadekoSubmodule + [NadekoCommand, Aliases] + [Priority(1)] + public async Task PrefixCommand() { - [NadekoCommand, Aliases] - [Priority(1)] - public async Task PrefixCommand() + await ReplyConfirmLocalizedAsync(strs.prefix_current(Format.Code(CmdHandler.GetPrefix(ctx.Guild)))).ConfigureAwait(false); + } + + public enum Set + { + Set + } + + [NadekoCommand, Aliases] + [RequireContext(ContextType.Guild)] + [UserPerm(GuildPerm.Administrator)] + [Priority(0)] + public Task PrefixCommand(Set _, [Leftover] string prefix) + => PrefixCommand(prefix); + + [NadekoCommand, Aliases] + [RequireContext(ContextType.Guild)] + [UserPerm(GuildPerm.Administrator)] + [Priority(0)] + public async Task PrefixCommand([Leftover]string prefix) + { + if (string.IsNullOrWhiteSpace(prefix)) + return; + + var oldPrefix = base.Prefix; + var newPrefix = CmdHandler.SetPrefix(ctx.Guild, prefix); + + await ReplyConfirmLocalizedAsync(strs.prefix_new(Format.Code(oldPrefix), Format.Code(newPrefix))).ConfigureAwait(false); + } + + [NadekoCommand, Aliases] + [OwnerOnly] + public async Task DefPrefix([Leftover]string prefix = null) + { + if (string.IsNullOrWhiteSpace(prefix)) { - await ReplyConfirmLocalizedAsync(strs.prefix_current(Format.Code(CmdHandler.GetPrefix(ctx.Guild)))).ConfigureAwait(false); + await ReplyConfirmLocalizedAsync(strs.defprefix_current(CmdHandler.GetPrefix())).ConfigureAwait(false); + return; } - public enum Set - { - Set - } + var oldPrefix = CmdHandler.GetPrefix(); + var newPrefix = CmdHandler.SetDefaultPrefix(prefix); - [NadekoCommand, Aliases] - [RequireContext(ContextType.Guild)] - [UserPerm(GuildPerm.Administrator)] - [Priority(0)] - public Task PrefixCommand(Set _, [Leftover] string prefix) - => PrefixCommand(prefix); - - [NadekoCommand, Aliases] - [RequireContext(ContextType.Guild)] - [UserPerm(GuildPerm.Administrator)] - [Priority(0)] - public async Task PrefixCommand([Leftover]string prefix) - { - if (string.IsNullOrWhiteSpace(prefix)) - return; - - var oldPrefix = base.Prefix; - var newPrefix = CmdHandler.SetPrefix(ctx.Guild, prefix); - - await ReplyConfirmLocalizedAsync(strs.prefix_new(Format.Code(oldPrefix), Format.Code(newPrefix))).ConfigureAwait(false); - } - - [NadekoCommand, Aliases] - [OwnerOnly] - public async Task DefPrefix([Leftover]string prefix = null) - { - if (string.IsNullOrWhiteSpace(prefix)) - { - await ReplyConfirmLocalizedAsync(strs.defprefix_current(CmdHandler.GetPrefix())).ConfigureAwait(false); - return; - } - - var oldPrefix = CmdHandler.GetPrefix(); - var newPrefix = CmdHandler.SetDefaultPrefix(prefix); - - await ReplyConfirmLocalizedAsync(strs.defprefix_new(Format.Code(oldPrefix), Format.Code(newPrefix))).ConfigureAwait(false); - } + await ReplyConfirmLocalizedAsync(strs.defprefix_new(Format.Code(oldPrefix), Format.Code(newPrefix))).ConfigureAwait(false); } } -} +} \ No newline at end of file diff --git a/src/NadekoBot/Modules/Administration/ProtectionCommands.cs b/src/NadekoBot/Modules/Administration/ProtectionCommands.cs index 313dded41..c4bfe2bea 100644 --- a/src/NadekoBot/Modules/Administration/ProtectionCommands.cs +++ b/src/NadekoBot/Modules/Administration/ProtectionCommands.cs @@ -1,294 +1,291 @@ -using System; -using Discord; +using Discord; using Discord.Commands; using NadekoBot.Common.Attributes; using NadekoBot.Services.Database.Models; using NadekoBot.Extensions; using NadekoBot.Modules.Administration.Common; using NadekoBot.Modules.Administration.Services; -using System.Linq; using System.Threading.Tasks; using NadekoBot.Common.TypeReaders.Models; -namespace NadekoBot.Modules.Administration +namespace NadekoBot.Modules.Administration; + +public partial class Administration { - public partial class Administration + [Group] + public class ProtectionCommands : NadekoSubmodule { - [Group] - public class ProtectionCommands : NadekoSubmodule + [NadekoCommand, Aliases] + [RequireContext(ContextType.Guild)] + [UserPerm(GuildPerm.Administrator)] + public async Task AntiAlt() { - [NadekoCommand, Aliases] - [RequireContext(ContextType.Guild)] - [UserPerm(GuildPerm.Administrator)] - public async Task AntiAlt() + if (await _service.TryStopAntiAlt(ctx.Guild.Id)) { - if (await _service.TryStopAntiAlt(ctx.Guild.Id)) - { - await ReplyErrorLocalizedAsync(strs.prot_disable("Anti-Alt")); - return; - } - - await ReplyConfirmLocalizedAsync(strs.protection_not_running("Anti-Alt")); + await ReplyErrorLocalizedAsync(strs.prot_disable("Anti-Alt")); + return; } - [NadekoCommand, Aliases] - [RequireContext(ContextType.Guild)] - [UserPerm(GuildPerm.Administrator)] - public async Task AntiAlt(StoopidTime minAge, PunishmentAction action, [Leftover] StoopidTime punishTime = null) - { - var minAgeMinutes = (int)minAge.Time.TotalMinutes; - var punishTimeMinutes = (int?) punishTime?.Time.TotalMinutes ?? 0; + await ReplyConfirmLocalizedAsync(strs.protection_not_running("Anti-Alt")); + } - if (minAgeMinutes < 1 || punishTimeMinutes < 0) - return; + [NadekoCommand, Aliases] + [RequireContext(ContextType.Guild)] + [UserPerm(GuildPerm.Administrator)] + public async Task AntiAlt(StoopidTime minAge, PunishmentAction action, [Leftover] StoopidTime punishTime = null) + { + var minAgeMinutes = (int)minAge.Time.TotalMinutes; + var punishTimeMinutes = (int?) punishTime?.Time.TotalMinutes ?? 0; - await _service.StartAntiAltAsync(ctx.Guild.Id, minAgeMinutes, action, (int?)punishTime?.Time.TotalMinutes ?? 0); + if (minAgeMinutes < 1 || punishTimeMinutes < 0) + return; - await ctx.OkAsync(); - } + await _service.StartAntiAltAsync(ctx.Guild.Id, minAgeMinutes, action, (int?)punishTime?.Time.TotalMinutes ?? 0); + + await ctx.OkAsync(); + } - [NadekoCommand, Aliases] - [RequireContext(ContextType.Guild)] - [UserPerm(GuildPerm.Administrator)] - public async Task AntiAlt(StoopidTime minAge, PunishmentAction action, [Leftover]IRole role) - { - var minAgeMinutes = (int)minAge.Time.TotalMinutes; + [NadekoCommand, Aliases] + [RequireContext(ContextType.Guild)] + [UserPerm(GuildPerm.Administrator)] + public async Task AntiAlt(StoopidTime minAge, PunishmentAction action, [Leftover]IRole role) + { + var minAgeMinutes = (int)minAge.Time.TotalMinutes; - if (minAgeMinutes < 1) - return; + if (minAgeMinutes < 1) + return; - await _service.StartAntiAltAsync(ctx.Guild.Id, minAgeMinutes, action, roleId: role.Id); + await _service.StartAntiAltAsync(ctx.Guild.Id, minAgeMinutes, action, roleId: role.Id); - await ctx.OkAsync(); - } + await ctx.OkAsync(); + } - [NadekoCommand, Aliases] - [RequireContext(ContextType.Guild)] - [UserPerm(GuildPerm.Administrator)] - public Task AntiRaid() + [NadekoCommand, Aliases] + [RequireContext(ContextType.Guild)] + [UserPerm(GuildPerm.Administrator)] + public Task AntiRaid() + { + if (_service.TryStopAntiRaid(ctx.Guild.Id)) { - if (_service.TryStopAntiRaid(ctx.Guild.Id)) - { - return ReplyConfirmLocalizedAsync(strs.prot_disable("Anti-Raid")); - } - else - { - return ReplyPendingLocalizedAsync(strs.protection_not_running("Anti-Raid")); - } + return ReplyConfirmLocalizedAsync(strs.prot_disable("Anti-Raid")); } - - [NadekoCommand, Aliases] - [RequireContext(ContextType.Guild)] - [UserPerm(GuildPerm.Administrator)] - [Priority(1)] - public Task AntiRaid(int userThreshold, int seconds, - PunishmentAction action, [Leftover] StoopidTime punishTime) - => InternalAntiRaid(userThreshold, seconds, action, punishTime: punishTime); - - [NadekoCommand, Aliases] - [RequireContext(ContextType.Guild)] - [UserPerm(GuildPerm.Administrator)] - [Priority(2)] - public Task AntiRaid(int userThreshold, int seconds, PunishmentAction action) - => InternalAntiRaid(userThreshold, seconds, action); - - private async Task InternalAntiRaid(int userThreshold, int seconds = 10, - PunishmentAction action = PunishmentAction.Mute, StoopidTime punishTime = null) + else { - if (action == PunishmentAction.AddRole) - { - await ReplyErrorLocalizedAsync(strs.punishment_unsupported(action)); - return; - } + return ReplyPendingLocalizedAsync(strs.protection_not_running("Anti-Raid")); + } + } + + [NadekoCommand, Aliases] + [RequireContext(ContextType.Guild)] + [UserPerm(GuildPerm.Administrator)] + [Priority(1)] + public Task AntiRaid(int userThreshold, int seconds, + PunishmentAction action, [Leftover] StoopidTime punishTime) + => InternalAntiRaid(userThreshold, seconds, action, punishTime: punishTime); + + [NadekoCommand, Aliases] + [RequireContext(ContextType.Guild)] + [UserPerm(GuildPerm.Administrator)] + [Priority(2)] + public Task AntiRaid(int userThreshold, int seconds, PunishmentAction action) + => InternalAntiRaid(userThreshold, seconds, action); + + private async Task InternalAntiRaid(int userThreshold, int seconds = 10, + PunishmentAction action = PunishmentAction.Mute, StoopidTime punishTime = null) + { + if (action == PunishmentAction.AddRole) + { + await ReplyErrorLocalizedAsync(strs.punishment_unsupported(action)); + return; + } - if (userThreshold < 2 || userThreshold > 30) - { - await ReplyErrorLocalizedAsync(strs.raid_cnt(2, 30)); - return; - } - - if (seconds < 2 || seconds > 300) - { - await ReplyErrorLocalizedAsync(strs.raid_time(2, 300)); - return; - } - - if (punishTime is not null) - { - if (!_service.IsDurationAllowed(action)) - { - await ReplyErrorLocalizedAsync(strs.prot_cant_use_time); - } - } - - var time = (int?) punishTime?.Time.TotalMinutes ?? 0; - if (time < 0 || time > 60 * 24) - return; - - var stats = await _service.StartAntiRaidAsync(ctx.Guild.Id, userThreshold, seconds, - action, time).ConfigureAwait(false); - - if (stats is null) - { - return; - } - - await SendConfirmAsync(GetText(strs.prot_enable("Anti-Raid")), - $"{ctx.User.Mention} {GetAntiRaidString(stats)}") - .ConfigureAwait(false); - } - - [NadekoCommand, Aliases] - [RequireContext(ContextType.Guild)] - [UserPerm(GuildPerm.Administrator)] - public Task AntiSpam() + if (userThreshold < 2 || userThreshold > 30) { - if (_service.TryStopAntiSpam(ctx.Guild.Id)) + await ReplyErrorLocalizedAsync(strs.raid_cnt(2, 30)); + return; + } + + if (seconds < 2 || seconds > 300) + { + await ReplyErrorLocalizedAsync(strs.raid_time(2, 300)); + return; + } + + if (punishTime is not null) + { + if (!_service.IsDurationAllowed(action)) { - return ReplyConfirmLocalizedAsync(strs.prot_disable("Anti-Spam")); - } - else - { - return ReplyPendingLocalizedAsync(strs.protection_not_running("Anti-Spam")); + await ReplyErrorLocalizedAsync(strs.prot_cant_use_time); } } + + var time = (int?) punishTime?.Time.TotalMinutes ?? 0; + if (time < 0 || time > 60 * 24) + return; + + var stats = await _service.StartAntiRaidAsync(ctx.Guild.Id, userThreshold, seconds, + action, time).ConfigureAwait(false); + + if (stats is null) + { + return; + } + + await SendConfirmAsync(GetText(strs.prot_enable("Anti-Raid")), + $"{ctx.User.Mention} {GetAntiRaidString(stats)}") + .ConfigureAwait(false); + } + + [NadekoCommand, Aliases] + [RequireContext(ContextType.Guild)] + [UserPerm(GuildPerm.Administrator)] + public Task AntiSpam() + { + if (_service.TryStopAntiSpam(ctx.Guild.Id)) + { + return ReplyConfirmLocalizedAsync(strs.prot_disable("Anti-Spam")); + } + else + { + return ReplyPendingLocalizedAsync(strs.protection_not_running("Anti-Spam")); + } + } - [NadekoCommand, Aliases] - [RequireContext(ContextType.Guild)] - [UserPerm(GuildPerm.Administrator)] - [Priority(0)] - public Task AntiSpam(int messageCount, PunishmentAction action, [Leftover] IRole role) + [NadekoCommand, Aliases] + [RequireContext(ContextType.Guild)] + [UserPerm(GuildPerm.Administrator)] + [Priority(0)] + public Task AntiSpam(int messageCount, PunishmentAction action, [Leftover] IRole role) + { + if (action != PunishmentAction.AddRole) + return Task.CompletedTask; + + return InternalAntiSpam(messageCount, action, null, role); + } + + [NadekoCommand, Aliases] + [RequireContext(ContextType.Guild)] + [UserPerm(GuildPerm.Administrator)] + [Priority(1)] + public Task AntiSpam(int messageCount, PunishmentAction action, [Leftover] StoopidTime punishTime) + => InternalAntiSpam(messageCount, action, punishTime, null); + + [NadekoCommand, Aliases] + [RequireContext(ContextType.Guild)] + [UserPerm(GuildPerm.Administrator)] + [Priority(2)] + public Task AntiSpam(int messageCount, PunishmentAction action) + => InternalAntiSpam(messageCount, action); + + public async Task InternalAntiSpam(int messageCount, PunishmentAction action, + StoopidTime timeData = null, IRole role = null) + { + if (messageCount < 2 || messageCount > 10) + return; + + if (timeData is not null) { - if (action != PunishmentAction.AddRole) - return Task.CompletedTask; - - return InternalAntiSpam(messageCount, action, null, role); - } - - [NadekoCommand, Aliases] - [RequireContext(ContextType.Guild)] - [UserPerm(GuildPerm.Administrator)] - [Priority(1)] - public Task AntiSpam(int messageCount, PunishmentAction action, [Leftover] StoopidTime punishTime) - => InternalAntiSpam(messageCount, action, punishTime, null); - - [NadekoCommand, Aliases] - [RequireContext(ContextType.Guild)] - [UserPerm(GuildPerm.Administrator)] - [Priority(2)] - public Task AntiSpam(int messageCount, PunishmentAction action) - => InternalAntiSpam(messageCount, action); - - public async Task InternalAntiSpam(int messageCount, PunishmentAction action, - StoopidTime timeData = null, IRole role = null) - { - if (messageCount < 2 || messageCount > 10) - return; - - if (timeData is not null) + if (!_service.IsDurationAllowed(action)) { - if (!_service.IsDurationAllowed(action)) - { - await ReplyErrorLocalizedAsync(strs.prot_cant_use_time); - } + await ReplyErrorLocalizedAsync(strs.prot_cant_use_time); } - - var time = (int?) timeData?.Time.TotalMinutes ?? 0; - if (time < 0 || time > 60 * 24) - return; - - var stats = await _service.StartAntiSpamAsync(ctx.Guild.Id, messageCount, action, time, role?.Id).ConfigureAwait(false); - - await SendConfirmAsync(GetText(strs.prot_enable("Anti-Spam")), - $"{ctx.User.Mention} {GetAntiSpamString(stats)}").ConfigureAwait(false); } - [NadekoCommand, Aliases] - [RequireContext(ContextType.Guild)] - [UserPerm(GuildPerm.Administrator)] - public async Task AntispamIgnore() + var time = (int?) timeData?.Time.TotalMinutes ?? 0; + if (time < 0 || time > 60 * 24) + return; + + var stats = await _service.StartAntiSpamAsync(ctx.Guild.Id, messageCount, action, time, role?.Id).ConfigureAwait(false); + + await SendConfirmAsync(GetText(strs.prot_enable("Anti-Spam")), + $"{ctx.User.Mention} {GetAntiSpamString(stats)}").ConfigureAwait(false); + } + + [NadekoCommand, Aliases] + [RequireContext(ContextType.Guild)] + [UserPerm(GuildPerm.Administrator)] + public async Task AntispamIgnore() + { + var added = await _service.AntiSpamIgnoreAsync(ctx.Guild.Id, ctx.Channel.Id).ConfigureAwait(false); + + if(added is null) { - var added = await _service.AntiSpamIgnoreAsync(ctx.Guild.Id, ctx.Channel.Id).ConfigureAwait(false); - - if(added is null) - { - await ReplyErrorLocalizedAsync(strs.protection_not_running("Anti-Spam")); - return; - } - - if (added.Value) - await ReplyConfirmLocalizedAsync(strs.spam_ignore("Anti-Spam")); - else - await ReplyConfirmLocalizedAsync(strs.spam_not_ignore("Anti-Spam")); + await ReplyErrorLocalizedAsync(strs.protection_not_running("Anti-Spam")); + return; } - [NadekoCommand, Aliases] - [RequireContext(ContextType.Guild)] - public async Task AntiList() + if (added.Value) + await ReplyConfirmLocalizedAsync(strs.spam_ignore("Anti-Spam")); + else + await ReplyConfirmLocalizedAsync(strs.spam_not_ignore("Anti-Spam")); + } + + [NadekoCommand, Aliases] + [RequireContext(ContextType.Guild)] + public async Task AntiList() + { + var (spam, raid, alt) = _service.GetAntiStats(ctx.Guild.Id); + + if (spam is null && raid is null && alt is null) { - var (spam, raid, alt) = _service.GetAntiStats(ctx.Guild.Id); - - if (spam is null && raid is null && alt is null) - { - await ReplyConfirmLocalizedAsync(strs.prot_none).ConfigureAwait(false); - return; - } - - var embed = _eb.Create().WithOkColor() - .WithTitle(GetText(strs.prot_active)); - - if (spam != null) - embed.AddField("Anti-Spam", GetAntiSpamString(spam).TrimTo(1024), true); - - if (raid != null) - embed.AddField("Anti-Raid", GetAntiRaidString(raid).TrimTo(1024), true); - - if (alt is not null) - embed.AddField("Anti-Alt", GetAntiAltString(alt), true); - - await ctx.Channel.EmbedAsync(embed).ConfigureAwait(false); + await ReplyConfirmLocalizedAsync(strs.prot_none).ConfigureAwait(false); + return; } - private string GetAntiAltString(AntiAltStats alt) - => GetText(strs.anti_alt_status( - Format.Bold(alt.MinAge.ToString(@"dd\d\ hh\h\ mm\m\ ")), - Format.Bold(alt.Action.ToString()), - Format.Bold(alt.Counter.ToString()))); + var embed = _eb.Create().WithOkColor() + .WithTitle(GetText(strs.prot_active)); - private string GetAntiSpamString(AntiSpamStats stats) + if (spam != null) + embed.AddField("Anti-Spam", GetAntiSpamString(spam).TrimTo(1024), true); + + if (raid != null) + embed.AddField("Anti-Raid", GetAntiRaidString(raid).TrimTo(1024), true); + + if (alt is not null) + embed.AddField("Anti-Alt", GetAntiAltString(alt), true); + + await ctx.Channel.EmbedAsync(embed).ConfigureAwait(false); + } + + private string GetAntiAltString(AntiAltStats alt) + => GetText(strs.anti_alt_status( + Format.Bold(alt.MinAge.ToString(@"dd\d\ hh\h\ mm\m\ ")), + Format.Bold(alt.Action.ToString()), + Format.Bold(alt.Counter.ToString()))); + + private string GetAntiSpamString(AntiSpamStats stats) + { + var settings = stats.AntiSpamSettings; + var ignoredString = string.Join(", ", settings.IgnoredChannels.Select(c => $"<#{c.ChannelId}>")); + + if (string.IsNullOrWhiteSpace(ignoredString)) + ignoredString = "none"; + + string add = ""; + if (settings.MuteTime > 0) { - var settings = stats.AntiSpamSettings; - var ignoredString = string.Join(", ", settings.IgnoredChannels.Select(c => $"<#{c.ChannelId}>")); - - if (string.IsNullOrWhiteSpace(ignoredString)) - ignoredString = "none"; - - string add = ""; - if (settings.MuteTime > 0) - { - add = $" ({TimeSpan.FromMinutes(settings.MuteTime):hh\\hmm\\m})"; - } - - return GetText(strs.spam_stats( - Format.Bold(settings.MessageThreshold.ToString()), - Format.Bold(settings.Action + add), - ignoredString)); + add = $" ({TimeSpan.FromMinutes(settings.MuteTime):hh\\hmm\\m})"; } - private string GetAntiRaidString(AntiRaidStats stats) + return GetText(strs.spam_stats( + Format.Bold(settings.MessageThreshold.ToString()), + Format.Bold(settings.Action + add), + ignoredString)); + } + + private string GetAntiRaidString(AntiRaidStats stats) + { + var actionString = Format.Bold(stats.AntiRaidSettings.Action.ToString()); + + if (stats.AntiRaidSettings.PunishDuration > 0) { - var actionString = Format.Bold(stats.AntiRaidSettings.Action.ToString()); - - if (stats.AntiRaidSettings.PunishDuration > 0) - { - actionString += $" **({TimeSpan.FromMinutes(stats.AntiRaidSettings.PunishDuration):hh\\hmm\\m})**"; - } - - return GetText(strs.raid_stats( - Format.Bold(stats.AntiRaidSettings.UserThreshold.ToString()), - Format.Bold(stats.AntiRaidSettings.Seconds.ToString()), - actionString)); + actionString += $" **({TimeSpan.FromMinutes(stats.AntiRaidSettings.PunishDuration):hh\\hmm\\m})**"; } + + return GetText(strs.raid_stats( + Format.Bold(stats.AntiRaidSettings.UserThreshold.ToString()), + Format.Bold(stats.AntiRaidSettings.Seconds.ToString()), + actionString)); } } } \ No newline at end of file diff --git a/src/NadekoBot/Modules/Administration/PruneCommands.cs b/src/NadekoBot/Modules/Administration/PruneCommands.cs index 07eaeede5..c9c9b72bb 100644 --- a/src/NadekoBot/Modules/Administration/PruneCommands.cs +++ b/src/NadekoBot/Modules/Administration/PruneCommands.cs @@ -1,85 +1,83 @@ using Discord; using Discord.Commands; using NadekoBot.Extensions; -using System; using System.Threading.Tasks; using NadekoBot.Common.Attributes; using NadekoBot.Modules.Administration.Services; using ITextChannel = Discord.ITextChannel; -namespace NadekoBot.Modules.Administration +namespace NadekoBot.Modules.Administration; + +public partial class Administration { - public partial class Administration + [Group] + public class PruneCommands : NadekoSubmodule { - [Group] - public class PruneCommands : NadekoSubmodule + private static readonly TimeSpan twoWeeks = TimeSpan.FromDays(14); + + //delets her own messages, no perm required + [NadekoCommand, Aliases] + [RequireContext(ContextType.Guild)] + public async Task Prune(string parameter = null) { - private static readonly TimeSpan twoWeeks = TimeSpan.FromDays(14); + var user = await ctx.Guild.GetCurrentUserAsync().ConfigureAwait(false); - //delets her own messages, no perm required - [NadekoCommand, Aliases] - [RequireContext(ContextType.Guild)] - public async Task Prune(string parameter = null) - { - var user = await ctx.Guild.GetCurrentUserAsync().ConfigureAwait(false); + if (parameter == "-s" || parameter == "--safe") + await _service.PruneWhere((ITextChannel)ctx.Channel, 100, (x) => x.Author.Id == user.Id && !x.IsPinned).ConfigureAwait(false); + else + await _service.PruneWhere((ITextChannel)ctx.Channel, 100, (x) => x.Author.Id == user.Id).ConfigureAwait(false); + ctx.Message.DeleteAfter(3); + } + // prune x + [NadekoCommand, Aliases] + [RequireContext(ContextType.Guild)] + [UserPerm(ChannelPerm.ManageMessages)] + [BotPerm(ChannelPerm.ManageMessages)] + [Priority(1)] + public async Task Prune(int count, string parameter = null) + { + count++; + if (count < 1) + return; + if (count > 1000) + count = 1000; - if (parameter == "-s" || parameter == "--safe") - await _service.PruneWhere((ITextChannel)ctx.Channel, 100, (x) => x.Author.Id == user.Id && !x.IsPinned).ConfigureAwait(false); - else - await _service.PruneWhere((ITextChannel)ctx.Channel, 100, (x) => x.Author.Id == user.Id).ConfigureAwait(false); - ctx.Message.DeleteAfter(3); - } - // prune x - [NadekoCommand, Aliases] - [RequireContext(ContextType.Guild)] - [UserPerm(ChannelPerm.ManageMessages)] - [BotPerm(ChannelPerm.ManageMessages)] - [Priority(1)] - public async Task Prune(int count, string parameter = null) - { + if (parameter == "-s" || parameter == "--safe") + await _service.PruneWhere((ITextChannel)ctx.Channel, count, (x) => !x.IsPinned).ConfigureAwait(false); + else + await _service.PruneWhere((ITextChannel)ctx.Channel, count, x => true).ConfigureAwait(false); + } + + //prune @user [x] + [NadekoCommand, Aliases] + [RequireContext(ContextType.Guild)] + [UserPerm(ChannelPerm.ManageMessages)] + [BotPerm(ChannelPerm.ManageMessages)] + [Priority(0)] + public Task Prune(IGuildUser user, int count = 100, string parameter = null) + => Prune(user.Id, count, parameter); + + //prune userid [x] + [NadekoCommand, Aliases] + [RequireContext(ContextType.Guild)] + [UserPerm(ChannelPerm.ManageMessages)] + [BotPerm(ChannelPerm.ManageMessages)] + [Priority(0)] + public async Task Prune(ulong userId, int count = 100, string parameter = null) + { + if (userId == ctx.User.Id) count++; - if (count < 1) - return; - if (count > 1000) - count = 1000; - if (parameter == "-s" || parameter == "--safe") - await _service.PruneWhere((ITextChannel)ctx.Channel, count, (x) => !x.IsPinned).ConfigureAwait(false); - else - await _service.PruneWhere((ITextChannel)ctx.Channel, count, x => true).ConfigureAwait(false); - } + if (count < 1) + return; - //prune @user [x] - [NadekoCommand, Aliases] - [RequireContext(ContextType.Guild)] - [UserPerm(ChannelPerm.ManageMessages)] - [BotPerm(ChannelPerm.ManageMessages)] - [Priority(0)] - public Task Prune(IGuildUser user, int count = 100, string parameter = null) - => Prune(user.Id, count, parameter); + if (count > 1000) + count = 1000; - //prune userid [x] - [NadekoCommand, Aliases] - [RequireContext(ContextType.Guild)] - [UserPerm(ChannelPerm.ManageMessages)] - [BotPerm(ChannelPerm.ManageMessages)] - [Priority(0)] - public async Task Prune(ulong userId, int count = 100, string parameter = null) - { - if (userId == ctx.User.Id) - count++; - - if (count < 1) - return; - - if (count > 1000) - count = 1000; - - if (parameter == "-s" || parameter == "--safe") - await _service.PruneWhere((ITextChannel)ctx.Channel, count, m => m.Author.Id == userId && DateTime.UtcNow - m.CreatedAt < twoWeeks && !m.IsPinned).ConfigureAwait(false); - else - await _service.PruneWhere((ITextChannel)ctx.Channel, count, m => m.Author.Id == userId && DateTime.UtcNow - m.CreatedAt < twoWeeks).ConfigureAwait(false); - } + if (parameter == "-s" || parameter == "--safe") + await _service.PruneWhere((ITextChannel)ctx.Channel, count, m => m.Author.Id == userId && DateTime.UtcNow - m.CreatedAt < twoWeeks && !m.IsPinned).ConfigureAwait(false); + else + await _service.PruneWhere((ITextChannel)ctx.Channel, count, m => m.Author.Id == userId && DateTime.UtcNow - m.CreatedAt < twoWeeks).ConfigureAwait(false); } } -} +} \ No newline at end of file diff --git a/src/NadekoBot/Modules/Administration/RoleCommands.cs b/src/NadekoBot/Modules/Administration/RoleCommands.cs index b14e97837..d2cab23a4 100644 --- a/src/NadekoBot/Modules/Administration/RoleCommands.cs +++ b/src/NadekoBot/Modules/Administration/RoleCommands.cs @@ -7,83 +7,80 @@ using NadekoBot.Services.Database.Models; using NadekoBot.Extensions; using NadekoBot.Modules.Administration.Services; using SixLabors.ImageSharp.PixelFormats; -using System; -using System.Linq; using System.Net; using System.Threading.Tasks; -using Serilog; -namespace NadekoBot.Modules.Administration +namespace NadekoBot.Modules.Administration; + +public partial class Administration { - public partial class Administration + public class RoleCommands : NadekoSubmodule { - public class RoleCommands : NadekoSubmodule + private IServiceProvider _services; + public enum Exclude { Excl } + + public RoleCommands(IServiceProvider services) { - private IServiceProvider _services; - public enum Exclude { Excl } + _services = services; + } - public RoleCommands(IServiceProvider services) - { - _services = services; - } + public async Task InternalReactionRoles(bool exclusive, ulong? messageId, params string[] input) + { + var target = messageId is ulong msgId + ? await ctx.Channel.GetMessageAsync(msgId).ConfigureAwait(false) + : (await ctx.Channel.GetMessagesAsync(2).FlattenAsync().ConfigureAwait(false)) + .Skip(1) + .FirstOrDefault(); - public async Task InternalReactionRoles(bool exclusive, ulong? messageId, params string[] input) - { - var target = messageId is ulong msgId - ? await ctx.Channel.GetMessageAsync(msgId).ConfigureAwait(false) - : (await ctx.Channel.GetMessagesAsync(2).FlattenAsync().ConfigureAwait(false)) - .Skip(1) - .FirstOrDefault(); + if (input.Length % 2 != 0) + return; - if (input.Length % 2 != 0) - return; - - var grp = 0; - var results = input - .GroupBy(x => grp++ / 2) - .Select(async x => - { - var inputRoleStr = x.First(); - var roleReader = new RoleTypeReader(); - var roleResult = await roleReader.ReadAsync(ctx, inputRoleStr, _services); - if (!roleResult.IsSuccess) - { - Log.Warning("Role {0} not found.", inputRoleStr); - return null; - } - var role = (IRole)roleResult.BestMatch; - if (role.Position > ((IGuildUser)ctx.User).GetRoles().Select(r => r.Position).Max() - && ctx.User.Id != ctx.Guild.OwnerId) - return null; - var emote = x.Last().ToIEmote(); - return new { role, emote }; - }) - .Where(x => x != null); - - var all = await Task.WhenAll(results); - - if (!all.Any()) - return; - - foreach (var x in all) + var grp = 0; + var results = input + .GroupBy(x => grp++ / 2) + .Select(async x => { - try + var inputRoleStr = x.First(); + var roleReader = new RoleTypeReader(); + var roleResult = await roleReader.ReadAsync(ctx, inputRoleStr, _services); + if (!roleResult.IsSuccess) { - await target.AddReactionAsync(x.emote, new RequestOptions() - { - RetryMode = RetryMode.Retry502 | RetryMode.RetryRatelimit - }).ConfigureAwait(false); - } - catch (Discord.Net.HttpException ex) when(ex.HttpCode == HttpStatusCode.BadRequest) - { - await ReplyErrorLocalizedAsync(strs.reaction_cant_access(Format.Code(x.emote.ToString()))); - return; + Log.Warning("Role {0} not found.", inputRoleStr); + return null; } + var role = (IRole)roleResult.BestMatch; + if (role.Position > ((IGuildUser)ctx.User).GetRoles().Select(r => r.Position).Max() + && ctx.User.Id != ctx.Guild.OwnerId) + return null; + var emote = x.Last().ToIEmote(); + return new { role, emote }; + }) + .Where(x => x != null); - await Task.Delay(500).ConfigureAwait(false); + var all = await Task.WhenAll(results); + + if (!all.Any()) + return; + + foreach (var x in all) + { + try + { + await target.AddReactionAsync(x.emote, new RequestOptions() + { + RetryMode = RetryMode.Retry502 | RetryMode.RetryRatelimit + }).ConfigureAwait(false); + } + catch (Discord.Net.HttpException ex) when(ex.HttpCode == HttpStatusCode.BadRequest) + { + await ReplyErrorLocalizedAsync(strs.reaction_cant_access(Format.Code(x.emote.ToString()))); + return; } - if (_service.Add(ctx.Guild.Id, new ReactionRoleMessage() + await Task.Delay(500).ConfigureAwait(false); + } + + if (_service.Add(ctx.Guild.Id, new ReactionRoleMessage() { Exclusive = exclusive, MessageId = target.Id, @@ -97,267 +94,266 @@ namespace NadekoBot.Modules.Administration }; }).ToList(), })) - { - await ctx.OkAsync(); - } - else - { - await ReplyErrorLocalizedAsync(strs.reaction_roles_full).ConfigureAwait(false); - } + { + await ctx.OkAsync(); } + else + { + await ReplyErrorLocalizedAsync(strs.reaction_roles_full).ConfigureAwait(false); + } + } - [NadekoCommand, Aliases] - [RequireContext(ContextType.Guild)] - [NoPublicBot] - [UserPerm(GuildPerm.ManageRoles)] - [BotPerm(GuildPerm.ManageRoles)] - [Priority(0)] - public Task ReactionRoles(ulong messageId, params string[] input) => - InternalReactionRoles(false, messageId, input); + [NadekoCommand, Aliases] + [RequireContext(ContextType.Guild)] + [NoPublicBot] + [UserPerm(GuildPerm.ManageRoles)] + [BotPerm(GuildPerm.ManageRoles)] + [Priority(0)] + public Task ReactionRoles(ulong messageId, params string[] input) => + InternalReactionRoles(false, messageId, input); - [NadekoCommand, Aliases] - [RequireContext(ContextType.Guild)] - [NoPublicBot] - [UserPerm(GuildPerm.ManageRoles)] - [BotPerm(GuildPerm.ManageRoles)] - [Priority(1)] - public Task ReactionRoles(ulong messageId, Exclude _, params string[] input) => - InternalReactionRoles(true, messageId, input); + [NadekoCommand, Aliases] + [RequireContext(ContextType.Guild)] + [NoPublicBot] + [UserPerm(GuildPerm.ManageRoles)] + [BotPerm(GuildPerm.ManageRoles)] + [Priority(1)] + public Task ReactionRoles(ulong messageId, Exclude _, params string[] input) => + InternalReactionRoles(true, messageId, input); - [NadekoCommand, Aliases] - [RequireContext(ContextType.Guild)] - [NoPublicBot] - [UserPerm(GuildPerm.ManageRoles)] - [BotPerm(GuildPerm.ManageRoles)] - [Priority(0)] - public Task ReactionRoles(params string[] input) => - InternalReactionRoles(false, null, input); + [NadekoCommand, Aliases] + [RequireContext(ContextType.Guild)] + [NoPublicBot] + [UserPerm(GuildPerm.ManageRoles)] + [BotPerm(GuildPerm.ManageRoles)] + [Priority(0)] + public Task ReactionRoles(params string[] input) => + InternalReactionRoles(false, null, input); - [NadekoCommand, Aliases] - [RequireContext(ContextType.Guild)] - [NoPublicBot] - [UserPerm(GuildPerm.ManageRoles)] - [BotPerm(GuildPerm.ManageRoles)] - [Priority(1)] - public Task ReactionRoles(Exclude _, params string[] input) => - InternalReactionRoles(true, null, input); + [NadekoCommand, Aliases] + [RequireContext(ContextType.Guild)] + [NoPublicBot] + [UserPerm(GuildPerm.ManageRoles)] + [BotPerm(GuildPerm.ManageRoles)] + [Priority(1)] + public Task ReactionRoles(Exclude _, params string[] input) => + InternalReactionRoles(true, null, input); - [NadekoCommand, Aliases] - [RequireContext(ContextType.Guild)] - [NoPublicBot] - [UserPerm(GuildPerm.ManageRoles)] - public async Task ReactionRolesList() + [NadekoCommand, Aliases] + [RequireContext(ContextType.Guild)] + [NoPublicBot] + [UserPerm(GuildPerm.ManageRoles)] + public async Task ReactionRolesList() + { + var embed = _eb.Create() + .WithOkColor(); + if (!_service.Get(ctx.Guild.Id, out var rrs) || + !rrs.Any()) { - var embed = _eb.Create() - .WithOkColor(); - if (!_service.Get(ctx.Guild.Id, out var rrs) || - !rrs.Any()) + embed.WithDescription(GetText(strs.no_reaction_roles)); + } + else + { + var g = ((SocketGuild)ctx.Guild); + foreach (var rr in rrs) { - embed.WithDescription(GetText(strs.no_reaction_roles)); - } - else - { - var g = ((SocketGuild)ctx.Guild); - foreach (var rr in rrs) + var ch = g.GetTextChannel(rr.ChannelId); + IUserMessage msg = null; + if (ch is not null) { - var ch = g.GetTextChannel(rr.ChannelId); - IUserMessage msg = null; - if (ch is not null) - { - msg = await ch.GetMessageAsync(rr.MessageId).ConfigureAwait(false) as IUserMessage; - } - var content = msg?.Content.TrimTo(30) ?? "DELETED!"; - embed.AddField($"**{rr.Index + 1}.** {(ch?.Name ?? "DELETED!")}", - GetText(strs.reaction_roles_message(rr.ReactionRoles?.Count ?? 0, content))); + msg = await ch.GetMessageAsync(rr.MessageId).ConfigureAwait(false) as IUserMessage; } + var content = msg?.Content.TrimTo(30) ?? "DELETED!"; + embed.AddField($"**{rr.Index + 1}.** {(ch?.Name ?? "DELETED!")}", + GetText(strs.reaction_roles_message(rr.ReactionRoles?.Count ?? 0, content))); } - await ctx.Channel.EmbedAsync(embed).ConfigureAwait(false); } + await ctx.Channel.EmbedAsync(embed).ConfigureAwait(false); + } - [NadekoCommand, Aliases] - [RequireContext(ContextType.Guild)] - [NoPublicBot] - [UserPerm(GuildPerm.ManageRoles)] - public async Task ReactionRolesRemove(int index) + [NadekoCommand, Aliases] + [RequireContext(ContextType.Guild)] + [NoPublicBot] + [UserPerm(GuildPerm.ManageRoles)] + public async Task ReactionRolesRemove(int index) + { + if (index < 1 || + !_service.Get(ctx.Guild.Id, out var rrs) || + !rrs.Any() || rrs.Count < index) { - if (index < 1 || - !_service.Get(ctx.Guild.Id, out var rrs) || - !rrs.Any() || rrs.Count < index) + return; + } + index--; + var rr = rrs[index]; + _service.Remove(ctx.Guild.Id, index); + await ReplyConfirmLocalizedAsync(strs.reaction_role_removed(index + 1)); + } + + [NadekoCommand, Aliases] + [RequireContext(ContextType.Guild)] + [UserPerm(GuildPerm.ManageRoles)] + [BotPerm(GuildPerm.ManageRoles)] + public async Task SetRole(IGuildUser targetUser, [Leftover] IRole roleToAdd) + { + var runnerUser = (IGuildUser)ctx.User; + var runnerMaxRolePosition = runnerUser.GetRoles().Max(x => x.Position); + if ((ctx.User.Id != ctx.Guild.OwnerId) && runnerMaxRolePosition <= roleToAdd.Position) + return; + try + { + await targetUser.AddRoleAsync(roleToAdd).ConfigureAwait(false); + + await ReplyConfirmLocalizedAsync( + strs.setrole(Format.Bold(roleToAdd.Name), + Format.Bold(targetUser.ToString()))); + } + catch (Exception ex) + { + Log.Warning(ex, "Error in setrole command"); + await ReplyErrorLocalizedAsync(strs.setrole_err).ConfigureAwait(false); + } + } + + [NadekoCommand, Aliases] + [RequireContext(ContextType.Guild)] + [UserPerm(GuildPerm.ManageRoles)] + [BotPerm(GuildPerm.ManageRoles)] + public async Task RemoveRole(IGuildUser targetUser, [Leftover] IRole roleToRemove) + { + var runnerUser = (IGuildUser)ctx.User; + if (ctx.User.Id != runnerUser.Guild.OwnerId && runnerUser.GetRoles().Max(x => x.Position) <= roleToRemove.Position) + return; + try + { + await targetUser.RemoveRoleAsync(roleToRemove).ConfigureAwait(false); + await ReplyConfirmLocalizedAsync(strs.remrole(Format.Bold(roleToRemove.Name), Format.Bold(targetUser.ToString()))).ConfigureAwait(false); + } + catch + { + await ReplyErrorLocalizedAsync(strs.remrole_err).ConfigureAwait(false); + } + } + + [NadekoCommand, Aliases] + [RequireContext(ContextType.Guild)] + [UserPerm(GuildPerm.ManageRoles)] + [BotPerm(GuildPerm.ManageRoles)] + public async Task RenameRole(IRole roleToEdit, [Leftover]string newname) + { + var guser = (IGuildUser)ctx.User; + if (ctx.User.Id != guser.Guild.OwnerId && guser.GetRoles().Max(x => x.Position) <= roleToEdit.Position) + return; + try + { + if (roleToEdit.Position > (await ctx.Guild.GetCurrentUserAsync().ConfigureAwait(false)).GetRoles().Max(r => r.Position)) { + await ReplyErrorLocalizedAsync(strs.renrole_perms).ConfigureAwait(false); return; } - index--; - var rr = rrs[index]; - _service.Remove(ctx.Guild.Id, index); - await ReplyConfirmLocalizedAsync(strs.reaction_role_removed(index + 1)); + await roleToEdit.ModifyAsync(g => g.Name = newname).ConfigureAwait(false); + await ReplyConfirmLocalizedAsync(strs.renrole).ConfigureAwait(false); } - - [NadekoCommand, Aliases] - [RequireContext(ContextType.Guild)] - [UserPerm(GuildPerm.ManageRoles)] - [BotPerm(GuildPerm.ManageRoles)] - public async Task SetRole(IGuildUser targetUser, [Leftover] IRole roleToAdd) + catch (Exception) { - var runnerUser = (IGuildUser)ctx.User; - var runnerMaxRolePosition = runnerUser.GetRoles().Max(x => x.Position); - if ((ctx.User.Id != ctx.Guild.OwnerId) && runnerMaxRolePosition <= roleToAdd.Position) - return; - try - { - await targetUser.AddRoleAsync(roleToAdd).ConfigureAwait(false); - - await ReplyConfirmLocalizedAsync( - strs.setrole(Format.Bold(roleToAdd.Name), - Format.Bold(targetUser.ToString()))); - } - catch (Exception ex) - { - Log.Warning(ex, "Error in setrole command"); - await ReplyErrorLocalizedAsync(strs.setrole_err).ConfigureAwait(false); - } + await ReplyErrorLocalizedAsync(strs.renrole_err).ConfigureAwait(false); } + } - [NadekoCommand, Aliases] - [RequireContext(ContextType.Guild)] - [UserPerm(GuildPerm.ManageRoles)] - [BotPerm(GuildPerm.ManageRoles)] - public async Task RemoveRole(IGuildUser targetUser, [Leftover] IRole roleToRemove) - { - var runnerUser = (IGuildUser)ctx.User; - if (ctx.User.Id != runnerUser.Guild.OwnerId && runnerUser.GetRoles().Max(x => x.Position) <= roleToRemove.Position) - return; - try - { - await targetUser.RemoveRoleAsync(roleToRemove).ConfigureAwait(false); - await ReplyConfirmLocalizedAsync(strs.remrole(Format.Bold(roleToRemove.Name), Format.Bold(targetUser.ToString()))).ConfigureAwait(false); - } - catch - { - await ReplyErrorLocalizedAsync(strs.remrole_err).ConfigureAwait(false); - } - } + [NadekoCommand, Aliases] + [RequireContext(ContextType.Guild)] + [UserPerm(GuildPerm.ManageRoles)] + [BotPerm(GuildPerm.ManageRoles)] + public async Task RemoveAllRoles([Leftover] IGuildUser user) + { + var guser = (IGuildUser)ctx.User; - [NadekoCommand, Aliases] - [RequireContext(ContextType.Guild)] - [UserPerm(GuildPerm.ManageRoles)] - [BotPerm(GuildPerm.ManageRoles)] - public async Task RenameRole(IRole roleToEdit, [Leftover]string newname) - { - var guser = (IGuildUser)ctx.User; - if (ctx.User.Id != guser.Guild.OwnerId && guser.GetRoles().Max(x => x.Position) <= roleToEdit.Position) - return; - try - { - if (roleToEdit.Position > (await ctx.Guild.GetCurrentUserAsync().ConfigureAwait(false)).GetRoles().Max(r => r.Position)) - { - await ReplyErrorLocalizedAsync(strs.renrole_perms).ConfigureAwait(false); - return; - } - await roleToEdit.ModifyAsync(g => g.Name = newname).ConfigureAwait(false); - await ReplyConfirmLocalizedAsync(strs.renrole).ConfigureAwait(false); - } - catch (Exception) - { - await ReplyErrorLocalizedAsync(strs.renrole_err).ConfigureAwait(false); - } - } - - [NadekoCommand, Aliases] - [RequireContext(ContextType.Guild)] - [UserPerm(GuildPerm.ManageRoles)] - [BotPerm(GuildPerm.ManageRoles)] - public async Task RemoveAllRoles([Leftover] IGuildUser user) - { - var guser = (IGuildUser)ctx.User; - - var userRoles = user.GetRoles() - .Where(x => !x.IsManaged && x != x.Guild.EveryoneRole) - .ToList(); + var userRoles = user.GetRoles() + .Where(x => !x.IsManaged && x != x.Guild.EveryoneRole) + .ToList(); - if (user.Id == ctx.Guild.OwnerId || (ctx.User.Id != ctx.Guild.OwnerId && guser.GetRoles().Max(x => x.Position) <= userRoles.Max(x => x.Position))) - return; - try - { - await user.RemoveRolesAsync(userRoles).ConfigureAwait(false); - await ReplyConfirmLocalizedAsync(strs.rar(Format.Bold(user.ToString()))).ConfigureAwait(false); - } - catch (Exception) - { - await ReplyErrorLocalizedAsync(strs.rar_err).ConfigureAwait(false); - } - } - - [NadekoCommand, Aliases] - [RequireContext(ContextType.Guild)] - [UserPerm(GuildPerm.ManageRoles)] - [BotPerm(GuildPerm.ManageRoles)] - public async Task CreateRole([Leftover] string roleName = null) + if (user.Id == ctx.Guild.OwnerId || (ctx.User.Id != ctx.Guild.OwnerId && guser.GetRoles().Max(x => x.Position) <= userRoles.Max(x => x.Position))) + return; + try { - if (string.IsNullOrWhiteSpace(roleName)) - return; - - var r = await ctx.Guild.CreateRoleAsync(roleName, isMentionable: false).ConfigureAwait(false); - await ReplyConfirmLocalizedAsync(strs.cr(Format.Bold(r.Name))).ConfigureAwait(false); + await user.RemoveRolesAsync(userRoles).ConfigureAwait(false); + await ReplyConfirmLocalizedAsync(strs.rar(Format.Bold(user.ToString()))).ConfigureAwait(false); } - - [NadekoCommand, Aliases] - [RequireContext(ContextType.Guild)] - [UserPerm(GuildPerm.ManageRoles)] - [BotPerm(GuildPerm.ManageRoles)] - public async Task DeleteRole([Leftover] IRole role) + catch (Exception) { - var guser = (IGuildUser)ctx.User; - if (ctx.User.Id != guser.Guild.OwnerId - && guser.GetRoles().Max(x => x.Position) <= role.Position) - return; - - await role.DeleteAsync().ConfigureAwait(false); - await ReplyConfirmLocalizedAsync(strs.dr(Format.Bold(role.Name))).ConfigureAwait(false); + await ReplyErrorLocalizedAsync(strs.rar_err).ConfigureAwait(false); } + } - [NadekoCommand, Aliases] - [RequireContext(ContextType.Guild)] - [UserPerm(GuildPerm.ManageRoles)] - [BotPerm(GuildPerm.ManageRoles)] - public async Task RoleHoist(IRole role) + [NadekoCommand, Aliases] + [RequireContext(ContextType.Guild)] + [UserPerm(GuildPerm.ManageRoles)] + [BotPerm(GuildPerm.ManageRoles)] + public async Task CreateRole([Leftover] string roleName = null) + { + if (string.IsNullOrWhiteSpace(roleName)) + return; + + var r = await ctx.Guild.CreateRoleAsync(roleName, isMentionable: false).ConfigureAwait(false); + await ReplyConfirmLocalizedAsync(strs.cr(Format.Bold(r.Name))).ConfigureAwait(false); + } + + [NadekoCommand, Aliases] + [RequireContext(ContextType.Guild)] + [UserPerm(GuildPerm.ManageRoles)] + [BotPerm(GuildPerm.ManageRoles)] + public async Task DeleteRole([Leftover] IRole role) + { + var guser = (IGuildUser)ctx.User; + if (ctx.User.Id != guser.Guild.OwnerId + && guser.GetRoles().Max(x => x.Position) <= role.Position) + return; + + await role.DeleteAsync().ConfigureAwait(false); + await ReplyConfirmLocalizedAsync(strs.dr(Format.Bold(role.Name))).ConfigureAwait(false); + } + + [NadekoCommand, Aliases] + [RequireContext(ContextType.Guild)] + [UserPerm(GuildPerm.ManageRoles)] + [BotPerm(GuildPerm.ManageRoles)] + public async Task RoleHoist(IRole role) + { + var newHoisted = !role.IsHoisted; + await role.ModifyAsync(r => r.Hoist = newHoisted).ConfigureAwait(false); + if (newHoisted) { - var newHoisted = !role.IsHoisted; - await role.ModifyAsync(r => r.Hoist = newHoisted).ConfigureAwait(false); - if (newHoisted) - { - await ReplyConfirmLocalizedAsync(strs.rolehoist_enabled(Format.Bold(role.Name))).ConfigureAwait(false); - } - else - { - await ReplyConfirmLocalizedAsync(strs.rolehoist_disabled(Format.Bold(role.Name))).ConfigureAwait(false); - } + await ReplyConfirmLocalizedAsync(strs.rolehoist_enabled(Format.Bold(role.Name))).ConfigureAwait(false); } - - [NadekoCommand, Aliases] - [RequireContext(ContextType.Guild)] - [Priority(1)] - public async Task RoleColor([Leftover] IRole role) + else { - await SendConfirmAsync("Role Color", role.Color.RawValue.ToString("x6")).ConfigureAwait(false); + await ReplyConfirmLocalizedAsync(strs.rolehoist_disabled(Format.Bold(role.Name))).ConfigureAwait(false); } + } - [NadekoCommand, Aliases] - [RequireContext(ContextType.Guild)] - [UserPerm(GuildPerm.ManageRoles)] - [BotPerm(GuildPerm.ManageRoles)] - [Priority(0)] - public async Task RoleColor(SixLabors.ImageSharp.Color color, [Leftover]IRole role) + [NadekoCommand, Aliases] + [RequireContext(ContextType.Guild)] + [Priority(1)] + public async Task RoleColor([Leftover] IRole role) + { + await SendConfirmAsync("Role Color", role.Color.RawValue.ToString("x6")).ConfigureAwait(false); + } + + [NadekoCommand, Aliases] + [RequireContext(ContextType.Guild)] + [UserPerm(GuildPerm.ManageRoles)] + [BotPerm(GuildPerm.ManageRoles)] + [Priority(0)] + public async Task RoleColor(SixLabors.ImageSharp.Color color, [Leftover]IRole role) + { + try { - try - { - var rgba32 = color.ToPixel(); - await role.ModifyAsync(r => r.Color = new Color(rgba32.R, rgba32.G, rgba32.B)).ConfigureAwait(false); - await ReplyConfirmLocalizedAsync(strs.rc(Format.Bold(role.Name))).ConfigureAwait(false); - } - catch (Exception) - { - await ReplyErrorLocalizedAsync(strs.rc_perms).ConfigureAwait(false); - } + var rgba32 = color.ToPixel(); + await role.ModifyAsync(r => r.Color = new Color(rgba32.R, rgba32.G, rgba32.B)).ConfigureAwait(false); + await ReplyConfirmLocalizedAsync(strs.rc(Format.Bold(role.Name))).ConfigureAwait(false); + } + catch (Exception) + { + await ReplyErrorLocalizedAsync(strs.rc_perms).ConfigureAwait(false); } } } diff --git a/src/NadekoBot/Modules/Administration/SelfAssignedRolesCommands.cs b/src/NadekoBot/Modules/Administration/SelfAssignedRolesCommands.cs index dcb056bf1..28d18de67 100644 --- a/src/NadekoBot/Modules/Administration/SelfAssignedRolesCommands.cs +++ b/src/NadekoBot/Modules/Administration/SelfAssignedRolesCommands.cs @@ -1,5 +1,4 @@ -using System.Linq; -using System.Text; +using System.Text; using System.Threading.Tasks; using Discord; using Discord.Commands; @@ -7,265 +6,264 @@ using NadekoBot.Common.Attributes; using NadekoBot.Modules.Administration.Services; using NadekoBot.Extensions; -namespace NadekoBot.Modules.Administration +namespace NadekoBot.Modules.Administration; + +public partial class Administration { - public partial class Administration + [Group] + public class SelfAssignedRolesCommands : NadekoSubmodule { - [Group] - public class SelfAssignedRolesCommands : NadekoSubmodule + [NadekoCommand, Aliases] + [RequireContext(ContextType.Guild)] + [UserPerm(GuildPerm.ManageMessages)] + [BotPerm(GuildPerm.ManageMessages)] + public async Task AdSarm() { - [NadekoCommand, Aliases] - [RequireContext(ContextType.Guild)] - [UserPerm(GuildPerm.ManageMessages)] - [BotPerm(GuildPerm.ManageMessages)] - public async Task AdSarm() - { - var newVal = _service.ToggleAdSarm(ctx.Guild.Id); + var newVal = _service.ToggleAdSarm(ctx.Guild.Id); - if (newVal) - { - await ReplyConfirmLocalizedAsync(strs.adsarm_enable(Prefix)); - } - else - { - await ReplyConfirmLocalizedAsync(strs.adsarm_disable(Prefix)); - } + if (newVal) + { + await ReplyConfirmLocalizedAsync(strs.adsarm_enable(Prefix)); } - - [NadekoCommand, Aliases] - [RequireContext(ContextType.Guild)] - [UserPerm(GuildPerm.ManageRoles)] - [BotPerm(GuildPerm.ManageRoles)] - [Priority(1)] - public Task Asar([Leftover] IRole role) => - Asar(0, role); - - [NadekoCommand, Aliases] - [RequireContext(ContextType.Guild)] - [UserPerm(GuildPerm.ManageRoles)] - [BotPerm(GuildPerm.ManageRoles)] - [Priority(0)] - public async Task Asar(int group, [Leftover] IRole role) + else { - var guser = (IGuildUser)ctx.User; - if (ctx.User.Id != guser.Guild.OwnerId && guser.GetRoles().Max(x => x.Position) <= role.Position) - return; - - var succ = _service.AddNew(ctx.Guild.Id, role, group); - - if (succ) - { - await ReplyConfirmLocalizedAsync(strs.role_added(Format.Bold(role.Name), Format.Bold(group.ToString()))).ConfigureAwait(false); - } - else - { - await ReplyErrorLocalizedAsync(strs.role_in_list(Format.Bold(role.Name))).ConfigureAwait(false); - } + await ReplyConfirmLocalizedAsync(strs.adsarm_disable(Prefix)); } + } - [NadekoCommand, Aliases] - [RequireContext(ContextType.Guild)] - [UserPerm(GuildPerm.ManageRoles)] - [BotPerm(GuildPerm.ManageRoles)] - [Priority(0)] - public async Task Sargn(int group, [Leftover] string name = null) + [NadekoCommand, Aliases] + [RequireContext(ContextType.Guild)] + [UserPerm(GuildPerm.ManageRoles)] + [BotPerm(GuildPerm.ManageRoles)] + [Priority(1)] + public Task Asar([Leftover] IRole role) => + Asar(0, role); + + [NadekoCommand, Aliases] + [RequireContext(ContextType.Guild)] + [UserPerm(GuildPerm.ManageRoles)] + [BotPerm(GuildPerm.ManageRoles)] + [Priority(0)] + public async Task Asar(int group, [Leftover] IRole role) + { + var guser = (IGuildUser)ctx.User; + if (ctx.User.Id != guser.Guild.OwnerId && guser.GetRoles().Max(x => x.Position) <= role.Position) + return; + + var succ = _service.AddNew(ctx.Guild.Id, role, group); + + if (succ) { - var guser = (IGuildUser)ctx.User; - - var set = await _service.SetNameAsync(ctx.Guild.Id, group, name).ConfigureAwait(false); - - if (set) - { - await ReplyConfirmLocalizedAsync(strs.group_name_added(Format.Bold(group.ToString()), Format.Bold(name.ToString()))).ConfigureAwait(false); - } - else - { - await ReplyConfirmLocalizedAsync(strs.group_name_removed(Format.Bold(group.ToString()))).ConfigureAwait(false); - } + await ReplyConfirmLocalizedAsync(strs.role_added(Format.Bold(role.Name), Format.Bold(group.ToString()))).ConfigureAwait(false); } - - [NadekoCommand, Aliases] - [RequireContext(ContextType.Guild)] - [UserPerm(GuildPerm.ManageRoles)] - public async Task Rsar([Leftover] IRole role) + else { - var guser = (IGuildUser)ctx.User; - if (ctx.User.Id != guser.Guild.OwnerId && guser.GetRoles().Max(x => x.Position) <= role.Position) - return; - - bool success = _service.RemoveSar(role.Guild.Id, role.Id); - if (!success) - { - await ReplyErrorLocalizedAsync(strs.self_assign_not).ConfigureAwait(false); - } - else - { - await ReplyConfirmLocalizedAsync(strs.self_assign_rem(Format.Bold(role.Name))).ConfigureAwait(false); - } + await ReplyErrorLocalizedAsync(strs.role_in_list(Format.Bold(role.Name))).ConfigureAwait(false); } + } - [NadekoCommand, Aliases] - [RequireContext(ContextType.Guild)] - public async Task Lsar(int page = 1) + [NadekoCommand, Aliases] + [RequireContext(ContextType.Guild)] + [UserPerm(GuildPerm.ManageRoles)] + [BotPerm(GuildPerm.ManageRoles)] + [Priority(0)] + public async Task Sargn(int group, [Leftover] string name = null) + { + var guser = (IGuildUser)ctx.User; + + var set = await _service.SetNameAsync(ctx.Guild.Id, group, name).ConfigureAwait(false); + + if (set) { - if (--page < 0) - return; + await ReplyConfirmLocalizedAsync(strs.group_name_added(Format.Bold(group.ToString()), Format.Bold(name.ToString()))).ConfigureAwait(false); + } + else + { + await ReplyConfirmLocalizedAsync(strs.group_name_removed(Format.Bold(group.ToString()))).ConfigureAwait(false); + } + } - var (exclusive, roles, groups) = _service.GetRoles(ctx.Guild); + [NadekoCommand, Aliases] + [RequireContext(ContextType.Guild)] + [UserPerm(GuildPerm.ManageRoles)] + public async Task Rsar([Leftover] IRole role) + { + var guser = (IGuildUser)ctx.User; + if (ctx.User.Id != guser.Guild.OwnerId && guser.GetRoles().Max(x => x.Position) <= role.Position) + return; - await ctx.SendPaginatedConfirmAsync(page, (cur) => + bool success = _service.RemoveSar(role.Guild.Id, role.Id); + if (!success) + { + await ReplyErrorLocalizedAsync(strs.self_assign_not).ConfigureAwait(false); + } + else + { + await ReplyConfirmLocalizedAsync(strs.self_assign_rem(Format.Bold(role.Name))).ConfigureAwait(false); + } + } + + [NadekoCommand, Aliases] + [RequireContext(ContextType.Guild)] + public async Task Lsar(int page = 1) + { + if (--page < 0) + return; + + var (exclusive, roles, groups) = _service.GetRoles(ctx.Guild); + + await ctx.SendPaginatedConfirmAsync(page, (cur) => + { + var rolesStr = new StringBuilder(); + var roleGroups = roles + .OrderBy(x => x.Model.Group) + .Skip(cur * 20) + .Take(20) + .GroupBy(x => x.Model.Group) + .OrderBy(x => x.Key); + + foreach (var kvp in roleGroups) { - var rolesStr = new StringBuilder(); - var roleGroups = roles - .OrderBy(x => x.Model.Group) - .Skip(cur * 20) - .Take(20) - .GroupBy(x => x.Model.Group) - .OrderBy(x => x.Key); - - foreach (var kvp in roleGroups) + var groupNameText = ""; + if (!groups.TryGetValue(kvp.Key, out var name)) { - var groupNameText = ""; - if (!groups.TryGetValue(kvp.Key, out var name)) + groupNameText = Format.Bold(GetText(strs.self_assign_group(kvp.Key))); + } + else + { + groupNameText = Format.Bold($"{kvp.Key} - {name.TrimTo(25, true)}"); + } + + rolesStr.AppendLine("\t\t\t\t ⟪" + groupNameText + "⟫"); + foreach (var (Model, Role) in kvp.AsEnumerable()) + { + if (Role is null) { - groupNameText = Format.Bold(GetText(strs.self_assign_group(kvp.Key))); + continue; } else { - groupNameText = Format.Bold($"{kvp.Key} - {name.TrimTo(25, true)}"); - } - - rolesStr.AppendLine("\t\t\t\t ⟪" + groupNameText + "⟫"); - foreach (var (Model, Role) in kvp.AsEnumerable()) - { - if (Role is null) - { - continue; - } + // first character is invisible space + if (Model.LevelRequirement == 0) + rolesStr.AppendLine("‌‌ " + Role.Name); else - { - // first character is invisible space - if (Model.LevelRequirement == 0) - rolesStr.AppendLine("‌‌ " + Role.Name); - else - rolesStr.AppendLine("‌‌ " + Role.Name + $" (lvl {Model.LevelRequirement}+)"); - } + rolesStr.AppendLine("‌‌ " + Role.Name + $" (lvl {Model.LevelRequirement}+)"); } - rolesStr.AppendLine(); } + rolesStr.AppendLine(); + } - return _eb.Create().WithOkColor() - .WithTitle(Format.Bold(GetText(strs.self_assign_list(roles.Count())))) - .WithDescription(rolesStr.ToString()) - .WithFooter(exclusive - ? GetText(strs.self_assign_are_exclusive) - : GetText(strs.self_assign_are_not_exclusive)); - }, roles.Count(), 20).ConfigureAwait(false); + return _eb.Create().WithOkColor() + .WithTitle(Format.Bold(GetText(strs.self_assign_list(roles.Count())))) + .WithDescription(rolesStr.ToString()) + .WithFooter(exclusive + ? GetText(strs.self_assign_are_exclusive) + : GetText(strs.self_assign_are_not_exclusive)); + }, roles.Count(), 20).ConfigureAwait(false); + } + + [NadekoCommand, Aliases] + [RequireContext(ContextType.Guild)] + [UserPerm(GuildPerm.ManageRoles)] + [BotPerm(GuildPerm.ManageRoles)] + public async Task Togglexclsar() + { + bool areExclusive = _service.ToggleEsar(ctx.Guild.Id); + if (areExclusive) + await ReplyConfirmLocalizedAsync(strs.self_assign_excl).ConfigureAwait(false); + else + await ReplyConfirmLocalizedAsync(strs.self_assign_no_excl).ConfigureAwait(false); + } + + [NadekoCommand, Aliases] + [RequireContext(ContextType.Guild)] + [UserPerm(GuildPerm.ManageRoles)] + [BotPerm(GuildPerm.ManageRoles)] + public async Task RoleLevelReq(int level, [Leftover] IRole role) + { + if (level < 0) + return; + + bool succ = _service.SetLevelReq(ctx.Guild.Id, role, level); + + if (!succ) + { + await ReplyErrorLocalizedAsync(strs.self_assign_not).ConfigureAwait(false); + return; } - [NadekoCommand, Aliases] - [RequireContext(ContextType.Guild)] - [UserPerm(GuildPerm.ManageRoles)] - [BotPerm(GuildPerm.ManageRoles)] - public async Task Togglexclsar() + await ReplyConfirmLocalizedAsync(strs.self_assign_level_req( + Format.Bold(role.Name), + Format.Bold(level.ToString()))); + } + + [NadekoCommand, Aliases] + [RequireContext(ContextType.Guild)] + public async Task Iam([Leftover] IRole role) + { + var guildUser = (IGuildUser)ctx.User; + + var (result, autoDelete, extra) = await _service.Assign(guildUser, role).ConfigureAwait(false); + + IUserMessage msg; + if (result == SelfAssignedRolesService.AssignResult.Err_Not_Assignable) { - bool areExclusive = _service.ToggleEsar(ctx.Guild.Id); - if (areExclusive) - await ReplyConfirmLocalizedAsync(strs.self_assign_excl).ConfigureAwait(false); - else - await ReplyConfirmLocalizedAsync(strs.self_assign_no_excl).ConfigureAwait(false); + msg = await ReplyErrorLocalizedAsync(strs.self_assign_not).ConfigureAwait(false); + } + else if (result == SelfAssignedRolesService.AssignResult.Err_Lvl_Req) + { + msg = await ReplyErrorLocalizedAsync(strs.self_assign_not_level(Format.Bold(extra.ToString()))).ConfigureAwait(false); + } + else if (result == SelfAssignedRolesService.AssignResult.Err_Already_Have) + { + msg = await ReplyErrorLocalizedAsync(strs.self_assign_already(Format.Bold(role.Name))).ConfigureAwait(false); + } + else if (result == SelfAssignedRolesService.AssignResult.Err_Not_Perms) + { + msg = await ReplyErrorLocalizedAsync(strs.self_assign_perms).ConfigureAwait(false); + } + else + { + msg = await ReplyConfirmLocalizedAsync(strs.self_assign_success(Format.Bold(role.Name))).ConfigureAwait(false); } - [NadekoCommand, Aliases] - [RequireContext(ContextType.Guild)] - [UserPerm(GuildPerm.ManageRoles)] - [BotPerm(GuildPerm.ManageRoles)] - public async Task RoleLevelReq(int level, [Leftover] IRole role) + if (autoDelete) { - if (level < 0) - return; + msg.DeleteAfter(3); + ctx.Message.DeleteAfter(3); + } + } - bool succ = _service.SetLevelReq(ctx.Guild.Id, role, level); + [NadekoCommand, Aliases] + [RequireContext(ContextType.Guild)] + public async Task Iamnot([Leftover] IRole role) + { + var guildUser = (IGuildUser)ctx.User; - if (!succ) - { - await ReplyErrorLocalizedAsync(strs.self_assign_not).ConfigureAwait(false); - return; - } + var (result, autoDelete) = await _service.Remove(guildUser, role).ConfigureAwait(false); - await ReplyConfirmLocalizedAsync(strs.self_assign_level_req( - Format.Bold(role.Name), - Format.Bold(level.ToString()))); + IUserMessage msg; + if (result == SelfAssignedRolesService.RemoveResult.Err_Not_Assignable) + { + msg = await ReplyErrorLocalizedAsync(strs.self_assign_not).ConfigureAwait(false); + } + else if (result == SelfAssignedRolesService.RemoveResult.Err_Not_Have) + { + msg = await ReplyErrorLocalizedAsync(strs.self_assign_not_have(Format.Bold(role.Name))).ConfigureAwait(false); + } + else if (result == SelfAssignedRolesService.RemoveResult.Err_Not_Perms) + { + msg = await ReplyErrorLocalizedAsync(strs.self_assign_perms).ConfigureAwait(false); + } + else + { + msg = await ReplyConfirmLocalizedAsync(strs.self_assign_remove(Format.Bold(role.Name))).ConfigureAwait(false); } - [NadekoCommand, Aliases] - [RequireContext(ContextType.Guild)] - public async Task Iam([Leftover] IRole role) + if (autoDelete) { - var guildUser = (IGuildUser)ctx.User; - - var (result, autoDelete, extra) = await _service.Assign(guildUser, role).ConfigureAwait(false); - - IUserMessage msg; - if (result == SelfAssignedRolesService.AssignResult.Err_Not_Assignable) - { - msg = await ReplyErrorLocalizedAsync(strs.self_assign_not).ConfigureAwait(false); - } - else if (result == SelfAssignedRolesService.AssignResult.Err_Lvl_Req) - { - msg = await ReplyErrorLocalizedAsync(strs.self_assign_not_level(Format.Bold(extra.ToString()))).ConfigureAwait(false); - } - else if (result == SelfAssignedRolesService.AssignResult.Err_Already_Have) - { - msg = await ReplyErrorLocalizedAsync(strs.self_assign_already(Format.Bold(role.Name))).ConfigureAwait(false); - } - else if (result == SelfAssignedRolesService.AssignResult.Err_Not_Perms) - { - msg = await ReplyErrorLocalizedAsync(strs.self_assign_perms).ConfigureAwait(false); - } - else - { - msg = await ReplyConfirmLocalizedAsync(strs.self_assign_success(Format.Bold(role.Name))).ConfigureAwait(false); - } - - if (autoDelete) - { - msg.DeleteAfter(3); - ctx.Message.DeleteAfter(3); - } - } - - [NadekoCommand, Aliases] - [RequireContext(ContextType.Guild)] - public async Task Iamnot([Leftover] IRole role) - { - var guildUser = (IGuildUser)ctx.User; - - var (result, autoDelete) = await _service.Remove(guildUser, role).ConfigureAwait(false); - - IUserMessage msg; - if (result == SelfAssignedRolesService.RemoveResult.Err_Not_Assignable) - { - msg = await ReplyErrorLocalizedAsync(strs.self_assign_not).ConfigureAwait(false); - } - else if (result == SelfAssignedRolesService.RemoveResult.Err_Not_Have) - { - msg = await ReplyErrorLocalizedAsync(strs.self_assign_not_have(Format.Bold(role.Name))).ConfigureAwait(false); - } - else if (result == SelfAssignedRolesService.RemoveResult.Err_Not_Perms) - { - msg = await ReplyErrorLocalizedAsync(strs.self_assign_perms).ConfigureAwait(false); - } - else - { - msg = await ReplyConfirmLocalizedAsync(strs.self_assign_remove(Format.Bold(role.Name))).ConfigureAwait(false); - } - - if (autoDelete) - { - msg.DeleteAfter(3); - ctx.Message.DeleteAfter(3); - } + msg.DeleteAfter(3); + ctx.Message.DeleteAfter(3); } } } diff --git a/src/NadekoBot/Modules/Administration/SelfCommands.cs b/src/NadekoBot/Modules/Administration/SelfCommands.cs index fa495702e..0ad7b3121 100644 --- a/src/NadekoBot/Modules/Administration/SelfCommands.cs +++ b/src/NadekoBot/Modules/Administration/SelfCommands.cs @@ -2,118 +2,114 @@ using Discord; using Discord.Commands; using Discord.Net; using Discord.WebSocket; -using NadekoBot.Common; using NadekoBot.Common.Attributes; using NadekoBot.Common.Replacements; using NadekoBot.Services.Database.Models; using NadekoBot.Extensions; using NadekoBot.Modules.Administration.Services; -using System; -using System.Linq; using System.Threading.Tasks; using NadekoBot.Services; -using Serilog; -namespace NadekoBot.Modules.Administration +namespace NadekoBot.Modules.Administration; + +public partial class Administration { - public partial class Administration + [Group] + public class SelfCommands : NadekoSubmodule { - [Group] - public class SelfCommands : NadekoSubmodule + private readonly DiscordSocketClient _client; + private readonly IBotStrings _strings; + private readonly ICoordinator _coord; + + public SelfCommands(DiscordSocketClient client, IBotStrings strings, ICoordinator coord) { - private readonly DiscordSocketClient _client; - private readonly IBotStrings _strings; - private readonly ICoordinator _coord; + _client = client; + _strings = strings; + _coord = coord; + } - public SelfCommands(DiscordSocketClient client, IBotStrings strings, ICoordinator coord) + [NadekoCommand, Aliases] + [RequireContext(ContextType.Guild)] + [UserPerm(GuildPerm.Administrator)] + [OwnerOnly] + public async Task StartupCommandAdd([Leftover] string cmdText) + { + if (cmdText.StartsWith(Prefix + "die", StringComparison.InvariantCulture)) + return; + + var guser = (IGuildUser)ctx.User; + var cmd = new AutoCommand() { - _client = client; - _strings = strings; - _coord = coord; - } + CommandText = cmdText, + ChannelId = ctx.Channel.Id, + ChannelName = ctx.Channel.Name, + GuildId = ctx.Guild?.Id, + GuildName = ctx.Guild?.Name, + VoiceChannelId = guser.VoiceChannel?.Id, + VoiceChannelName = guser.VoiceChannel?.Name, + Interval = 0, + }; + _service.AddNewAutoCommand(cmd); - [NadekoCommand, Aliases] - [RequireContext(ContextType.Guild)] - [UserPerm(GuildPerm.Administrator)] - [OwnerOnly] - public async Task StartupCommandAdd([Leftover] string cmdText) + await ctx.Channel.EmbedAsync(_eb.Create().WithOkColor() + .WithTitle(GetText(strs.scadd)) + .AddField(GetText(strs.server), cmd.GuildId is null ? $"-" : $"{cmd.GuildName}/{cmd.GuildId}", true) + .AddField(GetText(strs.channel), $"{cmd.ChannelName}/{cmd.ChannelId}", true) + .AddField(GetText(strs.command_text), cmdText, false)); + } + + [NadekoCommand, Aliases] + [RequireContext(ContextType.Guild)] + [UserPerm(GuildPerm.Administrator)] + [OwnerOnly] + public async Task AutoCommandAdd(int interval, [Leftover] string cmdText) + { + if (cmdText.StartsWith(Prefix + "die", StringComparison.InvariantCulture)) + return; + + if (interval < 5) + return; + + var guser = (IGuildUser)ctx.User; + var cmd = new AutoCommand() { - if (cmdText.StartsWith(Prefix + "die", StringComparison.InvariantCulture)) - return; + CommandText = cmdText, + ChannelId = ctx.Channel.Id, + ChannelName = ctx.Channel.Name, + GuildId = ctx.Guild?.Id, + GuildName = ctx.Guild?.Name, + VoiceChannelId = guser.VoiceChannel?.Id, + VoiceChannelName = guser.VoiceChannel?.Name, + Interval = interval, + }; + _service.AddNewAutoCommand(cmd); - var guser = (IGuildUser)ctx.User; - var cmd = new AutoCommand() - { - CommandText = cmdText, - ChannelId = ctx.Channel.Id, - ChannelName = ctx.Channel.Name, - GuildId = ctx.Guild?.Id, - GuildName = ctx.Guild?.Name, - VoiceChannelId = guser.VoiceChannel?.Id, - VoiceChannelName = guser.VoiceChannel?.Name, - Interval = 0, - }; - _service.AddNewAutoCommand(cmd); + await ReplyConfirmLocalizedAsync(strs.autocmd_add(Format.Code(Format.Sanitize(cmdText)), cmd.Interval)); + } - await ctx.Channel.EmbedAsync(_eb.Create().WithOkColor() - .WithTitle(GetText(strs.scadd)) - .AddField(GetText(strs.server), cmd.GuildId is null ? $"-" : $"{cmd.GuildName}/{cmd.GuildId}", true) - .AddField(GetText(strs.channel), $"{cmd.ChannelName}/{cmd.ChannelId}", true) - .AddField(GetText(strs.command_text), cmdText, false)); - } + [NadekoCommand, Aliases] + [RequireContext(ContextType.Guild)] + [OwnerOnly] + public async Task StartupCommandsList(int page = 1) + { + if (page-- < 1) + return; - [NadekoCommand, Aliases] - [RequireContext(ContextType.Guild)] - [UserPerm(GuildPerm.Administrator)] - [OwnerOnly] - public async Task AutoCommandAdd(int interval, [Leftover] string cmdText) - { - if (cmdText.StartsWith(Prefix + "die", StringComparison.InvariantCulture)) - return; - - if (interval < 5) - return; - - var guser = (IGuildUser)ctx.User; - var cmd = new AutoCommand() - { - CommandText = cmdText, - ChannelId = ctx.Channel.Id, - ChannelName = ctx.Channel.Name, - GuildId = ctx.Guild?.Id, - GuildName = ctx.Guild?.Name, - VoiceChannelId = guser.VoiceChannel?.Id, - VoiceChannelName = guser.VoiceChannel?.Name, - Interval = interval, - }; - _service.AddNewAutoCommand(cmd); - - await ReplyConfirmLocalizedAsync(strs.autocmd_add(Format.Code(Format.Sanitize(cmdText)), cmd.Interval)); - } - - [NadekoCommand, Aliases] - [RequireContext(ContextType.Guild)] - [OwnerOnly] - public async Task StartupCommandsList(int page = 1) - { - if (page-- < 1) - return; - - var scmds = _service.GetStartupCommands() - .Skip(page * 5) - .Take(5) - .ToList(); + var scmds = _service.GetStartupCommands() + .Skip(page * 5) + .Take(5) + .ToList(); - if (scmds.Count == 0) - { - await ReplyErrorLocalizedAsync(strs.startcmdlist_none).ConfigureAwait(false); - } - else - { - var i = 0; - await SendConfirmAsync( + if (scmds.Count == 0) + { + await ReplyErrorLocalizedAsync(strs.startcmdlist_none).ConfigureAwait(false); + } + else + { + var i = 0; + await SendConfirmAsync( text: string.Join("\n", scmds - .Select(x => $@"```css + .Select(x => $@"```css #{++i + page * 5} [{GetText(strs.server)}]: {(x.GuildId.HasValue ? $"{x.GuildName} #{x.GuildId}" : "-")} [{GetText(strs.channel)}]: {x.ChannelName} #{x.ChannelId} @@ -121,31 +117,31 @@ namespace NadekoBot.Modules.Administration title: string.Empty, footer: GetText(strs.page(page + 1))) .ConfigureAwait(false); - } } + } - [NadekoCommand, Aliases] - [RequireContext(ContextType.Guild)] - [OwnerOnly] - public async Task AutoCommandsList(int page = 1) + [NadekoCommand, Aliases] + [RequireContext(ContextType.Guild)] + [OwnerOnly] + public async Task AutoCommandsList(int page = 1) + { + if (page-- < 1) + return; + + var scmds = _service.GetAutoCommands() + .Skip(page * 5) + .Take(5) + .ToList(); + if (!scmds.Any()) { - if (page-- < 1) - return; - - var scmds = _service.GetAutoCommands() - .Skip(page * 5) - .Take(5) - .ToList(); - if (!scmds.Any()) - { - await ReplyErrorLocalizedAsync(strs.autocmdlist_none).ConfigureAwait(false); - } - else - { - var i = 0; - await SendConfirmAsync( + await ReplyErrorLocalizedAsync(strs.autocmdlist_none).ConfigureAwait(false); + } + else + { + var i = 0; + await SendConfirmAsync( text: string.Join("\n", scmds - .Select(x => $@"```css + .Select(x => $@"```css #{++i + page * 5} [{GetText(strs.server)}]: {(x.GuildId.HasValue ? $"{x.GuildName} #{x.GuildId}" : "-")} [{GetText(strs.channel)}]: {x.ChannelName} #{x.ChannelId} @@ -154,391 +150,390 @@ namespace NadekoBot.Modules.Administration title: string.Empty, footer: GetText(strs.page(page + 1))) .ConfigureAwait(false); - } } + } - private string GetIntervalText(int interval) + private string GetIntervalText(int interval) + { + return $"[{GetText(strs.interval)}]: {interval}"; + } + + [NadekoCommand, Aliases] + [OwnerOnly] + public async Task Wait(int miliseconds) + { + if (miliseconds <= 0) + return; + ctx.Message.DeleteAfter(0); + try { - return $"[{GetText(strs.interval)}]: {interval}"; + var msg = await SendConfirmAsync($"⏲ {miliseconds}ms") + .ConfigureAwait(false); + msg.DeleteAfter(miliseconds / 1000); } + catch { } - [NadekoCommand, Aliases] - [OwnerOnly] - public async Task Wait(int miliseconds) - { - if (miliseconds <= 0) - return; - ctx.Message.DeleteAfter(0); - try - { - var msg = await SendConfirmAsync($"⏲ {miliseconds}ms") - .ConfigureAwait(false); - msg.DeleteAfter(miliseconds / 1000); - } - catch { } - - await Task.Delay(miliseconds).ConfigureAwait(false); - } + await Task.Delay(miliseconds).ConfigureAwait(false); + } - [NadekoCommand, Aliases] - [RequireContext(ContextType.Guild)] - [UserPerm(GuildPerm.Administrator)] - [OwnerOnly] - public async Task AutoCommandRemove([Leftover] int index) + [NadekoCommand, Aliases] + [RequireContext(ContextType.Guild)] + [UserPerm(GuildPerm.Administrator)] + [OwnerOnly] + public async Task AutoCommandRemove([Leftover] int index) + { + if (!_service.RemoveAutoCommand(--index, out _)) { - if (!_service.RemoveAutoCommand(--index, out _)) - { - await ReplyErrorLocalizedAsync(strs.acrm_fail).ConfigureAwait(false); - return; - } + await ReplyErrorLocalizedAsync(strs.acrm_fail).ConfigureAwait(false); + return; + } - await ctx.OkAsync(); - } + await ctx.OkAsync(); + } - [NadekoCommand, Aliases] - [RequireContext(ContextType.Guild)] - [OwnerOnly] - public async Task StartupCommandRemove([Leftover] int index) - { - if (!_service.RemoveStartupCommand(--index, out _)) - await ReplyErrorLocalizedAsync(strs.scrm_fail).ConfigureAwait(false); - else - await ReplyConfirmLocalizedAsync(strs.scrm).ConfigureAwait(false); - } + [NadekoCommand, Aliases] + [RequireContext(ContextType.Guild)] + [OwnerOnly] + public async Task StartupCommandRemove([Leftover] int index) + { + if (!_service.RemoveStartupCommand(--index, out _)) + await ReplyErrorLocalizedAsync(strs.scrm_fail).ConfigureAwait(false); + else + await ReplyConfirmLocalizedAsync(strs.scrm).ConfigureAwait(false); + } - [NadekoCommand, Aliases] - [RequireContext(ContextType.Guild)] - [UserPerm(GuildPerm.Administrator)] - [OwnerOnly] - public async Task StartupCommandsClear() - { - _service.ClearStartupCommands(); + [NadekoCommand, Aliases] + [RequireContext(ContextType.Guild)] + [UserPerm(GuildPerm.Administrator)] + [OwnerOnly] + public async Task StartupCommandsClear() + { + _service.ClearStartupCommands(); - await ReplyConfirmLocalizedAsync(strs.startcmds_cleared).ConfigureAwait(false); - } + await ReplyConfirmLocalizedAsync(strs.startcmds_cleared).ConfigureAwait(false); + } - [NadekoCommand, Aliases] - [OwnerOnly] - public async Task ForwardMessages() - { - var enabled = _service.ForwardMessages(); + [NadekoCommand, Aliases] + [OwnerOnly] + public async Task ForwardMessages() + { + var enabled = _service.ForwardMessages(); - if (enabled) - await ReplyConfirmLocalizedAsync(strs.fwdm_start).ConfigureAwait(false); - else - await ReplyPendingLocalizedAsync(strs.fwdm_stop).ConfigureAwait(false); - } + if (enabled) + await ReplyConfirmLocalizedAsync(strs.fwdm_start).ConfigureAwait(false); + else + await ReplyPendingLocalizedAsync(strs.fwdm_stop).ConfigureAwait(false); + } - [NadekoCommand, Aliases] - [OwnerOnly] - public async Task ForwardToAll() - { - var enabled = _service.ForwardToAll(); + [NadekoCommand, Aliases] + [OwnerOnly] + public async Task ForwardToAll() + { + var enabled = _service.ForwardToAll(); - if (enabled) - await ReplyConfirmLocalizedAsync(strs.fwall_start).ConfigureAwait(false); - else - await ReplyPendingLocalizedAsync(strs.fwall_stop).ConfigureAwait(false); + if (enabled) + await ReplyConfirmLocalizedAsync(strs.fwall_start).ConfigureAwait(false); + else + await ReplyPendingLocalizedAsync(strs.fwall_stop).ConfigureAwait(false); - } + } - [NadekoCommand, Aliases] - public async Task ShardStats(int page = 1) - { - if (--page < 0) - return; + [NadekoCommand, Aliases] + public async Task ShardStats(int page = 1) + { + if (--page < 0) + return; - var statuses = _coord.GetAllShardStatuses(); + var statuses = _coord.GetAllShardStatuses(); - var status = string.Join(" : ", statuses - .Select(x => (ConnectionStateToEmoji(x), x)) - .GroupBy(x => x.Item1) - .Select(x => $"`{x.Count()} {x.Key}`") - .ToArray()); + var status = string.Join(" : ", statuses + .Select(x => (ConnectionStateToEmoji(x), x)) + .GroupBy(x => x.Item1) + .Select(x => $"`{x.Count()} {x.Key}`") + .ToArray()); - var allShardStrings = statuses - .Select(st => - { - var stateStr = ConnectionStateToEmoji(st); - var timeDiff = DateTime.UtcNow - st.LastUpdate; - var maxGuildCountLength = statuses.Max(x => x.GuildCount).ToString().Length; - return $"`{stateStr} " + - $"| #{st.ShardId.ToString().PadBoth(3)} " + - $"| {timeDiff:mm\\:ss} " + - $"| {st.GuildCount.ToString().PadBoth(maxGuildCountLength)} `"; - }) - .ToArray(); - await ctx.SendPaginatedConfirmAsync(page, (curPage) => + var allShardStrings = statuses + .Select(st => { - var str = string.Join("\n", allShardStrings.Skip(25 * curPage).Take(25)); + var stateStr = ConnectionStateToEmoji(st); + var timeDiff = DateTime.UtcNow - st.LastUpdate; + var maxGuildCountLength = statuses.Max(x => x.GuildCount).ToString().Length; + return $"`{stateStr} " + + $"| #{st.ShardId.ToString().PadBoth(3)} " + + $"| {timeDiff:mm\\:ss} " + + $"| {st.GuildCount.ToString().PadBoth(maxGuildCountLength)} `"; + }) + .ToArray(); + await ctx.SendPaginatedConfirmAsync(page, (curPage) => + { + var str = string.Join("\n", allShardStrings.Skip(25 * curPage).Take(25)); - if (string.IsNullOrWhiteSpace(str)) - str = GetText(strs.no_shards_on_page); + if (string.IsNullOrWhiteSpace(str)) + str = GetText(strs.no_shards_on_page); - return _eb.Create() - .WithOkColor() - .WithDescription($"{status}\n\n{str}"); - }, allShardStrings.Length, 25).ConfigureAwait(false); + return _eb.Create() + .WithOkColor() + .WithDescription($"{status}\n\n{str}"); + }, allShardStrings.Length, 25).ConfigureAwait(false); + } + + private static string ConnectionStateToEmoji(ShardStatus status) + { + var timeDiff = DateTime.UtcNow - status.LastUpdate; + return status.ConnectionState switch + { + ConnectionState.Connected => "✅", + ConnectionState.Disconnected => "🔻", + _ when timeDiff > TimeSpan.FromSeconds(30) => " ❗ ", + _ => " ⏳" + }; + } + + [NadekoCommand, Aliases] + [OwnerOnly] + public async Task RestartShard(int shardId) + { + var success = _coord.RestartShard(shardId); + if (success) + { + await ReplyConfirmLocalizedAsync(strs.shard_reconnecting(Format.Bold("#" + shardId))).ConfigureAwait(false); + } + else + { + await ReplyErrorLocalizedAsync(strs.no_shard_id).ConfigureAwait(false); + } + } + + [NadekoCommand, Aliases] + [OwnerOnly] + public Task Leave([Leftover] string guildStr) + { + return _service.LeaveGuild(guildStr); + } + + + [NadekoCommand, Aliases] + [OwnerOnly] + public async Task Die(bool graceful = false) + { + try + { + await ReplyConfirmLocalizedAsync(strs.shutting_down).ConfigureAwait(false); + } + catch + { + // ignored + } + await Task.Delay(2000).ConfigureAwait(false); + _coord.Die(graceful); + } + + [NadekoCommand, Aliases] + [OwnerOnly] + public async Task Restart() + { + bool success = _coord.RestartBot(); + if (!success) + { + await ReplyErrorLocalizedAsync(strs.restart_fail).ConfigureAwait(false); + return; } - private static string ConnectionStateToEmoji(ShardStatus status) + try { await ReplyConfirmLocalizedAsync(strs.restarting).ConfigureAwait(false); } catch { } + } + + [NadekoCommand, Aliases] + [OwnerOnly] + public async Task SetName([Leftover] string newName) + { + if (string.IsNullOrWhiteSpace(newName)) + return; + + try { - var timeDiff = DateTime.UtcNow - status.LastUpdate; - return status.ConnectionState switch - { - ConnectionState.Connected => "✅", - ConnectionState.Disconnected => "🔻", - _ when timeDiff > TimeSpan.FromSeconds(30) => " ❗ ", - _ => " ⏳" - }; + await _client.CurrentUser.ModifyAsync(u => u.Username = newName).ConfigureAwait(false); + } + catch (RateLimitedException) + { + Log.Warning("You've been ratelimited. Wait 2 hours to change your name"); } - [NadekoCommand, Aliases] - [OwnerOnly] - public async Task RestartShard(int shardId) + await ReplyConfirmLocalizedAsync(strs.bot_name(Format.Bold(newName))).ConfigureAwait(false); + } + + [NadekoCommand, Aliases] + [UserPerm(GuildPerm.ManageNicknames)] + [BotPerm(GuildPerm.ChangeNickname)] + [Priority(0)] + public async Task SetNick([Leftover] string newNick = null) + { + if (string.IsNullOrWhiteSpace(newNick)) + return; + var curUser = await ctx.Guild.GetCurrentUserAsync().ConfigureAwait(false); + await curUser.ModifyAsync(u => u.Nickname = newNick).ConfigureAwait(false); + + await ReplyConfirmLocalizedAsync(strs.bot_nick(Format.Bold(newNick) ?? "-")); + } + + [NadekoCommand, Aliases] + [BotPerm(GuildPerm.ManageNicknames)] + [UserPerm(GuildPerm.ManageNicknames)] + [Priority(1)] + public async Task SetNick(IGuildUser gu, [Leftover] string newNick = null) + { + var sg = (SocketGuild) ctx.Guild; + if (sg.OwnerId == gu.Id || + gu.GetRoles().Max(r => r.Position) >= sg.CurrentUser.GetRoles().Max(r => r.Position)) { - var success = _coord.RestartShard(shardId); - if (success) - { - await ReplyConfirmLocalizedAsync(strs.shard_reconnecting(Format.Bold("#" + shardId))).ConfigureAwait(false); - } - else - { - await ReplyErrorLocalizedAsync(strs.no_shard_id).ConfigureAwait(false); - } + await ReplyErrorLocalizedAsync(strs.insuf_perms_i); + return; } - - [NadekoCommand, Aliases] - [OwnerOnly] - public Task Leave([Leftover] string guildStr) - { - return _service.LeaveGuild(guildStr); - } - - - [NadekoCommand, Aliases] - [OwnerOnly] - public async Task Die(bool graceful = false) - { - try - { - await ReplyConfirmLocalizedAsync(strs.shutting_down).ConfigureAwait(false); - } - catch - { - // ignored - } - await Task.Delay(2000).ConfigureAwait(false); - _coord.Die(graceful); - } - - [NadekoCommand, Aliases] - [OwnerOnly] - public async Task Restart() - { - bool success = _coord.RestartBot(); - if (!success) - { - await ReplyErrorLocalizedAsync(strs.restart_fail).ConfigureAwait(false); - return; - } - - try { await ReplyConfirmLocalizedAsync(strs.restarting).ConfigureAwait(false); } catch { } - } - - [NadekoCommand, Aliases] - [OwnerOnly] - public async Task SetName([Leftover] string newName) - { - if (string.IsNullOrWhiteSpace(newName)) - return; - - try - { - await _client.CurrentUser.ModifyAsync(u => u.Username = newName).ConfigureAwait(false); - } - catch (RateLimitedException) - { - Log.Warning("You've been ratelimited. Wait 2 hours to change your name"); - } - - await ReplyConfirmLocalizedAsync(strs.bot_name(Format.Bold(newName))).ConfigureAwait(false); - } - - [NadekoCommand, Aliases] - [UserPerm(GuildPerm.ManageNicknames)] - [BotPerm(GuildPerm.ChangeNickname)] - [Priority(0)] - public async Task SetNick([Leftover] string newNick = null) - { - if (string.IsNullOrWhiteSpace(newNick)) - return; - var curUser = await ctx.Guild.GetCurrentUserAsync().ConfigureAwait(false); - await curUser.ModifyAsync(u => u.Nickname = newNick).ConfigureAwait(false); - - await ReplyConfirmLocalizedAsync(strs.bot_nick(Format.Bold(newNick) ?? "-")); - } - - [NadekoCommand, Aliases] - [BotPerm(GuildPerm.ManageNicknames)] - [UserPerm(GuildPerm.ManageNicknames)] - [Priority(1)] - public async Task SetNick(IGuildUser gu, [Leftover] string newNick = null) - { - var sg = (SocketGuild) ctx.Guild; - if (sg.OwnerId == gu.Id || - gu.GetRoles().Max(r => r.Position) >= sg.CurrentUser.GetRoles().Max(r => r.Position)) - { - await ReplyErrorLocalizedAsync(strs.insuf_perms_i); - return; - } - await gu.ModifyAsync(u => u.Nickname = newNick).ConfigureAwait(false); + await gu.ModifyAsync(u => u.Nickname = newNick).ConfigureAwait(false); - await ReplyConfirmLocalizedAsync(strs.user_nick(Format.Bold(gu.ToString()), Format.Bold(newNick) ?? "-")); - } + await ReplyConfirmLocalizedAsync(strs.user_nick(Format.Bold(gu.ToString()), Format.Bold(newNick) ?? "-")); + } - [NadekoCommand, Aliases] - [OwnerOnly] - public async Task SetStatus([Leftover] SettableUserStatus status) + [NadekoCommand, Aliases] + [OwnerOnly] + public async Task SetStatus([Leftover] SettableUserStatus status) + { + await _client.SetStatusAsync(SettableUserStatusToUserStatus(status)).ConfigureAwait(false); + + await ReplyConfirmLocalizedAsync(strs.bot_status(Format.Bold(status.ToString()))).ConfigureAwait(false); + } + + [NadekoCommand, Aliases] + [OwnerOnly] + public async Task SetAvatar([Leftover] string img = null) + { + var success = await _service.SetAvatar(img); + + if (success) { - await _client.SetStatusAsync(SettableUserStatusToUserStatus(status)).ConfigureAwait(false); - - await ReplyConfirmLocalizedAsync(strs.bot_status(Format.Bold(status.ToString()))).ConfigureAwait(false); + await ReplyConfirmLocalizedAsync(strs.set_avatar).ConfigureAwait(false); } + } - [NadekoCommand, Aliases] - [OwnerOnly] - public async Task SetAvatar([Leftover] string img = null) - { - var success = await _service.SetAvatar(img); + [NadekoCommand, Aliases] + [OwnerOnly] + public async Task SetGame(ActivityType type, [Leftover] string game = null) + { + var rep = new ReplacementBuilder() + .WithDefault(Context) + .Build(); - if (success) - { - await ReplyConfirmLocalizedAsync(strs.set_avatar).ConfigureAwait(false); - } - } + await _service.SetGameAsync(game is null ? game : rep.Replace(game), type).ConfigureAwait(false); - [NadekoCommand, Aliases] - [OwnerOnly] - public async Task SetGame(ActivityType type, [Leftover] string game = null) - { - var rep = new ReplacementBuilder() - .WithDefault(Context) - .Build(); + await ReplyConfirmLocalizedAsync(strs.set_game).ConfigureAwait(false); + } - await _service.SetGameAsync(game is null ? game : rep.Replace(game), type).ConfigureAwait(false); + [NadekoCommand, Aliases] + [OwnerOnly] + public async Task SetStream(string url, [Leftover] string name = null) + { + name = name ?? ""; - await ReplyConfirmLocalizedAsync(strs.set_game).ConfigureAwait(false); - } + await _service.SetStreamAsync(name, url).ConfigureAwait(false); - [NadekoCommand, Aliases] - [OwnerOnly] - public async Task SetStream(string url, [Leftover] string name = null) - { - name = name ?? ""; + await ReplyConfirmLocalizedAsync(strs.set_stream).ConfigureAwait(false); + } - await _service.SetStreamAsync(name, url).ConfigureAwait(false); - - await ReplyConfirmLocalizedAsync(strs.set_stream).ConfigureAwait(false); - } - - [NadekoCommand, Aliases] - [OwnerOnly] - public async Task Send(string where, [Leftover] SmartText text = null) - { - var ids = where.Split('|'); - if (ids.Length != 2) - return; + [NadekoCommand, Aliases] + [OwnerOnly] + public async Task Send(string where, [Leftover] SmartText text = null) + { + var ids = where.Split('|'); + if (ids.Length != 2) + return; - var sid = ulong.Parse(ids[0]); - var server = _client.Guilds.FirstOrDefault(s => s.Id == sid); + var sid = ulong.Parse(ids[0]); + var server = _client.Guilds.FirstOrDefault(s => s.Id == sid); - if (server is null) - return; + if (server is null) + return; - var rep = new ReplacementBuilder() - .WithDefault(Context) - .Build(); + var rep = new ReplacementBuilder() + .WithDefault(Context) + .Build(); - if (ids[1].ToUpperInvariant().StartsWith("C:", StringComparison.InvariantCulture)) - { - var cid = ulong.Parse(ids[1].Substring(2)); - var ch = server.TextChannels.FirstOrDefault(c => c.Id == cid); - if (ch is null) - return; - - text = rep.Replace(text); - await ch.SendAsync(text, sanitizeAll: false); - } - else if (ids[1].ToUpperInvariant().StartsWith("U:", StringComparison.InvariantCulture)) - { - var uid = ulong.Parse(ids[1].Substring(2)); - var user = server.Users.FirstOrDefault(u => u.Id == uid); - if (user is null) - return; - - var ch = await user.GetOrCreateDMChannelAsync(); - text = rep.Replace(text); - await ch.SendAsync(text); - } - else - { - await ReplyErrorLocalizedAsync(strs.invalid_format).ConfigureAwait(false); - return; - } - - await ReplyConfirmLocalizedAsync(strs.message_sent).ConfigureAwait(false); - } - - [NadekoCommand, Aliases] - [OwnerOnly] - public async Task ImagesReload() + if (ids[1].ToUpperInvariant().StartsWith("C:", StringComparison.InvariantCulture)) { - await _service.ReloadImagesAsync(); - await ReplyConfirmLocalizedAsync(strs.images_loading); + var cid = ulong.Parse(ids[1].Substring(2)); + var ch = server.TextChannels.FirstOrDefault(c => c.Id == cid); + if (ch is null) + return; + + text = rep.Replace(text); + await ch.SendAsync(text, sanitizeAll: false); } + else if (ids[1].ToUpperInvariant().StartsWith("U:", StringComparison.InvariantCulture)) + { + var uid = ulong.Parse(ids[1].Substring(2)); + var user = server.Users.FirstOrDefault(u => u.Id == uid); + if (user is null) + return; + + var ch = await user.GetOrCreateDMChannelAsync(); + text = rep.Replace(text); + await ch.SendAsync(text); + } + else + { + await ReplyErrorLocalizedAsync(strs.invalid_format).ConfigureAwait(false); + return; + } + + await ReplyConfirmLocalizedAsync(strs.message_sent).ConfigureAwait(false); + } + + [NadekoCommand, Aliases] + [OwnerOnly] + public async Task ImagesReload() + { + await _service.ReloadImagesAsync(); + await ReplyConfirmLocalizedAsync(strs.images_loading); + } - [NadekoCommand, Aliases] - [OwnerOnly] - public async Task StringsReload() - { - _strings.Reload(); - await ReplyConfirmLocalizedAsync(strs.bot_strings_reloaded).ConfigureAwait(false); - } + [NadekoCommand, Aliases] + [OwnerOnly] + public async Task StringsReload() + { + _strings.Reload(); + await ReplyConfirmLocalizedAsync(strs.bot_strings_reloaded).ConfigureAwait(false); + } - [NadekoCommand, Aliases] - [OwnerOnly] - public async Task CoordReload() + [NadekoCommand, Aliases] + [OwnerOnly] + public async Task CoordReload() + { + await _coord.Reload(); + await ctx.OkAsync(); + } + + private static UserStatus SettableUserStatusToUserStatus(SettableUserStatus sus) + { + switch (sus) { - await _coord.Reload(); - await ctx.OkAsync(); + case SettableUserStatus.Online: + return UserStatus.Online; + case SettableUserStatus.Invisible: + return UserStatus.Invisible; + case SettableUserStatus.Idle: + return UserStatus.AFK; + case SettableUserStatus.Dnd: + return UserStatus.DoNotDisturb; } - private static UserStatus SettableUserStatusToUserStatus(SettableUserStatus sus) - { - switch (sus) - { - case SettableUserStatus.Online: - return UserStatus.Online; - case SettableUserStatus.Invisible: - return UserStatus.Invisible; - case SettableUserStatus.Idle: - return UserStatus.AFK; - case SettableUserStatus.Dnd: - return UserStatus.DoNotDisturb; - } + return UserStatus.Online; + } - return UserStatus.Online; - } - - public enum SettableUserStatus - { - Online, - Invisible, - Idle, - Dnd - } + public enum SettableUserStatus + { + Online, + Invisible, + Idle, + Dnd } } } \ No newline at end of file diff --git a/src/NadekoBot/Modules/Administration/ServerGreetCommands.cs b/src/NadekoBot/Modules/Administration/ServerGreetCommands.cs index 173ca4e85..bf3eb7484 100644 --- a/src/NadekoBot/Modules/Administration/ServerGreetCommands.cs +++ b/src/NadekoBot/Modules/Administration/ServerGreetCommands.cs @@ -5,269 +5,268 @@ using NadekoBot.Services; using System.Threading.Tasks; using NadekoBot.Common.Attributes; -namespace NadekoBot.Modules.Administration +namespace NadekoBot.Modules.Administration; + +public partial class Administration { - public partial class Administration + [Group] + public class ServerGreetCommands : NadekoSubmodule { - [Group] - public class ServerGreetCommands : NadekoSubmodule + [NadekoCommand, Aliases] + [RequireContext(ContextType.Guild)] + [UserPerm(GuildPerm.ManageGuild)] + public async Task Boost() { - [NadekoCommand, Aliases] - [RequireContext(ContextType.Guild)] - [UserPerm(GuildPerm.ManageGuild)] - public async Task Boost() - { - var enabled = await _service.ToggleBoost(ctx.Guild.Id, ctx.Channel.Id); + var enabled = await _service.ToggleBoost(ctx.Guild.Id, ctx.Channel.Id); - if (enabled) - await ReplyConfirmLocalizedAsync(strs.boost_on).ConfigureAwait(false); - else - await ReplyPendingLocalizedAsync(strs.boost_off).ConfigureAwait(false); - } + if (enabled) + await ReplyConfirmLocalizedAsync(strs.boost_on).ConfigureAwait(false); + else + await ReplyPendingLocalizedAsync(strs.boost_off).ConfigureAwait(false); + } - [NadekoCommand, Aliases] - [RequireContext(ContextType.Guild)] - [UserPerm(GuildPerm.ManageGuild)] - public async Task BoostDel(int timer = 30) + [NadekoCommand, Aliases] + [RequireContext(ContextType.Guild)] + [UserPerm(GuildPerm.ManageGuild)] + public async Task BoostDel(int timer = 30) + { + if (timer < 0 || timer > 600) + return; + + await _service.SetBoostDel(ctx.Guild.Id, timer); + + if (timer > 0) + await ReplyConfirmLocalizedAsync(strs.boostdel_on(timer)); + else + await ReplyPendingLocalizedAsync(strs.boostdel_off).ConfigureAwait(false); + } + + [NadekoCommand, Aliases] + [RequireContext(ContextType.Guild)] + [UserPerm(GuildPerm.ManageGuild)] + public Task BoostMsg() + { + var boostMessage = _service.GetBoostMessage(ctx.Guild.Id); + return ReplyConfirmLocalizedAsync(strs.boostmsg_cur(boostMessage?.SanitizeMentions())); + } + + [NadekoCommand, Aliases] + [RequireContext(ContextType.Guild)] + [UserPerm(GuildPerm.ManageGuild)] + public async Task BoostMsg([Leftover] string text) + { + if (string.IsNullOrWhiteSpace(text)) { - if (timer < 0 || timer > 600) - return; - - await _service.SetBoostDel(ctx.Guild.Id, timer); - - if (timer > 0) - await ReplyConfirmLocalizedAsync(strs.boostdel_on(timer)); - else - await ReplyPendingLocalizedAsync(strs.boostdel_off).ConfigureAwait(false); + await BoostMsg().ConfigureAwait(false); + return; } - [NadekoCommand, Aliases] - [RequireContext(ContextType.Guild)] - [UserPerm(GuildPerm.ManageGuild)] - public Task BoostMsg() + var sendBoostEnabled = _service.SetBoostMessage(ctx.Guild.Id, ref text); + + await ReplyConfirmLocalizedAsync(strs.boostmsg_new).ConfigureAwait(false); + if (!sendBoostEnabled) + await ReplyPendingLocalizedAsync(strs.boostmsg_enable($"`{Prefix}boost`")); + } + + [NadekoCommand, Aliases] + [RequireContext(ContextType.Guild)] + [UserPerm(GuildPerm.ManageGuild)] + public async Task GreetDel(int timer = 30) + { + if (timer < 0 || timer > 600) + return; + + await _service.SetGreetDel(ctx.Guild.Id, timer).ConfigureAwait(false); + + if (timer > 0) + await ReplyConfirmLocalizedAsync(strs.greetdel_on(timer)); + else + await ReplyPendingLocalizedAsync(strs.greetdel_off).ConfigureAwait(false); + } + + [NadekoCommand, Aliases] + [RequireContext(ContextType.Guild)] + [UserPerm(GuildPerm.ManageGuild)] + public async Task Greet() + { + var enabled = await _service.SetGreet(ctx.Guild.Id, ctx.Channel.Id).ConfigureAwait(false); + + if (enabled) + await ReplyConfirmLocalizedAsync(strs.greet_on).ConfigureAwait(false); + else + await ReplyPendingLocalizedAsync(strs.greet_off).ConfigureAwait(false); + } + + [NadekoCommand, Aliases] + [RequireContext(ContextType.Guild)] + [UserPerm(GuildPerm.ManageGuild)] + public Task GreetMsg() + { + var greetMsg = _service.GetGreetMsg(ctx.Guild.Id); + return ReplyConfirmLocalizedAsync(strs.greetmsg_cur(greetMsg?.SanitizeMentions())); + } + + [NadekoCommand, Aliases] + [RequireContext(ContextType.Guild)] + [UserPerm(GuildPerm.ManageGuild)] + public async Task GreetMsg([Leftover] string text) + { + if (string.IsNullOrWhiteSpace(text)) { - var boostMessage = _service.GetBoostMessage(ctx.Guild.Id); - return ReplyConfirmLocalizedAsync(strs.boostmsg_cur(boostMessage?.SanitizeMentions())); + await GreetMsg().ConfigureAwait(false); + return; } - [NadekoCommand, Aliases] - [RequireContext(ContextType.Guild)] - [UserPerm(GuildPerm.ManageGuild)] - public async Task BoostMsg([Leftover] string text) + var sendGreetEnabled = _service.SetGreetMessage(ctx.Guild.Id, ref text); + + await ReplyConfirmLocalizedAsync(strs.greetmsg_new).ConfigureAwait(false); + if (!sendGreetEnabled) + await ReplyPendingLocalizedAsync(strs.greetmsg_enable($"`{Prefix}greet`")); + } + + [NadekoCommand, Aliases] + [RequireContext(ContextType.Guild)] + [UserPerm(GuildPerm.ManageGuild)] + public async Task GreetDm() + { + var enabled = await _service.SetGreetDm(ctx.Guild.Id).ConfigureAwait(false); + + if (enabled) + await ReplyConfirmLocalizedAsync(strs.greetdm_on).ConfigureAwait(false); + else + await ReplyConfirmLocalizedAsync(strs.greetdm_off).ConfigureAwait(false); + } + + [NadekoCommand, Aliases] + [RequireContext(ContextType.Guild)] + [UserPerm(GuildPerm.ManageGuild)] + public Task GreetDmMsg() + { + var dmGreetMsg = _service.GetDmGreetMsg(ctx.Guild.Id); + return ReplyConfirmLocalizedAsync(strs.greetdmmsg_cur(dmGreetMsg?.SanitizeMentions())); + } + + [NadekoCommand, Aliases] + [RequireContext(ContextType.Guild)] + [UserPerm(GuildPerm.ManageGuild)] + public async Task GreetDmMsg([Leftover] string text = null) + { + if (string.IsNullOrWhiteSpace(text)) { - if (string.IsNullOrWhiteSpace(text)) - { - await BoostMsg().ConfigureAwait(false); - return; - } - - var sendBoostEnabled = _service.SetBoostMessage(ctx.Guild.Id, ref text); - - await ReplyConfirmLocalizedAsync(strs.boostmsg_new).ConfigureAwait(false); - if (!sendBoostEnabled) - await ReplyPendingLocalizedAsync(strs.boostmsg_enable($"`{Prefix}boost`")); + await GreetDmMsg().ConfigureAwait(false); + return; } - [NadekoCommand, Aliases] - [RequireContext(ContextType.Guild)] - [UserPerm(GuildPerm.ManageGuild)] - public async Task GreetDel(int timer = 30) + var sendGreetEnabled = _service.SetGreetDmMessage(ctx.Guild.Id, ref text); + + await ReplyConfirmLocalizedAsync(strs.greetdmmsg_new).ConfigureAwait(false); + if (!sendGreetEnabled) + await ReplyPendingLocalizedAsync(strs.greetdmmsg_enable($"`{Prefix}greetdm`")); + } + + [NadekoCommand, Aliases] + [RequireContext(ContextType.Guild)] + [UserPerm(GuildPerm.ManageGuild)] + public async Task Bye() + { + var enabled = await _service.SetBye(ctx.Guild.Id, ctx.Channel.Id).ConfigureAwait(false); + + if (enabled) + await ReplyConfirmLocalizedAsync(strs.bye_on).ConfigureAwait(false); + else + await ReplyConfirmLocalizedAsync(strs.bye_off).ConfigureAwait(false); + } + + [NadekoCommand, Aliases] + [RequireContext(ContextType.Guild)] + [UserPerm(GuildPerm.ManageGuild)] + public Task ByeMsg() + { + var byeMsg = _service.GetByeMessage(ctx.Guild.Id); + return ReplyConfirmLocalizedAsync(strs.byemsg_cur(byeMsg?.SanitizeMentions())); + } + + [NadekoCommand, Aliases] + [RequireContext(ContextType.Guild)] + [UserPerm(GuildPerm.ManageGuild)] + public async Task ByeMsg([Leftover] string text) + { + if (string.IsNullOrWhiteSpace(text)) { - if (timer < 0 || timer > 600) - return; - - await _service.SetGreetDel(ctx.Guild.Id, timer).ConfigureAwait(false); - - if (timer > 0) - await ReplyConfirmLocalizedAsync(strs.greetdel_on(timer)); - else - await ReplyPendingLocalizedAsync(strs.greetdel_off).ConfigureAwait(false); + await ByeMsg().ConfigureAwait(false); + return; } - [NadekoCommand, Aliases] - [RequireContext(ContextType.Guild)] - [UserPerm(GuildPerm.ManageGuild)] - public async Task Greet() - { - var enabled = await _service.SetGreet(ctx.Guild.Id, ctx.Channel.Id).ConfigureAwait(false); + var sendByeEnabled = _service.SetByeMessage(ctx.Guild.Id, ref text); - if (enabled) - await ReplyConfirmLocalizedAsync(strs.greet_on).ConfigureAwait(false); - else - await ReplyPendingLocalizedAsync(strs.greet_off).ConfigureAwait(false); - } + await ReplyConfirmLocalizedAsync(strs.byemsg_new).ConfigureAwait(false); + if (!sendByeEnabled) + await ReplyPendingLocalizedAsync(strs.byemsg_enable($"`{Prefix}bye`")); + } - [NadekoCommand, Aliases] - [RequireContext(ContextType.Guild)] - [UserPerm(GuildPerm.ManageGuild)] - public Task GreetMsg() - { - var greetMsg = _service.GetGreetMsg(ctx.Guild.Id); - return ReplyConfirmLocalizedAsync(strs.greetmsg_cur(greetMsg?.SanitizeMentions())); - } + [NadekoCommand, Aliases] + [RequireContext(ContextType.Guild)] + [UserPerm(GuildPerm.ManageGuild)] + public async Task ByeDel(int timer = 30) + { + await _service.SetByeDel(ctx.Guild.Id, timer).ConfigureAwait(false); - [NadekoCommand, Aliases] - [RequireContext(ContextType.Guild)] - [UserPerm(GuildPerm.ManageGuild)] - public async Task GreetMsg([Leftover] string text) - { - if (string.IsNullOrWhiteSpace(text)) - { - await GreetMsg().ConfigureAwait(false); - return; - } - - var sendGreetEnabled = _service.SetGreetMessage(ctx.Guild.Id, ref text); - - await ReplyConfirmLocalizedAsync(strs.greetmsg_new).ConfigureAwait(false); - if (!sendGreetEnabled) - await ReplyPendingLocalizedAsync(strs.greetmsg_enable($"`{Prefix}greet`")); - } - - [NadekoCommand, Aliases] - [RequireContext(ContextType.Guild)] - [UserPerm(GuildPerm.ManageGuild)] - public async Task GreetDm() - { - var enabled = await _service.SetGreetDm(ctx.Guild.Id).ConfigureAwait(false); - - if (enabled) - await ReplyConfirmLocalizedAsync(strs.greetdm_on).ConfigureAwait(false); - else - await ReplyConfirmLocalizedAsync(strs.greetdm_off).ConfigureAwait(false); - } - - [NadekoCommand, Aliases] - [RequireContext(ContextType.Guild)] - [UserPerm(GuildPerm.ManageGuild)] - public Task GreetDmMsg() - { - var dmGreetMsg = _service.GetDmGreetMsg(ctx.Guild.Id); - return ReplyConfirmLocalizedAsync(strs.greetdmmsg_cur(dmGreetMsg?.SanitizeMentions())); - } - - [NadekoCommand, Aliases] - [RequireContext(ContextType.Guild)] - [UserPerm(GuildPerm.ManageGuild)] - public async Task GreetDmMsg([Leftover] string text = null) - { - if (string.IsNullOrWhiteSpace(text)) - { - await GreetDmMsg().ConfigureAwait(false); - return; - } - - var sendGreetEnabled = _service.SetGreetDmMessage(ctx.Guild.Id, ref text); - - await ReplyConfirmLocalizedAsync(strs.greetdmmsg_new).ConfigureAwait(false); - if (!sendGreetEnabled) - await ReplyPendingLocalizedAsync(strs.greetdmmsg_enable($"`{Prefix}greetdm`")); - } - - [NadekoCommand, Aliases] - [RequireContext(ContextType.Guild)] - [UserPerm(GuildPerm.ManageGuild)] - public async Task Bye() - { - var enabled = await _service.SetBye(ctx.Guild.Id, ctx.Channel.Id).ConfigureAwait(false); - - if (enabled) - await ReplyConfirmLocalizedAsync(strs.bye_on).ConfigureAwait(false); - else - await ReplyConfirmLocalizedAsync(strs.bye_off).ConfigureAwait(false); - } - - [NadekoCommand, Aliases] - [RequireContext(ContextType.Guild)] - [UserPerm(GuildPerm.ManageGuild)] - public Task ByeMsg() - { - var byeMsg = _service.GetByeMessage(ctx.Guild.Id); - return ReplyConfirmLocalizedAsync(strs.byemsg_cur(byeMsg?.SanitizeMentions())); - } - - [NadekoCommand, Aliases] - [RequireContext(ContextType.Guild)] - [UserPerm(GuildPerm.ManageGuild)] - public async Task ByeMsg([Leftover] string text) - { - if (string.IsNullOrWhiteSpace(text)) - { - await ByeMsg().ConfigureAwait(false); - return; - } - - var sendByeEnabled = _service.SetByeMessage(ctx.Guild.Id, ref text); - - await ReplyConfirmLocalizedAsync(strs.byemsg_new).ConfigureAwait(false); - if (!sendByeEnabled) - await ReplyPendingLocalizedAsync(strs.byemsg_enable($"`{Prefix}bye`")); - } - - [NadekoCommand, Aliases] - [RequireContext(ContextType.Guild)] - [UserPerm(GuildPerm.ManageGuild)] - public async Task ByeDel(int timer = 30) - { - await _service.SetByeDel(ctx.Guild.Id, timer).ConfigureAwait(false); - - if (timer > 0) - await ReplyConfirmLocalizedAsync(strs.byedel_on(timer)); - else - await ReplyPendingLocalizedAsync(strs.byedel_off).ConfigureAwait(false); - } + if (timer > 0) + await ReplyConfirmLocalizedAsync(strs.byedel_on(timer)); + else + await ReplyPendingLocalizedAsync(strs.byedel_off).ConfigureAwait(false); + } - [NadekoCommand, Aliases] - [RequireContext(ContextType.Guild)] - [UserPerm(GuildPerm.ManageGuild)] - [Ratelimit(5)] - public async Task ByeTest([Leftover] IGuildUser user = null) - { - user = user ?? (IGuildUser) ctx.User; + [NadekoCommand, Aliases] + [RequireContext(ContextType.Guild)] + [UserPerm(GuildPerm.ManageGuild)] + [Ratelimit(5)] + public async Task ByeTest([Leftover] IGuildUser user = null) + { + user = user ?? (IGuildUser) ctx.User; - await _service.ByeTest((ITextChannel)ctx.Channel, user); - var enabled = _service.GetByeEnabled(ctx.Guild.Id); - if (!enabled) - { - await ReplyPendingLocalizedAsync(strs.byemsg_enable($"`{Prefix}bye`")); - } + await _service.ByeTest((ITextChannel)ctx.Channel, user); + var enabled = _service.GetByeEnabled(ctx.Guild.Id); + if (!enabled) + { + await ReplyPendingLocalizedAsync(strs.byemsg_enable($"`{Prefix}bye`")); } + } - [NadekoCommand, Aliases] - [RequireContext(ContextType.Guild)] - [UserPerm(GuildPerm.ManageGuild)] - [Ratelimit(5)] - public async Task GreetTest([Leftover] IGuildUser user = null) - { - user = user ?? (IGuildUser) ctx.User; + [NadekoCommand, Aliases] + [RequireContext(ContextType.Guild)] + [UserPerm(GuildPerm.ManageGuild)] + [Ratelimit(5)] + public async Task GreetTest([Leftover] IGuildUser user = null) + { + user = user ?? (IGuildUser) ctx.User; - await _service.GreetTest((ITextChannel)ctx.Channel, user); - var enabled = _service.GetGreetEnabled(ctx.Guild.Id); - if (!enabled) - { - await ReplyPendingLocalizedAsync(strs.greetmsg_enable($"`{Prefix}greet`")); - } + await _service.GreetTest((ITextChannel)ctx.Channel, user); + var enabled = _service.GetGreetEnabled(ctx.Guild.Id); + if (!enabled) + { + await ReplyPendingLocalizedAsync(strs.greetmsg_enable($"`{Prefix}greet`")); } + } - [NadekoCommand, Aliases] - [RequireContext(ContextType.Guild)] - [UserPerm(GuildPerm.ManageGuild)] - [Ratelimit(5)] - public async Task GreetDmTest([Leftover] IGuildUser user = null) - { - user = user ?? (IGuildUser) ctx.User; + [NadekoCommand, Aliases] + [RequireContext(ContextType.Guild)] + [UserPerm(GuildPerm.ManageGuild)] + [Ratelimit(5)] + public async Task GreetDmTest([Leftover] IGuildUser user = null) + { + user = user ?? (IGuildUser) ctx.User; - var channel = await user.GetOrCreateDMChannelAsync(); - var success = await _service.GreetDmTest(channel, user); - if (success) - await ctx.OkAsync(); - else - await ctx.WarningAsync(); - var enabled = _service.GetGreetDmEnabled(ctx.Guild.Id); - if (!enabled) - await ReplyPendingLocalizedAsync(strs.greetdmmsg_enable($"`{Prefix}greetdm`")); - } + var channel = await user.GetOrCreateDMChannelAsync(); + var success = await _service.GreetDmTest(channel, user); + if (success) + await ctx.OkAsync(); + else + await ctx.WarningAsync(); + var enabled = _service.GetGreetDmEnabled(ctx.Guild.Id); + if (!enabled) + await ReplyPendingLocalizedAsync(strs.greetdmmsg_enable($"`{Prefix}greetdm`")); } } } \ No newline at end of file diff --git a/src/NadekoBot/Modules/Administration/Services/AdministrationService.cs b/src/NadekoBot/Modules/Administration/Services/AdministrationService.cs index 128f497e6..a68ebb29b 100644 --- a/src/NadekoBot/Modules/Administration/Services/AdministrationService.cs +++ b/src/NadekoBot/Modules/Administration/Services/AdministrationService.cs @@ -8,164 +8,161 @@ using NadekoBot.Services; using NadekoBot.Services.Database.Models; using NadekoBot.Extensions; using System.Collections.Concurrent; -using System.Collections.Generic; -using System.Linq; using System.Threading.Tasks; using NadekoBot.Db; -namespace NadekoBot.Modules.Administration.Services +namespace NadekoBot.Modules.Administration.Services; + +public class AdministrationService : INService { - public class AdministrationService : INService + public ConcurrentHashSet DeleteMessagesOnCommand { get; } + public ConcurrentDictionary DeleteMessagesOnCommandChannels { get; } + + private readonly DbService _db; + private readonly ILogCommandService _logService; + + public AdministrationService(Bot bot, CommandHandler cmdHandler, DbService db, ILogCommandService logService) { - public ConcurrentHashSet DeleteMessagesOnCommand { get; } - public ConcurrentDictionary DeleteMessagesOnCommandChannels { get; } + _db = db; + _logService = logService; - private readonly DbService _db; - private readonly ILogCommandService _logService; + DeleteMessagesOnCommand = new ConcurrentHashSet(bot.AllGuildConfigs + .Where(g => g.DeleteMessageOnCommand) + .Select(g => g.GuildId)); - public AdministrationService(Bot bot, CommandHandler cmdHandler, DbService db, ILogCommandService logService) + DeleteMessagesOnCommandChannels = new ConcurrentDictionary(bot.AllGuildConfigs + .SelectMany(x => x.DelMsgOnCmdChannels) + .ToDictionary(x => x.ChannelId, x => x.State) + .ToConcurrent()); + + cmdHandler.CommandExecuted += DelMsgOnCmd_Handler; + } + + public (bool DelMsgOnCmd, IEnumerable channels) GetDelMsgOnCmdData(ulong guildId) + { + using (var uow = _db.GetDbContext()) { - _db = db; - _logService = logService; + var conf = uow.GuildConfigsForId(guildId, + set => set.Include(x => x.DelMsgOnCmdChannels)); - DeleteMessagesOnCommand = new ConcurrentHashSet(bot.AllGuildConfigs - .Where(g => g.DeleteMessageOnCommand) - .Select(g => g.GuildId)); - - DeleteMessagesOnCommandChannels = new ConcurrentDictionary(bot.AllGuildConfigs - .SelectMany(x => x.DelMsgOnCmdChannels) - .ToDictionary(x => x.ChannelId, x => x.State) - .ToConcurrent()); - - cmdHandler.CommandExecuted += DelMsgOnCmd_Handler; + return (conf.DeleteMessageOnCommand, conf.DelMsgOnCmdChannels); } + } - public (bool DelMsgOnCmd, IEnumerable channels) GetDelMsgOnCmdData(ulong guildId) + private Task DelMsgOnCmd_Handler(IUserMessage msg, CommandInfo cmd) + { + var _ = Task.Run(async () => { - using (var uow = _db.GetDbContext()) + if (!(msg.Channel is SocketTextChannel channel)) + return; + + //wat ?! + if (DeleteMessagesOnCommandChannels.TryGetValue(channel.Id, out var state)) { - var conf = uow.GuildConfigsForId(guildId, - set => set.Include(x => x.DelMsgOnCmdChannels)); - - return (conf.DeleteMessageOnCommand, conf.DelMsgOnCmdChannels); - } - } - - private Task DelMsgOnCmd_Handler(IUserMessage msg, CommandInfo cmd) - { - var _ = Task.Run(async () => - { - if (!(msg.Channel is SocketTextChannel channel)) - return; - - //wat ?! - if (DeleteMessagesOnCommandChannels.TryGetValue(channel.Id, out var state)) - { - if (state && cmd.Name != "prune" && cmd.Name != "pick") - { - _logService.AddDeleteIgnore(msg.Id); - try { await msg.DeleteAsync().ConfigureAwait(false); } catch { } - } - //if state is false, that means do not do it - } - else if (DeleteMessagesOnCommand.Contains(channel.Guild.Id) && cmd.Name != "prune" && cmd.Name != "pick") + if (state && cmd.Name != "prune" && cmd.Name != "pick") { _logService.AddDeleteIgnore(msg.Id); try { await msg.DeleteAsync().ConfigureAwait(false); } catch { } } - }); - return Task.CompletedTask; - } + //if state is false, that means do not do it + } + else if (DeleteMessagesOnCommand.Contains(channel.Guild.Id) && cmd.Name != "prune" && cmd.Name != "pick") + { + _logService.AddDeleteIgnore(msg.Id); + try { await msg.DeleteAsync().ConfigureAwait(false); } catch { } + } + }); + return Task.CompletedTask; + } - public bool ToggleDeleteMessageOnCommand(ulong guildId) + public bool ToggleDeleteMessageOnCommand(ulong guildId) + { + bool enabled; + using (var uow = _db.GetDbContext()) { - bool enabled; - using (var uow = _db.GetDbContext()) - { - var conf = uow.GuildConfigsForId(guildId, set => set); - enabled = conf.DeleteMessageOnCommand = !conf.DeleteMessageOnCommand; + var conf = uow.GuildConfigsForId(guildId, set => set); + enabled = conf.DeleteMessageOnCommand = !conf.DeleteMessageOnCommand; - uow.SaveChanges(); - } - return enabled; + uow.SaveChanges(); } + return enabled; + } - public async Task SetDelMsgOnCmdState(ulong guildId, ulong chId, Administration.State newState) + public async Task SetDelMsgOnCmdState(ulong guildId, ulong chId, Administration.State newState) + { + using (var uow = _db.GetDbContext()) { - using (var uow = _db.GetDbContext()) - { - var conf = uow.GuildConfigsForId(guildId, - set => set.Include(x => x.DelMsgOnCmdChannels)); + var conf = uow.GuildConfigsForId(guildId, + set => set.Include(x => x.DelMsgOnCmdChannels)); - var old = conf.DelMsgOnCmdChannels.FirstOrDefault(x => x.ChannelId == chId); - if (newState == Administration.State.Inherit) + var old = conf.DelMsgOnCmdChannels.FirstOrDefault(x => x.ChannelId == chId); + if (newState == Administration.State.Inherit) + { + if (old is not null) { - if (old is not null) - { - conf.DelMsgOnCmdChannels.Remove(old); - uow.Remove(old); - } + conf.DelMsgOnCmdChannels.Remove(old); + uow.Remove(old); } - else - { - if (old is null) - { - old = new DelMsgOnCmdChannel { ChannelId = chId }; - conf.DelMsgOnCmdChannels.Add(old); - } - - old.State = newState == Administration.State.Enable; - DeleteMessagesOnCommandChannels[chId] = newState == Administration.State.Enable; - } - - await uow.SaveChangesAsync(); - } - - if (newState == Administration.State.Disable) - { - } - else if (newState == Administration.State.Enable) - { - DeleteMessagesOnCommandChannels[chId] = true; } else { - DeleteMessagesOnCommandChannels.TryRemove(chId, out var _); + if (old is null) + { + old = new DelMsgOnCmdChannel { ChannelId = chId }; + conf.DelMsgOnCmdChannels.Add(old); + } + + old.State = newState == Administration.State.Enable; + DeleteMessagesOnCommandChannels[chId] = newState == Administration.State.Enable; } + + await uow.SaveChangesAsync(); } - public async Task DeafenUsers(bool value, params IGuildUser[] users) + if (newState == Administration.State.Disable) { - if (!users.Any()) - return; - foreach (var u in users) - { - try - { - await u.ModifyAsync(usr => usr.Deaf = value).ConfigureAwait(false); - } - catch - { - // ignored - } - } } - - public async Task EditMessage(ICommandContext context, ITextChannel chanl, ulong messageId, string input) + else if (newState == Administration.State.Enable) { - var msg = await chanl.GetMessageAsync(messageId); - - if (!(msg is IUserMessage umsg) || msg.Author.Id != context.Client.CurrentUser.Id) - return; - - var rep = new ReplacementBuilder() - .WithDefault(context) - .Build(); - - var text = SmartText.CreateFrom(input); - text = rep.Replace(text); - - await umsg.EditAsync(text); + DeleteMessagesOnCommandChannels[chId] = true; + } + else + { + DeleteMessagesOnCommandChannels.TryRemove(chId, out var _); } } -} + + public async Task DeafenUsers(bool value, params IGuildUser[] users) + { + if (!users.Any()) + return; + foreach (var u in users) + { + try + { + await u.ModifyAsync(usr => usr.Deaf = value).ConfigureAwait(false); + } + catch + { + // ignored + } + } + } + + public async Task EditMessage(ICommandContext context, ITextChannel chanl, ulong messageId, string input) + { + var msg = await chanl.GetMessageAsync(messageId); + + if (!(msg is IUserMessage umsg) || msg.Author.Id != context.Client.CurrentUser.Id) + return; + + var rep = new ReplacementBuilder() + .WithDefault(context) + .Build(); + + var text = SmartText.CreateFrom(input); + text = rep.Replace(text); + + await umsg.EditAsync(text); + } +} \ No newline at end of file diff --git a/src/NadekoBot/Modules/Administration/Services/AutoAssignRoleService.cs b/src/NadekoBot/Modules/Administration/Services/AutoAssignRoleService.cs index ff58cf7fd..4af1f313f 100644 --- a/src/NadekoBot/Modules/Administration/Services/AutoAssignRoleService.cs +++ b/src/NadekoBot/Modules/Administration/Services/AutoAssignRoleService.cs @@ -1,171 +1,166 @@ -using System; -using System.Collections.Concurrent; -using System.Linq; +using System.Collections.Concurrent; using System.Threading.Tasks; using Discord.WebSocket; using NadekoBot.Services; -using System.Collections.Generic; using System.Threading.Channels; using LinqToDB; using Microsoft.EntityFrameworkCore; using NadekoBot.Services.Database.Models; using NadekoBot.Db; using NadekoBot.Extensions; -using Serilog; -namespace NadekoBot.Modules.Administration.Services +namespace NadekoBot.Modules.Administration.Services; + +public sealed class AutoAssignRoleService : INService { - public sealed class AutoAssignRoleService : INService - { - private readonly DiscordSocketClient _client; - private readonly DbService _db; + private readonly DiscordSocketClient _client; + private readonly DbService _db; - //guildid/roleid - private readonly ConcurrentDictionary> _autoAssignableRoles; + //guildid/roleid + private readonly ConcurrentDictionary> _autoAssignableRoles; - private Channel _assignQueue = Channel.CreateBounded( - new BoundedChannelOptions(100) - { - FullMode = BoundedChannelFullMode.DropOldest, - SingleReader = true, - SingleWriter = false, - }); - - public AutoAssignRoleService(DiscordSocketClient client, Bot bot, DbService db) + private Channel _assignQueue = Channel.CreateBounded( + new BoundedChannelOptions(100) { - _client = client; - _db = db; + FullMode = BoundedChannelFullMode.DropOldest, + SingleReader = true, + SingleWriter = false, + }); - _autoAssignableRoles = bot.AllGuildConfigs - .Where(x => !string.IsNullOrWhiteSpace(x.AutoAssignRoleIds)) - .ToDictionary>(k => k.GuildId, v => v.GetAutoAssignableRoles()) - .ToConcurrent(); + public AutoAssignRoleService(DiscordSocketClient client, Bot bot, DbService db) + { + _client = client; + _db = db; - _ = Task.Run(async () => + _autoAssignableRoles = bot.AllGuildConfigs + .Where(x => !string.IsNullOrWhiteSpace(x.AutoAssignRoleIds)) + .ToDictionary>(k => k.GuildId, v => v.GetAutoAssignableRoles()) + .ToConcurrent(); + + _ = Task.Run(async () => + { + while (true) { - while (true) - { - var user = await _assignQueue.Reader.ReadAsync(); - if (!_autoAssignableRoles.TryGetValue(user.Guild.Id, out var savedRoleIds)) - continue; + var user = await _assignQueue.Reader.ReadAsync(); + if (!_autoAssignableRoles.TryGetValue(user.Guild.Id, out var savedRoleIds)) + continue; - try - { - var roleIds = savedRoleIds - .Select(roleId => user.Guild.GetRole(roleId)) - .Where(x => x is not null) - .ToList(); + try + { + var roleIds = savedRoleIds + .Select(roleId => user.Guild.GetRole(roleId)) + .Where(x => x is not null) + .ToList(); - if (roleIds.Any()) - { - await user.AddRolesAsync(roleIds).ConfigureAwait(false); - await Task.Delay(250).ConfigureAwait(false); - } - else - { - Log.Warning( - "Disabled 'Auto assign role' feature on {GuildName} [{GuildId}] server the roles dont exist", - user.Guild.Name, - user.Guild.Id); - - await DisableAarAsync(user.Guild.Id); - } - } - catch (Discord.Net.HttpException ex) when (ex.HttpCode == System.Net.HttpStatusCode.Forbidden) + if (roleIds.Any()) { - Log.Warning("Disabled 'Auto assign role' feature on {GuildName} [{GuildId}] server because I don't have role management permissions", + await user.AddRolesAsync(roleIds).ConfigureAwait(false); + await Task.Delay(250).ConfigureAwait(false); + } + else + { + Log.Warning( + "Disabled 'Auto assign role' feature on {GuildName} [{GuildId}] server the roles dont exist", user.Guild.Name, user.Guild.Id); - + await DisableAarAsync(user.Guild.Id); } - catch (Exception ex) - { - Log.Warning(ex, "Error in aar. Probably one of the roles doesn't exist"); - } } - }); - - _client.UserJoined += OnClientOnUserJoined; - _client.RoleDeleted += OnClientRoleDeleted; - } - - private async Task OnClientRoleDeleted(SocketRole role) - { - if (_autoAssignableRoles.TryGetValue(role.Guild.Id, out var roles) - && roles.Contains(role.Id)) - { - await ToggleAarAsync(role.Guild.Id, role.Id); + catch (Discord.Net.HttpException ex) when (ex.HttpCode == System.Net.HttpStatusCode.Forbidden) + { + Log.Warning("Disabled 'Auto assign role' feature on {GuildName} [{GuildId}] server because I don't have role management permissions", + user.Guild.Name, + user.Guild.Id); + + await DisableAarAsync(user.Guild.Id); + } + catch (Exception ex) + { + Log.Warning(ex, "Error in aar. Probably one of the roles doesn't exist"); + } } - } + }); - private async Task OnClientOnUserJoined(SocketGuildUser user) - { - if (_autoAssignableRoles.TryGetValue(user.Guild.Id, out _)) - await _assignQueue.Writer.WriteAsync(user); - } + _client.UserJoined += OnClientOnUserJoined; + _client.RoleDeleted += OnClientRoleDeleted; + } - public async Task> ToggleAarAsync(ulong guildId, ulong roleId) + private async Task OnClientRoleDeleted(SocketRole role) + { + if (_autoAssignableRoles.TryGetValue(role.Guild.Id, out var roles) + && roles.Contains(role.Id)) { - using var uow = _db.GetDbContext(); - var gc = uow.GuildConfigsForId(guildId, set => set); - var roles = gc.GetAutoAssignableRoles(); - if(!roles.Remove(roleId) && roles.Count < 3) - roles.Add(roleId); + await ToggleAarAsync(role.Guild.Id, role.Id); + } + } + + private async Task OnClientOnUserJoined(SocketGuildUser user) + { + if (_autoAssignableRoles.TryGetValue(user.Guild.Id, out _)) + await _assignQueue.Writer.WriteAsync(user); + } + + public async Task> ToggleAarAsync(ulong guildId, ulong roleId) + { + using var uow = _db.GetDbContext(); + var gc = uow.GuildConfigsForId(guildId, set => set); + var roles = gc.GetAutoAssignableRoles(); + if(!roles.Remove(roleId) && roles.Count < 3) + roles.Add(roleId); - gc.SetAutoAssignableRoles(roles); - await uow.SaveChangesAsync(); - - if (roles.Count > 0) - _autoAssignableRoles[guildId] = roles; - else - _autoAssignableRoles.TryRemove(guildId, out _); - - return roles; - } - - public async Task DisableAarAsync(ulong guildId) - { - using var uow = _db.GetDbContext(); - - await uow - .GuildConfigs - .AsNoTracking() - .Where(x => x.GuildId == guildId) - .UpdateAsync(_ => new GuildConfig(){ AutoAssignRoleIds = null}); + gc.SetAutoAssignableRoles(roles); + await uow.SaveChangesAsync(); + if (roles.Count > 0) + _autoAssignableRoles[guildId] = roles; + else _autoAssignableRoles.TryRemove(guildId, out _); - await uow.SaveChangesAsync(); - } - - public async Task SetAarRolesAsync(ulong guildId, IEnumerable newRoles) - { - using var uow = _db.GetDbContext(); - - var gc = uow.GuildConfigsForId(guildId, set => set); - gc.SetAutoAssignableRoles(newRoles); - - await uow.SaveChangesAsync(); - } - - public bool TryGetRoles(ulong guildId, out IReadOnlyList roles) - => _autoAssignableRoles.TryGetValue(guildId, out roles); + return roles; } - public static class GuildConfigExtensions + public async Task DisableAarAsync(ulong guildId) { - public static List GetAutoAssignableRoles(this GuildConfig gc) - { - if (string.IsNullOrWhiteSpace(gc.AutoAssignRoleIds)) - return new List(); - - return gc.AutoAssignRoleIds.Split(',').Select(ulong.Parse).ToList(); - } - - public static void SetAutoAssignableRoles(this GuildConfig gc, IEnumerable roles) - { - gc.AutoAssignRoleIds = roles.JoinWith(','); - } + using var uow = _db.GetDbContext(); + + await uow + .GuildConfigs + .AsNoTracking() + .Where(x => x.GuildId == guildId) + .UpdateAsync(_ => new GuildConfig(){ AutoAssignRoleIds = null}); + + _autoAssignableRoles.TryRemove(guildId, out _); + + await uow.SaveChangesAsync(); } + + public async Task SetAarRolesAsync(ulong guildId, IEnumerable newRoles) + { + using var uow = _db.GetDbContext(); + + var gc = uow.GuildConfigsForId(guildId, set => set); + gc.SetAutoAssignableRoles(newRoles); + + await uow.SaveChangesAsync(); + } + + public bool TryGetRoles(ulong guildId, out IReadOnlyList roles) + => _autoAssignableRoles.TryGetValue(guildId, out roles); } + +public static class GuildConfigExtensions +{ + public static List GetAutoAssignableRoles(this GuildConfig gc) + { + if (string.IsNullOrWhiteSpace(gc.AutoAssignRoleIds)) + return new List(); + + return gc.AutoAssignRoleIds.Split(',').Select(ulong.Parse).ToList(); + } + + public static void SetAutoAssignableRoles(this GuildConfig gc, IEnumerable roles) + { + gc.AutoAssignRoleIds = roles.JoinWith(','); + } +} \ No newline at end of file diff --git a/src/NadekoBot/Modules/Administration/Services/DangerousCommandsService.cs b/src/NadekoBot/Modules/Administration/Services/DangerousCommandsService.cs index 8460b50d2..faeab172f 100644 --- a/src/NadekoBot/Modules/Administration/Services/DangerousCommandsService.cs +++ b/src/NadekoBot/Modules/Administration/Services/DangerousCommandsService.cs @@ -1,5 +1,3 @@ -using System.Collections.Generic; -using System.Linq; using System.Threading.Tasks; using Microsoft.EntityFrameworkCore; using NadekoBot.Services; @@ -7,20 +5,20 @@ using LinqToDB; using LinqToDB.EntityFrameworkCore; using NadekoBot.Services.Database.Models; -namespace NadekoBot.Modules.Administration.Services +namespace NadekoBot.Modules.Administration.Services; + +public class DangerousCommandsService : INService { - public class DangerousCommandsService : INService - { - public const string WaifusDeleteSql = @"DELETE FROM WaifuUpdates; + public const string WaifusDeleteSql = @"DELETE FROM WaifuUpdates; DELETE FROM WaifuItem; DELETE FROM WaifuInfo;"; - public const string WaifuDeleteSql = @"DELETE FROM WaifuUpdates WHERE UserId=(SELECT Id FROM DiscordUser WHERE UserId={0}); + public const string WaifuDeleteSql = @"DELETE FROM WaifuUpdates WHERE UserId=(SELECT Id FROM DiscordUser WHERE UserId={0}); DELETE FROM WaifuItem WHERE WaifuInfoId=(SELECT Id FROM WaifuInfo WHERE WaifuId=(SELECT Id FROM DiscordUser WHERE UserId={0})); UPDATE WaifuInfo SET ClaimerId=NULL WHERE ClaimerId=(SELECT Id FROM DiscordUser WHERE UserId={0}); DELETE FROM WaifuInfo WHERE WaifuId=(SELECT Id FROM DiscordUser WHERE UserId={0});"; - public const string CurrencyDeleteSql = "UPDATE DiscordUser SET CurrencyAmount=0; DELETE FROM CurrencyTransactions; DELETE FROM PlantedCurrency;"; - public const string MusicPlaylistDeleteSql = "DELETE FROM MusicPlaylists;"; - public const string XpDeleteSql = @"DELETE FROM UserXpStats; + public const string CurrencyDeleteSql = "UPDATE DiscordUser SET CurrencyAmount=0; DELETE FROM CurrencyTransactions; DELETE FROM PlantedCurrency;"; + public const string MusicPlaylistDeleteSql = "DELETE FROM MusicPlaylists;"; + public const string XpDeleteSql = @"DELETE FROM UserXpStats; UPDATE DiscordUser SET ClubId=NULL, IsClubAdmin=0, @@ -34,112 +32,111 @@ DELETE FROM Clubs;"; //DELETE FROM Quotes //WHERE UseCount=0 AND (DateAdded < date('now', '-7 day') OR DateAdded is null);"; - private readonly DbService _db; + private readonly DbService _db; - public DangerousCommandsService(DbService db) + public DangerousCommandsService(DbService db) + { + _db = db; + } + + public async Task ExecuteSql(string sql) + { + int res; + using (var uow = _db.GetDbContext()) { - _db = db; + res = await uow.Database.ExecuteSqlRawAsync(sql); } + return res; + } - public async Task ExecuteSql(string sql) + public class SelectResult + { + public List ColumnNames { get; set; } + public List Results { get; set; } + } + + public SelectResult SelectSql(string sql) + { + var result = new SelectResult() { - int res; - using (var uow = _db.GetDbContext()) - { - res = await uow.Database.ExecuteSqlRawAsync(sql); - } - return res; - } + ColumnNames = new List(), + Results = new List(), + }; - public class SelectResult + using (var uow = _db.GetDbContext()) { - public List ColumnNames { get; set; } - public List Results { get; set; } - } - - public SelectResult SelectSql(string sql) - { - var result = new SelectResult() + var conn = uow.Database.GetDbConnection(); + using (var cmd = conn.CreateCommand()) { - ColumnNames = new List(), - Results = new List(), - }; - - using (var uow = _db.GetDbContext()) - { - var conn = uow.Database.GetDbConnection(); - using (var cmd = conn.CreateCommand()) + cmd.CommandText = sql; + using (var reader = cmd.ExecuteReader()) { - cmd.CommandText = sql; - using (var reader = cmd.ExecuteReader()) + if (reader.HasRows) { - if (reader.HasRows) + for (int i = 0; i < reader.FieldCount; i++) { - for (int i = 0; i < reader.FieldCount; i++) - { - result.ColumnNames.Add(reader.GetName(i)); - } - while (reader.Read()) - { - var obj = new object[reader.FieldCount]; - reader.GetValues(obj); - result.Results.Add(obj.Select(x => x.ToString()).ToArray()); - } + result.ColumnNames.Add(reader.GetName(i)); + } + while (reader.Read()) + { + var obj = new object[reader.FieldCount]; + reader.GetValues(obj); + result.Results.Add(obj.Select(x => x.ToString()).ToArray()); } } } } - return result; - } - - public async Task PurgeUserAsync(ulong userId) - { - using var uow = _db.GetDbContext(); - - // get waifu info - var wi = await uow.Set() - .FirstOrDefaultAsyncEF(x => x.Waifu.UserId == userId); - - // if it exists, delete waifu related things - if (wi is not null) - { - // remove updates which have new or old as this waifu - await uow - .WaifuUpdates - .DeleteAsync(wu => wu.New.UserId == userId || wu.Old.UserId == userId); - - // delete all items this waifu owns - await uow - .Set() - .DeleteAsync(x => x.WaifuInfoId == wi.Id); - - // all waifus this waifu claims are released - await uow - .Set() - .AsQueryable() - .Where(x => x.Claimer.UserId == userId) - .UpdateAsync(x => new WaifuInfo() {ClaimerId = null}); - - // all affinities set to this waifu are reset - await uow - .Set() - .AsQueryable() - .Where(x => x.Affinity.UserId == userId) - .UpdateAsync(x => new WaifuInfo() {AffinityId = null}); - } - - // delete guild xp - await uow - .UserXpStats - .DeleteAsync(x => x.UserId == userId); - - // delete currency transactions - await uow.Set() - .DeleteAsync(x => x.UserId == userId); - - // delete user, currency, and clubs go away with it - await uow.DiscordUser - .DeleteAsync(u => u.UserId == userId); } + return result; } -} + + public async Task PurgeUserAsync(ulong userId) + { + using var uow = _db.GetDbContext(); + + // get waifu info + var wi = await uow.Set() + .FirstOrDefaultAsyncEF(x => x.Waifu.UserId == userId); + + // if it exists, delete waifu related things + if (wi is not null) + { + // remove updates which have new or old as this waifu + await uow + .WaifuUpdates + .DeleteAsync(wu => wu.New.UserId == userId || wu.Old.UserId == userId); + + // delete all items this waifu owns + await uow + .Set() + .DeleteAsync(x => x.WaifuInfoId == wi.Id); + + // all waifus this waifu claims are released + await uow + .Set() + .AsQueryable() + .Where(x => x.Claimer.UserId == userId) + .UpdateAsync(x => new WaifuInfo() {ClaimerId = null}); + + // all affinities set to this waifu are reset + await uow + .Set() + .AsQueryable() + .Where(x => x.Affinity.UserId == userId) + .UpdateAsync(x => new WaifuInfo() {AffinityId = null}); + } + + // delete guild xp + await uow + .UserXpStats + .DeleteAsync(x => x.UserId == userId); + + // delete currency transactions + await uow.Set() + .DeleteAsync(x => x.UserId == userId); + + // delete user, currency, and clubs go away with it + await uow.DiscordUser + .DeleteAsync(u => u.UserId == userId); + } +} \ No newline at end of file diff --git a/src/NadekoBot/Modules/Administration/Services/DiscordPermOverrideService.cs b/src/NadekoBot/Modules/Administration/Services/DiscordPermOverrideService.cs index 0429a1264..e0001b74d 100644 --- a/src/NadekoBot/Modules/Administration/Services/DiscordPermOverrideService.cs +++ b/src/NadekoBot/Modules/Administration/Services/DiscordPermOverrideService.cs @@ -1,158 +1,153 @@ -using System; -using System.Collections.Concurrent; -using System.Collections.Generic; -using System.Linq; +using System.Collections.Concurrent; using System.Threading.Tasks; using Discord; using Discord.Commands; -using Discord.WebSocket; using Microsoft.EntityFrameworkCore; using NadekoBot.Common.ModuleBehaviors; using NadekoBot.Services; using NadekoBot.Services.Database.Models; using NadekoBot.Extensions; -namespace NadekoBot.Modules.Administration.Services +namespace NadekoBot.Modules.Administration.Services; + +public class DiscordPermOverrideService : INService, ILateBlocker { - public class DiscordPermOverrideService : INService, ILateBlocker + private readonly DbService _db; + private readonly IServiceProvider _services; + + public int Priority { get; } = int.MaxValue; + + private readonly ConcurrentDictionary<(ulong, string), DiscordPermOverride> _overrides; + + public DiscordPermOverrideService(DbService db, IServiceProvider services) { - private readonly DbService _db; - private readonly IServiceProvider _services; - - public int Priority { get; } = int.MaxValue; - - private readonly ConcurrentDictionary<(ulong, string), DiscordPermOverride> _overrides; - - public DiscordPermOverrideService(DbService db, IServiceProvider services) - { - _db = db; - _services = services; - using var uow = _db.GetDbContext(); - _overrides = uow.DiscordPermOverrides - .AsNoTracking() - .AsEnumerable() - .ToDictionary(o => (o.GuildId ?? 0, o.Command), o => o) - .ToConcurrent(); - } + _db = db; + _services = services; + using var uow = _db.GetDbContext(); + _overrides = uow.DiscordPermOverrides + .AsNoTracking() + .AsEnumerable() + .ToDictionary(o => (o.GuildId ?? 0, o.Command), o => o) + .ToConcurrent(); + } - public bool TryGetOverrides(ulong guildId, string commandName, out GuildPerm? perm) + public bool TryGetOverrides(ulong guildId, string commandName, out GuildPerm? perm) + { + commandName = commandName.ToLowerInvariant(); + if (_overrides.TryGetValue((guildId, commandName), out var dpo)) { - commandName = commandName.ToLowerInvariant(); - if (_overrides.TryGetValue((guildId, commandName), out var dpo)) - { - perm = dpo.Perm; - return true; - } - - perm = null; - return false; + perm = dpo.Perm; + return true; } - public Task ExecuteOverrides(ICommandContext ctx, CommandInfo command, - GuildPerm perms, IServiceProvider services) - { - var rupa = new RequireUserPermissionAttribute((GuildPermission) perms); - return rupa.CheckPermissionsAsync(ctx, command, services); - } + perm = null; + return false; + } - public async Task AddOverride(ulong guildId, string commandName, GuildPerm perm) + public Task ExecuteOverrides(ICommandContext ctx, CommandInfo command, + GuildPerm perms, IServiceProvider services) + { + var rupa = new RequireUserPermissionAttribute((GuildPermission) perms); + return rupa.CheckPermissionsAsync(ctx, command, services); + } + + public async Task AddOverride(ulong guildId, string commandName, GuildPerm perm) + { + commandName = commandName.ToLowerInvariant(); + using (var uow = _db.GetDbContext()) { - commandName = commandName.ToLowerInvariant(); - using (var uow = _db.GetDbContext()) + var over = await uow + .Set() + .AsQueryable() + .FirstOrDefaultAsync(x => x.GuildId == guildId && commandName == x.Command); + + if (over is null) { - var over = await uow - .Set() - .AsQueryable() - .FirstOrDefaultAsync(x => x.GuildId == guildId && commandName == x.Command); - - if (over is null) - { - uow.Set() - .Add(over = new DiscordPermOverride() - { - Command = commandName, - Perm = perm, - GuildId = guildId, - }); - } - else - { - over.Perm = perm; - } - - _overrides[(guildId, commandName)] = over; - - await uow.SaveChangesAsync(); + uow.Set() + .Add(over = new DiscordPermOverride() + { + Command = commandName, + Perm = perm, + GuildId = guildId, + }); } - } - - public async Task ClearAllOverrides(ulong guildId) - { - using (var uow = _db.GetDbContext()) + else { - var overrides = await uow - .Set() - .AsQueryable() - .AsNoTracking() - .Where(x => x.GuildId == guildId) - .ToListAsync(); - - uow.RemoveRange(overrides); - await uow.SaveChangesAsync(); - - foreach (var over in overrides) - { - _overrides.TryRemove((guildId, over.Command), out _); - } - } - } - - public async Task RemoveOverride(ulong guildId, string commandName) - { - commandName = commandName.ToLowerInvariant(); - - using (var uow = _db.GetDbContext()) - { - var over = await uow - .Set() - .AsQueryable() - .AsNoTracking() - .FirstOrDefaultAsync(x => x.GuildId == guildId && x.Command == commandName); - - if (over is null) - return; - - uow.Remove(over); - await uow.SaveChangesAsync(); - - _overrides.TryRemove((guildId, commandName), out _); - } - } - - public Task> GetAllOverrides(ulong guildId) - { - using (var uow = _db.GetDbContext()) - { - return uow - .Set() - .AsQueryable() - .AsNoTracking() - .Where(x => x.GuildId == guildId) - .ToListAsync(); - } - } - - public async Task TryBlockLate(ICommandContext context, string moduleName, CommandInfo command) - { - if (TryGetOverrides(context.Guild?.Id ?? 0, command.Name, out var perm) && perm is not null) - { - var result = await new RequireUserPermissionAttribute((GuildPermission) perm) - .CheckPermissionsAsync(context, command, _services); - - return !result.IsSuccess; + over.Perm = perm; } - return false; + _overrides[(guildId, commandName)] = over; + + await uow.SaveChangesAsync(); } } + + public async Task ClearAllOverrides(ulong guildId) + { + using (var uow = _db.GetDbContext()) + { + var overrides = await uow + .Set() + .AsQueryable() + .AsNoTracking() + .Where(x => x.GuildId == guildId) + .ToListAsync(); + + uow.RemoveRange(overrides); + await uow.SaveChangesAsync(); + + foreach (var over in overrides) + { + _overrides.TryRemove((guildId, over.Command), out _); + } + } + } + + public async Task RemoveOverride(ulong guildId, string commandName) + { + commandName = commandName.ToLowerInvariant(); + + using (var uow = _db.GetDbContext()) + { + var over = await uow + .Set() + .AsQueryable() + .AsNoTracking() + .FirstOrDefaultAsync(x => x.GuildId == guildId && x.Command == commandName); + + if (over is null) + return; + + uow.Remove(over); + await uow.SaveChangesAsync(); + + _overrides.TryRemove((guildId, commandName), out _); + } + } + + public Task> GetAllOverrides(ulong guildId) + { + using (var uow = _db.GetDbContext()) + { + return uow + .Set() + .AsQueryable() + .AsNoTracking() + .Where(x => x.GuildId == guildId) + .ToListAsync(); + } + } + + public async Task TryBlockLate(ICommandContext context, string moduleName, CommandInfo command) + { + if (TryGetOverrides(context.Guild?.Id ?? 0, command.Name, out var perm) && perm is not null) + { + var result = await new RequireUserPermissionAttribute((GuildPermission) perm) + .CheckPermissionsAsync(context, command, _services); + + return !result.IsSuccess; + } + + return false; + } } \ No newline at end of file diff --git a/src/NadekoBot/Modules/Administration/Services/GameVoiceChannelService.cs b/src/NadekoBot/Modules/Administration/Services/GameVoiceChannelService.cs index 9795cb683..5423404ce 100644 --- a/src/NadekoBot/Modules/Administration/Services/GameVoiceChannelService.cs +++ b/src/NadekoBot/Modules/Administration/Services/GameVoiceChannelService.cs @@ -1,133 +1,129 @@ -using System; -using System.Linq; -using System.Threading.Tasks; +using System.Threading.Tasks; using Discord.WebSocket; using NadekoBot.Common.Collections; using NadekoBot.Extensions; using NadekoBot.Services; using NadekoBot.Db; -using Serilog; -namespace NadekoBot.Modules.Administration.Services +namespace NadekoBot.Modules.Administration.Services; + +public class GameVoiceChannelService : INService { - public class GameVoiceChannelService : INService + public ConcurrentHashSet GameVoiceChannels { get; } = new ConcurrentHashSet(); + + private readonly DbService _db; + private readonly DiscordSocketClient _client; + + public GameVoiceChannelService(DiscordSocketClient client, DbService db, Bot bot) { - public ConcurrentHashSet GameVoiceChannels { get; } = new ConcurrentHashSet(); + _db = db; + _client = client; - private readonly DbService _db; - private readonly DiscordSocketClient _client; + GameVoiceChannels = new ConcurrentHashSet( + bot.AllGuildConfigs.Where(gc => gc.GameVoiceChannel != null) + .Select(gc => gc.GameVoiceChannel.Value)); - public GameVoiceChannelService(DiscordSocketClient client, DbService db, Bot bot) - { - _db = db; - _client = client; - - GameVoiceChannels = new ConcurrentHashSet( - bot.AllGuildConfigs.Where(gc => gc.GameVoiceChannel != null) - .Select(gc => gc.GameVoiceChannel.Value)); - - _client.UserVoiceStateUpdated += Client_UserVoiceStateUpdated; - _client.GuildMemberUpdated += _client_GuildMemberUpdated; - } - - private Task _client_GuildMemberUpdated(SocketGuildUser before, SocketGuildUser after) - { - var _ = Task.Run(async () => - { - try - { - //if the user is in the voice channel and that voice channel is gvc - var vc = after.VoiceChannel; - if (vc is null || !GameVoiceChannels.Contains(vc.Id)) - return; - - //if the activity has changed, and is a playing activity - if (before.Activity != after.Activity - && after.Activity != null - && after.Activity.Type == Discord.ActivityType.Playing) - { - //trigger gvc - await TriggerGvc(after, after.Activity.Name); - } - - } - catch (Exception ex) - { - Log.Warning(ex, "Error running GuildMemberUpdated in gvc"); - } - }); - return Task.CompletedTask; - } - - public ulong? ToggleGameVoiceChannel(ulong guildId, ulong vchId) - { - ulong? id; - using (var uow = _db.GetDbContext()) - { - var gc = uow.GuildConfigsForId(guildId, set => set); - - if (gc.GameVoiceChannel == vchId) - { - GameVoiceChannels.TryRemove(vchId); - id = gc.GameVoiceChannel = null; - } - else - { - if (gc.GameVoiceChannel != null) - GameVoiceChannels.TryRemove(gc.GameVoiceChannel.Value); - GameVoiceChannels.Add(vchId); - id = gc.GameVoiceChannel = vchId; - } - - uow.SaveChanges(); - } - return id; - } - - private Task Client_UserVoiceStateUpdated(SocketUser usr, SocketVoiceState oldState, SocketVoiceState newState) - { - var _ = Task.Run(async () => - { - try - { - if (!(usr is SocketGuildUser gUser)) - return; - - var game = gUser.Activity?.Name; - - if (oldState.VoiceChannel == newState.VoiceChannel || - newState.VoiceChannel is null) - return; - - if (!GameVoiceChannels.Contains(newState.VoiceChannel.Id) || - string.IsNullOrWhiteSpace(game)) - return; - - await TriggerGvc(gUser, game); - } - catch (Exception ex) - { - Log.Warning(ex, "Error running VoiceStateUpdate in gvc"); - } - }); - - return Task.CompletedTask; - } - - private async Task TriggerGvc(SocketGuildUser gUser, string game) - { - if (string.IsNullOrWhiteSpace(game)) - return; - - game = game.TrimTo(50).ToLowerInvariant(); - var vch = gUser.Guild.VoiceChannels - .FirstOrDefault(x => x.Name.ToLowerInvariant() == game); - - if (vch is null) - return; - - await Task.Delay(1000).ConfigureAwait(false); - await gUser.ModifyAsync(gu => gu.Channel = vch).ConfigureAwait(false); - } + _client.UserVoiceStateUpdated += Client_UserVoiceStateUpdated; + _client.GuildMemberUpdated += _client_GuildMemberUpdated; } -} + + private Task _client_GuildMemberUpdated(SocketGuildUser before, SocketGuildUser after) + { + var _ = Task.Run(async () => + { + try + { + //if the user is in the voice channel and that voice channel is gvc + var vc = after.VoiceChannel; + if (vc is null || !GameVoiceChannels.Contains(vc.Id)) + return; + + //if the activity has changed, and is a playing activity + if (before.Activity != after.Activity + && after.Activity != null + && after.Activity.Type == Discord.ActivityType.Playing) + { + //trigger gvc + await TriggerGvc(after, after.Activity.Name); + } + + } + catch (Exception ex) + { + Log.Warning(ex, "Error running GuildMemberUpdated in gvc"); + } + }); + return Task.CompletedTask; + } + + public ulong? ToggleGameVoiceChannel(ulong guildId, ulong vchId) + { + ulong? id; + using (var uow = _db.GetDbContext()) + { + var gc = uow.GuildConfigsForId(guildId, set => set); + + if (gc.GameVoiceChannel == vchId) + { + GameVoiceChannels.TryRemove(vchId); + id = gc.GameVoiceChannel = null; + } + else + { + if (gc.GameVoiceChannel != null) + GameVoiceChannels.TryRemove(gc.GameVoiceChannel.Value); + GameVoiceChannels.Add(vchId); + id = gc.GameVoiceChannel = vchId; + } + + uow.SaveChanges(); + } + return id; + } + + private Task Client_UserVoiceStateUpdated(SocketUser usr, SocketVoiceState oldState, SocketVoiceState newState) + { + var _ = Task.Run(async () => + { + try + { + if (!(usr is SocketGuildUser gUser)) + return; + + var game = gUser.Activity?.Name; + + if (oldState.VoiceChannel == newState.VoiceChannel || + newState.VoiceChannel is null) + return; + + if (!GameVoiceChannels.Contains(newState.VoiceChannel.Id) || + string.IsNullOrWhiteSpace(game)) + return; + + await TriggerGvc(gUser, game); + } + catch (Exception ex) + { + Log.Warning(ex, "Error running VoiceStateUpdate in gvc"); + } + }); + + return Task.CompletedTask; + } + + private async Task TriggerGvc(SocketGuildUser gUser, string game) + { + if (string.IsNullOrWhiteSpace(game)) + return; + + game = game.TrimTo(50).ToLowerInvariant(); + var vch = gUser.Guild.VoiceChannels + .FirstOrDefault(x => x.Name.ToLowerInvariant() == game); + + if (vch is null) + return; + + await Task.Delay(1000).ConfigureAwait(false); + await gUser.ModifyAsync(gu => gu.Channel = vch).ConfigureAwait(false); + } +} \ No newline at end of file diff --git a/src/NadekoBot/Modules/Administration/Services/GuildTimezoneService.cs b/src/NadekoBot/Modules/Administration/Services/GuildTimezoneService.cs index 6405b18c4..86f927642 100644 --- a/src/NadekoBot/Modules/Administration/Services/GuildTimezoneService.cs +++ b/src/NadekoBot/Modules/Administration/Services/GuildTimezoneService.cs @@ -1,6 +1,4 @@ -using System; -using System.Collections.Concurrent; -using System.Linq; +using System.Collections.Concurrent; using Discord.WebSocket; using NadekoBot.Extensions; using NadekoBot.Services; @@ -8,79 +6,78 @@ using NadekoBot.Services.Database.Models; using System.Threading.Tasks; using NadekoBot.Db; -namespace NadekoBot.Modules.Administration.Services +namespace NadekoBot.Modules.Administration.Services; + +public class GuildTimezoneService : INService { - public class GuildTimezoneService : INService + public static ConcurrentDictionary AllServices { get; } = new ConcurrentDictionary(); + private readonly ConcurrentDictionary _timezones; + private readonly DbService _db; + + public GuildTimezoneService(DiscordSocketClient client, Bot bot, DbService db) { - public static ConcurrentDictionary AllServices { get; } = new ConcurrentDictionary(); - private readonly ConcurrentDictionary _timezones; - private readonly DbService _db; + _timezones = bot.AllGuildConfigs + .Select(GetTimzezoneTuple) + .Where(x => x.Timezone != null) + .ToDictionary(x => x.GuildId, x => x.Timezone) + .ToConcurrent(); - public GuildTimezoneService(DiscordSocketClient client, Bot bot, DbService db) - { - _timezones = bot.AllGuildConfigs - .Select(GetTimzezoneTuple) - .Where(x => x.Timezone != null) - .ToDictionary(x => x.GuildId, x => x.Timezone) - .ToConcurrent(); + var curUser = client.CurrentUser; + if (curUser != null) + AllServices.TryAdd(curUser.Id, this); + _db = db; - var curUser = client.CurrentUser; - if (curUser != null) - AllServices.TryAdd(curUser.Id, this); - _db = db; - - bot.JoinedGuild += Bot_JoinedGuild; - } - - private Task Bot_JoinedGuild(GuildConfig arg) - { - var (guildId, tz) = GetTimzezoneTuple(arg); - if (tz != null) - _timezones.TryAdd(guildId, tz); - return Task.CompletedTask; - } - - private static (ulong GuildId, TimeZoneInfo Timezone) GetTimzezoneTuple(GuildConfig x) - { - TimeZoneInfo tz; - try - { - if (x.TimeZoneId is null) - tz = null; - else - tz = TimeZoneInfo.FindSystemTimeZoneById(x.TimeZoneId); - } - catch - { - tz = null; - } - return (x.GuildId, Timezone: tz); - } - - public TimeZoneInfo GetTimeZoneOrDefault(ulong guildId) - { - if (_timezones.TryGetValue(guildId, out var tz)) - return tz; - return null; - } - - public void SetTimeZone(ulong guildId, TimeZoneInfo tz) - { - using (var uow = _db.GetDbContext()) - { - var gc = uow.GuildConfigsForId(guildId, set => set); - - gc.TimeZoneId = tz?.Id; - uow.SaveChanges(); - - if (tz is null) - _timezones.TryRemove(guildId, out tz); - else - _timezones.AddOrUpdate(guildId, tz, (key, old) => tz); - } - } - - public TimeZoneInfo GetTimeZoneOrUtc(ulong guildId) - => GetTimeZoneOrDefault(guildId) ?? TimeZoneInfo.Utc; + bot.JoinedGuild += Bot_JoinedGuild; } -} + + private Task Bot_JoinedGuild(GuildConfig arg) + { + var (guildId, tz) = GetTimzezoneTuple(arg); + if (tz != null) + _timezones.TryAdd(guildId, tz); + return Task.CompletedTask; + } + + private static (ulong GuildId, TimeZoneInfo Timezone) GetTimzezoneTuple(GuildConfig x) + { + TimeZoneInfo tz; + try + { + if (x.TimeZoneId is null) + tz = null; + else + tz = TimeZoneInfo.FindSystemTimeZoneById(x.TimeZoneId); + } + catch + { + tz = null; + } + return (x.GuildId, Timezone: tz); + } + + public TimeZoneInfo GetTimeZoneOrDefault(ulong guildId) + { + if (_timezones.TryGetValue(guildId, out var tz)) + return tz; + return null; + } + + public void SetTimeZone(ulong guildId, TimeZoneInfo tz) + { + using (var uow = _db.GetDbContext()) + { + var gc = uow.GuildConfigsForId(guildId, set => set); + + gc.TimeZoneId = tz?.Id; + uow.SaveChanges(); + + if (tz is null) + _timezones.TryRemove(guildId, out tz); + else + _timezones.AddOrUpdate(guildId, tz, (key, old) => tz); + } + } + + public TimeZoneInfo GetTimeZoneOrUtc(ulong guildId) + => GetTimeZoneOrDefault(guildId) ?? TimeZoneInfo.Utc; +} \ No newline at end of file diff --git a/src/NadekoBot/Modules/Administration/Services/ImageOnlyChannelService.cs b/src/NadekoBot/Modules/Administration/Services/ImageOnlyChannelService.cs index d305c4fa0..0845d997a 100644 --- a/src/NadekoBot/Modules/Administration/Services/ImageOnlyChannelService.cs +++ b/src/NadekoBot/Modules/Administration/Services/ImageOnlyChannelService.cs @@ -1,6 +1,4 @@ -using System; -using System.Collections.Concurrent; -using System.Linq; +using System.Collections.Concurrent; using System.Net; using System.Threading.Channels; using System.Threading.Tasks; @@ -13,175 +11,173 @@ using NadekoBot.Common.Collections; using NadekoBot.Common.ModuleBehaviors; using NadekoBot.Extensions; using NadekoBot.Services; -using Serilog; -namespace NadekoBot.Modules.Administration.Services +namespace NadekoBot.Modules.Administration.Services; + +public sealed class ImageOnlyChannelService : IEarlyBehavior { - public sealed class ImageOnlyChannelService : IEarlyBehavior + private readonly IMemoryCache _ticketCache; + private readonly DiscordSocketClient _client; + private readonly DbService _db; + private readonly ConcurrentDictionary> _enabledOn; + + private Channel _deleteQueue = Channel.CreateBounded(new BoundedChannelOptions(100) { - private readonly IMemoryCache _ticketCache; - private readonly DiscordSocketClient _client; - private readonly DbService _db; - private readonly ConcurrentDictionary> _enabledOn; - - private Channel _deleteQueue = Channel.CreateBounded(new BoundedChannelOptions(100) - { - FullMode = BoundedChannelFullMode.DropOldest, - SingleReader = true, - SingleWriter = false, - }); + FullMode = BoundedChannelFullMode.DropOldest, + SingleReader = true, + SingleWriter = false, + }); - public ImageOnlyChannelService(IMemoryCache ticketCache, DiscordSocketClient client, DbService db) - { - _ticketCache = ticketCache; - _client = client; - _db = db; + public ImageOnlyChannelService(IMemoryCache ticketCache, DiscordSocketClient client, DbService db) + { + _ticketCache = ticketCache; + _client = client; + _db = db; - var uow = _db.GetDbContext(); - _enabledOn = uow.ImageOnlyChannels - .ToList() - .GroupBy(x => x.GuildId) - .ToDictionary(x => x.Key, x => new ConcurrentHashSet(x.Select(x => x.ChannelId))) - .ToConcurrent(); + var uow = _db.GetDbContext(); + _enabledOn = uow.ImageOnlyChannels + .ToList() + .GroupBy(x => x.GuildId) + .ToDictionary(x => x.Key, x => new ConcurrentHashSet(x.Select(x => x.ChannelId))) + .ToConcurrent(); - _ = Task.Run(DeleteQueueRunner); + _ = Task.Run(DeleteQueueRunner); - _client.ChannelDestroyed += ClientOnChannelDestroyed; - } - - private Task ClientOnChannelDestroyed(SocketChannel ch) - { - if (ch is not IGuildChannel gch) - return Task.CompletedTask; - - if (_enabledOn.TryGetValue(gch.GuildId, out var channels) && channels.TryRemove(ch.Id)) - ToggleImageOnlyChannel(gch.GuildId, ch.Id, true); + _client.ChannelDestroyed += ClientOnChannelDestroyed; + } + private Task ClientOnChannelDestroyed(SocketChannel ch) + { + if (ch is not IGuildChannel gch) return Task.CompletedTask; - } - private async Task DeleteQueueRunner() + if (_enabledOn.TryGetValue(gch.GuildId, out var channels) && channels.TryRemove(ch.Id)) + ToggleImageOnlyChannel(gch.GuildId, ch.Id, true); + + return Task.CompletedTask; + } + + private async Task DeleteQueueRunner() + { + while (true) { - while (true) - { - var toDelete = await _deleteQueue.Reader.ReadAsync(); - try - { - await toDelete.DeleteAsync(); - await Task.Delay(1000); - } - catch (HttpException ex) when (ex.HttpCode == HttpStatusCode.Forbidden) - { - // disable if bot can't delete messages in the channel - ToggleImageOnlyChannel(((ITextChannel)toDelete.Channel).GuildId, toDelete.Channel.Id, true); - } - } - } - - public bool ToggleImageOnlyChannel(ulong guildId, ulong channelId, bool forceDisable = false) - { - var newState = false; - using var uow = _db.GetDbContext(); - if (forceDisable - || (_enabledOn.TryGetValue(guildId, out var channels) - && channels.TryRemove(channelId))) - { - uow.ImageOnlyChannels.Delete(x => x.ChannelId == channelId); - } - else - { - uow.ImageOnlyChannels.Add(new() - { - GuildId = guildId, - ChannelId = channelId - }); - - channels = _enabledOn.GetOrAdd(guildId, new ConcurrentHashSet()); - channels.Add(channelId); - newState = true; - } - - uow.SaveChanges(); - return newState; - } - - public async Task RunBehavior(IGuild guild, IUserMessage msg) - { - if (msg.Channel is not ITextChannel tch) - return false; - - if (msg.Attachments.Any(x => x is { Height: > 0, Width: > 0 })) - return false; - - if (!_enabledOn.TryGetValue(tch.GuildId, out var chs) - || !chs.Contains(msg.Channel.Id)) - return false; - - var user = await tch.Guild.GetUserAsync(msg.Author.Id) - ?? await _client.Rest.GetGuildUserAsync(tch.GuildId, msg.Author.Id); - - if (user is null) - return false; - - // ignore owner and admin - if (user.Id == tch.Guild.OwnerId || user.GuildPermissions.Administrator) - { - Log.Information("Image-Only: Ignoring owner od admin ({ChannelId})", msg.Channel.Id); - return false; - } - - // ignore users higher in hierarchy - var botUser = await tch.Guild.GetCurrentUserAsync(); - if (user.GetRoles().Max(x => x.Position) >= botUser.GetRoles().Max(x => x.Position)) - return false; - - // can't modify channel perms if not admin apparently - if (!botUser.GuildPermissions.ManageGuild) - { - ToggleImageOnlyChannel( tch.GuildId, tch.Id, true);; - return false; - } - - var shouldLock = AddUserTicket(tch.GuildId, msg.Author.Id); - if (shouldLock) - { - await tch.AddPermissionOverwriteAsync(msg.Author, new(sendMessages: PermValue.Deny)); - Log.Warning("Image-Only: User {User} [{UserId}] has been banned from typing in the channel [{ChannelId}]", - msg.Author, - msg.Author.Id, - msg.Channel.Id); - } - + var toDelete = await _deleteQueue.Reader.ReadAsync(); try { - await _deleteQueue.Writer.WriteAsync(msg); + await toDelete.DeleteAsync(); + await Task.Delay(1000); } - catch (Exception ex) + catch (HttpException ex) when (ex.HttpCode == HttpStatusCode.Forbidden) { - Log.Error(ex, "Error deleting message {MessageId} in image-only channel {ChannelId}.", - msg.Id, - tch.Id); + // disable if bot can't delete messages in the channel + ToggleImageOnlyChannel(((ITextChannel)toDelete.Channel).GuildId, toDelete.Channel.Id, true); } - - return true; } + } - private bool AddUserTicket(ulong guildId, ulong userId) + public bool ToggleImageOnlyChannel(ulong guildId, ulong channelId, bool forceDisable = false) + { + var newState = false; + using var uow = _db.GetDbContext(); + if (forceDisable + || (_enabledOn.TryGetValue(guildId, out var channels) + && channels.TryRemove(channelId))) { - var old = _ticketCache.GetOrCreate($"{guildId}_{userId}", entry => + uow.ImageOnlyChannels.Delete(x => x.ChannelId == channelId); + } + else + { + uow.ImageOnlyChannels.Add(new() { - entry.AbsoluteExpirationRelativeToNow = TimeSpan.FromDays(1); - return 0; + GuildId = guildId, + ChannelId = channelId }); - _ticketCache.Set($"{guildId}_{userId}", ++old); + channels = _enabledOn.GetOrAdd(guildId, new ConcurrentHashSet()); + channels.Add(channelId); + newState = true; + } - // if this is the third time that the user posts a - // non image in an image-only channel on this server - return old > 2; + uow.SaveChanges(); + return newState; + } + + public async Task RunBehavior(IGuild guild, IUserMessage msg) + { + if (msg.Channel is not ITextChannel tch) + return false; + + if (msg.Attachments.Any(x => x is { Height: > 0, Width: > 0 })) + return false; + + if (!_enabledOn.TryGetValue(tch.GuildId, out var chs) + || !chs.Contains(msg.Channel.Id)) + return false; + + var user = await tch.Guild.GetUserAsync(msg.Author.Id) + ?? await _client.Rest.GetGuildUserAsync(tch.GuildId, msg.Author.Id); + + if (user is null) + return false; + + // ignore owner and admin + if (user.Id == tch.Guild.OwnerId || user.GuildPermissions.Administrator) + { + Log.Information("Image-Only: Ignoring owner od admin ({ChannelId})", msg.Channel.Id); + return false; } - public int Priority { get; } = 0; + // ignore users higher in hierarchy + var botUser = await tch.Guild.GetCurrentUserAsync(); + if (user.GetRoles().Max(x => x.Position) >= botUser.GetRoles().Max(x => x.Position)) + return false; + + // can't modify channel perms if not admin apparently + if (!botUser.GuildPermissions.ManageGuild) + { + ToggleImageOnlyChannel( tch.GuildId, tch.Id, true);; + return false; + } + + var shouldLock = AddUserTicket(tch.GuildId, msg.Author.Id); + if (shouldLock) + { + await tch.AddPermissionOverwriteAsync(msg.Author, new(sendMessages: PermValue.Deny)); + Log.Warning("Image-Only: User {User} [{UserId}] has been banned from typing in the channel [{ChannelId}]", + msg.Author, + msg.Author.Id, + msg.Channel.Id); + } + + try + { + await _deleteQueue.Writer.WriteAsync(msg); + } + catch (Exception ex) + { + Log.Error(ex, "Error deleting message {MessageId} in image-only channel {ChannelId}.", + msg.Id, + tch.Id); + } + + return true; } + + private bool AddUserTicket(ulong guildId, ulong userId) + { + var old = _ticketCache.GetOrCreate($"{guildId}_{userId}", entry => + { + entry.AbsoluteExpirationRelativeToNow = TimeSpan.FromDays(1); + return 0; + }); + + _ticketCache.Set($"{guildId}_{userId}", ++old); + + // if this is the third time that the user posts a + // non image in an image-only channel on this server + return old > 2; + } + + public int Priority { get; } = 0; } \ No newline at end of file diff --git a/src/NadekoBot/Modules/Administration/Services/LogCommandService.cs b/src/NadekoBot/Modules/Administration/Services/LogCommandService.cs index 343067c80..2f1d8dab2 100644 --- a/src/NadekoBot/Modules/Administration/Services/LogCommandService.cs +++ b/src/NadekoBot/Modules/Administration/Services/LogCommandService.cs @@ -1,13 +1,8 @@ -using System; -using System.Collections.Concurrent; -using System.Collections.Generic; -using System.Linq; +using System.Collections.Concurrent; using System.Threading; using System.Threading.Tasks; using Discord; using Discord.WebSocket; -using LinqToDB; -using LinqToDB.EntityFrameworkCore; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Caching.Memory; using NadekoBot.Common.Collections; @@ -17,1244 +12,1243 @@ using NadekoBot.Db; using NadekoBot.Extensions; using NadekoBot.Modules.Administration.Common; -namespace NadekoBot.Modules.Administration.Services +namespace NadekoBot.Modules.Administration.Services; + +public interface ILogCommandService { - public interface ILogCommandService - { - void AddDeleteIgnore(ulong xId); - Task LogServer(ulong guildId, ulong channelId, bool actionValue); - bool LogIgnore(ulong guildId, ulong itemId, IgnoredItemType itemType); - LogSetting GetGuildLogSettings(ulong guildId); - bool Log(ulong guildId, ulong? channelId, LogType type); - } + void AddDeleteIgnore(ulong xId); + Task LogServer(ulong guildId, ulong channelId, bool actionValue); + bool LogIgnore(ulong guildId, ulong itemId, IgnoredItemType itemType); + LogSetting GetGuildLogSettings(ulong guildId); + bool Log(ulong guildId, ulong? channelId, LogType type); +} - public sealed class DummyLogCommandService : ILogCommandService +public sealed class DummyLogCommandService : ILogCommandService +{ + public void AddDeleteIgnore(ulong xId) { - public void AddDeleteIgnore(ulong xId) - { - } - - public Task LogServer(ulong guildId, ulong channelId, bool actionValue) - { - return Task.CompletedTask; - } - - public bool LogIgnore(ulong guildId, ulong itemId, IgnoredItemType itemType) - { - return false; - } - - public LogSetting GetGuildLogSettings(ulong guildId) - { - return default; - } - - public bool Log(ulong guildId, ulong? channelId, LogType type) - { - return false; - } } - - public sealed class LogCommandService : ILogCommandService + + public Task LogServer(ulong guildId, ulong channelId, bool actionValue) { - private readonly DiscordSocketClient _client; + return Task.CompletedTask; + } + + public bool LogIgnore(ulong guildId, ulong itemId, IgnoredItemType itemType) + { + return false; + } + + public LogSetting GetGuildLogSettings(ulong guildId) + { + return default; + } + + public bool Log(ulong guildId, ulong? channelId, LogType type) + { + return false; + } +} + +public sealed class LogCommandService : ILogCommandService +{ + private readonly DiscordSocketClient _client; - public ConcurrentDictionary GuildLogSettings { get; } + public ConcurrentDictionary GuildLogSettings { get; } - private ConcurrentDictionary> PresenceUpdates { get; } = - new ConcurrentDictionary>(); + private ConcurrentDictionary> PresenceUpdates { get; } = + new ConcurrentDictionary>(); - private readonly Timer _timerReference; - private readonly IBotStrings _strings; - private readonly DbService _db; - private readonly MuteService _mute; - private readonly ProtectionService _prot; - private readonly GuildTimezoneService _tz; - private readonly IEmbedBuilderService _eb; - private readonly IMemoryCache _memoryCache; + private readonly Timer _timerReference; + private readonly IBotStrings _strings; + private readonly DbService _db; + private readonly MuteService _mute; + private readonly ProtectionService _prot; + private readonly GuildTimezoneService _tz; + private readonly IEmbedBuilderService _eb; + private readonly IMemoryCache _memoryCache; - private readonly Timer _clearTimer; - private readonly ConcurrentHashSet _ignoreMessageIds = new ConcurrentHashSet(); + private readonly Timer _clearTimer; + private readonly ConcurrentHashSet _ignoreMessageIds = new ConcurrentHashSet(); - public LogCommandService(DiscordSocketClient client, IBotStrings strings, - DbService db, MuteService mute, ProtectionService prot, GuildTimezoneService tz, - IMemoryCache memoryCache, IEmbedBuilderService eb) - { - _client = client; - _memoryCache = memoryCache; - _eb = eb; - _strings = strings; - _db = db; - _mute = mute; - _prot = prot; - _tz = tz; + public LogCommandService(DiscordSocketClient client, IBotStrings strings, + DbService db, MuteService mute, ProtectionService prot, GuildTimezoneService tz, + IMemoryCache memoryCache, IEmbedBuilderService eb) + { + _client = client; + _memoryCache = memoryCache; + _eb = eb; + _strings = strings; + _db = db; + _mute = mute; + _prot = prot; + _tz = tz; #if !GLOBAL_NADEKO - using (var uow = db.GetDbContext()) + using (var uow = db.GetDbContext()) + { + var guildIds = client.Guilds.Select(x => x.Id).ToList(); + var configs = uow + .LogSettings + .AsQueryable() + .AsNoTracking() + .Where(x => guildIds.Contains(x.GuildId)) + .Include(ls => ls.LogIgnores) + .ToList(); + + GuildLogSettings = configs + .ToDictionary(ls => ls.GuildId) + .ToConcurrent(); + } + + _timerReference = new Timer(async (state) => + { + var keys = PresenceUpdates.Keys.ToList(); + + await Task.WhenAll(keys.Select(key => { - var guildIds = client.Guilds.Select(x => x.Id).ToList(); - var configs = uow - .LogSettings - .AsQueryable() - .AsNoTracking() - .Where(x => guildIds.Contains(x.GuildId)) - .Include(ls => ls.LogIgnores) - .ToList(); - - GuildLogSettings = configs - .ToDictionary(ls => ls.GuildId) - .ToConcurrent(); - } - - _timerReference = new Timer(async (state) => - { - var keys = PresenceUpdates.Keys.ToList(); - - await Task.WhenAll(keys.Select(key => - { - if (!((SocketGuild) key.Guild).CurrentUser.GetPermissions(key).SendMessages) - return Task.CompletedTask; - if (PresenceUpdates.TryRemove(key, out var msgs)) - { - var title = GetText(key.Guild, strs.presence_updates); - var desc = string.Join(Environment.NewLine, msgs); - return key.SendConfirmAsync(_eb, title, desc.TrimTo(2048)); - } - + if (!((SocketGuild) key.Guild).CurrentUser.GetPermissions(key).SendMessages) return Task.CompletedTask; - })).ConfigureAwait(false); - }, null, TimeSpan.FromSeconds(15), TimeSpan.FromSeconds(15)); + if (PresenceUpdates.TryRemove(key, out var msgs)) + { + var title = GetText(key.Guild, strs.presence_updates); + var desc = string.Join(Environment.NewLine, msgs); + return key.SendConfirmAsync(_eb, title, desc.TrimTo(2048)); + } - //_client.MessageReceived += _client_MessageReceived; - _client.MessageUpdated += _client_MessageUpdated; - _client.MessageDeleted += _client_MessageDeleted; - _client.UserBanned += _client_UserBanned; - _client.UserUnbanned += _client_UserUnbanned; - _client.UserJoined += _client_UserJoined; - _client.UserLeft += _client_UserLeft; - //_client.UserPresenceUpdated += _client_UserPresenceUpdated; - _client.UserVoiceStateUpdated += _client_UserVoiceStateUpdated; - _client.UserVoiceStateUpdated += _client_UserVoiceStateUpdated_TTS; - _client.GuildMemberUpdated += _client_GuildUserUpdated; - _client.UserUpdated += _client_UserUpdated; - _client.ChannelCreated += _client_ChannelCreated; - _client.ChannelDestroyed += _client_ChannelDestroyed; - _client.ChannelUpdated += _client_ChannelUpdated; - _client.RoleDeleted += _client_RoleDeleted; + return Task.CompletedTask; + })).ConfigureAwait(false); + }, null, TimeSpan.FromSeconds(15), TimeSpan.FromSeconds(15)); - _mute.UserMuted += MuteCommands_UserMuted; - _mute.UserUnmuted += MuteCommands_UserUnmuted; + //_client.MessageReceived += _client_MessageReceived; + _client.MessageUpdated += _client_MessageUpdated; + _client.MessageDeleted += _client_MessageDeleted; + _client.UserBanned += _client_UserBanned; + _client.UserUnbanned += _client_UserUnbanned; + _client.UserJoined += _client_UserJoined; + _client.UserLeft += _client_UserLeft; + //_client.UserPresenceUpdated += _client_UserPresenceUpdated; + _client.UserVoiceStateUpdated += _client_UserVoiceStateUpdated; + _client.UserVoiceStateUpdated += _client_UserVoiceStateUpdated_TTS; + _client.GuildMemberUpdated += _client_GuildUserUpdated; + _client.UserUpdated += _client_UserUpdated; + _client.ChannelCreated += _client_ChannelCreated; + _client.ChannelDestroyed += _client_ChannelDestroyed; + _client.ChannelUpdated += _client_ChannelUpdated; + _client.RoleDeleted += _client_RoleDeleted; - _prot.OnAntiProtectionTriggered += TriggeredAntiProtection; + _mute.UserMuted += MuteCommands_UserMuted; + _mute.UserUnmuted += MuteCommands_UserUnmuted; - _clearTimer = new Timer(_ => - { - _ignoreMessageIds.Clear(); - }, null, TimeSpan.FromHours(1), TimeSpan.FromHours(1)); + _prot.OnAntiProtectionTriggered += TriggeredAntiProtection; + + _clearTimer = new Timer(_ => + { + _ignoreMessageIds.Clear(); + }, null, TimeSpan.FromHours(1), TimeSpan.FromHours(1)); #endif - } + } - public LogSetting GetGuildLogSettings(ulong guildId) - { - GuildLogSettings.TryGetValue(guildId, out LogSetting logSetting); - return logSetting; - } + public LogSetting GetGuildLogSettings(ulong guildId) + { + GuildLogSettings.TryGetValue(guildId, out LogSetting logSetting); + return logSetting; + } - public void AddDeleteIgnore(ulong messageId) - { - _ignoreMessageIds.Add(messageId); - } + public void AddDeleteIgnore(ulong messageId) + { + _ignoreMessageIds.Add(messageId); + } - public bool LogIgnore(ulong gid, ulong itemId, IgnoredItemType itemType) + public bool LogIgnore(ulong gid, ulong itemId, IgnoredItemType itemType) + { + int removed = 0; + using (var uow = _db.GetDbContext()) { - int removed = 0; - using (var uow = _db.GetDbContext()) - { - var logSetting = uow.LogSettingsFor(gid); - removed = logSetting.LogIgnores - .RemoveAll(x => x.ItemType == itemType && itemId == x.LogItemId); + var logSetting = uow.LogSettingsFor(gid); + removed = logSetting.LogIgnores + .RemoveAll(x => x.ItemType == itemType && itemId == x.LogItemId); - if (removed == 0) - { - var toAdd = new IgnoredLogItem { LogItemId = itemId, ItemType = itemType}; - logSetting.LogIgnores.Add(toAdd); - } - - uow.SaveChanges(); - GuildLogSettings.AddOrUpdate(gid, logSetting, (_, _) => logSetting); + if (removed == 0) + { + var toAdd = new IgnoredLogItem { LogItemId = itemId, ItemType = itemType}; + logSetting.LogIgnores.Add(toAdd); } - return removed > 0; + uow.SaveChanges(); + GuildLogSettings.AddOrUpdate(gid, logSetting, (_, _) => logSetting); } + + return removed > 0; + } - private string GetText(IGuild guild, LocStr str) => - _strings.GetText(str, guild.Id); + private string GetText(IGuild guild, LocStr str) => + _strings.GetText(str, guild.Id); - private string PrettyCurrentTime(IGuild g) + private string PrettyCurrentTime(IGuild g) + { + var time = DateTime.UtcNow; + if (g != null) + time = TimeZoneInfo.ConvertTime(time, _tz.GetTimeZoneOrUtc(g.Id)); + return $"【{time:HH:mm:ss}】"; + } + + private string CurrentTime(IGuild g) + { + DateTime time = DateTime.UtcNow; + if (g != null) + time = TimeZoneInfo.ConvertTime(time, _tz.GetTimeZoneOrUtc(g.Id)); + + return $"{time:HH:mm:ss}"; + } + + public async Task LogServer(ulong guildId, ulong channelId, bool value) + { + using (var uow = _db.GetDbContext()) { - var time = DateTime.UtcNow; - if (g != null) - time = TimeZoneInfo.ConvertTime(time, _tz.GetTimeZoneOrUtc(g.Id)); - return $"【{time:HH:mm:ss}】"; - } - - private string CurrentTime(IGuild g) - { - DateTime time = DateTime.UtcNow; - if (g != null) - time = TimeZoneInfo.ConvertTime(time, _tz.GetTimeZoneOrUtc(g.Id)); - - return $"{time:HH:mm:ss}"; - } - - public async Task LogServer(ulong guildId, ulong channelId, bool value) - { - using (var uow = _db.GetDbContext()) - { - var logSetting = uow.LogSettingsFor(guildId); + var logSetting = uow.LogSettingsFor(guildId); - logSetting.LogOtherId = + logSetting.LogOtherId = logSetting.MessageUpdatedId = - logSetting.MessageDeletedId = - logSetting.UserJoinedId = - logSetting.UserLeftId = - logSetting.UserBannedId = - logSetting.UserUnbannedId = - logSetting.UserUpdatedId = - logSetting.ChannelCreatedId = - logSetting.ChannelDestroyedId = - logSetting.ChannelUpdatedId = - logSetting.LogUserPresenceId = - logSetting.LogVoicePresenceId = - logSetting.UserMutedId = - logSetting.LogVoicePresenceTTSId = - (value ? channelId : (ulong?) null); -; - await uow.SaveChangesAsync(); - GuildLogSettings.AddOrUpdate(guildId, (id) => logSetting, (id, old) => logSetting); - } + logSetting.MessageDeletedId = + logSetting.UserJoinedId = + logSetting.UserLeftId = + logSetting.UserBannedId = + logSetting.UserUnbannedId = + logSetting.UserUpdatedId = + logSetting.ChannelCreatedId = + logSetting.ChannelDestroyedId = + logSetting.ChannelUpdatedId = + logSetting.LogUserPresenceId = + logSetting.LogVoicePresenceId = + logSetting.UserMutedId = + logSetting.LogVoicePresenceTTSId = + (value ? channelId : (ulong?) null); + ; + await uow.SaveChangesAsync(); + GuildLogSettings.AddOrUpdate(guildId, (id) => logSetting, (id, old) => logSetting); } + } - private Task _client_UserUpdated(SocketUser before, SocketUser uAfter) + private Task _client_UserUpdated(SocketUser before, SocketUser uAfter) + { + var _ = Task.Run(async () => { - var _ = Task.Run(async () => + try { - try - { - if (!(uAfter is SocketGuildUser after)) - return; + if (!(uAfter is SocketGuildUser after)) + return; - var g = after.Guild; + var g = after.Guild; - if (!GuildLogSettings.TryGetValue(g.Id, out LogSetting logSetting) - || (logSetting.UserUpdatedId is null)) - return; + if (!GuildLogSettings.TryGetValue(g.Id, out LogSetting logSetting) + || (logSetting.UserUpdatedId is null)) + return; - ITextChannel logChannel; - if ((logChannel = + ITextChannel logChannel; + if ((logChannel = await TryGetLogChannel(g, logSetting, LogType.UserUpdated).ConfigureAwait(false)) is null) - return; + return; - var embed = _eb.Create(); + var embed = _eb.Create(); - if (before.Username != after.Username) - { - embed.WithTitle("👥 " + GetText(g, strs.username_changed)) - .WithDescription($"{before.Username}#{before.Discriminator} | {before.Id}") - .AddField("Old Name", $"{before.Username}", true) - .AddField("New Name", $"{after.Username}", true) - .WithFooter(CurrentTime(g)) - .WithOkColor(); - } - else if (before.AvatarId != after.AvatarId) - { - embed.WithTitle("👥" + GetText(g, strs.avatar_changed)) - .WithDescription($"{before.Username}#{before.Discriminator} | {before.Id}") - .WithFooter(CurrentTime(g)) - .WithOkColor(); - - var bav = before.RealAvatarUrl(); - if (bav != null && bav.IsAbsoluteUri) - embed.WithThumbnailUrl(bav.ToString()); - - var aav = after.RealAvatarUrl(); - if (aav != null && aav.IsAbsoluteUri) - embed.WithImageUrl(aav.ToString()); - } - else - { - return; - } - - await logChannel.EmbedAsync(embed).ConfigureAwait(false); - } - catch + if (before.Username != after.Username) { - // ignored + embed.WithTitle("👥 " + GetText(g, strs.username_changed)) + .WithDescription($"{before.Username}#{before.Discriminator} | {before.Id}") + .AddField("Old Name", $"{before.Username}", true) + .AddField("New Name", $"{after.Username}", true) + .WithFooter(CurrentTime(g)) + .WithOkColor(); } - }); - return Task.CompletedTask; - } + else if (before.AvatarId != after.AvatarId) + { + embed.WithTitle("👥" + GetText(g, strs.avatar_changed)) + .WithDescription($"{before.Username}#{before.Discriminator} | {before.Id}") + .WithFooter(CurrentTime(g)) + .WithOkColor(); - public bool Log(ulong gid, ulong? cid, LogType type/*, string options*/) - { - ulong? channelId = null; - using (var uow = _db.GetDbContext()) + var bav = before.RealAvatarUrl(); + if (bav != null && bav.IsAbsoluteUri) + embed.WithThumbnailUrl(bav.ToString()); + + var aav = after.RealAvatarUrl(); + if (aav != null && aav.IsAbsoluteUri) + embed.WithImageUrl(aav.ToString()); + } + else + { + return; + } + + await logChannel.EmbedAsync(embed).ConfigureAwait(false); + } + catch { - var logSetting = uow.LogSettingsFor(gid); - GuildLogSettings.AddOrUpdate(gid, (id) => logSetting, (id, old) => logSetting); - switch (type) - { - case LogType.Other: - channelId = logSetting.LogOtherId = (logSetting.LogOtherId is null ? cid : default); - break; - case LogType.MessageUpdated: - channelId = logSetting.MessageUpdatedId = (logSetting.MessageUpdatedId is null ? cid : default); - break; - case LogType.MessageDeleted: - channelId = logSetting.MessageDeletedId = (logSetting.MessageDeletedId is null ? cid : default); - //logSetting.DontLogBotMessageDeleted = (options == "nobot"); - break; - case LogType.UserJoined: - channelId = logSetting.UserJoinedId = (logSetting.UserJoinedId is null ? cid : default); - break; - case LogType.UserLeft: - channelId = logSetting.UserLeftId = (logSetting.UserLeftId is null ? cid : default); - break; - case LogType.UserBanned: - channelId = logSetting.UserBannedId = (logSetting.UserBannedId is null ? cid : default); - break; - case LogType.UserUnbanned: - channelId = logSetting.UserUnbannedId = (logSetting.UserUnbannedId is null ? cid : default); - break; - case LogType.UserUpdated: - channelId = logSetting.UserUpdatedId = (logSetting.UserUpdatedId is null ? cid : default); - break; - case LogType.UserMuted: - channelId = logSetting.UserMutedId = (logSetting.UserMutedId is null ? cid : default); - break; - case LogType.ChannelCreated: - channelId = logSetting.ChannelCreatedId = (logSetting.ChannelCreatedId is null ? cid : default); - break; - case LogType.ChannelDestroyed: - channelId = logSetting.ChannelDestroyedId = - (logSetting.ChannelDestroyedId is null ? cid : default); - break; - case LogType.ChannelUpdated: - channelId = logSetting.ChannelUpdatedId = (logSetting.ChannelUpdatedId is null ? cid : default); - break; - case LogType.UserPresence: - channelId = logSetting.LogUserPresenceId = - (logSetting.LogUserPresenceId is null ? cid : default); - break; - case LogType.VoicePresence: - channelId = logSetting.LogVoicePresenceId = - (logSetting.LogVoicePresenceId is null ? cid : default); - break; - case LogType.VoicePresenceTTS: - channelId = logSetting.LogVoicePresenceTTSId = - (logSetting.LogVoicePresenceTTSId is null ? cid : default); - break; - } + // ignored + } + }); + return Task.CompletedTask; + } - uow.SaveChanges(); + public bool Log(ulong gid, ulong? cid, LogType type/*, string options*/) + { + ulong? channelId = null; + using (var uow = _db.GetDbContext()) + { + var logSetting = uow.LogSettingsFor(gid); + GuildLogSettings.AddOrUpdate(gid, (id) => logSetting, (id, old) => logSetting); + switch (type) + { + case LogType.Other: + channelId = logSetting.LogOtherId = (logSetting.LogOtherId is null ? cid : default); + break; + case LogType.MessageUpdated: + channelId = logSetting.MessageUpdatedId = (logSetting.MessageUpdatedId is null ? cid : default); + break; + case LogType.MessageDeleted: + channelId = logSetting.MessageDeletedId = (logSetting.MessageDeletedId is null ? cid : default); + //logSetting.DontLogBotMessageDeleted = (options == "nobot"); + break; + case LogType.UserJoined: + channelId = logSetting.UserJoinedId = (logSetting.UserJoinedId is null ? cid : default); + break; + case LogType.UserLeft: + channelId = logSetting.UserLeftId = (logSetting.UserLeftId is null ? cid : default); + break; + case LogType.UserBanned: + channelId = logSetting.UserBannedId = (logSetting.UserBannedId is null ? cid : default); + break; + case LogType.UserUnbanned: + channelId = logSetting.UserUnbannedId = (logSetting.UserUnbannedId is null ? cid : default); + break; + case LogType.UserUpdated: + channelId = logSetting.UserUpdatedId = (logSetting.UserUpdatedId is null ? cid : default); + break; + case LogType.UserMuted: + channelId = logSetting.UserMutedId = (logSetting.UserMutedId is null ? cid : default); + break; + case LogType.ChannelCreated: + channelId = logSetting.ChannelCreatedId = (logSetting.ChannelCreatedId is null ? cid : default); + break; + case LogType.ChannelDestroyed: + channelId = logSetting.ChannelDestroyedId = + (logSetting.ChannelDestroyedId is null ? cid : default); + break; + case LogType.ChannelUpdated: + channelId = logSetting.ChannelUpdatedId = (logSetting.ChannelUpdatedId is null ? cid : default); + break; + case LogType.UserPresence: + channelId = logSetting.LogUserPresenceId = + (logSetting.LogUserPresenceId is null ? cid : default); + break; + case LogType.VoicePresence: + channelId = logSetting.LogVoicePresenceId = + (logSetting.LogVoicePresenceId is null ? cid : default); + break; + case LogType.VoicePresenceTTS: + channelId = logSetting.LogVoicePresenceTTSId = + (logSetting.LogVoicePresenceTTSId is null ? cid : default); + break; } - return channelId != null; + uow.SaveChanges(); } - private Task _client_UserVoiceStateUpdated_TTS(SocketUser iusr, SocketVoiceState before, SocketVoiceState after) + return channelId != null; + } + + private Task _client_UserVoiceStateUpdated_TTS(SocketUser iusr, SocketVoiceState before, SocketVoiceState after) + { + var _ = Task.Run(async () => { - var _ = Task.Run(async () => + try { - try - { - if (!(iusr is IGuildUser usr)) - return; + if (!(iusr is IGuildUser usr)) + return; - var beforeVch = before.VoiceChannel; - var afterVch = after.VoiceChannel; + var beforeVch = before.VoiceChannel; + var afterVch = after.VoiceChannel; - if (beforeVch == afterVch) - return; + if (beforeVch == afterVch) + return; - if (!GuildLogSettings.TryGetValue(usr.Guild.Id, out LogSetting logSetting) - || (logSetting.LogVoicePresenceTTSId is null)) - return; + if (!GuildLogSettings.TryGetValue(usr.Guild.Id, out LogSetting logSetting) + || (logSetting.LogVoicePresenceTTSId is null)) + return; - ITextChannel logChannel; - if ((logChannel = await TryGetLogChannel(usr.Guild, logSetting, LogType.VoicePresenceTTS) + ITextChannel logChannel; + if ((logChannel = await TryGetLogChannel(usr.Guild, logSetting, LogType.VoicePresenceTTS) .ConfigureAwait(false)) is null) - return; + return; - var str = ""; - if (beforeVch?.Guild == afterVch?.Guild) - { - str = GetText(logChannel.Guild, strs.log_vc_moved(usr.Username, beforeVch?.Name, afterVch?.Name)); - } - else if (beforeVch is null) - { - str = GetText(logChannel.Guild, strs.log_vc_joined(usr.Username, afterVch.Name)); - } - else if (afterVch is null) - { - str = GetText(logChannel.Guild, strs.log_vc_left(usr.Username, beforeVch.Name)); - } - - var toDelete = await logChannel.SendMessageAsync(str, true).ConfigureAwait(false); - toDelete.DeleteAfter(5); - } - catch + var str = ""; + if (beforeVch?.Guild == afterVch?.Guild) { - // ignored + str = GetText(logChannel.Guild, strs.log_vc_moved(usr.Username, beforeVch?.Name, afterVch?.Name)); + } + else if (beforeVch is null) + { + str = GetText(logChannel.Guild, strs.log_vc_joined(usr.Username, afterVch.Name)); + } + else if (afterVch is null) + { + str = GetText(logChannel.Guild, strs.log_vc_left(usr.Username, beforeVch.Name)); } - }); - return Task.CompletedTask; - } - private void MuteCommands_UserMuted(IGuildUser usr, IUser mod, MuteType muteType, string reason) + var toDelete = await logChannel.SendMessageAsync(str, true).ConfigureAwait(false); + toDelete.DeleteAfter(5); + } + catch + { + // ignored + } + }); + return Task.CompletedTask; + } + + private void MuteCommands_UserMuted(IGuildUser usr, IUser mod, MuteType muteType, string reason) + { + var _ = Task.Run(async () => { - var _ = Task.Run(async () => + try { - try - { - if (!GuildLogSettings.TryGetValue(usr.Guild.Id, out LogSetting logSetting) - || (logSetting.UserMutedId is null)) - return; + if (!GuildLogSettings.TryGetValue(usr.Guild.Id, out LogSetting logSetting) + || (logSetting.UserMutedId is null)) + return; - ITextChannel logChannel; - if ((logChannel = await TryGetLogChannel(usr.Guild, logSetting, LogType.UserMuted) + ITextChannel logChannel; + if ((logChannel = await TryGetLogChannel(usr.Guild, logSetting, LogType.UserMuted) .ConfigureAwait(false)) is null) - return; - var mutes = ""; - var mutedLocalized = GetText(logChannel.Guild, strs.muted_sn); - switch (muteType) - { - case MuteType.Voice: - mutes = "🔇 " + GetText(logChannel.Guild, strs.xmuted_voice(mutedLocalized, mod.ToString())); - break; - case MuteType.Chat: - mutes = "🔇 " + GetText(logChannel.Guild, strs.xmuted_text(mutedLocalized, mod.ToString())); - break; - case MuteType.All: - mutes = "🔇 " + GetText(logChannel.Guild, strs.xmuted_text_and_voice(mutedLocalized, - mod.ToString())); - break; - } - - var embed = _eb.Create().WithAuthor(mutes) - .WithTitle($"{usr.Username}#{usr.Discriminator} | {usr.Id}") - .WithFooter(CurrentTime(usr.Guild)) - .WithOkColor(); - - await logChannel.EmbedAsync(embed).ConfigureAwait(false); - } - catch + return; + var mutes = ""; + var mutedLocalized = GetText(logChannel.Guild, strs.muted_sn); + switch (muteType) { - // ignored + case MuteType.Voice: + mutes = "🔇 " + GetText(logChannel.Guild, strs.xmuted_voice(mutedLocalized, mod.ToString())); + break; + case MuteType.Chat: + mutes = "🔇 " + GetText(logChannel.Guild, strs.xmuted_text(mutedLocalized, mod.ToString())); + break; + case MuteType.All: + mutes = "🔇 " + GetText(logChannel.Guild, strs.xmuted_text_and_voice(mutedLocalized, + mod.ToString())); + break; } - }); - } - private void MuteCommands_UserUnmuted(IGuildUser usr, IUser mod, MuteType muteType, string reason) + var embed = _eb.Create().WithAuthor(mutes) + .WithTitle($"{usr.Username}#{usr.Discriminator} | {usr.Id}") + .WithFooter(CurrentTime(usr.Guild)) + .WithOkColor(); + + await logChannel.EmbedAsync(embed).ConfigureAwait(false); + } + catch + { + // ignored + } + }); + } + + private void MuteCommands_UserUnmuted(IGuildUser usr, IUser mod, MuteType muteType, string reason) + { + var _ = Task.Run(async () => { - var _ = Task.Run(async () => + try { - try - { - if (!GuildLogSettings.TryGetValue(usr.Guild.Id, out LogSetting logSetting) - || (logSetting.UserMutedId is null)) - return; + if (!GuildLogSettings.TryGetValue(usr.Guild.Id, out LogSetting logSetting) + || (logSetting.UserMutedId is null)) + return; - ITextChannel logChannel; - if ((logChannel = await TryGetLogChannel(usr.Guild, logSetting, LogType.UserMuted) + ITextChannel logChannel; + if ((logChannel = await TryGetLogChannel(usr.Guild, logSetting, LogType.UserMuted) .ConfigureAwait(false)) is null) - return; + return; - var mutes = ""; - var unmutedLocalized = GetText(logChannel.Guild, strs.unmuted_sn); - switch (muteType) - { - case MuteType.Voice: - mutes = "🔊 " + GetText(logChannel.Guild, strs.xmuted_voice(unmutedLocalized, mod.ToString())); - break; - case MuteType.Chat: - mutes = "🔊 " + GetText(logChannel.Guild, strs.xmuted_text(unmutedLocalized, mod.ToString())); - break; - case MuteType.All: - mutes = "🔊 " + GetText(logChannel.Guild, strs.xmuted_text_and_voice(unmutedLocalized, - mod.ToString())); - break; - } - - var embed = _eb.Create().WithAuthor(mutes) - .WithTitle($"{usr.Username}#{usr.Discriminator} | {usr.Id}") - .WithFooter($"{CurrentTime(usr.Guild)}") - .WithOkColor(); - - if (!string.IsNullOrWhiteSpace(reason)) - embed.WithDescription(reason); - - await logChannel.EmbedAsync(embed).ConfigureAwait(false); - } - catch + var mutes = ""; + var unmutedLocalized = GetText(logChannel.Guild, strs.unmuted_sn); + switch (muteType) { - // ignored + case MuteType.Voice: + mutes = "🔊 " + GetText(logChannel.Guild, strs.xmuted_voice(unmutedLocalized, mod.ToString())); + break; + case MuteType.Chat: + mutes = "🔊 " + GetText(logChannel.Guild, strs.xmuted_text(unmutedLocalized, mod.ToString())); + break; + case MuteType.All: + mutes = "🔊 " + GetText(logChannel.Guild, strs.xmuted_text_and_voice(unmutedLocalized, + mod.ToString())); + break; } - }); - } - public Task TriggeredAntiProtection(PunishmentAction action, ProtectionType protection, - params IGuildUser[] users) + var embed = _eb.Create().WithAuthor(mutes) + .WithTitle($"{usr.Username}#{usr.Discriminator} | {usr.Id}") + .WithFooter($"{CurrentTime(usr.Guild)}") + .WithOkColor(); + + if (!string.IsNullOrWhiteSpace(reason)) + embed.WithDescription(reason); + + await logChannel.EmbedAsync(embed).ConfigureAwait(false); + } + catch + { + // ignored + } + }); + } + + public Task TriggeredAntiProtection(PunishmentAction action, ProtectionType protection, + params IGuildUser[] users) + { + var _ = Task.Run(async () => { - var _ = Task.Run(async () => + try { - try - { - if (users.Length == 0) - return; + if (users.Length == 0) + return; - if (!GuildLogSettings.TryGetValue(users.First().Guild.Id, out LogSetting logSetting) - || (logSetting.LogOtherId is null)) - return; - ITextChannel logChannel; - if ((logChannel = await TryGetLogChannel(users.First().Guild, logSetting, LogType.Other) + if (!GuildLogSettings.TryGetValue(users.First().Guild.Id, out LogSetting logSetting) + || (logSetting.LogOtherId is null)) + return; + ITextChannel logChannel; + if ((logChannel = await TryGetLogChannel(users.First().Guild, logSetting, LogType.Other) .ConfigureAwait(false)) is null) - return; + return; - var punishment = ""; - switch (action) - { - case PunishmentAction.Mute: - punishment = "🔇 " + GetText(logChannel.Guild, strs.muted_pl).ToUpperInvariant(); - break; - case PunishmentAction.Kick: - punishment = "👢 " + GetText(logChannel.Guild, strs.kicked_pl).ToUpperInvariant(); - break; - case PunishmentAction.Softban: - punishment = "☣ " + GetText(logChannel.Guild, strs.soft_banned_pl).ToUpperInvariant(); - break; - case PunishmentAction.Ban: - punishment = "⛔️ " + GetText(logChannel.Guild, strs.banned_pl).ToUpperInvariant(); - break; - case PunishmentAction.RemoveRoles: - punishment = "⛔️ " + GetText(logChannel.Guild, strs.remove_roles_pl).ToUpperInvariant(); - break; - } - - var embed = _eb.Create().WithAuthor($"🛡 Anti-{protection}") - .WithTitle(GetText(logChannel.Guild, strs.users) + " " + punishment) - .WithDescription(string.Join("\n", users.Select(u => u.ToString()))) - .WithFooter(CurrentTime(logChannel.Guild)) - .WithOkColor(); - - await logChannel.EmbedAsync(embed).ConfigureAwait(false); - } - catch + var punishment = ""; + switch (action) { - // ignored + case PunishmentAction.Mute: + punishment = "🔇 " + GetText(logChannel.Guild, strs.muted_pl).ToUpperInvariant(); + break; + case PunishmentAction.Kick: + punishment = "👢 " + GetText(logChannel.Guild, strs.kicked_pl).ToUpperInvariant(); + break; + case PunishmentAction.Softban: + punishment = "☣ " + GetText(logChannel.Guild, strs.soft_banned_pl).ToUpperInvariant(); + break; + case PunishmentAction.Ban: + punishment = "⛔️ " + GetText(logChannel.Guild, strs.banned_pl).ToUpperInvariant(); + break; + case PunishmentAction.RemoveRoles: + punishment = "⛔️ " + GetText(logChannel.Guild, strs.remove_roles_pl).ToUpperInvariant(); + break; } - }); - return Task.CompletedTask; - } - private string GetRoleDeletedKey(ulong roleId) - => $"role_deleted_{roleId}"; + var embed = _eb.Create().WithAuthor($"🛡 Anti-{protection}") + .WithTitle(GetText(logChannel.Guild, strs.users) + " " + punishment) + .WithDescription(string.Join("\n", users.Select(u => u.ToString()))) + .WithFooter(CurrentTime(logChannel.Guild)) + .WithOkColor(); + + await logChannel.EmbedAsync(embed).ConfigureAwait(false); + } + catch + { + // ignored + } + }); + return Task.CompletedTask; + } + + private string GetRoleDeletedKey(ulong roleId) + => $"role_deleted_{roleId}"; - private Task _client_RoleDeleted(SocketRole socketRole) - { - Serilog.Log.Information("Role deleted {RoleId}", socketRole.Id); - _memoryCache.Set(GetRoleDeletedKey(socketRole.Id), - true, - TimeSpan.FromMinutes(5)); - return Task.CompletedTask; - } + private Task _client_RoleDeleted(SocketRole socketRole) + { + Serilog.Log.Information("Role deleted {RoleId}", socketRole.Id); + _memoryCache.Set(GetRoleDeletedKey(socketRole.Id), + true, + TimeSpan.FromMinutes(5)); + return Task.CompletedTask; + } - private bool IsRoleDeleted(ulong roleId) - { - var isDeleted = _memoryCache.TryGetValue(GetRoleDeletedKey(roleId), out var _); - return isDeleted; - } + private bool IsRoleDeleted(ulong roleId) + { + var isDeleted = _memoryCache.TryGetValue(GetRoleDeletedKey(roleId), out var _); + return isDeleted; + } - private Task _client_GuildUserUpdated(SocketGuildUser before, SocketGuildUser after) + private Task _client_GuildUserUpdated(SocketGuildUser before, SocketGuildUser after) + { + var _ = Task.Run(async () => { - var _ = Task.Run(async () => + try { - try - { - if (!GuildLogSettings.TryGetValue(before.Guild.Id, out LogSetting logSetting) - || logSetting.LogIgnores.Any(ilc => ilc.LogItemId == after.Id && ilc.ItemType == IgnoredItemType.User)) - return; + if (!GuildLogSettings.TryGetValue(before.Guild.Id, out LogSetting logSetting) + || logSetting.LogIgnores.Any(ilc => ilc.LogItemId == after.Id && ilc.ItemType == IgnoredItemType.User)) + return; - ITextChannel logChannel; - if (logSetting.UserUpdatedId != null && - (logChannel = await TryGetLogChannel(before.Guild, logSetting, LogType.UserUpdated) - .ConfigureAwait(false)) != null) + ITextChannel logChannel; + if (logSetting.UserUpdatedId != null && + (logChannel = await TryGetLogChannel(before.Guild, logSetting, LogType.UserUpdated) + .ConfigureAwait(false)) != null) + { + var embed = _eb.Create().WithOkColor() + .WithFooter(CurrentTime(before.Guild)) + .WithTitle($"{before.Username}#{before.Discriminator} | {before.Id}"); + if (before.Nickname != after.Nickname) { - var embed = _eb.Create().WithOkColor() - .WithFooter(CurrentTime(before.Guild)) - .WithTitle($"{before.Username}#{before.Discriminator} | {before.Id}"); - if (before.Nickname != after.Nickname) + embed.WithAuthor("👥 " + GetText(logChannel.Guild, strs.nick_change)) + .AddField(GetText(logChannel.Guild, strs.old_nick) + , $"{before.Nickname}#{before.Discriminator}") + .AddField(GetText(logChannel.Guild, strs.new_nick) + , $"{after.Nickname}#{after.Discriminator}"); + + await logChannel.EmbedAsync(embed).ConfigureAwait(false); + } + else if (!before.Roles.SequenceEqual(after.Roles)) + { + if (before.Roles.Count < after.Roles.Count) { - embed.WithAuthor("👥 " + GetText(logChannel.Guild, strs.nick_change)) - .AddField(GetText(logChannel.Guild, strs.old_nick) - , $"{before.Nickname}#{before.Discriminator}") - .AddField(GetText(logChannel.Guild, strs.new_nick) - , $"{after.Nickname}#{after.Discriminator}"); + var diffRoles = after.Roles.Where(r => !before.Roles.Contains(r)).Select(r => r.Name); + embed.WithAuthor("⚔ " + GetText(logChannel.Guild, strs.user_role_add)) + .WithDescription(string.Join(", ", diffRoles).SanitizeMentions()); await logChannel.EmbedAsync(embed).ConfigureAwait(false); } - else if (!before.Roles.SequenceEqual(after.Roles)) + else if (before.Roles.Count > after.Roles.Count) { - if (before.Roles.Count < after.Roles.Count) + await Task.Delay(1000); + var diffRoles = before.Roles + .Where(r => !after.Roles.Contains(r) && !IsRoleDeleted(r.Id)) + .Select(r => r.Name) + .ToList(); + + if (diffRoles.Any()) { - var diffRoles = after.Roles.Where(r => !before.Roles.Contains(r)).Select(r => r.Name); - embed.WithAuthor("⚔ " + GetText(logChannel.Guild, strs.user_role_add)) + embed.WithAuthor("⚔ " + GetText(logChannel.Guild, strs.user_role_rem)) .WithDescription(string.Join(", ", diffRoles).SanitizeMentions()); await logChannel.EmbedAsync(embed).ConfigureAwait(false); } - else if (before.Roles.Count > after.Roles.Count) - { - await Task.Delay(1000); - var diffRoles = before.Roles - .Where(r => !after.Roles.Contains(r) && !IsRoleDeleted(r.Id)) - .Select(r => r.Name) - .ToList(); - - if (diffRoles.Any()) - { - embed.WithAuthor("⚔ " + GetText(logChannel.Guild, strs.user_role_rem)) - .WithDescription(string.Join(", ", diffRoles).SanitizeMentions()); - - await logChannel.EmbedAsync(embed).ConfigureAwait(false); - } - } } } + } - logChannel = null; - if (!before.IsBot && logSetting.LogUserPresenceId != null && (logChannel = + logChannel = null; + if (!before.IsBot && logSetting.LogUserPresenceId != null && (logChannel = await TryGetLogChannel(before.Guild, logSetting, LogType.UserPresence) .ConfigureAwait(false)) != null) + { + if (before.Status != after.Status) { - if (before.Status != after.Status) - { - var str = "🎭" + Format.Code(PrettyCurrentTime(after.Guild)) + - GetText(logChannel.Guild, strs.user_status_change( - "👤" + Format.Bold(after.Username), - Format.Bold(after.Status.ToString()))); - PresenceUpdates.AddOrUpdate(logChannel, - new List() {str}, (id, list) => - { - list.Add(str); - return list; - }); - } - else if (before.Activity?.Name != after.Activity?.Name) - { - var str = - $"👾`{PrettyCurrentTime(after.Guild)}`👤__**{after.Username}**__ is now playing **{after.Activity?.Name ?? "-"}**."; - PresenceUpdates.AddOrUpdate(logChannel, - new List() {str}, (id, list) => - { - list.Add(str); - return list; - }); - } + var str = "🎭" + Format.Code(PrettyCurrentTime(after.Guild)) + + GetText(logChannel.Guild, strs.user_status_change( + "👤" + Format.Bold(after.Username), + Format.Bold(after.Status.ToString()))); + PresenceUpdates.AddOrUpdate(logChannel, + new List() {str}, (id, list) => + { + list.Add(str); + return list; + }); + } + else if (before.Activity?.Name != after.Activity?.Name) + { + var str = + $"👾`{PrettyCurrentTime(after.Guild)}`👤__**{after.Username}**__ is now playing **{after.Activity?.Name ?? "-"}**."; + PresenceUpdates.AddOrUpdate(logChannel, + new List() {str}, (id, list) => + { + list.Add(str); + return list; + }); } } - catch - { - // ignored - } - }); - return Task.CompletedTask; - } - - private Task _client_ChannelUpdated(IChannel cbefore, IChannel cafter) - { - var _ = Task.Run(async () => + } + catch { - try - { - if (!(cbefore is IGuildChannel before)) - return; + // ignored + } + }); + return Task.CompletedTask; + } - var after = (IGuildChannel) cafter; - - if (!GuildLogSettings.TryGetValue(before.Guild.Id, out LogSetting logSetting) - || (logSetting.ChannelUpdatedId is null) - || logSetting.LogIgnores.Any(ilc => ilc.LogItemId == after.Id && ilc.ItemType == IgnoredItemType.Channel)) - return; - - ITextChannel logChannel; - if ((logChannel = await TryGetLogChannel(before.Guild, logSetting, LogType.ChannelUpdated) - .ConfigureAwait(false)) is null) - return; - - var embed = _eb.Create().WithOkColor() - .WithFooter(CurrentTime(before.Guild)); - - var beforeTextChannel = cbefore as ITextChannel; - var afterTextChannel = cafter as ITextChannel; - - if (before.Name != after.Name) - { - embed.WithTitle("ℹ️ " + GetText(logChannel.Guild, strs.ch_name_change)) - .WithDescription($"{after} | {after.Id}") - .AddField(GetText(logChannel.Guild, strs.ch_old_name), before.Name); - } - else if (beforeTextChannel?.Topic != afterTextChannel?.Topic) - { - embed.WithTitle("ℹ️ " + GetText(logChannel.Guild, strs.ch_topic_change)) - .WithDescription($"{after} | {after.Id}") - .AddField(GetText(logChannel.Guild, strs.old_topic) , beforeTextChannel?.Topic ?? "-") - .AddField(GetText(logChannel.Guild, strs.new_topic), afterTextChannel?.Topic ?? "-"); - } - else - return; - - await logChannel.EmbedAsync(embed).ConfigureAwait(false); - } - catch - { - // ignored - } - }); - return Task.CompletedTask; - } - - private Task _client_ChannelDestroyed(IChannel ich) + private Task _client_ChannelUpdated(IChannel cbefore, IChannel cafter) + { + var _ = Task.Run(async () => { - var _ = Task.Run(async () => + try { - try - { - if (!(ich is IGuildChannel ch)) - return; + if (!(cbefore is IGuildChannel before)) + return; - if (!GuildLogSettings.TryGetValue(ch.Guild.Id, out LogSetting logSetting) - || (logSetting.ChannelDestroyedId is null) - || logSetting.LogIgnores.Any(ilc => ilc.LogItemId == ch.Id && ilc.ItemType == IgnoredItemType.Channel)) - return; + var after = (IGuildChannel) cafter; - ITextChannel logChannel; - if ((logChannel = await TryGetLogChannel(ch.Guild, logSetting, LogType.ChannelDestroyed) + if (!GuildLogSettings.TryGetValue(before.Guild.Id, out LogSetting logSetting) + || (logSetting.ChannelUpdatedId is null) + || logSetting.LogIgnores.Any(ilc => ilc.LogItemId == after.Id && ilc.ItemType == IgnoredItemType.Channel)) + return; + + ITextChannel logChannel; + if ((logChannel = await TryGetLogChannel(before.Guild, logSetting, LogType.ChannelUpdated) .ConfigureAwait(false)) is null) - return; - string title; - if (ch is IVoiceChannel) - { - title = GetText(logChannel.Guild, strs.voice_chan_destroyed); - } - else - title = GetText(logChannel.Guild, strs.text_chan_destroyed); + return; - await logChannel.EmbedAsync(_eb.Create() - .WithOkColor() - .WithTitle("🆕 " + title) - .WithDescription($"{ch.Name} | {ch.Id}") - .WithFooter(CurrentTime(ch.Guild))).ConfigureAwait(false); - } - catch + var embed = _eb.Create().WithOkColor() + .WithFooter(CurrentTime(before.Guild)); + + var beforeTextChannel = cbefore as ITextChannel; + var afterTextChannel = cafter as ITextChannel; + + if (before.Name != after.Name) { - // ignored + embed.WithTitle("ℹ️ " + GetText(logChannel.Guild, strs.ch_name_change)) + .WithDescription($"{after} | {after.Id}") + .AddField(GetText(logChannel.Guild, strs.ch_old_name), before.Name); } - }); - return Task.CompletedTask; - } + else if (beforeTextChannel?.Topic != afterTextChannel?.Topic) + { + embed.WithTitle("ℹ️ " + GetText(logChannel.Guild, strs.ch_topic_change)) + .WithDescription($"{after} | {after.Id}") + .AddField(GetText(logChannel.Guild, strs.old_topic) , beforeTextChannel?.Topic ?? "-") + .AddField(GetText(logChannel.Guild, strs.new_topic), afterTextChannel?.Topic ?? "-"); + } + else + return; - private Task _client_ChannelCreated(IChannel ich) + await logChannel.EmbedAsync(embed).ConfigureAwait(false); + } + catch + { + // ignored + } + }); + return Task.CompletedTask; + } + + private Task _client_ChannelDestroyed(IChannel ich) + { + var _ = Task.Run(async () => { - var _ = Task.Run(async () => + try { - try - { - if (!(ich is IGuildChannel ch)) - return; + if (!(ich is IGuildChannel ch)) + return; - if (!GuildLogSettings.TryGetValue(ch.Guild.Id, out LogSetting logSetting) - || logSetting.ChannelCreatedId is null) - return; + if (!GuildLogSettings.TryGetValue(ch.Guild.Id, out LogSetting logSetting) + || (logSetting.ChannelDestroyedId is null) + || logSetting.LogIgnores.Any(ilc => ilc.LogItemId == ch.Id && ilc.ItemType == IgnoredItemType.Channel)) + return; - ITextChannel logChannel; - if ((logChannel = await TryGetLogChannel(ch.Guild, logSetting, LogType.ChannelCreated) + ITextChannel logChannel; + if ((logChannel = await TryGetLogChannel(ch.Guild, logSetting, LogType.ChannelDestroyed) .ConfigureAwait(false)) is null) - return; - string title; - if (ch is IVoiceChannel) - { - title = GetText(logChannel.Guild, strs.voice_chan_created); - } - else - title = GetText(logChannel.Guild, strs.text_chan_created); - - await logChannel.EmbedAsync(_eb.Create() - .WithOkColor() - .WithTitle("🆕 " + title) - .WithDescription($"{ch.Name} | {ch.Id}") - .WithFooter(CurrentTime(ch.Guild))).ConfigureAwait(false); - } - catch (Exception) + return; + string title; + if (ch is IVoiceChannel) { - // ignored + title = GetText(logChannel.Guild, strs.voice_chan_destroyed); } - }); - return Task.CompletedTask; - } + else + title = GetText(logChannel.Guild, strs.text_chan_destroyed); - private Task _client_UserVoiceStateUpdated(SocketUser iusr, SocketVoiceState before, SocketVoiceState after) + await logChannel.EmbedAsync(_eb.Create() + .WithOkColor() + .WithTitle("🆕 " + title) + .WithDescription($"{ch.Name} | {ch.Id}") + .WithFooter(CurrentTime(ch.Guild))).ConfigureAwait(false); + } + catch + { + // ignored + } + }); + return Task.CompletedTask; + } + + private Task _client_ChannelCreated(IChannel ich) + { + var _ = Task.Run(async () => { - var _ = Task.Run(async () => + try { - try - { - if (!(iusr is IGuildUser usr) || usr.IsBot) - return; + if (!(ich is IGuildChannel ch)) + return; - var beforeVch = before.VoiceChannel; - var afterVch = after.VoiceChannel; + if (!GuildLogSettings.TryGetValue(ch.Guild.Id, out LogSetting logSetting) + || logSetting.ChannelCreatedId is null) + return; - if (beforeVch == afterVch) - return; - - if (!GuildLogSettings.TryGetValue(usr.Guild.Id, out LogSetting logSetting) - || (logSetting.LogVoicePresenceId is null) - || logSetting.LogIgnores.Any(ilc => ilc.LogItemId == iusr.Id && ilc.ItemType == IgnoredItemType.User)) - return; - - ITextChannel logChannel; - if ((logChannel = await TryGetLogChannel(usr.Guild, logSetting, LogType.VoicePresence) + ITextChannel logChannel; + if ((logChannel = await TryGetLogChannel(ch.Guild, logSetting, LogType.ChannelCreated) .ConfigureAwait(false)) is null) - return; + return; + string title; + if (ch is IVoiceChannel) + { + title = GetText(logChannel.Guild, strs.voice_chan_created); + } + else + title = GetText(logChannel.Guild, strs.text_chan_created); - string str = null; - if (beforeVch?.Guild == afterVch?.Guild) - { - str = "🎙" + Format.Code(PrettyCurrentTime(usr.Guild)) + GetText(logChannel.Guild, - strs.user_vmoved( + await logChannel.EmbedAsync(_eb.Create() + .WithOkColor() + .WithTitle("🆕 " + title) + .WithDescription($"{ch.Name} | {ch.Id}") + .WithFooter(CurrentTime(ch.Guild))).ConfigureAwait(false); + } + catch (Exception) + { + // ignored + } + }); + return Task.CompletedTask; + } + + private Task _client_UserVoiceStateUpdated(SocketUser iusr, SocketVoiceState before, SocketVoiceState after) + { + var _ = Task.Run(async () => + { + try + { + if (!(iusr is IGuildUser usr) || usr.IsBot) + return; + + var beforeVch = before.VoiceChannel; + var afterVch = after.VoiceChannel; + + if (beforeVch == afterVch) + return; + + if (!GuildLogSettings.TryGetValue(usr.Guild.Id, out LogSetting logSetting) + || (logSetting.LogVoicePresenceId is null) + || logSetting.LogIgnores.Any(ilc => ilc.LogItemId == iusr.Id && ilc.ItemType == IgnoredItemType.User)) + return; + + ITextChannel logChannel; + if ((logChannel = await TryGetLogChannel(usr.Guild, logSetting, LogType.VoicePresence) + .ConfigureAwait(false)) is null) + return; + + string str = null; + if (beforeVch?.Guild == afterVch?.Guild) + { + str = "🎙" + Format.Code(PrettyCurrentTime(usr.Guild)) + GetText(logChannel.Guild, + strs.user_vmoved( "👤" + Format.Bold(usr.Username + "#" + usr.Discriminator), Format.Bold(beforeVch?.Name ?? ""), Format.Bold(afterVch?.Name ?? ""))); - } - else if (beforeVch is null) + } + else if (beforeVch is null) + { + str = "🎙" + Format.Code(PrettyCurrentTime(usr.Guild)) + GetText(logChannel.Guild, + strs.user_vjoined( + "👤" + Format.Bold(usr.Username + "#" + usr.Discriminator), + Format.Bold(afterVch.Name ?? ""))); + } + else if (afterVch is null) + { + str = "🎙" + Format.Code(PrettyCurrentTime(usr.Guild)) + GetText(logChannel.Guild, + strs.user_vleft("👤" + Format.Bold(usr.Username + "#" + usr.Discriminator), + Format.Bold(beforeVch.Name ?? ""))); + } + + if (!string.IsNullOrWhiteSpace(str)) + PresenceUpdates.AddOrUpdate(logChannel, new List() {str}, (id, list) => { - str = "🎙" + Format.Code(PrettyCurrentTime(usr.Guild)) + GetText(logChannel.Guild, - strs.user_vjoined( - "👤" + Format.Bold(usr.Username + "#" + usr.Discriminator), - Format.Bold(afterVch.Name ?? ""))); - } - else if (afterVch is null) - { - str = "🎙" + Format.Code(PrettyCurrentTime(usr.Guild)) + GetText(logChannel.Guild, - strs.user_vleft("👤" + Format.Bold(usr.Username + "#" + usr.Discriminator), - Format.Bold(beforeVch.Name ?? ""))); - } - - if (!string.IsNullOrWhiteSpace(str)) - PresenceUpdates.AddOrUpdate(logChannel, new List() {str}, (id, list) => - { - list.Add(str); - return list; - }); - } - catch - { - // ignored - } - }); - return Task.CompletedTask; - } - - private Task _client_UserLeft(IGuildUser usr) - { - var _ = Task.Run(async () => + list.Add(str); + return list; + }); + } + catch { - try - { - if (!GuildLogSettings.TryGetValue(usr.Guild.Id, out LogSetting logSetting) - || (logSetting.UserLeftId is null) - || logSetting.LogIgnores.Any(ilc => ilc.LogItemId == usr.Id && ilc.ItemType == IgnoredItemType.User)) - return; + // ignored + } + }); + return Task.CompletedTask; + } - ITextChannel logChannel; - if ((logChannel = await TryGetLogChannel(usr.Guild, logSetting, LogType.UserLeft) + private Task _client_UserLeft(IGuildUser usr) + { + var _ = Task.Run(async () => + { + try + { + if (!GuildLogSettings.TryGetValue(usr.Guild.Id, out LogSetting logSetting) + || (logSetting.UserLeftId is null) + || logSetting.LogIgnores.Any(ilc => ilc.LogItemId == usr.Id && ilc.ItemType == IgnoredItemType.User)) + return; + + ITextChannel logChannel; + if ((logChannel = await TryGetLogChannel(usr.Guild, logSetting, LogType.UserLeft) .ConfigureAwait(false)) is null) - return; - var embed = _eb.Create() - .WithOkColor() - .WithTitle("❌ " + GetText(logChannel.Guild, strs.user_left)) - .WithDescription(usr.ToString()) - .AddField("Id", usr.Id.ToString()) - .WithFooter(CurrentTime(usr.Guild)); + return; + var embed = _eb.Create() + .WithOkColor() + .WithTitle("❌ " + GetText(logChannel.Guild, strs.user_left)) + .WithDescription(usr.ToString()) + .AddField("Id", usr.Id.ToString()) + .WithFooter(CurrentTime(usr.Guild)); - if (Uri.IsWellFormedUriString(usr.GetAvatarUrl(), UriKind.Absolute)) - embed.WithThumbnailUrl(usr.GetAvatarUrl()); + if (Uri.IsWellFormedUriString(usr.GetAvatarUrl(), UriKind.Absolute)) + embed.WithThumbnailUrl(usr.GetAvatarUrl()); - await logChannel.EmbedAsync(embed).ConfigureAwait(false); - } - catch - { - // ignored - } - }); - return Task.CompletedTask; - } - - private Task _client_UserJoined(IGuildUser usr) - { - var _ = Task.Run(async () => + await logChannel.EmbedAsync(embed).ConfigureAwait(false); + } + catch { - try - { - if (!GuildLogSettings.TryGetValue(usr.Guild.Id, out LogSetting logSetting) - || (logSetting.UserJoinedId is null)) - return; + // ignored + } + }); + return Task.CompletedTask; + } - ITextChannel logChannel; - if ((logChannel = await TryGetLogChannel(usr.Guild, logSetting, LogType.UserJoined) + private Task _client_UserJoined(IGuildUser usr) + { + var _ = Task.Run(async () => + { + try + { + if (!GuildLogSettings.TryGetValue(usr.Guild.Id, out LogSetting logSetting) + || (logSetting.UserJoinedId is null)) + return; + + ITextChannel logChannel; + if ((logChannel = await TryGetLogChannel(usr.Guild, logSetting, LogType.UserJoined) .ConfigureAwait(false)) is null) - return; + return; - var embed = _eb.Create() - .WithOkColor() - .WithTitle("✅ " + GetText(logChannel.Guild, strs.user_joined)) - .WithDescription($"{usr.Mention} `{usr}`") - .AddField("Id", usr.Id.ToString()) - .AddField(GetText(logChannel.Guild, strs.joined_server), - $"{usr.JoinedAt?.ToString("dd.MM.yyyy HH:mm" ?? "?")}", - true) - .AddField(GetText(logChannel.Guild, strs.joined_discord), - $"{usr.CreatedAt:dd.MM.yyyy HH:mm}", - true) - .WithFooter(CurrentTime(usr.Guild)); + var embed = _eb.Create() + .WithOkColor() + .WithTitle("✅ " + GetText(logChannel.Guild, strs.user_joined)) + .WithDescription($"{usr.Mention} `{usr}`") + .AddField("Id", usr.Id.ToString()) + .AddField(GetText(logChannel.Guild, strs.joined_server), + $"{usr.JoinedAt?.ToString("dd.MM.yyyy HH:mm" ?? "?")}", + true) + .AddField(GetText(logChannel.Guild, strs.joined_discord), + $"{usr.CreatedAt:dd.MM.yyyy HH:mm}", + true) + .WithFooter(CurrentTime(usr.Guild)); - if (Uri.IsWellFormedUriString(usr.GetAvatarUrl(), UriKind.Absolute)) - embed.WithThumbnailUrl(usr.GetAvatarUrl()); + if (Uri.IsWellFormedUriString(usr.GetAvatarUrl(), UriKind.Absolute)) + embed.WithThumbnailUrl(usr.GetAvatarUrl()); - await logChannel.EmbedAsync(embed).ConfigureAwait(false); - } - catch (Exception) - { - // ignored - } - }); - return Task.CompletedTask; - } - - private Task _client_UserUnbanned(IUser usr, IGuild guild) - { - var _ = Task.Run(async () => + await logChannel.EmbedAsync(embed).ConfigureAwait(false); + } + catch (Exception) { - try - { - if (!GuildLogSettings.TryGetValue(guild.Id, out LogSetting logSetting) - || (logSetting.UserUnbannedId is null) - || logSetting.LogIgnores.Any(ilc => ilc.LogItemId == usr.Id && ilc.ItemType == IgnoredItemType.User)) - return; + // ignored + } + }); + return Task.CompletedTask; + } - ITextChannel logChannel; - if ((logChannel = await TryGetLogChannel(guild, logSetting, LogType.UserUnbanned) + private Task _client_UserUnbanned(IUser usr, IGuild guild) + { + var _ = Task.Run(async () => + { + try + { + if (!GuildLogSettings.TryGetValue(guild.Id, out LogSetting logSetting) + || (logSetting.UserUnbannedId is null) + || logSetting.LogIgnores.Any(ilc => ilc.LogItemId == usr.Id && ilc.ItemType == IgnoredItemType.User)) + return; + + ITextChannel logChannel; + if ((logChannel = await TryGetLogChannel(guild, logSetting, LogType.UserUnbanned) .ConfigureAwait(false)) is null) - return; - var embed = _eb.Create() - .WithOkColor() - .WithTitle("♻️ " + GetText(logChannel.Guild, strs.user_unbanned)) - .WithDescription(usr.ToString()) - .AddField("Id", usr.Id.ToString()) - .WithFooter(CurrentTime(guild)); + return; + var embed = _eb.Create() + .WithOkColor() + .WithTitle("♻️ " + GetText(logChannel.Guild, strs.user_unbanned)) + .WithDescription(usr.ToString()) + .AddField("Id", usr.Id.ToString()) + .WithFooter(CurrentTime(guild)); - if (Uri.IsWellFormedUriString(usr.GetAvatarUrl(), UriKind.Absolute)) - embed.WithThumbnailUrl(usr.GetAvatarUrl()); + if (Uri.IsWellFormedUriString(usr.GetAvatarUrl(), UriKind.Absolute)) + embed.WithThumbnailUrl(usr.GetAvatarUrl()); - await logChannel.EmbedAsync(embed).ConfigureAwait(false); - } - catch (Exception) - { - // ignored - } - }); - return Task.CompletedTask; - } - - private Task _client_UserBanned(IUser usr, IGuild guild) - { - var _ = Task.Run(async () => + await logChannel.EmbedAsync(embed).ConfigureAwait(false); + } + catch (Exception) { - try - { - if (!GuildLogSettings.TryGetValue(guild.Id, out LogSetting logSetting) - || (logSetting.UserBannedId is null) - || logSetting.LogIgnores.Any(ilc => ilc.LogItemId == usr.Id && ilc.ItemType == IgnoredItemType.User)) - return; + // ignored + } + }); + return Task.CompletedTask; + } - ITextChannel logChannel; - if ((logChannel = - await TryGetLogChannel(guild, logSetting, LogType.UserBanned).ConfigureAwait(false)) == - null) - return; - var embed = _eb.Create() - .WithOkColor() - .WithTitle("🚫 " + GetText(logChannel.Guild, strs.user_banned)) - .WithDescription(usr.ToString()) - .AddField("Id", usr.Id.ToString()) - .WithFooter(CurrentTime(guild)); - - var avatarUrl = usr.GetAvatarUrl(); - - if (Uri.IsWellFormedUriString(avatarUrl, UriKind.Absolute)) - embed.WithThumbnailUrl(usr.GetAvatarUrl()); - - await logChannel.EmbedAsync(embed).ConfigureAwait(false); - } - catch (Exception) - { - // ignored - } - }); - return Task.CompletedTask; - } - - private Task _client_MessageDeleted(Cacheable optMsg, ISocketMessageChannel ch) + private Task _client_UserBanned(IUser usr, IGuild guild) + { + var _ = Task.Run(async () => { - var _ = Task.Run(async () => + try { - try - { - var msg = (optMsg.HasValue ? optMsg.Value : null) as IUserMessage; - if (msg is null || msg.IsAuthor(_client)) - return; + if (!GuildLogSettings.TryGetValue(guild.Id, out LogSetting logSetting) + || (logSetting.UserBannedId is null) + || logSetting.LogIgnores.Any(ilc => ilc.LogItemId == usr.Id && ilc.ItemType == IgnoredItemType.User)) + return; - if (_ignoreMessageIds.Contains(msg.Id)) - return; + ITextChannel logChannel; + if ((logChannel = + await TryGetLogChannel(guild, logSetting, LogType.UserBanned).ConfigureAwait(false)) == + null) + return; + var embed = _eb.Create() + .WithOkColor() + .WithTitle("🚫 " + GetText(logChannel.Guild, strs.user_banned)) + .WithDescription(usr.ToString()) + .AddField("Id", usr.Id.ToString()) + .WithFooter(CurrentTime(guild)); - if (!(ch is ITextChannel channel)) - return; + var avatarUrl = usr.GetAvatarUrl(); - if (!GuildLogSettings.TryGetValue(channel.Guild.Id, out LogSetting logSetting) - || (logSetting.MessageDeletedId is null) - || logSetting.LogIgnores.Any(ilc => ilc.LogItemId == channel.Id && ilc.ItemType == IgnoredItemType.Channel)) - return; + if (Uri.IsWellFormedUriString(avatarUrl, UriKind.Absolute)) + embed.WithThumbnailUrl(usr.GetAvatarUrl()); - ITextChannel logChannel; - if ((logChannel = await TryGetLogChannel(channel.Guild, logSetting, LogType.MessageDeleted) + await logChannel.EmbedAsync(embed).ConfigureAwait(false); + } + catch (Exception) + { + // ignored + } + }); + return Task.CompletedTask; + } + + private Task _client_MessageDeleted(Cacheable optMsg, ISocketMessageChannel ch) + { + var _ = Task.Run(async () => + { + try + { + var msg = (optMsg.HasValue ? optMsg.Value : null) as IUserMessage; + if (msg is null || msg.IsAuthor(_client)) + return; + + if (_ignoreMessageIds.Contains(msg.Id)) + return; + + if (!(ch is ITextChannel channel)) + return; + + if (!GuildLogSettings.TryGetValue(channel.Guild.Id, out LogSetting logSetting) + || (logSetting.MessageDeletedId is null) + || logSetting.LogIgnores.Any(ilc => ilc.LogItemId == channel.Id && ilc.ItemType == IgnoredItemType.Channel)) + return; + + ITextChannel logChannel; + if ((logChannel = await TryGetLogChannel(channel.Guild, logSetting, LogType.MessageDeleted) .ConfigureAwait(false)) is null || logChannel.Id == msg.Id) - return; + return; - var resolvedMessage = msg.Resolve(userHandling: TagHandling.FullName); - var embed = _eb.Create() - .WithOkColor() - .WithTitle("🗑 " + GetText(logChannel.Guild, strs.msg_del(((ITextChannel) msg.Channel).Name))) - .WithDescription(msg.Author.ToString()) - .AddField(GetText(logChannel.Guild, strs.content), - string.IsNullOrWhiteSpace(resolvedMessage) ? "-" : resolvedMessage, - false) - .AddField("Id", msg.Id.ToString(), false) - .WithFooter(CurrentTime(channel.Guild)); - if (msg.Attachments.Any()) - embed.AddField(GetText(logChannel.Guild, strs.attachments), - string.Join(", ", msg.Attachments.Select(a => a.Url)), - false); + var resolvedMessage = msg.Resolve(userHandling: TagHandling.FullName); + var embed = _eb.Create() + .WithOkColor() + .WithTitle("🗑 " + GetText(logChannel.Guild, strs.msg_del(((ITextChannel) msg.Channel).Name))) + .WithDescription(msg.Author.ToString()) + .AddField(GetText(logChannel.Guild, strs.content), + string.IsNullOrWhiteSpace(resolvedMessage) ? "-" : resolvedMessage, + false) + .AddField("Id", msg.Id.ToString(), false) + .WithFooter(CurrentTime(channel.Guild)); + if (msg.Attachments.Any()) + embed.AddField(GetText(logChannel.Guild, strs.attachments), + string.Join(", ", msg.Attachments.Select(a => a.Url)), + false); - await logChannel.EmbedAsync(embed).ConfigureAwait(false); - } - catch (Exception) - { - // ignored - } - }); - return Task.CompletedTask; - } - - private Task _client_MessageUpdated(Cacheable optmsg, SocketMessage imsg2, - ISocketMessageChannel ch) - { - var _ = Task.Run(async () => + await logChannel.EmbedAsync(embed).ConfigureAwait(false); + } + catch (Exception) { - try - { - if (!(imsg2 is IUserMessage after) || after.IsAuthor(_client)) - return; + // ignored + } + }); + return Task.CompletedTask; + } - var before = (optmsg.HasValue ? optmsg.Value : null) as IUserMessage; - if (before is null) - return; + private Task _client_MessageUpdated(Cacheable optmsg, SocketMessage imsg2, + ISocketMessageChannel ch) + { + var _ = Task.Run(async () => + { + try + { + if (!(imsg2 is IUserMessage after) || after.IsAuthor(_client)) + return; - if (!(ch is ITextChannel channel)) - return; + var before = (optmsg.HasValue ? optmsg.Value : null) as IUserMessage; + if (before is null) + return; - if (before.Content == after.Content) - return; + if (!(ch is ITextChannel channel)) + return; - if (before.Author.IsBot) - return; + if (before.Content == after.Content) + return; - if (!GuildLogSettings.TryGetValue(channel.Guild.Id, out LogSetting logSetting) - || (logSetting.MessageUpdatedId is null) - || logSetting.LogIgnores.Any(ilc => ilc.LogItemId == channel.Id && ilc.ItemType == IgnoredItemType.Channel)) - return; + if (before.Author.IsBot) + return; - ITextChannel logChannel; - if ((logChannel = await TryGetLogChannel(channel.Guild, logSetting, LogType.MessageUpdated) + if (!GuildLogSettings.TryGetValue(channel.Guild.Id, out LogSetting logSetting) + || (logSetting.MessageUpdatedId is null) + || logSetting.LogIgnores.Any(ilc => ilc.LogItemId == channel.Id && ilc.ItemType == IgnoredItemType.Channel)) + return; + + ITextChannel logChannel; + if ((logChannel = await TryGetLogChannel(channel.Guild, logSetting, LogType.MessageUpdated) .ConfigureAwait(false)) is null || logChannel.Id == after.Channel.Id) - return; + return; - var embed = _eb.Create() - .WithOkColor() - .WithTitle("📝 " + GetText(logChannel.Guild, strs.msg_update(((ITextChannel)after.Channel).Name))) - .WithDescription(after.Author.ToString()) - .AddField(GetText(logChannel.Guild, strs.old_msg), - string.IsNullOrWhiteSpace(before.Content) - ? "-" - : before.Resolve(userHandling: TagHandling.FullName), - false) - .AddField( - GetText(logChannel.Guild, strs.new_msg), - string.IsNullOrWhiteSpace(after.Content) - ? "-" - : after.Resolve(userHandling: TagHandling.FullName), - false) - .AddField("Id", after.Id.ToString(), false) - .WithFooter(CurrentTime(channel.Guild)); + var embed = _eb.Create() + .WithOkColor() + .WithTitle("📝 " + GetText(logChannel.Guild, strs.msg_update(((ITextChannel)after.Channel).Name))) + .WithDescription(after.Author.ToString()) + .AddField(GetText(logChannel.Guild, strs.old_msg), + string.IsNullOrWhiteSpace(before.Content) + ? "-" + : before.Resolve(userHandling: TagHandling.FullName), + false) + .AddField( + GetText(logChannel.Guild, strs.new_msg), + string.IsNullOrWhiteSpace(after.Content) + ? "-" + : after.Resolve(userHandling: TagHandling.FullName), + false) + .AddField("Id", after.Id.ToString(), false) + .WithFooter(CurrentTime(channel.Guild)); - await logChannel.EmbedAsync(embed).ConfigureAwait(false); - } - catch - { - // ignored - } - }); - return Task.CompletedTask; + await logChannel.EmbedAsync(embed).ConfigureAwait(false); + } + catch + { + // ignored + } + }); + return Task.CompletedTask; + } + + private async Task TryGetLogChannel(IGuild guild, LogSetting logSetting, LogType logChannelType) + { + ulong? id = null; + switch (logChannelType) + { + case LogType.Other: + id = logSetting.LogOtherId; + break; + case LogType.MessageUpdated: + id = logSetting.MessageUpdatedId; + break; + case LogType.MessageDeleted: + id = logSetting.MessageDeletedId; + break; + case LogType.UserJoined: + id = logSetting.UserJoinedId; + break; + case LogType.UserLeft: + id = logSetting.UserLeftId; + break; + case LogType.UserBanned: + id = logSetting.UserBannedId; + break; + case LogType.UserUnbanned: + id = logSetting.UserUnbannedId; + break; + case LogType.UserUpdated: + id = logSetting.UserUpdatedId; + break; + case LogType.ChannelCreated: + id = logSetting.ChannelCreatedId; + break; + case LogType.ChannelDestroyed: + id = logSetting.ChannelDestroyedId; + break; + case LogType.ChannelUpdated: + id = logSetting.ChannelUpdatedId; + break; + case LogType.UserPresence: + id = logSetting.LogUserPresenceId; + break; + case LogType.VoicePresence: + id = logSetting.LogVoicePresenceId; + break; + case LogType.VoicePresenceTTS: + id = logSetting.LogVoicePresenceTTSId; + break; + case LogType.UserMuted: + id = logSetting.UserMutedId; + break; } - private async Task TryGetLogChannel(IGuild guild, LogSetting logSetting, LogType logChannelType) + if (!id.HasValue || id == 0) { - ulong? id = null; + UnsetLogSetting(guild.Id, logChannelType); + return null; + } + + var channel = await guild.GetTextChannelAsync(id.Value).ConfigureAwait(false); + + if (channel is null) + { + UnsetLogSetting(guild.Id, logChannelType); + return null; + } + else + return channel; + } + + private void UnsetLogSetting(ulong guildId, LogType logChannelType) + { + using (var uow = _db.GetDbContext()) + { + var newLogSetting = uow.LogSettingsFor(guildId); switch (logChannelType) { case LogType.Other: - id = logSetting.LogOtherId; + newLogSetting.LogOtherId = null; break; case LogType.MessageUpdated: - id = logSetting.MessageUpdatedId; + newLogSetting.MessageUpdatedId = null; break; case LogType.MessageDeleted: - id = logSetting.MessageDeletedId; + newLogSetting.MessageDeletedId = null; break; case LogType.UserJoined: - id = logSetting.UserJoinedId; + newLogSetting.UserJoinedId = null; break; case LogType.UserLeft: - id = logSetting.UserLeftId; + newLogSetting.UserLeftId = null; break; case LogType.UserBanned: - id = logSetting.UserBannedId; + newLogSetting.UserBannedId = null; break; case LogType.UserUnbanned: - id = logSetting.UserUnbannedId; + newLogSetting.UserUnbannedId = null; break; case LogType.UserUpdated: - id = logSetting.UserUpdatedId; - break; - case LogType.ChannelCreated: - id = logSetting.ChannelCreatedId; - break; - case LogType.ChannelDestroyed: - id = logSetting.ChannelDestroyedId; - break; - case LogType.ChannelUpdated: - id = logSetting.ChannelUpdatedId; - break; - case LogType.UserPresence: - id = logSetting.LogUserPresenceId; - break; - case LogType.VoicePresence: - id = logSetting.LogVoicePresenceId; - break; - case LogType.VoicePresenceTTS: - id = logSetting.LogVoicePresenceTTSId; + newLogSetting.UserUpdatedId = null; break; case LogType.UserMuted: - id = logSetting.UserMutedId; + newLogSetting.UserMutedId = null; + break; + case LogType.ChannelCreated: + newLogSetting.ChannelCreatedId = null; + break; + case LogType.ChannelDestroyed: + newLogSetting.ChannelDestroyedId = null; + break; + case LogType.ChannelUpdated: + newLogSetting.ChannelUpdatedId = null; + break; + case LogType.UserPresence: + newLogSetting.LogUserPresenceId = null; + break; + case LogType.VoicePresence: + newLogSetting.LogVoicePresenceId = null; + break; + case LogType.VoicePresenceTTS: + newLogSetting.LogVoicePresenceTTSId = null; break; } - if (!id.HasValue || id == 0) - { - UnsetLogSetting(guild.Id, logChannelType); - return null; - } - - var channel = await guild.GetTextChannelAsync(id.Value).ConfigureAwait(false); - - if (channel is null) - { - UnsetLogSetting(guild.Id, logChannelType); - return null; - } - else - return channel; - } - - private void UnsetLogSetting(ulong guildId, LogType logChannelType) - { - using (var uow = _db.GetDbContext()) - { - var newLogSetting = uow.LogSettingsFor(guildId); - switch (logChannelType) - { - case LogType.Other: - newLogSetting.LogOtherId = null; - break; - case LogType.MessageUpdated: - newLogSetting.MessageUpdatedId = null; - break; - case LogType.MessageDeleted: - newLogSetting.MessageDeletedId = null; - break; - case LogType.UserJoined: - newLogSetting.UserJoinedId = null; - break; - case LogType.UserLeft: - newLogSetting.UserLeftId = null; - break; - case LogType.UserBanned: - newLogSetting.UserBannedId = null; - break; - case LogType.UserUnbanned: - newLogSetting.UserUnbannedId = null; - break; - case LogType.UserUpdated: - newLogSetting.UserUpdatedId = null; - break; - case LogType.UserMuted: - newLogSetting.UserMutedId = null; - break; - case LogType.ChannelCreated: - newLogSetting.ChannelCreatedId = null; - break; - case LogType.ChannelDestroyed: - newLogSetting.ChannelDestroyedId = null; - break; - case LogType.ChannelUpdated: - newLogSetting.ChannelUpdatedId = null; - break; - case LogType.UserPresence: - newLogSetting.LogUserPresenceId = null; - break; - case LogType.VoicePresence: - newLogSetting.LogVoicePresenceId = null; - break; - case LogType.VoicePresenceTTS: - newLogSetting.LogVoicePresenceTTSId = null; - break; - } - - GuildLogSettings.AddOrUpdate(guildId, newLogSetting, (gid, old) => newLogSetting); - uow.SaveChanges(); - } + GuildLogSettings.AddOrUpdate(guildId, newLogSetting, (gid, old) => newLogSetting); + uow.SaveChanges(); } } } \ No newline at end of file diff --git a/src/NadekoBot/Modules/Administration/Services/MuteService.cs b/src/NadekoBot/Modules/Administration/Services/MuteService.cs index d23846039..66254b7a4 100644 --- a/src/NadekoBot/Modules/Administration/Services/MuteService.cs +++ b/src/NadekoBot/Modules/Administration/Services/MuteService.cs @@ -1,6 +1,4 @@ -using System; -using System.Collections.Concurrent; -using System.Linq; +using System.Collections.Concurrent; using System.Threading; using System.Threading.Tasks; using Discord; @@ -11,470 +9,468 @@ using NadekoBot.Extensions; using NadekoBot.Services; using NadekoBot.Services.Database.Models; using NadekoBot.Db; -using Serilog; -namespace NadekoBot.Modules.Administration.Services +namespace NadekoBot.Modules.Administration.Services; + +public enum MuteType { - public enum MuteType + Voice, + Chat, + All +} + +public class MuteService : INService +{ + public ConcurrentDictionary GuildMuteRoles { get; } + public ConcurrentDictionary> MutedUsers { get; } + + public ConcurrentDictionary> Un_Timers { get; } + = new ConcurrentDictionary>(); + + public event Action UserMuted = delegate { }; + public event Action UserUnmuted = delegate { }; + + private static readonly OverwritePermissions denyOverwrite = + new OverwritePermissions(addReactions: PermValue.Deny, sendMessages: PermValue.Deny, + attachFiles: PermValue.Deny); + + private readonly DiscordSocketClient _client; + private readonly DbService _db; + private readonly IEmbedBuilderService _eb; + + public MuteService(DiscordSocketClient client, DbService db, IEmbedBuilderService eb) { - Voice, - Chat, - All - } + _client = client; + _db = db; + _eb = eb; - public class MuteService : INService - { - public ConcurrentDictionary GuildMuteRoles { get; } - public ConcurrentDictionary> MutedUsers { get; } - - public ConcurrentDictionary> Un_Timers { get; } - = new ConcurrentDictionary>(); - - public event Action UserMuted = delegate { }; - public event Action UserUnmuted = delegate { }; - - private static readonly OverwritePermissions denyOverwrite = - new OverwritePermissions(addReactions: PermValue.Deny, sendMessages: PermValue.Deny, - attachFiles: PermValue.Deny); - - private readonly DiscordSocketClient _client; - private readonly DbService _db; - private readonly IEmbedBuilderService _eb; - - public MuteService(DiscordSocketClient client, DbService db, IEmbedBuilderService eb) + using (var uow = db.GetDbContext()) { - _client = client; - _db = db; - _eb = eb; + var guildIds = client.Guilds.Select(x => x.Id).ToList(); + var configs = uow.Set().AsQueryable() + .Include(x => x.MutedUsers) + .Include(x => x.UnbanTimer) + .Include(x => x.UnmuteTimers) + .Include(x => x.UnroleTimer) + .Where(x => guildIds.Contains(x.GuildId)) + .ToList(); - using (var uow = db.GetDbContext()) + GuildMuteRoles = configs + .Where(c => !string.IsNullOrWhiteSpace(c.MuteRoleName)) + .ToDictionary(c => c.GuildId, c => c.MuteRoleName) + .ToConcurrent(); + + MutedUsers = new ConcurrentDictionary>(configs + .ToDictionary( + k => k.GuildId, + v => new ConcurrentHashSet(v.MutedUsers.Select(m => m.UserId)) + )); + + var max = TimeSpan.FromDays(49); + + foreach (var conf in configs) { - var guildIds = client.Guilds.Select(x => x.Id).ToList(); - var configs = uow.Set().AsQueryable() - .Include(x => x.MutedUsers) - .Include(x => x.UnbanTimer) - .Include(x => x.UnmuteTimers) - .Include(x => x.UnroleTimer) - .Where(x => guildIds.Contains(x.GuildId)) - .ToList(); - - GuildMuteRoles = configs - .Where(c => !string.IsNullOrWhiteSpace(c.MuteRoleName)) - .ToDictionary(c => c.GuildId, c => c.MuteRoleName) - .ToConcurrent(); - - MutedUsers = new ConcurrentDictionary>(configs - .ToDictionary( - k => k.GuildId, - v => new ConcurrentHashSet(v.MutedUsers.Select(m => m.UserId)) - )); - - var max = TimeSpan.FromDays(49); - - foreach (var conf in configs) + foreach (var x in conf.UnmuteTimers) { - foreach (var x in conf.UnmuteTimers) + TimeSpan after; + if (x.UnmuteAt - TimeSpan.FromMinutes(2) <= DateTime.UtcNow) { - TimeSpan after; - if (x.UnmuteAt - TimeSpan.FromMinutes(2) <= DateTime.UtcNow) - { - after = TimeSpan.FromMinutes(2); - } - else - { - var unmute = x.UnmuteAt - DateTime.UtcNow; - after = unmute > max ? max : unmute; - } - - StartUn_Timer(conf.GuildId, x.UserId, after, TimerType.Mute); + after = TimeSpan.FromMinutes(2); + } + else + { + var unmute = x.UnmuteAt - DateTime.UtcNow; + after = unmute > max ? max : unmute; } - foreach (var x in conf.UnbanTimer) - { - TimeSpan after; - if (x.UnbanAt - TimeSpan.FromMinutes(2) <= DateTime.UtcNow) - { - after = TimeSpan.FromMinutes(2); - } - else - { - var unban = x.UnbanAt - DateTime.UtcNow; - after = unban > max ? max : unban; - } - - StartUn_Timer(conf.GuildId, x.UserId, after, TimerType.Ban); - } - - foreach (var x in conf.UnroleTimer) - { - TimeSpan after; - if (x.UnbanAt - TimeSpan.FromMinutes(2) <= DateTime.UtcNow) - { - after = TimeSpan.FromMinutes(2); - } - else - { - var unban = x.UnbanAt - DateTime.UtcNow; - after = unban > max ? max : unban; - } - - StartUn_Timer(conf.GuildId, x.UserId, after, TimerType.AddRole, x.RoleId); - } + StartUn_Timer(conf.GuildId, x.UserId, after, TimerType.Mute); } - _client.UserJoined += Client_UserJoined; + foreach (var x in conf.UnbanTimer) + { + TimeSpan after; + if (x.UnbanAt - TimeSpan.FromMinutes(2) <= DateTime.UtcNow) + { + after = TimeSpan.FromMinutes(2); + } + else + { + var unban = x.UnbanAt - DateTime.UtcNow; + after = unban > max ? max : unban; + } + + StartUn_Timer(conf.GuildId, x.UserId, after, TimerType.Ban); + } + + foreach (var x in conf.UnroleTimer) + { + TimeSpan after; + if (x.UnbanAt - TimeSpan.FromMinutes(2) <= DateTime.UtcNow) + { + after = TimeSpan.FromMinutes(2); + } + else + { + var unban = x.UnbanAt - DateTime.UtcNow; + after = unban > max ? max : unban; + } + + StartUn_Timer(conf.GuildId, x.UserId, after, TimerType.AddRole, x.RoleId); + } } - UserMuted += OnUserMuted; - UserUnmuted += OnUserUnmuted; + _client.UserJoined += Client_UserJoined; } - private void OnUserMuted(IGuildUser user, IUser mod, MuteType type, string reason) - { - if (string.IsNullOrWhiteSpace(reason)) - return; + UserMuted += OnUserMuted; + UserUnmuted += OnUserUnmuted; + } + + private void OnUserMuted(IGuildUser user, IUser mod, MuteType type, string reason) + { + if (string.IsNullOrWhiteSpace(reason)) + return; - var _ = Task.Run(() => user.SendMessageAsync(embed: _eb.Create() - .WithDescription($"You've been muted in {user.Guild} server") - .AddField("Mute Type", type.ToString()) - .AddField("Moderator", mod.ToString()) - .AddField("Reason", reason) - .Build())); - } + var _ = Task.Run(() => user.SendMessageAsync(embed: _eb.Create() + .WithDescription($"You've been muted in {user.Guild} server") + .AddField("Mute Type", type.ToString()) + .AddField("Moderator", mod.ToString()) + .AddField("Reason", reason) + .Build())); + } - private void OnUserUnmuted(IGuildUser user, IUser mod, MuteType type, string reason) - { - if (string.IsNullOrWhiteSpace(reason)) - return; + private void OnUserUnmuted(IGuildUser user, IUser mod, MuteType type, string reason) + { + if (string.IsNullOrWhiteSpace(reason)) + return; - var _ = Task.Run(() => user.SendMessageAsync(embed: _eb.Create() - .WithDescription($"You've been unmuted in {user.Guild} server") - .AddField("Unmute Type", type.ToString()) - .AddField("Moderator", mod.ToString()) - .AddField("Reason", reason) - .Build())); - } + var _ = Task.Run(() => user.SendMessageAsync(embed: _eb.Create() + .WithDescription($"You've been unmuted in {user.Guild} server") + .AddField("Unmute Type", type.ToString()) + .AddField("Moderator", mod.ToString()) + .AddField("Reason", reason) + .Build())); + } - private Task Client_UserJoined(IGuildUser usr) + private Task Client_UserJoined(IGuildUser usr) + { + try + { + MutedUsers.TryGetValue(usr.Guild.Id, out ConcurrentHashSet muted); + + if (muted is null || !muted.Contains(usr.Id)) + return Task.CompletedTask; + var _ = Task.Run(() => MuteUser(usr, _client.CurrentUser, reason: "Sticky mute").ConfigureAwait(false)); + } + catch (Exception ex) + { + Log.Warning(ex, "Error in MuteService UserJoined event"); + } + return Task.CompletedTask; + } + + public async Task SetMuteRoleAsync(ulong guildId, string name) + { + using (var uow = _db.GetDbContext()) + { + var config = uow.GuildConfigsForId(guildId, set => set); + config.MuteRoleName = name; + GuildMuteRoles.AddOrUpdate(guildId, name, (id, old) => name); + await uow.SaveChangesAsync(); + } + } + + public async Task MuteUser(IGuildUser usr, IUser mod, MuteType type = MuteType.All, string reason = "") + { + if (type == MuteType.All) + { + try { await usr.ModifyAsync(x => x.Mute = true).ConfigureAwait(false); } catch { } + var muteRole = await GetMuteRole(usr.Guild).ConfigureAwait(false); + if (!usr.RoleIds.Contains(muteRole.Id)) + await usr.AddRoleAsync(muteRole).ConfigureAwait(false); + StopTimer(usr.GuildId, usr.Id, TimerType.Mute); + using (var uow = _db.GetDbContext()) + { + var config = uow.GuildConfigsForId(usr.Guild.Id, + set => set.Include(gc => gc.MutedUsers) + .Include(gc => gc.UnmuteTimers)); + config.MutedUsers.Add(new MutedUserId() + { + UserId = usr.Id + }); + if (MutedUsers.TryGetValue(usr.Guild.Id, out ConcurrentHashSet muted)) + muted.Add(usr.Id); + + config.UnmuteTimers.RemoveWhere(x => x.UserId == usr.Id); + + await uow.SaveChangesAsync(); + } + UserMuted(usr, mod, MuteType.All, reason); + } + else if (type == MuteType.Voice) { try { - MutedUsers.TryGetValue(usr.Guild.Id, out ConcurrentHashSet muted); - - if (muted is null || !muted.Contains(usr.Id)) - return Task.CompletedTask; - var _ = Task.Run(() => MuteUser(usr, _client.CurrentUser, reason: "Sticky mute").ConfigureAwait(false)); + await usr.ModifyAsync(x => x.Mute = true).ConfigureAwait(false); + UserMuted(usr, mod, MuteType.Voice, reason); } - catch (Exception ex) - { - Log.Warning(ex, "Error in MuteService UserJoined event"); - } - return Task.CompletedTask; + catch { } } - - public async Task SetMuteRoleAsync(ulong guildId, string name) + else if (type == MuteType.Chat) { + await usr.AddRoleAsync(await GetMuteRole(usr.Guild).ConfigureAwait(false)).ConfigureAwait(false); + UserMuted(usr, mod, MuteType.Chat, reason); + } + } + + public async Task UnmuteUser(ulong guildId, ulong usrId, IUser mod, MuteType type = MuteType.All, string reason = "") + { + var usr = _client.GetGuild(guildId)?.GetUser(usrId); + if (type == MuteType.All) + { + StopTimer(guildId, usrId, TimerType.Mute); using (var uow = _db.GetDbContext()) { - var config = uow.GuildConfigsForId(guildId, set => set); - config.MuteRoleName = name; - GuildMuteRoles.AddOrUpdate(guildId, name, (id, old) => name); + var config = uow.GuildConfigsForId(guildId, set => set.Include(gc => gc.MutedUsers) + .Include(gc => gc.UnmuteTimers)); + var match = new MutedUserId() + { + UserId = usrId + }; + var toRemove = config.MutedUsers.FirstOrDefault(x => x.Equals(match)); + if (toRemove != null) + { + uow.Remove(toRemove); + } + if (MutedUsers.TryGetValue(guildId, out ConcurrentHashSet muted)) + muted.TryRemove(usrId); + + config.UnmuteTimers.RemoveWhere(x => x.UserId == usrId); + await uow.SaveChangesAsync(); } - } - - public async Task MuteUser(IGuildUser usr, IUser mod, MuteType type = MuteType.All, string reason = "") - { - if (type == MuteType.All) + if (usr != null) { - try { await usr.ModifyAsync(x => x.Mute = true).ConfigureAwait(false); } catch { } - var muteRole = await GetMuteRole(usr.Guild).ConfigureAwait(false); - if (!usr.RoleIds.Contains(muteRole.Id)) - await usr.AddRoleAsync(muteRole).ConfigureAwait(false); - StopTimer(usr.GuildId, usr.Id, TimerType.Mute); - using (var uow = _db.GetDbContext()) - { - var config = uow.GuildConfigsForId(usr.Guild.Id, - set => set.Include(gc => gc.MutedUsers) - .Include(gc => gc.UnmuteTimers)); - config.MutedUsers.Add(new MutedUserId() - { - UserId = usr.Id - }); - if (MutedUsers.TryGetValue(usr.Guild.Id, out ConcurrentHashSet muted)) - muted.Add(usr.Id); - - config.UnmuteTimers.RemoveWhere(x => x.UserId == usr.Id); - - await uow.SaveChangesAsync(); - } - UserMuted(usr, mod, MuteType.All, reason); - } - else if (type == MuteType.Voice) - { - try - { - await usr.ModifyAsync(x => x.Mute = true).ConfigureAwait(false); - UserMuted(usr, mod, MuteType.Voice, reason); - } - catch { } - } - else if (type == MuteType.Chat) - { - await usr.AddRoleAsync(await GetMuteRole(usr.Guild).ConfigureAwait(false)).ConfigureAwait(false); - UserMuted(usr, mod, MuteType.Chat, reason); + try { await usr.ModifyAsync(x => x.Mute = false).ConfigureAwait(false); } catch { } + try { await usr.RemoveRoleAsync(await GetMuteRole(usr.Guild).ConfigureAwait(false)).ConfigureAwait(false); } catch { /*ignore*/ } + UserUnmuted(usr, mod, MuteType.All, reason); } } - - public async Task UnmuteUser(ulong guildId, ulong usrId, IUser mod, MuteType type = MuteType.All, string reason = "") + else if (type == MuteType.Voice) { - var usr = _client.GetGuild(guildId)?.GetUser(usrId); - if (type == MuteType.All) - { - StopTimer(guildId, usrId, TimerType.Mute); - using (var uow = _db.GetDbContext()) - { - var config = uow.GuildConfigsForId(guildId, set => set.Include(gc => gc.MutedUsers) - .Include(gc => gc.UnmuteTimers)); - var match = new MutedUserId() - { - UserId = usrId - }; - var toRemove = config.MutedUsers.FirstOrDefault(x => x.Equals(match)); - if (toRemove != null) - { - uow.Remove(toRemove); - } - if (MutedUsers.TryGetValue(guildId, out ConcurrentHashSet muted)) - muted.TryRemove(usrId); - - config.UnmuteTimers.RemoveWhere(x => x.UserId == usrId); - - await uow.SaveChangesAsync(); - } - if (usr != null) - { - try { await usr.ModifyAsync(x => x.Mute = false).ConfigureAwait(false); } catch { } - try { await usr.RemoveRoleAsync(await GetMuteRole(usr.Guild).ConfigureAwait(false)).ConfigureAwait(false); } catch { /*ignore*/ } - UserUnmuted(usr, mod, MuteType.All, reason); - } - } - else if (type == MuteType.Voice) - { - if (usr is null) - return; - try - { - await usr.ModifyAsync(x => x.Mute = false).ConfigureAwait(false); - UserUnmuted(usr, mod, MuteType.Voice, reason); - } - catch { } - } - else if (type == MuteType.Chat) - { - if (usr is null) - return; - await usr.RemoveRoleAsync(await GetMuteRole(usr.Guild).ConfigureAwait(false)).ConfigureAwait(false); - UserUnmuted(usr, mod, MuteType.Chat, reason); - } - } - - public async Task GetMuteRole(IGuild guild) - { - if (guild is null) - throw new ArgumentNullException(nameof(guild)); - - const string defaultMuteRoleName = "nadeko-mute"; - - var muteRoleName = GuildMuteRoles.GetOrAdd(guild.Id, defaultMuteRoleName); - - var muteRole = guild.Roles.FirstOrDefault(r => r.Name == muteRoleName); - if (muteRole is null) - { - - //if it doesn't exist, create it - try { muteRole = await guild.CreateRoleAsync(muteRoleName, isMentionable: false).ConfigureAwait(false); } - catch - { - //if creations fails, maybe the name is not correct, find default one, if doesn't work, create default one - muteRole = guild.Roles.FirstOrDefault(r => r.Name == muteRoleName) ?? - await guild.CreateRoleAsync(defaultMuteRoleName, isMentionable: false).ConfigureAwait(false); - } - } - - foreach (var toOverwrite in (await guild.GetTextChannelsAsync().ConfigureAwait(false))) - { - try - { - if (!toOverwrite.PermissionOverwrites.Any(x => x.TargetId == muteRole.Id - && x.TargetType == PermissionTarget.Role)) - { - await toOverwrite.AddPermissionOverwriteAsync(muteRole, denyOverwrite) - .ConfigureAwait(false); - - await Task.Delay(200).ConfigureAwait(false); - } - } - catch - { - // ignored - } - } - - return muteRole; - } - - public async Task TimedMute(IGuildUser user, IUser mod, TimeSpan after, MuteType muteType = MuteType.All, string reason = "") - { - await MuteUser(user, mod, muteType, reason).ConfigureAwait(false); // mute the user. This will also remove any previous unmute timers - using (var uow = _db.GetDbContext()) - { - var config = uow.GuildConfigsForId(user.GuildId, set => set.Include(x => x.UnmuteTimers)); - config.UnmuteTimers.Add(new UnmuteTimer() - { - UserId = user.Id, - UnmuteAt = DateTime.UtcNow + after, - }); // add teh unmute timer to the database - uow.SaveChanges(); - } - - StartUn_Timer(user.GuildId, user.Id, after, TimerType.Mute); // start the timer - } - - public async Task TimedBan(IGuild guild, IUser user, TimeSpan after, string reason) - { - await guild.AddBanAsync(user.Id, 0, reason).ConfigureAwait(false); - using (var uow = _db.GetDbContext()) - { - var config = uow.GuildConfigsForId(guild.Id, set => set.Include(x => x.UnbanTimer)); - config.UnbanTimer.Add(new UnbanTimer() - { - UserId = user.Id, - UnbanAt = DateTime.UtcNow + after, - }); // add teh unmute timer to the database - uow.SaveChanges(); - } - - StartUn_Timer(guild.Id, user.Id, after, TimerType.Ban); // start the timer - } - - public async Task TimedRole(IGuildUser user, TimeSpan after, string reason, IRole role) - { - await user.AddRoleAsync(role).ConfigureAwait(false); - using (var uow = _db.GetDbContext()) - { - var config = uow.GuildConfigsForId(user.GuildId, set => set.Include(x => x.UnroleTimer)); - config.UnroleTimer.Add(new UnroleTimer() - { - UserId = user.Id, - UnbanAt = DateTime.UtcNow + after, - RoleId = role.Id - }); // add teh unmute timer to the database - uow.SaveChanges(); - } - StartUn_Timer(user.GuildId, user.Id, after, TimerType.AddRole, role.Id); // start the timer - } - - public enum TimerType { Mute, Ban, AddRole } - public void StartUn_Timer(ulong guildId, ulong userId, TimeSpan after, TimerType type, ulong? roleId = null) - { - //load the unmute timers for this guild - var userUnTimers = Un_Timers.GetOrAdd(guildId, new ConcurrentDictionary<(ulong, TimerType), Timer>()); - - //unmute timer to be added - var toAdd = new Timer(async _ => - { - if (type == TimerType.Ban) - { - try - { - RemoveTimerFromDb(guildId, userId, type); - StopTimer(guildId, userId, type); - var guild = _client.GetGuild(guildId); // load the guild - if (guild != null) - { - await guild.RemoveBanAsync(userId).ConfigureAwait(false); - } - } - catch (Exception ex) - { - Log.Warning(ex, "Couldn't unban user {0} in guild {1}", userId, guildId); - } - } - else if (type == TimerType.AddRole) - { - try - { - RemoveTimerFromDb(guildId, userId, type); - StopTimer(guildId, userId, type); - var guild = _client.GetGuild(guildId); - var user = guild?.GetUser(userId); - var role = guild.GetRole(roleId.Value); - if (guild != null && user != null && user.Roles.Contains(role)) - { - await user.RemoveRoleAsync(role).ConfigureAwait(false); - } - } - catch (Exception ex) - { - Log.Warning(ex, "Couldn't remove role from user {0} in guild {1}", userId, guildId); - } - } - else - { - try - { - // unmute the user, this will also remove the timer from the db - await UnmuteUser(guildId, userId, _client.CurrentUser, reason: "Timed mute expired").ConfigureAwait(false); - } - catch (Exception ex) - { - RemoveTimerFromDb(guildId, userId, type); // if unmute errored, just remove unmute from db - Log.Warning(ex, "Couldn't unmute user {0} in guild {1}", userId, guildId); - } - } - }, null, after, Timeout.InfiniteTimeSpan); - - //add it, or stop the old one and add this one - userUnTimers.AddOrUpdate((userId, type), (key) => toAdd, (key, old) => - { - old.Change(Timeout.Infinite, Timeout.Infinite); - return toAdd; - }); - } - - public void StopTimer(ulong guildId, ulong userId, TimerType type) - { - if (!Un_Timers.TryGetValue(guildId, out ConcurrentDictionary<(ulong, TimerType), Timer> userTimer)) + if (usr is null) return; - - if (userTimer.TryRemove((userId, type), out Timer removed)) + try { - removed.Change(Timeout.Infinite, Timeout.Infinite); + await usr.ModifyAsync(x => x.Mute = false).ConfigureAwait(false); + UserUnmuted(usr, mod, MuteType.Voice, reason); + } + catch { } + } + else if (type == MuteType.Chat) + { + if (usr is null) + return; + await usr.RemoveRoleAsync(await GetMuteRole(usr.Guild).ConfigureAwait(false)).ConfigureAwait(false); + UserUnmuted(usr, mod, MuteType.Chat, reason); + } + } + + public async Task GetMuteRole(IGuild guild) + { + if (guild is null) + throw new ArgumentNullException(nameof(guild)); + + const string defaultMuteRoleName = "nadeko-mute"; + + var muteRoleName = GuildMuteRoles.GetOrAdd(guild.Id, defaultMuteRoleName); + + var muteRole = guild.Roles.FirstOrDefault(r => r.Name == muteRoleName); + if (muteRole is null) + { + + //if it doesn't exist, create it + try { muteRole = await guild.CreateRoleAsync(muteRoleName, isMentionable: false).ConfigureAwait(false); } + catch + { + //if creations fails, maybe the name is not correct, find default one, if doesn't work, create default one + muteRole = guild.Roles.FirstOrDefault(r => r.Name == muteRoleName) ?? + await guild.CreateRoleAsync(defaultMuteRoleName, isMentionable: false).ConfigureAwait(false); } } - private void RemoveTimerFromDb(ulong guildId, ulong userId, TimerType type) + foreach (var toOverwrite in (await guild.GetTextChannelsAsync().ConfigureAwait(false))) { - using (var uow = _db.GetDbContext()) + try { - object toDelete; - if (type == TimerType.Mute) + if (!toOverwrite.PermissionOverwrites.Any(x => x.TargetId == muteRole.Id + && x.TargetType == PermissionTarget.Role)) { - var config = uow.GuildConfigsForId(guildId, set => set.Include(x => x.UnmuteTimers)); - toDelete = config.UnmuteTimers.FirstOrDefault(x => x.UserId == userId); + await toOverwrite.AddPermissionOverwriteAsync(muteRole, denyOverwrite) + .ConfigureAwait(false); + + await Task.Delay(200).ConfigureAwait(false); } - else - { - var config = uow.GuildConfigsForId(guildId, set => set.Include(x => x.UnbanTimer)); - toDelete = config.UnbanTimer.FirstOrDefault(x => x.UserId == userId); - } - if (toDelete != null) - { - uow.Remove(toDelete); - } - uow.SaveChanges(); } + catch + { + // ignored + } + } + + return muteRole; + } + + public async Task TimedMute(IGuildUser user, IUser mod, TimeSpan after, MuteType muteType = MuteType.All, string reason = "") + { + await MuteUser(user, mod, muteType, reason).ConfigureAwait(false); // mute the user. This will also remove any previous unmute timers + using (var uow = _db.GetDbContext()) + { + var config = uow.GuildConfigsForId(user.GuildId, set => set.Include(x => x.UnmuteTimers)); + config.UnmuteTimers.Add(new UnmuteTimer() + { + UserId = user.Id, + UnmuteAt = DateTime.UtcNow + after, + }); // add teh unmute timer to the database + uow.SaveChanges(); + } + + StartUn_Timer(user.GuildId, user.Id, after, TimerType.Mute); // start the timer + } + + public async Task TimedBan(IGuild guild, IUser user, TimeSpan after, string reason) + { + await guild.AddBanAsync(user.Id, 0, reason).ConfigureAwait(false); + using (var uow = _db.GetDbContext()) + { + var config = uow.GuildConfigsForId(guild.Id, set => set.Include(x => x.UnbanTimer)); + config.UnbanTimer.Add(new UnbanTimer() + { + UserId = user.Id, + UnbanAt = DateTime.UtcNow + after, + }); // add teh unmute timer to the database + uow.SaveChanges(); + } + + StartUn_Timer(guild.Id, user.Id, after, TimerType.Ban); // start the timer + } + + public async Task TimedRole(IGuildUser user, TimeSpan after, string reason, IRole role) + { + await user.AddRoleAsync(role).ConfigureAwait(false); + using (var uow = _db.GetDbContext()) + { + var config = uow.GuildConfigsForId(user.GuildId, set => set.Include(x => x.UnroleTimer)); + config.UnroleTimer.Add(new UnroleTimer() + { + UserId = user.Id, + UnbanAt = DateTime.UtcNow + after, + RoleId = role.Id + }); // add teh unmute timer to the database + uow.SaveChanges(); + } + StartUn_Timer(user.GuildId, user.Id, after, TimerType.AddRole, role.Id); // start the timer + } + + public enum TimerType { Mute, Ban, AddRole } + public void StartUn_Timer(ulong guildId, ulong userId, TimeSpan after, TimerType type, ulong? roleId = null) + { + //load the unmute timers for this guild + var userUnTimers = Un_Timers.GetOrAdd(guildId, new ConcurrentDictionary<(ulong, TimerType), Timer>()); + + //unmute timer to be added + var toAdd = new Timer(async _ => + { + if (type == TimerType.Ban) + { + try + { + RemoveTimerFromDb(guildId, userId, type); + StopTimer(guildId, userId, type); + var guild = _client.GetGuild(guildId); // load the guild + if (guild != null) + { + await guild.RemoveBanAsync(userId).ConfigureAwait(false); + } + } + catch (Exception ex) + { + Log.Warning(ex, "Couldn't unban user {0} in guild {1}", userId, guildId); + } + } + else if (type == TimerType.AddRole) + { + try + { + RemoveTimerFromDb(guildId, userId, type); + StopTimer(guildId, userId, type); + var guild = _client.GetGuild(guildId); + var user = guild?.GetUser(userId); + var role = guild.GetRole(roleId.Value); + if (guild != null && user != null && user.Roles.Contains(role)) + { + await user.RemoveRoleAsync(role).ConfigureAwait(false); + } + } + catch (Exception ex) + { + Log.Warning(ex, "Couldn't remove role from user {0} in guild {1}", userId, guildId); + } + } + else + { + try + { + // unmute the user, this will also remove the timer from the db + await UnmuteUser(guildId, userId, _client.CurrentUser, reason: "Timed mute expired").ConfigureAwait(false); + } + catch (Exception ex) + { + RemoveTimerFromDb(guildId, userId, type); // if unmute errored, just remove unmute from db + Log.Warning(ex, "Couldn't unmute user {0} in guild {1}", userId, guildId); + } + } + }, null, after, Timeout.InfiniteTimeSpan); + + //add it, or stop the old one and add this one + userUnTimers.AddOrUpdate((userId, type), (key) => toAdd, (key, old) => + { + old.Change(Timeout.Infinite, Timeout.Infinite); + return toAdd; + }); + } + + public void StopTimer(ulong guildId, ulong userId, TimerType type) + { + if (!Un_Timers.TryGetValue(guildId, out ConcurrentDictionary<(ulong, TimerType), Timer> userTimer)) + return; + + if (userTimer.TryRemove((userId, type), out Timer removed)) + { + removed.Change(Timeout.Infinite, Timeout.Infinite); + } + } + + private void RemoveTimerFromDb(ulong guildId, ulong userId, TimerType type) + { + using (var uow = _db.GetDbContext()) + { + object toDelete; + if (type == TimerType.Mute) + { + var config = uow.GuildConfigsForId(guildId, set => set.Include(x => x.UnmuteTimers)); + toDelete = config.UnmuteTimers.FirstOrDefault(x => x.UserId == userId); + } + else + { + var config = uow.GuildConfigsForId(guildId, set => set.Include(x => x.UnbanTimer)); + toDelete = config.UnbanTimer.FirstOrDefault(x => x.UserId == userId); + } + if (toDelete != null) + { + uow.Remove(toDelete); + } + uow.SaveChanges(); } } } \ No newline at end of file diff --git a/src/NadekoBot/Modules/Administration/Services/PlayingRotateService.cs b/src/NadekoBot/Modules/Administration/Services/PlayingRotateService.cs index 1e6e40a45..ebd824c75 100644 --- a/src/NadekoBot/Modules/Administration/Services/PlayingRotateService.cs +++ b/src/NadekoBot/Modules/Administration/Services/PlayingRotateService.cs @@ -1,7 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading; +using System.Threading; using Discord.WebSocket; using NadekoBot.Common.Replacements; using NadekoBot.Services; @@ -10,115 +7,113 @@ using Discord; using System.Threading.Tasks; using Microsoft.EntityFrameworkCore; using NadekoBot.Common; -using Serilog; -namespace NadekoBot.Modules.Administration.Services +namespace NadekoBot.Modules.Administration.Services; + +public sealed class PlayingRotateService : INService { - public sealed class PlayingRotateService : INService + private readonly Timer _t; + private readonly BotConfigService _bss; + private readonly SelfService _selfService; + private readonly Replacer _rep; + private readonly DbService _db; + private readonly Bot _bot; + + private class TimerState { - private readonly Timer _t; - private readonly BotConfigService _bss; - private readonly SelfService _selfService; - private readonly Replacer _rep; - private readonly DbService _db; - private readonly Bot _bot; + public int Index { get; set; } + } - private class TimerState + public PlayingRotateService(DiscordSocketClient client, DbService db, Bot bot, + BotConfigService bss, IEnumerable phProviders, SelfService selfService) + { + _db = db; + _bot = bot; + _bss = bss; + _selfService = selfService; + + if (client.ShardId == 0) { - public int Index { get; set; } - } + _rep = new ReplacementBuilder() + .WithClient(client) + .WithProviders(phProviders) + .Build(); - public PlayingRotateService(DiscordSocketClient client, DbService db, Bot bot, - BotConfigService bss, IEnumerable phProviders, SelfService selfService) - { - _db = db; - _bot = bot; - _bss = bss; - _selfService = selfService; - - if (client.ShardId == 0) - { - _rep = new ReplacementBuilder() - .WithClient(client) - .WithProviders(phProviders) - .Build(); - - _t = new Timer(RotatingStatuses, new TimerState(), TimeSpan.FromMinutes(1), TimeSpan.FromMinutes(1)); - } - } - - private async void RotatingStatuses(object objState) - { - try - { - var state = (TimerState) objState; - - if (!_bss.Data.RotateStatuses) return; - - IReadOnlyList rotatingStatuses; - using (var uow = _db.GetDbContext()) - { - rotatingStatuses = uow.RotatingStatus - .AsNoTracking() - .OrderBy(x => x.Id) - .ToList(); - } - - if (rotatingStatuses.Count == 0) - return; - - var playingStatus = state.Index >= rotatingStatuses.Count - ? rotatingStatuses[state.Index = 0] - : rotatingStatuses[state.Index++]; - - var statusText = _rep.Replace(playingStatus.Status); - await _selfService.SetGameAsync(statusText, playingStatus.Type); - } - catch (Exception ex) - { - Log.Warning(ex, "Rotating playing status errored: {ErrorMessage}", ex.Message); - } - } - - public async Task RemovePlayingAsync(int index) - { - if (index < 0) - throw new ArgumentOutOfRangeException(nameof(index)); - - using var uow = _db.GetDbContext(); - var toRemove = await uow.RotatingStatus - .AsQueryable() - .AsNoTracking() - .Skip(index) - .FirstOrDefaultAsync(); - - if (toRemove is null) - return null; - - uow.Remove(toRemove); - await uow.SaveChangesAsync(); - return toRemove.Status; - } - - public async Task AddPlaying(ActivityType t, string status) - { - using var uow = _db.GetDbContext(); - var toAdd = new RotatingPlayingStatus {Status = status, Type = t}; - uow.Add(toAdd); - await uow.SaveChangesAsync(); - } - - public bool ToggleRotatePlaying() - { - var enabled = false; - _bss.ModifyConfig(bs => { enabled = bs.RotateStatuses = !bs.RotateStatuses; }); - return enabled; - } - - public IReadOnlyList GetRotatingStatuses() - { - using var uow = _db.GetDbContext(); - return uow.RotatingStatus.AsNoTracking().ToList(); + _t = new Timer(RotatingStatuses, new TimerState(), TimeSpan.FromMinutes(1), TimeSpan.FromMinutes(1)); } } + + private async void RotatingStatuses(object objState) + { + try + { + var state = (TimerState) objState; + + if (!_bss.Data.RotateStatuses) return; + + IReadOnlyList rotatingStatuses; + using (var uow = _db.GetDbContext()) + { + rotatingStatuses = uow.RotatingStatus + .AsNoTracking() + .OrderBy(x => x.Id) + .ToList(); + } + + if (rotatingStatuses.Count == 0) + return; + + var playingStatus = state.Index >= rotatingStatuses.Count + ? rotatingStatuses[state.Index = 0] + : rotatingStatuses[state.Index++]; + + var statusText = _rep.Replace(playingStatus.Status); + await _selfService.SetGameAsync(statusText, playingStatus.Type); + } + catch (Exception ex) + { + Log.Warning(ex, "Rotating playing status errored: {ErrorMessage}", ex.Message); + } + } + + public async Task RemovePlayingAsync(int index) + { + if (index < 0) + throw new ArgumentOutOfRangeException(nameof(index)); + + using var uow = _db.GetDbContext(); + var toRemove = await uow.RotatingStatus + .AsQueryable() + .AsNoTracking() + .Skip(index) + .FirstOrDefaultAsync(); + + if (toRemove is null) + return null; + + uow.Remove(toRemove); + await uow.SaveChangesAsync(); + return toRemove.Status; + } + + public async Task AddPlaying(ActivityType t, string status) + { + using var uow = _db.GetDbContext(); + var toAdd = new RotatingPlayingStatus {Status = status, Type = t}; + uow.Add(toAdd); + await uow.SaveChangesAsync(); + } + + public bool ToggleRotatePlaying() + { + var enabled = false; + _bss.ModifyConfig(bs => { enabled = bs.RotateStatuses = !bs.RotateStatuses; }); + return enabled; + } + + public IReadOnlyList GetRotatingStatuses() + { + using var uow = _db.GetDbContext(); + return uow.RotatingStatus.AsNoTracking().ToList(); + } } \ No newline at end of file diff --git a/src/NadekoBot/Modules/Administration/Services/ProtectionService.cs b/src/NadekoBot/Modules/Administration/Services/ProtectionService.cs index 0c368e0a0..cf9804207 100644 --- a/src/NadekoBot/Modules/Administration/Services/ProtectionService.cs +++ b/src/NadekoBot/Modules/Administration/Services/ProtectionService.cs @@ -1,6 +1,4 @@ -using System; -using System.Collections.Concurrent; -using System.Linq; +using System.Collections.Concurrent; using System.Threading.Channels; using System.Threading.Tasks; using Discord; @@ -10,479 +8,476 @@ using NadekoBot.Services; using NadekoBot.Services.Database.Models; using NadekoBot.Extensions; using Microsoft.EntityFrameworkCore; -using NadekoBot.Common.TypeReaders.Models; using NadekoBot.Db; -using Serilog; -namespace NadekoBot.Modules.Administration.Services +namespace NadekoBot.Modules.Administration.Services; + +public class ProtectionService : INService { - public class ProtectionService : INService + private readonly ConcurrentDictionary _antiRaidGuilds + = new ConcurrentDictionary(); + + private readonly ConcurrentDictionary _antiSpamGuilds + = new ConcurrentDictionary(); + + private readonly ConcurrentDictionary _antiAltGuilds + = new ConcurrentDictionary(); + + public event Func OnAntiProtectionTriggered + = delegate { return Task.CompletedTask; }; + + private readonly DiscordSocketClient _client; + private readonly MuteService _mute; + private readonly DbService _db; + private readonly UserPunishService _punishService; + + private readonly Channel PunishUserQueue = + System.Threading.Channels.Channel.CreateUnbounded(new UnboundedChannelOptions() + { + SingleReader = true, + SingleWriter = false + }); + + public ProtectionService(DiscordSocketClient client, Bot bot, + MuteService mute, DbService db, UserPunishService punishService) + { + _client = client; + _mute = mute; + _db = db; + _punishService = punishService; + + var ids = client.GetGuildIds(); + using (var uow = db.GetDbContext()) + { + var configs = uow.Set() + .AsQueryable() + .Include(x => x.AntiRaidSetting) + .Include(x => x.AntiSpamSetting) + .ThenInclude(x => x.IgnoredChannels) + .Include(x => x.AntiAltSetting) + .Where(x => ids.Contains(x.GuildId)) + .ToList(); + + foreach (var gc in configs) + { + Initialize(gc); + } + } + + _client.MessageReceived += HandleAntiSpam; + _client.UserJoined += HandleUserJoined; + + bot.JoinedGuild += _bot_JoinedGuild; + _client.LeftGuild += _client_LeftGuild; + + _ = Task.Run(RunQueue); + } + + private async Task RunQueue() { - private readonly ConcurrentDictionary _antiRaidGuilds - = new ConcurrentDictionary(); - - private readonly ConcurrentDictionary _antiSpamGuilds - = new ConcurrentDictionary(); - - private readonly ConcurrentDictionary _antiAltGuilds - = new ConcurrentDictionary(); - - public event Func OnAntiProtectionTriggered - = delegate { return Task.CompletedTask; }; - - private readonly DiscordSocketClient _client; - private readonly MuteService _mute; - private readonly DbService _db; - private readonly UserPunishService _punishService; - - private readonly Channel PunishUserQueue = - System.Threading.Channels.Channel.CreateUnbounded(new UnboundedChannelOptions() - { - SingleReader = true, - SingleWriter = false - }); - - public ProtectionService(DiscordSocketClient client, Bot bot, - MuteService mute, DbService db, UserPunishService punishService) - { - _client = client; - _mute = mute; - _db = db; - _punishService = punishService; - - var ids = client.GetGuildIds(); - using (var uow = db.GetDbContext()) - { - var configs = uow.Set() - .AsQueryable() - .Include(x => x.AntiRaidSetting) - .Include(x => x.AntiSpamSetting) - .ThenInclude(x => x.IgnoredChannels) - .Include(x => x.AntiAltSetting) - .Where(x => ids.Contains(x.GuildId)) - .ToList(); - - foreach (var gc in configs) - { - Initialize(gc); - } - } - - _client.MessageReceived += HandleAntiSpam; - _client.UserJoined += HandleUserJoined; - - bot.JoinedGuild += _bot_JoinedGuild; - _client.LeftGuild += _client_LeftGuild; - - _ = Task.Run(RunQueue); - } - - private async Task RunQueue() + while (true) { - while (true) - { - var item = await PunishUserQueue.Reader.ReadAsync(); + var item = await PunishUserQueue.Reader.ReadAsync(); - var muteTime = item.MuteTime; - var gu = item.User; - try - { - await _punishService.ApplyPunishment(gu.Guild, gu, _client.CurrentUser, - item.Action, muteTime, item.RoleId, $"{item.Type} Protection"); - } - catch (Exception ex) - { - Log.Warning(ex, "Error in punish queue: {Message}", ex.Message); - } - finally - { - await Task.Delay(1000); - } + var muteTime = item.MuteTime; + var gu = item.User; + try + { + await _punishService.ApplyPunishment(gu.Guild, gu, _client.CurrentUser, + item.Action, muteTime, item.RoleId, $"{item.Type} Protection"); + } + catch (Exception ex) + { + Log.Warning(ex, "Error in punish queue: {Message}", ex.Message); + } + finally + { + await Task.Delay(1000); } } + } - private Task _client_LeftGuild(SocketGuild guild) + private Task _client_LeftGuild(SocketGuild guild) + { + var _ = Task.Run(async () => { - var _ = Task.Run(async () => - { - TryStopAntiRaid(guild.Id); - TryStopAntiSpam(guild.Id); - await TryStopAntiAlt(guild.Id); - }); + TryStopAntiRaid(guild.Id); + TryStopAntiSpam(guild.Id); + await TryStopAntiAlt(guild.Id); + }); + return Task.CompletedTask; + } + + private Task _bot_JoinedGuild(GuildConfig gc) + { + using var uow = _db.GetDbContext(); + var gcWithData = uow.GuildConfigsForId(gc.GuildId, + set => set + .Include(x => x.AntiRaidSetting) + .Include(x => x.AntiAltSetting) + .Include(x => x.AntiSpamSetting) + .ThenInclude(x => x.IgnoredChannels)); + + Initialize(gcWithData); + return Task.CompletedTask; + } + + private void Initialize(GuildConfig gc) + { + var raid = gc.AntiRaidSetting; + var spam = gc.AntiSpamSetting; + + if (raid != null) + { + var raidStats = new AntiRaidStats() { AntiRaidSettings = raid }; + _antiRaidGuilds[gc.GuildId] = raidStats; + } + + if (spam != null) + _antiSpamGuilds[gc.GuildId] = new AntiSpamStats() { AntiSpamSettings = spam }; + + var alt = gc.AntiAltSetting; + if (alt is not null) + _antiAltGuilds[gc.GuildId] = new AntiAltStats(alt); + } + + private Task HandleUserJoined(SocketGuildUser user) + { + if (user.IsBot) return Task.CompletedTask; - } - - private Task _bot_JoinedGuild(GuildConfig gc) - { - using var uow = _db.GetDbContext(); - var gcWithData = uow.GuildConfigsForId(gc.GuildId, - set => set - .Include(x => x.AntiRaidSetting) - .Include(x => x.AntiAltSetting) - .Include(x => x.AntiSpamSetting) - .ThenInclude(x => x.IgnoredChannels)); - - Initialize(gcWithData); + + _antiRaidGuilds.TryGetValue(user.Guild.Id, out var maybeStats); + _antiAltGuilds.TryGetValue(user.Guild.Id, out var maybeAlts); + + if (maybeStats is null && maybeAlts is null) return Task.CompletedTask; - } - private void Initialize(GuildConfig gc) + _ = Task.Run(async () => { - var raid = gc.AntiRaidSetting; - var spam = gc.AntiSpamSetting; - - if (raid != null) + if (maybeAlts is AntiAltStats alts) { - var raidStats = new AntiRaidStats() { AntiRaidSettings = raid }; - _antiRaidGuilds[gc.GuildId] = raidStats; - } - - if (spam != null) - _antiSpamGuilds[gc.GuildId] = new AntiSpamStats() { AntiSpamSettings = spam }; - - var alt = gc.AntiAltSetting; - if (alt is not null) - _antiAltGuilds[gc.GuildId] = new AntiAltStats(alt); - } - - private Task HandleUserJoined(SocketGuildUser user) - { - if (user.IsBot) - return Task.CompletedTask; - - _antiRaidGuilds.TryGetValue(user.Guild.Id, out var maybeStats); - _antiAltGuilds.TryGetValue(user.Guild.Id, out var maybeAlts); - - if (maybeStats is null && maybeAlts is null) - return Task.CompletedTask; - - _ = Task.Run(async () => - { - if (maybeAlts is AntiAltStats alts) + if (user.CreatedAt != default) { - if (user.CreatedAt != default) + var diff = DateTime.UtcNow - user.CreatedAt.UtcDateTime; + if (diff < alts.MinAge) { - var diff = DateTime.UtcNow - user.CreatedAt.UtcDateTime; - if (diff < alts.MinAge) - { - alts.Increment(); + alts.Increment(); - await PunishUsers( - alts.Action, - ProtectionType.Alting, - alts.ActionDurationMinutes, - alts.RoleId, - user); + await PunishUsers( + alts.Action, + ProtectionType.Alting, + alts.ActionDurationMinutes, + alts.RoleId, + user); - return; - } + return; } } + } - try - { - if (!(maybeStats is AntiRaidStats stats) || !stats.RaidUsers.Add(user)) - return; + try + { + if (!(maybeStats is AntiRaidStats stats) || !stats.RaidUsers.Add(user)) + return; - ++stats.UsersCount; + ++stats.UsersCount; - if (stats.UsersCount >= stats.AntiRaidSettings.UserThreshold) - { - var users = stats.RaidUsers.ToArray(); - stats.RaidUsers.Clear(); - var settings = stats.AntiRaidSettings; - - await PunishUsers(settings.Action, ProtectionType.Raiding, - settings.PunishDuration, null, users).ConfigureAwait(false); - } - await Task.Delay(1000 * stats.AntiRaidSettings.Seconds).ConfigureAwait(false); - - stats.RaidUsers.TryRemove(user); - --stats.UsersCount; - - } - catch + if (stats.UsersCount >= stats.AntiRaidSettings.UserThreshold) { - // ignored + var users = stats.RaidUsers.ToArray(); + stats.RaidUsers.Clear(); + var settings = stats.AntiRaidSettings; + + await PunishUsers(settings.Action, ProtectionType.Raiding, + settings.PunishDuration, null, users).ConfigureAwait(false); } - }); - return Task.CompletedTask; - } + await Task.Delay(1000 * stats.AntiRaidSettings.Seconds).ConfigureAwait(false); - private Task HandleAntiSpam(SocketMessage arg) - { - if (!(arg is SocketUserMessage msg) || msg.Author.IsBot) - return Task.CompletedTask; + stats.RaidUsers.TryRemove(user); + --stats.UsersCount; - if (!(msg.Channel is ITextChannel channel)) - return Task.CompletedTask; - var _ = Task.Run(async () => - { - try - { - if (!_antiSpamGuilds.TryGetValue(channel.Guild.Id, out var spamSettings) || - spamSettings.AntiSpamSettings.IgnoredChannels.Contains(new AntiSpamIgnore() - { - ChannelId = channel.Id - })) - return; - - var stats = spamSettings.UserStats.AddOrUpdate(msg.Author.Id, (id) => new UserSpamStats(msg), - (id, old) => - { - old.ApplyNextMessage(msg); return old; - }); - - if (stats.Count >= spamSettings.AntiSpamSettings.MessageThreshold) - { - if (spamSettings.UserStats.TryRemove(msg.Author.Id, out stats)) - { - stats.Dispose(); - var settings = spamSettings.AntiSpamSettings; - await PunishUsers(settings.Action, ProtectionType.Spamming, settings.MuteTime, - settings.RoleId, (IGuildUser)msg.Author) - .ConfigureAwait(false); - } - } - } - catch - { - // ignored - } - }); - return Task.CompletedTask; - } - - private async Task PunishUsers(PunishmentAction action, ProtectionType pt, int muteTime, ulong? roleId, - params IGuildUser[] gus) - { - Log.Information( - "[{PunishType}] - Punishing [{Count}] users with [{PunishAction}] in {GuildName} guild", - pt, - gus.Length, - action, - gus[0].Guild.Name); - - foreach (var gu in gus) - { - await PunishUserQueue.Writer.WriteAsync(new PunishQueueItem() - { - Action = action, - Type = pt, - User = gu, - MuteTime = muteTime, - RoleId = roleId - }); } + catch + { + // ignored + } + }); + return Task.CompletedTask; + } - _ = OnAntiProtectionTriggered(action, pt, gus); + private Task HandleAntiSpam(SocketMessage arg) + { + if (!(arg is SocketUserMessage msg) || msg.Author.IsBot) + return Task.CompletedTask; + + if (!(msg.Channel is ITextChannel channel)) + return Task.CompletedTask; + var _ = Task.Run(async () => + { + try + { + if (!_antiSpamGuilds.TryGetValue(channel.Guild.Id, out var spamSettings) || + spamSettings.AntiSpamSettings.IgnoredChannels.Contains(new AntiSpamIgnore() + { + ChannelId = channel.Id + })) + return; + + var stats = spamSettings.UserStats.AddOrUpdate(msg.Author.Id, (id) => new UserSpamStats(msg), + (id, old) => + { + old.ApplyNextMessage(msg); return old; + }); + + if (stats.Count >= spamSettings.AntiSpamSettings.MessageThreshold) + { + if (spamSettings.UserStats.TryRemove(msg.Author.Id, out stats)) + { + stats.Dispose(); + var settings = spamSettings.AntiSpamSettings; + await PunishUsers(settings.Action, ProtectionType.Spamming, settings.MuteTime, + settings.RoleId, (IGuildUser)msg.Author) + .ConfigureAwait(false); + } + } + } + catch + { + // ignored + } + }); + return Task.CompletedTask; + } + + private async Task PunishUsers(PunishmentAction action, ProtectionType pt, int muteTime, ulong? roleId, + params IGuildUser[] gus) + { + Log.Information( + "[{PunishType}] - Punishing [{Count}] users with [{PunishAction}] in {GuildName} guild", + pt, + gus.Length, + action, + gus[0].Guild.Name); + + foreach (var gu in gus) + { + await PunishUserQueue.Writer.WriteAsync(new PunishQueueItem() + { + Action = action, + Type = pt, + User = gu, + MuteTime = muteTime, + RoleId = roleId + }); } - public async Task StartAntiRaidAsync(ulong guildId, int userThreshold, int seconds, - PunishmentAction action, int minutesDuration) - { - var g = _client.GetGuild(guildId); - await _mute.GetMuteRole(g).ConfigureAwait(false); + _ = OnAntiProtectionTriggered(action, pt, gus); + } - if (action == PunishmentAction.AddRole) - return null; + public async Task StartAntiRaidAsync(ulong guildId, int userThreshold, int seconds, + PunishmentAction action, int minutesDuration) + { + var g = _client.GetGuild(guildId); + await _mute.GetMuteRole(g).ConfigureAwait(false); + + if (action == PunishmentAction.AddRole) + return null; - if (!IsDurationAllowed(action)) - minutesDuration = 0; + if (!IsDurationAllowed(action)) + minutesDuration = 0; - var stats = new AntiRaidStats() + var stats = new AntiRaidStats() + { + AntiRaidSettings = new AntiRaidSetting() { - AntiRaidSettings = new AntiRaidSetting() - { - Action = action, - Seconds = seconds, - UserThreshold = userThreshold, - PunishDuration = minutesDuration - } - }; + Action = action, + Seconds = seconds, + UserThreshold = userThreshold, + PunishDuration = minutesDuration + } + }; - _antiRaidGuilds.AddOrUpdate(guildId, stats, (key, old) => stats); + _antiRaidGuilds.AddOrUpdate(guildId, stats, (key, old) => stats); + using (var uow = _db.GetDbContext()) + { + var gc = uow.GuildConfigsForId(guildId, set => set.Include(x => x.AntiRaidSetting)); + + gc.AntiRaidSetting = stats.AntiRaidSettings; + await uow.SaveChangesAsync(); + } + + return stats; + } + + public bool TryStopAntiRaid(ulong guildId) + { + if (_antiRaidGuilds.TryRemove(guildId, out _)) + { using (var uow = _db.GetDbContext()) { var gc = uow.GuildConfigsForId(guildId, set => set.Include(x => x.AntiRaidSetting)); - gc.AntiRaidSetting = stats.AntiRaidSettings; - await uow.SaveChangesAsync(); + gc.AntiRaidSetting = null; + uow.SaveChanges(); } - - return stats; - } - - public bool TryStopAntiRaid(ulong guildId) - { - if (_antiRaidGuilds.TryRemove(guildId, out _)) - { - using (var uow = _db.GetDbContext()) - { - var gc = uow.GuildConfigsForId(guildId, set => set.Include(x => x.AntiRaidSetting)); - - gc.AntiRaidSetting = null; - uow.SaveChanges(); - } - return true; - } - return false; - } - - public bool TryStopAntiSpam(ulong guildId) - { - if (_antiSpamGuilds.TryRemove(guildId, out var removed)) - { - removed.UserStats.ForEach(x => x.Value.Dispose()); - using (var uow = _db.GetDbContext()) - { - var gc = uow.GuildConfigsForId(guildId, set => set.Include(x => x.AntiSpamSetting) - .ThenInclude(x => x.IgnoredChannels)); - - gc.AntiSpamSetting = null; - uow.SaveChanges(); - } - return true; - } - return false; - } - - public async Task StartAntiSpamAsync(ulong guildId, int messageCount, PunishmentAction action, - int punishDurationMinutes, ulong? roleId) - { - var g = _client.GetGuild(guildId); - await _mute.GetMuteRole(g).ConfigureAwait(false); - - if (!IsDurationAllowed(action)) - punishDurationMinutes = 0; - - var stats = new AntiSpamStats - { - AntiSpamSettings = new AntiSpamSetting() - { - Action = action, - MessageThreshold = messageCount, - MuteTime = punishDurationMinutes, - RoleId = roleId, - } - }; - - stats = _antiSpamGuilds.AddOrUpdate(guildId, stats, (key, old) => - { - stats.AntiSpamSettings.IgnoredChannels = old.AntiSpamSettings.IgnoredChannels; - return stats; - }); - - using (var uow = _db.GetDbContext()) - { - var gc = uow.GuildConfigsForId(guildId, set => set.Include(x => x.AntiSpamSetting)); - - if (gc.AntiSpamSetting != null) - { - gc.AntiSpamSetting.Action = stats.AntiSpamSettings.Action; - gc.AntiSpamSetting.MessageThreshold = stats.AntiSpamSettings.MessageThreshold; - gc.AntiSpamSetting.MuteTime = stats.AntiSpamSettings.MuteTime; - gc.AntiSpamSetting.RoleId = stats.AntiSpamSettings.RoleId; - } - else - { - gc.AntiSpamSetting = stats.AntiSpamSettings; - } - await uow.SaveChangesAsync(); - } - return stats; - } - - public async Task AntiSpamIgnoreAsync(ulong guildId, ulong channelId) - { - var obj = new AntiSpamIgnore() - { - ChannelId = channelId - }; - bool added; - using (var uow = _db.GetDbContext()) - { - var gc = uow.GuildConfigsForId(guildId, set => set.Include(x => x.AntiSpamSetting).ThenInclude(x => x.IgnoredChannels)); - var spam = gc.AntiSpamSetting; - if (spam is null) - { - return null; - } - - if (spam.IgnoredChannels.Add(obj)) // if adding to db is successful - { - if (_antiSpamGuilds.TryGetValue(guildId, out var temp)) - temp.AntiSpamSettings.IgnoredChannels.Add(obj); // add to local cache - added = true; - } - else - { - var toRemove = spam.IgnoredChannels.First(x => x.ChannelId == channelId); - uow.Set().Remove(toRemove); // remove from db - if (_antiSpamGuilds.TryGetValue(guildId, out var temp)) - { - temp.AntiSpamSettings.IgnoredChannels.Remove(toRemove); // remove from local cache - } - added = false; - } - - await uow.SaveChangesAsync(); - } - return added; - } - - public (AntiSpamStats, AntiRaidStats, AntiAltStats) GetAntiStats(ulong guildId) - { - _antiRaidGuilds.TryGetValue(guildId, out var antiRaidStats); - _antiSpamGuilds.TryGetValue(guildId, out var antiSpamStats); - _antiAltGuilds.TryGetValue(guildId, out var antiAltStats); - - return (antiSpamStats, antiRaidStats, antiAltStats); - } - - public bool IsDurationAllowed(PunishmentAction action) - { - switch (action) - { - case PunishmentAction.Ban: - case PunishmentAction.Mute: - case PunishmentAction.ChatMute: - case PunishmentAction.VoiceMute: - case PunishmentAction.AddRole: - return true; - default: - return false; - } - } - - public async Task StartAntiAltAsync(ulong guildId, int minAgeMinutes, PunishmentAction action, - int actionDurationMinutes = 0, ulong? roleId = null) - { - using var uow = _db.GetDbContext(); - var gc = uow.GuildConfigsForId(guildId, set => set.Include(x => x.AntiAltSetting)); - gc.AntiAltSetting = new AntiAltSetting() - { - Action = action, - ActionDurationMinutes = actionDurationMinutes, - MinAge = TimeSpan.FromMinutes(minAgeMinutes), - RoleId = roleId, - }; - - await uow.SaveChangesAsync(); - _antiAltGuilds[guildId] = new AntiAltStats(gc.AntiAltSetting); - } - - public async Task TryStopAntiAlt(ulong guildId) - { - if (!_antiAltGuilds.TryRemove(guildId, out _)) - return false; - - using var uow = _db.GetDbContext(); - var gc = uow.GuildConfigsForId(guildId, set => set.Include(x => x.AntiAltSetting)); - gc.AntiAltSetting = null; - await uow.SaveChangesAsync(); return true; } + return false; } -} + + public bool TryStopAntiSpam(ulong guildId) + { + if (_antiSpamGuilds.TryRemove(guildId, out var removed)) + { + removed.UserStats.ForEach(x => x.Value.Dispose()); + using (var uow = _db.GetDbContext()) + { + var gc = uow.GuildConfigsForId(guildId, set => set.Include(x => x.AntiSpamSetting) + .ThenInclude(x => x.IgnoredChannels)); + + gc.AntiSpamSetting = null; + uow.SaveChanges(); + } + return true; + } + return false; + } + + public async Task StartAntiSpamAsync(ulong guildId, int messageCount, PunishmentAction action, + int punishDurationMinutes, ulong? roleId) + { + var g = _client.GetGuild(guildId); + await _mute.GetMuteRole(g).ConfigureAwait(false); + + if (!IsDurationAllowed(action)) + punishDurationMinutes = 0; + + var stats = new AntiSpamStats + { + AntiSpamSettings = new AntiSpamSetting() + { + Action = action, + MessageThreshold = messageCount, + MuteTime = punishDurationMinutes, + RoleId = roleId, + } + }; + + stats = _antiSpamGuilds.AddOrUpdate(guildId, stats, (key, old) => + { + stats.AntiSpamSettings.IgnoredChannels = old.AntiSpamSettings.IgnoredChannels; + return stats; + }); + + using (var uow = _db.GetDbContext()) + { + var gc = uow.GuildConfigsForId(guildId, set => set.Include(x => x.AntiSpamSetting)); + + if (gc.AntiSpamSetting != null) + { + gc.AntiSpamSetting.Action = stats.AntiSpamSettings.Action; + gc.AntiSpamSetting.MessageThreshold = stats.AntiSpamSettings.MessageThreshold; + gc.AntiSpamSetting.MuteTime = stats.AntiSpamSettings.MuteTime; + gc.AntiSpamSetting.RoleId = stats.AntiSpamSettings.RoleId; + } + else + { + gc.AntiSpamSetting = stats.AntiSpamSettings; + } + await uow.SaveChangesAsync(); + } + return stats; + } + + public async Task AntiSpamIgnoreAsync(ulong guildId, ulong channelId) + { + var obj = new AntiSpamIgnore() + { + ChannelId = channelId + }; + bool added; + using (var uow = _db.GetDbContext()) + { + var gc = uow.GuildConfigsForId(guildId, set => set.Include(x => x.AntiSpamSetting).ThenInclude(x => x.IgnoredChannels)); + var spam = gc.AntiSpamSetting; + if (spam is null) + { + return null; + } + + if (spam.IgnoredChannels.Add(obj)) // if adding to db is successful + { + if (_antiSpamGuilds.TryGetValue(guildId, out var temp)) + temp.AntiSpamSettings.IgnoredChannels.Add(obj); // add to local cache + added = true; + } + else + { + var toRemove = spam.IgnoredChannels.First(x => x.ChannelId == channelId); + uow.Set().Remove(toRemove); // remove from db + if (_antiSpamGuilds.TryGetValue(guildId, out var temp)) + { + temp.AntiSpamSettings.IgnoredChannels.Remove(toRemove); // remove from local cache + } + added = false; + } + + await uow.SaveChangesAsync(); + } + return added; + } + + public (AntiSpamStats, AntiRaidStats, AntiAltStats) GetAntiStats(ulong guildId) + { + _antiRaidGuilds.TryGetValue(guildId, out var antiRaidStats); + _antiSpamGuilds.TryGetValue(guildId, out var antiSpamStats); + _antiAltGuilds.TryGetValue(guildId, out var antiAltStats); + + return (antiSpamStats, antiRaidStats, antiAltStats); + } + + public bool IsDurationAllowed(PunishmentAction action) + { + switch (action) + { + case PunishmentAction.Ban: + case PunishmentAction.Mute: + case PunishmentAction.ChatMute: + case PunishmentAction.VoiceMute: + case PunishmentAction.AddRole: + return true; + default: + return false; + } + } + + public async Task StartAntiAltAsync(ulong guildId, int minAgeMinutes, PunishmentAction action, + int actionDurationMinutes = 0, ulong? roleId = null) + { + using var uow = _db.GetDbContext(); + var gc = uow.GuildConfigsForId(guildId, set => set.Include(x => x.AntiAltSetting)); + gc.AntiAltSetting = new AntiAltSetting() + { + Action = action, + ActionDurationMinutes = actionDurationMinutes, + MinAge = TimeSpan.FromMinutes(minAgeMinutes), + RoleId = roleId, + }; + + await uow.SaveChangesAsync(); + _antiAltGuilds[guildId] = new AntiAltStats(gc.AntiAltSetting); + } + + public async Task TryStopAntiAlt(ulong guildId) + { + if (!_antiAltGuilds.TryRemove(guildId, out _)) + return false; + + using var uow = _db.GetDbContext(); + var gc = uow.GuildConfigsForId(guildId, set => set.Include(x => x.AntiAltSetting)); + gc.AntiAltSetting = null; + await uow.SaveChangesAsync(); + return true; + } +} \ No newline at end of file diff --git a/src/NadekoBot/Modules/Administration/Services/PruneService.cs b/src/NadekoBot/Modules/Administration/Services/PruneService.cs index ab895bd0f..4adcd9087 100644 --- a/src/NadekoBot/Modules/Administration/Services/PruneService.cs +++ b/src/NadekoBot/Modules/Administration/Services/PruneService.cs @@ -1,78 +1,74 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; +using System.Threading.Tasks; using Discord; using NadekoBot.Common.Collections; using NadekoBot.Extensions; using NadekoBot.Services; -namespace NadekoBot.Modules.Administration.Services +namespace NadekoBot.Modules.Administration.Services; + +public class PruneService : INService { - public class PruneService : INService + //channelids where prunes are currently occuring + private ConcurrentHashSet _pruningGuilds = new ConcurrentHashSet(); + private readonly TimeSpan twoWeeks = TimeSpan.FromDays(14); + private readonly ILogCommandService _logService; + + public PruneService(ILogCommandService logService) { - //channelids where prunes are currently occuring - private ConcurrentHashSet _pruningGuilds = new ConcurrentHashSet(); - private readonly TimeSpan twoWeeks = TimeSpan.FromDays(14); - private readonly ILogCommandService _logService; + this._logService = logService; + } - public PruneService(ILogCommandService logService) + public async Task PruneWhere(ITextChannel channel, int amount, Func predicate) + { + channel.ThrowIfNull(nameof(channel)); + if (amount <= 0) + throw new ArgumentOutOfRangeException(nameof(amount)); + + if (!_pruningGuilds.Add(channel.GuildId)) + return; + + try { - this._logService = logService; - } - - public async Task PruneWhere(ITextChannel channel, int amount, Func predicate) - { - channel.ThrowIfNull(nameof(channel)); - if (amount <= 0) - throw new ArgumentOutOfRangeException(nameof(amount)); - - if (!_pruningGuilds.Add(channel.GuildId)) - return; - - try + IMessage[] msgs; + IMessage lastMessage = null; + msgs = (await channel.GetMessagesAsync(50).FlattenAsync().ConfigureAwait(false)).Where(predicate).Take(amount).ToArray(); + while (amount > 0 && msgs.Any()) { - IMessage[] msgs; - IMessage lastMessage = null; - msgs = (await channel.GetMessagesAsync(50).FlattenAsync().ConfigureAwait(false)).Where(predicate).Take(amount).ToArray(); - while (amount > 0 && msgs.Any()) + lastMessage = msgs[msgs.Length - 1]; + + var bulkDeletable = new List(); + var singleDeletable = new List(); + foreach (var x in msgs) { - lastMessage = msgs[msgs.Length - 1]; + _logService.AddDeleteIgnore(x.Id); - var bulkDeletable = new List(); - var singleDeletable = new List(); - foreach (var x in msgs) - { - _logService.AddDeleteIgnore(x.Id); - - if (DateTime.UtcNow - x.CreatedAt < twoWeeks) - bulkDeletable.Add(x); - else - singleDeletable.Add(x); - } - - if (bulkDeletable.Count > 0) - await Task.WhenAll(Task.Delay(1000), channel.DeleteMessagesAsync(bulkDeletable)).ConfigureAwait(false); - - var i = 0; - foreach (var group in singleDeletable.GroupBy(x => ++i / (singleDeletable.Count / 5))) - await Task.WhenAll(Task.Delay(1000), Task.WhenAll(group.Select(x => x.DeleteAsync()))).ConfigureAwait(false); - - //this isn't good, because this still work as if i want to remove only specific user's messages from the last - //100 messages, Maybe this needs to be reduced by msgs.Length instead of 100 - amount -= 50; - if(amount > 0) - msgs = (await channel.GetMessagesAsync(lastMessage, Direction.Before, 50).FlattenAsync().ConfigureAwait(false)).Where(predicate).Take(amount).ToArray(); + if (DateTime.UtcNow - x.CreatedAt < twoWeeks) + bulkDeletable.Add(x); + else + singleDeletable.Add(x); } + + if (bulkDeletable.Count > 0) + await Task.WhenAll(Task.Delay(1000), channel.DeleteMessagesAsync(bulkDeletable)).ConfigureAwait(false); + + var i = 0; + foreach (var group in singleDeletable.GroupBy(x => ++i / (singleDeletable.Count / 5))) + await Task.WhenAll(Task.Delay(1000), Task.WhenAll(group.Select(x => x.DeleteAsync()))).ConfigureAwait(false); + + //this isn't good, because this still work as if i want to remove only specific user's messages from the last + //100 messages, Maybe this needs to be reduced by msgs.Length instead of 100 + amount -= 50; + if(amount > 0) + msgs = (await channel.GetMessagesAsync(lastMessage, Direction.Before, 50).FlattenAsync().ConfigureAwait(false)).Where(predicate).Take(amount).ToArray(); } - catch - { - //ignore - } - finally - { - _pruningGuilds.TryRemove(channel.GuildId); - } + } + catch + { + //ignore + } + finally + { + _pruningGuilds.TryRemove(channel.GuildId); } } -} +} \ No newline at end of file diff --git a/src/NadekoBot/Modules/Administration/Services/RoleCommandsService.cs b/src/NadekoBot/Modules/Administration/Services/RoleCommandsService.cs index baee2f084..45e3cd8cd 100644 --- a/src/NadekoBot/Modules/Administration/Services/RoleCommandsService.cs +++ b/src/NadekoBot/Modules/Administration/Services/RoleCommandsService.cs @@ -6,52 +6,114 @@ using NadekoBot.Services; using NadekoBot.Services.Database.Models; using NadekoBot.Extensions; using System.Collections.Concurrent; -using System.Linq; using System.Threading.Tasks; using LinqToDB; using LinqToDB.EntityFrameworkCore; using NadekoBot.Db; -using Serilog; using System.Threading; -using System; -namespace NadekoBot.Modules.Administration.Services +namespace NadekoBot.Modules.Administration.Services; + +public class RoleCommandsService : INService { - public class RoleCommandsService : INService + private readonly DbService _db; + private readonly DiscordSocketClient _client; + private readonly ConcurrentDictionary> _models; + + /// + /// Contains the (Message ID, User ID) of reaction roles that are currently being processed. + /// + private readonly ConcurrentHashSet<(ulong, ulong)> _reacting = new(); + + public RoleCommandsService(DiscordSocketClient client, DbService db, + Bot bot) { - private readonly DbService _db; - private readonly DiscordSocketClient _client; - private readonly ConcurrentDictionary> _models; - - /// - /// Contains the (Message ID, User ID) of reaction roles that are currently being processed. - /// - private readonly ConcurrentHashSet<(ulong, ulong)> _reacting = new(); - - public RoleCommandsService(DiscordSocketClient client, DbService db, - Bot bot) - { - _db = db; - _client = client; + _db = db; + _client = client; #if !GLOBAL_NADEKO - _models = bot.AllGuildConfigs.ToDictionary(x => x.GuildId, + _models = bot.AllGuildConfigs.ToDictionary(x => x.GuildId, x => x.ReactionRoleMessages) - .ToConcurrent(); + .ToConcurrent(); - _client.ReactionAdded += _client_ReactionAdded; - _client.ReactionRemoved += _client_ReactionRemoved; + _client.ReactionAdded += _client_ReactionAdded; + _client.ReactionRemoved += _client_ReactionRemoved; #endif - } + } - private Task _client_ReactionAdded(Cacheable msg, ISocketMessageChannel chan, SocketReaction reaction) + private Task _client_ReactionAdded(Cacheable msg, ISocketMessageChannel chan, SocketReaction reaction) + { + _ = Task.Run(async () => { - _ = Task.Run(async () => + if (!reaction.User.IsSpecified || + reaction.User.Value.IsBot || + reaction.User.Value is not SocketGuildUser gusr || + chan is not SocketGuildChannel gch || + !_models.TryGetValue(gch.Guild.Id, out var confs)) + return; + + var conf = confs.FirstOrDefault(x => x.MessageId == msg.Id); + + if (conf is null) + return; + + // compare emote names for backwards compatibility :facepalm: + var reactionRole = conf.ReactionRoles.FirstOrDefault(x => x.EmoteName == reaction.Emote.Name || x.EmoteName == reaction.Emote.ToString()); + + if (reactionRole != null) + { + if (!conf.Exclusive) + { + await AddReactionRoleAsync(gusr, reactionRole); + return; + } + + // If same (message, user) are being processed in an exclusive rero, quit + if (!_reacting.Add((msg.Id, reaction.UserId))) + return; + + try + { + var removeExclusiveTask = RemoveExclusiveReactionRoleAsync(msg, gusr, reaction, conf, reactionRole, CancellationToken.None); + var addRoleTask = AddReactionRoleAsync(gusr, reactionRole); + + await Task.WhenAll(removeExclusiveTask, addRoleTask).ConfigureAwait(false); + } + finally + { + // Free (message/user) for another exclusive rero + _reacting.TryRemove((msg.Id, reaction.UserId)); + } + } + else + { + var dl = await msg.GetOrDownloadAsync().ConfigureAwait(false); + await dl.RemoveReactionAsync(reaction.Emote, dl.Author, + new RequestOptions() + { + RetryMode = RetryMode.RetryRatelimit | RetryMode.Retry502 + }).ConfigureAwait(false); + Log.Warning("User {0} is adding unrelated reactions to the reaction roles message.", dl.Author); + } + }); + + return Task.CompletedTask; + } + + private Task _client_ReactionRemoved(Cacheable msg, ISocketMessageChannel chan, SocketReaction reaction) + { + _ = Task.Run(async () => + { + try { if (!reaction.User.IsSpecified || reaction.User.Value.IsBot || - reaction.User.Value is not SocketGuildUser gusr || - chan is not SocketGuildChannel gch || - !_models.TryGetValue(gch.Guild.Id, out var confs)) + reaction.User.Value is not SocketGuildUser gusr) + return; + + if (chan is not SocketGuildChannel gch) + return; + + if (!_models.TryGetValue(gch.Guild.Id, out var confs)) return; var conf = confs.FirstOrDefault(x => x.MessageId == msg.Id); @@ -59,195 +121,129 @@ namespace NadekoBot.Modules.Administration.Services if (conf is null) return; - // compare emote names for backwards compatibility :facepalm: var reactionRole = conf.ReactionRoles.FirstOrDefault(x => x.EmoteName == reaction.Emote.Name || x.EmoteName == reaction.Emote.ToString()); if (reactionRole != null) { - if (!conf.Exclusive) - { - await AddReactionRoleAsync(gusr, reactionRole); + var role = gusr.Guild.GetRole(reactionRole.RoleId); + if (role is null) return; - } - - // If same (message, user) are being processed in an exclusive rero, quit - if (!_reacting.Add((msg.Id, reaction.UserId))) - return; - - try - { - var removeExclusiveTask = RemoveExclusiveReactionRoleAsync(msg, gusr, reaction, conf, reactionRole, CancellationToken.None); - var addRoleTask = AddReactionRoleAsync(gusr, reactionRole); - - await Task.WhenAll(removeExclusiveTask, addRoleTask).ConfigureAwait(false); - } - finally - { - // Free (message/user) for another exclusive rero - _reacting.TryRemove((msg.Id, reaction.UserId)); - } + await gusr.RemoveRoleAsync(role).ConfigureAwait(false); } - else - { - var dl = await msg.GetOrDownloadAsync().ConfigureAwait(false); - await dl.RemoveReactionAsync(reaction.Emote, dl.Author, - new RequestOptions() - { - RetryMode = RetryMode.RetryRatelimit | RetryMode.Retry502 - }).ConfigureAwait(false); - Log.Warning("User {0} is adding unrelated reactions to the reaction roles message.", dl.Author); - } - }); + } + catch { } + }); - return Task.CompletedTask; - } + return Task.CompletedTask; + } - private Task _client_ReactionRemoved(Cacheable msg, ISocketMessageChannel chan, SocketReaction reaction) + public bool Get(ulong id, out IndexedCollection rrs) + { + return _models.TryGetValue(id, out rrs); + } + + public bool Add(ulong id, ReactionRoleMessage rrm) + { + using var uow = _db.GetDbContext(); + var table = uow.GetTable(); + table.Delete(x => x.MessageId == rrm.MessageId); + + var gc = uow.GuildConfigsForId(id, set => set + .Include(x => x.ReactionRoleMessages) + .ThenInclude(x => x.ReactionRoles)); + + if (gc.ReactionRoleMessages.Count >= 10) + return false; + + gc.ReactionRoleMessages.Add(rrm); + uow.SaveChanges(); + + _models.AddOrUpdate(id, + gc.ReactionRoleMessages, + delegate { return gc.ReactionRoleMessages; }); + return true; + } + + public void Remove(ulong id, int index) + { + using (var uow = _db.GetDbContext()) { - _ = Task.Run(async () => - { - try - { - if (!reaction.User.IsSpecified || - reaction.User.Value.IsBot || - reaction.User.Value is not SocketGuildUser gusr) - return; - - if (chan is not SocketGuildChannel gch) - return; - - if (!_models.TryGetValue(gch.Guild.Id, out var confs)) - return; - - var conf = confs.FirstOrDefault(x => x.MessageId == msg.Id); - - if (conf is null) - return; - - var reactionRole = conf.ReactionRoles.FirstOrDefault(x => x.EmoteName == reaction.Emote.Name || x.EmoteName == reaction.Emote.ToString()); - - if (reactionRole != null) - { - var role = gusr.Guild.GetRole(reactionRole.RoleId); - if (role is null) - return; - await gusr.RemoveRoleAsync(role).ConfigureAwait(false); - } - } - catch { } - }); - - return Task.CompletedTask; - } - - public bool Get(ulong id, out IndexedCollection rrs) - { - return _models.TryGetValue(id, out rrs); - } - - public bool Add(ulong id, ReactionRoleMessage rrm) - { - using var uow = _db.GetDbContext(); - var table = uow.GetTable(); - table.Delete(x => x.MessageId == rrm.MessageId); - - var gc = uow.GuildConfigsForId(id, set => set - .Include(x => x.ReactionRoleMessages) - .ThenInclude(x => x.ReactionRoles)); - - if (gc.ReactionRoleMessages.Count >= 10) - return false; - - gc.ReactionRoleMessages.Add(rrm); - uow.SaveChanges(); - + var gc = uow.GuildConfigsForId(id, + set => set.Include(x => x.ReactionRoleMessages) + .ThenInclude(x => x.ReactionRoles)); + uow.Set() + .RemoveRange(gc.ReactionRoleMessages[index].ReactionRoles); + gc.ReactionRoleMessages.RemoveAt(index); _models.AddOrUpdate(id, gc.ReactionRoleMessages, delegate { return gc.ReactionRoleMessages; }); - return true; - } - - public void Remove(ulong id, int index) - { - using (var uow = _db.GetDbContext()) - { - var gc = uow.GuildConfigsForId(id, - set => set.Include(x => x.ReactionRoleMessages) - .ThenInclude(x => x.ReactionRoles)); - uow.Set() - .RemoveRange(gc.ReactionRoleMessages[index].ReactionRoles); - gc.ReactionRoleMessages.RemoveAt(index); - _models.AddOrUpdate(id, - gc.ReactionRoleMessages, - delegate { return gc.ReactionRoleMessages; }); - uow.SaveChanges(); - } - } - - /// - /// Adds a reaction role to the specified user. - /// - /// A Discord guild user. - /// The database settings of this reaction role. - private Task AddReactionRoleAsync(SocketGuildUser user, ReactionRole dbRero) - { - var toAdd = user.Guild.GetRole(dbRero.RoleId); - - return (toAdd != null && !user.Roles.Contains(toAdd)) - ? user.AddRoleAsync(toAdd) - : Task.CompletedTask; - } - - /// - /// Removes the exclusive reaction roles and reactions from the specified user. - /// - /// The Discord message that contains the reaction roles. - /// A Discord guild user. - /// The Discord reaction of the user. - /// The database entry of the reaction role message. - /// The database settings of this reaction role. - /// A cancellation token to cancel the operation. - /// Occurs when the operation is cancelled before it began. - /// Occurs when the operation is cancelled while it's still executing. - private Task RemoveExclusiveReactionRoleAsync(Cacheable reactionMessage, SocketGuildUser user, SocketReaction reaction, ReactionRoleMessage dbReroMsg, ReactionRole dbRero, CancellationToken cToken = default) - { - cToken.ThrowIfCancellationRequested(); - - var roleIds = dbReroMsg.ReactionRoles.Select(x => x.RoleId) - .Where(x => x != dbRero.RoleId) - .Select(x => user.Guild.GetRole(x)) - .Where(x => x != null); - - var removeReactionsTask = RemoveOldReactionsAsync(reactionMessage, user, reaction, cToken); - - var removeRolesTask = user.RemoveRolesAsync(roleIds); - - return Task.WhenAll(removeReactionsTask, removeRolesTask); - } - - /// - /// Removes old reactions from an exclusive reaction role. - /// - /// The Discord message that contains the reaction roles. - /// A Discord guild user. - /// The Discord reaction of the user. - /// A cancellation token to cancel the operation. - /// Occurs when the operation is cancelled before it began. - /// Occurs when the operation is cancelled while it's still executing. - private async Task RemoveOldReactionsAsync(Cacheable reactionMessage, SocketGuildUser user, SocketReaction reaction, CancellationToken cToken = default) - { - cToken.ThrowIfCancellationRequested(); - - //if the role is exclusive, - // remove all other reactions user added to the message - var dl = await reactionMessage.GetOrDownloadAsync().ConfigureAwait(false); - foreach (var r in dl.Reactions) - { - if (r.Key.Name == reaction.Emote.Name) - continue; - try { await dl.RemoveReactionAsync(r.Key, user).ConfigureAwait(false); } catch { } - await Task.Delay(100, cToken).ConfigureAwait(false); - } + uow.SaveChanges(); } } -} + + /// + /// Adds a reaction role to the specified user. + /// + /// A Discord guild user. + /// The database settings of this reaction role. + private Task AddReactionRoleAsync(SocketGuildUser user, ReactionRole dbRero) + { + var toAdd = user.Guild.GetRole(dbRero.RoleId); + + return (toAdd != null && !user.Roles.Contains(toAdd)) + ? user.AddRoleAsync(toAdd) + : Task.CompletedTask; + } + + /// + /// Removes the exclusive reaction roles and reactions from the specified user. + /// + /// The Discord message that contains the reaction roles. + /// A Discord guild user. + /// The Discord reaction of the user. + /// The database entry of the reaction role message. + /// The database settings of this reaction role. + /// A cancellation token to cancel the operation. + /// Occurs when the operation is cancelled before it began. + /// Occurs when the operation is cancelled while it's still executing. + private Task RemoveExclusiveReactionRoleAsync(Cacheable reactionMessage, SocketGuildUser user, SocketReaction reaction, ReactionRoleMessage dbReroMsg, ReactionRole dbRero, CancellationToken cToken = default) + { + cToken.ThrowIfCancellationRequested(); + + var roleIds = dbReroMsg.ReactionRoles.Select(x => x.RoleId) + .Where(x => x != dbRero.RoleId) + .Select(x => user.Guild.GetRole(x)) + .Where(x => x != null); + + var removeReactionsTask = RemoveOldReactionsAsync(reactionMessage, user, reaction, cToken); + + var removeRolesTask = user.RemoveRolesAsync(roleIds); + + return Task.WhenAll(removeReactionsTask, removeRolesTask); + } + + /// + /// Removes old reactions from an exclusive reaction role. + /// + /// The Discord message that contains the reaction roles. + /// A Discord guild user. + /// The Discord reaction of the user. + /// A cancellation token to cancel the operation. + /// Occurs when the operation is cancelled before it began. + /// Occurs when the operation is cancelled while it's still executing. + private async Task RemoveOldReactionsAsync(Cacheable reactionMessage, SocketGuildUser user, SocketReaction reaction, CancellationToken cToken = default) + { + cToken.ThrowIfCancellationRequested(); + + //if the role is exclusive, + // remove all other reactions user added to the message + var dl = await reactionMessage.GetOrDownloadAsync().ConfigureAwait(false); + foreach (var r in dl.Reactions) + { + if (r.Key.Name == reaction.Emote.Name) + continue; + try { await dl.RemoveReactionAsync(r.Key, user).ConfigureAwait(false); } catch { } + await Task.Delay(100, cToken).ConfigureAwait(false); + } + } +} \ No newline at end of file diff --git a/src/NadekoBot/Modules/Administration/Services/SelfAssignedRolesService.cs b/src/NadekoBot/Modules/Administration/Services/SelfAssignedRolesService.cs index 8f6819e84..ffeae378f 100644 --- a/src/NadekoBot/Modules/Administration/Services/SelfAssignedRolesService.cs +++ b/src/NadekoBot/Modules/Administration/Services/SelfAssignedRolesService.cs @@ -1,271 +1,267 @@ using Discord; using NadekoBot.Services; using NadekoBot.Services.Database.Models; -using System; -using System.Collections.Generic; -using System.Linq; using System.Threading.Tasks; using Microsoft.EntityFrameworkCore; using NadekoBot.Db; using NadekoBot.Modules.Xp; -namespace NadekoBot.Modules.Administration.Services +namespace NadekoBot.Modules.Administration.Services; + +public class SelfAssignedRolesService : INService { - public class SelfAssignedRolesService : INService + private readonly DbService _db; + + public enum RemoveResult { - private readonly DbService _db; + Removed, // successfully removed + Err_Not_Assignable, // not assignable (error) + Err_Not_Have, // you don't have a role you want to remove (error) + Err_Not_Perms, // bot doesn't have perms (error) + } - public enum RemoveResult - { - Removed, // successfully removed - Err_Not_Assignable, // not assignable (error) - Err_Not_Have, // you don't have a role you want to remove (error) - Err_Not_Perms, // bot doesn't have perms (error) - } + public enum AssignResult + { + Assigned, // successfully removed + Err_Not_Assignable, // not assignable (error) + Err_Already_Have, // you already have that role (error) + Err_Not_Perms, // bot doesn't have perms (error) + Err_Lvl_Req, // you are not required level (error) + } - public enum AssignResult - { - Assigned, // successfully removed - Err_Not_Assignable, // not assignable (error) - Err_Already_Have, // you already have that role (error) - Err_Not_Perms, // bot doesn't have perms (error) - Err_Lvl_Req, // you are not required level (error) - } + public SelfAssignedRolesService(DbService db) + { + _db = db; + } - public SelfAssignedRolesService(DbService db) + public bool AddNew(ulong guildId, IRole role, int group) + { + using (var uow = _db.GetDbContext()) { - _db = db; - } - - public bool AddNew(ulong guildId, IRole role, int group) - { - using (var uow = _db.GetDbContext()) + var roles = uow.SelfAssignableRoles.GetFromGuild(guildId); + if (roles.Any(s => s.RoleId == role.Id && s.GuildId == role.Guild.Id)) { - var roles = uow.SelfAssignableRoles.GetFromGuild(guildId); - if (roles.Any(s => s.RoleId == role.Id && s.GuildId == role.Guild.Id)) + return false; + } + + uow.SelfAssignableRoles.Add(new SelfAssignedRole + { + Group = group, + RoleId = role.Id, + GuildId = role.Guild.Id + }); + uow.SaveChanges(); + } + return true; + } + + public bool ToggleAdSarm(ulong guildId) + { + bool newval; + using (var uow = _db.GetDbContext()) + { + var config = uow.GuildConfigsForId(guildId, set => set); + newval = config.AutoDeleteSelfAssignedRoleMessages = !config.AutoDeleteSelfAssignedRoleMessages; + uow.SaveChanges(); + } + return newval; + } + + public async Task<(AssignResult Result, bool AutoDelete, object extra)> Assign(IGuildUser guildUser, IRole role) + { + LevelStats userLevelData; + using (var uow = _db.GetDbContext()) + { + var stats = uow.GetOrCreateUserXpStats(guildUser.Guild.Id, guildUser.Id); + userLevelData = new LevelStats(stats.Xp + stats.AwardedXp); + } + + var (autoDelete, exclusive, roles) = GetAdAndRoles(guildUser.Guild.Id); + + var theRoleYouWant = roles.FirstOrDefault(r => r.RoleId == role.Id); + if (theRoleYouWant is null) + { + return (AssignResult.Err_Not_Assignable, autoDelete, null); + } + else if (theRoleYouWant.LevelRequirement > userLevelData.Level) + { + return (AssignResult.Err_Lvl_Req, autoDelete, theRoleYouWant.LevelRequirement); + } + else if (guildUser.RoleIds.Contains(role.Id)) + { + return (AssignResult.Err_Already_Have, autoDelete, null); + } + + var roleIds = roles + .Where(x => x.Group == theRoleYouWant.Group) + .Select(x => x.RoleId).ToArray(); + if (exclusive) + { + var sameRoles = guildUser.RoleIds + .Where(r => roleIds.Contains(r)); + + foreach (var roleId in sameRoles) + { + var sameRole = guildUser.Guild.GetRole(roleId); + if (sameRole != null) { - return false; - } - - uow.SelfAssignableRoles.Add(new SelfAssignedRole - { - Group = group, - RoleId = role.Id, - GuildId = role.Guild.Id - }); - uow.SaveChanges(); - } - return true; - } - - public bool ToggleAdSarm(ulong guildId) - { - bool newval; - using (var uow = _db.GetDbContext()) - { - var config = uow.GuildConfigsForId(guildId, set => set); - newval = config.AutoDeleteSelfAssignedRoleMessages = !config.AutoDeleteSelfAssignedRoleMessages; - uow.SaveChanges(); - } - return newval; - } - - public async Task<(AssignResult Result, bool AutoDelete, object extra)> Assign(IGuildUser guildUser, IRole role) - { - LevelStats userLevelData; - using (var uow = _db.GetDbContext()) - { - var stats = uow.GetOrCreateUserXpStats(guildUser.Guild.Id, guildUser.Id); - userLevelData = new LevelStats(stats.Xp + stats.AwardedXp); - } - - var (autoDelete, exclusive, roles) = GetAdAndRoles(guildUser.Guild.Id); - - var theRoleYouWant = roles.FirstOrDefault(r => r.RoleId == role.Id); - if (theRoleYouWant is null) - { - return (AssignResult.Err_Not_Assignable, autoDelete, null); - } - else if (theRoleYouWant.LevelRequirement > userLevelData.Level) - { - return (AssignResult.Err_Lvl_Req, autoDelete, theRoleYouWant.LevelRequirement); - } - else if (guildUser.RoleIds.Contains(role.Id)) - { - return (AssignResult.Err_Already_Have, autoDelete, null); - } - - var roleIds = roles - .Where(x => x.Group == theRoleYouWant.Group) - .Select(x => x.RoleId).ToArray(); - if (exclusive) - { - var sameRoles = guildUser.RoleIds - .Where(r => roleIds.Contains(r)); - - foreach (var roleId in sameRoles) - { - var sameRole = guildUser.Guild.GetRole(roleId); - if (sameRole != null) + try { - try - { - await guildUser.RemoveRoleAsync(sameRole).ConfigureAwait(false); - await Task.Delay(300).ConfigureAwait(false); - } - catch - { - // ignored - } + await guildUser.RemoveRoleAsync(sameRole).ConfigureAwait(false); + await Task.Delay(300).ConfigureAwait(false); + } + catch + { + // ignored } } } - try - { - await guildUser.AddRoleAsync(role).ConfigureAwait(false); - } - catch (Exception ex) - { - return (AssignResult.Err_Not_Perms, autoDelete, ex); - } - - return (AssignResult.Assigned, autoDelete, null); + } + try + { + await guildUser.AddRoleAsync(role).ConfigureAwait(false); + } + catch (Exception ex) + { + return (AssignResult.Err_Not_Perms, autoDelete, ex); } - public async Task SetNameAsync(ulong guildId, int group, string name) - { - bool set = false; - using (var uow = _db.GetDbContext()) - { - var gc = uow.GuildConfigsForId(guildId, y => y.Include(x => x.SelfAssignableRoleGroupNames)); - var toUpdate = gc.SelfAssignableRoleGroupNames.FirstOrDefault(x => x.Number == group); + return (AssignResult.Assigned, autoDelete, null); + } - if (string.IsNullOrWhiteSpace(name)) + public async Task SetNameAsync(ulong guildId, int group, string name) + { + bool set = false; + using (var uow = _db.GetDbContext()) + { + var gc = uow.GuildConfigsForId(guildId, y => y.Include(x => x.SelfAssignableRoleGroupNames)); + var toUpdate = gc.SelfAssignableRoleGroupNames.FirstOrDefault(x => x.Number == group); + + if (string.IsNullOrWhiteSpace(name)) + { + if (toUpdate != null) + gc.SelfAssignableRoleGroupNames.Remove(toUpdate); + } + else if (toUpdate is null) + { + gc.SelfAssignableRoleGroupNames.Add(new GroupName { - if (toUpdate != null) - gc.SelfAssignableRoleGroupNames.Remove(toUpdate); - } - else if (toUpdate is null) - { - gc.SelfAssignableRoleGroupNames.Add(new GroupName - { - Name = name, - Number = group, - }); - set = true; - } - else - { - toUpdate.Name = name; - set = true; - } - - await uow.SaveChangesAsync(); + Name = name, + Number = group, + }); + set = true; + } + else + { + toUpdate.Name = name; + set = true; } - return set; + await uow.SaveChangesAsync(); } - public async Task<(RemoveResult Result, bool AutoDelete)> Remove(IGuildUser guildUser, IRole role) + return set; + } + + public async Task<(RemoveResult Result, bool AutoDelete)> Remove(IGuildUser guildUser, IRole role) + { + var (autoDelete, _, roles) = GetAdAndRoles(guildUser.Guild.Id); + + if (roles.FirstOrDefault(r => r.RoleId == role.Id) is null) { - var (autoDelete, _, roles) = GetAdAndRoles(guildUser.Guild.Id); - - if (roles.FirstOrDefault(r => r.RoleId == role.Id) is null) - { - return (RemoveResult.Err_Not_Assignable, autoDelete); - } - if (!guildUser.RoleIds.Contains(role.Id)) - { - return (RemoveResult.Err_Not_Have, autoDelete); - } - try - { - await guildUser.RemoveRoleAsync(role).ConfigureAwait(false); - } - catch (Exception) - { - return (RemoveResult.Err_Not_Perms, autoDelete); - } - - return (RemoveResult.Removed, autoDelete); + return (RemoveResult.Err_Not_Assignable, autoDelete); + } + if (!guildUser.RoleIds.Contains(role.Id)) + { + return (RemoveResult.Err_Not_Have, autoDelete); + } + try + { + await guildUser.RemoveRoleAsync(role).ConfigureAwait(false); + } + catch (Exception) + { + return (RemoveResult.Err_Not_Perms, autoDelete); } - public bool RemoveSar(ulong guildId, ulong roleId) + return (RemoveResult.Removed, autoDelete); + } + + public bool RemoveSar(ulong guildId, ulong roleId) + { + bool success; + using (var uow = _db.GetDbContext()) { - bool success; - using (var uow = _db.GetDbContext()) - { - success = uow.SelfAssignableRoles.DeleteByGuildAndRoleId(guildId, roleId); - uow.SaveChanges(); - } - return success; + success = uow.SelfAssignableRoles.DeleteByGuildAndRoleId(guildId, roleId); + uow.SaveChanges(); } + return success; + } - public (bool AutoDelete, bool Exclusive, IEnumerable) GetAdAndRoles(ulong guildId) + public (bool AutoDelete, bool Exclusive, IEnumerable) GetAdAndRoles(ulong guildId) + { + using (var uow = _db.GetDbContext()) { - using (var uow = _db.GetDbContext()) - { - var gc = uow.GuildConfigsForId(guildId, set => set); - var autoDelete = gc.AutoDeleteSelfAssignedRoleMessages; - var exclusive = gc.ExclusiveSelfAssignedRoles; - var roles = uow.SelfAssignableRoles.GetFromGuild(guildId); + var gc = uow.GuildConfigsForId(guildId, set => set); + var autoDelete = gc.AutoDeleteSelfAssignedRoleMessages; + var exclusive = gc.ExclusiveSelfAssignedRoles; + var roles = uow.SelfAssignableRoles.GetFromGuild(guildId); - return (autoDelete, exclusive, roles); - } - } - - public bool SetLevelReq(ulong guildId, IRole role, int level) - { - using (var uow = _db.GetDbContext()) - { - var roles = uow.SelfAssignableRoles.GetFromGuild(guildId); - var sar = roles.FirstOrDefault(x => x.RoleId == role.Id); - if (sar != null) - { - sar.LevelRequirement = level; - uow.SaveChanges(); - } - else - { - return false; - } - } - - return true; - } - - public bool ToggleEsar(ulong guildId) - { - bool areExclusive; - using (var uow = _db.GetDbContext()) - { - var config = uow.GuildConfigsForId(guildId, set => set); - - areExclusive = config.ExclusiveSelfAssignedRoles = !config.ExclusiveSelfAssignedRoles; - uow.SaveChanges(); - } - return areExclusive; - } - - public (bool Exclusive, IEnumerable<(SelfAssignedRole Model, IRole Role)> Roles, IDictionary GroupNames) GetRoles(IGuild guild) - { - var exclusive = false; - - IEnumerable<(SelfAssignedRole Model, IRole Role)> roles; - IDictionary groupNames; - using (var uow = _db.GetDbContext()) - { - var gc = uow.GuildConfigsForId(guild.Id, set => set.Include(x => x.SelfAssignableRoleGroupNames)); - exclusive = gc.ExclusiveSelfAssignedRoles; - groupNames = gc.SelfAssignableRoleGroupNames.ToDictionary(x => x.Number, x => x.Name); - var roleModels = uow.SelfAssignableRoles.GetFromGuild(guild.Id); - roles = roleModels - .Select(x => (Model: x, Role: guild.GetRole(x.RoleId))); - uow.SelfAssignableRoles.RemoveRange(roles.Where(x => x.Role is null).Select(x => x.Model).ToArray()); - uow.SaveChanges(); - } - - return (exclusive, roles.Where(x => x.Role != null), groupNames); + return (autoDelete, exclusive, roles); } } -} + + public bool SetLevelReq(ulong guildId, IRole role, int level) + { + using (var uow = _db.GetDbContext()) + { + var roles = uow.SelfAssignableRoles.GetFromGuild(guildId); + var sar = roles.FirstOrDefault(x => x.RoleId == role.Id); + if (sar != null) + { + sar.LevelRequirement = level; + uow.SaveChanges(); + } + else + { + return false; + } + } + + return true; + } + + public bool ToggleEsar(ulong guildId) + { + bool areExclusive; + using (var uow = _db.GetDbContext()) + { + var config = uow.GuildConfigsForId(guildId, set => set); + + areExclusive = config.ExclusiveSelfAssignedRoles = !config.ExclusiveSelfAssignedRoles; + uow.SaveChanges(); + } + return areExclusive; + } + + public (bool Exclusive, IEnumerable<(SelfAssignedRole Model, IRole Role)> Roles, IDictionary GroupNames) GetRoles(IGuild guild) + { + var exclusive = false; + + IEnumerable<(SelfAssignedRole Model, IRole Role)> roles; + IDictionary groupNames; + using (var uow = _db.GetDbContext()) + { + var gc = uow.GuildConfigsForId(guild.Id, set => set.Include(x => x.SelfAssignableRoleGroupNames)); + exclusive = gc.ExclusiveSelfAssignedRoles; + groupNames = gc.SelfAssignableRoleGroupNames.ToDictionary(x => x.Number, x => x.Name); + var roleModels = uow.SelfAssignableRoles.GetFromGuild(guild.Id); + roles = roleModels + .Select(x => (Model: x, Role: guild.GetRole(x.RoleId))); + uow.SelfAssignableRoles.RemoveRange(roles.Where(x => x.Role is null).Select(x => x.Model).ToArray()); + uow.SaveChanges(); + } + + return (exclusive, roles.Where(x => x.Role != null), groupNames); + } +} \ No newline at end of file diff --git a/src/NadekoBot/Modules/Administration/Services/SelfService.cs b/src/NadekoBot/Modules/Administration/Services/SelfService.cs index 6e1b07c06..a7a273494 100644 --- a/src/NadekoBot/Modules/Administration/Services/SelfService.cs +++ b/src/NadekoBot/Modules/Administration/Services/SelfService.cs @@ -1,420 +1,415 @@ using System.Collections.Immutable; -using System.Linq; using System.Threading.Tasks; using Discord; using Discord.WebSocket; using NadekoBot.Common.ModuleBehaviors; using NadekoBot.Extensions; using NadekoBot.Services; -using System.Collections.Generic; using Microsoft.EntityFrameworkCore; using NadekoBot.Services.Database.Models; using System.Threading; using System.Collections.Concurrent; -using System; using System.Net.Http; using NadekoBot.Common; -using Serilog; -namespace NadekoBot.Modules.Administration.Services +namespace NadekoBot.Modules.Administration.Services; + +public sealed class SelfService : ILateExecutor, IReadyExecutor, INService { - public sealed class SelfService : ILateExecutor, IReadyExecutor, INService + private readonly CommandHandler _cmdHandler; + private readonly DbService _db; + private readonly IBotStrings _strings; + private readonly DiscordSocketClient _client; + + private readonly IBotCredentials _creds; + + private ImmutableDictionary ownerChannels = + new Dictionary().ToImmutableDictionary(); + + private ConcurrentDictionary> _autoCommands = + new ConcurrentDictionary>(); + + private readonly IImageCache _imgs; + private readonly IHttpClientFactory _httpFactory; + private readonly BotConfigService _bss; + private readonly IPubSub _pubSub; + private readonly IEmbedBuilderService _eb; + + //keys + private readonly TypedKey _activitySetKey; + private readonly TypedKey _imagesReloadKey; + private readonly TypedKey _guildLeaveKey; + + public SelfService( + DiscordSocketClient client, + CommandHandler cmdHandler, + DbService db, + IBotStrings strings, + IBotCredentials creds, + IDataCache cache, + IHttpClientFactory factory, + BotConfigService bss, + IPubSub pubSub, + IEmbedBuilderService eb) { - private readonly CommandHandler _cmdHandler; - private readonly DbService _db; - private readonly IBotStrings _strings; - private readonly DiscordSocketClient _client; - - private readonly IBotCredentials _creds; - - private ImmutableDictionary ownerChannels = - new Dictionary().ToImmutableDictionary(); - - private ConcurrentDictionary> _autoCommands = - new ConcurrentDictionary>(); - - private readonly IImageCache _imgs; - private readonly IHttpClientFactory _httpFactory; - private readonly BotConfigService _bss; - private readonly IPubSub _pubSub; - private readonly IEmbedBuilderService _eb; - - //keys - private readonly TypedKey _activitySetKey; - private readonly TypedKey _imagesReloadKey; - private readonly TypedKey _guildLeaveKey; - - public SelfService( - DiscordSocketClient client, - CommandHandler cmdHandler, - DbService db, - IBotStrings strings, - IBotCredentials creds, - IDataCache cache, - IHttpClientFactory factory, - BotConfigService bss, - IPubSub pubSub, - IEmbedBuilderService eb) - { - _cmdHandler = cmdHandler; - _db = db; - _strings = strings; - _client = client; - _creds = creds; - _imgs = cache.LocalImages; - _httpFactory = factory; - _bss = bss; - _pubSub = pubSub; - _eb = eb; - _activitySetKey = new("activity.set"); - _imagesReloadKey = new("images.reload"); - _guildLeaveKey = new("guild.leave"); + _cmdHandler = cmdHandler; + _db = db; + _strings = strings; + _client = client; + _creds = creds; + _imgs = cache.LocalImages; + _httpFactory = factory; + _bss = bss; + _pubSub = pubSub; + _eb = eb; + _activitySetKey = new("activity.set"); + _imagesReloadKey = new("images.reload"); + _guildLeaveKey = new("guild.leave"); - HandleStatusChanges(); + HandleStatusChanges(); - if (_client.ShardId == 0) - { - _pubSub.Sub(_imagesReloadKey, async _ => await _imgs.Reload()); - } - - _pubSub.Sub(_guildLeaveKey, async input => - { - var guildStr = input.ToString().Trim().ToUpperInvariant(); - if (string.IsNullOrWhiteSpace(guildStr)) - return; - - var server = _client.Guilds.FirstOrDefault(g => g.Id.ToString() == guildStr - || g.Name.Trim().ToUpperInvariant() == guildStr); - if (server is null) - { - return; - } - - if (server.OwnerId != _client.CurrentUser.Id) - { - await server.LeaveAsync().ConfigureAwait(false); - Log.Information($"Left server {server.Name} [{server.Id}]"); - } - else - { - await server.DeleteAsync().ConfigureAwait(false); - Log.Information($"Deleted server {server.Name} [{server.Id}]"); - } - }); - } - - public async Task OnReadyAsync() + if (_client.ShardId == 0) { - using var uow = _db.GetDbContext(); - - _autoCommands = uow - .AutoCommands - .AsNoTracking() - .Where(x => x.Interval >= 5) - .AsEnumerable() - .GroupBy(x => x.GuildId) - .ToDictionary(x => x.Key, - y => y.ToDictionary(x => x.Id, TimerFromAutoCommand) - .ToConcurrent()) - .ToConcurrent(); - - var startupCommands = uow.AutoCommands.AsNoTracking().Where(x => x.Interval == 0); - foreach (var cmd in startupCommands) - { - try - { - await ExecuteCommand(cmd).ConfigureAwait(false); - } - catch - { - } - } - - if (_client.ShardId == 0) - { - await LoadOwnerChannels().ConfigureAwait(false); - } + _pubSub.Sub(_imagesReloadKey, async _ => await _imgs.Reload()); } - private Timer TimerFromAutoCommand(AutoCommand x) + _pubSub.Sub(_guildLeaveKey, async input => { - return new Timer(async (obj) => await ExecuteCommand((AutoCommand) obj).ConfigureAwait(false), - x, - x.Interval * 1000, - x.Interval * 1000); - } + var guildStr = input.ToString().Trim().ToUpperInvariant(); + if (string.IsNullOrWhiteSpace(guildStr)) + return; - private async Task ExecuteCommand(AutoCommand cmd) + var server = _client.Guilds.FirstOrDefault(g => g.Id.ToString() == guildStr + || g.Name.Trim().ToUpperInvariant() == guildStr); + if (server is null) + { + return; + } + + if (server.OwnerId != _client.CurrentUser.Id) + { + await server.LeaveAsync().ConfigureAwait(false); + Log.Information($"Left server {server.Name} [{server.Id}]"); + } + else + { + await server.DeleteAsync().ConfigureAwait(false); + Log.Information($"Deleted server {server.Name} [{server.Id}]"); + } + }); + } + + public async Task OnReadyAsync() + { + using var uow = _db.GetDbContext(); + + _autoCommands = uow + .AutoCommands + .AsNoTracking() + .Where(x => x.Interval >= 5) + .AsEnumerable() + .GroupBy(x => x.GuildId) + .ToDictionary(x => x.Key, + y => y.ToDictionary(x => x.Id, TimerFromAutoCommand) + .ToConcurrent()) + .ToConcurrent(); + + var startupCommands = uow.AutoCommands.AsNoTracking().Where(x => x.Interval == 0); + foreach (var cmd in startupCommands) { try { - if (cmd.GuildId is null) - return; + await ExecuteCommand(cmd).ConfigureAwait(false); + } + catch + { + } + } - var guildShard = (int) ((cmd.GuildId.Value >> 22) % (ulong) _creds.TotalShards); - if (guildShard != _client.ShardId) - return; - var prefix = _cmdHandler.GetPrefix(cmd.GuildId); - //if someone already has .die as their startup command, ignore it - if (cmd.CommandText.StartsWith(prefix + "die", StringComparison.InvariantCulture)) - return; - await _cmdHandler.ExecuteExternal(cmd.GuildId, cmd.ChannelId, cmd.CommandText).ConfigureAwait(false); + if (_client.ShardId == 0) + { + await LoadOwnerChannels().ConfigureAwait(false); + } + } + + private Timer TimerFromAutoCommand(AutoCommand x) + { + return new Timer(async (obj) => await ExecuteCommand((AutoCommand) obj).ConfigureAwait(false), + x, + x.Interval * 1000, + x.Interval * 1000); + } + + private async Task ExecuteCommand(AutoCommand cmd) + { + try + { + if (cmd.GuildId is null) + return; + + var guildShard = (int) ((cmd.GuildId.Value >> 22) % (ulong) _creds.TotalShards); + if (guildShard != _client.ShardId) + return; + var prefix = _cmdHandler.GetPrefix(cmd.GuildId); + //if someone already has .die as their startup command, ignore it + if (cmd.CommandText.StartsWith(prefix + "die", StringComparison.InvariantCulture)) + return; + await _cmdHandler.ExecuteExternal(cmd.GuildId, cmd.ChannelId, cmd.CommandText).ConfigureAwait(false); + } + catch (Exception ex) + { + Log.Warning(ex, "Error in SelfService ExecuteCommand"); + } + } + + public void AddNewAutoCommand(AutoCommand cmd) + { + using (var uow = _db.GetDbContext()) + { + uow.AutoCommands.Add(cmd); + uow.SaveChanges(); + } + + if (cmd.Interval >= 5) + { + var autos = _autoCommands.GetOrAdd(cmd.GuildId, new ConcurrentDictionary()); + autos.AddOrUpdate(cmd.Id, key => TimerFromAutoCommand(cmd), (key, old) => + { + old.Change(Timeout.Infinite, Timeout.Infinite); + return TimerFromAutoCommand(cmd); + }); + } + } + + public IEnumerable GetStartupCommands() + { + using var uow = _db.GetDbContext(); + return uow + .AutoCommands + .AsNoTracking() + .Where(x => x.Interval == 0) + .OrderBy(x => x.Id) + .ToList(); + } + + public IEnumerable GetAutoCommands() + { + using var uow = _db.GetDbContext(); + return uow + .AutoCommands + .AsNoTracking() + .Where(x => x.Interval >= 5) + .OrderBy(x => x.Id) + .ToList(); + } + + private async Task LoadOwnerChannels() + { + var channels = await Task.WhenAll(_creds.OwnerIds.Select(id => + { + var user = _client.GetUser(id); + if (user is null) + return Task.FromResult(null); + + return user.GetOrCreateDMChannelAsync(); + })).ConfigureAwait(false); + + ownerChannels = channels.Where(x => x != null) + .ToDictionary(x => x.Recipient.Id, x => x) + .ToImmutableDictionary(); + + if (!ownerChannels.Any()) + Log.Warning( + "No owner channels created! Make sure you've specified the correct OwnerId in the creds.yml file and invited the bot to a Discord server."); + else + Log.Information($"Created {ownerChannels.Count} out of {_creds.OwnerIds.Count} owner message channels."); + } + + public Task LeaveGuild(string guildStr) + => _pubSub.Pub(_guildLeaveKey, guildStr); + + // forwards dms + public async Task LateExecute(IGuild guild, IUserMessage msg) + { + var bs = _bss.Data; + if (msg.Channel is IDMChannel && bs.ForwardMessages && ownerChannels.Any()) + { + var title = _strings.GetText(strs.dm_from) + + $" [{msg.Author}]({msg.Author.Id})"; + + var attachamentsTxt = _strings.GetText(strs.attachments); + + var toSend = msg.Content; + + if (msg.Attachments.Count > 0) + { + toSend += $"\n\n{Format.Code(attachamentsTxt)}:\n" + + string.Join("\n", msg.Attachments.Select(a => a.ProxyUrl)); + } + + if (bs.ForwardToAllOwners) + { + var allOwnerChannels = ownerChannels.Values; + + foreach (var ownerCh in allOwnerChannels.Where(ch => ch.Recipient.Id != msg.Author.Id)) + { + try + { + await ownerCh.SendConfirmAsync(_eb, title, toSend).ConfigureAwait(false); + } + catch + { + Log.Warning("Can't contact owner with id {0}", ownerCh.Recipient.Id); + } + } + } + else + { + var firstOwnerChannel = ownerChannels.Values.First(); + if (firstOwnerChannel.Recipient.Id != msg.Author.Id) + { + try + { + await firstOwnerChannel.SendConfirmAsync(_eb, title, toSend).ConfigureAwait(false); + } + catch + { + // ignored + } + } + } + } + } + + public bool RemoveStartupCommand(int index, out AutoCommand cmd) + { + using (var uow = _db.GetDbContext()) + { + cmd = uow.AutoCommands + .AsNoTracking() + .Where(x => x.Interval == 0) + .Skip(index) + .FirstOrDefault(); + + if (cmd != null) + { + uow.Remove(cmd); + uow.SaveChanges(); + return true; + } + } + + return false; + } + + public bool RemoveAutoCommand(int index, out AutoCommand cmd) + { + using (var uow = _db.GetDbContext()) + { + cmd = uow.AutoCommands + .AsNoTracking() + .Where(x => x.Interval >= 5) + .Skip(index) + .FirstOrDefault(); + + if (cmd != null) + { + uow.Remove(cmd); + if (_autoCommands.TryGetValue(cmd.GuildId, out var autos)) + if (autos.TryRemove(cmd.Id, out var timer)) + timer.Change(Timeout.Infinite, Timeout.Infinite); + uow.SaveChanges(); + return true; + } + } + + return false; + } + + public async Task SetAvatar(string img) + { + if (string.IsNullOrWhiteSpace(img)) + return false; + + if (!Uri.IsWellFormedUriString(img, UriKind.Absolute)) + return false; + + var uri = new Uri(img); + + using (var http = _httpFactory.CreateClient()) + using (var sr = await http.GetAsync(uri, HttpCompletionOption.ResponseHeadersRead).ConfigureAwait(false)) + { + if (!sr.IsImage()) + return false; + + // i can't just do ReadAsStreamAsync because dicord.net's image poops itself + var imgData = await sr.Content.ReadAsByteArrayAsync().ConfigureAwait(false); + using (var imgStream = imgData.ToStream()) + { + await _client.CurrentUser.ModifyAsync(u => u.Avatar = new Image(imgStream)).ConfigureAwait(false); + } + } + + return true; + } + + public void ClearStartupCommands() + { + using (var uow = _db.GetDbContext()) + { + var toRemove = uow + .AutoCommands + .AsNoTracking() + .Where(x => x.Interval == 0); + + uow.AutoCommands.RemoveRange(toRemove); + uow.SaveChanges(); + } + } + + public Task ReloadImagesAsync() + => _pubSub.Pub(_imagesReloadKey, true); + + public bool ForwardMessages() + { + var isForwarding = false; + _bss.ModifyConfig(config => { isForwarding = config.ForwardMessages = !config.ForwardMessages; }); + + return isForwarding; + } + + public bool ForwardToAll() + { + var isToAll = false; + _bss.ModifyConfig(config => { isToAll = config.ForwardToAllOwners = !config.ForwardToAllOwners; }); + return isToAll; + } + + private void HandleStatusChanges() + { + _pubSub.Sub(_activitySetKey, async data => + { + try + { + await _client.SetGameAsync(data.Name, data.Link, type: data.Type); } catch (Exception ex) { - Log.Warning(ex, "Error in SelfService ExecuteCommand"); + Log.Warning(ex, "Error setting activity"); } - } + }); + } - public void AddNewAutoCommand(AutoCommand cmd) - { - using (var uow = _db.GetDbContext()) - { - uow.AutoCommands.Add(cmd); - uow.SaveChanges(); - } + public Task SetGameAsync(string game, ActivityType type) + => _pubSub.Pub(_activitySetKey, new() {Name = game, Link = null, Type = type}); - if (cmd.Interval >= 5) - { - var autos = _autoCommands.GetOrAdd(cmd.GuildId, new ConcurrentDictionary()); - autos.AddOrUpdate(cmd.Id, key => TimerFromAutoCommand(cmd), (key, old) => - { - old.Change(Timeout.Infinite, Timeout.Infinite); - return TimerFromAutoCommand(cmd); - }); - } - } + public Task SetStreamAsync(string name, string link) + => _pubSub.Pub(_activitySetKey, new() { Name = name, Link = link, Type = ActivityType.Streaming }); - public IEnumerable GetStartupCommands() - { - using var uow = _db.GetDbContext(); - return uow - .AutoCommands - .AsNoTracking() - .Where(x => x.Interval == 0) - .OrderBy(x => x.Id) - .ToList(); - } - - public IEnumerable GetAutoCommands() - { - using var uow = _db.GetDbContext(); - return uow - .AutoCommands - .AsNoTracking() - .Where(x => x.Interval >= 5) - .OrderBy(x => x.Id) - .ToList(); - } - - private async Task LoadOwnerChannels() - { - var channels = await Task.WhenAll(_creds.OwnerIds.Select(id => - { - var user = _client.GetUser(id); - if (user is null) - return Task.FromResult(null); - - return user.GetOrCreateDMChannelAsync(); - })).ConfigureAwait(false); - - ownerChannels = channels.Where(x => x != null) - .ToDictionary(x => x.Recipient.Id, x => x) - .ToImmutableDictionary(); - - if (!ownerChannels.Any()) - Log.Warning( - "No owner channels created! Make sure you've specified the correct OwnerId in the creds.yml file and invited the bot to a Discord server."); - else - Log.Information($"Created {ownerChannels.Count} out of {_creds.OwnerIds.Count} owner message channels."); - } - - public Task LeaveGuild(string guildStr) - => _pubSub.Pub(_guildLeaveKey, guildStr); - - // forwards dms - public async Task LateExecute(IGuild guild, IUserMessage msg) - { - var bs = _bss.Data; - if (msg.Channel is IDMChannel && bs.ForwardMessages && ownerChannels.Any()) - { - var title = _strings.GetText(strs.dm_from) + - $" [{msg.Author}]({msg.Author.Id})"; - - var attachamentsTxt = _strings.GetText(strs.attachments); - - var toSend = msg.Content; - - if (msg.Attachments.Count > 0) - { - toSend += $"\n\n{Format.Code(attachamentsTxt)}:\n" + - string.Join("\n", msg.Attachments.Select(a => a.ProxyUrl)); - } - - if (bs.ForwardToAllOwners) - { - var allOwnerChannels = ownerChannels.Values; - - foreach (var ownerCh in allOwnerChannels.Where(ch => ch.Recipient.Id != msg.Author.Id)) - { - try - { - await ownerCh.SendConfirmAsync(_eb, title, toSend).ConfigureAwait(false); - } - catch - { - Log.Warning("Can't contact owner with id {0}", ownerCh.Recipient.Id); - } - } - } - else - { - var firstOwnerChannel = ownerChannels.Values.First(); - if (firstOwnerChannel.Recipient.Id != msg.Author.Id) - { - try - { - await firstOwnerChannel.SendConfirmAsync(_eb, title, toSend).ConfigureAwait(false); - } - catch - { - // ignored - } - } - } - } - } - - public bool RemoveStartupCommand(int index, out AutoCommand cmd) - { - using (var uow = _db.GetDbContext()) - { - cmd = uow.AutoCommands - .AsNoTracking() - .Where(x => x.Interval == 0) - .Skip(index) - .FirstOrDefault(); - - if (cmd != null) - { - uow.Remove(cmd); - uow.SaveChanges(); - return true; - } - } - - return false; - } - - public bool RemoveAutoCommand(int index, out AutoCommand cmd) - { - using (var uow = _db.GetDbContext()) - { - cmd = uow.AutoCommands - .AsNoTracking() - .Where(x => x.Interval >= 5) - .Skip(index) - .FirstOrDefault(); - - if (cmd != null) - { - uow.Remove(cmd); - if (_autoCommands.TryGetValue(cmd.GuildId, out var autos)) - if (autos.TryRemove(cmd.Id, out var timer)) - timer.Change(Timeout.Infinite, Timeout.Infinite); - uow.SaveChanges(); - return true; - } - } - - return false; - } - - public async Task SetAvatar(string img) - { - if (string.IsNullOrWhiteSpace(img)) - return false; - - if (!Uri.IsWellFormedUriString(img, UriKind.Absolute)) - return false; - - var uri = new Uri(img); - - using (var http = _httpFactory.CreateClient()) - using (var sr = await http.GetAsync(uri, HttpCompletionOption.ResponseHeadersRead).ConfigureAwait(false)) - { - if (!sr.IsImage()) - return false; - - // i can't just do ReadAsStreamAsync because dicord.net's image poops itself - var imgData = await sr.Content.ReadAsByteArrayAsync().ConfigureAwait(false); - using (var imgStream = imgData.ToStream()) - { - await _client.CurrentUser.ModifyAsync(u => u.Avatar = new Image(imgStream)).ConfigureAwait(false); - } - } - - return true; - } - - public void ClearStartupCommands() - { - using (var uow = _db.GetDbContext()) - { - var toRemove = uow - .AutoCommands - .AsNoTracking() - .Where(x => x.Interval == 0); - - uow.AutoCommands.RemoveRange(toRemove); - uow.SaveChanges(); - } - } - - public Task ReloadImagesAsync() - => _pubSub.Pub(_imagesReloadKey, true); - - public bool ForwardMessages() - { - var isForwarding = false; - _bss.ModifyConfig(config => { isForwarding = config.ForwardMessages = !config.ForwardMessages; }); - - return isForwarding; - } - - public bool ForwardToAll() - { - var isToAll = false; - _bss.ModifyConfig(config => { isToAll = config.ForwardToAllOwners = !config.ForwardToAllOwners; }); - return isToAll; - } - - private void HandleStatusChanges() - { - _pubSub.Sub(_activitySetKey, async data => - { - try - { - await _client.SetGameAsync(data.Name, data.Link, type: data.Type); - } - catch (Exception ex) - { - Log.Warning(ex, "Error setting activity"); - } - }); - } - - public Task SetGameAsync(string game, ActivityType type) - => _pubSub.Pub(_activitySetKey, new() {Name = game, Link = null, Type = type}); - - public Task SetStreamAsync(string name, string link) - => _pubSub.Pub(_activitySetKey, new() { Name = name, Link = link, Type = ActivityType.Streaming }); - - private sealed class ActivityPubData - { - public string Name { get; init; } - public string Link { get; init; } - public ActivityType Type { get; init; } - } + private sealed class ActivityPubData + { + public string Name { get; init; } + public string Link { get; init; } + public ActivityType Type { get; init; } } } \ No newline at end of file diff --git a/src/NadekoBot/Modules/Administration/Services/UserPunishService.cs b/src/NadekoBot/Modules/Administration/Services/UserPunishService.cs index be357e140..cbf464d61 100644 --- a/src/NadekoBot/Modules/Administration/Services/UserPunishService.cs +++ b/src/NadekoBot/Modules/Administration/Services/UserPunishService.cs @@ -1,13 +1,9 @@ -using System; -using System.Collections.Generic; -using System.Linq; using System.Threading; using System.Threading.Tasks; using Discord; using Discord.Commands; using Discord.WebSocket; using Microsoft.EntityFrameworkCore; -using NadekoBot.Common; using NadekoBot.Common.Replacements; using NadekoBot.Common.TypeReaders.Models; using NadekoBot.Services; @@ -16,511 +12,509 @@ using NadekoBot.Db; using NadekoBot.Extensions; using NadekoBot.Modules.Permissions.Services; using Newtonsoft.Json; -using Serilog; -namespace NadekoBot.Modules.Administration.Services +namespace NadekoBot.Modules.Administration.Services; + +public class UserPunishService : INService { - public class UserPunishService : INService + private readonly MuteService _mute; + private readonly DbService _db; + private readonly BlacklistService _blacklistService; + private readonly BotConfigService _bcs; + private readonly Timer _warnExpiryTimer; + + public UserPunishService(MuteService mute, DbService db, BlacklistService blacklistService, BotConfigService bcs) { - private readonly MuteService _mute; - private readonly DbService _db; - private readonly BlacklistService _blacklistService; - private readonly BotConfigService _bcs; - private readonly Timer _warnExpiryTimer; + _mute = mute; + _db = db; + _blacklistService = blacklistService; + _bcs = bcs; - public UserPunishService(MuteService mute, DbService db, BlacklistService blacklistService, BotConfigService bcs) + _warnExpiryTimer = new Timer(async _ => { - _mute = mute; - _db = db; - _blacklistService = blacklistService; - _bcs = bcs; + await CheckAllWarnExpiresAsync(); + }, null, TimeSpan.FromSeconds(0), TimeSpan.FromHours(12)); + } - _warnExpiryTimer = new Timer(async _ => - { - await CheckAllWarnExpiresAsync(); - }, null, TimeSpan.FromSeconds(0), TimeSpan.FromHours(12)); + public async Task Warn(IGuild guild, ulong userId, IUser mod, int weight, string reason) + { + if (weight <= 0) + throw new ArgumentOutOfRangeException(nameof(weight)); + + var modName = mod.ToString(); + + if (string.IsNullOrWhiteSpace(reason)) + reason = "-"; + + var guildId = guild.Id; + + var warn = new Warning() + { + UserId = userId, + GuildId = guildId, + Forgiven = false, + Reason = reason, + Moderator = modName, + Weight = weight, + }; + + int warnings = 1; + List ps; + using (var uow = _db.GetDbContext()) + { + ps = uow.GuildConfigsForId(guildId, set => set.Include(x => x.WarnPunishments)) + .WarnPunishments; + + warnings += uow + .Warnings + .ForId(guildId, userId) + .Where(w => !w.Forgiven && w.UserId == userId) + .Sum(x => x.Weight); + + uow.Warnings.Add(warn); + + uow.SaveChanges(); } - public async Task Warn(IGuild guild, ulong userId, IUser mod, int weight, string reason) + var p = ps.FirstOrDefault(x => x.Count == warnings); + + if (p != null) { - if (weight <= 0) - throw new ArgumentOutOfRangeException(nameof(weight)); - - var modName = mod.ToString(); + var user = await guild.GetUserAsync(userId).ConfigureAwait(false); + if (user is null) + return null; - if (string.IsNullOrWhiteSpace(reason)) - reason = "-"; - - var guildId = guild.Id; - - var warn = new Warning() - { - UserId = userId, - GuildId = guildId, - Forgiven = false, - Reason = reason, - Moderator = modName, - Weight = weight, - }; - - int warnings = 1; - List ps; - using (var uow = _db.GetDbContext()) - { - ps = uow.GuildConfigsForId(guildId, set => set.Include(x => x.WarnPunishments)) - .WarnPunishments; - - warnings += uow - .Warnings - .ForId(guildId, userId) - .Where(w => !w.Forgiven && w.UserId == userId) - .Sum(x => x.Weight); - - uow.Warnings.Add(warn); - - uow.SaveChanges(); - } - - var p = ps.FirstOrDefault(x => x.Count == warnings); - - if (p != null) - { - var user = await guild.GetUserAsync(userId).ConfigureAwait(false); - if (user is null) - return null; - - await ApplyPunishment(guild, user, mod, p.Punishment, p.Time, p.RoleId, "Warned too many times."); - return p; - } - - return null; + await ApplyPunishment(guild, user, mod, p.Punishment, p.Time, p.RoleId, "Warned too many times."); + return p; } - public async Task ApplyPunishment(IGuild guild, IGuildUser user, IUser mod, PunishmentAction p, int minutes, - ulong? roleId, string reason) - { + return null; + } - if (!await CheckPermission(guild, p)) - return; + public async Task ApplyPunishment(IGuild guild, IGuildUser user, IUser mod, PunishmentAction p, int minutes, + ulong? roleId, string reason) + { + + if (!await CheckPermission(guild, p)) + return; - switch (p) - { - case PunishmentAction.Mute: - if (minutes == 0) - await _mute.MuteUser(user, mod, reason: reason).ConfigureAwait(false); - else - await _mute.TimedMute(user, mod, TimeSpan.FromMinutes(minutes), reason: reason) - .ConfigureAwait(false); - break; - case PunishmentAction.VoiceMute: - if (minutes == 0) - await _mute.MuteUser(user, mod, MuteType.Voice, reason).ConfigureAwait(false); - else - await _mute.TimedMute(user, mod, TimeSpan.FromMinutes(minutes), MuteType.Voice, reason) - .ConfigureAwait(false); - break; - case PunishmentAction.ChatMute: - if (minutes == 0) - await _mute.MuteUser(user, mod, MuteType.Chat, reason).ConfigureAwait(false); - else - await _mute.TimedMute(user, mod, TimeSpan.FromMinutes(minutes), MuteType.Chat, reason) - .ConfigureAwait(false); - break; - case PunishmentAction.Kick: - await user.KickAsync(reason).ConfigureAwait(false); - break; - case PunishmentAction.Ban: - if (minutes == 0) - await guild.AddBanAsync(user, reason: reason, pruneDays: 7).ConfigureAwait(false); - else - await _mute.TimedBan(user.Guild, user, TimeSpan.FromMinutes(minutes), reason) - .ConfigureAwait(false); - break; - case PunishmentAction.Softban: - await guild.AddBanAsync(user, 7, reason: $"Softban | {reason}").ConfigureAwait(false); - try - { - await guild.RemoveBanAsync(user).ConfigureAwait(false); - } - catch - { - await guild.RemoveBanAsync(user).ConfigureAwait(false); - } - - break; - case PunishmentAction.RemoveRoles: - await user.RemoveRolesAsync(user.GetRoles().Where(x => !x.IsManaged && x != x.Guild.EveryoneRole)) + switch (p) + { + case PunishmentAction.Mute: + if (minutes == 0) + await _mute.MuteUser(user, mod, reason: reason).ConfigureAwait(false); + else + await _mute.TimedMute(user, mod, TimeSpan.FromMinutes(minutes), reason: reason) .ConfigureAwait(false); - break; - case PunishmentAction.AddRole: - if (roleId is null) - return; - var role = guild.GetRole(roleId.Value); - if (role is not null) - { - if (minutes == 0) - await user.AddRoleAsync(role).ConfigureAwait(false); - else - await _mute.TimedRole(user, TimeSpan.FromMinutes(minutes), reason, role) - .ConfigureAwait(false); - } + break; + case PunishmentAction.VoiceMute: + if (minutes == 0) + await _mute.MuteUser(user, mod, MuteType.Voice, reason).ConfigureAwait(false); + else + await _mute.TimedMute(user, mod, TimeSpan.FromMinutes(minutes), MuteType.Voice, reason) + .ConfigureAwait(false); + break; + case PunishmentAction.ChatMute: + if (minutes == 0) + await _mute.MuteUser(user, mod, MuteType.Chat, reason).ConfigureAwait(false); + else + await _mute.TimedMute(user, mod, TimeSpan.FromMinutes(minutes), MuteType.Chat, reason) + .ConfigureAwait(false); + break; + case PunishmentAction.Kick: + await user.KickAsync(reason).ConfigureAwait(false); + break; + case PunishmentAction.Ban: + if (minutes == 0) + await guild.AddBanAsync(user, reason: reason, pruneDays: 7).ConfigureAwait(false); + else + await _mute.TimedBan(user.Guild, user, TimeSpan.FromMinutes(minutes), reason) + .ConfigureAwait(false); + break; + case PunishmentAction.Softban: + await guild.AddBanAsync(user, 7, reason: $"Softban | {reason}").ConfigureAwait(false); + try + { + await guild.RemoveBanAsync(user).ConfigureAwait(false); + } + catch + { + await guild.RemoveBanAsync(user).ConfigureAwait(false); + } + + break; + case PunishmentAction.RemoveRoles: + await user.RemoveRolesAsync(user.GetRoles().Where(x => !x.IsManaged && x != x.Guild.EveryoneRole)) + .ConfigureAwait(false); + break; + case PunishmentAction.AddRole: + if (roleId is null) + return; + var role = guild.GetRole(roleId.Value); + if (role is not null) + { + if (minutes == 0) + await user.AddRoleAsync(role).ConfigureAwait(false); else - { - Log.Warning($"Can't find role {roleId.Value} on server {guild.Id} to apply punishment."); - } + await _mute.TimedRole(user, TimeSpan.FromMinutes(minutes), reason, role) + .ConfigureAwait(false); + } + else + { + Log.Warning($"Can't find role {roleId.Value} on server {guild.Id} to apply punishment."); + } - break; - default: - break; - } + break; + default: + break; } + } - /// - /// Used to prevent the bot from hitting 403's when it needs to - /// apply punishments with insufficient permissions - /// - /// Guild the punishment is applied in - /// Punishment to apply - /// Whether the bot has sufficient permissions - private async Task CheckPermission(IGuild guild, PunishmentAction punish) - { + /// + /// Used to prevent the bot from hitting 403's when it needs to + /// apply punishments with insufficient permissions + /// + /// Guild the punishment is applied in + /// Punishment to apply + /// Whether the bot has sufficient permissions + private async Task CheckPermission(IGuild guild, PunishmentAction punish) + { - var botUser = await guild.GetCurrentUserAsync(); - switch (punish) - { - case PunishmentAction.Mute: - return botUser.GuildPermissions.MuteMembers && botUser.GuildPermissions.ManageRoles; - case PunishmentAction.Kick: - return botUser.GuildPermissions.KickMembers; - case PunishmentAction.Ban: - return botUser.GuildPermissions.BanMembers; - case PunishmentAction.Softban: - return botUser.GuildPermissions.BanMembers; // ban + unban - case PunishmentAction.RemoveRoles: - return botUser.GuildPermissions.ManageRoles; - case PunishmentAction.ChatMute: - return botUser.GuildPermissions.ManageRoles; // adds nadeko-mute role - case PunishmentAction.VoiceMute: - return botUser.GuildPermissions.MuteMembers; - case PunishmentAction.AddRole: - return botUser.GuildPermissions.ManageRoles; - default: - return true; - } - } - - public async Task CheckAllWarnExpiresAsync() + var botUser = await guild.GetCurrentUserAsync(); + switch (punish) { - using (var uow = _db.GetDbContext()) - { - var cleared = await uow.Database.ExecuteSqlRawAsync($@"UPDATE Warnings + case PunishmentAction.Mute: + return botUser.GuildPermissions.MuteMembers && botUser.GuildPermissions.ManageRoles; + case PunishmentAction.Kick: + return botUser.GuildPermissions.KickMembers; + case PunishmentAction.Ban: + return botUser.GuildPermissions.BanMembers; + case PunishmentAction.Softban: + return botUser.GuildPermissions.BanMembers; // ban + unban + case PunishmentAction.RemoveRoles: + return botUser.GuildPermissions.ManageRoles; + case PunishmentAction.ChatMute: + return botUser.GuildPermissions.ManageRoles; // adds nadeko-mute role + case PunishmentAction.VoiceMute: + return botUser.GuildPermissions.MuteMembers; + case PunishmentAction.AddRole: + return botUser.GuildPermissions.ManageRoles; + default: + return true; + } + } + + public async Task CheckAllWarnExpiresAsync() + { + using (var uow = _db.GetDbContext()) + { + var cleared = await uow.Database.ExecuteSqlRawAsync($@"UPDATE Warnings SET Forgiven = 1, ForgivenBy = 'Expiry' WHERE GuildId in (SELECT GuildId FROM GuildConfigs WHERE WarnExpireHours > 0 AND WarnExpireAction = 0) AND Forgiven = 0 AND DateAdded < datetime('now', (SELECT '-' || WarnExpireHours || ' hours' FROM GuildConfigs as gc WHERE gc.GuildId = Warnings.GuildId));"); - var deleted = await uow.Database.ExecuteSqlRawAsync($@"DELETE FROM Warnings + var deleted = await uow.Database.ExecuteSqlRawAsync($@"DELETE FROM Warnings WHERE GuildId in (SELECT GuildId FROM GuildConfigs WHERE WarnExpireHours > 0 AND WarnExpireAction = 1) AND DateAdded < datetime('now', (SELECT '-' || WarnExpireHours || ' hours' FROM GuildConfigs as gc WHERE gc.GuildId = Warnings.GuildId));"); - if(cleared > 0 || deleted > 0) - { - Log.Information($"Cleared {cleared} warnings and deleted {deleted} warnings due to expiry."); - } + if(cleared > 0 || deleted > 0) + { + Log.Information($"Cleared {cleared} warnings and deleted {deleted} warnings due to expiry."); } } + } - public async Task CheckWarnExpiresAsync(ulong guildId) + public async Task CheckWarnExpiresAsync(ulong guildId) + { + using (var uow = _db.GetDbContext()) { - using (var uow = _db.GetDbContext()) + var config = uow.GuildConfigsForId(guildId, inc => inc); + + if (config.WarnExpireHours == 0) + return; + + var hours = $"{-config.WarnExpireHours} hours"; + if (config.WarnExpireAction == WarnExpireAction.Clear) { - var config = uow.GuildConfigsForId(guildId, inc => inc); - - if (config.WarnExpireHours == 0) - return; - - var hours = $"{-config.WarnExpireHours} hours"; - if (config.WarnExpireAction == WarnExpireAction.Clear) - { - await uow.Database.ExecuteSqlInterpolatedAsync($@"UPDATE warnings + await uow.Database.ExecuteSqlInterpolatedAsync($@"UPDATE warnings SET Forgiven = 1, ForgivenBy = 'Expiry' WHERE GuildId={guildId} AND Forgiven = 0 AND DateAdded < datetime('now', {hours})"); - } - else if (config.WarnExpireAction == WarnExpireAction.Delete) - { - await uow.Database.ExecuteSqlInterpolatedAsync($@"DELETE FROM warnings + } + else if (config.WarnExpireAction == WarnExpireAction.Delete) + { + await uow.Database.ExecuteSqlInterpolatedAsync($@"DELETE FROM warnings WHERE GuildId={guildId} AND DateAdded < datetime('now', {hours})"); - } - - await uow.SaveChangesAsync(); - } - } - - public Task GetWarnExpire(ulong guildId) - { - using var uow = _db.GetDbContext(); - var config = uow.GuildConfigsForId(guildId, set => set); - return Task.FromResult(config.WarnExpireHours / 24); - } - - public async Task WarnExpireAsync(ulong guildId, int days, bool delete) - { - using (var uow = _db.GetDbContext()) - { - var config = uow.GuildConfigsForId(guildId, inc => inc); - - config.WarnExpireHours = days * 24; - config.WarnExpireAction = delete ? WarnExpireAction.Delete : WarnExpireAction.Clear; - await uow.SaveChangesAsync(); - - // no need to check for warn expires - if (config.WarnExpireHours == 0) - return; } - await CheckWarnExpiresAsync(guildId); - } - - public IGrouping[] WarnlogAll(ulong gid) - { - using (var uow = _db.GetDbContext()) - { - return uow.Warnings.GetForGuild(gid).GroupBy(x => x.UserId).ToArray(); - } - } - - public Warning[] UserWarnings(ulong gid, ulong userId) - { - using (var uow = _db.GetDbContext()) - { - return uow.Warnings.ForId(gid, userId); - } - } - - public async Task WarnClearAsync(ulong guildId, ulong userId, int index, string moderator) - { - bool toReturn = true; - using (var uow = _db.GetDbContext()) - { - if (index == 0) - { - await uow.Warnings.ForgiveAll(guildId, userId, moderator); - } - else - { - toReturn = uow.Warnings.Forgive(guildId, userId, moderator, index - 1); - } - uow.SaveChanges(); - } - return toReturn; - } - - public bool WarnPunish(ulong guildId, int number, PunishmentAction punish, StoopidTime time, IRole role = null) - { - // these 3 don't make sense with time - if ((punish == PunishmentAction.Softban || punish == PunishmentAction.Kick || punish == PunishmentAction.RemoveRoles) && time != null) - return false; - if (number <= 0 || (time != null && time.Time > TimeSpan.FromDays(49))) - return false; - - using (var uow = _db.GetDbContext()) - { - var ps = uow.GuildConfigsForId(guildId, set => set.Include(x => x.WarnPunishments)).WarnPunishments; - var toDelete = ps.Where(x => x.Count == number); - - uow.RemoveRange(toDelete); - - ps.Add(new WarningPunishment() - { - Count = number, - Punishment = punish, - Time = (int?)(time?.Time.TotalMinutes) ?? 0, - RoleId = punish == PunishmentAction.AddRole ? role.Id : default(ulong?), - }); - uow.SaveChanges(); - } - return true; - } - - public bool WarnPunishRemove(ulong guildId, int number) - { - if (number <= 0) - return false; - - using (var uow = _db.GetDbContext()) - { - var ps = uow.GuildConfigsForId(guildId, set => set.Include(x => x.WarnPunishments)).WarnPunishments; - var p = ps.FirstOrDefault(x => x.Count == number); - - if (p != null) - { - uow.Remove(p); - uow.SaveChanges(); - } - } - return true; - } - - public WarningPunishment[] WarnPunishList(ulong guildId) - { - using (var uow = _db.GetDbContext()) - { - return uow.GuildConfigsForId(guildId, gc => gc.Include(x => x.WarnPunishments)) - .WarnPunishments - .OrderBy(x => x.Count) - .ToArray(); - } - } - - public (IEnumerable<(string Original, ulong? Id, string Reason)> Bans, int Missing) MassKill(SocketGuild guild, string people) - { - var gusers = guild.Users; - //get user objects and reasons - var bans = people.Split("\n") - .Select(x => - { - var split = x.Trim().Split(" "); - - var reason = string.Join(" ", split.Skip(1)); - - if (ulong.TryParse(split[0], out var id)) - return (Original: split[0], Id: id, Reason: reason); - - return (Original: split[0], - Id: gusers - .FirstOrDefault(u => u.ToString().ToLowerInvariant() == x) - ?.Id, - Reason: reason); - }) - .ToArray(); - - //if user is null, means that person couldn't be found - var missing = bans - .Count(x => !x.Id.HasValue); - - //get only data for found users - var found = bans - .Where(x => x.Id.HasValue) - .Select(x => x.Id.Value) - .ToList(); - - _blacklistService.BlacklistUsers(found); - - return (bans, missing); - } - - public string GetBanTemplate(ulong guildId) - { - using (var uow = _db.GetDbContext()) - { - var template = uow.BanTemplates - .AsQueryable() - .FirstOrDefault(x => x.GuildId == guildId); - return template?.Text; - } - } - - public void SetBanTemplate(ulong guildId, string text) - { - using (var uow = _db.GetDbContext()) - { - var template = uow.BanTemplates - .AsQueryable() - .FirstOrDefault(x => x.GuildId == guildId); - - if (text is null) - { - if (template is null) - return; - - uow.Remove(template); - } - else if (template is null) - { - uow.BanTemplates.Add(new BanTemplate() - { - GuildId = guildId, - Text = text, - }); - } - else - { - template.Text = text; - } - - uow.SaveChanges(); - } - } - - public SmartText GetBanUserDmEmbed(ICommandContext context, IGuildUser target, string defaultMessage, - string banReason, TimeSpan? duration) - { - return GetBanUserDmEmbed( - (DiscordSocketClient) context.Client, - (SocketGuild) context.Guild, - (IGuildUser) context.User, - target, - defaultMessage, - banReason, - duration); - } - - public SmartText GetBanUserDmEmbed(DiscordSocketClient client, SocketGuild guild, - IGuildUser moderator, IGuildUser target, string defaultMessage, string banReason, TimeSpan? duration) - { - var template = GetBanTemplate(guild.Id); - - banReason = string.IsNullOrWhiteSpace(banReason) - ? "-" - : banReason; - - var replacer = new ReplacementBuilder() - .WithServer(client, guild) - .WithOverride("%ban.mod%", () => moderator.ToString()) - .WithOverride("%ban.mod.fullname%", () => moderator.ToString()) - .WithOverride("%ban.mod.name%", () => moderator.Username) - .WithOverride("%ban.mod.discrim%", () => moderator.Discriminator) - .WithOverride("%ban.user%", () => target.ToString()) - .WithOverride("%ban.user.fullname%", () => target.ToString()) - .WithOverride("%ban.user.name%", () => target.Username) - .WithOverride("%ban.user.discrim%", () => target.Discriminator) - .WithOverride("%reason%", () => banReason) - .WithOverride("%ban.reason%", () => banReason) - .WithOverride("%ban.duration%", () => duration?.ToString(@"d\.hh\:mm")?? "perma") - .Build(); - - // if template isn't set, use the old message style - if (string.IsNullOrWhiteSpace(template)) - { - template = JsonConvert.SerializeObject(new - { - color = _bcs.Data.Color.Error, - description = defaultMessage - }); - } - // if template is set to "-" do not dm the user - else if (template == "-") - { - return default; - } - // if template is an embed, send that embed with replacements - // otherwise, treat template as a regular string with replacements - else if (!SmartText.CreateFrom(template).IsEmbed) - { - template = JsonConvert.SerializeObject(new - { - color = _bcs.Data.Color.Error, - description = template - }); - } - - var output = SmartText.CreateFrom(template); - return replacer.Replace(output); + await uow.SaveChangesAsync(); } } -} + + public Task GetWarnExpire(ulong guildId) + { + using var uow = _db.GetDbContext(); + var config = uow.GuildConfigsForId(guildId, set => set); + return Task.FromResult(config.WarnExpireHours / 24); + } + + public async Task WarnExpireAsync(ulong guildId, int days, bool delete) + { + using (var uow = _db.GetDbContext()) + { + var config = uow.GuildConfigsForId(guildId, inc => inc); + + config.WarnExpireHours = days * 24; + config.WarnExpireAction = delete ? WarnExpireAction.Delete : WarnExpireAction.Clear; + await uow.SaveChangesAsync(); + + // no need to check for warn expires + if (config.WarnExpireHours == 0) + return; + } + + await CheckWarnExpiresAsync(guildId); + } + + public IGrouping[] WarnlogAll(ulong gid) + { + using (var uow = _db.GetDbContext()) + { + return uow.Warnings.GetForGuild(gid).GroupBy(x => x.UserId).ToArray(); + } + } + + public Warning[] UserWarnings(ulong gid, ulong userId) + { + using (var uow = _db.GetDbContext()) + { + return uow.Warnings.ForId(gid, userId); + } + } + + public async Task WarnClearAsync(ulong guildId, ulong userId, int index, string moderator) + { + bool toReturn = true; + using (var uow = _db.GetDbContext()) + { + if (index == 0) + { + await uow.Warnings.ForgiveAll(guildId, userId, moderator); + } + else + { + toReturn = uow.Warnings.Forgive(guildId, userId, moderator, index - 1); + } + uow.SaveChanges(); + } + return toReturn; + } + + public bool WarnPunish(ulong guildId, int number, PunishmentAction punish, StoopidTime time, IRole role = null) + { + // these 3 don't make sense with time + if ((punish == PunishmentAction.Softban || punish == PunishmentAction.Kick || punish == PunishmentAction.RemoveRoles) && time != null) + return false; + if (number <= 0 || (time != null && time.Time > TimeSpan.FromDays(49))) + return false; + + using (var uow = _db.GetDbContext()) + { + var ps = uow.GuildConfigsForId(guildId, set => set.Include(x => x.WarnPunishments)).WarnPunishments; + var toDelete = ps.Where(x => x.Count == number); + + uow.RemoveRange(toDelete); + + ps.Add(new WarningPunishment() + { + Count = number, + Punishment = punish, + Time = (int?)(time?.Time.TotalMinutes) ?? 0, + RoleId = punish == PunishmentAction.AddRole ? role.Id : default(ulong?), + }); + uow.SaveChanges(); + } + return true; + } + + public bool WarnPunishRemove(ulong guildId, int number) + { + if (number <= 0) + return false; + + using (var uow = _db.GetDbContext()) + { + var ps = uow.GuildConfigsForId(guildId, set => set.Include(x => x.WarnPunishments)).WarnPunishments; + var p = ps.FirstOrDefault(x => x.Count == number); + + if (p != null) + { + uow.Remove(p); + uow.SaveChanges(); + } + } + return true; + } + + public WarningPunishment[] WarnPunishList(ulong guildId) + { + using (var uow = _db.GetDbContext()) + { + return uow.GuildConfigsForId(guildId, gc => gc.Include(x => x.WarnPunishments)) + .WarnPunishments + .OrderBy(x => x.Count) + .ToArray(); + } + } + + public (IEnumerable<(string Original, ulong? Id, string Reason)> Bans, int Missing) MassKill(SocketGuild guild, string people) + { + var gusers = guild.Users; + //get user objects and reasons + var bans = people.Split("\n") + .Select(x => + { + var split = x.Trim().Split(" "); + + var reason = string.Join(" ", split.Skip(1)); + + if (ulong.TryParse(split[0], out var id)) + return (Original: split[0], Id: id, Reason: reason); + + return (Original: split[0], + Id: gusers + .FirstOrDefault(u => u.ToString().ToLowerInvariant() == x) + ?.Id, + Reason: reason); + }) + .ToArray(); + + //if user is null, means that person couldn't be found + var missing = bans + .Count(x => !x.Id.HasValue); + + //get only data for found users + var found = bans + .Where(x => x.Id.HasValue) + .Select(x => x.Id.Value) + .ToList(); + + _blacklistService.BlacklistUsers(found); + + return (bans, missing); + } + + public string GetBanTemplate(ulong guildId) + { + using (var uow = _db.GetDbContext()) + { + var template = uow.BanTemplates + .AsQueryable() + .FirstOrDefault(x => x.GuildId == guildId); + return template?.Text; + } + } + + public void SetBanTemplate(ulong guildId, string text) + { + using (var uow = _db.GetDbContext()) + { + var template = uow.BanTemplates + .AsQueryable() + .FirstOrDefault(x => x.GuildId == guildId); + + if (text is null) + { + if (template is null) + return; + + uow.Remove(template); + } + else if (template is null) + { + uow.BanTemplates.Add(new BanTemplate() + { + GuildId = guildId, + Text = text, + }); + } + else + { + template.Text = text; + } + + uow.SaveChanges(); + } + } + + public SmartText GetBanUserDmEmbed(ICommandContext context, IGuildUser target, string defaultMessage, + string banReason, TimeSpan? duration) + { + return GetBanUserDmEmbed( + (DiscordSocketClient) context.Client, + (SocketGuild) context.Guild, + (IGuildUser) context.User, + target, + defaultMessage, + banReason, + duration); + } + + public SmartText GetBanUserDmEmbed(DiscordSocketClient client, SocketGuild guild, + IGuildUser moderator, IGuildUser target, string defaultMessage, string banReason, TimeSpan? duration) + { + var template = GetBanTemplate(guild.Id); + + banReason = string.IsNullOrWhiteSpace(banReason) + ? "-" + : banReason; + + var replacer = new ReplacementBuilder() + .WithServer(client, guild) + .WithOverride("%ban.mod%", () => moderator.ToString()) + .WithOverride("%ban.mod.fullname%", () => moderator.ToString()) + .WithOverride("%ban.mod.name%", () => moderator.Username) + .WithOverride("%ban.mod.discrim%", () => moderator.Discriminator) + .WithOverride("%ban.user%", () => target.ToString()) + .WithOverride("%ban.user.fullname%", () => target.ToString()) + .WithOverride("%ban.user.name%", () => target.Username) + .WithOverride("%ban.user.discrim%", () => target.Discriminator) + .WithOverride("%reason%", () => banReason) + .WithOverride("%ban.reason%", () => banReason) + .WithOverride("%ban.duration%", () => duration?.ToString(@"d\.hh\:mm")?? "perma") + .Build(); + + // if template isn't set, use the old message style + if (string.IsNullOrWhiteSpace(template)) + { + template = JsonConvert.SerializeObject(new + { + color = _bcs.Data.Color.Error, + description = defaultMessage + }); + } + // if template is set to "-" do not dm the user + else if (template == "-") + { + return default; + } + // if template is an embed, send that embed with replacements + // otherwise, treat template as a regular string with replacements + else if (!SmartText.CreateFrom(template).IsEmbed) + { + template = JsonConvert.SerializeObject(new + { + color = _bcs.Data.Color.Error, + description = template + }); + } + + var output = SmartText.CreateFrom(template); + return replacer.Replace(output); + } +} \ No newline at end of file diff --git a/src/NadekoBot/Modules/Administration/Services/VcRoleService.cs b/src/NadekoBot/Modules/Administration/Services/VcRoleService.cs index 4d7b3eff9..eba52993a 100644 --- a/src/NadekoBot/Modules/Administration/Services/VcRoleService.cs +++ b/src/NadekoBot/Modules/Administration/Services/VcRoleService.cs @@ -1,7 +1,4 @@ -using System; -using System.Collections.Concurrent; -using System.Collections.Generic; -using System.Linq; +using System.Collections.Concurrent; using System.Threading.Tasks; using Discord; using Discord.WebSocket; @@ -9,223 +6,221 @@ using Microsoft.EntityFrameworkCore; using NadekoBot.Services; using NadekoBot.Services.Database.Models; using NadekoBot.Db; -using Serilog; -namespace NadekoBot.Modules.Administration.Services +namespace NadekoBot.Modules.Administration.Services; + +public class VcRoleService : INService { - public class VcRoleService : INService + private readonly DbService _db; + private readonly DiscordSocketClient _client; + + public ConcurrentDictionary> VcRoles { get; } + public ConcurrentDictionary> ToAssign { get; } + + public VcRoleService(DiscordSocketClient client, Bot bot, DbService db) { - private readonly DbService _db; - private readonly DiscordSocketClient _client; + _db = db; + _client = client; - public ConcurrentDictionary> VcRoles { get; } - public ConcurrentDictionary> ToAssign { get; } + _client.UserVoiceStateUpdated += ClientOnUserVoiceStateUpdated; + VcRoles = new ConcurrentDictionary>(); + ToAssign = new ConcurrentDictionary>(); + var missingRoles = new ConcurrentBag(); - public VcRoleService(DiscordSocketClient client, Bot bot, DbService db) + using (var uow = db.GetDbContext()) { - _db = db; - _client = client; - - _client.UserVoiceStateUpdated += ClientOnUserVoiceStateUpdated; - VcRoles = new ConcurrentDictionary>(); - ToAssign = new ConcurrentDictionary>(); - var missingRoles = new ConcurrentBag(); - - using (var uow = db.GetDbContext()) - { - var guildIds = client.Guilds.Select(x => x.Id).ToList(); - var configs = uow.Set() - .AsQueryable() - .Include(x => x.VcRoleInfos) - .Where(x => guildIds.Contains(x.GuildId)) - .ToList(); + var guildIds = client.Guilds.Select(x => x.Id).ToList(); + var configs = uow.Set() + .AsQueryable() + .Include(x => x.VcRoleInfos) + .Where(x => guildIds.Contains(x.GuildId)) + .ToList(); - Task.WhenAll(configs.Select(InitializeVcRole)); - } + Task.WhenAll(configs.Select(InitializeVcRole)); + } - Task.Run(async () => + Task.Run(async () => + { + while (true) { - while (true) + var tasks = ToAssign.Values.Select(queue => Task.Run(async () => { - var tasks = ToAssign.Values.Select(queue => Task.Run(async () => + while (queue.TryDequeue(out var item)) { - while (queue.TryDequeue(out var item)) + var (add, user, role) = item; + if (add) { - var (add, user, role) = item; - if (add) + if (!user.RoleIds.Contains(role.Id)) { - if (!user.RoleIds.Contains(role.Id)) - { - try { await user.AddRoleAsync(role).ConfigureAwait(false); } catch { } - } + try { await user.AddRoleAsync(role).ConfigureAwait(false); } catch { } } - else - { - if (user.RoleIds.Contains(role.Id)) - { - try { await user.RemoveRoleAsync(role).ConfigureAwait(false); } catch { } - } - } - - await Task.Delay(250).ConfigureAwait(false); } - })); - - await Task.WhenAll(tasks.Append(Task.Delay(1000))).ConfigureAwait(false); - } - }); - - _client.LeftGuild += _client_LeftGuild; - bot.JoinedGuild += Bot_JoinedGuild; - } - - private Task Bot_JoinedGuild(GuildConfig arg) - { - // includeall no longer loads vcrole - // need to load new guildconfig with vc role included - using (var uow = _db.GetDbContext()) - { - var configWithVcRole = uow.GuildConfigsForId( - arg.GuildId, - set => set.Include(x => x.VcRoleInfos) - ); - var _ = InitializeVcRole(configWithVcRole); - } - - return Task.CompletedTask; - } - - private Task _client_LeftGuild(SocketGuild arg) - { - VcRoles.TryRemove(arg.Id, out _); - ToAssign.TryRemove(arg.Id, out _); - return Task.CompletedTask; - } - - private async Task InitializeVcRole(GuildConfig gconf) - { - await Task.Yield(); - var g = _client.GetGuild(gconf.GuildId); - if (g is null) - return; - - var infos = new ConcurrentDictionary(); - var missingRoles = new List(); - VcRoles.AddOrUpdate(gconf.GuildId, infos, delegate { return infos; }); - foreach (var ri in gconf.VcRoleInfos) - { - var role = g.GetRole(ri.RoleId); - if (role is null) - { - missingRoles.Add(ri); - continue; - } - - infos.TryAdd(ri.VoiceChannelId, role); - } - - if (missingRoles.Any()) - { - using (var uow = _db.GetDbContext()) - { - Log.Warning($"Removing {missingRoles.Count} missing roles from {nameof(VcRoleService)}"); - uow.RemoveRange(missingRoles); - await uow.SaveChangesAsync(); - } - } - } - - public void AddVcRole(ulong guildId, IRole role, ulong vcId) - { - if (role is null) - throw new ArgumentNullException(nameof(role)); - - var guildVcRoles = VcRoles.GetOrAdd(guildId, new ConcurrentDictionary()); - - guildVcRoles.AddOrUpdate(vcId, role, (key, old) => role); - using (var uow = _db.GetDbContext()) - { - var conf = uow.GuildConfigsForId(guildId, set => set.Include(x => x.VcRoleInfos)); - var toDelete = conf.VcRoleInfos.FirstOrDefault(x => x.VoiceChannelId == vcId); // remove old one - if(toDelete != null) - { - uow.Remove(toDelete); - } - conf.VcRoleInfos.Add(new VcRoleInfo() - { - VoiceChannelId = vcId, - RoleId = role.Id, - }); // add new one - uow.SaveChanges(); - } - } - - public bool RemoveVcRole(ulong guildId, ulong vcId) - { - if (!VcRoles.TryGetValue(guildId, out var guildVcRoles)) - return false; - - if (!guildVcRoles.TryRemove(vcId, out _)) - return false; - - using (var uow = _db.GetDbContext()) - { - var conf = uow.GuildConfigsForId(guildId, set => set.Include(x => x.VcRoleInfos)); - var toRemove = conf.VcRoleInfos.Where(x => x.VoiceChannelId == vcId).ToList(); - uow.RemoveRange(toRemove); - uow.SaveChanges(); - } - - return true; - } - - private Task ClientOnUserVoiceStateUpdated(SocketUser usr, SocketVoiceState oldState, - SocketVoiceState newState) - { - - var gusr = usr as SocketGuildUser; - if (gusr is null) - return Task.CompletedTask; - - var oldVc = oldState.VoiceChannel; - var newVc = newState.VoiceChannel; - var _ = Task.Run(() => - { - try - { - if (oldVc != newVc) - { - ulong guildId; - guildId = newVc?.Guild.Id ?? oldVc.Guild.Id; - - if (VcRoles.TryGetValue(guildId, out ConcurrentDictionary guildVcRoles)) + else { - //remove old - if (oldVc != null && guildVcRoles.TryGetValue(oldVc.Id, out IRole role)) + if (user.RoleIds.Contains(role.Id)) { - Assign(false, gusr, role); + try { await user.RemoveRoleAsync(role).ConfigureAwait(false); } catch { } } - //add new - if (newVc != null && guildVcRoles.TryGetValue(newVc.Id, out role)) - { - Assign(true, gusr, role); - } - } + + await Task.Delay(250).ConfigureAwait(false); } - } - catch (Exception ex) - { - Log.Warning(ex, "Error in VcRoleService VoiceStateUpdate"); - } - }); - return Task.CompletedTask; + })); + + await Task.WhenAll(tasks.Append(Task.Delay(1000))).ConfigureAwait(false); + } + }); + + _client.LeftGuild += _client_LeftGuild; + bot.JoinedGuild += Bot_JoinedGuild; + } + + private Task Bot_JoinedGuild(GuildConfig arg) + { + // includeall no longer loads vcrole + // need to load new guildconfig with vc role included + using (var uow = _db.GetDbContext()) + { + var configWithVcRole = uow.GuildConfigsForId( + arg.GuildId, + set => set.Include(x => x.VcRoleInfos) + ); + var _ = InitializeVcRole(configWithVcRole); } - private void Assign(bool v, SocketGuildUser gusr, IRole role) + return Task.CompletedTask; + } + + private Task _client_LeftGuild(SocketGuild arg) + { + VcRoles.TryRemove(arg.Id, out _); + ToAssign.TryRemove(arg.Id, out _); + return Task.CompletedTask; + } + + private async Task InitializeVcRole(GuildConfig gconf) + { + await Task.Yield(); + var g = _client.GetGuild(gconf.GuildId); + if (g is null) + return; + + var infos = new ConcurrentDictionary(); + var missingRoles = new List(); + VcRoles.AddOrUpdate(gconf.GuildId, infos, delegate { return infos; }); + foreach (var ri in gconf.VcRoleInfos) { - var queue = ToAssign.GetOrAdd(gusr.Guild.Id, new ConcurrentQueue<(bool, IGuildUser, IRole)>()); - queue.Enqueue((v, gusr, role)); + var role = g.GetRole(ri.RoleId); + if (role is null) + { + missingRoles.Add(ri); + continue; + } + + infos.TryAdd(ri.VoiceChannelId, role); + } + + if (missingRoles.Any()) + { + using (var uow = _db.GetDbContext()) + { + Log.Warning($"Removing {missingRoles.Count} missing roles from {nameof(VcRoleService)}"); + uow.RemoveRange(missingRoles); + await uow.SaveChangesAsync(); + } } } -} + + public void AddVcRole(ulong guildId, IRole role, ulong vcId) + { + if (role is null) + throw new ArgumentNullException(nameof(role)); + + var guildVcRoles = VcRoles.GetOrAdd(guildId, new ConcurrentDictionary()); + + guildVcRoles.AddOrUpdate(vcId, role, (key, old) => role); + using (var uow = _db.GetDbContext()) + { + var conf = uow.GuildConfigsForId(guildId, set => set.Include(x => x.VcRoleInfos)); + var toDelete = conf.VcRoleInfos.FirstOrDefault(x => x.VoiceChannelId == vcId); // remove old one + if(toDelete != null) + { + uow.Remove(toDelete); + } + conf.VcRoleInfos.Add(new VcRoleInfo() + { + VoiceChannelId = vcId, + RoleId = role.Id, + }); // add new one + uow.SaveChanges(); + } + } + + public bool RemoveVcRole(ulong guildId, ulong vcId) + { + if (!VcRoles.TryGetValue(guildId, out var guildVcRoles)) + return false; + + if (!guildVcRoles.TryRemove(vcId, out _)) + return false; + + using (var uow = _db.GetDbContext()) + { + var conf = uow.GuildConfigsForId(guildId, set => set.Include(x => x.VcRoleInfos)); + var toRemove = conf.VcRoleInfos.Where(x => x.VoiceChannelId == vcId).ToList(); + uow.RemoveRange(toRemove); + uow.SaveChanges(); + } + + return true; + } + + private Task ClientOnUserVoiceStateUpdated(SocketUser usr, SocketVoiceState oldState, + SocketVoiceState newState) + { + + var gusr = usr as SocketGuildUser; + if (gusr is null) + return Task.CompletedTask; + + var oldVc = oldState.VoiceChannel; + var newVc = newState.VoiceChannel; + var _ = Task.Run(() => + { + try + { + if (oldVc != newVc) + { + ulong guildId; + guildId = newVc?.Guild.Id ?? oldVc.Guild.Id; + + if (VcRoles.TryGetValue(guildId, out ConcurrentDictionary guildVcRoles)) + { + //remove old + if (oldVc != null && guildVcRoles.TryGetValue(oldVc.Id, out IRole role)) + { + Assign(false, gusr, role); + } + //add new + if (newVc != null && guildVcRoles.TryGetValue(newVc.Id, out role)) + { + Assign(true, gusr, role); + } + + } + } + } + catch (Exception ex) + { + Log.Warning(ex, "Error in VcRoleService VoiceStateUpdate"); + } + }); + return Task.CompletedTask; + } + + private void Assign(bool v, SocketGuildUser gusr, IRole role) + { + var queue = ToAssign.GetOrAdd(gusr.Guild.Id, new ConcurrentQueue<(bool, IGuildUser, IRole)>()); + queue.Enqueue((v, gusr, role)); + } +} \ No newline at end of file diff --git a/src/NadekoBot/Modules/Administration/TimeZoneCommands.cs b/src/NadekoBot/Modules/Administration/TimeZoneCommands.cs index 0aa6d935e..77e57619b 100644 --- a/src/NadekoBot/Modules/Administration/TimeZoneCommands.cs +++ b/src/NadekoBot/Modules/Administration/TimeZoneCommands.cs @@ -1,90 +1,87 @@ using Discord; using Discord.Commands; using NadekoBot.Extensions; -using System; -using System.Linq; using System.Threading.Tasks; using NadekoBot.Common.Attributes; using NadekoBot.Modules.Administration.Services; -namespace NadekoBot.Modules.Administration +namespace NadekoBot.Modules.Administration; + +public partial class Administration { - public partial class Administration + [Group] + public class TimeZoneCommands : NadekoSubmodule { - [Group] - public class TimeZoneCommands : NadekoSubmodule + [NadekoCommand, Aliases] + [RequireContext(ContextType.Guild)] + public async Task Timezones(int page = 1) { - [NadekoCommand, Aliases] - [RequireContext(ContextType.Guild)] - public async Task Timezones(int page = 1) - { - page--; + page--; - if (page < 0 || page > 20) - return; + if (page < 0 || page > 20) + return; - var timezones = TimeZoneInfo.GetSystemTimeZones() - .OrderBy(x => x.BaseUtcOffset) - .ToArray(); - var timezonesPerPage = 20; + var timezones = TimeZoneInfo.GetSystemTimeZones() + .OrderBy(x => x.BaseUtcOffset) + .ToArray(); + var timezonesPerPage = 20; - var curTime = DateTimeOffset.UtcNow; + var curTime = DateTimeOffset.UtcNow; - var i = 0; - var timezoneStrings = timezones - .Select(x => (x, ++i % 2 == 0)) - .Select(data => - { - var (tzInfo, flip) = data; - var nameStr = $"{tzInfo.Id,-30}"; - var offset = curTime.ToOffset(tzInfo.GetUtcOffset(curTime)).ToString("zzz"); - if (flip) - { - return $"{offset} {Format.Code(nameStr)}"; - } - else - { - return $"{Format.Code(offset)} {nameStr}"; - } - }); - - - - await ctx.SendPaginatedConfirmAsync(page, - (curPage) => _eb.Create() - .WithOkColor() - .WithTitle(GetText(strs.timezones_available)) - .WithDescription(string.Join("\n", timezoneStrings - .Skip(curPage * timezonesPerPage) - .Take(timezonesPerPage))), - timezones.Length, timezonesPerPage).ConfigureAwait(false); - } - - [NadekoCommand, Aliases] - [RequireContext(ContextType.Guild)] - public async Task Timezone() - { - await ReplyConfirmLocalizedAsync(strs.timezone_guild(_service.GetTimeZoneOrUtc(ctx.Guild.Id))).ConfigureAwait(false); - } - - [NadekoCommand, Aliases] - [RequireContext(ContextType.Guild)] - [UserPerm(GuildPerm.Administrator)] - public async Task Timezone([Leftover] string id) - { - TimeZoneInfo tz; - try { tz = TimeZoneInfo.FindSystemTimeZoneById(id); } catch { tz = null; } - - - if (tz is null) + var i = 0; + var timezoneStrings = timezones + .Select(x => (x, ++i % 2 == 0)) + .Select(data => { - await ReplyErrorLocalizedAsync(strs.timezone_not_found).ConfigureAwait(false); - return; - } - _service.SetTimeZone(ctx.Guild.Id, tz); + var (tzInfo, flip) = data; + var nameStr = $"{tzInfo.Id,-30}"; + var offset = curTime.ToOffset(tzInfo.GetUtcOffset(curTime)).ToString("zzz"); + if (flip) + { + return $"{offset} {Format.Code(nameStr)}"; + } + else + { + return $"{Format.Code(offset)} {nameStr}"; + } + }); + + + + await ctx.SendPaginatedConfirmAsync(page, + (curPage) => _eb.Create() + .WithOkColor() + .WithTitle(GetText(strs.timezones_available)) + .WithDescription(string.Join("\n", timezoneStrings + .Skip(curPage * timezonesPerPage) + .Take(timezonesPerPage))), + timezones.Length, timezonesPerPage).ConfigureAwait(false); + } - await SendConfirmAsync(tz.ToString()).ConfigureAwait(false); + [NadekoCommand, Aliases] + [RequireContext(ContextType.Guild)] + public async Task Timezone() + { + await ReplyConfirmLocalizedAsync(strs.timezone_guild(_service.GetTimeZoneOrUtc(ctx.Guild.Id))).ConfigureAwait(false); + } + + [NadekoCommand, Aliases] + [RequireContext(ContextType.Guild)] + [UserPerm(GuildPerm.Administrator)] + public async Task Timezone([Leftover] string id) + { + TimeZoneInfo tz; + try { tz = TimeZoneInfo.FindSystemTimeZoneById(id); } catch { tz = null; } + + + if (tz is null) + { + await ReplyErrorLocalizedAsync(strs.timezone_not_found).ConfigureAwait(false); + return; } + _service.SetTimeZone(ctx.Guild.Id, tz); + + await SendConfirmAsync(tz.ToString()).ConfigureAwait(false); } } -} +} \ No newline at end of file diff --git a/src/NadekoBot/Modules/Administration/UserPunishCommands.cs b/src/NadekoBot/Modules/Administration/UserPunishCommands.cs index 344a2fef0..6fae982d0 100644 --- a/src/NadekoBot/Modules/Administration/UserPunishCommands.cs +++ b/src/NadekoBot/Modules/Administration/UserPunishCommands.cs @@ -8,525 +8,453 @@ using NadekoBot.Common.TypeReaders.Models; using NadekoBot.Services.Database.Models; using NadekoBot.Extensions; using NadekoBot.Modules.Administration.Services; -using System; -using System.Collections.Generic; -using System.Linq; using System.Threading.Tasks; using NadekoBot.Modules.Permissions.Services; -using NadekoBot.Modules.Searches.Common; -using Serilog; -namespace NadekoBot.Modules.Administration +namespace NadekoBot.Modules.Administration; + +public partial class Administration { - public partial class Administration + [Group] + public class UserPunishCommands : NadekoSubmodule { - [Group] - public class UserPunishCommands : NadekoSubmodule + private readonly MuteService _mute; + private readonly BlacklistService _blacklistService; + + public UserPunishCommands(MuteService mute, BlacklistService blacklistService) { - private readonly MuteService _mute; - private readonly BlacklistService _blacklistService; + _mute = mute; + _blacklistService = blacklistService; + } - public UserPunishCommands(MuteService mute, BlacklistService blacklistService) + private async Task CheckRoleHierarchy(IGuildUser target) + { + var curUser = ((SocketGuild) ctx.Guild).CurrentUser; + var ownerId = ctx.Guild.OwnerId; + var modMaxRole = ((IGuildUser) ctx.User).GetRoles().Max(r => r.Position); + var targetMaxRole = target.GetRoles().Max(r => r.Position); + var botMaxRole = curUser.GetRoles().Max(r => r.Position); + // bot can't punish a user who is higher in the hierarchy. Discord will return 403 + // moderator can be owner, in which case role hierarchy doesn't matter + // otherwise, moderator has to have a higher role + if ((botMaxRole <= targetMaxRole || (ctx.User.Id != ownerId && targetMaxRole >= modMaxRole)) || target.Id == ownerId) { - _mute = mute; - _blacklistService = blacklistService; + await ReplyErrorLocalizedAsync(strs.hierarchy); + return false; } - - private async Task CheckRoleHierarchy(IGuildUser target) - { - var curUser = ((SocketGuild) ctx.Guild).CurrentUser; - var ownerId = ctx.Guild.OwnerId; - var modMaxRole = ((IGuildUser) ctx.User).GetRoles().Max(r => r.Position); - var targetMaxRole = target.GetRoles().Max(r => r.Position); - var botMaxRole = curUser.GetRoles().Max(r => r.Position); - // bot can't punish a user who is higher in the hierarchy. Discord will return 403 - // moderator can be owner, in which case role hierarchy doesn't matter - // otherwise, moderator has to have a higher role - if ((botMaxRole <= targetMaxRole || (ctx.User.Id != ownerId && targetMaxRole >= modMaxRole)) || target.Id == ownerId) - { - await ReplyErrorLocalizedAsync(strs.hierarchy); - return false; - } - return true; - } + return true; + } - [NadekoCommand, Aliases] - [RequireContext(ContextType.Guild)] - [UserPerm(GuildPerm.BanMembers)] - public Task Warn(IGuildUser user, [Leftover] string reason = null) - => Warn(1, user, reason); + [NadekoCommand, Aliases] + [RequireContext(ContextType.Guild)] + [UserPerm(GuildPerm.BanMembers)] + public Task Warn(IGuildUser user, [Leftover] string reason = null) + => Warn(1, user, reason); - [NadekoCommand, Aliases] - [RequireContext(ContextType.Guild)] - [UserPerm(GuildPerm.BanMembers)] - public async Task Warn(int weight, IGuildUser user, [Leftover] string reason = null) - { - if (weight <= 0) - return; + [NadekoCommand, Aliases] + [RequireContext(ContextType.Guild)] + [UserPerm(GuildPerm.BanMembers)] + public async Task Warn(int weight, IGuildUser user, [Leftover] string reason = null) + { + if (weight <= 0) + return; - if (!await CheckRoleHierarchy(user)) - return; + if (!await CheckRoleHierarchy(user)) + return; - var dmFailed = false; - try - { - await (await user.GetOrCreateDMChannelAsync().ConfigureAwait(false)).EmbedAsync(_eb.Create().WithErrorColor() - .WithDescription(GetText(strs.warned_on(ctx.Guild.ToString()))) - .AddField(GetText(strs.moderator), ctx.User.ToString()) - .AddField(GetText(strs.reason), reason ?? "-")) - .ConfigureAwait(false); - } - catch - { - dmFailed = true; - } - - WarningPunishment punishment; - try - { - punishment = await _service.Warn(ctx.Guild, user.Id, ctx.User, weight, reason).ConfigureAwait(false); - } - catch (Exception ex) - { - Log.Warning(ex.Message); - var errorEmbed = _eb.Create() - .WithErrorColor() - .WithDescription(GetText(strs.cant_apply_punishment)); - - if (dmFailed) - { - errorEmbed.WithFooter("⚠️ " + GetText(strs.unable_to_dm_user)); - } - - await ctx.Channel.EmbedAsync(errorEmbed); - return; - } - - var embed = _eb.Create() - .WithOkColor(); - if (punishment is null) - { - embed.WithDescription(GetText(strs.user_warned(Format.Bold(user.ToString())))); - } - else - { - embed.WithDescription(GetText(strs.user_warned_and_punished(Format.Bold(user.ToString()), - Format.Bold(punishment.Punishment.ToString())))); - } - - if (dmFailed) - { - embed.WithFooter("⚠️ " + GetText(strs.unable_to_dm_user)); - } - - await ctx.Channel.EmbedAsync(embed); - } - - public class WarnExpireOptions : INadekoCommandOptions + var dmFailed = false; + try { - [Option('d', "delete", Default = false, HelpText = "Delete warnings instead of clearing them.")] - public bool Delete { get; set; } = false; - public void NormalizeOptions() - { - - } - } - - [NadekoCommand, Aliases] - [RequireContext(ContextType.Guild)] - [UserPerm(GuildPerm.Administrator)] - [NadekoOptions(typeof(WarnExpireOptions))] - [Priority(1)] - public async Task WarnExpire() - { - var expireDays = await _service.GetWarnExpire(ctx.Guild.Id); - - if (expireDays == 0) - await ReplyConfirmLocalizedAsync(strs.warns_dont_expire); - else - await ReplyErrorLocalizedAsync(strs.warns_expire_in(expireDays)); - } - - [NadekoCommand, Aliases] - [RequireContext(ContextType.Guild)] - [UserPerm(GuildPerm.Administrator)] - [NadekoOptions(typeof(WarnExpireOptions))] - [Priority(2)] - public async Task WarnExpire(int days, params string[] args) - { - if (days < 0 || days > 366) - return; - - var opts = OptionsParser.ParseFrom(args); - - await ctx.Channel.TriggerTypingAsync().ConfigureAwait(false); - - await _service.WarnExpireAsync(ctx.Guild.Id, days, opts.Delete).ConfigureAwait(false); - if(days == 0) - { - await ReplyConfirmLocalizedAsync(strs.warn_expire_reset).ConfigureAwait(false); - return; - } - - if (opts.Delete) - { - await ReplyConfirmLocalizedAsync(strs.warn_expire_set_delete(Format.Bold(days.ToString()))).ConfigureAwait(false); - } - else - { - await ReplyConfirmLocalizedAsync(strs.warn_expire_set_clear(Format.Bold(days.ToString()))).ConfigureAwait(false); - } - } - - [NadekoCommand, Aliases] - [RequireContext(ContextType.Guild)] - [UserPerm(GuildPerm.BanMembers)] - [Priority(2)] - public Task Warnlog(int page, [Leftover] IGuildUser user = null) - { - user ??= (IGuildUser) ctx.User; - - return Warnlog(page, user.Id); - } - - [NadekoCommand, Aliases] - [RequireContext(ContextType.Guild)] - [Priority(3)] - public Task Warnlog(IGuildUser user = null) - { - user ??= (IGuildUser) ctx.User; - - return ctx.User.Id == user.Id || ((IGuildUser)ctx.User).GuildPermissions.BanMembers ? Warnlog(user.Id) : Task.CompletedTask; - } - - [NadekoCommand, Aliases] - [RequireContext(ContextType.Guild)] - [UserPerm(GuildPerm.BanMembers)] - [Priority(0)] - public Task Warnlog(int page, ulong userId) - => InternalWarnlog(userId, page - 1); - - [NadekoCommand, Aliases] - [RequireContext(ContextType.Guild)] - [UserPerm(GuildPerm.BanMembers)] - [Priority(1)] - public Task Warnlog(ulong userId) - => InternalWarnlog(userId, 0); - - private async Task InternalWarnlog(ulong userId, int inputPage) - { - if (inputPage < 0) - return; - - var allWarnings = _service.UserWarnings(ctx.Guild.Id, userId); - - await ctx.SendPaginatedConfirmAsync(inputPage, page => - { - var warnings = allWarnings - .Skip(page * 9) - .Take(9) - .ToArray(); - - var user = (ctx.Guild as SocketGuild)?.GetUser(userId)?.ToString() ?? userId.ToString(); - var embed = _eb.Create() - .WithOkColor() - .WithTitle(GetText(strs.warnlog_for(user))); - - if (!warnings.Any()) - { - embed.WithDescription(GetText(strs.warnings_none)); - } - else - { - var descText = GetText(strs.warn_count( - Format.Bold(warnings.Where(x => !x.Forgiven).Sum(x => x.Weight).ToString()), - Format.Bold(warnings.Sum(x => x.Weight).ToString()))); - - embed.WithDescription(descText); - - var i = page * 9; - foreach (var w in warnings) - { - i++; - var name = GetText(strs.warned_on_by( - w.DateAdded?.ToString("dd.MM.yyy"), - w.DateAdded?.ToString("HH:mm"), - w.Moderator)); - - if (w.Forgiven) - name = $"{Format.Strikethrough(name)} {GetText(strs.warn_cleared_by(w.ForgivenBy))}"; - - - embed.AddField($"#`{i}` " + name, - Format.Code(GetText(strs.warn_weight(w.Weight))) + - '\n' + - w.Reason.TrimTo(1000)); - } - } - - return embed; - }, allWarnings.Length, 9); - } - - [NadekoCommand, Aliases] - [RequireContext(ContextType.Guild)] - [UserPerm(GuildPerm.BanMembers)] - public async Task WarnlogAll(int page = 1) - { - if (--page < 0) - return; - var warnings = _service.WarnlogAll(ctx.Guild.Id); - - await ctx.SendPaginatedConfirmAsync(page, (curPage) => - { - var ws = warnings.Skip(curPage * 15) - .Take(15) - .ToArray() - .Select(x => - { - var all = x.Count(); - var forgiven = x.Count(y => y.Forgiven); - var total = all - forgiven; - var usr = ((SocketGuild)ctx.Guild).GetUser(x.Key); - return (usr?.ToString() ?? x.Key.ToString()) + $" | {total} ({all} - {forgiven})"; - }); - - return _eb.Create().WithOkColor() - .WithTitle(GetText(strs.warnings_list)) - .WithDescription(string.Join("\n", ws)); - }, warnings.Length, 15).ConfigureAwait(false); - } - - [NadekoCommand, Aliases] - [RequireContext(ContextType.Guild)] - [UserPerm(GuildPerm.BanMembers)] - public Task Warnclear(IGuildUser user, int index = 0) - => Warnclear(user.Id, index); - - [NadekoCommand, Aliases] - [RequireContext(ContextType.Guild)] - [UserPerm(GuildPerm.BanMembers)] - public async Task Warnclear(ulong userId, int index = 0) - { - if (index < 0) - return; - var success = await _service.WarnClearAsync(ctx.Guild.Id, userId, index, ctx.User.ToString()); - var userStr = Format.Bold((ctx.Guild as SocketGuild)?.GetUser(userId)?.ToString() ?? userId.ToString()); - if (index == 0) - { - await ReplyErrorLocalizedAsync(strs.warnings_cleared(userStr)); - } - else - { - if (success) - { - await ReplyConfirmLocalizedAsync(strs.warning_cleared(Format.Bold(index.ToString()), userStr)); - } - else - { - await ReplyErrorLocalizedAsync(strs.warning_clear_fail).ConfigureAwait(false); - } - } - } - - public enum AddRole - { - AddRole - } - - [NadekoCommand, Aliases] - [RequireContext(ContextType.Guild)] - [UserPerm(GuildPerm.BanMembers)] - [Priority(1)] - public async Task WarnPunish(int number, AddRole _, IRole role, StoopidTime time = null) - { - var punish = PunishmentAction.AddRole; - - if (ctx.Guild.OwnerId != ctx.User.Id && - role.Position >= ((IGuildUser)ctx.User).GetRoles().Max(x => x.Position)) - { - await ReplyErrorLocalizedAsync(strs.role_too_high); - return; - } - - var success = _service.WarnPunish(ctx.Guild.Id, number, punish, time, role); - - if (!success) - return; - - if (time is null) - { - await ReplyConfirmLocalizedAsync(strs.warn_punish_set( - Format.Bold(punish.ToString()), - Format.Bold(number.ToString()))); - } - else - { - await ReplyConfirmLocalizedAsync(strs.warn_punish_set_timed( - Format.Bold(punish.ToString()), - Format.Bold(number.ToString()), - Format.Bold(time.Input))); - } - } - - [NadekoCommand, Aliases] - [RequireContext(ContextType.Guild)] - [UserPerm(GuildPerm.BanMembers)] - public async Task WarnPunish(int number, PunishmentAction punish, StoopidTime time = null) - { - // this should never happen. Addrole has its own method with higher priority - if (punish == PunishmentAction.AddRole) - return; - - var success = _service.WarnPunish(ctx.Guild.Id, number, punish, time); - - if (!success) - return; - - if (time is null) - { - await ReplyConfirmLocalizedAsync(strs.warn_punish_set( - Format.Bold(punish.ToString()), - Format.Bold(number.ToString()))); - } - else - { - await ReplyConfirmLocalizedAsync(strs.warn_punish_set_timed( - Format.Bold(punish.ToString()), - Format.Bold(number.ToString()), - Format.Bold(time.Input))); - } - } - - [NadekoCommand, Aliases] - [RequireContext(ContextType.Guild)] - [UserPerm(GuildPerm.BanMembers)] - public async Task WarnPunish(int number) - { - if (!_service.WarnPunishRemove(ctx.Guild.Id, number)) - { - return; - } - - await ReplyConfirmLocalizedAsync(strs.warn_punish_rem( - Format.Bold(number.ToString()))); - } - - [NadekoCommand, Aliases] - [RequireContext(ContextType.Guild)] - public async Task WarnPunishList() - { - var ps = _service.WarnPunishList(ctx.Guild.Id); - - string list; - if (ps.Any()) - { - - list = string.Join("\n", ps.Select(x => $"{x.Count} -> {x.Punishment} {(x.Punishment == PunishmentAction.AddRole ? $"<@&{x.RoleId}>" : "")} {(x.Time <= 0 ? "" : x.Time.ToString() + "m")} ")); - } - else - { - list = GetText(strs.warnpl_none); - } - await SendConfirmAsync( - GetText(strs.warn_punish_list), - list).ConfigureAwait(false); - } - - [NadekoCommand, Aliases] - [RequireContext(ContextType.Guild)] - [UserPerm(GuildPerm.BanMembers)] - [BotPerm(GuildPerm.BanMembers)] - [Priority(1)] - public async Task Ban(StoopidTime time, IUser user, [Leftover] string msg = null) - { - if (time.Time > TimeSpan.FromDays(49)) - return; - - var guildUser = await ((DiscordSocketClient)Context.Client).Rest.GetGuildUserAsync(ctx.Guild.Id, user.Id); - - if (guildUser != null && !await CheckRoleHierarchy(guildUser)) - return; - - var dmFailed = false; - - if (guildUser != null) - { - try - { - var defaultMessage = GetText(strs.bandm(Format.Bold(ctx.Guild.Name), msg)); - var embed = _service.GetBanUserDmEmbed(Context, guildUser, defaultMessage, msg, time.Time); - if (embed is not null) - { - var userChannel = await guildUser.GetOrCreateDMChannelAsync(); - await userChannel.SendAsync(embed); - } - } - catch - { - dmFailed = true; - } - } - - await _mute.TimedBan(ctx.Guild, user, time.Time, (ctx.User.ToString() + " | " + msg).TrimTo(512)).ConfigureAwait(false); - var toSend = _eb.Create().WithOkColor() - .WithTitle("⛔️ " + GetText(strs.banned_user)) - .AddField(GetText(strs.username), user.ToString(), true) - .AddField("ID", user.Id.ToString(), true) - .AddField(GetText(strs.duration), $"{time.Time.Days}d {time.Time.Hours}h {time.Time.Minutes}m", true); - - if (dmFailed) - { - toSend.WithFooter("⚠️ " + GetText(strs.unable_to_dm_user)); - } - - await ctx.Channel.EmbedAsync(toSend) + await (await user.GetOrCreateDMChannelAsync().ConfigureAwait(false)).EmbedAsync(_eb.Create().WithErrorColor() + .WithDescription(GetText(strs.warned_on(ctx.Guild.ToString()))) + .AddField(GetText(strs.moderator), ctx.User.ToString()) + .AddField(GetText(strs.reason), reason ?? "-")) .ConfigureAwait(false); } - - [NadekoCommand, Aliases] - [RequireContext(ContextType.Guild)] - [UserPerm(GuildPerm.BanMembers)] - [BotPerm(GuildPerm.BanMembers)] - [Priority(0)] - public async Task Ban(ulong userId, [Leftover] string msg = null) + catch { - var user = await ((DiscordSocketClient)Context.Client).Rest.GetGuildUserAsync(ctx.Guild.Id, userId); - if (user is null) - { - await ctx.Guild.AddBanAsync(userId, 7, (ctx.User.ToString() + " | " + msg).TrimTo(512)); + dmFailed = true; + } + + WarningPunishment punishment; + try + { + punishment = await _service.Warn(ctx.Guild, user.Id, ctx.User, weight, reason).ConfigureAwait(false); + } + catch (Exception ex) + { + Log.Warning(ex.Message); + var errorEmbed = _eb.Create() + .WithErrorColor() + .WithDescription(GetText(strs.cant_apply_punishment)); - await ctx.Channel.EmbedAsync(_eb.Create().WithOkColor() - .WithTitle("⛔️ " + GetText(strs.banned_user)) - .AddField("ID", userId.ToString(), true)) - .ConfigureAwait(false); + if (dmFailed) + { + errorEmbed.WithFooter("⚠️ " + GetText(strs.unable_to_dm_user)); + } + + await ctx.Channel.EmbedAsync(errorEmbed); + return; + } + + var embed = _eb.Create() + .WithOkColor(); + if (punishment is null) + { + embed.WithDescription(GetText(strs.user_warned(Format.Bold(user.ToString())))); + } + else + { + embed.WithDescription(GetText(strs.user_warned_and_punished(Format.Bold(user.ToString()), + Format.Bold(punishment.Punishment.ToString())))); + } + + if (dmFailed) + { + embed.WithFooter("⚠️ " + GetText(strs.unable_to_dm_user)); + } + + await ctx.Channel.EmbedAsync(embed); + } + + public class WarnExpireOptions : INadekoCommandOptions + { + [Option('d', "delete", Default = false, HelpText = "Delete warnings instead of clearing them.")] + public bool Delete { get; set; } = false; + public void NormalizeOptions() + { + + } + } + + [NadekoCommand, Aliases] + [RequireContext(ContextType.Guild)] + [UserPerm(GuildPerm.Administrator)] + [NadekoOptions(typeof(WarnExpireOptions))] + [Priority(1)] + public async Task WarnExpire() + { + var expireDays = await _service.GetWarnExpire(ctx.Guild.Id); + + if (expireDays == 0) + await ReplyConfirmLocalizedAsync(strs.warns_dont_expire); + else + await ReplyErrorLocalizedAsync(strs.warns_expire_in(expireDays)); + } + + [NadekoCommand, Aliases] + [RequireContext(ContextType.Guild)] + [UserPerm(GuildPerm.Administrator)] + [NadekoOptions(typeof(WarnExpireOptions))] + [Priority(2)] + public async Task WarnExpire(int days, params string[] args) + { + if (days < 0 || days > 366) + return; + + var opts = OptionsParser.ParseFrom(args); + + await ctx.Channel.TriggerTypingAsync().ConfigureAwait(false); + + await _service.WarnExpireAsync(ctx.Guild.Id, days, opts.Delete).ConfigureAwait(false); + if(days == 0) + { + await ReplyConfirmLocalizedAsync(strs.warn_expire_reset).ConfigureAwait(false); + return; + } + + if (opts.Delete) + { + await ReplyConfirmLocalizedAsync(strs.warn_expire_set_delete(Format.Bold(days.ToString()))).ConfigureAwait(false); + } + else + { + await ReplyConfirmLocalizedAsync(strs.warn_expire_set_clear(Format.Bold(days.ToString()))).ConfigureAwait(false); + } + } + + [NadekoCommand, Aliases] + [RequireContext(ContextType.Guild)] + [UserPerm(GuildPerm.BanMembers)] + [Priority(2)] + public Task Warnlog(int page, [Leftover] IGuildUser user = null) + { + user ??= (IGuildUser) ctx.User; + + return Warnlog(page, user.Id); + } + + [NadekoCommand, Aliases] + [RequireContext(ContextType.Guild)] + [Priority(3)] + public Task Warnlog(IGuildUser user = null) + { + user ??= (IGuildUser) ctx.User; + + return ctx.User.Id == user.Id || ((IGuildUser)ctx.User).GuildPermissions.BanMembers ? Warnlog(user.Id) : Task.CompletedTask; + } + + [NadekoCommand, Aliases] + [RequireContext(ContextType.Guild)] + [UserPerm(GuildPerm.BanMembers)] + [Priority(0)] + public Task Warnlog(int page, ulong userId) + => InternalWarnlog(userId, page - 1); + + [NadekoCommand, Aliases] + [RequireContext(ContextType.Guild)] + [UserPerm(GuildPerm.BanMembers)] + [Priority(1)] + public Task Warnlog(ulong userId) + => InternalWarnlog(userId, 0); + + private async Task InternalWarnlog(ulong userId, int inputPage) + { + if (inputPage < 0) + return; + + var allWarnings = _service.UserWarnings(ctx.Guild.Id, userId); + + await ctx.SendPaginatedConfirmAsync(inputPage, page => + { + var warnings = allWarnings + .Skip(page * 9) + .Take(9) + .ToArray(); + + var user = (ctx.Guild as SocketGuild)?.GetUser(userId)?.ToString() ?? userId.ToString(); + var embed = _eb.Create() + .WithOkColor() + .WithTitle(GetText(strs.warnlog_for(user))); + + if (!warnings.Any()) + { + embed.WithDescription(GetText(strs.warnings_none)); } else { - await Ban(user, msg); + var descText = GetText(strs.warn_count( + Format.Bold(warnings.Where(x => !x.Forgiven).Sum(x => x.Weight).ToString()), + Format.Bold(warnings.Sum(x => x.Weight).ToString()))); + + embed.WithDescription(descText); + + var i = page * 9; + foreach (var w in warnings) + { + i++; + var name = GetText(strs.warned_on_by( + w.DateAdded?.ToString("dd.MM.yyy"), + w.DateAdded?.ToString("HH:mm"), + w.Moderator)); + + if (w.Forgiven) + name = $"{Format.Strikethrough(name)} {GetText(strs.warn_cleared_by(w.ForgivenBy))}"; + + + embed.AddField($"#`{i}` " + name, + Format.Code(GetText(strs.warn_weight(w.Weight))) + + '\n' + + w.Reason.TrimTo(1000)); + } + } + + return embed; + }, allWarnings.Length, 9); + } + + [NadekoCommand, Aliases] + [RequireContext(ContextType.Guild)] + [UserPerm(GuildPerm.BanMembers)] + public async Task WarnlogAll(int page = 1) + { + if (--page < 0) + return; + var warnings = _service.WarnlogAll(ctx.Guild.Id); + + await ctx.SendPaginatedConfirmAsync(page, (curPage) => + { + var ws = warnings.Skip(curPage * 15) + .Take(15) + .ToArray() + .Select(x => + { + var all = x.Count(); + var forgiven = x.Count(y => y.Forgiven); + var total = all - forgiven; + var usr = ((SocketGuild)ctx.Guild).GetUser(x.Key); + return (usr?.ToString() ?? x.Key.ToString()) + $" | {total} ({all} - {forgiven})"; + }); + + return _eb.Create().WithOkColor() + .WithTitle(GetText(strs.warnings_list)) + .WithDescription(string.Join("\n", ws)); + }, warnings.Length, 15).ConfigureAwait(false); + } + + [NadekoCommand, Aliases] + [RequireContext(ContextType.Guild)] + [UserPerm(GuildPerm.BanMembers)] + public Task Warnclear(IGuildUser user, int index = 0) + => Warnclear(user.Id, index); + + [NadekoCommand, Aliases] + [RequireContext(ContextType.Guild)] + [UserPerm(GuildPerm.BanMembers)] + public async Task Warnclear(ulong userId, int index = 0) + { + if (index < 0) + return; + var success = await _service.WarnClearAsync(ctx.Guild.Id, userId, index, ctx.User.ToString()); + var userStr = Format.Bold((ctx.Guild as SocketGuild)?.GetUser(userId)?.ToString() ?? userId.ToString()); + if (index == 0) + { + await ReplyErrorLocalizedAsync(strs.warnings_cleared(userStr)); + } + else + { + if (success) + { + await ReplyConfirmLocalizedAsync(strs.warning_cleared(Format.Bold(index.ToString()), userStr)); + } + else + { + await ReplyErrorLocalizedAsync(strs.warning_clear_fail).ConfigureAwait(false); } } + } - [NadekoCommand, Aliases] - [RequireContext(ContextType.Guild)] - [UserPerm(GuildPerm.BanMembers)] - [BotPerm(GuildPerm.BanMembers)] - [Priority(2)] - public async Task Ban(IGuildUser user, [Leftover] string msg = null) + public enum AddRole + { + AddRole + } + + [NadekoCommand, Aliases] + [RequireContext(ContextType.Guild)] + [UserPerm(GuildPerm.BanMembers)] + [Priority(1)] + public async Task WarnPunish(int number, AddRole _, IRole role, StoopidTime time = null) + { + var punish = PunishmentAction.AddRole; + + if (ctx.Guild.OwnerId != ctx.User.Id && + role.Position >= ((IGuildUser)ctx.User).GetRoles().Max(x => x.Position)) { - if (!await CheckRoleHierarchy(user)) - return; + await ReplyErrorLocalizedAsync(strs.role_too_high); + return; + } + + var success = _service.WarnPunish(ctx.Guild.Id, number, punish, time, role); - var dmFailed = false; + if (!success) + return; + if (time is null) + { + await ReplyConfirmLocalizedAsync(strs.warn_punish_set( + Format.Bold(punish.ToString()), + Format.Bold(number.ToString()))); + } + else + { + await ReplyConfirmLocalizedAsync(strs.warn_punish_set_timed( + Format.Bold(punish.ToString()), + Format.Bold(number.ToString()), + Format.Bold(time.Input))); + } + } + + [NadekoCommand, Aliases] + [RequireContext(ContextType.Guild)] + [UserPerm(GuildPerm.BanMembers)] + public async Task WarnPunish(int number, PunishmentAction punish, StoopidTime time = null) + { + // this should never happen. Addrole has its own method with higher priority + if (punish == PunishmentAction.AddRole) + return; + + var success = _service.WarnPunish(ctx.Guild.Id, number, punish, time); + + if (!success) + return; + + if (time is null) + { + await ReplyConfirmLocalizedAsync(strs.warn_punish_set( + Format.Bold(punish.ToString()), + Format.Bold(number.ToString()))); + } + else + { + await ReplyConfirmLocalizedAsync(strs.warn_punish_set_timed( + Format.Bold(punish.ToString()), + Format.Bold(number.ToString()), + Format.Bold(time.Input))); + } + } + + [NadekoCommand, Aliases] + [RequireContext(ContextType.Guild)] + [UserPerm(GuildPerm.BanMembers)] + public async Task WarnPunish(int number) + { + if (!_service.WarnPunishRemove(ctx.Guild.Id, number)) + { + return; + } + + await ReplyConfirmLocalizedAsync(strs.warn_punish_rem( + Format.Bold(number.ToString()))); + } + + [NadekoCommand, Aliases] + [RequireContext(ContextType.Guild)] + public async Task WarnPunishList() + { + var ps = _service.WarnPunishList(ctx.Guild.Id); + + string list; + if (ps.Any()) + { + + list = string.Join("\n", ps.Select(x => $"{x.Count} -> {x.Punishment} {(x.Punishment == PunishmentAction.AddRole ? $"<@&{x.RoleId}>" : "")} {(x.Time <= 0 ? "" : x.Time.ToString() + "m")} ")); + } + else + { + list = GetText(strs.warnpl_none); + } + await SendConfirmAsync( + GetText(strs.warn_punish_list), + list).ConfigureAwait(false); + } + + [NadekoCommand, Aliases] + [RequireContext(ContextType.Guild)] + [UserPerm(GuildPerm.BanMembers)] + [BotPerm(GuildPerm.BanMembers)] + [Priority(1)] + public async Task Ban(StoopidTime time, IUser user, [Leftover] string msg = null) + { + if (time.Time > TimeSpan.FromDays(49)) + return; + + var guildUser = await ((DiscordSocketClient)Context.Client).Rest.GetGuildUserAsync(ctx.Guild.Id, user.Id); + + if (guildUser != null && !await CheckRoleHierarchy(guildUser)) + return; + + var dmFailed = false; + + if (guildUser != null) + { try { var defaultMessage = GetText(strs.bandm(Format.Bold(ctx.Guild.Name), msg)); - var embed = _service.GetBanUserDmEmbed(Context, user, defaultMessage, msg, null); + var embed = _service.GetBanUserDmEmbed(Context, guildUser, defaultMessage, msg, time.Time); if (embed is not null) { - var userChannel = await user.GetOrCreateDMChannelAsync(); + var userChannel = await guildUser.GetOrCreateDMChannelAsync(); await userChannel.SendAsync(embed); } } @@ -534,377 +462,443 @@ namespace NadekoBot.Modules.Administration { dmFailed = true; } - - await ctx.Guild.AddBanAsync(user, 7, (ctx.User.ToString() + " | " + msg).TrimTo(512)).ConfigureAwait(false); - - var toSend = _eb.Create().WithOkColor() - .WithTitle("⛔️ " + GetText(strs.banned_user)) - .AddField(GetText(strs.username), user.ToString(), true) - .AddField("ID", user.Id.ToString(), true); - - if (dmFailed) - { - toSend.WithFooter("⚠️ " + GetText(strs.unable_to_dm_user)); - } - - await ctx.Channel.EmbedAsync(toSend) - .ConfigureAwait(false); } - [NadekoCommand, Aliases] - [RequireContext(ContextType.Guild)] - [UserPerm(GuildPerm.BanMembers)] - [BotPerm(GuildPerm.BanMembers)] - public async Task BanMessage([Leftover] string message = null) + await _mute.TimedBan(ctx.Guild, user, time.Time, (ctx.User.ToString() + " | " + msg).TrimTo(512)).ConfigureAwait(false); + var toSend = _eb.Create().WithOkColor() + .WithTitle("⛔️ " + GetText(strs.banned_user)) + .AddField(GetText(strs.username), user.ToString(), true) + .AddField("ID", user.Id.ToString(), true) + .AddField(GetText(strs.duration), $"{time.Time.Days}d {time.Time.Hours}h {time.Time.Minutes}m", true); + + if (dmFailed) { - if (message is null) + toSend.WithFooter("⚠️ " + GetText(strs.unable_to_dm_user)); + } + + await ctx.Channel.EmbedAsync(toSend) + .ConfigureAwait(false); + } + + [NadekoCommand, Aliases] + [RequireContext(ContextType.Guild)] + [UserPerm(GuildPerm.BanMembers)] + [BotPerm(GuildPerm.BanMembers)] + [Priority(0)] + public async Task Ban(ulong userId, [Leftover] string msg = null) + { + var user = await ((DiscordSocketClient)Context.Client).Rest.GetGuildUserAsync(ctx.Guild.Id, userId); + if (user is null) + { + await ctx.Guild.AddBanAsync(userId, 7, (ctx.User.ToString() + " | " + msg).TrimTo(512)); + + await ctx.Channel.EmbedAsync(_eb.Create().WithOkColor() + .WithTitle("⛔️ " + GetText(strs.banned_user)) + .AddField("ID", userId.ToString(), true)) + .ConfigureAwait(false); + } + else + { + await Ban(user, msg); + } + } + + [NadekoCommand, Aliases] + [RequireContext(ContextType.Guild)] + [UserPerm(GuildPerm.BanMembers)] + [BotPerm(GuildPerm.BanMembers)] + [Priority(2)] + public async Task Ban(IGuildUser user, [Leftover] string msg = null) + { + if (!await CheckRoleHierarchy(user)) + return; + + var dmFailed = false; + + try + { + var defaultMessage = GetText(strs.bandm(Format.Bold(ctx.Guild.Name), msg)); + var embed = _service.GetBanUserDmEmbed(Context, user, defaultMessage, msg, null); + if (embed is not null) { - var template = _service.GetBanTemplate(ctx.Guild.Id); - if (template is null) + var userChannel = await user.GetOrCreateDMChannelAsync(); + await userChannel.SendAsync(embed); + } + } + catch + { + dmFailed = true; + } + + await ctx.Guild.AddBanAsync(user, 7, (ctx.User.ToString() + " | " + msg).TrimTo(512)).ConfigureAwait(false); + + var toSend = _eb.Create().WithOkColor() + .WithTitle("⛔️ " + GetText(strs.banned_user)) + .AddField(GetText(strs.username), user.ToString(), true) + .AddField("ID", user.Id.ToString(), true); + + if (dmFailed) + { + toSend.WithFooter("⚠️ " + GetText(strs.unable_to_dm_user)); + } + + await ctx.Channel.EmbedAsync(toSend) + .ConfigureAwait(false); + } + + [NadekoCommand, Aliases] + [RequireContext(ContextType.Guild)] + [UserPerm(GuildPerm.BanMembers)] + [BotPerm(GuildPerm.BanMembers)] + public async Task BanMessage([Leftover] string message = null) + { + if (message is null) + { + var template = _service.GetBanTemplate(ctx.Guild.Id); + if (template is null) + { + await ReplyConfirmLocalizedAsync(strs.banmsg_default); + return; + } + + await SendConfirmAsync(template); + return; + } + + _service.SetBanTemplate(ctx.Guild.Id, message); + await ctx.OkAsync(); + } + + [NadekoCommand, Aliases] + [RequireContext(ContextType.Guild)] + [UserPerm(GuildPerm.BanMembers)] + [BotPerm(GuildPerm.BanMembers)] + public async Task BanMsgReset() + { + _service.SetBanTemplate(ctx.Guild.Id, null); + await ctx.OkAsync(); + } + + [NadekoCommand, Aliases] + [RequireContext(ContextType.Guild)] + [UserPerm(GuildPerm.BanMembers)] + [BotPerm(GuildPerm.BanMembers)] + [Priority(0)] + public Task BanMessageTest([Leftover] string reason = null) + => InternalBanMessageTest(reason, null); + + [NadekoCommand, Aliases] + [RequireContext(ContextType.Guild)] + [UserPerm(GuildPerm.BanMembers)] + [BotPerm(GuildPerm.BanMembers)] + [Priority(1)] + public Task BanMessageTest(StoopidTime duration, [Leftover] string reason = null) + => InternalBanMessageTest(reason, duration.Time); + + private async Task InternalBanMessageTest(string reason, TimeSpan? duration) + { + var dmChannel = await ctx.User.GetOrCreateDMChannelAsync(); + var defaultMessage = GetText(strs.bandm(Format.Bold(ctx.Guild.Name), reason)); + var embed = _service.GetBanUserDmEmbed(Context, + (IGuildUser)ctx.User, + defaultMessage, + reason, + duration); + + if (embed is null) + { + await ConfirmLocalizedAsync(strs.banmsg_disabled); + } + else + { + try + { + await dmChannel.SendAsync(embed); + } + catch (Exception) + { + await ReplyErrorLocalizedAsync(strs.unable_to_dm_user); + return; + } + + await ctx.OkAsync(); + } + } + + [NadekoCommand, Aliases] + [RequireContext(ContextType.Guild)] + [UserPerm(GuildPerm.BanMembers)] + [BotPerm(GuildPerm.BanMembers)] + public async Task Unban([Leftover] string user) + { + var bans = await ctx.Guild.GetBansAsync().ConfigureAwait(false); + + var bun = bans.FirstOrDefault(x => x.User.ToString().ToLowerInvariant() == user.ToLowerInvariant()); + + if (bun is null) + { + await ReplyErrorLocalizedAsync(strs.user_not_found).ConfigureAwait(false); + return; + } + + await UnbanInternal(bun.User).ConfigureAwait(false); + } + + [NadekoCommand, Aliases] + [RequireContext(ContextType.Guild)] + [UserPerm(GuildPerm.BanMembers)] + [BotPerm(GuildPerm.BanMembers)] + public async Task Unban(ulong userId) + { + var bans = await ctx.Guild.GetBansAsync().ConfigureAwait(false); + + var bun = bans.FirstOrDefault(x => x.User.Id == userId); + + if (bun is null) + { + await ReplyErrorLocalizedAsync(strs.user_not_found).ConfigureAwait(false); + return; + } + + await UnbanInternal(bun.User).ConfigureAwait(false); + } + + private async Task UnbanInternal(IUser user) + { + await ctx.Guild.RemoveBanAsync(user).ConfigureAwait(false); + + await ReplyConfirmLocalizedAsync(strs.unbanned_user(Format.Bold(user.ToString()))).ConfigureAwait(false); + } + + [NadekoCommand, Aliases] + [RequireContext(ContextType.Guild)] + [UserPerm(GuildPerm.KickMembers | GuildPerm.ManageMessages)] + [BotPerm(GuildPerm.BanMembers)] + public Task Softban(IGuildUser user, [Leftover] string msg = null) + => SoftbanInternal(user, msg); + + [NadekoCommand, Aliases] + [RequireContext(ContextType.Guild)] + [UserPerm(GuildPerm.KickMembers | GuildPerm.ManageMessages)] + [BotPerm(GuildPerm.BanMembers)] + public async Task Softban(ulong userId, [Leftover] string msg = null) + { + var user = await ((DiscordSocketClient)Context.Client).Rest.GetGuildUserAsync(ctx.Guild.Id, userId); + if (user is null) + return; + + await SoftbanInternal(user, msg); + } + + private async Task SoftbanInternal(IGuildUser user, [Leftover] string msg = null) + { + if (!await CheckRoleHierarchy(user)) + return; + + var dmFailed = false; + + try + { + await user.SendErrorAsync(_eb, GetText(strs.sbdm(Format.Bold(ctx.Guild.Name), msg))); + } + catch + { + dmFailed = true; + } + + await ctx.Guild.AddBanAsync(user, 7, ("Softban | " + ctx.User.ToString() + " | " + msg).TrimTo(512)).ConfigureAwait(false); + try { await ctx.Guild.RemoveBanAsync(user).ConfigureAwait(false); } + catch { await ctx.Guild.RemoveBanAsync(user).ConfigureAwait(false); } + + var toSend = _eb.Create().WithOkColor() + .WithTitle("☣ " + GetText(strs.sb_user)) + .AddField(GetText(strs.username), user.ToString(), true) + .AddField("ID", user.Id.ToString(), true); + + if (dmFailed) + { + toSend.WithFooter("⚠️ " + GetText(strs.unable_to_dm_user)); + } + + await ctx.Channel.EmbedAsync(toSend) + .ConfigureAwait(false); + } + + [NadekoCommand, Aliases] + [RequireContext(ContextType.Guild)] + [UserPerm(GuildPerm.KickMembers)] + [BotPerm(GuildPerm.KickMembers)] + [Priority(1)] + public Task Kick(IGuildUser user, [Leftover] string msg = null) + => KickInternal(user, msg); + + [NadekoCommand, Aliases] + [RequireContext(ContextType.Guild)] + [UserPerm(GuildPerm.KickMembers)] + [BotPerm(GuildPerm.KickMembers)] + [Priority(0)] + public async Task Kick(ulong userId, [Leftover] string msg = null) + { + var user = await ((DiscordSocketClient)Context.Client).Rest.GetGuildUserAsync(ctx.Guild.Id, userId); + if (user is null) + return; + + await KickInternal(user, msg); + } + + public async Task KickInternal(IGuildUser user, string msg = null) + { + if (!await CheckRoleHierarchy(user)) + return; + + var dmFailed = false; + + try + { + await user.SendErrorAsync(_eb, GetText(strs.kickdm(Format.Bold(ctx.Guild.Name), msg))) + .ConfigureAwait(false); + } + catch + { + dmFailed = true; + } + + await user.KickAsync((ctx.User.ToString() + " | " + msg).TrimTo(512)).ConfigureAwait(false); + + var toSend = _eb.Create().WithOkColor() + .WithTitle(GetText(strs.kicked_user)) + .AddField(GetText(strs.username), user.ToString(), true) + .AddField("ID", user.Id.ToString(), true); + + if (dmFailed) + { + toSend.WithFooter("⚠️ " + GetText(strs.unable_to_dm_user)); + } + + await ctx.Channel.EmbedAsync(toSend) + .ConfigureAwait(false); + } + + [NadekoCommand, Aliases] + [RequireContext(ContextType.Guild)] + [UserPerm(GuildPerm.BanMembers)] + [BotPerm(GuildPerm.BanMembers)] + [Ratelimit(30)] + public async Task MassBan(params string[] userStrings) + { + if (userStrings.Length == 0) + return; + + var missing = new List(); + var banning = new HashSet(); + + await ctx.Channel.TriggerTypingAsync(); + foreach (var userStr in userStrings) + { + if (ulong.TryParse(userStr, out var userId)) + { + IUser user = await ctx.Guild.GetUserAsync(userId) ?? + await ((DiscordSocketClient)Context.Client).Rest.GetGuildUserAsync(ctx.Guild.Id, userId); + + if (user is null) + { + // if IGuildUser is null, try to get IUser + user = await ((DiscordSocketClient)Context.Client).Rest.GetUserAsync(userId); + + // only add to missing if *still* null + if (user is null) + { + missing.Add(userStr); + continue; + } + + } + + //Hierachy checks only if the user is in the guild + if (user is IGuildUser gu && !await CheckRoleHierarchy(gu)) { - await ReplyConfirmLocalizedAsync(strs.banmsg_default); return; } - await SendConfirmAsync(template); - return; - } - - _service.SetBanTemplate(ctx.Guild.Id, message); - await ctx.OkAsync(); - } - - [NadekoCommand, Aliases] - [RequireContext(ContextType.Guild)] - [UserPerm(GuildPerm.BanMembers)] - [BotPerm(GuildPerm.BanMembers)] - public async Task BanMsgReset() - { - _service.SetBanTemplate(ctx.Guild.Id, null); - await ctx.OkAsync(); - } - - [NadekoCommand, Aliases] - [RequireContext(ContextType.Guild)] - [UserPerm(GuildPerm.BanMembers)] - [BotPerm(GuildPerm.BanMembers)] - [Priority(0)] - public Task BanMessageTest([Leftover] string reason = null) - => InternalBanMessageTest(reason, null); - - [NadekoCommand, Aliases] - [RequireContext(ContextType.Guild)] - [UserPerm(GuildPerm.BanMembers)] - [BotPerm(GuildPerm.BanMembers)] - [Priority(1)] - public Task BanMessageTest(StoopidTime duration, [Leftover] string reason = null) - => InternalBanMessageTest(reason, duration.Time); - - private async Task InternalBanMessageTest(string reason, TimeSpan? duration) - { - var dmChannel = await ctx.User.GetOrCreateDMChannelAsync(); - var defaultMessage = GetText(strs.bandm(Format.Bold(ctx.Guild.Name), reason)); - var embed = _service.GetBanUserDmEmbed(Context, - (IGuildUser)ctx.User, - defaultMessage, - reason, - duration); - - if (embed is null) - { - await ConfirmLocalizedAsync(strs.banmsg_disabled); + banning.Add(user); } else { - try - { - await dmChannel.SendAsync(embed); - } - catch (Exception) - { - await ReplyErrorLocalizedAsync(strs.unable_to_dm_user); - return; - } - - await ctx.OkAsync(); + missing.Add(userStr); } } - [NadekoCommand, Aliases] - [RequireContext(ContextType.Guild)] - [UserPerm(GuildPerm.BanMembers)] - [BotPerm(GuildPerm.BanMembers)] - public async Task Unban([Leftover] string user) + var missStr = string.Join("\n", missing); + if (string.IsNullOrWhiteSpace(missStr)) + missStr = "-"; + + var toSend = _eb.Create(ctx) + .WithDescription(GetText(strs.mass_ban_in_progress(banning.Count))) + .AddField(GetText(strs.invalid(missing.Count)), missStr) + .WithPendingColor(); + + var banningMessage = await ctx.Channel.EmbedAsync(toSend); + + foreach (var toBan in banning) { - var bans = await ctx.Guild.GetBansAsync().ConfigureAwait(false); - - var bun = bans.FirstOrDefault(x => x.User.ToString().ToLowerInvariant() == user.ToLowerInvariant()); - - if (bun is null) - { - await ReplyErrorLocalizedAsync(strs.user_not_found).ConfigureAwait(false); - return; - } - - await UnbanInternal(bun.User).ConfigureAwait(false); - } - - [NadekoCommand, Aliases] - [RequireContext(ContextType.Guild)] - [UserPerm(GuildPerm.BanMembers)] - [BotPerm(GuildPerm.BanMembers)] - public async Task Unban(ulong userId) - { - var bans = await ctx.Guild.GetBansAsync().ConfigureAwait(false); - - var bun = bans.FirstOrDefault(x => x.User.Id == userId); - - if (bun is null) - { - await ReplyErrorLocalizedAsync(strs.user_not_found).ConfigureAwait(false); - return; - } - - await UnbanInternal(bun.User).ConfigureAwait(false); - } - - private async Task UnbanInternal(IUser user) - { - await ctx.Guild.RemoveBanAsync(user).ConfigureAwait(false); - - await ReplyConfirmLocalizedAsync(strs.unbanned_user(Format.Bold(user.ToString()))).ConfigureAwait(false); - } - - [NadekoCommand, Aliases] - [RequireContext(ContextType.Guild)] - [UserPerm(GuildPerm.KickMembers | GuildPerm.ManageMessages)] - [BotPerm(GuildPerm.BanMembers)] - public Task Softban(IGuildUser user, [Leftover] string msg = null) - => SoftbanInternal(user, msg); - - [NadekoCommand, Aliases] - [RequireContext(ContextType.Guild)] - [UserPerm(GuildPerm.KickMembers | GuildPerm.ManageMessages)] - [BotPerm(GuildPerm.BanMembers)] - public async Task Softban(ulong userId, [Leftover] string msg = null) - { - var user = await ((DiscordSocketClient)Context.Client).Rest.GetGuildUserAsync(ctx.Guild.Id, userId); - if (user is null) - return; - - await SoftbanInternal(user, msg); - } - - private async Task SoftbanInternal(IGuildUser user, [Leftover] string msg = null) - { - if (!await CheckRoleHierarchy(user)) - return; - - var dmFailed = false; - try { - await user.SendErrorAsync(_eb, GetText(strs.sbdm(Format.Bold(ctx.Guild.Name), msg))); + await ctx.Guild.AddBanAsync(toBan.Id, 7, $"{ctx.User} | Massban"); } - catch + catch (Exception ex) { - dmFailed = true; + Log.Warning(ex, "Error banning {User} user in {GuildId} server", + toBan.Id, + ctx.Guild.Id); } - - await ctx.Guild.AddBanAsync(user, 7, ("Softban | " + ctx.User.ToString() + " | " + msg).TrimTo(512)).ConfigureAwait(false); - try { await ctx.Guild.RemoveBanAsync(user).ConfigureAwait(false); } - catch { await ctx.Guild.RemoveBanAsync(user).ConfigureAwait(false); } - - var toSend = _eb.Create().WithOkColor() - .WithTitle("☣ " + GetText(strs.sb_user)) - .AddField(GetText(strs.username), user.ToString(), true) - .AddField("ID", user.Id.ToString(), true); - - if (dmFailed) - { - toSend.WithFooter("⚠️ " + GetText(strs.unable_to_dm_user)); - } - - await ctx.Channel.EmbedAsync(toSend) - .ConfigureAwait(false); } - - [NadekoCommand, Aliases] - [RequireContext(ContextType.Guild)] - [UserPerm(GuildPerm.KickMembers)] - [BotPerm(GuildPerm.KickMembers)] - [Priority(1)] - public Task Kick(IGuildUser user, [Leftover] string msg = null) - => KickInternal(user, msg); - - [NadekoCommand, Aliases] - [RequireContext(ContextType.Guild)] - [UserPerm(GuildPerm.KickMembers)] - [BotPerm(GuildPerm.KickMembers)] - [Priority(0)] - public async Task Kick(ulong userId, [Leftover] string msg = null) - { - var user = await ((DiscordSocketClient)Context.Client).Rest.GetGuildUserAsync(ctx.Guild.Id, userId); - if (user is null) - return; - await KickInternal(user, msg); - } + await banningMessage.ModifyAsync(x => x.Embed = _eb.Create() + .WithDescription(GetText(strs.mass_ban_completed(banning.Count()))) + .AddField(GetText(strs.invalid(missing.Count)), missStr) + .WithOkColor() + .Build()).ConfigureAwait(false); + } - public async Task KickInternal(IGuildUser user, string msg = null) - { - if (!await CheckRoleHierarchy(user)) - return; + [NadekoCommand, Aliases] + [RequireContext(ContextType.Guild)] + [UserPerm(GuildPerm.BanMembers)] + [BotPerm(GuildPerm.BanMembers)] + [OwnerOnly] + public async Task MassKill([Leftover] string people) + { + if (string.IsNullOrWhiteSpace(people)) + return; - var dmFailed = false; + var (bans, missing) = _service.MassKill((SocketGuild)ctx.Guild, people); - try - { - await user.SendErrorAsync(_eb, GetText(strs.kickdm(Format.Bold(ctx.Guild.Name), msg))) - .ConfigureAwait(false); - } - catch - { - dmFailed = true; - } - - await user.KickAsync((ctx.User.ToString() + " | " + msg).TrimTo(512)).ConfigureAwait(false); - - var toSend = _eb.Create().WithOkColor() - .WithTitle(GetText(strs.kicked_user)) - .AddField(GetText(strs.username), user.ToString(), true) - .AddField("ID", user.Id.ToString(), true); + var missStr = string.Join("\n", missing); + if (string.IsNullOrWhiteSpace(missStr)) + missStr = "-"; - if (dmFailed) - { - toSend.WithFooter("⚠️ " + GetText(strs.unable_to_dm_user)); - } - - await ctx.Channel.EmbedAsync(toSend) - .ConfigureAwait(false); - } - - [NadekoCommand, Aliases] - [RequireContext(ContextType.Guild)] - [UserPerm(GuildPerm.BanMembers)] - [BotPerm(GuildPerm.BanMembers)] - [Ratelimit(30)] - public async Task MassBan(params string[] userStrings) - { - if (userStrings.Length == 0) - return; + //send a message but don't wait for it + var banningMessageTask = ctx.Channel.EmbedAsync(_eb.Create() + .WithDescription(GetText(strs.mass_kill_in_progress(bans.Count()))) + .AddField(GetText(strs.invalid(missing)), missStr) + .WithPendingColor()); - var missing = new List(); - var banning = new HashSet(); - - await ctx.Channel.TriggerTypingAsync(); - foreach (var userStr in userStrings) - { - if (ulong.TryParse(userStr, out var userId)) - { - IUser user = await ctx.Guild.GetUserAsync(userId) ?? - await ((DiscordSocketClient)Context.Client).Rest.GetGuildUserAsync(ctx.Guild.Id, userId); - - if (user is null) - { - // if IGuildUser is null, try to get IUser - user = await ((DiscordSocketClient)Context.Client).Rest.GetUserAsync(userId); - - // only add to missing if *still* null - if (user is null) - { - missing.Add(userStr); - continue; - } - - } - - //Hierachy checks only if the user is in the guild - if (user is IGuildUser gu && !await CheckRoleHierarchy(gu)) - { - return; - } - - banning.Add(user); - } - else - { - missing.Add(userStr); - } - } - - var missStr = string.Join("\n", missing); - if (string.IsNullOrWhiteSpace(missStr)) - missStr = "-"; - - var toSend = _eb.Create(ctx) - .WithDescription(GetText(strs.mass_ban_in_progress(banning.Count))) - .AddField(GetText(strs.invalid(missing.Count)), missStr) - .WithPendingColor(); - - var banningMessage = await ctx.Channel.EmbedAsync(toSend); - - foreach (var toBan in banning) - { - try - { - await ctx.Guild.AddBanAsync(toBan.Id, 7, $"{ctx.User} | Massban"); - } - catch (Exception ex) - { - Log.Warning(ex, "Error banning {User} user in {GuildId} server", - toBan.Id, - ctx.Guild.Id); - } - } - - await banningMessage.ModifyAsync(x => x.Embed = _eb.Create() - .WithDescription(GetText(strs.mass_ban_completed(banning.Count()))) - .AddField(GetText(strs.invalid(missing.Count)), missStr) - .WithOkColor() - .Build()).ConfigureAwait(false); - } - - [NadekoCommand, Aliases] - [RequireContext(ContextType.Guild)] - [UserPerm(GuildPerm.BanMembers)] - [BotPerm(GuildPerm.BanMembers)] - [OwnerOnly] - public async Task MassKill([Leftover] string people) - { - if (string.IsNullOrWhiteSpace(people)) - return; - - var (bans, missing) = _service.MassKill((SocketGuild)ctx.Guild, people); - - var missStr = string.Join("\n", missing); - if (string.IsNullOrWhiteSpace(missStr)) - missStr = "-"; - - //send a message but don't wait for it - var banningMessageTask = ctx.Channel.EmbedAsync(_eb.Create() - .WithDescription(GetText(strs.mass_kill_in_progress(bans.Count()))) - .AddField(GetText(strs.invalid(missing)), missStr) - .WithPendingColor()); - - //do the banning - await Task.WhenAll(bans + //do the banning + await Task.WhenAll(bans .Where(x => x.Id.HasValue) .Select(x => ctx.Guild.AddBanAsync(x.Id.Value, 7, x.Reason, new RequestOptions() { RetryMode = RetryMode.AlwaysRetry, }))) - .ConfigureAwait(false); + .ConfigureAwait(false); - //wait for the message and edit it - var banningMessage = await banningMessageTask.ConfigureAwait(false); + //wait for the message and edit it + var banningMessage = await banningMessageTask.ConfigureAwait(false); - await banningMessage.ModifyAsync(x => x.Embed = _eb.Create() - .WithDescription(GetText(strs.mass_kill_completed(bans.Count()))) - .AddField(GetText(strs.invalid(missing)), missStr) - .WithOkColor() - .Build()).ConfigureAwait(false); - } + await banningMessage.ModifyAsync(x => x.Embed = _eb.Create() + .WithDescription(GetText(strs.mass_kill_completed(bans.Count()))) + .AddField(GetText(strs.invalid(missing)), missStr) + .WithOkColor() + .Build()).ConfigureAwait(false); } } -} +} \ No newline at end of file diff --git a/src/NadekoBot/Modules/Administration/VcRoleCommands.cs b/src/NadekoBot/Modules/Administration/VcRoleCommands.cs index f7acbdb87..6e7d4ad67 100644 --- a/src/NadekoBot/Modules/Administration/VcRoleCommands.cs +++ b/src/NadekoBot/Modules/Administration/VcRoleCommands.cs @@ -1,5 +1,4 @@ using System.Collections.Concurrent; -using System.Linq; using Discord; using Discord.Commands; using System.Threading.Tasks; @@ -8,86 +7,85 @@ using NadekoBot.Common.Attributes; using NadekoBot.Extensions; using NadekoBot.Modules.Administration.Services; -namespace NadekoBot.Modules.Administration +namespace NadekoBot.Modules.Administration; + +public partial class Administration { - public partial class Administration + [Group] + public class VcRoleCommands : NadekoSubmodule { - [Group] - public class VcRoleCommands : NadekoSubmodule + [NadekoCommand, Aliases] + [UserPerm(GuildPerm.ManageRoles)] + [BotPerm(GuildPerm.ManageRoles)] + [RequireContext(ContextType.Guild)] + public async Task VcRoleRm(ulong vcId) { - [NadekoCommand, Aliases] - [UserPerm(GuildPerm.ManageRoles)] - [BotPerm(GuildPerm.ManageRoles)] - [RequireContext(ContextType.Guild)] - public async Task VcRoleRm(ulong vcId) + if (_service.RemoveVcRole(ctx.Guild.Id, vcId)) { - if (_service.RemoveVcRole(ctx.Guild.Id, vcId)) - { - await ReplyConfirmLocalizedAsync(strs.vcrole_removed(Format.Bold(vcId.ToString()))).ConfigureAwait(false); - } - else - { - await ReplyErrorLocalizedAsync(strs.vcrole_not_found).ConfigureAwait(false); - } + await ReplyConfirmLocalizedAsync(strs.vcrole_removed(Format.Bold(vcId.ToString()))).ConfigureAwait(false); + } + else + { + await ReplyErrorLocalizedAsync(strs.vcrole_not_found).ConfigureAwait(false); + } + } + + [NadekoCommand, Aliases] + [UserPerm(GuildPerm.ManageRoles)] + [BotPerm(GuildPerm.ManageRoles)] + [RequireContext(ContextType.Guild)] + public async Task VcRole([Leftover] IRole role = null) + { + var user = (IGuildUser)ctx.User; + + var vc = user.VoiceChannel; + + if (vc is null || vc.GuildId != user.GuildId) + { + await ReplyErrorLocalizedAsync(strs.must_be_in_voice).ConfigureAwait(false); + return; } - [NadekoCommand, Aliases] - [UserPerm(GuildPerm.ManageRoles)] - [BotPerm(GuildPerm.ManageRoles)] - [RequireContext(ContextType.Guild)] - public async Task VcRole([Leftover] IRole role = null) + if (role is null) { - var user = (IGuildUser)ctx.User; - - var vc = user.VoiceChannel; - - if (vc is null || vc.GuildId != user.GuildId) + if (_service.RemoveVcRole(ctx.Guild.Id, vc.Id)) { - await ReplyErrorLocalizedAsync(strs.must_be_in_voice).ConfigureAwait(false); - return; - } - - if (role is null) - { - if (_service.RemoveVcRole(ctx.Guild.Id, vc.Id)) - { - await ReplyConfirmLocalizedAsync(strs.vcrole_removed(Format.Bold(vc.Name))).ConfigureAwait(false); - } - } - else - { - _service.AddVcRole(ctx.Guild.Id, role, vc.Id); - await ReplyConfirmLocalizedAsync(strs.vcrole_added(Format.Bold(vc.Name), Format.Bold(role.Name))).ConfigureAwait(false); + await ReplyConfirmLocalizedAsync(strs.vcrole_removed(Format.Bold(vc.Name))).ConfigureAwait(false); } } - - [NadekoCommand, Aliases] - [RequireContext(ContextType.Guild)] - public async Task VcRoleList() + else { - var guild = (SocketGuild)ctx.Guild; - string text; - if (_service.VcRoles.TryGetValue(ctx.Guild.Id, out ConcurrentDictionary roles)) - { - if (!roles.Any()) - { - text = GetText(strs.no_vcroles); - } - else - { - text = string.Join("\n", roles.Select(x => - $"{Format.Bold(guild.GetVoiceChannel(x.Key)?.Name ?? x.Key.ToString())} => {x.Value}")); - } - } - else + _service.AddVcRole(ctx.Guild.Id, role, vc.Id); + await ReplyConfirmLocalizedAsync(strs.vcrole_added(Format.Bold(vc.Name), Format.Bold(role.Name))).ConfigureAwait(false); + } + } + + [NadekoCommand, Aliases] + [RequireContext(ContextType.Guild)] + public async Task VcRoleList() + { + var guild = (SocketGuild)ctx.Guild; + string text; + if (_service.VcRoles.TryGetValue(ctx.Guild.Id, out ConcurrentDictionary roles)) + { + if (!roles.Any()) { text = GetText(strs.no_vcroles); } - await ctx.Channel.EmbedAsync(_eb.Create().WithOkColor() - .WithTitle(GetText(strs.vc_role_list)) - .WithDescription(text)) - .ConfigureAwait(false); + else + { + text = string.Join("\n", roles.Select(x => + $"{Format.Bold(guild.GetVoiceChannel(x.Key)?.Name ?? x.Key.ToString())} => {x.Value}")); + } } + else + { + text = GetText(strs.no_vcroles); + } + await ctx.Channel.EmbedAsync(_eb.Create().WithOkColor() + .WithTitle(GetText(strs.vc_role_list)) + .WithDescription(text)) + .ConfigureAwait(false); } } -} +} \ No newline at end of file diff --git a/src/NadekoBot/Modules/CustomReactions/Common/ExportedExpr.cs b/src/NadekoBot/Modules/CustomReactions/Common/ExportedExpr.cs index 3c7dade79..b1a1310dc 100644 --- a/src/NadekoBot/Modules/CustomReactions/Common/ExportedExpr.cs +++ b/src/NadekoBot/Modules/CustomReactions/Common/ExportedExpr.cs @@ -1,30 +1,29 @@ using NadekoBot.Services.Database.Models; using NadekoBot.Common; -namespace NadekoBot.Modules.CustomReactions -{ - public class ExportedExpr - { - public string Res { get; set; } - public string Id { get; set; } - public bool Ad { get; set; } - public bool Dm { get; set; } - public bool At { get; set; } - public bool Ca { get; set; } - public string[] React; +namespace NadekoBot.Modules.CustomReactions; - public static ExportedExpr FromModel(CustomReaction cr) - => new ExportedExpr() - { - Res = cr.Response, - Id = ((kwum)cr.Id).ToString(), - Ad = cr.AutoDeleteTrigger, - At = cr.AllowTarget, - Ca = cr.ContainsAnywhere, - Dm = cr.DmResponse, - React = string.IsNullOrWhiteSpace(cr.Reactions) - ? null - : cr.GetReactions(), - }; - } +public class ExportedExpr +{ + public string Res { get; set; } + public string Id { get; set; } + public bool Ad { get; set; } + public bool Dm { get; set; } + public bool At { get; set; } + public bool Ca { get; set; } + public string[] React; + + public static ExportedExpr FromModel(CustomReaction cr) + => new ExportedExpr() + { + Res = cr.Response, + Id = ((kwum)cr.Id).ToString(), + Ad = cr.AutoDeleteTrigger, + At = cr.AllowTarget, + Ca = cr.ContainsAnywhere, + Dm = cr.DmResponse, + React = string.IsNullOrWhiteSpace(cr.Reactions) + ? null + : cr.GetReactions(), + }; } \ No newline at end of file diff --git a/src/NadekoBot/Modules/CustomReactions/CustomReactions.cs b/src/NadekoBot/Modules/CustomReactions/CustomReactions.cs index af194db03..aaddc4a6e 100644 --- a/src/NadekoBot/Modules/CustomReactions/CustomReactions.cs +++ b/src/NadekoBot/Modules/CustomReactions/CustomReactions.cs @@ -1,362 +1,358 @@ using Discord; using Discord.Commands; using NadekoBot.Common.Attributes; -using NadekoBot.Services; using NadekoBot.Extensions; using NadekoBot.Modules.CustomReactions.Services; -using System.Collections.Generic; -using System.Linq; using System.Net.Http; using System.Threading.Tasks; using NadekoBot.Common; -namespace NadekoBot.Modules.CustomReactions +namespace NadekoBot.Modules.CustomReactions; + +public class CustomReactions : NadekoModule { - public class CustomReactions : NadekoModule + private readonly IBotCredentials _creds; + private readonly IHttpClientFactory _clientFactory; + + public CustomReactions(IBotCredentials creds, IHttpClientFactory clientFactory) { - private readonly IBotCredentials _creds; - private readonly IHttpClientFactory _clientFactory; + _creds = creds; + _clientFactory = clientFactory; + } - public CustomReactions(IBotCredentials creds, IHttpClientFactory clientFactory) + private bool AdminInGuildOrOwnerInDm() => (ctx.Guild is null && _creds.IsOwner(ctx.User)) + || (ctx.Guild != null && ((IGuildUser)ctx.User).GuildPermissions.Administrator); + + [NadekoCommand, Aliases] + public async Task AddCustReact(string key, [Leftover] string message) + { + var channel = ctx.Channel as ITextChannel; + if (string.IsNullOrWhiteSpace(message) || string.IsNullOrWhiteSpace(key)) + return; + + if (!AdminInGuildOrOwnerInDm()) { - _creds = creds; - _clientFactory = clientFactory; + await ReplyErrorLocalizedAsync(strs.insuff_perms).ConfigureAwait(false); + return; } - private bool AdminInGuildOrOwnerInDm() => (ctx.Guild is null && _creds.IsOwner(ctx.User)) - || (ctx.Guild != null && ((IGuildUser)ctx.User).GuildPermissions.Administrator); + var cr = await _service.AddAsync(ctx.Guild?.Id, key, message); - [NadekoCommand, Aliases] - public async Task AddCustReact(string key, [Leftover] string message) + await ctx.Channel.EmbedAsync(_eb.Create().WithOkColor() + .WithTitle(GetText(strs.new_cust_react)) + .WithDescription($"#{(kwum)cr.Id}") + .AddField(GetText(strs.trigger), key) + .AddField(GetText(strs.response), message.Length > 1024 ? GetText(strs.redacted_too_long) : message) + ).ConfigureAwait(false); + } + + [NadekoCommand, Aliases] + public async Task EditCustReact(kwum id, [Leftover] string message) + { + var channel = ctx.Channel as ITextChannel; + if (string.IsNullOrWhiteSpace(message) || id < 0) + return; + + if ((channel is null && !_creds.IsOwner(ctx.User)) || (channel != null && !((IGuildUser)ctx.User).GuildPermissions.Administrator)) { - var channel = ctx.Channel as ITextChannel; - if (string.IsNullOrWhiteSpace(message) || string.IsNullOrWhiteSpace(key)) - return; - - if (!AdminInGuildOrOwnerInDm()) - { - await ReplyErrorLocalizedAsync(strs.insuff_perms).ConfigureAwait(false); - return; - } - - var cr = await _service.AddAsync(ctx.Guild?.Id, key, message); + await ReplyErrorLocalizedAsync(strs.insuff_perms).ConfigureAwait(false); + return; + } + var cr = await _service.EditAsync(ctx.Guild?.Id, (int)id, message).ConfigureAwait(false); + if (cr != null) + { await ctx.Channel.EmbedAsync(_eb.Create().WithOkColor() - .WithTitle(GetText(strs.new_cust_react)) - .WithDescription($"#{(kwum)cr.Id}") - .AddField(GetText(strs.trigger), key) + .WithTitle(GetText(strs.edited_cust_react)) + .WithDescription($"#{id}") + .AddField(GetText(strs.trigger), cr.Trigger) .AddField(GetText(strs.response), message.Length > 1024 ? GetText(strs.redacted_too_long) : message) - ).ConfigureAwait(false); + ).ConfigureAwait(false); } - - [NadekoCommand, Aliases] - public async Task EditCustReact(kwum id, [Leftover] string message) + else { - var channel = ctx.Channel as ITextChannel; - if (string.IsNullOrWhiteSpace(message) || id < 0) - return; - - if ((channel is null && !_creds.IsOwner(ctx.User)) || (channel != null && !((IGuildUser)ctx.User).GuildPermissions.Administrator)) - { - await ReplyErrorLocalizedAsync(strs.insuff_perms).ConfigureAwait(false); - return; - } - - var cr = await _service.EditAsync(ctx.Guild?.Id, (int)id, message).ConfigureAwait(false); - if (cr != null) - { - await ctx.Channel.EmbedAsync(_eb.Create().WithOkColor() - .WithTitle(GetText(strs.edited_cust_react)) - .WithDescription($"#{id}") - .AddField(GetText(strs.trigger), cr.Trigger) - .AddField(GetText(strs.response), message.Length > 1024 ? GetText(strs.redacted_too_long) : message) - ).ConfigureAwait(false); - } - else - { - await ReplyErrorLocalizedAsync(strs.edit_fail).ConfigureAwait(false); - } + await ReplyErrorLocalizedAsync(strs.edit_fail).ConfigureAwait(false); } + } - [NadekoCommand, Aliases] - [Priority(1)] - public async Task ListCustReact(int page = 1) + [NadekoCommand, Aliases] + [Priority(1)] + public async Task ListCustReact(int page = 1) + { + if (--page < 0 || page > 999) + return; + + var customReactions = _service.GetCustomReactionsFor(ctx.Guild?.Id); + + if (customReactions is null || !customReactions.Any()) { - if (--page < 0 || page > 999) - return; - - var customReactions = _service.GetCustomReactionsFor(ctx.Guild?.Id); - - if (customReactions is null || !customReactions.Any()) - { - await ReplyErrorLocalizedAsync(strs.no_found).ConfigureAwait(false); - return; - } - - await ctx.SendPaginatedConfirmAsync(page, pageFunc: curPage => - { - var desc = customReactions.OrderBy(cr => cr.Trigger) - .Skip(curPage * 20) - .Take(20) - .Select(cr => $"{(cr.ContainsAnywhere ? "🗯" : "◾")}" + - $"{(cr.DmResponse ? "✉" : "◾")}" + - $"{(cr.AutoDeleteTrigger ? "❌" : "◾")}" + - $"`{(kwum) cr.Id}` {cr.Trigger}" - + (string.IsNullOrWhiteSpace(cr.Reactions) - ? string.Empty - : " // " + string.Join(" ", cr.GetReactions()))) - .JoinWith('\n'); - - return _eb.Create().WithOkColor() - .WithTitle(GetText(strs.custom_reactions)) - .WithDescription(desc); - - }, customReactions.Length, 20); + await ReplyErrorLocalizedAsync(strs.no_found).ConfigureAwait(false); + return; } - public enum All + await ctx.SendPaginatedConfirmAsync(page, pageFunc: curPage => { - All - } + var desc = customReactions.OrderBy(cr => cr.Trigger) + .Skip(curPage * 20) + .Take(20) + .Select(cr => $"{(cr.ContainsAnywhere ? "🗯" : "◾")}" + + $"{(cr.DmResponse ? "✉" : "◾")}" + + $"{(cr.AutoDeleteTrigger ? "❌" : "◾")}" + + $"`{(kwum) cr.Id}` {cr.Trigger}" + + (string.IsNullOrWhiteSpace(cr.Reactions) + ? string.Empty + : " // " + string.Join(" ", cr.GetReactions()))) + .JoinWith('\n'); - [NadekoCommand, Aliases] - [Priority(0)] - public async Task ListCustReact(All _) + return _eb.Create().WithOkColor() + .WithTitle(GetText(strs.custom_reactions)) + .WithDescription(desc); + + }, customReactions.Length, 20); + } + + public enum All + { + All + } + + [NadekoCommand, Aliases] + [Priority(0)] + public async Task ListCustReact(All _) + { + await ReplyPendingLocalizedAsync(strs.obsolete_use(Format.Code($"{Prefix}crsexport"))); + await CrsExport(); + } + + [NadekoCommand, Aliases] + public async Task ListCustReactG(int page = 1) + { + await ReplyPendingLocalizedAsync(strs.obsolete_use(Format.Code($"{Prefix}crsexport"))); + await CrsExport(); + } + + [NadekoCommand, Aliases] + public async Task ShowCustReact(kwum id) + { + var found = _service.GetCustomReaction(ctx.Guild?.Id, (int)id); + + if (found is null) { - await ReplyPendingLocalizedAsync(strs.obsolete_use(Format.Code($"{Prefix}crsexport"))); - await CrsExport(); + await ReplyErrorLocalizedAsync(strs.no_found_id).ConfigureAwait(false); + return; } - - [NadekoCommand, Aliases] - public async Task ListCustReactG(int page = 1) + else { - await ReplyPendingLocalizedAsync(strs.obsolete_use(Format.Code($"{Prefix}crsexport"))); - await CrsExport(); + await ctx.Channel.EmbedAsync(_eb.Create().WithOkColor() + .WithDescription($"#{id}") + .AddField(GetText(strs.trigger), found.Trigger.TrimTo(1024)) + .AddField(GetText(strs.response), found.Response.TrimTo(1000).Replace("](", "]\\(")) + ).ConfigureAwait(false); } + } - [NadekoCommand, Aliases] - public async Task ShowCustReact(kwum id) + [NadekoCommand, Aliases] + public async Task DelCustReact(kwum id) + { + if (!AdminInGuildOrOwnerInDm()) { - var found = _service.GetCustomReaction(ctx.Guild?.Id, (int)id); - - if (found is null) - { - await ReplyErrorLocalizedAsync(strs.no_found_id).ConfigureAwait(false); - return; - } - else - { - await ctx.Channel.EmbedAsync(_eb.Create().WithOkColor() - .WithDescription($"#{id}") - .AddField(GetText(strs.trigger), found.Trigger.TrimTo(1024)) - .AddField(GetText(strs.response), found.Response.TrimTo(1000).Replace("](", "]\\(")) - ).ConfigureAwait(false); - } + await ReplyErrorLocalizedAsync(strs.insuff_perms).ConfigureAwait(false); + return; } - [NadekoCommand, Aliases] - public async Task DelCustReact(kwum id) + var cr = await _service.DeleteAsync(ctx.Guild?.Id, (int)id); + + if (cr != null) { - if (!AdminInGuildOrOwnerInDm()) - { - await ReplyErrorLocalizedAsync(strs.insuff_perms).ConfigureAwait(false); - return; - } - - var cr = await _service.DeleteAsync(ctx.Guild?.Id, (int)id); - - if (cr != null) - { - await ctx.Channel.EmbedAsync(_eb.Create().WithOkColor() - .WithTitle(GetText(strs.deleted)) - .WithDescription($"#{id}") - .AddField(GetText(strs.trigger), cr.Trigger.TrimTo(1024)) - .AddField(GetText(strs.response), cr.Response.TrimTo(1024))).ConfigureAwait(false); - } - else - { - await ReplyErrorLocalizedAsync(strs.no_found_id).ConfigureAwait(false); - } + await ctx.Channel.EmbedAsync(_eb.Create().WithOkColor() + .WithTitle(GetText(strs.deleted)) + .WithDescription($"#{id}") + .AddField(GetText(strs.trigger), cr.Trigger.TrimTo(1024)) + .AddField(GetText(strs.response), cr.Response.TrimTo(1024))).ConfigureAwait(false); } - - [NadekoCommand, Aliases] - public async Task CrReact(kwum id, params string[] emojiStrs) + else { - if (!AdminInGuildOrOwnerInDm()) - { - await ReplyErrorLocalizedAsync(strs.insuff_perms).ConfigureAwait(false); - return; - } - - var cr = _service.GetCustomReaction(ctx.Guild?.Id, id); - if (cr is null) - { - await ReplyErrorLocalizedAsync(strs.no_found).ConfigureAwait(false); - return; - } - - if (emojiStrs.Length == 0) - { - await _service.ResetCrReactions(ctx.Guild?.Id, id); - await ReplyConfirmLocalizedAsync(strs.crr_reset(Format.Bold(id.ToString()))).ConfigureAwait(false); - return; - } - - List succ = new List(); - foreach (var emojiStr in emojiStrs) - { - - var emote = emojiStr.ToIEmote(); - - // i should try adding these emojis right away to the message, to make sure the bot can react with these emojis. If it fails, skip that emoji - try - { - await ctx.Message.AddReactionAsync(emote).ConfigureAwait(false); - await Task.Delay(100).ConfigureAwait(false); - succ.Add(emojiStr); - - if (succ.Count >= 3) - break; - } - catch { } - } - - if(succ.Count == 0) - { - await ReplyErrorLocalizedAsync(strs.invalid_emojis).ConfigureAwait(false); - return; - } - - await _service.SetCrReactions(ctx.Guild?.Id, id, succ); - - - await ReplyConfirmLocalizedAsync(strs.crr_set(Format.Bold(id.ToString()), string.Join(", ", succ.Select(x => x.ToString())))).ConfigureAwait(false); + await ReplyErrorLocalizedAsync(strs.no_found_id).ConfigureAwait(false); + } + } + [NadekoCommand, Aliases] + public async Task CrReact(kwum id, params string[] emojiStrs) + { + if (!AdminInGuildOrOwnerInDm()) + { + await ReplyErrorLocalizedAsync(strs.insuff_perms).ConfigureAwait(false); + return; } - [NadekoCommand, Aliases] - public Task CrCa(kwum id) - => InternalCrEdit(id, CustomReactionsService.CrField.ContainsAnywhere); + var cr = _service.GetCustomReaction(ctx.Guild?.Id, id); + if (cr is null) + { + await ReplyErrorLocalizedAsync(strs.no_found).ConfigureAwait(false); + return; + } - [NadekoCommand, Aliases] - public Task CrDm(kwum id) - => InternalCrEdit(id, CustomReactionsService.CrField.DmResponse); + if (emojiStrs.Length == 0) + { + await _service.ResetCrReactions(ctx.Guild?.Id, id); + await ReplyConfirmLocalizedAsync(strs.crr_reset(Format.Bold(id.ToString()))).ConfigureAwait(false); + return; + } - [NadekoCommand, Aliases] - public Task CrAd(kwum id) - => InternalCrEdit(id, CustomReactionsService.CrField.AutoDelete); + List succ = new List(); + foreach (var emojiStr in emojiStrs) + { + + var emote = emojiStr.ToIEmote(); + + // i should try adding these emojis right away to the message, to make sure the bot can react with these emojis. If it fails, skip that emoji + try + { + await ctx.Message.AddReactionAsync(emote).ConfigureAwait(false); + await Task.Delay(100).ConfigureAwait(false); + succ.Add(emojiStr); + + if (succ.Count >= 3) + break; + } + catch { } + } + + if(succ.Count == 0) + { + await ReplyErrorLocalizedAsync(strs.invalid_emojis).ConfigureAwait(false); + return; + } + + await _service.SetCrReactions(ctx.Guild?.Id, id, succ); + + + await ReplyConfirmLocalizedAsync(strs.crr_set(Format.Bold(id.ToString()), string.Join(", ", succ.Select(x => x.ToString())))).ConfigureAwait(false); + + } + + [NadekoCommand, Aliases] + public Task CrCa(kwum id) + => InternalCrEdit(id, CustomReactionsService.CrField.ContainsAnywhere); + + [NadekoCommand, Aliases] + public Task CrDm(kwum id) + => InternalCrEdit(id, CustomReactionsService.CrField.DmResponse); + + [NadekoCommand, Aliases] + public Task CrAd(kwum id) + => InternalCrEdit(id, CustomReactionsService.CrField.AutoDelete); - [NadekoCommand, Aliases] - public Task CrAt(kwum id) - => InternalCrEdit(id, CustomReactionsService.CrField.AllowTarget); + [NadekoCommand, Aliases] + public Task CrAt(kwum id) + => InternalCrEdit(id, CustomReactionsService.CrField.AllowTarget); - [NadekoCommand, Aliases] - [OwnerOnly] - public async Task CrsReload() + [NadekoCommand, Aliases] + [OwnerOnly] + public async Task CrsReload() + { + await _service.TriggerReloadCustomReactions(); + + await ctx.OkAsync(); + } + + private async Task InternalCrEdit(kwum id, CustomReactionsService.CrField option) + { + var cr = _service.GetCustomReaction(ctx.Guild?.Id, id); + if (!AdminInGuildOrOwnerInDm()) { - await _service.TriggerReloadCustomReactions(); - - await ctx.OkAsync(); + await ReplyErrorLocalizedAsync(strs.insuff_perms).ConfigureAwait(false); + return; + } + var (success, newVal) = await _service.ToggleCrOptionAsync(id, option).ConfigureAwait(false); + if (!success) + { + await ReplyErrorLocalizedAsync(strs.no_found_id).ConfigureAwait(false); + return; } - private async Task InternalCrEdit(kwum id, CustomReactionsService.CrField option) + if (newVal) { - var cr = _service.GetCustomReaction(ctx.Guild?.Id, id); - if (!AdminInGuildOrOwnerInDm()) - { - await ReplyErrorLocalizedAsync(strs.insuff_perms).ConfigureAwait(false); - return; - } - var (success, newVal) = await _service.ToggleCrOptionAsync(id, option).ConfigureAwait(false); - if (!success) - { - await ReplyErrorLocalizedAsync(strs.no_found_id).ConfigureAwait(false); - return; - } - - if (newVal) - { - await ReplyConfirmLocalizedAsync(strs.option_enabled(Format.Code(option.ToString()), Format.Code(id.ToString()))).ConfigureAwait(false); - } - else - { - await ReplyConfirmLocalizedAsync(strs.option_disabled(Format.Code(option.ToString()), Format.Code(id.ToString()))).ConfigureAwait(false); - } + await ReplyConfirmLocalizedAsync(strs.option_enabled(Format.Code(option.ToString()), Format.Code(id.ToString()))).ConfigureAwait(false); } - - [NadekoCommand, Aliases] - [RequireContext(ContextType.Guild)] - [UserPerm(GuildPerm.Administrator)] - public async Task CrClear() + else { - if (await PromptUserConfirmAsync(_eb.Create() + await ReplyConfirmLocalizedAsync(strs.option_disabled(Format.Code(option.ToString()), Format.Code(id.ToString()))).ConfigureAwait(false); + } + } + + [NadekoCommand, Aliases] + [RequireContext(ContextType.Guild)] + [UserPerm(GuildPerm.Administrator)] + public async Task CrClear() + { + if (await PromptUserConfirmAsync(_eb.Create() .WithTitle("Custom reaction clear") .WithDescription("This will delete all custom reactions on this server.")).ConfigureAwait(false)) - { - var count = _service.DeleteAllCustomReactions(ctx.Guild.Id); - await ReplyConfirmLocalizedAsync(strs.cleared(count)); - } - } - - [NadekoCommand, Aliases] - public async Task CrsExport() { - if (!AdminInGuildOrOwnerInDm()) - { - await ReplyErrorLocalizedAsync(strs.insuff_perms).ConfigureAwait(false); - return; - } - - _ = ctx.Channel.TriggerTypingAsync(); - - var serialized = _service.ExportCrs(ctx.Guild?.Id); - await using var stream = await serialized.ToStream(); - await ctx.Channel.SendFileAsync(stream, "crs-export.yml", text: null); + var count = _service.DeleteAllCustomReactions(ctx.Guild.Id); + await ReplyConfirmLocalizedAsync(strs.cleared(count)); } + } - [NadekoCommand, Aliases] + [NadekoCommand, Aliases] + public async Task CrsExport() + { + if (!AdminInGuildOrOwnerInDm()) + { + await ReplyErrorLocalizedAsync(strs.insuff_perms).ConfigureAwait(false); + return; + } + + _ = ctx.Channel.TriggerTypingAsync(); + + var serialized = _service.ExportCrs(ctx.Guild?.Id); + await using var stream = await serialized.ToStream(); + await ctx.Channel.SendFileAsync(stream, "crs-export.yml", text: null); + } + + [NadekoCommand, Aliases] #if GLOBAL_NADEKO [OwnerOnly] #endif - public async Task CrsImport([Leftover]string input = null) + public async Task CrsImport([Leftover]string input = null) + { + if (!AdminInGuildOrOwnerInDm()) { - if (!AdminInGuildOrOwnerInDm()) - { - await ReplyErrorLocalizedAsync(strs.insuff_perms).ConfigureAwait(false); - return; - } - - input = input?.Trim(); - - _ = ctx.Channel.TriggerTypingAsync(); - - if (input is null) - { - var attachment = ctx.Message.Attachments.FirstOrDefault(); - if (attachment is null) - { - await ReplyErrorLocalizedAsync(strs.expr_import_no_input); - return; - } - - using var client = _clientFactory.CreateClient(); - input = await client.GetStringAsync(attachment.Url); - - if (string.IsNullOrWhiteSpace(input)) - { - await ReplyErrorLocalizedAsync(strs.expr_import_no_input); - return; - } - } - - var succ = await _service.ImportCrsAsync(ctx.Guild?.Id, input); - if (!succ) - { - await ReplyErrorLocalizedAsync(strs.expr_import_invalid_data); - return; - } - - await ctx.OkAsync(); + await ReplyErrorLocalizedAsync(strs.insuff_perms).ConfigureAwait(false); + return; } + + input = input?.Trim(); + + _ = ctx.Channel.TriggerTypingAsync(); + + if (input is null) + { + var attachment = ctx.Message.Attachments.FirstOrDefault(); + if (attachment is null) + { + await ReplyErrorLocalizedAsync(strs.expr_import_no_input); + return; + } + + using var client = _clientFactory.CreateClient(); + input = await client.GetStringAsync(attachment.Url); + + if (string.IsNullOrWhiteSpace(input)) + { + await ReplyErrorLocalizedAsync(strs.expr_import_no_input); + return; + } + } + + var succ = await _service.ImportCrsAsync(ctx.Guild?.Id, input); + if (!succ) + { + await ReplyErrorLocalizedAsync(strs.expr_import_invalid_data); + return; + } + + await ctx.OkAsync(); } -} +} \ No newline at end of file diff --git a/src/NadekoBot/Modules/CustomReactions/Extensions/CustomReactionExtensions.cs b/src/NadekoBot/Modules/CustomReactions/Extensions/CustomReactionExtensions.cs index 337f84a90..69cbd45f5 100644 --- a/src/NadekoBot/Modules/CustomReactions/Extensions/CustomReactionExtensions.cs +++ b/src/NadekoBot/Modules/CustomReactions/Extensions/CustomReactionExtensions.cs @@ -3,94 +3,92 @@ using Discord.WebSocket; using NadekoBot.Common.Replacements; using NadekoBot.Services.Database.Models; using NadekoBot.Extensions; -using System; using System.Runtime.CompilerServices; using System.Threading.Tasks; -namespace NadekoBot.Modules.CustomReactions.Extensions +namespace NadekoBot.Modules.CustomReactions.Extensions; + +public static class CustomReactionExtensions { - public static class CustomReactionExtensions + private static string ResolveTriggerString(this string str, DiscordSocketClient client) + => str.Replace("%bot.mention%", client.CurrentUser.Mention, StringComparison.Ordinal); + + public static async Task Send(this CustomReaction cr, IUserMessage ctx, + DiscordSocketClient client, bool sanitize) { - private static string ResolveTriggerString(this string str, DiscordSocketClient client) - => str.Replace("%bot.mention%", client.CurrentUser.Mention, StringComparison.Ordinal); + var channel = cr.DmResponse + ? await ctx.Author.GetOrCreateDMChannelAsync().ConfigureAwait(false) + : ctx.Channel; - public static async Task Send(this CustomReaction cr, IUserMessage ctx, - DiscordSocketClient client, bool sanitize) + var trigger = cr.Trigger.ResolveTriggerString(client); + var substringIndex = trigger.Length; + if (cr.ContainsAnywhere) { - var channel = cr.DmResponse - ? await ctx.Author.GetOrCreateDMChannelAsync().ConfigureAwait(false) - : ctx.Channel; - - var trigger = cr.Trigger.ResolveTriggerString(client); - var substringIndex = trigger.Length; - if (cr.ContainsAnywhere) - { - var pos = ctx.Content.AsSpan().GetWordPosition(trigger); - if (pos == WordPosition.Start) - substringIndex += 1; - else if (pos == WordPosition.End) - substringIndex = ctx.Content.Length; - else if (pos == WordPosition.Middle) - substringIndex += ctx.Content.IndexOf(trigger, StringComparison.InvariantCulture); - } - - var canMentionEveryone = (ctx.Author as IGuildUser)?.GuildPermissions.MentionEveryone ?? true; - - var rep = new ReplacementBuilder() - .WithDefault(ctx.Author, ctx.Channel, (ctx.Channel as ITextChannel)?.Guild as SocketGuild, client) - .WithOverride("%target%", () => canMentionEveryone - ? ctx.Content.Substring(substringIndex).Trim() - : ctx.Content.Substring(substringIndex).Trim().SanitizeMentions(true)) - .Build(); - - var text = SmartText.CreateFrom(cr.Response); - text = rep.Replace(text); - - return await channel.SendAsync(text, sanitize); + var pos = ctx.Content.AsSpan().GetWordPosition(trigger); + if (pos == WordPosition.Start) + substringIndex += 1; + else if (pos == WordPosition.End) + substringIndex = ctx.Content.Length; + else if (pos == WordPosition.Middle) + substringIndex += ctx.Content.IndexOf(trigger, StringComparison.InvariantCulture); } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static WordPosition GetWordPosition(this ReadOnlySpan str, in ReadOnlySpan word) - { - var wordIndex = str.IndexOf(word, StringComparison.OrdinalIgnoreCase); - if (wordIndex == -1) - return WordPosition.None; + var canMentionEveryone = (ctx.Author as IGuildUser)?.GuildPermissions.MentionEveryone ?? true; - if (wordIndex == 0) - { - if (word.Length < str.Length && str.isValidWordDivider(word.Length)) - return WordPosition.Start; - } - else if ((wordIndex + word.Length) == str.Length) - { - if (str.isValidWordDivider(wordIndex - 1)) - return WordPosition.End; - } - else if (str.isValidWordDivider(wordIndex - 1) && str.isValidWordDivider(wordIndex + word.Length)) - return WordPosition.Middle; + var rep = new ReplacementBuilder() + .WithDefault(ctx.Author, ctx.Channel, (ctx.Channel as ITextChannel)?.Guild as SocketGuild, client) + .WithOverride("%target%", () => canMentionEveryone + ? ctx.Content.Substring(substringIndex).Trim() + : ctx.Content.Substring(substringIndex).Trim().SanitizeMentions(true)) + .Build(); + var text = SmartText.CreateFrom(cr.Response); + text = rep.Replace(text); + + return await channel.SendAsync(text, sanitize); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static WordPosition GetWordPosition(this ReadOnlySpan str, in ReadOnlySpan word) + { + var wordIndex = str.IndexOf(word, StringComparison.OrdinalIgnoreCase); + if (wordIndex == -1) return WordPosition.None; - } - private static bool isValidWordDivider(this in ReadOnlySpan str, int index) + if (wordIndex == 0) { - var ch = str[index]; - if (ch >= 'a' && ch <= 'z') - return false; - if (ch >= 'A' && ch <= 'Z') - return false; - if (ch >= '1' && ch <= '9') - return false; - - return true; + if (word.Length < str.Length && str.isValidWordDivider(word.Length)) + return WordPosition.Start; } + else if ((wordIndex + word.Length) == str.Length) + { + if (str.isValidWordDivider(wordIndex - 1)) + return WordPosition.End; + } + else if (str.isValidWordDivider(wordIndex - 1) && str.isValidWordDivider(wordIndex + word.Length)) + return WordPosition.Middle; + + return WordPosition.None; } - public enum WordPosition + private static bool isValidWordDivider(this in ReadOnlySpan str, int index) { - None, - Start, - Middle, - End, + var ch = str[index]; + if (ch >= 'a' && ch <= 'z') + return false; + if (ch >= 'A' && ch <= 'Z') + return false; + if (ch >= '1' && ch <= '9') + return false; + + return true; } +} + +public enum WordPosition +{ + None, + Start, + Middle, + End, } \ No newline at end of file diff --git a/src/NadekoBot/Modules/CustomReactions/Services/CustomReactionsService.cs b/src/NadekoBot/Modules/CustomReactions/Services/CustomReactionsService.cs index 5cfef396b..f460f0ec3 100644 --- a/src/NadekoBot/Modules/CustomReactions/Services/CustomReactionsService.cs +++ b/src/NadekoBot/Modules/CustomReactions/Services/CustomReactionsService.cs @@ -8,674 +8,670 @@ using NadekoBot.Extensions; using NadekoBot.Modules.CustomReactions.Extensions; using NadekoBot.Modules.Permissions.Common; using NadekoBot.Modules.Permissions.Services; -using System; using System.Collections.Concurrent; -using System.Collections.Generic; -using System.Linq; using System.Runtime.CompilerServices; using System.Threading.Tasks; using Microsoft.EntityFrameworkCore; using NadekoBot.Common.Yml; using NadekoBot.Db; -using Serilog; using YamlDotNet.Serialization; -namespace NadekoBot.Modules.CustomReactions.Services +namespace NadekoBot.Modules.CustomReactions.Services; + +public sealed class CustomReactionsService : IEarlyBehavior, IReadyExecutor { - public sealed class CustomReactionsService : IEarlyBehavior, IReadyExecutor + public enum CrField { - public enum CrField - { - AutoDelete, - DmResponse, - AllowTarget, - ContainsAnywhere, - Message, - } + AutoDelete, + DmResponse, + AllowTarget, + ContainsAnywhere, + Message, + } - private readonly object _gcrWriteLock = new object(); + private readonly object _gcrWriteLock = new object(); - private readonly TypedKey _gcrAddedKey = new TypedKey("gcr.added"); - private readonly TypedKey _gcrDeletedkey = new TypedKey("gcr.deleted"); - private readonly TypedKey _gcrEditedKey = new TypedKey("gcr.edited"); - private readonly TypedKey _crsReloadedKey = new TypedKey("crs.reloaded"); - private const string MentionPh = "%bot.mention%"; + private readonly TypedKey _gcrAddedKey = new TypedKey("gcr.added"); + private readonly TypedKey _gcrDeletedkey = new TypedKey("gcr.deleted"); + private readonly TypedKey _gcrEditedKey = new TypedKey("gcr.edited"); + private readonly TypedKey _crsReloadedKey = new TypedKey("crs.reloaded"); + private const string MentionPh = "%bot.mention%"; - // it is perfectly fine to have global customreactions as an array - // 1. custom reactions are almost never added (compared to how many times they are being looped through) - // 2. only need write locks for this as we'll rebuild+replace the array on every edit - // 3. there's never many of them (at most a thousand, usually < 100) - private CustomReaction[] _globalReactions; - private ConcurrentDictionary _newGuildReactions; + // it is perfectly fine to have global customreactions as an array + // 1. custom reactions are almost never added (compared to how many times they are being looped through) + // 2. only need write locks for this as we'll rebuild+replace the array on every edit + // 3. there's never many of them (at most a thousand, usually < 100) + private CustomReaction[] _globalReactions; + private ConcurrentDictionary _newGuildReactions; - public int Priority => 0; + public int Priority => 0; - private readonly DbService _db; - private readonly DiscordSocketClient _client; - private readonly PermissionService _perms; - private readonly CommandHandler _cmd; - private readonly IBotStrings _strings; - private readonly Bot _bot; - private readonly GlobalPermissionService _gperm; - private readonly CmdCdService _cmdCds; - private readonly IPubSub _pubSub; - private readonly IEmbedBuilderService _eb; - private readonly Random _rng; + private readonly DbService _db; + private readonly DiscordSocketClient _client; + private readonly PermissionService _perms; + private readonly CommandHandler _cmd; + private readonly IBotStrings _strings; + private readonly Bot _bot; + private readonly GlobalPermissionService _gperm; + private readonly CmdCdService _cmdCds; + private readonly IPubSub _pubSub; + private readonly IEmbedBuilderService _eb; + private readonly Random _rng; - public CustomReactionsService(PermissionService perms, DbService db, IBotStrings strings, Bot bot, - DiscordSocketClient client, CommandHandler cmd, GlobalPermissionService gperm, CmdCdService cmdCds, - IPubSub pubSub, IEmbedBuilderService eb) - { - _db = db; - _client = client; - _perms = perms; - _cmd = cmd; - _strings = strings; - _bot = bot; - _gperm = gperm; - _cmdCds = cmdCds; - _pubSub = pubSub; - _eb = eb; - _rng = new NadekoRandom(); + public CustomReactionsService(PermissionService perms, DbService db, IBotStrings strings, Bot bot, + DiscordSocketClient client, CommandHandler cmd, GlobalPermissionService gperm, CmdCdService cmdCds, + IPubSub pubSub, IEmbedBuilderService eb) + { + _db = db; + _client = client; + _perms = perms; + _cmd = cmd; + _strings = strings; + _bot = bot; + _gperm = gperm; + _cmdCds = cmdCds; + _pubSub = pubSub; + _eb = eb; + _rng = new NadekoRandom(); - _pubSub.Sub(_crsReloadedKey, OnCrsShouldReload); - pubSub.Sub(_gcrAddedKey, OnGcrAdded); - pubSub.Sub(_gcrDeletedkey, OnGcrDeleted); - pubSub.Sub(_gcrEditedKey, OnGcrEdited); + _pubSub.Sub(_crsReloadedKey, OnCrsShouldReload); + pubSub.Sub(_gcrAddedKey, OnGcrAdded); + pubSub.Sub(_gcrDeletedkey, OnGcrDeleted); + pubSub.Sub(_gcrEditedKey, OnGcrEdited); - bot.JoinedGuild += OnJoinedGuild; - _client.LeftGuild += OnLeftGuild; - } + bot.JoinedGuild += OnJoinedGuild; + _client.LeftGuild += OnLeftGuild; + } - private async Task ReloadInternal(IReadOnlyList allGuildIds) - { - using var uow = _db.GetDbContext(); - var guildItems = await uow.CustomReactions - .AsNoTracking() - .Where(x => allGuildIds.Contains(x.GuildId.Value)) - .ToListAsync(); + private async Task ReloadInternal(IReadOnlyList allGuildIds) + { + using var uow = _db.GetDbContext(); + var guildItems = await uow.CustomReactions + .AsNoTracking() + .Where(x => allGuildIds.Contains(x.GuildId.Value)) + .ToListAsync(); - _newGuildReactions = guildItems - .GroupBy(k => k.GuildId!.Value) - .ToDictionary(g => g.Key, - g => g.Select(x => - { - x.Trigger = x.Trigger.Replace(MentionPh, _bot.Mention); - return x; - }).ToArray()) - .ToConcurrent(); - - lock (_gcrWriteLock) - { - var globalItems = uow - .CustomReactions - .AsNoTracking() - .Where(x => x.GuildId == null || x.GuildId == 0) - .AsEnumerable() - .Select(x => - { - x.Trigger = x.Trigger.Replace(MentionPh, _bot.Mention); - return x; - }) - .ToArray(); - - _globalReactions = globalItems; - } - - ready = true; - } - - #region Event Handlers - - public Task OnReadyAsync() - => ReloadInternal(_bot.GetCurrentGuildIds()); - - private ValueTask OnCrsShouldReload(bool _) - => new ValueTask(ReloadInternal(_bot.GetCurrentGuildIds())); - - private ValueTask OnGcrAdded(CustomReaction c) - { - lock (_gcrWriteLock) - { - var newGlobalReactions = new CustomReaction[_globalReactions.Length + 1]; - Array.Copy(_globalReactions, newGlobalReactions, _globalReactions.Length); - newGlobalReactions[_globalReactions.Length] = c; - _globalReactions = newGlobalReactions; - } - - return default; - } - - private ValueTask OnGcrEdited(CustomReaction c) - { - lock (_gcrWriteLock) - { - for (var i = 0; i < _globalReactions.Length; i++) + _newGuildReactions = guildItems + .GroupBy(k => k.GuildId!.Value) + .ToDictionary(g => g.Key, + g => g.Select(x => { - if (_globalReactions[i].Id == c.Id) + x.Trigger = x.Trigger.Replace(MentionPh, _bot.Mention); + return x; + }).ToArray()) + .ToConcurrent(); + + lock (_gcrWriteLock) + { + var globalItems = uow + .CustomReactions + .AsNoTracking() + .Where(x => x.GuildId == null || x.GuildId == 0) + .AsEnumerable() + .Select(x => + { + x.Trigger = x.Trigger.Replace(MentionPh, _bot.Mention); + return x; + }) + .ToArray(); + + _globalReactions = globalItems; + } + + ready = true; + } + + #region Event Handlers + + public Task OnReadyAsync() + => ReloadInternal(_bot.GetCurrentGuildIds()); + + private ValueTask OnCrsShouldReload(bool _) + => new ValueTask(ReloadInternal(_bot.GetCurrentGuildIds())); + + private ValueTask OnGcrAdded(CustomReaction c) + { + lock (_gcrWriteLock) + { + var newGlobalReactions = new CustomReaction[_globalReactions.Length + 1]; + Array.Copy(_globalReactions, newGlobalReactions, _globalReactions.Length); + newGlobalReactions[_globalReactions.Length] = c; + _globalReactions = newGlobalReactions; + } + + return default; + } + + private ValueTask OnGcrEdited(CustomReaction c) + { + lock (_gcrWriteLock) + { + for (var i = 0; i < _globalReactions.Length; i++) + { + if (_globalReactions[i].Id == c.Id) + { + _globalReactions[i] = c; + return default; + } + } + + // if edited cr is not found?! + // add it + OnGcrAdded(c); + } + + return default; + } + + private ValueTask OnGcrDeleted(int id) + { + lock (_gcrWriteLock) + { + var newGlobalReactions = DeleteInternal(_globalReactions, id, out _); + _globalReactions = newGlobalReactions; + } + + return default; + } + + public Task TriggerReloadCustomReactions() + => _pubSub.Pub(_crsReloadedKey, true); + + #endregion + + #region Client Event Handlers + + private Task OnLeftGuild(SocketGuild arg) + { + _newGuildReactions.TryRemove(arg.Id, out _); + + return Task.CompletedTask; + } + + private async Task OnJoinedGuild(GuildConfig gc) + { + using var uow = _db.GetDbContext(); + var crs = await uow + .CustomReactions + .AsNoTracking() + .Where(x => x.GuildId == gc.GuildId) + .ToArrayAsync(); + + _newGuildReactions[gc.GuildId] = crs; + } + + #endregion + + #region Basic Operations + + public async Task AddAsync(ulong? guildId, string key, string message) + { + key = key.ToLowerInvariant(); + var cr = new CustomReaction() + { + GuildId = guildId, + Trigger = key, + Response = message, + }; + + if (cr.Response.Contains("%target%", StringComparison.OrdinalIgnoreCase)) + cr.AllowTarget = true; + + using (var uow = _db.GetDbContext()) + { + uow.CustomReactions.Add(cr); + await uow.SaveChangesAsync(); + } + + await AddInternalAsync(guildId, cr); + + return cr; + } + + public async Task EditAsync(ulong? guildId, int id, string message) + { + using var uow = _db.GetDbContext(); + var cr = uow.CustomReactions.GetById(id); + + if (cr is null || cr.GuildId != guildId) + return null; + + // disable allowtarget if message had target, but it was removed from it + if (!message.Contains("%target%", StringComparison.OrdinalIgnoreCase) + && cr.Response.Contains("%target%", StringComparison.OrdinalIgnoreCase)) + { + cr.AllowTarget = false; + } + + cr.Response = message; + + // enable allow target if message is edited to contain target + if (cr.Response.Contains("%target%", StringComparison.OrdinalIgnoreCase)) + cr.AllowTarget = true; + + await uow.SaveChangesAsync(); + await UpdateInternalAsync(guildId, cr); + + return cr; + } + + + public async Task DeleteAsync(ulong? guildId, int id) + { + using var uow = _db.GetDbContext(); + var toDelete = uow.CustomReactions.GetById(id); + + if (toDelete is null) + return null; + + if ((toDelete.IsGlobal() && guildId is null) || (guildId == toDelete.GuildId)) + { + uow.CustomReactions.Remove(toDelete); + await uow.SaveChangesAsync(); + await DeleteInternalAsync(guildId, id); + return toDelete; + } + + return null; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public CustomReaction[] GetCustomReactionsFor(ulong? maybeGuildId) + { + if (maybeGuildId is ulong guildId) + { + return _newGuildReactions.TryGetValue(guildId, out var crs) + ? crs + : Array.Empty(); + } + + return _globalReactions; + } + + #endregion + + private bool ready; + + private CustomReaction TryGetCustomReaction(IUserMessage umsg) + { + if (!ready) + return null; + + if (!(umsg.Channel is SocketTextChannel channel)) + return null; + + var content = umsg.Content.Trim().ToLowerInvariant(); + + if (_newGuildReactions.TryGetValue(channel.Guild.Id, out var reactions) && reactions.Length > 0) + { + var cr = MatchCustomReactions(content, reactions); + if (cr is not null) + return cr; + } + + var localGrs = _globalReactions; + + return MatchCustomReactions(content, localGrs); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private CustomReaction MatchCustomReactions(in ReadOnlySpan content, CustomReaction[] crs) + { + var result = new List(1); + for (var i = 0; i < crs.Length; i++) + { + var cr = crs[i]; + var trigger = cr.Trigger; + if (content.Length > trigger.Length) + { + // if input is greater than the trigger, it can only work if: + // it has CA enabled + if (cr.ContainsAnywhere) + { + // if ca is enabled, we have to check if it is a word within the content + var wp = content.GetWordPosition(trigger); + + // if it is, then that's valid + if (wp != WordPosition.None) { - _globalReactions[i] = c; - return default; + result.Add(cr); } + + // if it's not, then it cant' work under any circumstance, + // because content is greater than the trigger length + // so it can't be equal, and it's not contained as a word + continue; } - // if edited cr is not found?! - // add it - OnGcrAdded(c); + // if CA is disabled, and CR has AllowTarget, then the + // content has to start with the trigger followed by a space + if (cr.AllowTarget && content.StartsWith(trigger, StringComparison.OrdinalIgnoreCase) + && content[trigger.Length] == ' ') + { + result.Add(cr); + } + } + else if (content.Length < cr.Trigger.Length) + { + // if input length is less than trigger length, it means + // that the reaction can never be triggered + } + else + { + // if input length is the same as trigger length + // reaction can only trigger if the strings are equal + if (content.SequenceEqual(cr.Trigger)) + { + result.Add(cr); + } } - - return default; } - private ValueTask OnGcrDeleted(int id) + if (result.Count == 0) + return null; + + var cancelled = result.FirstOrDefault(x => x.Response == "-"); + if (cancelled is not null) + return cancelled; + + return result[_rng.Next(0, result.Count)]; + } + + public async Task RunBehavior(IGuild guild, IUserMessage msg) + { + // maybe this message is a custom reaction + var cr = TryGetCustomReaction(msg); + + if (cr is null || cr.Response == "-") + return false; + + if(await _cmdCds.TryBlock(guild, msg.Author, cr.Trigger)) + return false; + + try + { + if (_gperm.BlockedModules.Contains("ActualCustomReactions")) + { + Log.Information("User {UserName} [{UserId}] tried to use a custom reaction but 'ActualCustomReactions' are globally disabled.", + msg.Author.ToString(), + msg.Author.Id); + + return true; + } + + if (guild is SocketGuild sg) + { + var pc = _perms.GetCacheFor(guild.Id); + if (!pc.Permissions.CheckPermissions(msg, cr.Trigger, "ActualCustomReactions", + out int index)) + { + if (pc.Verbose) + { + var returnMsg = _strings.GetText( + strs.perm_prevent(index + 1, + Format.Bold(pc.Permissions[index].GetCommand(_cmd.GetPrefix(guild), sg))), + sg.Id); + + try + { + await msg.Channel.SendErrorAsync(_eb, returnMsg).ConfigureAwait(false); + } + catch + { + } + + Log.Information(returnMsg); + } + + return true; + } + } + + var sentMsg = await cr.Send(msg, _client, false).ConfigureAwait(false); + + var reactions = cr.GetReactions(); + foreach (var reaction in reactions) + { + try + { + await sentMsg.AddReactionAsync(reaction.ToIEmote()); + } + catch + { + Log.Warning("Unable to add reactions to message {Message} in server {GuildId}", sentMsg.Id, + cr.GuildId); + break; + } + + await Task.Delay(1000); + } + + if (cr.AutoDeleteTrigger) + { + try + { + await msg.DeleteAsync().ConfigureAwait(false); + } + catch + { + } + } + + Log.Information("s: {GuildId} c: {ChannelId} u: {UserId} | {UserName} executed expression {Expr}", + guild.Id, + msg.Channel.Id, + msg.Author.Id, + msg.Author.ToString(), + cr.Trigger); + + return true; + } + catch (Exception ex) + { + Log.Warning(ex.Message); + } + + return false; + } + + public async Task ResetCrReactions(ulong? maybeGuildId, int id) + { + CustomReaction cr; + using var uow = _db.GetDbContext(); + cr = uow.CustomReactions.GetById(id); + if (cr is null) + return; + + cr.Reactions = string.Empty; + + await uow.SaveChangesAsync(); + } + + private Task UpdateInternalAsync(ulong? maybeGuildId, CustomReaction cr) + { + if (maybeGuildId is ulong guildId) + UpdateInternal(guildId, cr); + else + return _pubSub.Pub(_gcrEditedKey, cr); + + return Task.CompletedTask; + } + + private void UpdateInternal(ulong? maybeGuildId, CustomReaction cr) + { + if (maybeGuildId is ulong guildId) + { + _newGuildReactions.AddOrUpdate(guildId, new[] {cr}, + (key, old) => + { + var newArray = old.ToArray(); + for (var i = 0; i < newArray.Length; i++) + { + if (newArray[i].Id == cr.Id) + newArray[i] = cr; + } + return newArray; + }); + } + else { lock (_gcrWriteLock) { - var newGlobalReactions = DeleteInternal(_globalReactions, id, out _); - _globalReactions = newGlobalReactions; + var crs = _globalReactions; + for (var i = 0; i < crs.Length; i++) + { + if (crs[i].Id == cr.Id) + crs[i] = cr; + } } + } + } - return default; + private Task AddInternalAsync(ulong? maybeGuildId, CustomReaction cr) + { + // only do this for perf purposes + cr.Trigger = cr.Trigger.Replace(MentionPh, _client.CurrentUser.Mention); + + if (maybeGuildId is ulong guildId) + { + _newGuildReactions.AddOrUpdate(guildId, + new[] {cr}, + (key, old) => old.With(cr)); + } + else + { + return _pubSub.Pub(_gcrAddedKey, cr); } - public Task TriggerReloadCustomReactions() - => _pubSub.Pub(_crsReloadedKey, true); + return Task.CompletedTask; + } - #endregion - - #region Client Event Handlers - - private Task OnLeftGuild(SocketGuild arg) + private Task DeleteInternalAsync(ulong? maybeGuildId, int id) + { + if (maybeGuildId is ulong guildId) { - _newGuildReactions.TryRemove(arg.Id, out _); - + _newGuildReactions.AddOrUpdate(guildId, + Array.Empty(), + (key, old) => DeleteInternal(old, id, out _)); + return Task.CompletedTask; } - private async Task OnJoinedGuild(GuildConfig gc) + lock (_gcrWriteLock) { - using var uow = _db.GetDbContext(); - var crs = await uow - .CustomReactions - .AsNoTracking() - .Where(x => x.GuildId == gc.GuildId) - .ToArrayAsync(); - - _newGuildReactions[gc.GuildId] = crs; + var cr = Array.Find(_globalReactions, item => item.Id == id); + if (cr is not null) + { + return _pubSub.Pub(_gcrDeletedkey, cr.Id); + } } - #endregion - - #region Basic Operations + return Task.CompletedTask; + } - public async Task AddAsync(ulong? guildId, string key, string message) + private CustomReaction[] DeleteInternal(IReadOnlyList crs, int id, out CustomReaction deleted) + { + deleted = null; + if (crs is null || crs.Count == 0) + return crs as CustomReaction[] ?? crs?.ToArray(); + + var newCrs = new CustomReaction[crs.Count - 1]; + for (int i = 0, k = 0; i < crs.Count; i++, k++) { - key = key.ToLowerInvariant(); - var cr = new CustomReaction() + if (crs[i].Id == id) { - GuildId = guildId, - Trigger = key, - Response = message, - }; - - if (cr.Response.Contains("%target%", StringComparison.OrdinalIgnoreCase)) - cr.AllowTarget = true; - - using (var uow = _db.GetDbContext()) - { - uow.CustomReactions.Add(cr); - await uow.SaveChangesAsync(); + deleted = crs[i]; + k--; + continue; } - await AddInternalAsync(guildId, cr); - - return cr; + newCrs[k] = crs[i]; } - public async Task EditAsync(ulong? guildId, int id, string message) + return newCrs; + } + + public async Task SetCrReactions(ulong? guildId, int id, IEnumerable emojis) + { + CustomReaction cr; + using (var uow = _db.GetDbContext()) { - using var uow = _db.GetDbContext(); - var cr = uow.CustomReactions.GetById(id); - - if (cr is null || cr.GuildId != guildId) - return null; - - // disable allowtarget if message had target, but it was removed from it - if (!message.Contains("%target%", StringComparison.OrdinalIgnoreCase) - && cr.Response.Contains("%target%", StringComparison.OrdinalIgnoreCase)) - { - cr.AllowTarget = false; - } - - cr.Response = message; - - // enable allow target if message is edited to contain target - if (cr.Response.Contains("%target%", StringComparison.OrdinalIgnoreCase)) - cr.AllowTarget = true; - - await uow.SaveChangesAsync(); - await UpdateInternalAsync(guildId, cr); - - return cr; - } - - - public async Task DeleteAsync(ulong? guildId, int id) - { - using var uow = _db.GetDbContext(); - var toDelete = uow.CustomReactions.GetById(id); - - if (toDelete is null) - return null; - - if ((toDelete.IsGlobal() && guildId is null) || (guildId == toDelete.GuildId)) - { - uow.CustomReactions.Remove(toDelete); - await uow.SaveChangesAsync(); - await DeleteInternalAsync(guildId, id); - return toDelete; - } - - return null; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public CustomReaction[] GetCustomReactionsFor(ulong? maybeGuildId) - { - if (maybeGuildId is ulong guildId) - { - return _newGuildReactions.TryGetValue(guildId, out var crs) - ? crs - : Array.Empty(); - } - - return _globalReactions; - } - - #endregion - - private bool ready; - - private CustomReaction TryGetCustomReaction(IUserMessage umsg) - { - if (!ready) - return null; - - if (!(umsg.Channel is SocketTextChannel channel)) - return null; - - var content = umsg.Content.Trim().ToLowerInvariant(); - - if (_newGuildReactions.TryGetValue(channel.Guild.Id, out var reactions) && reactions.Length > 0) - { - var cr = MatchCustomReactions(content, reactions); - if (cr is not null) - return cr; - } - - var localGrs = _globalReactions; - - return MatchCustomReactions(content, localGrs); - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private CustomReaction MatchCustomReactions(in ReadOnlySpan content, CustomReaction[] crs) - { - var result = new List(1); - for (var i = 0; i < crs.Length; i++) - { - var cr = crs[i]; - var trigger = cr.Trigger; - if (content.Length > trigger.Length) - { - // if input is greater than the trigger, it can only work if: - // it has CA enabled - if (cr.ContainsAnywhere) - { - // if ca is enabled, we have to check if it is a word within the content - var wp = content.GetWordPosition(trigger); - - // if it is, then that's valid - if (wp != WordPosition.None) - { - result.Add(cr); - } - - // if it's not, then it cant' work under any circumstance, - // because content is greater than the trigger length - // so it can't be equal, and it's not contained as a word - continue; - } - - // if CA is disabled, and CR has AllowTarget, then the - // content has to start with the trigger followed by a space - if (cr.AllowTarget && content.StartsWith(trigger, StringComparison.OrdinalIgnoreCase) - && content[trigger.Length] == ' ') - { - result.Add(cr); - } - } - else if (content.Length < cr.Trigger.Length) - { - // if input length is less than trigger length, it means - // that the reaction can never be triggered - } - else - { - // if input length is the same as trigger length - // reaction can only trigger if the strings are equal - if (content.SequenceEqual(cr.Trigger)) - { - result.Add(cr); - } - } - } - - if (result.Count == 0) - return null; - - var cancelled = result.FirstOrDefault(x => x.Response == "-"); - if (cancelled is not null) - return cancelled; - - return result[_rng.Next(0, result.Count)]; - } - - public async Task RunBehavior(IGuild guild, IUserMessage msg) - { - // maybe this message is a custom reaction - var cr = TryGetCustomReaction(msg); - - if (cr is null || cr.Response == "-") - return false; - - if(await _cmdCds.TryBlock(guild, msg.Author, cr.Trigger)) - return false; - - try - { - if (_gperm.BlockedModules.Contains("ActualCustomReactions")) - { - Log.Information("User {UserName} [{UserId}] tried to use a custom reaction but 'ActualCustomReactions' are globally disabled.", - msg.Author.ToString(), - msg.Author.Id); - - return true; - } - - if (guild is SocketGuild sg) - { - var pc = _perms.GetCacheFor(guild.Id); - if (!pc.Permissions.CheckPermissions(msg, cr.Trigger, "ActualCustomReactions", - out int index)) - { - if (pc.Verbose) - { - var returnMsg = _strings.GetText( - strs.perm_prevent(index + 1, - Format.Bold(pc.Permissions[index].GetCommand(_cmd.GetPrefix(guild), sg))), - sg.Id); - - try - { - await msg.Channel.SendErrorAsync(_eb, returnMsg).ConfigureAwait(false); - } - catch - { - } - - Log.Information(returnMsg); - } - - return true; - } - } - - var sentMsg = await cr.Send(msg, _client, false).ConfigureAwait(false); - - var reactions = cr.GetReactions(); - foreach (var reaction in reactions) - { - try - { - await sentMsg.AddReactionAsync(reaction.ToIEmote()); - } - catch - { - Log.Warning("Unable to add reactions to message {Message} in server {GuildId}", sentMsg.Id, - cr.GuildId); - break; - } - - await Task.Delay(1000); - } - - if (cr.AutoDeleteTrigger) - { - try - { - await msg.DeleteAsync().ConfigureAwait(false); - } - catch - { - } - } - - Log.Information("s: {GuildId} c: {ChannelId} u: {UserId} | {UserName} executed expression {Expr}", - guild.Id, - msg.Channel.Id, - msg.Author.Id, - msg.Author.ToString(), - cr.Trigger); - - return true; - } - catch (Exception ex) - { - Log.Warning(ex.Message); - } - - return false; - } - - public async Task ResetCrReactions(ulong? maybeGuildId, int id) - { - CustomReaction cr; - using var uow = _db.GetDbContext(); cr = uow.CustomReactions.GetById(id); if (cr is null) return; - cr.Reactions = string.Empty; + cr.Reactions = string.Join("@@@", emojis); await uow.SaveChangesAsync(); } - private Task UpdateInternalAsync(ulong? maybeGuildId, CustomReaction cr) - { - if (maybeGuildId is ulong guildId) - UpdateInternal(guildId, cr); - else - return _pubSub.Pub(_gcrEditedKey, cr); + await UpdateInternalAsync(guildId, cr); + } - return Task.CompletedTask; + public async Task<(bool Sucess, bool NewValue)> ToggleCrOptionAsync(int id, CrField field) + { + var newVal = false; + CustomReaction cr; + using (var uow = _db.GetDbContext()) + { + cr = uow.CustomReactions.GetById(id); + if (cr is null) + return (false, false); + if (field == CrField.AutoDelete) + newVal = cr.AutoDeleteTrigger = !cr.AutoDeleteTrigger; + else if (field == CrField.ContainsAnywhere) + newVal = cr.ContainsAnywhere = !cr.ContainsAnywhere; + else if (field == CrField.DmResponse) + newVal = cr.DmResponse = !cr.DmResponse; + else if (field == CrField.AllowTarget) + newVal = cr.AllowTarget = !cr.AllowTarget; + + await uow.SaveChangesAsync(); } - private void UpdateInternal(ulong? maybeGuildId, CustomReaction cr) - { - if (maybeGuildId is ulong guildId) - { - _newGuildReactions.AddOrUpdate(guildId, new[] {cr}, - (key, old) => - { - var newArray = old.ToArray(); - for (var i = 0; i < newArray.Length; i++) - { - if (newArray[i].Id == cr.Id) - newArray[i] = cr; - } - return newArray; - }); - } - else - { - lock (_gcrWriteLock) - { - var crs = _globalReactions; - for (var i = 0; i < crs.Length; i++) - { - if (crs[i].Id == cr.Id) - crs[i] = cr; - } - } - } - } + await UpdateInternalAsync(cr.GuildId, cr); - private Task AddInternalAsync(ulong? maybeGuildId, CustomReaction cr) - { - // only do this for perf purposes - cr.Trigger = cr.Trigger.Replace(MentionPh, _client.CurrentUser.Mention); + return (true, newVal); + } - if (maybeGuildId is ulong guildId) - { - _newGuildReactions.AddOrUpdate(guildId, - new[] {cr}, - (key, old) => old.With(cr)); - } - else - { - return _pubSub.Pub(_gcrAddedKey, cr); - } + public CustomReaction GetCustomReaction(ulong? guildId, int id) + { + using var uow = _db.GetDbContext(); + var cr = uow.CustomReactions.GetById(id); + if (cr is null || cr.GuildId != guildId) + return null; - return Task.CompletedTask; - } + return cr; + } - private Task DeleteInternalAsync(ulong? maybeGuildId, int id) - { - if (maybeGuildId is ulong guildId) - { - _newGuildReactions.AddOrUpdate(guildId, - Array.Empty(), - (key, old) => DeleteInternal(old, id, out _)); - - return Task.CompletedTask; - } - - lock (_gcrWriteLock) - { - var cr = Array.Find(_globalReactions, item => item.Id == id); - if (cr is not null) - { - return _pubSub.Pub(_gcrDeletedkey, cr.Id); - } - } - - return Task.CompletedTask; - } - - private CustomReaction[] DeleteInternal(IReadOnlyList crs, int id, out CustomReaction deleted) - { - deleted = null; - if (crs is null || crs.Count == 0) - return crs as CustomReaction[] ?? crs?.ToArray(); + public int DeleteAllCustomReactions(ulong guildId) + { + using var uow = _db.GetDbContext(); + var count = uow.CustomReactions.ClearFromGuild(guildId); + uow.SaveChanges(); - var newCrs = new CustomReaction[crs.Count - 1]; - for (int i = 0, k = 0; i < crs.Count; i++, k++) - { - if (crs[i].Id == id) - { - deleted = crs[i]; - k--; - continue; - } + _newGuildReactions.TryRemove(guildId, out _); - newCrs[k] = crs[i]; - } + return count; + } - return newCrs; - } + public bool ReactionExists(ulong? guildId, string input) + { + using var uow = _db.GetDbContext(); + var cr = uow.CustomReactions.GetByGuildIdAndInput(guildId, input); + return cr != null; + } - public async Task SetCrReactions(ulong? guildId, int id, IEnumerable emojis) - { - CustomReaction cr; - using (var uow = _db.GetDbContext()) - { - cr = uow.CustomReactions.GetById(id); - if (cr is null) - return; + private static readonly ISerializer _exportSerializer = new SerializerBuilder() + .WithEventEmitter(args => new MultilineScalarFlowStyleEmitter(args)) + .WithNamingConvention(YamlDotNet.Serialization.NamingConventions.CamelCaseNamingConvention.Instance) + .WithIndentedSequences() + .ConfigureDefaultValuesHandling(DefaultValuesHandling.OmitDefaults) + .DisableAliases() + .Build(); - cr.Reactions = string.Join("@@@", emojis); - - await uow.SaveChangesAsync(); - } - - await UpdateInternalAsync(guildId, cr); - } - - public async Task<(bool Sucess, bool NewValue)> ToggleCrOptionAsync(int id, CrField field) - { - var newVal = false; - CustomReaction cr; - using (var uow = _db.GetDbContext()) - { - cr = uow.CustomReactions.GetById(id); - if (cr is null) - return (false, false); - if (field == CrField.AutoDelete) - newVal = cr.AutoDeleteTrigger = !cr.AutoDeleteTrigger; - else if (field == CrField.ContainsAnywhere) - newVal = cr.ContainsAnywhere = !cr.ContainsAnywhere; - else if (field == CrField.DmResponse) - newVal = cr.DmResponse = !cr.DmResponse; - else if (field == CrField.AllowTarget) - newVal = cr.AllowTarget = !cr.AllowTarget; - - await uow.SaveChangesAsync(); - } - - await UpdateInternalAsync(cr.GuildId, cr); - - return (true, newVal); - } - - public CustomReaction GetCustomReaction(ulong? guildId, int id) - { - using var uow = _db.GetDbContext(); - var cr = uow.CustomReactions.GetById(id); - if (cr is null || cr.GuildId != guildId) - return null; - - return cr; - } - - public int DeleteAllCustomReactions(ulong guildId) - { - using var uow = _db.GetDbContext(); - var count = uow.CustomReactions.ClearFromGuild(guildId); - uow.SaveChanges(); - - _newGuildReactions.TryRemove(guildId, out _); - - return count; - } - - public bool ReactionExists(ulong? guildId, string input) - { - using var uow = _db.GetDbContext(); - var cr = uow.CustomReactions.GetByGuildIdAndInput(guildId, input); - return cr != null; - } - - private static readonly ISerializer _exportSerializer = new SerializerBuilder() - .WithEventEmitter(args => new MultilineScalarFlowStyleEmitter(args)) - .WithNamingConvention(YamlDotNet.Serialization.NamingConventions.CamelCaseNamingConvention.Instance) - .WithIndentedSequences() - .ConfigureDefaultValuesHandling(DefaultValuesHandling.OmitDefaults) - .DisableAliases() - .Build(); - - private const string _prependExport = - @"# Keys are triggers, Each key has a LIST of custom reactions in the following format: + private const string _prependExport = + @"# Keys are triggers, Each key has a LIST of custom reactions in the following format: # - res: Response string # id: Alphanumeric id used for commands related to the custom reaction. (Note, when using .crsimport, a new id will be generated.) # react: @@ -688,55 +684,54 @@ namespace NadekoBot.Modules.CustomReactions.Services # ad: Whether custom reaction automatically deletes triggering message (see .h .crad) "; - public string ExportCrs(ulong? guildId) - { - var crs = GetCustomReactionsFor(guildId); + public string ExportCrs(ulong? guildId) + { + var crs = GetCustomReactionsFor(guildId); - var crsDict = crs - .GroupBy(x => x.Trigger) - .ToDictionary(x => x.Key, x => x.Select(ExportedExpr.FromModel)); + var crsDict = crs + .GroupBy(x => x.Trigger) + .ToDictionary(x => x.Key, x => x.Select(ExportedExpr.FromModel)); - return _prependExport + _exportSerializer - .Serialize(crsDict) - .UnescapeUnicodeCodePoints(); - } + return _prependExport + _exportSerializer + .Serialize(crsDict) + .UnescapeUnicodeCodePoints(); + } - public async Task ImportCrsAsync(ulong? guildId, string input) + public async Task ImportCrsAsync(ulong? guildId, string input) + { + Dictionary> data; + try { - Dictionary> data; - try - { - data = Yaml.Deserializer.Deserialize>>(input); - if (data.Sum(x => x.Value.Count) == 0) - return false; - } - catch - { + data = Yaml.Deserializer.Deserialize>>(input); + if (data.Sum(x => x.Value.Count) == 0) return false; - } - - using var uow = _db.GetDbContext(); - foreach (var entry in data) - { - var trigger = entry.Key; - await uow.CustomReactions.AddRangeAsync(entry.Value - .Where(cr => !string.IsNullOrWhiteSpace(cr.Res)) - .Select(cr => new CustomReaction() - { - GuildId = guildId, - Response = cr.Res, - Reactions = cr.React?.JoinWith("@@@"), - Trigger = trigger, - AllowTarget = cr.At, - ContainsAnywhere = cr.Ca, - DmResponse = cr.Dm, - AutoDeleteTrigger = cr.Ad, - })); - } - - await uow.SaveChangesAsync(); - await TriggerReloadCustomReactions(); - return true; } + catch + { + return false; + } + + using var uow = _db.GetDbContext(); + foreach (var entry in data) + { + var trigger = entry.Key; + await uow.CustomReactions.AddRangeAsync(entry.Value + .Where(cr => !string.IsNullOrWhiteSpace(cr.Res)) + .Select(cr => new CustomReaction() + { + GuildId = guildId, + Response = cr.Res, + Reactions = cr.React?.JoinWith("@@@"), + Trigger = trigger, + AllowTarget = cr.At, + ContainsAnywhere = cr.Ca, + DmResponse = cr.Dm, + AutoDeleteTrigger = cr.Ad, + })); + } + + await uow.SaveChangesAsync(); + await TriggerReloadCustomReactions(); + return true; } } \ No newline at end of file diff --git a/src/NadekoBot/Modules/Gambling/AnimalRacingCommands.cs b/src/NadekoBot/Modules/Gambling/AnimalRacingCommands.cs index 25d7b9398..ac3c61131 100644 --- a/src/NadekoBot/Modules/Gambling/AnimalRacingCommands.cs +++ b/src/NadekoBot/Modules/Gambling/AnimalRacingCommands.cs @@ -1,6 +1,4 @@ -using System; -using System.Linq; -using System.Threading.Tasks; +using System.Threading.Tasks; using Discord; using Discord.Commands; using Discord.WebSocket; @@ -14,172 +12,171 @@ using NadekoBot.Extensions; using NadekoBot.Modules.Gambling.Common.AnimalRacing.Exceptions; using NadekoBot.Modules.Games.Services; -namespace NadekoBot.Modules.Gambling +namespace NadekoBot.Modules.Gambling; + +// wth is this, needs full rewrite +public partial class Gambling { - // wth is this, needs full rewrite - public partial class Gambling + [Group] + public class AnimalRacingCommands : GamblingSubmodule { - [Group] - public class AnimalRacingCommands : GamblingSubmodule + private readonly ICurrencyService _cs; + private readonly DiscordSocketClient _client; + private readonly GamesConfigService _gamesConf; + + public AnimalRacingCommands(ICurrencyService cs, DiscordSocketClient client, + GamblingConfigService gamblingConf, GamesConfigService gamesConf) : base(gamblingConf) { - private readonly ICurrencyService _cs; - private readonly DiscordSocketClient _client; - private readonly GamesConfigService _gamesConf; + _cs = cs; + _client = client; + _gamesConf = gamesConf; + } - public AnimalRacingCommands(ICurrencyService cs, DiscordSocketClient client, - GamblingConfigService gamblingConf, GamesConfigService gamesConf) : base(gamblingConf) + private IUserMessage raceMessage = null; + + [NadekoCommand, Aliases] + [RequireContext(ContextType.Guild)] + [NadekoOptionsAttribute(typeof(RaceOptions))] + public Task Race(params string[] args) + { + var (options, success) = OptionsParser.ParseFrom(new RaceOptions(), args); + + var ar = new AnimalRace(options, _cs, _gamesConf.Data.RaceAnimals.Shuffle()); + if (!_service.AnimalRaces.TryAdd(ctx.Guild.Id, ar)) + return SendErrorAsync(GetText(strs.animal_race), GetText(strs.animal_race_already_started)); + + ar.Initialize(); + + var count = 0; + Task _client_MessageReceived(SocketMessage arg) { - _cs = cs; - _client = client; - _gamesConf = gamesConf; - } - - private IUserMessage raceMessage = null; - - [NadekoCommand, Aliases] - [RequireContext(ContextType.Guild)] - [NadekoOptionsAttribute(typeof(RaceOptions))] - public Task Race(params string[] args) - { - var (options, success) = OptionsParser.ParseFrom(new RaceOptions(), args); - - var ar = new AnimalRace(options, _cs, _gamesConf.Data.RaceAnimals.Shuffle()); - if (!_service.AnimalRaces.TryAdd(ctx.Guild.Id, ar)) - return SendErrorAsync(GetText(strs.animal_race), GetText(strs.animal_race_already_started)); - - ar.Initialize(); - - var count = 0; - Task _client_MessageReceived(SocketMessage arg) + var _ = Task.Run(() => { - var _ = Task.Run(() => + try { - try + if (arg.Channel.Id == ctx.Channel.Id) { - if (arg.Channel.Id == ctx.Channel.Id) + if (ar.CurrentPhase == AnimalRace.Phase.Running && ++count % 9 == 0) { - if (ar.CurrentPhase == AnimalRace.Phase.Running && ++count % 9 == 0) - { - raceMessage = null; - } + raceMessage = null; } } - catch { } - }); - return Task.CompletedTask; - } - - Task Ar_OnEnded(AnimalRace race) - { - _client.MessageReceived -= _client_MessageReceived; - _service.AnimalRaces.TryRemove(ctx.Guild.Id, out _); - var winner = race.FinishedUsers[0]; - if (race.FinishedUsers[0].Bet > 0) - { - return SendConfirmAsync(GetText(strs.animal_race), - GetText(strs.animal_race_won_money( - Format.Bold(winner.Username), - winner.Animal.Icon, - (race.FinishedUsers[0].Bet * (race.Users.Count - 1)) + CurrencySign))); } - else - { - return SendConfirmAsync(GetText(strs.animal_race), - GetText(strs.animal_race_won(Format.Bold(winner.Username), winner.Animal.Icon))); - } - } - - ar.OnStartingFailed += Ar_OnStartingFailed; - ar.OnStateUpdate += Ar_OnStateUpdate; - ar.OnEnded += Ar_OnEnded; - ar.OnStarted += Ar_OnStarted; - _client.MessageReceived += _client_MessageReceived; - - return SendConfirmAsync(GetText(strs.animal_race), GetText(strs.animal_race_starting(options.StartTime)), - footer: GetText(strs.animal_race_join_instr(Prefix))); + catch { } + }); + return Task.CompletedTask; } - private Task Ar_OnStarted(AnimalRace race) + Task Ar_OnEnded(AnimalRace race) { - if (race.Users.Count == race.MaxUsers) - return SendConfirmAsync(GetText(strs.animal_race), GetText(strs.animal_race_full)); + _client.MessageReceived -= _client_MessageReceived; + _service.AnimalRaces.TryRemove(ctx.Guild.Id, out _); + var winner = race.FinishedUsers[0]; + if (race.FinishedUsers[0].Bet > 0) + { + return SendConfirmAsync(GetText(strs.animal_race), + GetText(strs.animal_race_won_money( + Format.Bold(winner.Username), + winner.Animal.Icon, + (race.FinishedUsers[0].Bet * (race.Users.Count - 1)) + CurrencySign))); + } else - return SendConfirmAsync(GetText(strs.animal_race), GetText(strs.animal_race_starting_with_x(race.Users.Count))); + { + return SendConfirmAsync(GetText(strs.animal_race), + GetText(strs.animal_race_won(Format.Bold(winner.Username), winner.Animal.Icon))); + } } - private async Task Ar_OnStateUpdate(AnimalRace race) - { - var text = $@"|🏁🏁🏁🏁🏁🏁🏁🏁🏁🏁🏁🏁🏁🏁🏁🔚| + ar.OnStartingFailed += Ar_OnStartingFailed; + ar.OnStateUpdate += Ar_OnStateUpdate; + ar.OnEnded += Ar_OnEnded; + ar.OnStarted += Ar_OnStarted; + _client.MessageReceived += _client_MessageReceived; + + return SendConfirmAsync(GetText(strs.animal_race), GetText(strs.animal_race_starting(options.StartTime)), + footer: GetText(strs.animal_race_join_instr(Prefix))); + } + + private Task Ar_OnStarted(AnimalRace race) + { + if (race.Users.Count == race.MaxUsers) + return SendConfirmAsync(GetText(strs.animal_race), GetText(strs.animal_race_full)); + else + return SendConfirmAsync(GetText(strs.animal_race), GetText(strs.animal_race_starting_with_x(race.Users.Count))); + } + + private async Task Ar_OnStateUpdate(AnimalRace race) + { + var text = $@"|🏁🏁🏁🏁🏁🏁🏁🏁🏁🏁🏁🏁🏁🏁🏁🔚| {String.Join("\n", race.Users.Select(p => - { - var index = race.FinishedUsers.IndexOf(p); - var extra = (index == -1 ? "" : $"#{index + 1} {(index == 0 ? "🏆" : "")}"); - return $"{(int)(p.Progress / 60f * 100),-2}%|{new string('‣', p.Progress) + p.Animal.Icon + extra}"; - }))} +{ + var index = race.FinishedUsers.IndexOf(p); + var extra = (index == -1 ? "" : $"#{index + 1} {(index == 0 ? "🏆" : "")}"); + return $"{(int)(p.Progress / 60f * 100),-2}%|{new string('‣', p.Progress) + p.Animal.Icon + extra}"; +}))} |🏁🏁🏁🏁🏁🏁🏁🏁🏁🏁🏁🏁🏁🏁🏁🔚|"; - var msg = raceMessage; + var msg = raceMessage; - if (msg is null) - raceMessage = await SendConfirmAsync(text) - .ConfigureAwait(false); - else - await msg.ModifyAsync(x => x.Embed = _eb.Create() + if (msg is null) + raceMessage = await SendConfirmAsync(text) + .ConfigureAwait(false); + else + await msg.ModifyAsync(x => x.Embed = _eb.Create() .WithTitle(GetText(strs.animal_race)) .WithDescription(text) .WithOkColor() .Build()) - .ConfigureAwait(false); - } + .ConfigureAwait(false); + } - private Task Ar_OnStartingFailed(AnimalRace race) + private Task Ar_OnStartingFailed(AnimalRace race) + { + _service.AnimalRaces.TryRemove(ctx.Guild.Id, out _); + return ReplyErrorLocalizedAsync(strs.animal_race_failed); + } + + [NadekoCommand, Aliases] + [RequireContext(ContextType.Guild)] + public async Task JoinRace(ShmartNumber amount = default) + { + if (!await CheckBetOptional(amount).ConfigureAwait(false)) + return; + + if (!_service.AnimalRaces.TryGetValue(ctx.Guild.Id, out var ar)) { - _service.AnimalRaces.TryRemove(ctx.Guild.Id, out _); - return ReplyErrorLocalizedAsync(strs.animal_race_failed); + await ReplyErrorLocalizedAsync(strs.race_not_exist).ConfigureAwait(false); + return; } - - [NadekoCommand, Aliases] - [RequireContext(ContextType.Guild)] - public async Task JoinRace(ShmartNumber amount = default) + try { - if (!await CheckBetOptional(amount).ConfigureAwait(false)) - return; - - if (!_service.AnimalRaces.TryGetValue(ctx.Guild.Id, out var ar)) - { - await ReplyErrorLocalizedAsync(strs.race_not_exist).ConfigureAwait(false); - return; - } - try - { - var user = await ar.JoinRace(ctx.User.Id, ctx.User.ToString(), amount) - .ConfigureAwait(false); - if (amount > 0) - await SendConfirmAsync(GetText(strs.animal_race_join_bet(ctx.User.Mention, user.Animal.Icon, amount + CurrencySign))); - else - await SendConfirmAsync(GetText(strs.animal_race_join(ctx.User.Mention, user.Animal.Icon))); - } - catch (ArgumentOutOfRangeException) - { - //ignore if user inputed an invalid amount - } - catch (AlreadyJoinedException) - { - // just ignore this - } - catch (AlreadyStartedException) - { - //ignore - } - catch (AnimalRaceFullException) - { - await SendConfirmAsync(GetText(strs.animal_race), GetText(strs.animal_race_full)) - .ConfigureAwait(false); - } - catch (NotEnoughFundsException) - { - await SendErrorAsync(GetText(strs.not_enough(CurrencySign))); - } + var user = await ar.JoinRace(ctx.User.Id, ctx.User.ToString(), amount) + .ConfigureAwait(false); + if (amount > 0) + await SendConfirmAsync(GetText(strs.animal_race_join_bet(ctx.User.Mention, user.Animal.Icon, amount + CurrencySign))); + else + await SendConfirmAsync(GetText(strs.animal_race_join(ctx.User.Mention, user.Animal.Icon))); + } + catch (ArgumentOutOfRangeException) + { + //ignore if user inputed an invalid amount + } + catch (AlreadyJoinedException) + { + // just ignore this + } + catch (AlreadyStartedException) + { + //ignore + } + catch (AnimalRaceFullException) + { + await SendConfirmAsync(GetText(strs.animal_race), GetText(strs.animal_race_full)) + .ConfigureAwait(false); + } + catch (NotEnoughFundsException) + { + await SendErrorAsync(GetText(strs.not_enough(CurrencySign))); } } } diff --git a/src/NadekoBot/Modules/Gambling/BlackJackCommands.cs b/src/NadekoBot/Modules/Gambling/BlackJackCommands.cs index 5b989e9c3..9d6ca7a19 100644 --- a/src/NadekoBot/Modules/Gambling/BlackJackCommands.cs +++ b/src/NadekoBot/Modules/Gambling/BlackJackCommands.cs @@ -6,188 +6,185 @@ using NadekoBot.Modules.Gambling.Common; using NadekoBot.Modules.Gambling.Common.Blackjack; using NadekoBot.Modules.Gambling.Services; using NadekoBot.Extensions; -using System.Linq; using System.Threading.Tasks; using NadekoBot.Services; -using Serilog; -namespace NadekoBot.Modules.Gambling +namespace NadekoBot.Modules.Gambling; + +public partial class Gambling { - public partial class Gambling + public class BlackJackCommands : GamblingSubmodule { - public class BlackJackCommands : GamblingSubmodule + private readonly ICurrencyService _cs; + private readonly DbService _db; + private IUserMessage _msg; + + public enum BjAction { - private readonly ICurrencyService _cs; - private readonly DbService _db; - private IUserMessage _msg; + Hit = int.MinValue, + Stand, + Double, + } - public enum BjAction + public BlackJackCommands(ICurrencyService cs, DbService db, + GamblingConfigService gamblingConf) : base(gamblingConf) + { + _cs = cs; + _db = db; + } + + [NadekoCommand, Aliases] + [RequireContext(ContextType.Guild)] + public async Task BlackJack(ShmartNumber amount) + { + if (!await CheckBetMandatory(amount).ConfigureAwait(false)) + return; + + var newBj = new Blackjack(_cs, _db); + Blackjack bj; + if (newBj == (bj = _service.Games.GetOrAdd(ctx.Channel.Id, newBj))) { - Hit = int.MinValue, - Stand, - Double, - } - - public BlackJackCommands(ICurrencyService cs, DbService db, - GamblingConfigService gamblingConf) : base(gamblingConf) - { - _cs = cs; - _db = db; - } - - [NadekoCommand, Aliases] - [RequireContext(ContextType.Guild)] - public async Task BlackJack(ShmartNumber amount) - { - if (!await CheckBetMandatory(amount).ConfigureAwait(false)) - return; - - var newBj = new Blackjack(_cs, _db); - Blackjack bj; - if (newBj == (bj = _service.Games.GetOrAdd(ctx.Channel.Id, newBj))) + if (!await bj.Join(ctx.User, amount).ConfigureAwait(false)) { - if (!await bj.Join(ctx.User, amount).ConfigureAwait(false)) - { - _service.Games.TryRemove(ctx.Channel.Id, out _); - await ReplyErrorLocalizedAsync(strs.not_enough(CurrencySign)); - return; - } - bj.StateUpdated += Bj_StateUpdated; - bj.GameEnded += Bj_GameEnded; - bj.Start(); - - await ReplyConfirmLocalizedAsync(strs.bj_created).ConfigureAwait(false); + _service.Games.TryRemove(ctx.Channel.Id, out _); + await ReplyErrorLocalizedAsync(strs.not_enough(CurrencySign)); + return; } + bj.StateUpdated += Bj_StateUpdated; + bj.GameEnded += Bj_GameEnded; + bj.Start(); + + await ReplyConfirmLocalizedAsync(strs.bj_created).ConfigureAwait(false); + } + else + { + if (await bj.Join(ctx.User, amount).ConfigureAwait(false)) + await ReplyConfirmLocalizedAsync(strs.bj_joined).ConfigureAwait(false); else { - if (await bj.Join(ctx.User, amount).ConfigureAwait(false)) - await ReplyConfirmLocalizedAsync(strs.bj_joined).ConfigureAwait(false); - else - { - Log.Information($"{ctx.User} can't join a blackjack game as it's in " + bj.State.ToString() + " state already."); - } + Log.Information($"{ctx.User} can't join a blackjack game as it's in " + bj.State.ToString() + " state already."); + } + } + + await ctx.Message.DeleteAsync().ConfigureAwait(false); + } + + private Task Bj_GameEnded(Blackjack arg) + { + _service.Games.TryRemove(ctx.Channel.Id, out _); + return Task.CompletedTask; + } + + private async Task Bj_StateUpdated(Blackjack bj) + { + try + { + if (_msg != null) + { + var _ = _msg.DeleteAsync(); } - await ctx.Message.DeleteAsync().ConfigureAwait(false); - } - - private Task Bj_GameEnded(Blackjack arg) - { - _service.Games.TryRemove(ctx.Channel.Id, out _); - return Task.CompletedTask; - } - - private async Task Bj_StateUpdated(Blackjack bj) - { - try + var c = bj.Dealer.Cards.Select(x => x.GetEmojiString()); + var dealerIcon = "❔ "; + if (bj.State == Blackjack.GameState.Ended) { - if (_msg != null) - { - var _ = _msg.DeleteAsync(); - } + if (bj.Dealer.GetHandValue() == 21) + dealerIcon = "💰 "; + else if (bj.Dealer.GetHandValue() > 21) + dealerIcon = "💥 "; + else + dealerIcon = "🏁 "; + } - var c = bj.Dealer.Cards.Select(x => x.GetEmojiString()); - var dealerIcon = "❔ "; + var cStr = string.Concat(c.Select(x => x.Substring(0, x.Length - 1) + " ")); + cStr += "\n" + string.Concat(c.Select(x => x.Last() + " ")); + var embed = _eb.Create() + .WithOkColor() + .WithTitle("BlackJack") + .AddField($"{dealerIcon} Dealer's Hand | Value: {bj.Dealer.GetHandValue()}", cStr); + + if (bj.CurrentUser != null) + { + embed.WithFooter($"Player to make a choice: {bj.CurrentUser.DiscordUser.ToString()}"); + } + + foreach (var p in bj.Players) + { + c = p.Cards.Select(x => x.GetEmojiString()); + cStr = "-\t" + string.Concat(c.Select(x => x.Substring(0, x.Length - 1) + " ")); + cStr += "\n-\t" + string.Concat(c.Select(x => x.Last() + " ")); + var full = $"{p.DiscordUser.ToString().TrimTo(20)} | Bet: {p.Bet} | Value: {p.GetHandValue()}"; if (bj.State == Blackjack.GameState.Ended) { - if (bj.Dealer.GetHandValue() == 21) - dealerIcon = "💰 "; - else if (bj.Dealer.GetHandValue() > 21) - dealerIcon = "💥 "; - else - dealerIcon = "🏁 "; - } - - var cStr = string.Concat(c.Select(x => x.Substring(0, x.Length - 1) + " ")); - cStr += "\n" + string.Concat(c.Select(x => x.Last() + " ")); - var embed = _eb.Create() - .WithOkColor() - .WithTitle("BlackJack") - .AddField($"{dealerIcon} Dealer's Hand | Value: {bj.Dealer.GetHandValue()}", cStr); - - if (bj.CurrentUser != null) - { - embed.WithFooter($"Player to make a choice: {bj.CurrentUser.DiscordUser.ToString()}"); - } - - foreach (var p in bj.Players) - { - c = p.Cards.Select(x => x.GetEmojiString()); - cStr = "-\t" + string.Concat(c.Select(x => x.Substring(0, x.Length - 1) + " ")); - cStr += "\n-\t" + string.Concat(c.Select(x => x.Last() + " ")); - var full = $"{p.DiscordUser.ToString().TrimTo(20)} | Bet: {p.Bet} | Value: {p.GetHandValue()}"; - if (bj.State == Blackjack.GameState.Ended) + if (p.State == User.UserState.Lost) { - if (p.State == User.UserState.Lost) - { - full = "❌ " + full; - } - else - { - full = "✅ " + full; - } + full = "❌ " + full; + } + else + { + full = "✅ " + full; } - else if (p == bj.CurrentUser) - full = "▶ " + full; - else if (p.State == User.UserState.Stand) - full = "⏹ " + full; - else if (p.State == User.UserState.Bust) - full = "💥 " + full; - else if (p.State == User.UserState.Blackjack) - full = "💰 " + full; - embed.AddField(full, cStr); } - _msg = await ctx.Channel.EmbedAsync(embed).ConfigureAwait(false); - } - catch - { - + else if (p == bj.CurrentUser) + full = "▶ " + full; + else if (p.State == User.UserState.Stand) + full = "⏹ " + full; + else if (p.State == User.UserState.Bust) + full = "💥 " + full; + else if (p.State == User.UserState.Blackjack) + full = "💰 " + full; + embed.AddField(full, cStr); } + _msg = await ctx.Channel.EmbedAsync(embed).ConfigureAwait(false); } - - private string UserToString(User x) + catch { - var playerName = x.State == User.UserState.Bust ? - Format.Strikethrough(x.DiscordUser.ToString().TrimTo(30)) : - x.DiscordUser.ToString(); - var hand = $"{string.Concat(x.Cards.Select(y => "〖" + y.GetEmojiString() + "〗"))}"; - - - return $"{playerName} | Bet: {x.Bet}\n"; - } - - [NadekoCommand, Aliases] - [RequireContext(ContextType.Guild)] - public Task Hit() => InternalBlackJack(BjAction.Hit); - - [NadekoCommand, Aliases] - [RequireContext(ContextType.Guild)] - public Task Stand() => InternalBlackJack(BjAction.Stand); - - [NadekoCommand, Aliases] - [RequireContext(ContextType.Guild)] - public Task Double() => InternalBlackJack(BjAction.Double); - - public async Task InternalBlackJack(BjAction a) - { - if (!_service.Games.TryGetValue(ctx.Channel.Id, out var bj)) - return; - - if (a == BjAction.Hit) - await bj.Hit(ctx.User).ConfigureAwait(false); - else if (a == BjAction.Stand) - await bj.Stand(ctx.User).ConfigureAwait(false); - else if (a == BjAction.Double) - { - if (!await bj.Double(ctx.User).ConfigureAwait(false)) - { - await ReplyErrorLocalizedAsync(strs.not_enough(CurrencySign)); - } - } - - await ctx.Message.DeleteAsync().ConfigureAwait(false); } } + + private string UserToString(User x) + { + var playerName = x.State == User.UserState.Bust ? + Format.Strikethrough(x.DiscordUser.ToString().TrimTo(30)) : + x.DiscordUser.ToString(); + + var hand = $"{string.Concat(x.Cards.Select(y => "〖" + y.GetEmojiString() + "〗"))}"; + + + return $"{playerName} | Bet: {x.Bet}\n"; + } + + [NadekoCommand, Aliases] + [RequireContext(ContextType.Guild)] + public Task Hit() => InternalBlackJack(BjAction.Hit); + + [NadekoCommand, Aliases] + [RequireContext(ContextType.Guild)] + public Task Stand() => InternalBlackJack(BjAction.Stand); + + [NadekoCommand, Aliases] + [RequireContext(ContextType.Guild)] + public Task Double() => InternalBlackJack(BjAction.Double); + + public async Task InternalBlackJack(BjAction a) + { + if (!_service.Games.TryGetValue(ctx.Channel.Id, out var bj)) + return; + + if (a == BjAction.Hit) + await bj.Hit(ctx.User).ConfigureAwait(false); + else if (a == BjAction.Stand) + await bj.Stand(ctx.User).ConfigureAwait(false); + else if (a == BjAction.Double) + { + if (!await bj.Double(ctx.User).ConfigureAwait(false)) + { + await ReplyErrorLocalizedAsync(strs.not_enough(CurrencySign)); + } + } + + await ctx.Message.DeleteAsync().ConfigureAwait(false); + } } -} +} \ No newline at end of file diff --git a/src/NadekoBot/Modules/Gambling/Common/AnimalRacing/AnimalRace.cs b/src/NadekoBot/Modules/Gambling/Common/AnimalRacing/AnimalRace.cs index 71319b5a9..a43048bff 100644 --- a/src/NadekoBot/Modules/Gambling/Common/AnimalRacing/AnimalRace.cs +++ b/src/NadekoBot/Modules/Gambling/Common/AnimalRacing/AnimalRace.cs @@ -1,163 +1,156 @@ -using System; -using System.Collections.Generic; -using System.Collections.Immutable; -using System.Linq; -using System.Threading; +using System.Threading; using System.Threading.Tasks; using NadekoBot.Common; -using NadekoBot.Modules.Gambling.Common.AnimalRacing; using NadekoBot.Services; -using NadekoBot.Services.Database.Models; using NadekoBot.Extensions; using NadekoBot.Modules.Gambling.Common.AnimalRacing.Exceptions; using NadekoBot.Modules.Games.Common; -namespace NadekoBot.Modules.Gambling.Common.AnimalRacing +namespace NadekoBot.Modules.Gambling.Common.AnimalRacing; + +public sealed class AnimalRace : IDisposable { - public sealed class AnimalRace : IDisposable + public enum Phase { - public enum Phase + WaitingForPlayers, + Running, + Ended, + } + + public Phase CurrentPhase { get; private set; } = Phase.WaitingForPlayers; + + public event Func OnStarted = delegate { return Task.CompletedTask; }; + public event Func OnStartingFailed = delegate { return Task.CompletedTask; }; + public event Func OnStateUpdate = delegate { return Task.CompletedTask; }; + public event Func OnEnded = delegate { return Task.CompletedTask; }; + + public IReadOnlyCollection Users => _users.ToList(); + public List FinishedUsers { get; } = new List(); + + private readonly SemaphoreSlim _locker = new SemaphoreSlim(1, 1); + private readonly HashSet _users = new HashSet(); + private readonly ICurrencyService _currency; + private readonly RaceOptions _options; + private readonly Queue _animalsQueue; + public int MaxUsers { get; } + + public AnimalRace(RaceOptions options, ICurrencyService currency, IEnumerable availableAnimals) + { + this._currency = currency; + this._options = options; + this._animalsQueue = new Queue(availableAnimals); + this.MaxUsers = _animalsQueue.Count; + + if (this._animalsQueue.Count == 0) + CurrentPhase = Phase.Ended; + } + + public void Initialize() //lame name + { + var _t = Task.Run(async () => { - WaitingForPlayers, - Running, - Ended, - } - - public Phase CurrentPhase { get; private set; } = Phase.WaitingForPlayers; - - public event Func OnStarted = delegate { return Task.CompletedTask; }; - public event Func OnStartingFailed = delegate { return Task.CompletedTask; }; - public event Func OnStateUpdate = delegate { return Task.CompletedTask; }; - public event Func OnEnded = delegate { return Task.CompletedTask; }; - - public IReadOnlyCollection Users => _users.ToList(); - public List FinishedUsers { get; } = new List(); - - private readonly SemaphoreSlim _locker = new SemaphoreSlim(1, 1); - private readonly HashSet _users = new HashSet(); - private readonly ICurrencyService _currency; - private readonly RaceOptions _options; - private readonly Queue _animalsQueue; - public int MaxUsers { get; } - - public AnimalRace(RaceOptions options, ICurrencyService currency, IEnumerable availableAnimals) - { - this._currency = currency; - this._options = options; - this._animalsQueue = new Queue(availableAnimals); - this.MaxUsers = _animalsQueue.Count; - - if (this._animalsQueue.Count == 0) - CurrentPhase = Phase.Ended; - } - - public void Initialize() //lame name - { - var _t = Task.Run(async () => - { - await Task.Delay(_options.StartTime * 1000).ConfigureAwait(false); - - await _locker.WaitAsync().ConfigureAwait(false); - try - { - if (CurrentPhase != Phase.WaitingForPlayers) - return; - - await Start().ConfigureAwait(false); - } - finally { _locker.Release(); } - }); - } - - public async Task JoinRace(ulong userId, string userName, long bet = 0) - { - if (bet < 0) - throw new ArgumentOutOfRangeException(nameof(bet)); - - var user = new AnimalRacingUser(userName, userId, bet); + await Task.Delay(_options.StartTime * 1000).ConfigureAwait(false); await _locker.WaitAsync().ConfigureAwait(false); try { - if (_users.Count == MaxUsers) - throw new AnimalRaceFullException(); - if (CurrentPhase != Phase.WaitingForPlayers) - throw new AlreadyStartedException(); + return; - if (!await _currency.RemoveAsync(userId, "BetRace", bet).ConfigureAwait(false)) - throw new NotEnoughFundsException(); - - if (_users.Contains(user)) - throw new AlreadyJoinedException(); - - var animal = _animalsQueue.Dequeue(); - user.Animal = animal; - _users.Add(user); - - if (_animalsQueue.Count == 0) //start if no more spots left - await Start().ConfigureAwait(false); - - return user; + await Start().ConfigureAwait(false); } finally { _locker.Release(); } + }); + } + + public async Task JoinRace(ulong userId, string userName, long bet = 0) + { + if (bet < 0) + throw new ArgumentOutOfRangeException(nameof(bet)); + + var user = new AnimalRacingUser(userName, userId, bet); + + await _locker.WaitAsync().ConfigureAwait(false); + try + { + if (_users.Count == MaxUsers) + throw new AnimalRaceFullException(); + + if (CurrentPhase != Phase.WaitingForPlayers) + throw new AlreadyStartedException(); + + if (!await _currency.RemoveAsync(userId, "BetRace", bet).ConfigureAwait(false)) + throw new NotEnoughFundsException(); + + if (_users.Contains(user)) + throw new AlreadyJoinedException(); + + var animal = _animalsQueue.Dequeue(); + user.Animal = animal; + _users.Add(user); + + if (_animalsQueue.Count == 0) //start if no more spots left + await Start().ConfigureAwait(false); + + return user; + } + finally { _locker.Release(); } + } + + private async Task Start() + { + CurrentPhase = Phase.Running; + if (_users.Count <= 1) + { + foreach (var user in _users) + { + if (user.Bet > 0) + await _currency.AddAsync(user.UserId, "Race refund", user.Bet).ConfigureAwait(false); + } + + var _sf = OnStartingFailed?.Invoke(this); + CurrentPhase = Phase.Ended; + return; } - private async Task Start() + var _ = OnStarted?.Invoke(this); + var _t = Task.Run(async () => { - CurrentPhase = Phase.Running; - if (_users.Count <= 1) + var rng = new NadekoRandom(); + while (!_users.All(x => x.Progress >= 60)) { foreach (var user in _users) { - if (user.Bet > 0) - await _currency.AddAsync(user.UserId, "Race refund", user.Bet).ConfigureAwait(false); + user.Progress += rng.Next(1, 11); + if (user.Progress >= 60) + user.Progress = 60; } - var _sf = OnStartingFailed?.Invoke(this); - CurrentPhase = Phase.Ended; - return; + var finished = _users.Where(x => x.Progress >= 60 && !FinishedUsers.Contains(x)) + .Shuffle(); + + FinishedUsers.AddRange(finished); + + var _ignore = OnStateUpdate?.Invoke(this); + await Task.Delay(2500).ConfigureAwait(false); } - var _ = OnStarted?.Invoke(this); - var _t = Task.Run(async () => - { - var rng = new NadekoRandom(); - while (!_users.All(x => x.Progress >= 60)) - { - foreach (var user in _users) - { - user.Progress += rng.Next(1, 11); - if (user.Progress >= 60) - user.Progress = 60; - } + if (FinishedUsers[0].Bet > 0) + await _currency.AddAsync(FinishedUsers[0].UserId, "Won a Race", FinishedUsers[0].Bet * (_users.Count - 1)) + .ConfigureAwait(false); - var finished = _users.Where(x => x.Progress >= 60 && !FinishedUsers.Contains(x)) - .Shuffle(); + var _ended = OnEnded?.Invoke(this); + }); + } - FinishedUsers.AddRange(finished); - - var _ignore = OnStateUpdate?.Invoke(this); - await Task.Delay(2500).ConfigureAwait(false); - } - - if (FinishedUsers[0].Bet > 0) - await _currency.AddAsync(FinishedUsers[0].UserId, "Won a Race", FinishedUsers[0].Bet * (_users.Count - 1)) - .ConfigureAwait(false); - - var _ended = OnEnded?.Invoke(this); - }); - } - - public void Dispose() - { - CurrentPhase = Phase.Ended; - OnStarted = null; - OnEnded = null; - OnStartingFailed = null; - OnStateUpdate = null; - _locker.Dispose(); - _users.Clear(); - } + public void Dispose() + { + CurrentPhase = Phase.Ended; + OnStarted = null; + OnEnded = null; + OnStartingFailed = null; + OnStateUpdate = null; + _locker.Dispose(); + _users.Clear(); } } \ No newline at end of file diff --git a/src/NadekoBot/Modules/Gambling/Common/AnimalRacing/AnimalRacingUser.cs b/src/NadekoBot/Modules/Gambling/Common/AnimalRacing/AnimalRacingUser.cs index ca0623fc5..e15447ca7 100644 --- a/src/NadekoBot/Modules/Gambling/Common/AnimalRacing/AnimalRacingUser.cs +++ b/src/NadekoBot/Modules/Gambling/Common/AnimalRacing/AnimalRacingUser.cs @@ -1,32 +1,31 @@ using NadekoBot.Modules.Games.Common; -namespace NadekoBot.Modules.Gambling.Common.AnimalRacing +namespace NadekoBot.Modules.Gambling.Common.AnimalRacing; + +public class AnimalRacingUser { - public class AnimalRacingUser + public long Bet { get; } + public string Username { get; } + public ulong UserId { get; } + public RaceAnimal Animal { get; set; } + public int Progress { get; set; } + + public AnimalRacingUser(string username, ulong userId, long bet) { - public long Bet { get; } - public string Username { get; } - public ulong UserId { get; } - public RaceAnimal Animal { get; set; } - public int Progress { get; set; } - - public AnimalRacingUser(string username, ulong userId, long bet) - { - this.Bet = bet; - this.Username = username; - this.UserId = userId; - } - - public override bool Equals(object obj) - { - return obj is AnimalRacingUser x - ? x.UserId == this.UserId - : false; - } - - public override int GetHashCode() - { - return this.UserId.GetHashCode(); - } + this.Bet = bet; + this.Username = username; + this.UserId = userId; } -} + + public override bool Equals(object obj) + { + return obj is AnimalRacingUser x + ? x.UserId == this.UserId + : false; + } + + public override int GetHashCode() + { + return this.UserId.GetHashCode(); + } +} \ No newline at end of file diff --git a/src/NadekoBot/Modules/Gambling/Common/AnimalRacing/Exceptions/AlreadyJoinedException.cs b/src/NadekoBot/Modules/Gambling/Common/AnimalRacing/Exceptions/AlreadyJoinedException.cs index 8abb59a30..6638b5d50 100644 --- a/src/NadekoBot/Modules/Gambling/Common/AnimalRacing/Exceptions/AlreadyJoinedException.cs +++ b/src/NadekoBot/Modules/Gambling/Common/AnimalRacing/Exceptions/AlreadyJoinedException.cs @@ -1,20 +1,17 @@ -using System; +namespace NadekoBot.Modules.Gambling.Common.AnimalRacing.Exceptions; -namespace NadekoBot.Modules.Gambling.Common.AnimalRacing.Exceptions +public class AlreadyJoinedException : Exception { - public class AlreadyJoinedException : Exception + public AlreadyJoinedException() { - public AlreadyJoinedException() - { - } - - public AlreadyJoinedException(string message) : base(message) - { - } - - public AlreadyJoinedException(string message, Exception innerException) : base(message, innerException) - { - } } -} + + public AlreadyJoinedException(string message) : base(message) + { + } + + public AlreadyJoinedException(string message, Exception innerException) : base(message, innerException) + { + } +} \ No newline at end of file diff --git a/src/NadekoBot/Modules/Gambling/Common/AnimalRacing/Exceptions/AlreadyStartedException.cs b/src/NadekoBot/Modules/Gambling/Common/AnimalRacing/Exceptions/AlreadyStartedException.cs index b89d68a0e..537d51a81 100644 --- a/src/NadekoBot/Modules/Gambling/Common/AnimalRacing/Exceptions/AlreadyStartedException.cs +++ b/src/NadekoBot/Modules/Gambling/Common/AnimalRacing/Exceptions/AlreadyStartedException.cs @@ -1,19 +1,16 @@ -using System; +namespace NadekoBot.Modules.Gambling.Common.AnimalRacing.Exceptions; -namespace NadekoBot.Modules.Gambling.Common.AnimalRacing.Exceptions +public class AlreadyStartedException : Exception { - public class AlreadyStartedException : Exception + public AlreadyStartedException() { - public AlreadyStartedException() - { - } - - public AlreadyStartedException(string message) : base(message) - { - } - - public AlreadyStartedException(string message, Exception innerException) : base(message, innerException) - { - } } -} + + public AlreadyStartedException(string message) : base(message) + { + } + + public AlreadyStartedException(string message, Exception innerException) : base(message, innerException) + { + } +} \ No newline at end of file diff --git a/src/NadekoBot/Modules/Gambling/Common/AnimalRacing/Exceptions/AnimalRaceFullException.cs b/src/NadekoBot/Modules/Gambling/Common/AnimalRacing/Exceptions/AnimalRaceFullException.cs index da2f3dfa2..d8dafe293 100644 --- a/src/NadekoBot/Modules/Gambling/Common/AnimalRacing/Exceptions/AnimalRaceFullException.cs +++ b/src/NadekoBot/Modules/Gambling/Common/AnimalRacing/Exceptions/AnimalRaceFullException.cs @@ -1,19 +1,16 @@ -using System; +namespace NadekoBot.Modules.Gambling.Common.AnimalRacing.Exceptions; -namespace NadekoBot.Modules.Gambling.Common.AnimalRacing.Exceptions +public class AnimalRaceFullException : Exception { - public class AnimalRaceFullException : Exception + public AnimalRaceFullException() { - public AnimalRaceFullException() - { - } - - public AnimalRaceFullException(string message) : base(message) - { - } - - public AnimalRaceFullException(string message, Exception innerException) : base(message, innerException) - { - } } -} + + public AnimalRaceFullException(string message) : base(message) + { + } + + public AnimalRaceFullException(string message, Exception innerException) : base(message, innerException) + { + } +} \ No newline at end of file diff --git a/src/NadekoBot/Modules/Gambling/Common/AnimalRacing/Exceptions/NotEnoughFundsException.cs b/src/NadekoBot/Modules/Gambling/Common/AnimalRacing/Exceptions/NotEnoughFundsException.cs index 7b394880b..d0ba2557c 100644 --- a/src/NadekoBot/Modules/Gambling/Common/AnimalRacing/Exceptions/NotEnoughFundsException.cs +++ b/src/NadekoBot/Modules/Gambling/Common/AnimalRacing/Exceptions/NotEnoughFundsException.cs @@ -1,19 +1,16 @@ -using System; +namespace NadekoBot.Modules.Gambling.Common.AnimalRacing.Exceptions; -namespace NadekoBot.Modules.Gambling.Common.AnimalRacing.Exceptions +public class NotEnoughFundsException : Exception { - public class NotEnoughFundsException : Exception + public NotEnoughFundsException() { - public NotEnoughFundsException() - { - } - - public NotEnoughFundsException(string message) : base(message) - { - } - - public NotEnoughFundsException(string message, Exception innerException) : base(message, innerException) - { - } } -} + + public NotEnoughFundsException(string message) : base(message) + { + } + + public NotEnoughFundsException(string message, Exception innerException) : base(message, innerException) + { + } +} \ No newline at end of file diff --git a/src/NadekoBot/Modules/Gambling/Common/AnimalRacing/RaceOptions.cs b/src/NadekoBot/Modules/Gambling/Common/AnimalRacing/RaceOptions.cs index 4f7c440f0..19121f37b 100644 --- a/src/NadekoBot/Modules/Gambling/Common/AnimalRacing/RaceOptions.cs +++ b/src/NadekoBot/Modules/Gambling/Common/AnimalRacing/RaceOptions.cs @@ -1,17 +1,16 @@ using CommandLine; using NadekoBot.Common; -namespace NadekoBot.Modules.Gambling.Common.AnimalRacing -{ - public class RaceOptions : INadekoCommandOptions - { - [Option('s', "start-time", Default = 20, Required = false)] - public int StartTime { get; set; } = 20; +namespace NadekoBot.Modules.Gambling.Common.AnimalRacing; - public void NormalizeOptions() - { - if (this.StartTime < 10 || this.StartTime > 120) - this.StartTime = 20; - } +public class RaceOptions : INadekoCommandOptions +{ + [Option('s', "start-time", Default = 20, Required = false)] + public int StartTime { get; set; } = 20; + + public void NormalizeOptions() + { + if (this.StartTime < 10 || this.StartTime > 120) + this.StartTime = 20; } } \ No newline at end of file diff --git a/src/NadekoBot/Modules/Gambling/Common/BetRoll.cs b/src/NadekoBot/Modules/Gambling/Common/BetRoll.cs index 60f38d80b..18ca1bab4 100644 --- a/src/NadekoBot/Modules/Gambling/Common/BetRoll.cs +++ b/src/NadekoBot/Modules/Gambling/Common/BetRoll.cs @@ -1,47 +1,43 @@ -using System; -using System.Linq; +namespace NadekoBot.Modules.Gambling.Common; -namespace NadekoBot.Modules.Gambling.Common +public class Betroll { - public class Betroll + public class Result { - public class Result - { - public int Roll { get; set; } - public float Multiplier { get; set; } - public int Threshold { get; set; } - } + public int Roll { get; set; } + public float Multiplier { get; set; } + public int Threshold { get; set; } + } - private readonly IOrderedEnumerable _thresholdPairs; - private readonly Random _rng; + private readonly IOrderedEnumerable _thresholdPairs; + private readonly Random _rng; - public Betroll(BetRollConfig settings) + public Betroll(BetRollConfig settings) + { + _thresholdPairs = settings.Pairs.OrderByDescending(x => x.WhenAbove); + _rng = new Random(); + } + + public Result Roll() + { + var roll = _rng.Next(0, 101); + + var pair = _thresholdPairs.FirstOrDefault(x => x.WhenAbove < roll); + if (pair is null) { - _thresholdPairs = settings.Pairs.OrderByDescending(x => x.WhenAbove); - _rng = new Random(); - } - - public Result Roll() - { - var roll = _rng.Next(0, 101); - - var pair = _thresholdPairs.FirstOrDefault(x => x.WhenAbove < roll); - if (pair is null) - { - return new Result - { - Multiplier = 0, - Roll = roll, - }; - } - return new Result { - Multiplier = pair.MultiplyBy, + Multiplier = 0, Roll = roll, - Threshold = pair.WhenAbove, }; } + + return new Result + { + Multiplier = pair.MultiplyBy, + Roll = roll, + Threshold = pair.WhenAbove, + }; } } \ No newline at end of file diff --git a/src/NadekoBot/Modules/Gambling/Common/Blackjack/Blackjack.cs b/src/NadekoBot/Modules/Gambling/Common/Blackjack/Blackjack.cs index b2ccb7759..a61d70000 100644 --- a/src/NadekoBot/Modules/Gambling/Common/Blackjack/Blackjack.cs +++ b/src/NadekoBot/Modules/Gambling/Common/Blackjack/Blackjack.cs @@ -1,353 +1,347 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading; +using System.Threading; using System.Threading.Tasks; using Discord; using NadekoBot.Services; -using NadekoBot.Modules.Gambling.Common; -using Serilog; -namespace NadekoBot.Modules.Gambling.Common.Blackjack +namespace NadekoBot.Modules.Gambling.Common.Blackjack; + +public class Blackjack { - public class Blackjack + public enum GameState { - public enum GameState - { - Starting, - Playing, - Ended - } + Starting, + Playing, + Ended + } - private Deck Deck { get; set; } = new QuadDeck(); - public Dealer Dealer { get; set; } + private Deck Deck { get; set; } = new QuadDeck(); + public Dealer Dealer { get; set; } - public List Players { get; set; } = new List(); - public GameState State { get; set; } = GameState.Starting; - public User CurrentUser { get; private set; } + public List Players { get; set; } = new List(); + public GameState State { get; set; } = GameState.Starting; + public User CurrentUser { get; private set; } - private TaskCompletionSource _currentUserMove; - private readonly ICurrencyService _cs; - private readonly DbService _db; + private TaskCompletionSource _currentUserMove; + private readonly ICurrencyService _cs; + private readonly DbService _db; - public event Func StateUpdated; - public event Func GameEnded; + public event Func StateUpdated; + public event Func GameEnded; - private readonly SemaphoreSlim locker = new SemaphoreSlim(1, 1); + private readonly SemaphoreSlim locker = new SemaphoreSlim(1, 1); - public Blackjack(ICurrencyService cs, DbService db) - { - _cs = cs; - _db = db; - Dealer = new Dealer(); - } - - public void Start() - { - var _ = GameLoop(); - } - - public async Task GameLoop() + public Blackjack(ICurrencyService cs, DbService db) + { + _cs = cs; + _db = db; + Dealer = new Dealer(); + } + + public void Start() + { + var _ = GameLoop(); + } + + public async Task GameLoop() + { + try { + //wait for players to join + await Task.Delay(20000).ConfigureAwait(false); + await locker.WaitAsync().ConfigureAwait(false); try { - //wait for players to join - await Task.Delay(20000).ConfigureAwait(false); - await locker.WaitAsync().ConfigureAwait(false); - try - { - State = GameState.Playing; - } - finally - { - locker.Release(); - } - await PrintState().ConfigureAwait(false); - //if no users joined the game, end it - if (!Players.Any()) - { - State = GameState.Ended; - var end = GameEnded?.Invoke(this); - return; - } - //give 1 card to the dealer and 2 to each player - Dealer.Cards.Add(Deck.Draw()); - foreach (var usr in Players) - { - usr.Cards.Add(Deck.Draw()); - usr.Cards.Add(Deck.Draw()); - - if (usr.GetHandValue() == 21) - usr.State = User.UserState.Blackjack; - } - //go through all users and ask them what they want to do - foreach (var usr in Players.Where(x => !x.Done)) - { - while (!usr.Done) - { - Log.Information($"Waiting for {usr.DiscordUser}'s move"); - await PromptUserMove(usr).ConfigureAwait(false); - } - } - await PrintState().ConfigureAwait(false); - State = GameState.Ended; - await Task.Delay(2500).ConfigureAwait(false); - Log.Information("Dealer moves"); - await DealerMoves().ConfigureAwait(false); - await PrintState().ConfigureAwait(false); - var _ = GameEnded?.Invoke(this); + State = GameState.Playing; } - catch (Exception ex) + finally { - Log.Error(ex, "REPORT THE MESSAGE BELOW IN #NadekoLog SERVER PLEASE"); - State = GameState.Ended; - var _ = GameEnded?.Invoke(this); + locker.Release(); } - } - - private async Task PromptUserMove(User usr) - { - var pause = Task.Delay(20000); //10 seconds to decide - CurrentUser = usr; - _currentUserMove = new TaskCompletionSource(); await PrintState().ConfigureAwait(false); - // either wait for the user to make an action and - // if he doesn't - stand - var finished = await Task.WhenAny(pause, _currentUserMove.Task).ConfigureAwait(false); - if (finished == pause) + //if no users joined the game, end it + if (!Players.Any()) { - await Stand(usr).ConfigureAwait(false); + State = GameState.Ended; + var end = GameEnded?.Invoke(this); + return; } - CurrentUser = null; - _currentUserMove = null; - } - - public async Task Join(IUser user, long bet) - { - await locker.WaitAsync().ConfigureAwait(false); - try + //give 1 card to the dealer and 2 to each player + Dealer.Cards.Add(Deck.Draw()); + foreach (var usr in Players) { - if (State != GameState.Starting) - return false; + usr.Cards.Add(Deck.Draw()); + usr.Cards.Add(Deck.Draw()); - if (Players.Count >= 5) - return false; - - if (!await _cs.RemoveAsync(user, "BlackJack-gamble", bet, gamble: true).ConfigureAwait(false)) + if (usr.GetHandValue() == 21) + usr.State = User.UserState.Blackjack; + } + //go through all users and ask them what they want to do + foreach (var usr in Players.Where(x => !x.Done)) + { + while (!usr.Done) { - return false; + Log.Information($"Waiting for {usr.DiscordUser}'s move"); + await PromptUserMove(usr).ConfigureAwait(false); } - - Players.Add(new User(user, bet)); - var _ = PrintState(); - return true; - } - finally - { - locker.Release(); } + await PrintState().ConfigureAwait(false); + State = GameState.Ended; + await Task.Delay(2500).ConfigureAwait(false); + Log.Information("Dealer moves"); + await DealerMoves().ConfigureAwait(false); + await PrintState().ConfigureAwait(false); + var _ = GameEnded?.Invoke(this); } - - public async Task Stand(IUser u) + catch (Exception ex) { - var cu = CurrentUser; + Log.Error(ex, "REPORT THE MESSAGE BELOW IN #NadekoLog SERVER PLEASE"); + State = GameState.Ended; + var _ = GameEnded?.Invoke(this); + } + } - if (cu != null && cu.DiscordUser == u) - return await Stand(cu).ConfigureAwait(false); + private async Task PromptUserMove(User usr) + { + var pause = Task.Delay(20000); //10 seconds to decide + CurrentUser = usr; + _currentUserMove = new TaskCompletionSource(); + await PrintState().ConfigureAwait(false); + // either wait for the user to make an action and + // if he doesn't - stand + var finished = await Task.WhenAny(pause, _currentUserMove.Task).ConfigureAwait(false); + if (finished == pause) + { + await Stand(usr).ConfigureAwait(false); + } + CurrentUser = null; + _currentUserMove = null; + } + + public async Task Join(IUser user, long bet) + { + await locker.WaitAsync().ConfigureAwait(false); + try + { + if (State != GameState.Starting) + return false; + + if (Players.Count >= 5) + return false; + + if (!await _cs.RemoveAsync(user, "BlackJack-gamble", bet, gamble: true).ConfigureAwait(false)) + { + return false; + } + + Players.Add(new User(user, bet)); + var _ = PrintState(); + return true; + } + finally + { + locker.Release(); + } + } + + public async Task Stand(IUser u) + { + var cu = CurrentUser; + + if (cu != null && cu.DiscordUser == u) + return await Stand(cu).ConfigureAwait(false); - return false; + return false; + } + + public async Task Stand(User u) + { + await locker.WaitAsync().ConfigureAwait(false); + try + { + if (State != GameState.Playing) + return false; + + if (CurrentUser != u) + return false; + + u.State = User.UserState.Stand; + _currentUserMove.TrySetResult(true); + return true; + } + finally + { + locker.Release(); + } + } + + private async Task DealerMoves() + { + var hw = Dealer.GetHandValue(); + while (hw < 17 + || (hw == 17 && Dealer.Cards.Count(x => x.Number == 1) > (Dealer.GetRawHandValue() - 17) / 10))// hit on soft 17 + { + /* Dealer has + A 6 + That's 17, soft + hw == 17 => true + number of aces = 1 + 1 > 17-17 /10 => true + + AA 5 + That's 17, again soft, since one ace is worth 11, even though another one is 1 + hw == 17 => true + number of aces = 2 + 2 > 27 - 17 / 10 => true + + AA Q 5 + That's 17, but not soft, since both aces are worth 1 + hw == 17 => true + number of aces = 2 + 2 > 37 - 17 / 10 => false + * */ + Dealer.Cards.Add(Deck.Draw()); + hw = Dealer.GetHandValue(); } - public async Task Stand(User u) + if (hw > 21) { - await locker.WaitAsync().ConfigureAwait(false); - try + foreach (var usr in Players) { - if (State != GameState.Playing) - return false; - - if (CurrentUser != u) - return false; - - u.State = User.UserState.Stand; - _currentUserMove.TrySetResult(true); - return true; + if (usr.State == User.UserState.Stand || usr.State == User.UserState.Blackjack) + usr.State = User.UserState.Won; + else + usr.State = User.UserState.Lost; } - finally + } + else + { + foreach (var usr in Players) { - locker.Release(); + if (usr.State == User.UserState.Blackjack) + usr.State = User.UserState.Won; + else if (usr.State == User.UserState.Stand) + usr.State = hw < usr.GetHandValue() + ? User.UserState.Won + : User.UserState.Lost; + else + usr.State = User.UserState.Lost; } } - private async Task DealerMoves() + foreach (var usr in Players) { - var hw = Dealer.GetHandValue(); - while (hw < 17 - || (hw == 17 && Dealer.Cards.Count(x => x.Number == 1) > (Dealer.GetRawHandValue() - 17) / 10))// hit on soft 17 + if (usr.State == User.UserState.Won || usr.State == User.UserState.Blackjack) { - /* Dealer has - A 6 - That's 17, soft - hw == 17 => true - number of aces = 1 - 1 > 17-17 /10 => true - - AA 5 - That's 17, again soft, since one ace is worth 11, even though another one is 1 - hw == 17 => true - number of aces = 2 - 2 > 27 - 17 / 10 => true - - AA Q 5 - That's 17, but not soft, since both aces are worth 1 - hw == 17 => true - number of aces = 2 - 2 > 37 - 17 / 10 => false - * */ - Dealer.Cards.Add(Deck.Draw()); - hw = Dealer.GetHandValue(); + await _cs.AddAsync(usr.DiscordUser.Id, "BlackJack-win", usr.Bet * 2, gamble: true).ConfigureAwait(false); } + } + } - if (hw > 21) + public async Task Double(IUser u) + { + var cu = CurrentUser; + + if (cu != null && cu.DiscordUser == u) + return await Double(cu).ConfigureAwait(false); + + return false; + } + + public async Task Double(User u) + { + await locker.WaitAsync().ConfigureAwait(false); + try + { + if (State != GameState.Playing) + return false; + + if (CurrentUser != u) + return false; + + if (!await _cs.RemoveAsync(u.DiscordUser.Id, "Blackjack-double", u.Bet).ConfigureAwait(false)) + return false; + + u.Bet *= 2; + + u.Cards.Add(Deck.Draw()); + + if (u.GetHandValue() == 21) { - foreach (var usr in Players) - { - if (usr.State == User.UserState.Stand || usr.State == User.UserState.Blackjack) - usr.State = User.UserState.Won; - else - usr.State = User.UserState.Lost; - } + //blackjack + u.State = User.UserState.Blackjack; + } + else if (u.GetHandValue() > 21) + { + // user busted + u.State = User.UserState.Bust; } else { - foreach (var usr in Players) - { - if (usr.State == User.UserState.Blackjack) - usr.State = User.UserState.Won; - else if (usr.State == User.UserState.Stand) - usr.State = hw < usr.GetHandValue() - ? User.UserState.Won - : User.UserState.Lost; - else - usr.State = User.UserState.Lost; - } + //with double you just get one card, and then you're done + u.State = User.UserState.Stand; } + _currentUserMove.TrySetResult(true); - foreach (var usr in Players) - { - if (usr.State == User.UserState.Won || usr.State == User.UserState.Blackjack) - { - await _cs.AddAsync(usr.DiscordUser.Id, "BlackJack-win", usr.Bet * 2, gamble: true).ConfigureAwait(false); - } - } + return true; } - - public async Task Double(IUser u) + finally { - var cu = CurrentUser; - - if (cu != null && cu.DiscordUser == u) - return await Double(cu).ConfigureAwait(false); - - return false; - } - - public async Task Double(User u) - { - await locker.WaitAsync().ConfigureAwait(false); - try - { - if (State != GameState.Playing) - return false; - - if (CurrentUser != u) - return false; - - if (!await _cs.RemoveAsync(u.DiscordUser.Id, "Blackjack-double", u.Bet).ConfigureAwait(false)) - return false; - - u.Bet *= 2; - - u.Cards.Add(Deck.Draw()); - - if (u.GetHandValue() == 21) - { - //blackjack - u.State = User.UserState.Blackjack; - } - else if (u.GetHandValue() > 21) - { - // user busted - u.State = User.UserState.Bust; - } - else - { - //with double you just get one card, and then you're done - u.State = User.UserState.Stand; - } - _currentUserMove.TrySetResult(true); - - return true; - } - finally - { - locker.Release(); - } - } - - public async Task Hit(IUser u) - { - var cu = CurrentUser; - - if (cu != null && cu.DiscordUser == u) - return await Hit(cu).ConfigureAwait(false); - - return false; - } - - public async Task Hit(User u) - { - await locker.WaitAsync().ConfigureAwait(false); - try - { - if (State != GameState.Playing) - return false; - - if (CurrentUser != u) - return false; - - u.Cards.Add(Deck.Draw()); - - if (u.GetHandValue() == 21) - { - //blackjack - u.State = User.UserState.Blackjack; - } - else if (u.GetHandValue() > 21) - { - // user busted - u.State = User.UserState.Bust; - } - else - { - //you can hit or stand again - } - _currentUserMove.TrySetResult(true); - - return true; - } - finally - { - locker.Release(); - } - } - - public Task PrintState() - { - if (StateUpdated is null) - return Task.CompletedTask; - return StateUpdated.Invoke(this); + locker.Release(); } } -} + + public async Task Hit(IUser u) + { + var cu = CurrentUser; + + if (cu != null && cu.DiscordUser == u) + return await Hit(cu).ConfigureAwait(false); + + return false; + } + + public async Task Hit(User u) + { + await locker.WaitAsync().ConfigureAwait(false); + try + { + if (State != GameState.Playing) + return false; + + if (CurrentUser != u) + return false; + + u.Cards.Add(Deck.Draw()); + + if (u.GetHandValue() == 21) + { + //blackjack + u.State = User.UserState.Blackjack; + } + else if (u.GetHandValue() > 21) + { + // user busted + u.State = User.UserState.Bust; + } + else + { + //you can hit or stand again + } + _currentUserMove.TrySetResult(true); + + return true; + } + finally + { + locker.Release(); + } + } + + public Task PrintState() + { + if (StateUpdated is null) + return Task.CompletedTask; + return StateUpdated.Invoke(this); + } +} \ No newline at end of file diff --git a/src/NadekoBot/Modules/Gambling/Common/Blackjack/Player.cs b/src/NadekoBot/Modules/Gambling/Common/Blackjack/Player.cs index f40a37173..2640548da 100644 --- a/src/NadekoBot/Modules/Gambling/Common/Blackjack/Player.cs +++ b/src/NadekoBot/Modules/Gambling/Common/Blackjack/Player.cs @@ -1,65 +1,60 @@ using Discord; -using NadekoBot.Modules.Gambling.Common; -using System; -using System.Collections.Generic; -using System.Linq; -namespace NadekoBot.Modules.Gambling.Common.Blackjack +namespace NadekoBot.Modules.Gambling.Common.Blackjack; + +public abstract class Player { - public abstract class Player + public List Cards { get; } = new List(); + + public int GetHandValue() { - public List Cards { get; } = new List(); + var val = GetRawHandValue(); - public int GetHandValue() + // while the hand value is greater than 21, for each ace you have in the deck + // reduce the value by 10 until it drops below 22 + // (emulating the fact that ace is either a 1 or a 11) + var i = Cards.Count(x => x.Number == 1); + while (val > 21 && i-- > 0) { - var val = GetRawHandValue(); - - // while the hand value is greater than 21, for each ace you have in the deck - // reduce the value by 10 until it drops below 22 - // (emulating the fact that ace is either a 1 or a 11) - var i = Cards.Count(x => x.Number == 1); - while (val > 21 && i-- > 0) - { - val -= 10; - } - return val; - } - - public int GetRawHandValue() - { - return Cards.Sum(x => x.Number == 1 ? 11 : x.Number >= 10 ? 10 : x.Number); + val -= 10; } + return val; } - public class Dealer : Player + public int GetRawHandValue() { - - } - - public class User : Player - { - public enum UserState - { - Waiting, - Stand, - Bust, - Blackjack, - Won, - Lost - } - - public User(IUser user, long bet) - { - if (bet <= 0) - throw new ArgumentOutOfRangeException(nameof(bet)); - - this.Bet = bet; - this.DiscordUser = user; - } - - public UserState State { get; set; } = UserState.Waiting; - public long Bet { get; set; } - public IUser DiscordUser { get; } - public bool Done => State != UserState.Waiting; + return Cards.Sum(x => x.Number == 1 ? 11 : x.Number >= 10 ? 10 : x.Number); } } + +public class Dealer : Player +{ + +} + +public class User : Player +{ + public enum UserState + { + Waiting, + Stand, + Bust, + Blackjack, + Won, + Lost + } + + public User(IUser user, long bet) + { + if (bet <= 0) + throw new ArgumentOutOfRangeException(nameof(bet)); + + this.Bet = bet; + this.DiscordUser = user; + } + + public UserState State { get; set; } = UserState.Waiting; + public long Bet { get; set; } + public IUser DiscordUser { get; } + public bool Done => State != UserState.Waiting; +} \ No newline at end of file diff --git a/src/NadekoBot/Modules/Gambling/Common/CurrencyRaffleGame.cs b/src/NadekoBot/Modules/Gambling/Common/CurrencyRaffleGame.cs index aaa19b4f5..7292c7b73 100644 --- a/src/NadekoBot/Modules/Gambling/Common/CurrencyRaffleGame.cs +++ b/src/NadekoBot/Modules/Gambling/Common/CurrencyRaffleGame.cs @@ -1,81 +1,78 @@ using Discord; using NadekoBot.Common; -using System.Collections.Generic; -using System.Linq; -namespace NadekoBot.Modules.Gambling.Common +namespace NadekoBot.Modules.Gambling.Common; + +public class CurrencyRaffleGame { - public class CurrencyRaffleGame + public enum Type { + Mixed, + Normal + } + + public class User { - public enum Type { - Mixed, - Normal + public IUser DiscordUser { get; set; } + public long Amount { get; set; } + + public override int GetHashCode() + { + return DiscordUser.GetHashCode(); } - public class User + public override bool Equals(object obj) { - public IUser DiscordUser { get; set; } - public long Amount { get; set; } - - public override int GetHashCode() - { - return DiscordUser.GetHashCode(); - } - - public override bool Equals(object obj) - { - return obj is User u - ? u.DiscordUser == DiscordUser - : false; - } + return obj is User u + ? u.DiscordUser == DiscordUser + : false; } + } - private readonly HashSet _users = new HashSet(); - public IEnumerable Users => _users; - public Type GameType { get; } + private readonly HashSet _users = new HashSet(); + public IEnumerable Users => _users; + public Type GameType { get; } - public CurrencyRaffleGame(Type type) - { - GameType = type; - } + public CurrencyRaffleGame(Type type) + { + GameType = type; + } - public bool AddUser(IUser usr, long amount) - { - // if game type is normal, and someone already joined the game - // (that's the user who created it) - if (GameType == Type.Normal && _users.Count > 0 && - _users.First().Amount != amount) - return false; + public bool AddUser(IUser usr, long amount) + { + // if game type is normal, and someone already joined the game + // (that's the user who created it) + if (GameType == Type.Normal && _users.Count > 0 && + _users.First().Amount != amount) + return false; - if (!_users.Add(new User + if (!_users.Add(new User { DiscordUser = usr, Amount = amount, })) - { - return false; - } - - return true; - } - - public User GetWinner() { - var rng = new NadekoRandom(); - if (GameType == Type.Mixed) - { - var num = rng.NextLong(0L, Users.Sum(x => x.Amount)); - var sum = 0L; - foreach (var u in Users) - { - sum += u.Amount; - if (sum > num) - return u; - } - } - - var usrs = _users.ToArray(); - return usrs[rng.Next(0, usrs.Length)]; + return false; } + + return true; } -} + + public User GetWinner() + { + var rng = new NadekoRandom(); + if (GameType == Type.Mixed) + { + var num = rng.NextLong(0L, Users.Sum(x => x.Amount)); + var sum = 0L; + foreach (var u in Users) + { + sum += u.Amount; + if (sum > num) + return u; + } + } + + var usrs = _users.ToArray(); + return usrs[rng.Next(0, usrs.Length)]; + } +} \ No newline at end of file diff --git a/src/NadekoBot/Modules/Gambling/Common/Deck.cs b/src/NadekoBot/Modules/Gambling/Common/Deck.cs index 7e87a6cfa..4d5732cbe 100644 --- a/src/NadekoBot/Modules/Gambling/Common/Deck.cs +++ b/src/NadekoBot/Modules/Gambling/Common/Deck.cs @@ -1,312 +1,308 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using NadekoBot.Common; +using NadekoBot.Common; using NadekoBot.Extensions; -namespace NadekoBot.Modules.Gambling.Common +namespace NadekoBot.Modules.Gambling.Common; + +public class QuadDeck : Deck { - public class QuadDeck : Deck + protected override void RefillPool() { - protected override void RefillPool() + CardPool = new List(52 * 4); + for (var j = 1; j < 14; j++) { - CardPool = new List(52 * 4); - for (var j = 1; j < 14; j++) + for (var i = 1; i < 5; i++) { - for (var i = 1; i < 5; i++) - { - CardPool.Add(new Card((CardSuit)i, j)); - CardPool.Add(new Card((CardSuit)i, j)); - CardPool.Add(new Card((CardSuit)i, j)); - CardPool.Add(new Card((CardSuit)i, j)); - } + CardPool.Add(new Card((CardSuit)i, j)); + CardPool.Add(new Card((CardSuit)i, j)); + CardPool.Add(new Card((CardSuit)i, j)); + CardPool.Add(new Card((CardSuit)i, j)); } } } +} - public class Deck +public class Deck +{ + private static readonly Dictionary cardNames = new Dictionary() { + { 1, "Ace" }, + { 2, "Two" }, + { 3, "Three" }, + { 4, "Four" }, + { 5, "Five" }, + { 6, "Six" }, + { 7, "Seven" }, + { 8, "Eight" }, + { 9, "Nine" }, + { 10, "Ten" }, + { 11, "Jack" }, + { 12, "Queen" }, + { 13, "King" } + }; + private static Dictionary, bool>> handValues; + + + public enum CardSuit { - private static readonly Dictionary cardNames = new Dictionary() { - { 1, "Ace" }, - { 2, "Two" }, - { 3, "Three" }, - { 4, "Four" }, - { 5, "Five" }, - { 6, "Six" }, - { 7, "Seven" }, - { 8, "Eight" }, - { 9, "Nine" }, - { 10, "Ten" }, - { 11, "Jack" }, - { 12, "Queen" }, - { 13, "King" } - }; - private static Dictionary, bool>> handValues; + Spades = 1, + Hearts = 2, + Diamonds = 3, + Clubs = 4 + } + public class Card : IComparable + { + public CardSuit Suit { get; } + public int Number { get; } - public enum CardSuit + public string FullName { - Spades = 1, - Hearts = 2, - Diamonds = 3, - Clubs = 4 - } - - public class Card : IComparable - { - public CardSuit Suit { get; } - public int Number { get; } - - public string FullName - { - get - { - var str = ""; - - if (Number <= 10 && Number > 1) - { - str += "_" + Number; - } - else - { - str += GetValueText().ToLowerInvariant(); - } - return str + "_of_" + Suit.ToString().ToLowerInvariant(); - } - } - - public Card(CardSuit s, int cardNum) - { - this.Suit = s; - this.Number = cardNum; - } - - public string GetValueText() => cardNames[Number]; - - public override string ToString() => cardNames[Number] + " Of " + Suit; - - public int CompareTo(object obj) - { - if (!(obj is Card)) return 0; - var c = (Card)obj; - return this.Number - c.Number; - } - - public static Card Parse(string input) - { - if (string.IsNullOrWhiteSpace(input)) - throw new ArgumentNullException(nameof(input)); - - if (input.Length != 2 - || !_numberCharToNumber.TryGetValue(input[0], out var n) - || !_suitCharToSuit.TryGetValue(input[1].ToString(), out var s)) - { - throw new ArgumentException("Invalid input", nameof(input)); - } - - return new Card(s, n); - } - - public string GetEmojiString() + get { var str = ""; - str += _regIndicators[this.Number - 1]; - str += _suitToSuitChar[this.Suit]; - - return str; - } - private readonly string[] _regIndicators = new[] - { - "🇦", - ":two:", - ":three:", - ":four:", - ":five:", - ":six:", - ":seven:", - ":eight:", - ":nine:", - ":keycap_ten:", - "🇯", - "🇶", - "🇰" - }; - private static readonly IReadOnlyDictionary _suitToSuitChar = new Dictionary - { - {CardSuit.Diamonds, "♦"}, - {CardSuit.Clubs, "♣"}, - {CardSuit.Spades, "♠"}, - {CardSuit.Hearts, "♥"}, - }; - private static IReadOnlyDictionary _suitCharToSuit = new Dictionary - { - {"♦", CardSuit.Diamonds }, - {"d", CardSuit.Diamonds }, - {"♣", CardSuit.Clubs }, - {"c", CardSuit.Clubs }, - {"♠", CardSuit.Spades }, - {"s", CardSuit.Spades }, - {"♥", CardSuit.Hearts }, - {"h", CardSuit.Hearts }, - }; - private static IReadOnlyDictionary _numberCharToNumber = new Dictionary() - { - {'a', 1 }, - {'2', 2 }, - {'3', 3 }, - {'4', 4 }, - {'5', 5 }, - {'6', 6 }, - {'7', 7 }, - {'8', 8 }, - {'9', 9 }, - {'t', 10 }, - {'j', 11 }, - {'q', 12 }, - {'k', 13 }, - }; - } - - public List CardPool { get; set; } - /// - /// Creates a new instance of the BlackJackGame, this allows you to create multiple games running at one time. - /// - public Deck() - { - RefillPool(); - } - static Deck() - { - InitHandValues(); - } - /// - /// Restart the game of blackjack. It will only refill the pool for now. Probably wont be used, unless you want to have only 1 bjg running at one time, - /// then you will restart the same game every time. - /// - public void Restart() => RefillPool(); - - /// - /// Removes all cards from the pool and refills the pool with all of the possible cards. NOTE: I think this is too expensive. - /// We should probably make it so it copies another premade list with all the cards, or something. - /// - protected virtual void RefillPool() - { - CardPool = new List(52); - //foreach suit - for (var j = 1; j < 14; j++) - { - // and number - for (var i = 1; i < 5; i++) + if (Number <= 10 && Number > 1) { - //generate a card of that suit and number and add it to the pool - - // the pool will go from ace of spades,hears,diamonds,clubs all the way to the king of spades. hearts, ... - CardPool.Add(new Card((CardSuit)i, j)); + str += "_" + Number; } + else + { + str += GetValueText().ToLowerInvariant(); + } + return str + "_of_" + Suit.ToString().ToLowerInvariant(); } } - private Random r = new NadekoRandom(); - /// - /// Take a card from the pool, you either take it from the top if the deck is shuffled, or from a random place if the deck is in the default order. - /// - /// A card from the pool - public Card Draw() + + public Card(CardSuit s, int cardNum) { - if (CardPool.Count == 0) - Restart(); - //you can either do this if your deck is not shuffled - - var num = r.Next(0, CardPool.Count); - var c = CardPool[num]; - CardPool.RemoveAt(num); - return c; - - // if you want to shuffle when you fill, then take the first one - /* - Card c = cardPool[0]; - cardPool.RemoveAt(0); - return c; - */ + this.Suit = s; + this.Number = cardNum; } - /// - /// Shuffles the deck. Use this if you want to take cards from the top of the deck, instead of randomly. See DrawACard method. - /// - private void Shuffle() + + public string GetValueText() => cardNames[Number]; + + public override string ToString() => cardNames[Number] + " Of " + Suit; + + public int CompareTo(object obj) { - if (CardPool.Count <= 1) return; - var orderedPool = CardPool.Shuffle(); - CardPool = CardPool as List ?? orderedPool.ToList(); + if (!(obj is Card)) return 0; + var c = (Card)obj; + return this.Number - c.Number; } - public override string ToString() => string.Concat(CardPool.Select(c => c.ToString())) + Environment.NewLine; - private static void InitHandValues() + public static Card Parse(string input) { - bool hasPair(List cards) => cards.GroupBy(card => card.Number) - .Count(group => group.Count() == 2) == 1; - bool isPair(List cards) => cards.GroupBy(card => card.Number) - .Count(group => group.Count() == 3) == 0 - && hasPair(cards); + if (string.IsNullOrWhiteSpace(input)) + throw new ArgumentNullException(nameof(input)); - bool isTwoPair(List cards) => cards.GroupBy(card => card.Number) - .Count(group => group.Count() == 2) == 2; - - bool isStraight(List cards) + if (input.Length != 2 + || !_numberCharToNumber.TryGetValue(input[0], out var n) + || !_suitCharToSuit.TryGetValue(input[1].ToString(), out var s)) { - if (cards.GroupBy(card => card.Number).Count() != cards.Count()) - return false; - var toReturn = (cards.Max(card => (int)card.Number) - - cards.Min(card => (int)card.Number) == 4); - if (toReturn || cards.All(c => c.Number != 1)) return toReturn; - - var newCards = cards.Select(c => c.Number == 1 ? new Card(c.Suit, 14) : c); - return (newCards.Max(card => (int)card.Number) - - newCards.Min(card => (int)card.Number) == 4); + throw new ArgumentException("Invalid input", nameof(input)); } - bool hasThreeOfKind(List cards) => cards.GroupBy(card => card.Number) - .Any(group => group.Count() == 3); - - bool isThreeOfKind(List cards) => hasThreeOfKind(cards) && !hasPair(cards); - - bool isFlush(List cards) => cards.GroupBy(card => card.Suit).Count() == 1; - - bool isFourOfKind(List cards) => cards.GroupBy(card => card.Number) - .Any(group => group.Count() == 4); - - bool isFullHouse(List cards) => hasPair(cards) && hasThreeOfKind(cards); - - bool hasStraightFlush(List cards) => isFlush(cards) && isStraight(cards); - - bool isRoyalFlush(List cards) => cards.Min(card => card.Number) == 1 && - cards.Max(card => card.Number) == 13 - && hasStraightFlush(cards); - - bool isStraightFlush(List cards) => hasStraightFlush(cards) && !isRoyalFlush(cards); - - handValues = new Dictionary, bool>> - { - { "Royal Flush", isRoyalFlush }, - { "Straight Flush", isStraightFlush }, - { "Four Of A Kind", isFourOfKind }, - { "Full House", isFullHouse }, - { "Flush", isFlush }, - { "Straight", isStraight }, - { "Three Of A Kind", isThreeOfKind }, - { "Two Pairs", isTwoPair }, - { "A Pair", isPair } - }; + return new Card(s, n); } - public static string GetHandValue(List cards) + public string GetEmojiString() { - if (handValues is null) - InitHandValues(); - foreach (var kvp in handValues.Where(x => x.Value(cards))) + var str = ""; + + str += _regIndicators[this.Number - 1]; + str += _suitToSuitChar[this.Suit]; + + return str; + } + private readonly string[] _regIndicators = new[] + { + "🇦", + ":two:", + ":three:", + ":four:", + ":five:", + ":six:", + ":seven:", + ":eight:", + ":nine:", + ":keycap_ten:", + "🇯", + "🇶", + "🇰" + }; + private static readonly IReadOnlyDictionary _suitToSuitChar = new Dictionary + { + {CardSuit.Diamonds, "♦"}, + {CardSuit.Clubs, "♣"}, + {CardSuit.Spades, "♠"}, + {CardSuit.Hearts, "♥"}, + }; + private static IReadOnlyDictionary _suitCharToSuit = new Dictionary + { + {"♦", CardSuit.Diamonds }, + {"d", CardSuit.Diamonds }, + {"♣", CardSuit.Clubs }, + {"c", CardSuit.Clubs }, + {"♠", CardSuit.Spades }, + {"s", CardSuit.Spades }, + {"♥", CardSuit.Hearts }, + {"h", CardSuit.Hearts }, + }; + private static IReadOnlyDictionary _numberCharToNumber = new Dictionary() + { + {'a', 1 }, + {'2', 2 }, + {'3', 3 }, + {'4', 4 }, + {'5', 5 }, + {'6', 6 }, + {'7', 7 }, + {'8', 8 }, + {'9', 9 }, + {'t', 10 }, + {'j', 11 }, + {'q', 12 }, + {'k', 13 }, + }; + } + + public List CardPool { get; set; } + /// + /// Creates a new instance of the BlackJackGame, this allows you to create multiple games running at one time. + /// + public Deck() + { + RefillPool(); + } + static Deck() + { + InitHandValues(); + } + /// + /// Restart the game of blackjack. It will only refill the pool for now. Probably wont be used, unless you want to have only 1 bjg running at one time, + /// then you will restart the same game every time. + /// + public void Restart() => RefillPool(); + + /// + /// Removes all cards from the pool and refills the pool with all of the possible cards. NOTE: I think this is too expensive. + /// We should probably make it so it copies another premade list with all the cards, or something. + /// + protected virtual void RefillPool() + { + CardPool = new List(52); + //foreach suit + for (var j = 1; j < 14; j++) + { + // and number + for (var i = 1; i < 5; i++) { - return kvp.Key; + //generate a card of that suit and number and add it to the pool + + // the pool will go from ace of spades,hears,diamonds,clubs all the way to the king of spades. hearts, ... + CardPool.Add(new Card((CardSuit)i, j)); } - return "High card " + (cards.FirstOrDefault(c => c.Number == 1)?.GetValueText() ?? cards.Max().GetValueText()); } } + private Random r = new NadekoRandom(); + /// + /// Take a card from the pool, you either take it from the top if the deck is shuffled, or from a random place if the deck is in the default order. + /// + /// A card from the pool + public Card Draw() + { + if (CardPool.Count == 0) + Restart(); + //you can either do this if your deck is not shuffled + + var num = r.Next(0, CardPool.Count); + var c = CardPool[num]; + CardPool.RemoveAt(num); + return c; + + // if you want to shuffle when you fill, then take the first one + /* + Card c = cardPool[0]; + cardPool.RemoveAt(0); + return c; + */ + } + /// + /// Shuffles the deck. Use this if you want to take cards from the top of the deck, instead of randomly. See DrawACard method. + /// + private void Shuffle() + { + if (CardPool.Count <= 1) return; + var orderedPool = CardPool.Shuffle(); + CardPool = CardPool as List ?? orderedPool.ToList(); + } + public override string ToString() => string.Concat(CardPool.Select(c => c.ToString())) + Environment.NewLine; + + private static void InitHandValues() + { + bool hasPair(List cards) => cards.GroupBy(card => card.Number) + .Count(group => group.Count() == 2) == 1; + bool isPair(List cards) => cards.GroupBy(card => card.Number) + .Count(group => group.Count() == 3) == 0 + && hasPair(cards); + + bool isTwoPair(List cards) => cards.GroupBy(card => card.Number) + .Count(group => group.Count() == 2) == 2; + + bool isStraight(List cards) + { + if (cards.GroupBy(card => card.Number).Count() != cards.Count()) + return false; + var toReturn = (cards.Max(card => (int)card.Number) + - cards.Min(card => (int)card.Number) == 4); + if (toReturn || cards.All(c => c.Number != 1)) return toReturn; + + var newCards = cards.Select(c => c.Number == 1 ? new Card(c.Suit, 14) : c); + return (newCards.Max(card => (int)card.Number) + - newCards.Min(card => (int)card.Number) == 4); + } + + bool hasThreeOfKind(List cards) => cards.GroupBy(card => card.Number) + .Any(group => group.Count() == 3); + + bool isThreeOfKind(List cards) => hasThreeOfKind(cards) && !hasPair(cards); + + bool isFlush(List cards) => cards.GroupBy(card => card.Suit).Count() == 1; + + bool isFourOfKind(List cards) => cards.GroupBy(card => card.Number) + .Any(group => group.Count() == 4); + + bool isFullHouse(List cards) => hasPair(cards) && hasThreeOfKind(cards); + + bool hasStraightFlush(List cards) => isFlush(cards) && isStraight(cards); + + bool isRoyalFlush(List cards) => cards.Min(card => card.Number) == 1 && + cards.Max(card => card.Number) == 13 + && hasStraightFlush(cards); + + bool isStraightFlush(List cards) => hasStraightFlush(cards) && !isRoyalFlush(cards); + + handValues = new Dictionary, bool>> + { + { "Royal Flush", isRoyalFlush }, + { "Straight Flush", isStraightFlush }, + { "Four Of A Kind", isFourOfKind }, + { "Full House", isFullHouse }, + { "Flush", isFlush }, + { "Straight", isStraight }, + { "Three Of A Kind", isThreeOfKind }, + { "Two Pairs", isTwoPair }, + { "A Pair", isPair } + }; + } + + public static string GetHandValue(List cards) + { + if (handValues is null) + InitHandValues(); + foreach (var kvp in handValues.Where(x => x.Value(cards))) + { + return kvp.Key; + } + return "High card " + (cards.FirstOrDefault(c => c.Number == 1)?.GetValueText() ?? cards.Max().GetValueText()); + } } \ No newline at end of file diff --git a/src/NadekoBot/Modules/Gambling/Common/Events/EventOptions.cs b/src/NadekoBot/Modules/Gambling/Common/Events/EventOptions.cs index d7cc3aa66..7bfd919fc 100644 --- a/src/NadekoBot/Modules/Gambling/Common/Events/EventOptions.cs +++ b/src/NadekoBot/Modules/Gambling/Common/Events/EventOptions.cs @@ -1,30 +1,29 @@ using CommandLine; using NadekoBot.Common; -namespace NadekoBot.Modules.Gambling.Common.Events +namespace NadekoBot.Modules.Gambling.Common.Events; + +public class EventOptions : INadekoCommandOptions { - public class EventOptions : INadekoCommandOptions + [Option('a', "amount", Required = false, Default = 100, HelpText = "Amount of currency each user receives.")] + public long Amount { get; set; } = 100; + [Option('p', "pot-size", Required = false, Default = 0, HelpText = "The maximum amount of currency that can be rewarded. 0 means no limit.")] + public long PotSize { get; set; } = 0; + //[Option('t', "type", Required = false, Default = "reaction", HelpText = "Type of the event. reaction, gamestatus or joinserver.")] + //public string TypeString { get; set; } = "reaction"; + [Option('d', "duration", Required = false, Default = 24, HelpText = "Number of hours the event should run for. Default 24.")] + public int Hours { get; set; } = 24; + + + public void NormalizeOptions() { - [Option('a', "amount", Required = false, Default = 100, HelpText = "Amount of currency each user receives.")] - public long Amount { get; set; } = 100; - [Option('p', "pot-size", Required = false, Default = 0, HelpText = "The maximum amount of currency that can be rewarded. 0 means no limit.")] - public long PotSize { get; set; } = 0; - //[Option('t', "type", Required = false, Default = "reaction", HelpText = "Type of the event. reaction, gamestatus or joinserver.")] - //public string TypeString { get; set; } = "reaction"; - [Option('d', "duration", Required = false, Default = 24, HelpText = "Number of hours the event should run for. Default 24.")] - public int Hours { get; set; } = 24; - - - public void NormalizeOptions() - { - if (Amount < 0) - Amount = 100; - if (PotSize < 0) - PotSize = 0; - if (Hours <= 0) - Hours = 24; - if (PotSize != 0 && PotSize < Amount) - PotSize = 0; - } + if (Amount < 0) + Amount = 100; + if (PotSize < 0) + PotSize = 0; + if (Hours <= 0) + Hours = 24; + if (PotSize != 0 && PotSize < Amount) + PotSize = 0; } -} +} \ No newline at end of file diff --git a/src/NadekoBot/Modules/Gambling/Common/Events/GameStatusEvent.cs b/src/NadekoBot/Modules/Gambling/Common/Events/GameStatusEvent.cs index cdf3790b8..35046b462 100644 --- a/src/NadekoBot/Modules/Gambling/Common/Events/GameStatusEvent.cs +++ b/src/NadekoBot/Modules/Gambling/Common/Events/GameStatusEvent.cs @@ -4,207 +4,201 @@ using NadekoBot.Common.Collections; using NadekoBot.Services; using NadekoBot.Services.Database.Models; using NadekoBot.Extensions; -using NadekoBot.Modules.Gambling.Common; -using System; using System.Collections.Concurrent; -using System.Collections.Generic; -using System.Linq; using System.Threading; using System.Threading.Tasks; -using Serilog; -namespace NadekoBot.Modules.Gambling.Common.Events +namespace NadekoBot.Modules.Gambling.Common.Events; + +public class GameStatusEvent : ICurrencyEvent { - public class GameStatusEvent : ICurrencyEvent + private readonly DiscordSocketClient _client; + private readonly IGuild _guild; + private IUserMessage _msg; + private readonly ICurrencyService _cs; + private readonly long _amount; + + private long PotSize { get; set; } + public bool Stopped { get; private set; } + public bool PotEmptied { get; private set; } = false; + + private readonly Func _embedFunc; + private readonly bool _isPotLimited; + private readonly ITextChannel _channel; + private readonly ConcurrentHashSet _awardedUsers = new ConcurrentHashSet(); + private readonly ConcurrentQueue _toAward = new ConcurrentQueue(); + private readonly Timer _t; + private readonly Timer _timeout = null; + private readonly EventOptions _opts; + + private readonly string _code; + + public event Func OnEnded; + + private readonly char[] _sneakyGameStatusChars = Enumerable.Range(48, 10) + .Concat(Enumerable.Range(65, 26)) + .Concat(Enumerable.Range(97, 26)) + .Select(x => (char)x) + .ToArray(); + + public GameStatusEvent(DiscordSocketClient client, ICurrencyService cs,SocketGuild g, ITextChannel ch, + EventOptions opt, Func embedFunc) { - private readonly DiscordSocketClient _client; - private readonly IGuild _guild; - private IUserMessage _msg; - private readonly ICurrencyService _cs; - private readonly long _amount; + _client = client; + _guild = g; + _cs = cs; + _amount = opt.Amount; + PotSize = opt.PotSize; + _embedFunc = embedFunc; + _isPotLimited = PotSize > 0; + _channel = ch; + _opts = opt; + // generate code + _code = new string(_sneakyGameStatusChars.Shuffle().Take(5).ToArray()); - private long PotSize { get; set; } - public bool Stopped { get; private set; } - public bool PotEmptied { get; private set; } = false; - - private readonly Func _embedFunc; - private readonly bool _isPotLimited; - private readonly ITextChannel _channel; - private readonly ConcurrentHashSet _awardedUsers = new ConcurrentHashSet(); - private readonly ConcurrentQueue _toAward = new ConcurrentQueue(); - private readonly Timer _t; - private readonly Timer _timeout = null; - private readonly EventOptions _opts; - - private readonly string _code; - - public event Func OnEnded; - - private readonly char[] _sneakyGameStatusChars = Enumerable.Range(48, 10) - .Concat(Enumerable.Range(65, 26)) - .Concat(Enumerable.Range(97, 26)) - .Select(x => (char)x) - .ToArray(); - - public GameStatusEvent(DiscordSocketClient client, ICurrencyService cs,SocketGuild g, ITextChannel ch, - EventOptions opt, Func embedFunc) + _t = new Timer(OnTimerTick, null, Timeout.InfiniteTimeSpan, TimeSpan.FromSeconds(2)); + if (_opts.Hours > 0) { - _client = client; - _guild = g; - _cs = cs; - _amount = opt.Amount; - PotSize = opt.PotSize; - _embedFunc = embedFunc; - _isPotLimited = PotSize > 0; - _channel = ch; - _opts = opt; - // generate code - _code = new string(_sneakyGameStatusChars.Shuffle().Take(5).ToArray()); + _timeout = new Timer(EventTimeout, null, TimeSpan.FromHours(_opts.Hours), Timeout.InfiniteTimeSpan); + } + } - _t = new Timer(OnTimerTick, null, Timeout.InfiniteTimeSpan, TimeSpan.FromSeconds(2)); - if (_opts.Hours > 0) - { - _timeout = new Timer(EventTimeout, null, TimeSpan.FromHours(_opts.Hours), Timeout.InfiniteTimeSpan); - } + private void EventTimeout(object state) + { + var _ = StopEvent(); + } + + private async void OnTimerTick(object state) + { + var potEmpty = PotEmptied; + List toAward = new List(); + while (_toAward.TryDequeue(out var x)) + { + toAward.Add(x); } - private void EventTimeout(object state) - { - var _ = StopEvent(); - } + if (!toAward.Any()) + return; - private async void OnTimerTick(object state) + try { - var potEmpty = PotEmptied; - List toAward = new List(); - while (_toAward.TryDequeue(out var x)) + await _cs.AddBulkAsync(toAward, + toAward.Select(x => "GameStatus Event"), + toAward.Select(x => _amount), + gamble: true).ConfigureAwait(false); + + if (_isPotLimited) { - toAward.Add(x); + await _msg.ModifyAsync(m => + { + m.Embed = GetEmbed(PotSize).Build(); + }, new RequestOptions() { RetryMode = RetryMode.AlwaysRetry }).ConfigureAwait(false); } - if (!toAward.Any()) + Log.Information("Awarded {0} users {1} currency.{2}", + toAward.Count, + _amount, + _isPotLimited ? $" {PotSize} left." : ""); + + if (potEmpty) + { + var _ = StopEvent(); + } + + } + catch (Exception ex) + { + Log.Warning(ex, "Error in OnTimerTick in gamestatusevent"); + } + } + + public async Task StartEvent() + { + _msg = await _channel.EmbedAsync(GetEmbed(_opts.PotSize)).ConfigureAwait(false); + await _client.SetGameAsync(_code).ConfigureAwait(false); + _client.MessageDeleted += OnMessageDeleted; + _client.MessageReceived += HandleMessage; + _t.Change(TimeSpan.FromSeconds(2), TimeSpan.FromSeconds(2)); + } + + private IEmbedBuilder GetEmbed(long pot) + { + return _embedFunc(CurrencyEvent.Type.GameStatus, _opts, pot); + } + + private async Task OnMessageDeleted(Cacheable msg, ISocketMessageChannel _) + { + if (msg.Id == _msg.Id) + { + await StopEvent().ConfigureAwait(false); + } + } + + private readonly object stopLock = new object(); + public async Task StopEvent() + { + await Task.Yield(); + lock (stopLock) + { + if (Stopped) return; + Stopped = true; + _client.MessageDeleted -= OnMessageDeleted; + _client.MessageReceived -= HandleMessage; + _client.SetGameAsync(null); + _t.Change(Timeout.Infinite, Timeout.Infinite); + _timeout?.Change(Timeout.Infinite, Timeout.Infinite); + try { var _ = _msg.DeleteAsync(); } catch { } + var os = OnEnded(_guild.Id); + } + } + + private Task HandleMessage(SocketMessage msg) + { + var _ = Task.Run(async () => + { + if (!(msg.Author is IGuildUser gu) // no unknown users, as they could be bots, or alts + || gu.IsBot // no bots + || msg.Content != _code // code has to be the same + || (DateTime.UtcNow - gu.CreatedAt).TotalDays <= 5) // no recently created accounts + { + return; + } + // there has to be money left in the pot + // and the user wasn't rewarded + if (_awardedUsers.Add(msg.Author.Id) && TryTakeFromPot()) + { + _toAward.Enqueue(msg.Author.Id); + if (_isPotLimited && PotSize < _amount) + PotEmptied = true; + } try { - await _cs.AddBulkAsync(toAward, - toAward.Select(x => "GameStatus Event"), - toAward.Select(x => _amount), - gamble: true).ConfigureAwait(false); - - if (_isPotLimited) + await msg.DeleteAsync(new RequestOptions() { - await _msg.ModifyAsync(m => - { - m.Embed = GetEmbed(PotSize).Build(); - }, new RequestOptions() { RetryMode = RetryMode.AlwaysRetry }).ConfigureAwait(false); - } - - Log.Information("Awarded {0} users {1} currency.{2}", - toAward.Count, - _amount, - _isPotLimited ? $" {PotSize} left." : ""); - - if (potEmpty) - { - var _ = StopEvent(); - } - + RetryMode = RetryMode.AlwaysFail + }); } - catch (Exception ex) - { - Log.Warning(ex, "Error in OnTimerTick in gamestatusevent"); - } - } - - public async Task StartEvent() - { - _msg = await _channel.EmbedAsync(GetEmbed(_opts.PotSize)).ConfigureAwait(false); - await _client.SetGameAsync(_code).ConfigureAwait(false); - _client.MessageDeleted += OnMessageDeleted; - _client.MessageReceived += HandleMessage; - _t.Change(TimeSpan.FromSeconds(2), TimeSpan.FromSeconds(2)); - } - - private IEmbedBuilder GetEmbed(long pot) - { - return _embedFunc(CurrencyEvent.Type.GameStatus, _opts, pot); - } - - private async Task OnMessageDeleted(Cacheable msg, ISocketMessageChannel _) - { - if (msg.Id == _msg.Id) - { - await StopEvent().ConfigureAwait(false); - } - } - - private readonly object stopLock = new object(); - public async Task StopEvent() - { - await Task.Yield(); - lock (stopLock) - { - if (Stopped) - return; - Stopped = true; - _client.MessageDeleted -= OnMessageDeleted; - _client.MessageReceived -= HandleMessage; - _client.SetGameAsync(null); - _t.Change(Timeout.Infinite, Timeout.Infinite); - _timeout?.Change(Timeout.Infinite, Timeout.Infinite); - try { var _ = _msg.DeleteAsync(); } catch { } - var os = OnEnded(_guild.Id); - } - } - - private Task HandleMessage(SocketMessage msg) - { - var _ = Task.Run(async () => - { - if (!(msg.Author is IGuildUser gu) // no unknown users, as they could be bots, or alts - || gu.IsBot // no bots - || msg.Content != _code // code has to be the same - || (DateTime.UtcNow - gu.CreatedAt).TotalDays <= 5) // no recently created accounts - { - return; - } - // there has to be money left in the pot - // and the user wasn't rewarded - if (_awardedUsers.Add(msg.Author.Id) && TryTakeFromPot()) - { - _toAward.Enqueue(msg.Author.Id); - if (_isPotLimited && PotSize < _amount) - PotEmptied = true; - } - - try - { - await msg.DeleteAsync(new RequestOptions() - { - RetryMode = RetryMode.AlwaysFail - }); - } - catch { } - }); - return Task.CompletedTask; - } - - private readonly object potLock = new object(); - private bool TryTakeFromPot() - { - if (_isPotLimited) - { - lock (potLock) - { - if (PotSize < _amount) - return false; - - PotSize -= _amount; - return true; - } - } - return true; - } + catch { } + }); + return Task.CompletedTask; } -} + + private readonly object potLock = new object(); + private bool TryTakeFromPot() + { + if (_isPotLimited) + { + lock (potLock) + { + if (PotSize < _amount) + return false; + + PotSize -= _amount; + return true; + } + } + return true; + } +} \ No newline at end of file diff --git a/src/NadekoBot/Modules/Gambling/Common/Events/ICurrencyEvent.cs b/src/NadekoBot/Modules/Gambling/Common/Events/ICurrencyEvent.cs index bf2a46eb0..fb5db3ffc 100644 --- a/src/NadekoBot/Modules/Gambling/Common/Events/ICurrencyEvent.cs +++ b/src/NadekoBot/Modules/Gambling/Common/Events/ICurrencyEvent.cs @@ -1,12 +1,10 @@ -using System; -using System.Threading.Tasks; +using System.Threading.Tasks; -namespace NadekoBot.Modules.Gambling.Common +namespace NadekoBot.Modules.Gambling.Common; + +public interface ICurrencyEvent { - public interface ICurrencyEvent - { - event Func OnEnded; - Task StopEvent(); - Task StartEvent(); - } -} + event Func OnEnded; + Task StopEvent(); + Task StartEvent(); +} \ No newline at end of file diff --git a/src/NadekoBot/Modules/Gambling/Common/Events/ReactionEvent.cs b/src/NadekoBot/Modules/Gambling/Common/Events/ReactionEvent.cs index 061964a60..8bbd70d66 100644 --- a/src/NadekoBot/Modules/Gambling/Common/Events/ReactionEvent.cs +++ b/src/NadekoBot/Modules/Gambling/Common/Events/ReactionEvent.cs @@ -4,207 +4,200 @@ using NadekoBot.Common.Collections; using NadekoBot.Services; using NadekoBot.Services.Database.Models; using NadekoBot.Extensions; -using NadekoBot.Modules.Gambling.Common; -using System; using System.Collections.Concurrent; -using System.Collections.Generic; -using System.Linq; using System.Threading; using System.Threading.Tasks; -using NadekoBot.Modules.Gambling.Services; -using Serilog; -namespace NadekoBot.Modules.Gambling.Common.Events +namespace NadekoBot.Modules.Gambling.Common.Events; + +public class ReactionEvent : ICurrencyEvent { - public class ReactionEvent : ICurrencyEvent + private readonly DiscordSocketClient _client; + private readonly IGuild _guild; + private IUserMessage _msg; + private IEmote _emote; + private readonly ICurrencyService _cs; + private readonly long _amount; + + private long PotSize { get; set; } + public bool Stopped { get; private set; } + public bool PotEmptied { get; private set; } = false; + + private readonly Func _embedFunc; + private readonly bool _isPotLimited; + private readonly ITextChannel _channel; + private readonly ConcurrentHashSet _awardedUsers = new ConcurrentHashSet(); + private readonly ConcurrentQueue _toAward = new ConcurrentQueue(); + private readonly Timer _t; + private readonly Timer _timeout = null; + private readonly bool _noRecentlyJoinedServer; + private readonly EventOptions _opts; + private readonly GamblingConfig _config; + + public event Func OnEnded; + + public ReactionEvent(DiscordSocketClient client, ICurrencyService cs, + SocketGuild g, ITextChannel ch, EventOptions opt, GamblingConfig config, + Func embedFunc) { - private readonly DiscordSocketClient _client; - private readonly IGuild _guild; - private IUserMessage _msg; - private IEmote _emote; - private readonly ICurrencyService _cs; - private readonly long _amount; + _client = client; + _guild = g; + _cs = cs; + _amount = opt.Amount; + PotSize = opt.PotSize; + _embedFunc = embedFunc; + _isPotLimited = PotSize > 0; + _channel = ch; + _noRecentlyJoinedServer = false; + _opts = opt; + _config = config; - private long PotSize { get; set; } - public bool Stopped { get; private set; } - public bool PotEmptied { get; private set; } = false; - - private readonly Func _embedFunc; - private readonly bool _isPotLimited; - private readonly ITextChannel _channel; - private readonly ConcurrentHashSet _awardedUsers = new ConcurrentHashSet(); - private readonly ConcurrentQueue _toAward = new ConcurrentQueue(); - private readonly Timer _t; - private readonly Timer _timeout = null; - private readonly bool _noRecentlyJoinedServer; - private readonly EventOptions _opts; - private readonly GamblingConfig _config; - - public event Func OnEnded; - - public ReactionEvent(DiscordSocketClient client, ICurrencyService cs, - SocketGuild g, ITextChannel ch, EventOptions opt, GamblingConfig config, - Func embedFunc) + _t = new Timer(OnTimerTick, null, Timeout.InfiniteTimeSpan, TimeSpan.FromSeconds(2)); + if (_opts.Hours > 0) { - _client = client; - _guild = g; - _cs = cs; - _amount = opt.Amount; - PotSize = opt.PotSize; - _embedFunc = embedFunc; - _isPotLimited = PotSize > 0; - _channel = ch; - _noRecentlyJoinedServer = false; - _opts = opt; - _config = config; - - _t = new Timer(OnTimerTick, null, Timeout.InfiniteTimeSpan, TimeSpan.FromSeconds(2)); - if (_opts.Hours > 0) - { - _timeout = new Timer(EventTimeout, null, TimeSpan.FromHours(_opts.Hours), Timeout.InfiniteTimeSpan); - } - } - - private void EventTimeout(object state) - { - var _ = StopEvent(); - } - - private async void OnTimerTick(object state) - { - var potEmpty = PotEmptied; - List toAward = new List(); - while (_toAward.TryDequeue(out var x)) - { - toAward.Add(x); - } - - if (!toAward.Any()) - return; - - try - { - await _cs.AddBulkAsync(toAward, - toAward.Select(x => "Reaction Event"), - toAward.Select(x => _amount), - gamble: true).ConfigureAwait(false); - - if (_isPotLimited) - { - await _msg.ModifyAsync(m => - { - m.Embed = GetEmbed(PotSize).Build(); - }, new RequestOptions() { RetryMode = RetryMode.AlwaysRetry }).ConfigureAwait(false); - } - - Log.Information("Awarded {0} users {1} currency.{2}", - toAward.Count, - _amount, - _isPotLimited ? $" {PotSize} left." : ""); - - if (potEmpty) - { - var _ = StopEvent(); - } - } - catch (Exception ex) - { - Log.Warning(ex, "Error adding bulk currency to users"); - } - } - - public async Task StartEvent() - { - if (Emote.TryParse(_config.Currency.Sign, out var emote)) - { - _emote = emote; - } - else - { - _emote = new Emoji(_config.Currency.Sign); - } - _msg = await _channel.EmbedAsync(GetEmbed(_opts.PotSize)).ConfigureAwait(false); - await _msg.AddReactionAsync(_emote).ConfigureAwait(false); - _client.MessageDeleted += OnMessageDeleted; - _client.ReactionAdded += HandleReaction; - _t.Change(TimeSpan.FromSeconds(2), TimeSpan.FromSeconds(2)); - } - - private IEmbedBuilder GetEmbed(long pot) - { - return _embedFunc(CurrencyEvent.Type.Reaction, _opts, pot); - } - - private async Task OnMessageDeleted(Cacheable msg, ISocketMessageChannel _) - { - if (msg.Id == _msg.Id) - { - await StopEvent().ConfigureAwait(false); - } - } - - private readonly object stopLock = new object(); - public async Task StopEvent() - { - await Task.Yield(); - lock (stopLock) - { - if (Stopped) - return; - Stopped = true; - _client.MessageDeleted -= OnMessageDeleted; - _client.ReactionAdded -= HandleReaction; - _t.Change(Timeout.Infinite, Timeout.Infinite); - _timeout?.Change(Timeout.Infinite, Timeout.Infinite); - try { var _ = _msg.DeleteAsync(); } catch { } - var os = OnEnded(_guild.Id); - } - } - - private Task HandleReaction(Cacheable msg, - ISocketMessageChannel ch, SocketReaction r) - { - var _ = Task.Run(() => - { - if (_emote.Name != r.Emote.Name) - return; - var gu = (r.User.IsSpecified ? r.User.Value : null) as IGuildUser; - if (gu is null // no unknown users, as they could be bots, or alts - || msg.Id != _msg.Id // same message - || gu.IsBot // no bots - || (DateTime.UtcNow - gu.CreatedAt).TotalDays <= 5 // no recently created accounts - || (_noRecentlyJoinedServer && // if specified, no users who joined the server in the last 24h - (gu.JoinedAt is null || (DateTime.UtcNow - gu.JoinedAt.Value).TotalDays < 1))) // and no users for who we don't know when they joined - { - return; - } - // there has to be money left in the pot - // and the user wasn't rewarded - if (_awardedUsers.Add(r.UserId) && TryTakeFromPot()) - { - _toAward.Enqueue(r.UserId); - if (_isPotLimited && PotSize < _amount) - PotEmptied = true; - } - }); - return Task.CompletedTask; - } - - private readonly object potLock = new object(); - private bool TryTakeFromPot() - { - if (_isPotLimited) - { - lock (potLock) - { - if (PotSize < _amount) - return false; - - PotSize -= _amount; - return true; - } - } - return true; + _timeout = new Timer(EventTimeout, null, TimeSpan.FromHours(_opts.Hours), Timeout.InfiniteTimeSpan); } } -} + + private void EventTimeout(object state) + { + var _ = StopEvent(); + } + + private async void OnTimerTick(object state) + { + var potEmpty = PotEmptied; + List toAward = new List(); + while (_toAward.TryDequeue(out var x)) + { + toAward.Add(x); + } + + if (!toAward.Any()) + return; + + try + { + await _cs.AddBulkAsync(toAward, + toAward.Select(x => "Reaction Event"), + toAward.Select(x => _amount), + gamble: true).ConfigureAwait(false); + + if (_isPotLimited) + { + await _msg.ModifyAsync(m => + { + m.Embed = GetEmbed(PotSize).Build(); + }, new RequestOptions() { RetryMode = RetryMode.AlwaysRetry }).ConfigureAwait(false); + } + + Log.Information("Awarded {0} users {1} currency.{2}", + toAward.Count, + _amount, + _isPotLimited ? $" {PotSize} left." : ""); + + if (potEmpty) + { + var _ = StopEvent(); + } + } + catch (Exception ex) + { + Log.Warning(ex, "Error adding bulk currency to users"); + } + } + + public async Task StartEvent() + { + if (Emote.TryParse(_config.Currency.Sign, out var emote)) + { + _emote = emote; + } + else + { + _emote = new Emoji(_config.Currency.Sign); + } + _msg = await _channel.EmbedAsync(GetEmbed(_opts.PotSize)).ConfigureAwait(false); + await _msg.AddReactionAsync(_emote).ConfigureAwait(false); + _client.MessageDeleted += OnMessageDeleted; + _client.ReactionAdded += HandleReaction; + _t.Change(TimeSpan.FromSeconds(2), TimeSpan.FromSeconds(2)); + } + + private IEmbedBuilder GetEmbed(long pot) + { + return _embedFunc(CurrencyEvent.Type.Reaction, _opts, pot); + } + + private async Task OnMessageDeleted(Cacheable msg, ISocketMessageChannel _) + { + if (msg.Id == _msg.Id) + { + await StopEvent().ConfigureAwait(false); + } + } + + private readonly object stopLock = new object(); + public async Task StopEvent() + { + await Task.Yield(); + lock (stopLock) + { + if (Stopped) + return; + Stopped = true; + _client.MessageDeleted -= OnMessageDeleted; + _client.ReactionAdded -= HandleReaction; + _t.Change(Timeout.Infinite, Timeout.Infinite); + _timeout?.Change(Timeout.Infinite, Timeout.Infinite); + try { var _ = _msg.DeleteAsync(); } catch { } + var os = OnEnded(_guild.Id); + } + } + + private Task HandleReaction(Cacheable msg, + ISocketMessageChannel ch, SocketReaction r) + { + var _ = Task.Run(() => + { + if (_emote.Name != r.Emote.Name) + return; + var gu = (r.User.IsSpecified ? r.User.Value : null) as IGuildUser; + if (gu is null // no unknown users, as they could be bots, or alts + || msg.Id != _msg.Id // same message + || gu.IsBot // no bots + || (DateTime.UtcNow - gu.CreatedAt).TotalDays <= 5 // no recently created accounts + || (_noRecentlyJoinedServer && // if specified, no users who joined the server in the last 24h + (gu.JoinedAt is null || (DateTime.UtcNow - gu.JoinedAt.Value).TotalDays < 1))) // and no users for who we don't know when they joined + { + return; + } + // there has to be money left in the pot + // and the user wasn't rewarded + if (_awardedUsers.Add(r.UserId) && TryTakeFromPot()) + { + _toAward.Enqueue(r.UserId); + if (_isPotLimited && PotSize < _amount) + PotEmptied = true; + } + }); + return Task.CompletedTask; + } + + private readonly object potLock = new object(); + private bool TryTakeFromPot() + { + if (_isPotLimited) + { + lock (potLock) + { + if (PotSize < _amount) + return false; + + PotSize -= _amount; + return true; + } + } + return true; + } +} \ No newline at end of file diff --git a/src/NadekoBot/Modules/Gambling/Common/GamblingConfig.cs b/src/NadekoBot/Modules/Gambling/Common/GamblingConfig.cs index e938e941a..c763b228c 100644 --- a/src/NadekoBot/Modules/Gambling/Common/GamblingConfig.cs +++ b/src/NadekoBot/Modules/Gambling/Common/GamblingConfig.cs @@ -1,321 +1,318 @@ -using System; -using System.Collections.Generic; -using Cloneable; +using Cloneable; using NadekoBot.Common; using NadekoBot.Common.Yml; using SixLabors.ImageSharp.PixelFormats; using YamlDotNet.Serialization; -namespace NadekoBot.Modules.Gambling.Common +namespace NadekoBot.Modules.Gambling.Common; + +[Cloneable] +public sealed partial class GamblingConfig : ICloneable { - [Cloneable] - public sealed partial class GamblingConfig : ICloneable + public GamblingConfig() { - public GamblingConfig() - { - BetRoll = new BetRollConfig(); - WheelOfFortune = new WheelOfFortuneSettings(); - Waifu = new WaifuConfig(); - Currency = new CurrencyConfig(); - BetFlip = new BetFlipConfig(); - Generation = new GenerationConfig(); - Timely = new TimelyConfig(); - Decay = new DecayConfig(); - Slots = new SlotsConfig(); - } + BetRoll = new BetRollConfig(); + WheelOfFortune = new WheelOfFortuneSettings(); + Waifu = new WaifuConfig(); + Currency = new CurrencyConfig(); + BetFlip = new BetFlipConfig(); + Generation = new GenerationConfig(); + Timely = new TimelyConfig(); + Decay = new DecayConfig(); + Slots = new SlotsConfig(); + } - [Comment(@"DO NOT CHANGE")] - public int Version { get; set; } = 2; + [Comment(@"DO NOT CHANGE")] + public int Version { get; set; } = 2; - [Comment(@"Currency settings")] - public CurrencyConfig Currency { get; set; } + [Comment(@"Currency settings")] + public CurrencyConfig Currency { get; set; } - [Comment(@"Minimum amount users can bet (>=0)")] - public int MinBet { get; set; } = 0; + [Comment(@"Minimum amount users can bet (>=0)")] + public int MinBet { get; set; } = 0; - [Comment(@"Maximum amount users can bet + [Comment(@"Maximum amount users can bet Set 0 for unlimited")] - public int MaxBet { get; set; } = 0; + public int MaxBet { get; set; } = 0; - [Comment(@"Settings for betflip command")] - public BetFlipConfig BetFlip { get; set; } + [Comment(@"Settings for betflip command")] + public BetFlipConfig BetFlip { get; set; } - [Comment(@"Settings for betroll command")] - public BetRollConfig BetRoll { get; set; } + [Comment(@"Settings for betroll command")] + public BetRollConfig BetRoll { get; set; } - [Comment(@"Automatic currency generation settings.")] - public GenerationConfig Generation { get; set; } + [Comment(@"Automatic currency generation settings.")] + public GenerationConfig Generation { get; set; } - [Comment(@"Settings for timely command + [Comment(@"Settings for timely command (letting people claim X amount of currency every Y hours)")] - public TimelyConfig Timely { get; set; } + public TimelyConfig Timely { get; set; } - [Comment(@"How much will each user's owned currency decay over time.")] - public DecayConfig Decay { get; set; } + [Comment(@"How much will each user's owned currency decay over time.")] + public DecayConfig Decay { get; set; } - [Comment(@"Settings for Wheel Of Fortune command.")] - public WheelOfFortuneSettings WheelOfFortune { get; set; } + [Comment(@"Settings for Wheel Of Fortune command.")] + public WheelOfFortuneSettings WheelOfFortune { get; set; } - [Comment(@"Settings related to waifus")] - public WaifuConfig Waifu { get; set; } + [Comment(@"Settings related to waifus")] + public WaifuConfig Waifu { get; set; } - [Comment(@"Amount of currency selfhosters will get PER pledged dollar CENT. + [Comment(@"Amount of currency selfhosters will get PER pledged dollar CENT. 1 = 100 currency per $. Used almost exclusively on public nadeko.")] - public decimal PatreonCurrencyPerCent { get; set; } = 1; + public decimal PatreonCurrencyPerCent { get; set; } = 1; - [Comment(@"Currency reward per vote. + [Comment(@"Currency reward per vote. This will work only if you've set up VotesApi and correct credentials for topgg and/or discords voting")] - public long VoteReward { get; set; } = 100; + public long VoteReward { get; set; } = 100; - [Comment(@"Slot config")] - public SlotsConfig Slots { get; set; } - } + [Comment(@"Slot config")] + public SlotsConfig Slots { get; set; } +} - public class CurrencyConfig - { - [Comment(@"What is the emoji/character which represents the currency")] - public string Sign { get; set; } = "🌸"; +public class CurrencyConfig +{ + [Comment(@"What is the emoji/character which represents the currency")] + public string Sign { get; set; } = "🌸"; - [Comment(@"What is the name of the currency")] - public string Name { get; set; } = "Nadeko Flower"; - } + [Comment(@"What is the name of the currency")] + public string Name { get; set; } = "Nadeko Flower"; +} - [Cloneable] - public partial class TimelyConfig - { - [Comment(@"How much currency will the users get every time they run .timely command +[Cloneable] +public partial class TimelyConfig +{ + [Comment(@"How much currency will the users get every time they run .timely command setting to 0 or less will disable this feature")] - public int Amount { get; set; } = 0; + public int Amount { get; set; } = 0; - [Comment(@"How often (in hours) can users claim currency with .timely command + [Comment(@"How often (in hours) can users claim currency with .timely command setting to 0 or less will disable this feature")] - public int Cooldown { get; set; } = 24; - } + public int Cooldown { get; set; } = 24; +} - [Cloneable] - public partial class BetFlipConfig - { - [Comment(@"Bet multiplier if user guesses correctly")] - public decimal Multiplier { get; set; } = 1.95M; - } +[Cloneable] +public partial class BetFlipConfig +{ + [Comment(@"Bet multiplier if user guesses correctly")] + public decimal Multiplier { get; set; } = 1.95M; +} - [Cloneable] - public partial class BetRollConfig - { - [Comment(@"When betroll is played, user will roll a number 0-100. +[Cloneable] +public partial class BetRollConfig +{ + [Comment(@"When betroll is played, user will roll a number 0-100. This setting will describe which multiplier is used for when the roll is higher than the given number. Doesn't have to be ordered.")] - public BetRollPair[] Pairs { get; set; } = Array.Empty(); + public BetRollPair[] Pairs { get; set; } = Array.Empty(); - public BetRollConfig() - { - Pairs = new BetRollPair[] - { - new BetRollPair { WhenAbove = 99, MultiplyBy = 10 }, - new BetRollPair { WhenAbove = 90, MultiplyBy = 4 }, - new BetRollPair { WhenAbove = 66, MultiplyBy = 2 } - }; - } - } - - [Cloneable] - public partial class GenerationConfig + public BetRollConfig() { - [Comment(@"when currency is generated, should it also have a random password + Pairs = new BetRollPair[] + { + new BetRollPair { WhenAbove = 99, MultiplyBy = 10 }, + new BetRollPair { WhenAbove = 90, MultiplyBy = 4 }, + new BetRollPair { WhenAbove = 66, MultiplyBy = 2 } + }; + } +} + +[Cloneable] +public partial class GenerationConfig +{ + [Comment(@"when currency is generated, should it also have a random password associated with it which users have to type after the .pick command in order to get it")] - public bool HasPassword { get; set; } = true; + public bool HasPassword { get; set; } = true; - [Comment(@"Every message sent has a certain % chance to generate the currency + [Comment(@"Every message sent has a certain % chance to generate the currency specify the percentage here (1 being 100%, 0 being 0% - for example default is 0.02, which is 2%")] - public decimal Chance { get; set; } = 0.02M; + public decimal Chance { get; set; } = 0.02M; - [Comment(@"How many seconds have to pass for the next message to have a chance to spawn currency")] - public int GenCooldown { get; set; } = 10; + [Comment(@"How many seconds have to pass for the next message to have a chance to spawn currency")] + public int GenCooldown { get; set; } = 10; - [Comment(@"Minimum amount of currency that can spawn")] - public int MinAmount { get; set; } = 1; + [Comment(@"Minimum amount of currency that can spawn")] + public int MinAmount { get; set; } = 1; - [Comment(@"Maximum amount of currency that can spawn. + [Comment(@"Maximum amount of currency that can spawn. Set to the same value as MinAmount to always spawn the same amount")] - public int MaxAmount { get; set; } = 1; - } + public int MaxAmount { get; set; } = 1; +} - [Cloneable] - public partial class DecayConfig - { - [Comment(@"Percentage of user's current currency which will be deducted every 24h. +[Cloneable] +public partial class DecayConfig +{ + [Comment(@"Percentage of user's current currency which will be deducted every 24h. 0 - 1 (1 is 100%, 0.5 50%, 0 disabled)")] - public decimal Percent { get; set; } = 0; + public decimal Percent { get; set; } = 0; - [Comment(@"Maximum amount of user's currency that can decay at each interval. 0 for unlimited.")] - public int MaxDecay { get; set; } = 0; + [Comment(@"Maximum amount of user's currency that can decay at each interval. 0 for unlimited.")] + public int MaxDecay { get; set; } = 0; - [Comment(@"Only users who have more than this amount will have their currency decay.")] - public int MinThreshold { get; set; } = 99; + [Comment(@"Only users who have more than this amount will have their currency decay.")] + public int MinThreshold { get; set; } = 99; - [Comment(@"How often, in hours, does the decay run. Default is 24 hours")] - public int HourInterval { get; set; } = 24; - } + [Comment(@"How often, in hours, does the decay run. Default is 24 hours")] + public int HourInterval { get; set; } = 24; +} - [Cloneable] - public partial class WheelOfFortuneSettings +[Cloneable] +public partial class WheelOfFortuneSettings +{ + [Comment(@"Self-Explanatory. Has to have 8 values, otherwise the command won't work.")] + public decimal[] Multipliers { get; set; } + + public WheelOfFortuneSettings() { - [Comment(@"Self-Explanatory. Has to have 8 values, otherwise the command won't work.")] - public decimal[] Multipliers { get; set; } - - public WheelOfFortuneSettings() + Multipliers = new decimal[] { - Multipliers = new decimal[] - { - 1.7M, - 1.5M, - 0.2M, - 0.1M, - 0.3M, - 0.5M, - 1.2M, - 2.4M, - }; - } + 1.7M, + 1.5M, + 0.2M, + 0.1M, + 0.3M, + 0.5M, + 1.2M, + 2.4M, + }; } +} - [Cloneable] - public sealed partial class WaifuConfig - { - [Comment(@"Minimum price a waifu can have")] - public int MinPrice { get; set; } = 50; +[Cloneable] +public sealed partial class WaifuConfig +{ + [Comment(@"Minimum price a waifu can have")] + public int MinPrice { get; set; } = 50; - public MultipliersData Multipliers { get; set; } = new MultipliersData(); + public MultipliersData Multipliers { get; set; } = new MultipliersData(); - [Comment(@"List of items available for gifting. + [Comment(@"List of items available for gifting. If negative is true, gift will instead reduce waifu value.")] - public List Items { get; set; } = new List(); + public List Items { get; set; } = new List(); - public WaifuConfig() - { - Items = new() - { - new("🥔", 5, "Potato"), - new("🍪", 10, "Cookie"), - new("🥖", 20, "Bread"), - new("🍭", 30, "Lollipop"), - new("🌹", 50, "Rose"), - new("🍺", 70, "Beer"), - new("🌮", 85, "Taco"), - new("💌", 100, "LoveLetter"), - new("🥛", 125, "Milk"), - new("🍕", 150, "Pizza"), - new("🍫", 200, "Chocolate"), - new("🍦", 250, "Icecream"), - new("🍣", 300, "Sushi"), - new("🍚", 400, "Rice"), - new("🍉", 500, "Watermelon"), - new("🍱", 600, "Bento"), - new("🎟", 800, "MovieTicket"), - new("🍰", 1000, "Cake"), - new("📔", 1500, "Book"), - new("🐱", 2000, "Cat"), - new("🐶", 2001, "Dog"), - new("🐼", 2500, "Panda"), - new("💄", 3000, "Lipstick"), - new("👛", 3500, "Purse"), - new("📱", 4000, "iPhone"), - new("👗", 4500, "Dress"), - new("💻", 5000, "Laptop"), - new("🎻", 7500, "Violin"), - new("🎹", 8000, "Piano"), - new("🚗", 9000, "Car"), - new("💍", 10000, "Ring"), - new("🛳", 12000, "Ship"), - new("🏠", 15000, "House"), - new("🚁", 20000, "Helicopter"), - new("🚀", 30000, "Spaceship"), - new("🌕", 50000, "Moon") - }; - } - } - - [Cloneable] - public sealed partial class MultipliersData + public WaifuConfig() { - [Comment(@"Multiplier for waifureset. Default 150. + Items = new() + { + new("🥔", 5, "Potato"), + new("🍪", 10, "Cookie"), + new("🥖", 20, "Bread"), + new("🍭", 30, "Lollipop"), + new("🌹", 50, "Rose"), + new("🍺", 70, "Beer"), + new("🌮", 85, "Taco"), + new("💌", 100, "LoveLetter"), + new("🥛", 125, "Milk"), + new("🍕", 150, "Pizza"), + new("🍫", 200, "Chocolate"), + new("🍦", 250, "Icecream"), + new("🍣", 300, "Sushi"), + new("🍚", 400, "Rice"), + new("🍉", 500, "Watermelon"), + new("🍱", 600, "Bento"), + new("🎟", 800, "MovieTicket"), + new("🍰", 1000, "Cake"), + new("📔", 1500, "Book"), + new("🐱", 2000, "Cat"), + new("🐶", 2001, "Dog"), + new("🐼", 2500, "Panda"), + new("💄", 3000, "Lipstick"), + new("👛", 3500, "Purse"), + new("📱", 4000, "iPhone"), + new("👗", 4500, "Dress"), + new("💻", 5000, "Laptop"), + new("🎻", 7500, "Violin"), + new("🎹", 8000, "Piano"), + new("🚗", 9000, "Car"), + new("💍", 10000, "Ring"), + new("🛳", 12000, "Ship"), + new("🏠", 15000, "House"), + new("🚁", 20000, "Helicopter"), + new("🚀", 30000, "Spaceship"), + new("🌕", 50000, "Moon") + }; + } +} + +[Cloneable] +public sealed partial class MultipliersData +{ + [Comment(@"Multiplier for waifureset. Default 150. Formula (at the time of writing this): price = (waifu_price * 1.25f) + ((number_of_divorces + changes_of_heart + 2) * WaifuReset) rounded up")] - public int WaifuReset { get; set; } = 150; + public int WaifuReset { get; set; } = 150; - [Comment(@"The minimum amount of currency that you have to pay + [Comment(@"The minimum amount of currency that you have to pay in order to buy a waifu who doesn't have a crush on you. Default is 1.1 Example: If a waifu is worth 100, you will have to pay at least 100 * NormalClaim currency to claim her. (100 * 1.1 = 110)")] - public decimal NormalClaim { get; set; } = 1.1m; + public decimal NormalClaim { get; set; } = 1.1m; - [Comment(@"The minimum amount of currency that you have to pay + [Comment(@"The minimum amount of currency that you have to pay in order to buy a waifu that has a crush on you. Default is 0.88 Example: If a waifu is worth 100, you will have to pay at least 100 * CrushClaim currency to claim her. (100 * 0.88 = 88)")] - public decimal CrushClaim { get; set; } = 0.88M; + public decimal CrushClaim { get; set; } = 0.88M; - [Comment(@"When divorcing a waifu, her new value will be her current value multiplied by this number. + [Comment(@"When divorcing a waifu, her new value will be her current value multiplied by this number. Default 0.75 (meaning will lose 25% of her value)")] - public decimal DivorceNewValue { get; set; } = 0.75M; + public decimal DivorceNewValue { get; set; } = 0.75M; - [Comment(@"All gift prices will be multiplied by this number. + [Comment(@"All gift prices will be multiplied by this number. Default 1 (meaning no effect)")] - public decimal AllGiftPrices { get; set; } = 1.0M; + public decimal AllGiftPrices { get; set; } = 1.0M; - [Comment(@"What percentage of the value of the gift will a waifu gain when she's gifted. + [Comment(@"What percentage of the value of the gift will a waifu gain when she's gifted. Default 0.95 (meaning 95%) Example: If a waifu is worth 1000, and she receives a gift worth 100, her new value will be 1095)")] - public decimal GiftEffect { get; set; } = 0.95M; + public decimal GiftEffect { get; set; } = 0.95M; - [Comment(@"What percentage of the value of the gift will a waifu lose when she's gifted a gift marked as 'negative'. + [Comment(@"What percentage of the value of the gift will a waifu lose when she's gifted a gift marked as 'negative'. Default 0.5 (meaning 50%) Example: If a waifu is worth 1000, and she receives a negative gift worth 100, her new value will be 950)")] - public decimal NegativeGiftEffect { get; set; } = 0.50M; - } + public decimal NegativeGiftEffect { get; set; } = 0.50M; +} - public sealed partial class SlotsConfig - { - [Comment(@"Hex value of the color which the numbers on the slot image will have.")] - public Rgba32 CurrencyFontColor { get; set; } = SixLabors.ImageSharp.Color.Red; - } +public sealed partial class SlotsConfig +{ + [Comment(@"Hex value of the color which the numbers on the slot image will have.")] + public Rgba32 CurrencyFontColor { get; set; } = SixLabors.ImageSharp.Color.Red; +} - [Cloneable] - public sealed partial class WaifuItemModel - { - public string ItemEmoji { get; set; } - public int Price { get; set; } - public string Name { get; set; } +[Cloneable] +public sealed partial class WaifuItemModel +{ + public string ItemEmoji { get; set; } + public int Price { get; set; } + public string Name { get; set; } - [YamlMember(DefaultValuesHandling = DefaultValuesHandling.OmitDefaults)] - public bool Negative { get; set; } + [YamlMember(DefaultValuesHandling = DefaultValuesHandling.OmitDefaults)] + public bool Negative { get; set; } - public WaifuItemModel() - { + public WaifuItemModel() + { - } + } - public WaifuItemModel(string itemEmoji, int price, string name, bool negative = false) - { - ItemEmoji = itemEmoji; - Price = price; - Name = name; - Negative = negative; - } - - - public override string ToString() => Name; - } - - - [Cloneable] - public sealed partial class BetRollPair + public WaifuItemModel(string itemEmoji, int price, string name, bool negative = false) { - public int WhenAbove { get; set; } - public float MultiplyBy { get; set; } + ItemEmoji = itemEmoji; + Price = price; + Name = name; + Negative = negative; } + + + public override string ToString() => Name; +} + + +[Cloneable] +public sealed partial class BetRollPair +{ + public int WhenAbove { get; set; } + public float MultiplyBy { get; set; } } \ No newline at end of file diff --git a/src/NadekoBot/Modules/Gambling/Common/GamblingError.cs b/src/NadekoBot/Modules/Gambling/Common/GamblingError.cs index 4732cb67e..919f56695 100644 --- a/src/NadekoBot/Modules/Gambling/Common/GamblingError.cs +++ b/src/NadekoBot/Modules/Gambling/Common/GamblingError.cs @@ -1,8 +1,7 @@ -namespace NadekoBot.Modules.Gambling +namespace NadekoBot.Modules.Gambling; + +public enum GamblingError { - public enum GamblingError - { - None, - NotEnough - } + None, + NotEnough } \ No newline at end of file diff --git a/src/NadekoBot/Modules/Gambling/Common/GamblingTopLevelModule.cs b/src/NadekoBot/Modules/Gambling/Common/GamblingTopLevelModule.cs index 0aebdebc8..42fd12bf5 100644 --- a/src/NadekoBot/Modules/Gambling/Common/GamblingTopLevelModule.cs +++ b/src/NadekoBot/Modules/Gambling/Common/GamblingTopLevelModule.cs @@ -1,68 +1,64 @@ -using System; -using Discord; -using NadekoBot.Services; -using NadekoBot.Modules; +using Discord; using System.Threading.Tasks; using NadekoBot.Modules.Gambling.Services; -namespace NadekoBot.Modules.Gambling.Common +namespace NadekoBot.Modules.Gambling.Common; + +public abstract class GamblingModule : NadekoModule { - public abstract class GamblingModule : NadekoModule - { - private readonly Lazy _lazyConfig; - protected GamblingConfig _config => _lazyConfig.Value; - protected string CurrencySign => _config.Currency.Sign; - protected string CurrencyName => _config.Currency.Name; + private readonly Lazy _lazyConfig; + protected GamblingConfig _config => _lazyConfig.Value; + protected string CurrencySign => _config.Currency.Sign; + protected string CurrencyName => _config.Currency.Name; - protected GamblingModule(GamblingConfigService gambService) - { - _lazyConfig = new Lazy(() => gambService.Data); - } - - private async Task InternalCheckBet(long amount) - { - if (amount < 1) - { - return false; - } - if (amount < _config.MinBet) - { - await ReplyErrorLocalizedAsync(strs.min_bet_limit( - Format.Bold(_config.MinBet.ToString()) + CurrencySign)); - return false; - } - if (_config.MaxBet > 0 && amount > _config.MaxBet) - { - await ReplyErrorLocalizedAsync(strs.max_bet_limit( - Format.Bold(_config.MaxBet.ToString()) + CurrencySign)); - return false; - } - return true; - } - - protected Task CheckBetMandatory(long amount) - { - if (amount < 1) - { - return Task.FromResult(false); - } - return InternalCheckBet(amount); - } - - protected Task CheckBetOptional(long amount) - { - if (amount == 0) - { - return Task.FromResult(true); - } - return InternalCheckBet(amount); - } + protected GamblingModule(GamblingConfigService gambService) + { + _lazyConfig = new Lazy(() => gambService.Data); } - public abstract class GamblingSubmodule : GamblingModule + private async Task InternalCheckBet(long amount) { - protected GamblingSubmodule(GamblingConfigService gamblingConfService) : base(gamblingConfService) + if (amount < 1) { + return false; } + if (amount < _config.MinBet) + { + await ReplyErrorLocalizedAsync(strs.min_bet_limit( + Format.Bold(_config.MinBet.ToString()) + CurrencySign)); + return false; + } + if (_config.MaxBet > 0 && amount > _config.MaxBet) + { + await ReplyErrorLocalizedAsync(strs.max_bet_limit( + Format.Bold(_config.MaxBet.ToString()) + CurrencySign)); + return false; + } + return true; + } + + protected Task CheckBetMandatory(long amount) + { + if (amount < 1) + { + return Task.FromResult(false); + } + return InternalCheckBet(amount); + } + + protected Task CheckBetOptional(long amount) + { + if (amount == 0) + { + return Task.FromResult(true); + } + return InternalCheckBet(amount); } } + +public abstract class GamblingSubmodule : GamblingModule +{ + protected GamblingSubmodule(GamblingConfigService gamblingConfService) : base(gamblingConfService) + { + } +} \ No newline at end of file diff --git a/src/NadekoBot/Modules/Gambling/Common/Payout.cs b/src/NadekoBot/Modules/Gambling/Common/Payout.cs index 2a2610d14..4e7b50ed4 100644 --- a/src/NadekoBot/Modules/Gambling/Common/Payout.cs +++ b/src/NadekoBot/Modules/Gambling/Common/Payout.cs @@ -1,8 +1,7 @@ -namespace NadekoBot.Modules.Gambling.Common +namespace NadekoBot.Modules.Gambling.Common; + +public class Payout { - public class Payout - { - public string User { get; set; } - public int Amount { get; set; } - } -} + public string User { get; set; } + public int Amount { get; set; } +} \ No newline at end of file diff --git a/src/NadekoBot/Modules/Gambling/Common/RollDuelGame.cs b/src/NadekoBot/Modules/Gambling/Common/RollDuelGame.cs index 899f7d935..cc3d296b1 100644 --- a/src/NadekoBot/Modules/Gambling/Common/RollDuelGame.cs +++ b/src/NadekoBot/Modules/Gambling/Common/RollDuelGame.cs @@ -1,140 +1,137 @@ using NadekoBot.Common; using NadekoBot.Services; -using System; -using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; -namespace NadekoBot.Modules.Gambling.Common +namespace NadekoBot.Modules.Gambling.Common; + +public class RollDuelGame { - public class RollDuelGame + public ulong P1 { get; } + public ulong P2 { get; } + + private readonly ulong _botId; + + public long Amount { get; } + + private readonly ICurrencyService _cs; + + public enum State { - public ulong P1 { get; } - public ulong P2 { get; } + Waiting, + Running, + Ended, + } - private readonly ulong _botId; - - public long Amount { get; } - - private readonly ICurrencyService _cs; - - public enum State - { - Waiting, - Running, - Ended, - } - - public enum Reason - { - Normal, - NoFunds, - Timeout, - } + public enum Reason + { + Normal, + NoFunds, + Timeout, + } - private readonly Timer _timeoutTimer; - private readonly NadekoRandom _rng = new NadekoRandom(); - private readonly SemaphoreSlim _locker = new SemaphoreSlim(1, 1); + private readonly Timer _timeoutTimer; + private readonly NadekoRandom _rng = new NadekoRandom(); + private readonly SemaphoreSlim _locker = new SemaphoreSlim(1, 1); - public event Func OnGameTick; - public event Func OnEnded; + public event Func OnGameTick; + public event Func OnEnded; - public List<(int, int)> Rolls { get; } = new List<(int, int)>(); - public State CurrentState { get; private set; } - public ulong Winner { get; private set; } + public List<(int, int)> Rolls { get; } = new List<(int, int)>(); + public State CurrentState { get; private set; } + public ulong Winner { get; private set; } - public RollDuelGame(ICurrencyService cs, ulong botId, ulong p1, ulong p2, long amount) - { - this.P1 = p1; - this.P2 = p2; - this._botId = botId; - this.Amount = amount; - _cs = cs; + public RollDuelGame(ICurrencyService cs, ulong botId, ulong p1, ulong p2, long amount) + { + this.P1 = p1; + this.P2 = p2; + this._botId = botId; + this.Amount = amount; + _cs = cs; - _timeoutTimer = new Timer(async delegate - { - await _locker.WaitAsync().ConfigureAwait(false); - try - { - if (CurrentState != State.Waiting) - return; - CurrentState = State.Ended; - await (OnEnded?.Invoke(this, Reason.Timeout)).ConfigureAwait(false); - } - catch { } - finally - { - _locker.Release(); - } - }, null, TimeSpan.FromSeconds(15), TimeSpan.FromMilliseconds(-1)); - } - - public async Task StartGame() + _timeoutTimer = new Timer(async delegate { await _locker.WaitAsync().ConfigureAwait(false); try { if (CurrentState != State.Waiting) return; - _timeoutTimer.Change(Timeout.Infinite, Timeout.Infinite); - CurrentState = State.Running; + CurrentState = State.Ended; + await (OnEnded?.Invoke(this, Reason.Timeout)).ConfigureAwait(false); } + catch { } finally { _locker.Release(); } - - if(!await _cs.RemoveAsync(P1, "Roll Duel", Amount).ConfigureAwait(false)) - { - await (OnEnded?.Invoke(this, Reason.NoFunds)).ConfigureAwait(false); - CurrentState = State.Ended; - return; - } - if(!await _cs.RemoveAsync(P2, "Roll Duel", Amount).ConfigureAwait(false)) - { - await _cs.AddAsync(P1, "Roll Duel - refund", Amount).ConfigureAwait(false); - await (OnEnded?.Invoke(this, Reason.NoFunds)).ConfigureAwait(false); - CurrentState = State.Ended; - return; - } - - int n1, n2; - do - { - n1 = _rng.Next(0, 5); - n2 = _rng.Next(0, 5); - Rolls.Add((n1, n2)); - if (n1 != n2) - { - if (n1 > n2) - { - Winner = P1; - } - else - { - Winner = P2; - } - var won = (long)(Amount * 2 * 0.98f); - await _cs.AddAsync(Winner, "Roll Duel win", won) - .ConfigureAwait(false); - - await _cs.AddAsync(_botId, "Roll Duel fee", Amount * 2 - won) - .ConfigureAwait(false); - } - try { await (OnGameTick?.Invoke(this)).ConfigureAwait(false); } catch { } - await Task.Delay(2500).ConfigureAwait(false); - if (n1 != n2) - break; - } - while (true); - CurrentState = State.Ended; - await (OnEnded?.Invoke(this, Reason.Normal)).ConfigureAwait(false); - } + }, null, TimeSpan.FromSeconds(15), TimeSpan.FromMilliseconds(-1)); } - public struct RollDuelChallenge + public async Task StartGame() { - public ulong Player1 { get; set; } - public ulong Player2 { get; set; } + await _locker.WaitAsync().ConfigureAwait(false); + try + { + if (CurrentState != State.Waiting) + return; + _timeoutTimer.Change(Timeout.Infinite, Timeout.Infinite); + CurrentState = State.Running; + } + finally + { + _locker.Release(); + } + + if(!await _cs.RemoveAsync(P1, "Roll Duel", Amount).ConfigureAwait(false)) + { + await (OnEnded?.Invoke(this, Reason.NoFunds)).ConfigureAwait(false); + CurrentState = State.Ended; + return; + } + if(!await _cs.RemoveAsync(P2, "Roll Duel", Amount).ConfigureAwait(false)) + { + await _cs.AddAsync(P1, "Roll Duel - refund", Amount).ConfigureAwait(false); + await (OnEnded?.Invoke(this, Reason.NoFunds)).ConfigureAwait(false); + CurrentState = State.Ended; + return; + } + + int n1, n2; + do + { + n1 = _rng.Next(0, 5); + n2 = _rng.Next(0, 5); + Rolls.Add((n1, n2)); + if (n1 != n2) + { + if (n1 > n2) + { + Winner = P1; + } + else + { + Winner = P2; + } + var won = (long)(Amount * 2 * 0.98f); + await _cs.AddAsync(Winner, "Roll Duel win", won) + .ConfigureAwait(false); + + await _cs.AddAsync(_botId, "Roll Duel fee", Amount * 2 - won) + .ConfigureAwait(false); + } + try { await (OnGameTick?.Invoke(this)).ConfigureAwait(false); } catch { } + await Task.Delay(2500).ConfigureAwait(false); + if (n1 != n2) + break; + } + while (true); + CurrentState = State.Ended; + await (OnEnded?.Invoke(this, Reason.Normal)).ConfigureAwait(false); } } + +public struct RollDuelChallenge +{ + public ulong Player1 { get; set; } + public ulong Player2 { get; set; } +} \ No newline at end of file diff --git a/src/NadekoBot/Modules/Gambling/Common/Slot/SlotGame.cs b/src/NadekoBot/Modules/Gambling/Common/Slot/SlotGame.cs index 2140b640b..0a0cca049 100644 --- a/src/NadekoBot/Modules/Gambling/Common/Slot/SlotGame.cs +++ b/src/NadekoBot/Modules/Gambling/Common/Slot/SlotGame.cs @@ -1,44 +1,41 @@ -using System; -using System.Linq; -using NadekoBot.Common; +using NadekoBot.Common; -namespace NadekoBot.Modules.Gambling.Common.Slot +namespace NadekoBot.Modules.Gambling.Common.Slot; + +public class SlotGame { - public class SlotGame + public class Result { - public class Result + public float Multiplier { get; } + public int[] Rolls { get; } + + public Result(float multiplier, int[] rolls) { - public float Multiplier { get; } - public int[] Rolls { get; } - - public Result(float multiplier, int[] rolls) - { - Multiplier = multiplier; - Rolls = rolls; - } - } - - private static readonly Random _rng = new NadekoRandom(); - - public SlotGame() - { - } - - public Result Spin() - { - var rolls = new int[] { _rng.Next(0, 6), _rng.Next(0, 6), _rng.Next(0, 6) }; - var multi = 0; - - if (rolls.All(x => x == 5)) - multi = 30; - else if (rolls.All(x => x == rolls[0])) - multi = 10; - else if (rolls.Count(x => x == 5) == 2) - multi = 4; - else if (rolls.Any(x => x == 5)) - multi = 1; - - return new Result(multi, rolls); + Multiplier = multiplier; + Rolls = rolls; } } + + private static readonly Random _rng = new NadekoRandom(); + + public SlotGame() + { + } + + public Result Spin() + { + var rolls = new int[] { _rng.Next(0, 6), _rng.Next(0, 6), _rng.Next(0, 6) }; + var multi = 0; + + if (rolls.All(x => x == 5)) + multi = 30; + else if (rolls.All(x => x == rolls[0])) + multi = 10; + else if (rolls.Count(x => x == 5) == 2) + multi = 4; + else if (rolls.Any(x => x == 5)) + multi = 1; + + return new Result(multi, rolls); + } } \ No newline at end of file diff --git a/src/NadekoBot/Modules/Gambling/Common/SlotResponse.cs b/src/NadekoBot/Modules/Gambling/Common/SlotResponse.cs index a9ecfaafe..57220166f 100644 --- a/src/NadekoBot/Modules/Gambling/Common/SlotResponse.cs +++ b/src/NadekoBot/Modules/Gambling/Common/SlotResponse.cs @@ -1,12 +1,9 @@ -using System.Collections.Generic; +namespace NadekoBot.Modules.Gambling; -namespace NadekoBot.Modules.Gambling +public class SlotResponse { - public class SlotResponse - { - public float Multiplier { get; set; } - public long Won { get; set; } - public List Rolls { get; set; } = new List(); - public GamblingError Error { get; set; } - } + public float Multiplier { get; set; } + public long Won { get; set; } + public List Rolls { get; set; } = new List(); + public GamblingError Error { get; set; } } \ No newline at end of file diff --git a/src/NadekoBot/Modules/Gambling/Common/Waifu/AffinityTitle.cs b/src/NadekoBot/Modules/Gambling/Common/Waifu/AffinityTitle.cs index 9e4c9775a..d9f308b73 100644 --- a/src/NadekoBot/Modules/Gambling/Common/Waifu/AffinityTitle.cs +++ b/src/NadekoBot/Modules/Gambling/Common/Waifu/AffinityTitle.cs @@ -1,16 +1,15 @@ -namespace NadekoBot.Modules.Gambling.Common.Waifu +namespace NadekoBot.Modules.Gambling.Common.Waifu; + +public enum AffinityTitle { - public enum AffinityTitle - { - Pure, - Faithful, - Playful, - Cheater, - Tainted, - Corrupted, - Lewd, - Sloot, - Depraved, - Harlot - } -} + Pure, + Faithful, + Playful, + Cheater, + Tainted, + Corrupted, + Lewd, + Sloot, + Depraved, + Harlot +} \ No newline at end of file diff --git a/src/NadekoBot/Modules/Gambling/Common/Waifu/ClaimTitle.cs b/src/NadekoBot/Modules/Gambling/Common/Waifu/ClaimTitle.cs index 66b01b4ee..c32fb97ce 100644 --- a/src/NadekoBot/Modules/Gambling/Common/Waifu/ClaimTitle.cs +++ b/src/NadekoBot/Modules/Gambling/Common/Waifu/ClaimTitle.cs @@ -1,18 +1,17 @@ -namespace NadekoBot.Modules.Gambling.Common.Waifu +namespace NadekoBot.Modules.Gambling.Common.Waifu; + +public enum ClaimTitle { - public enum ClaimTitle - { - Lonely, - Devoted, - Rookie, - Schemer, - Dilettante, - Intermediate, - Seducer, - Expert, - Veteran, - Incubis, - Harem_King, - Harem_God, - } -} + Lonely, + Devoted, + Rookie, + Schemer, + Dilettante, + Intermediate, + Seducer, + Expert, + Veteran, + Incubis, + Harem_King, + Harem_God, +} \ No newline at end of file diff --git a/src/NadekoBot/Modules/Gambling/Common/Waifu/DivorceResult.cs b/src/NadekoBot/Modules/Gambling/Common/Waifu/DivorceResult.cs index 821cfe05c..e42e73c8e 100644 --- a/src/NadekoBot/Modules/Gambling/Common/Waifu/DivorceResult.cs +++ b/src/NadekoBot/Modules/Gambling/Common/Waifu/DivorceResult.cs @@ -1,10 +1,9 @@ -namespace NadekoBot.Modules.Gambling.Common.Waifu +namespace NadekoBot.Modules.Gambling.Common.Waifu; + +public enum DivorceResult { - public enum DivorceResult - { - Success, - SucessWithPenalty, - NotYourWife, - Cooldown - } -} + Success, + SucessWithPenalty, + NotYourWife, + Cooldown +} \ No newline at end of file diff --git a/src/NadekoBot/Modules/Gambling/Common/Waifu/WaifuClaimResult.cs b/src/NadekoBot/Modules/Gambling/Common/Waifu/WaifuClaimResult.cs index b126a5eed..adcb73250 100644 --- a/src/NadekoBot/Modules/Gambling/Common/Waifu/WaifuClaimResult.cs +++ b/src/NadekoBot/Modules/Gambling/Common/Waifu/WaifuClaimResult.cs @@ -1,9 +1,8 @@ -namespace NadekoBot.Modules.Gambling.Common.Waifu +namespace NadekoBot.Modules.Gambling.Common.Waifu; + +public enum WaifuClaimResult { - public enum WaifuClaimResult - { - Success, - NotEnoughFunds, - InsufficientAmount - } -} + Success, + NotEnoughFunds, + InsufficientAmount +} \ No newline at end of file diff --git a/src/NadekoBot/Modules/Gambling/Common/WheelOfFortune/WheelOfFortune.cs b/src/NadekoBot/Modules/Gambling/Common/WheelOfFortune/WheelOfFortune.cs index e63f5419a..358ecd832 100644 --- a/src/NadekoBot/Modules/Gambling/Common/WheelOfFortune/WheelOfFortune.cs +++ b/src/NadekoBot/Modules/Gambling/Common/WheelOfFortune/WheelOfFortune.cs @@ -1,47 +1,45 @@ using System.Threading.Tasks; using NadekoBot.Common; -using NadekoBot.Modules.Gambling.Common; using NadekoBot.Services; -namespace NadekoBot.Modules.Gambling.Common.WheelOfFortune +namespace NadekoBot.Modules.Gambling.Common.WheelOfFortune; + +public class WheelOfFortuneGame { - public class WheelOfFortuneGame + public class Result { - public class Result - { - public int Index { get; set; } - public long Amount { get; set; } - } - - private readonly NadekoRandom _rng; - private readonly ICurrencyService _cs; - private readonly long _bet; - private readonly GamblingConfig _config; - private readonly ulong _userId; - - public WheelOfFortuneGame(ulong userId, long bet, GamblingConfig config, ICurrencyService cs) - { - _rng = new NadekoRandom(); - _cs = cs; - _bet = bet; - _config = config; - _userId = userId; - } - - public async Task SpinAsync() - { - var result = _rng.Next(0, _config.WheelOfFortune.Multipliers.Length); - - var amount = (long)(_bet * _config.WheelOfFortune.Multipliers[result]); - - if (amount > 0) - await _cs.AddAsync(_userId, "Wheel Of Fortune - won", amount, gamble: true).ConfigureAwait(false); - - return new Result - { - Index = result, - Amount = amount, - }; - } + public int Index { get; set; } + public long Amount { get; set; } } -} + + private readonly NadekoRandom _rng; + private readonly ICurrencyService _cs; + private readonly long _bet; + private readonly GamblingConfig _config; + private readonly ulong _userId; + + public WheelOfFortuneGame(ulong userId, long bet, GamblingConfig config, ICurrencyService cs) + { + _rng = new NadekoRandom(); + _cs = cs; + _bet = bet; + _config = config; + _userId = userId; + } + + public async Task SpinAsync() + { + var result = _rng.Next(0, _config.WheelOfFortune.Multipliers.Length); + + var amount = (long)(_bet * _config.WheelOfFortune.Multipliers[result]); + + if (amount > 0) + await _cs.AddAsync(_userId, "Wheel Of Fortune - won", amount, gamble: true).ConfigureAwait(false); + + return new Result + { + Index = result, + Amount = amount, + }; + } +} \ No newline at end of file diff --git a/src/NadekoBot/Modules/Gambling/Connect4/Connect4.cs b/src/NadekoBot/Modules/Gambling/Connect4/Connect4.cs index d5d5c3853..35e9fab8b 100644 --- a/src/NadekoBot/Modules/Gambling/Connect4/Connect4.cs +++ b/src/NadekoBot/Modules/Gambling/Connect4/Connect4.cs @@ -1,400 +1,397 @@ using CommandLine; using NadekoBot.Common; using NadekoBot.Services; -using System; using System.Collections.Immutable; -using System.Linq; using System.Threading; using System.Threading.Tasks; -namespace NadekoBot.Modules.Gambling.Common.Connect4 +namespace NadekoBot.Modules.Gambling.Common.Connect4; + +public sealed class Connect4Game : IDisposable { - public sealed class Connect4Game : IDisposable + public enum Phase { - public enum Phase + Joining, // waiting for second player to join + P1Move, + P2Move, + Ended, + } + + public enum Field //temporary most likely + { + Empty, + P1, + P2, + } + + public enum Result + { + Draw, + CurrentPlayerWon, + OtherPlayerWon, + } + + public const int NumberOfColumns = 7; + public const int NumberOfRows = 6; + + public Phase CurrentPhase { get; private set; } = Phase.Joining; + + //state is bottom to top, left to right + private readonly Field[] _gameState = new Field[NumberOfRows * NumberOfColumns]; + private readonly (ulong UserId, string Username)?[] _players = new(ulong, string)?[2]; + + public ImmutableArray GameState => _gameState.ToImmutableArray(); + public ImmutableArray<(ulong UserId, string Username)?> Players => _players.ToImmutableArray(); + + public (ulong UserId, string Username) CurrentPlayer => CurrentPhase == Phase.P1Move + ? _players[0].Value + : _players[1].Value; + + public (ulong UserId, string Username) OtherPlayer => CurrentPhase == Phase.P2Move + ? _players[0].Value + : _players[1].Value; + + //public event Func OnGameStarted; + public event Func OnGameStateUpdated; + public event Func OnGameFailedToStart; + public event Func OnGameEnded; + + private readonly SemaphoreSlim _locker = new SemaphoreSlim(1, 1); + private readonly Options _options; + private readonly ICurrencyService _cs; + private readonly NadekoRandom _rng; + + private Timer _playerTimeoutTimer; + + /* [ ][ ][ ][ ][ ][ ] + * [ ][ ][ ][ ][ ][ ] + * [ ][ ][ ][ ][ ][ ] + * [ ][ ][ ][ ][ ][ ] + * [ ][ ][ ][ ][ ][ ] + * [ ][ ][ ][ ][ ][ ] + * [ ][ ][ ][ ][ ][ ] + */ + + public Connect4Game(ulong userId, string userName, Options options, ICurrencyService cs) + { + _players[0] = (userId, userName); + _options = options; + _cs = cs; + + _rng = new NadekoRandom(); + for (int i = 0; i < NumberOfColumns * NumberOfRows; i++) { - Joining, // waiting for second player to join - P1Move, - P2Move, - Ended, + _gameState[i] = Field.Empty; } + } - public enum Field //temporary most likely + public void Initialize() + { + if (CurrentPhase != Phase.Joining) + return; + var _ = Task.Run(async () => { - Empty, - P1, - P2, - } - - public enum Result - { - Draw, - CurrentPlayerWon, - OtherPlayerWon, - } - - public const int NumberOfColumns = 7; - public const int NumberOfRows = 6; - - public Phase CurrentPhase { get; private set; } = Phase.Joining; - - //state is bottom to top, left to right - private readonly Field[] _gameState = new Field[NumberOfRows * NumberOfColumns]; - private readonly (ulong UserId, string Username)?[] _players = new(ulong, string)?[2]; - - public ImmutableArray GameState => _gameState.ToImmutableArray(); - public ImmutableArray<(ulong UserId, string Username)?> Players => _players.ToImmutableArray(); - - public (ulong UserId, string Username) CurrentPlayer => CurrentPhase == Phase.P1Move - ? _players[0].Value - : _players[1].Value; - - public (ulong UserId, string Username) OtherPlayer => CurrentPhase == Phase.P2Move - ? _players[0].Value - : _players[1].Value; - - //public event Func OnGameStarted; - public event Func OnGameStateUpdated; - public event Func OnGameFailedToStart; - public event Func OnGameEnded; - - private readonly SemaphoreSlim _locker = new SemaphoreSlim(1, 1); - private readonly Options _options; - private readonly ICurrencyService _cs; - private readonly NadekoRandom _rng; - - private Timer _playerTimeoutTimer; - - /* [ ][ ][ ][ ][ ][ ] - * [ ][ ][ ][ ][ ][ ] - * [ ][ ][ ][ ][ ][ ] - * [ ][ ][ ][ ][ ][ ] - * [ ][ ][ ][ ][ ][ ] - * [ ][ ][ ][ ][ ][ ] - * [ ][ ][ ][ ][ ][ ] - */ - - public Connect4Game(ulong userId, string userName, Options options, ICurrencyService cs) - { - _players[0] = (userId, userName); - _options = options; - _cs = cs; - - _rng = new NadekoRandom(); - for (int i = 0; i < NumberOfColumns * NumberOfRows; i++) + await Task.Delay(15000).ConfigureAwait(false); + await _locker.WaitAsync().ConfigureAwait(false); + try { - _gameState[i] = Field.Empty; + if (_players[1] is null) + { + var __ = OnGameFailedToStart?.Invoke(this); + CurrentPhase = Phase.Ended; + await _cs.AddAsync(_players[0].Value.UserId, "Connect4-refund", _options.Bet, true).ConfigureAwait(false); + return; + } } - } + finally { _locker.Release(); } + }); + } - public void Initialize() + public async Task Join(ulong userId, string userName, int bet) + { + await _locker.WaitAsync().ConfigureAwait(false); + try { - if (CurrentPhase != Phase.Joining) - return; - var _ = Task.Run(async () => + if (CurrentPhase != Phase.Joining) //can't join if its not a joining phase + return false; + + if (_players[0].Value.UserId == userId) // same user can't join own game + return false; + + if (bet != _options.Bet) // can't join if bet amount is not the same + return false; + + if (!await _cs.RemoveAsync(userId, "Connect4-bet", bet, true).ConfigureAwait(false)) // user doesn't have enough money to gamble + return false; + + if (_rng.Next(0, 2) == 0) //rolling from 0-1, if number is 0, join as first player + { + _players[1] = _players[0]; + _players[0] = (userId, userName); + } + else //else join as a second player + _players[1] = (userId, userName); + + CurrentPhase = Phase.P1Move; //start the game + _playerTimeoutTimer = new Timer(async state => { - await Task.Delay(15000).ConfigureAwait(false); await _locker.WaitAsync().ConfigureAwait(false); try { - if (_players[1] is null) - { - var __ = OnGameFailedToStart?.Invoke(this); - CurrentPhase = Phase.Ended; - await _cs.AddAsync(_players[0].Value.UserId, "Connect4-refund", _options.Bet, true).ConfigureAwait(false); - return; - } + EndGame(Result.OtherPlayerWon, OtherPlayer.UserId); } finally { _locker.Release(); } - }); + }, null, TimeSpan.FromSeconds(_options.TurnTimer), TimeSpan.FromSeconds(_options.TurnTimer)); + var __ = OnGameStateUpdated?.Invoke(this); + + return true; } + finally { _locker.Release(); } + } - public async Task Join(ulong userId, string userName, int bet) + public async Task Input(ulong userId, int inputCol) + { + await _locker.WaitAsync().ConfigureAwait(false); + try { - await _locker.WaitAsync().ConfigureAwait(false); - try - { - if (CurrentPhase != Phase.Joining) //can't join if its not a joining phase - return false; + inputCol -= 1; + if (CurrentPhase == Phase.Ended || CurrentPhase == Phase.Joining) + return false; - if (_players[0].Value.UserId == userId) // same user can't join own game - return false; + if (!((_players[0].Value.UserId == userId && CurrentPhase == Phase.P1Move) + || (_players[1].Value.UserId == userId && CurrentPhase == Phase.P2Move))) + return false; - if (bet != _options.Bet) // can't join if bet amount is not the same - return false; + if (inputCol < 0 || inputCol > NumberOfColumns) //invalid input + return false; - if (!await _cs.RemoveAsync(userId, "Connect4-bet", bet, true).ConfigureAwait(false)) // user doesn't have enough money to gamble - return false; + if (IsColumnFull(inputCol)) //can't play there event? + return false; - if (_rng.Next(0, 2) == 0) //rolling from 0-1, if number is 0, join as first player - { - _players[1] = _players[0]; - _players[0] = (userId, userName); - } - else //else join as a second player - _players[1] = (userId, userName); - - CurrentPhase = Phase.P1Move; //start the game - _playerTimeoutTimer = new Timer(async state => - { - await _locker.WaitAsync().ConfigureAwait(false); - try - { - EndGame(Result.OtherPlayerWon, OtherPlayer.UserId); - } - finally { _locker.Release(); } - }, null, TimeSpan.FromSeconds(_options.TurnTimer), TimeSpan.FromSeconds(_options.TurnTimer)); - var __ = OnGameStateUpdated?.Invoke(this); - - return true; - } - finally { _locker.Release(); } - } - - public async Task Input(ulong userId, int inputCol) - { - await _locker.WaitAsync().ConfigureAwait(false); - try - { - inputCol -= 1; - if (CurrentPhase == Phase.Ended || CurrentPhase == Phase.Joining) - return false; - - if (!((_players[0].Value.UserId == userId && CurrentPhase == Phase.P1Move) - || (_players[1].Value.UserId == userId && CurrentPhase == Phase.P2Move))) - return false; - - if (inputCol < 0 || inputCol > NumberOfColumns) //invalid input - return false; - - if (IsColumnFull(inputCol)) //can't play there event? - return false; - - var start = NumberOfRows * inputCol; - for (int i = start; i < start + NumberOfRows; i++) - { - if (_gameState[i] == Field.Empty) - { - _gameState[i] = GetPlayerPiece(userId); - break; - } - } - - //check winnning condition - // ok, i'll go from [0-2] in rows (and through all columns) and check upward if 4 are connected - - for (int i = 0; i < NumberOfRows - 3; i++) - { - if (CurrentPhase == Phase.Ended) - break; - - for (int j = 0; j < NumberOfColumns; j++) - { - if (CurrentPhase == Phase.Ended) - break; - - var first = _gameState[i + j * NumberOfRows]; - if (first != Field.Empty) - { - for (int k = 1; k < 4; k++) - { - var next = _gameState[i + k + j * NumberOfRows]; - if (next == first) - { - if (k == 3) - EndGame(Result.CurrentPlayerWon, CurrentPlayer.UserId); - else - continue; - } - else break; - } - } - } - } - - // i'll go [0-1] in columns (and through all rows) and check to the right if 4 are connected - for (int i = 0; i < NumberOfColumns - 3; i++) - { - if (CurrentPhase == Phase.Ended) - break; - - for (int j = 0; j < NumberOfRows; j++) - { - if (CurrentPhase == Phase.Ended) - break; - - var first = _gameState[j + i * NumberOfRows]; - if (first != Field.Empty) - { - for (int k = 1; k < 4; k++) - { - var next = _gameState[j + (i + k) * NumberOfRows]; - if (next == first) - if (k == 3) - EndGame(Result.CurrentPlayerWon, CurrentPlayer.UserId); - else - continue; - else break; - } - } - } - } - - //need to check diagonal now - for (int col = 0; col < NumberOfColumns; col++) - { - if (CurrentPhase == Phase.Ended) - break; - - for (int row = 0; row < NumberOfRows; row++) - { - if (CurrentPhase == Phase.Ended) - break; - - var first = _gameState[row + col * NumberOfRows]; - - if (first != Field.Empty) - { - var same = 1; - - //top left - for (int i = 1; i < 4; i++) - { - //while going top left, rows are increasing, columns are decreasing - var curRow = row + i; - var curCol = col - i; - - //check if current values are in range - if (curRow >= NumberOfRows || curRow < 0) - break; - if (curCol < 0 || curCol >= NumberOfColumns) - break; - - var cur = _gameState[curRow + curCol * NumberOfRows]; - if (cur == first) - same++; - else break; - } - - if (same == 4) - { - EndGame(Result.CurrentPlayerWon, CurrentPlayer.UserId); - break; - } - - same = 1; - - //top right - for (int i = 1; i < 4; i++) - { - //while going top right, rows are increasing, columns are increasing - var curRow = row + i; - var curCol = col + i; - - //check if current values are in range - if (curRow >= NumberOfRows || curRow < 0) - break; - if (curCol < 0 || curCol >= NumberOfColumns) - break; - - var cur = _gameState[curRow + curCol * NumberOfRows]; - if (cur == first) - same++; - else break; - } - - if (same == 4) - { - EndGame(Result.CurrentPlayerWon, CurrentPlayer.UserId); - break; - } - } - } - } - - //check draw? if it's even possible - if (_gameState.All(x => x != Field.Empty)) - { - EndGame(Result.Draw, null); - } - - if (CurrentPhase != Phase.Ended) - { - if (CurrentPhase == Phase.P1Move) - CurrentPhase = Phase.P2Move; - else - CurrentPhase = Phase.P1Move; - - ResetTimer(); - } - var _ = OnGameStateUpdated?.Invoke(this); - return true; - } - finally { _locker.Release(); } - } - - private void ResetTimer() - { - _playerTimeoutTimer.Change(TimeSpan.FromSeconds(_options.TurnTimer), TimeSpan.FromSeconds(_options.TurnTimer)); - } - - private void EndGame(Result result, ulong? winId) - { - if (CurrentPhase == Phase.Ended) - return; - var _ = OnGameEnded?.Invoke(this, result); - CurrentPhase = Phase.Ended; - - if (result == Result.Draw) - { - _cs.AddAsync(CurrentPlayer.UserId, "Connect4-draw", this._options.Bet, true); - _cs.AddAsync(OtherPlayer.UserId, "Connect4-draw", this._options.Bet, true); - return; - } - if (winId != null) - _cs.AddAsync(winId.Value, "Connnect4-win", (long)(this._options.Bet * 1.98), true); - } - - private Field GetPlayerPiece(ulong userId) => _players[0].Value.UserId == userId - ? Field.P1 - : Field.P2; - - //column is full if there are no empty fields - private bool IsColumnFull(int column) - { - var start = NumberOfRows * column; + var start = NumberOfRows * inputCol; for (int i = start; i < start + NumberOfRows; i++) { if (_gameState[i] == Field.Empty) - return false; + { + _gameState[i] = GetPlayerPiece(userId); + break; + } } + + //check winnning condition + // ok, i'll go from [0-2] in rows (and through all columns) and check upward if 4 are connected + + for (int i = 0; i < NumberOfRows - 3; i++) + { + if (CurrentPhase == Phase.Ended) + break; + + for (int j = 0; j < NumberOfColumns; j++) + { + if (CurrentPhase == Phase.Ended) + break; + + var first = _gameState[i + j * NumberOfRows]; + if (first != Field.Empty) + { + for (int k = 1; k < 4; k++) + { + var next = _gameState[i + k + j * NumberOfRows]; + if (next == first) + { + if (k == 3) + EndGame(Result.CurrentPlayerWon, CurrentPlayer.UserId); + else + continue; + } + else break; + } + } + } + } + + // i'll go [0-1] in columns (and through all rows) and check to the right if 4 are connected + for (int i = 0; i < NumberOfColumns - 3; i++) + { + if (CurrentPhase == Phase.Ended) + break; + + for (int j = 0; j < NumberOfRows; j++) + { + if (CurrentPhase == Phase.Ended) + break; + + var first = _gameState[j + i * NumberOfRows]; + if (first != Field.Empty) + { + for (int k = 1; k < 4; k++) + { + var next = _gameState[j + (i + k) * NumberOfRows]; + if (next == first) + if (k == 3) + EndGame(Result.CurrentPlayerWon, CurrentPlayer.UserId); + else + continue; + else break; + } + } + } + } + + //need to check diagonal now + for (int col = 0; col < NumberOfColumns; col++) + { + if (CurrentPhase == Phase.Ended) + break; + + for (int row = 0; row < NumberOfRows; row++) + { + if (CurrentPhase == Phase.Ended) + break; + + var first = _gameState[row + col * NumberOfRows]; + + if (first != Field.Empty) + { + var same = 1; + + //top left + for (int i = 1; i < 4; i++) + { + //while going top left, rows are increasing, columns are decreasing + var curRow = row + i; + var curCol = col - i; + + //check if current values are in range + if (curRow >= NumberOfRows || curRow < 0) + break; + if (curCol < 0 || curCol >= NumberOfColumns) + break; + + var cur = _gameState[curRow + curCol * NumberOfRows]; + if (cur == first) + same++; + else break; + } + + if (same == 4) + { + EndGame(Result.CurrentPlayerWon, CurrentPlayer.UserId); + break; + } + + same = 1; + + //top right + for (int i = 1; i < 4; i++) + { + //while going top right, rows are increasing, columns are increasing + var curRow = row + i; + var curCol = col + i; + + //check if current values are in range + if (curRow >= NumberOfRows || curRow < 0) + break; + if (curCol < 0 || curCol >= NumberOfColumns) + break; + + var cur = _gameState[curRow + curCol * NumberOfRows]; + if (cur == first) + same++; + else break; + } + + if (same == 4) + { + EndGame(Result.CurrentPlayerWon, CurrentPlayer.UserId); + break; + } + } + } + } + + //check draw? if it's even possible + if (_gameState.All(x => x != Field.Empty)) + { + EndGame(Result.Draw, null); + } + + if (CurrentPhase != Phase.Ended) + { + if (CurrentPhase == Phase.P1Move) + CurrentPhase = Phase.P2Move; + else + CurrentPhase = Phase.P1Move; + + ResetTimer(); + } + var _ = OnGameStateUpdated?.Invoke(this); return true; } - - public void Dispose() - { - OnGameFailedToStart = null; - OnGameStateUpdated = null; - OnGameEnded = null; - _playerTimeoutTimer?.Change(Timeout.Infinite, Timeout.Infinite); - } - - - public class Options : INadekoCommandOptions - { - public void NormalizeOptions() - { - if (TurnTimer < 5 || TurnTimer > 60) - TurnTimer = 15; - - if (Bet < 0) - Bet = 0; - } - - [Option('t', "turn-timer", Required = false, Default = 15, HelpText = "Turn time in seconds. It has to be between 5 and 60. Default 15.")] - public int TurnTimer { get; set; } = 15; - [Option('b', "bet", Required = false, Default = 0, HelpText = "Amount you bet. Default 0.")] - public int Bet { get; set; } = 0; - } + finally { _locker.Release(); } } -} + + private void ResetTimer() + { + _playerTimeoutTimer.Change(TimeSpan.FromSeconds(_options.TurnTimer), TimeSpan.FromSeconds(_options.TurnTimer)); + } + + private void EndGame(Result result, ulong? winId) + { + if (CurrentPhase == Phase.Ended) + return; + var _ = OnGameEnded?.Invoke(this, result); + CurrentPhase = Phase.Ended; + + if (result == Result.Draw) + { + _cs.AddAsync(CurrentPlayer.UserId, "Connect4-draw", this._options.Bet, true); + _cs.AddAsync(OtherPlayer.UserId, "Connect4-draw", this._options.Bet, true); + return; + } + if (winId != null) + _cs.AddAsync(winId.Value, "Connnect4-win", (long)(this._options.Bet * 1.98), true); + } + + private Field GetPlayerPiece(ulong userId) => _players[0].Value.UserId == userId + ? Field.P1 + : Field.P2; + + //column is full if there are no empty fields + private bool IsColumnFull(int column) + { + var start = NumberOfRows * column; + for (int i = start; i < start + NumberOfRows; i++) + { + if (_gameState[i] == Field.Empty) + return false; + } + return true; + } + + public void Dispose() + { + OnGameFailedToStart = null; + OnGameStateUpdated = null; + OnGameEnded = null; + _playerTimeoutTimer?.Change(Timeout.Infinite, Timeout.Infinite); + } + + + public class Options : INadekoCommandOptions + { + public void NormalizeOptions() + { + if (TurnTimer < 5 || TurnTimer > 60) + TurnTimer = 15; + + if (Bet < 0) + Bet = 0; + } + + [Option('t', "turn-timer", Required = false, Default = 15, HelpText = "Turn time in seconds. It has to be between 5 and 60. Default 15.")] + public int TurnTimer { get; set; } = 15; + [Option('b', "bet", Required = false, Default = 0, HelpText = "Amount you bet. Default 0.")] + public int Bet { get; set; } = 0; + } +} \ No newline at end of file diff --git a/src/NadekoBot/Modules/Gambling/Connect4Commands.cs b/src/NadekoBot/Modules/Gambling/Connect4Commands.cs index c4ee3cc1f..a0daa78f9 100644 --- a/src/NadekoBot/Modules/Gambling/Connect4Commands.cs +++ b/src/NadekoBot/Modules/Gambling/Connect4Commands.cs @@ -6,204 +6,202 @@ using NadekoBot.Modules.Gambling.Common; using NadekoBot.Extensions; using NadekoBot.Modules.Gambling.Common.Connect4; using NadekoBot.Modules.Gambling.Services; -using System.Linq; using System.Text; using System.Threading.Tasks; using NadekoBot.Common; using NadekoBot.Services; -namespace NadekoBot.Modules.Gambling +namespace NadekoBot.Modules.Gambling; + +public partial class Gambling { - public partial class Gambling + [Group] + public class Connect4Commands : GamblingSubmodule { - [Group] - public class Connect4Commands : GamblingSubmodule + private readonly DiscordSocketClient _client; + private readonly ICurrencyService _cs; + private static readonly string[] numbers = new string[] { ":one:", ":two:", ":three:", ":four:", ":five:", ":six:", ":seven:", ":eight:" }; + + public Connect4Commands(DiscordSocketClient client, ICurrencyService cs, GamblingConfigService gamb) + : base(gamb) { - private readonly DiscordSocketClient _client; - private readonly ICurrencyService _cs; - private static readonly string[] numbers = new string[] { ":one:", ":two:", ":three:", ":four:", ":five:", ":six:", ":seven:", ":eight:" }; + _client = client; + _cs = cs; + } - public Connect4Commands(DiscordSocketClient client, ICurrencyService cs, GamblingConfigService gamb) - : base(gamb) + [NadekoCommand, Aliases] + [RequireContext(ContextType.Guild)] + [NadekoOptionsAttribute(typeof(Connect4Game.Options))] + public async Task Connect4(params string[] args) + { + var (options, _) = OptionsParser.ParseFrom(new Connect4Game.Options(), args); + if (!await CheckBetOptional(options.Bet).ConfigureAwait(false)) + return; + + var newGame = new Connect4Game(ctx.User.Id, ctx.User.ToString(), options, _cs); + Connect4Game game; + if ((game = _service.Connect4Games.GetOrAdd(ctx.Channel.Id, newGame)) != newGame) { - _client = client; - _cs = cs; + if (game.CurrentPhase != Connect4Game.Phase.Joining) + return; + + newGame.Dispose(); + //means game already exists, try to join + var joined = await game.Join(ctx.User.Id, ctx.User.ToString(), options.Bet).ConfigureAwait(false); + return; } - [NadekoCommand, Aliases] - [RequireContext(ContextType.Guild)] - [NadekoOptionsAttribute(typeof(Connect4Game.Options))] - public async Task Connect4(params string[] args) + if (options.Bet > 0) { - var (options, _) = OptionsParser.ParseFrom(new Connect4Game.Options(), args); - if (!await CheckBetOptional(options.Bet).ConfigureAwait(false)) - return; - - var newGame = new Connect4Game(ctx.User.Id, ctx.User.ToString(), options, _cs); - Connect4Game game; - if ((game = _service.Connect4Games.GetOrAdd(ctx.Channel.Id, newGame)) != newGame) + if (!await _cs.RemoveAsync(ctx.User.Id, "Connect4-bet", options.Bet, true).ConfigureAwait(false)) { - if (game.CurrentPhase != Connect4Game.Phase.Joining) - return; - - newGame.Dispose(); - //means game already exists, try to join - var joined = await game.Join(ctx.User.Id, ctx.User.ToString(), options.Bet).ConfigureAwait(false); + await ReplyErrorLocalizedAsync(strs.not_enough(CurrencySign)); + _service.Connect4Games.TryRemove(ctx.Channel.Id, out _); + game.Dispose(); return; } + } - if (options.Bet > 0) - { - if (!await _cs.RemoveAsync(ctx.User.Id, "Connect4-bet", options.Bet, true).ConfigureAwait(false)) - { - await ReplyErrorLocalizedAsync(strs.not_enough(CurrencySign)); - _service.Connect4Games.TryRemove(ctx.Channel.Id, out _); - game.Dispose(); - return; - } - } + game.OnGameStateUpdated += Game_OnGameStateUpdated; + game.OnGameFailedToStart += Game_OnGameFailedToStart; + game.OnGameEnded += Game_OnGameEnded; + _client.MessageReceived += _client_MessageReceived; - game.OnGameStateUpdated += Game_OnGameStateUpdated; - game.OnGameFailedToStart += Game_OnGameFailedToStart; - game.OnGameEnded += Game_OnGameEnded; - _client.MessageReceived += _client_MessageReceived; + game.Initialize(); + if (options.Bet == 0) + { + await ReplyConfirmLocalizedAsync(strs.connect4_created).ConfigureAwait(false); + } + else + { + await ReplyErrorLocalizedAsync(strs.connect4_created_bet(options.Bet + CurrencySign)); + } - game.Initialize(); - if (options.Bet == 0) - { - await ReplyConfirmLocalizedAsync(strs.connect4_created).ConfigureAwait(false); - } - else - { - await ReplyErrorLocalizedAsync(strs.connect4_created_bet(options.Bet + CurrencySign)); - } - - Task _client_MessageReceived(SocketMessage arg) - { - if (ctx.Channel.Id != arg.Channel.Id) - return Task.CompletedTask; - - var _ = Task.Run(async () => - { - bool success = false; - if (int.TryParse(arg.Content, out var col)) - { - success = await game.Input(arg.Author.Id, col).ConfigureAwait(false); - } - - if (success) - try { await arg.DeleteAsync().ConfigureAwait(false); } catch { } - else - { - if (game.CurrentPhase == Connect4Game.Phase.Joining - || game.CurrentPhase == Connect4Game.Phase.Ended) - { - return; - } - RepostCounter++; - if (RepostCounter == 0) - try { msg = await ctx.Channel.SendMessageAsync("", embed: (Embed)msg.Embeds.First()).ConfigureAwait(false); } catch { } - } - }); + Task _client_MessageReceived(SocketMessage arg) + { + if (ctx.Channel.Id != arg.Channel.Id) return Task.CompletedTask; - } - Task Game_OnGameFailedToStart(Connect4Game arg) + var _ = Task.Run(async () => { - if (_service.Connect4Games.TryRemove(ctx.Channel.Id, out var toDispose)) + bool success = false; + if (int.TryParse(arg.Content, out var col)) { - _client.MessageReceived -= _client_MessageReceived; - toDispose.Dispose(); - } - return ErrorLocalizedAsync(strs.connect4_failed_to_start); - } - - Task Game_OnGameEnded(Connect4Game arg, Connect4Game.Result result) - { - if (_service.Connect4Games.TryRemove(ctx.Channel.Id, out var toDispose)) - { - _client.MessageReceived -= _client_MessageReceived; - toDispose.Dispose(); + success = await game.Input(arg.Author.Id, col).ConfigureAwait(false); } - string title; - if (result == Connect4Game.Result.CurrentPlayerWon) - { - title = GetText(strs.connect4_won(Format.Bold(arg.CurrentPlayer.Username), Format.Bold(arg.OtherPlayer.Username))); - } - else if (result == Connect4Game.Result.OtherPlayerWon) - { - title = GetText(strs.connect4_won(Format.Bold(arg.OtherPlayer.Username), Format.Bold(arg.CurrentPlayer.Username))); - } + if (success) + try { await arg.DeleteAsync().ConfigureAwait(false); } catch { } else - title = GetText(strs.connect4_draw); - - return msg.ModifyAsync(x => x.Embed = _eb.Create() - .WithTitle(title) - .WithDescription(GetGameStateText(game)) - .WithOkColor() - .Build()); - } - } - - private IUserMessage msg; - - private int _repostCounter = 0; - private int RepostCounter - { - get => _repostCounter; - set - { - if (value < 0 || value > 7) - _repostCounter = 0; - else _repostCounter = value; - } - } - - private async Task Game_OnGameStateUpdated(Connect4Game game) - { - var embed = _eb.Create() - .WithTitle($"{game.CurrentPlayer.Username} vs {game.OtherPlayer.Username}") - .WithDescription(GetGameStateText(game)) - .WithOkColor(); - - - if (msg is null) - msg = await ctx.Channel.EmbedAsync(embed).ConfigureAwait(false); - else - await msg.ModifyAsync(x => x.Embed = embed.Build()).ConfigureAwait(false); - } - - private string GetGameStateText(Connect4Game game) - { - var sb = new StringBuilder(); - - if (game.CurrentPhase == Connect4Game.Phase.P1Move || - game.CurrentPhase == Connect4Game.Phase.P2Move) - sb.AppendLine(GetText(strs.connect4_player_to_move(Format.Bold(game.CurrentPlayer.Username)))); - - for (int i = Connect4Game.NumberOfRows; i > 0; i--) - { - for (int j = 0; j < Connect4Game.NumberOfColumns; j++) { - var cur = game.GameState[i + (j * Connect4Game.NumberOfRows) - 1]; - - if (cur == Connect4Game.Field.Empty) - sb.Append("⚫"); //black circle - else if (cur == Connect4Game.Field.P1) - sb.Append("🔴"); //red circle - else - sb.Append("🔵"); //blue circle + if (game.CurrentPhase == Connect4Game.Phase.Joining + || game.CurrentPhase == Connect4Game.Phase.Ended) + { + return; + } + RepostCounter++; + if (RepostCounter == 0) + try { msg = await ctx.Channel.SendMessageAsync("", embed: (Embed)msg.Embeds.First()).ConfigureAwait(false); } catch { } } - sb.AppendLine(); + }); + return Task.CompletedTask; + } + + Task Game_OnGameFailedToStart(Connect4Game arg) + { + if (_service.Connect4Games.TryRemove(ctx.Channel.Id, out var toDispose)) + { + _client.MessageReceived -= _client_MessageReceived; + toDispose.Dispose(); + } + return ErrorLocalizedAsync(strs.connect4_failed_to_start); + } + + Task Game_OnGameEnded(Connect4Game arg, Connect4Game.Result result) + { + if (_service.Connect4Games.TryRemove(ctx.Channel.Id, out var toDispose)) + { + _client.MessageReceived -= _client_MessageReceived; + toDispose.Dispose(); } - for (int i = 0; i < Connect4Game.NumberOfColumns; i++) + string title; + if (result == Connect4Game.Result.CurrentPlayerWon) { - sb.Append(numbers[i]); + title = GetText(strs.connect4_won(Format.Bold(arg.CurrentPlayer.Username), Format.Bold(arg.OtherPlayer.Username))); } - return sb.ToString(); + else if (result == Connect4Game.Result.OtherPlayerWon) + { + title = GetText(strs.connect4_won(Format.Bold(arg.OtherPlayer.Username), Format.Bold(arg.CurrentPlayer.Username))); + } + else + title = GetText(strs.connect4_draw); + + return msg.ModifyAsync(x => x.Embed = _eb.Create() + .WithTitle(title) + .WithDescription(GetGameStateText(game)) + .WithOkColor() + .Build()); } } + + private IUserMessage msg; + + private int _repostCounter = 0; + private int RepostCounter + { + get => _repostCounter; + set + { + if (value < 0 || value > 7) + _repostCounter = 0; + else _repostCounter = value; + } + } + + private async Task Game_OnGameStateUpdated(Connect4Game game) + { + var embed = _eb.Create() + .WithTitle($"{game.CurrentPlayer.Username} vs {game.OtherPlayer.Username}") + .WithDescription(GetGameStateText(game)) + .WithOkColor(); + + + if (msg is null) + msg = await ctx.Channel.EmbedAsync(embed).ConfigureAwait(false); + else + await msg.ModifyAsync(x => x.Embed = embed.Build()).ConfigureAwait(false); + } + + private string GetGameStateText(Connect4Game game) + { + var sb = new StringBuilder(); + + if (game.CurrentPhase == Connect4Game.Phase.P1Move || + game.CurrentPhase == Connect4Game.Phase.P2Move) + sb.AppendLine(GetText(strs.connect4_player_to_move(Format.Bold(game.CurrentPlayer.Username)))); + + for (int i = Connect4Game.NumberOfRows; i > 0; i--) + { + for (int j = 0; j < Connect4Game.NumberOfColumns; j++) + { + var cur = game.GameState[i + (j * Connect4Game.NumberOfRows) - 1]; + + if (cur == Connect4Game.Field.Empty) + sb.Append("⚫"); //black circle + else if (cur == Connect4Game.Field.P1) + sb.Append("🔴"); //red circle + else + sb.Append("🔵"); //blue circle + } + sb.AppendLine(); + } + + for (int i = 0; i < Connect4Game.NumberOfColumns; i++) + { + sb.Append(numbers[i]); + } + return sb.ToString(); + } } -} +} \ No newline at end of file diff --git a/src/NadekoBot/Modules/Gambling/CurrencyEventsCommands.cs b/src/NadekoBot/Modules/Gambling/CurrencyEventsCommands.cs index 442451405..61f6892e6 100644 --- a/src/NadekoBot/Modules/Gambling/CurrencyEventsCommands.cs +++ b/src/NadekoBot/Modules/Gambling/CurrencyEventsCommands.cs @@ -5,80 +5,78 @@ using System.Threading.Tasks; using NadekoBot.Common.Attributes; using NadekoBot.Modules.Gambling.Services; using NadekoBot.Modules.Gambling.Common.Events; -using System; using NadekoBot.Common; using NadekoBot.Services.Database.Models; using NadekoBot.Modules.Gambling.Common; -namespace NadekoBot.Modules.Gambling -{ - public partial class Gambling - { - [Group] - public class CurrencyEventsCommands : GamblingSubmodule - { - public CurrencyEventsCommands(GamblingConfigService gamblingConf) : base(gamblingConf) - { - } +namespace NadekoBot.Modules.Gambling; - [NadekoCommand, Aliases] - [RequireContext(ContextType.Guild)] - [NadekoOptionsAttribute(typeof(EventOptions))] - [OwnerOnly] - public async Task EventStart(CurrencyEvent.Type ev, params string[] options) - { - var (opts, _) = OptionsParser.ParseFrom(new EventOptions(), options); - if (!await _service.TryCreateEventAsync(ctx.Guild.Id, +public partial class Gambling +{ + [Group] + public class CurrencyEventsCommands : GamblingSubmodule + { + public CurrencyEventsCommands(GamblingConfigService gamblingConf) : base(gamblingConf) + { + } + + [NadekoCommand, Aliases] + [RequireContext(ContextType.Guild)] + [NadekoOptionsAttribute(typeof(EventOptions))] + [OwnerOnly] + public async Task EventStart(CurrencyEvent.Type ev, params string[] options) + { + var (opts, _) = OptionsParser.ParseFrom(new EventOptions(), options); + if (!await _service.TryCreateEventAsync(ctx.Guild.Id, ctx.Channel.Id, ev, opts, GetEmbed)) - { - await ReplyErrorLocalizedAsync(strs.start_event_fail).ConfigureAwait(false); - } - } - - private IEmbedBuilder GetEmbed(CurrencyEvent.Type type, EventOptions opts, long currentPot) { - return type switch - { - CurrencyEvent.Type.Reaction => _eb.Create() - .WithOkColor() - .WithTitle(GetText(strs.event_title(type.ToString()))) - .WithDescription(GetReactionDescription(opts.Amount, currentPot)) - .WithFooter(GetText(strs.event_duration_footer(opts.Hours))), - CurrencyEvent.Type.GameStatus => _eb.Create() - .WithOkColor() - .WithTitle(GetText(strs.event_title(type.ToString()))) - .WithDescription(GetGameStatusDescription(opts.Amount, currentPot)) - .WithFooter(GetText(strs.event_duration_footer(opts.Hours))), - _ => throw new ArgumentOutOfRangeException(nameof(type)) - }; - } - - private string GetReactionDescription(long amount, long potSize) - { - var potSizeStr = Format.Bold(potSize == 0 - ? "∞" + CurrencySign - : potSize + CurrencySign); - - return GetText(strs.new_reaction_event( - CurrencySign, - Format.Bold(amount + CurrencySign), - potSizeStr)); - } - - private string GetGameStatusDescription(long amount, long potSize) - { - var potSizeStr = Format.Bold(potSize == 0 - ? "∞" + CurrencySign - : potSize + CurrencySign); - - return GetText(strs.new_gamestatus_event( - CurrencySign, - Format.Bold(amount + CurrencySign), - potSizeStr)); + await ReplyErrorLocalizedAsync(strs.start_event_fail).ConfigureAwait(false); } } + + private IEmbedBuilder GetEmbed(CurrencyEvent.Type type, EventOptions opts, long currentPot) + { + return type switch + { + CurrencyEvent.Type.Reaction => _eb.Create() + .WithOkColor() + .WithTitle(GetText(strs.event_title(type.ToString()))) + .WithDescription(GetReactionDescription(opts.Amount, currentPot)) + .WithFooter(GetText(strs.event_duration_footer(opts.Hours))), + CurrencyEvent.Type.GameStatus => _eb.Create() + .WithOkColor() + .WithTitle(GetText(strs.event_title(type.ToString()))) + .WithDescription(GetGameStatusDescription(opts.Amount, currentPot)) + .WithFooter(GetText(strs.event_duration_footer(opts.Hours))), + _ => throw new ArgumentOutOfRangeException(nameof(type)) + }; + } + + private string GetReactionDescription(long amount, long potSize) + { + var potSizeStr = Format.Bold(potSize == 0 + ? "∞" + CurrencySign + : potSize + CurrencySign); + + return GetText(strs.new_reaction_event( + CurrencySign, + Format.Bold(amount + CurrencySign), + potSizeStr)); + } + + private string GetGameStatusDescription(long amount, long potSize) + { + var potSizeStr = Format.Bold(potSize == 0 + ? "∞" + CurrencySign + : potSize + CurrencySign); + + return GetText(strs.new_gamestatus_event( + CurrencySign, + Format.Bold(amount + CurrencySign), + potSizeStr)); + } } -} +} \ No newline at end of file diff --git a/src/NadekoBot/Modules/Gambling/CurrencyRaffleCommands.cs b/src/NadekoBot/Modules/Gambling/CurrencyRaffleCommands.cs index 2812f6115..94ec03f2e 100644 --- a/src/NadekoBot/Modules/Gambling/CurrencyRaffleCommands.cs +++ b/src/NadekoBot/Modules/Gambling/CurrencyRaffleCommands.cs @@ -2,59 +2,56 @@ using NadekoBot.Modules.Gambling.Services; using System.Threading.Tasks; using Discord; -using NadekoBot.Extensions; -using System.Linq; using Discord.Commands; using NadekoBot.Modules.Gambling.Common; using NadekoBot.Common; -namespace NadekoBot.Modules.Gambling +namespace NadekoBot.Modules.Gambling; + +public partial class Gambling { - public partial class Gambling + public class CurrencyRaffleCommands : GamblingSubmodule { - public class CurrencyRaffleCommands : GamblingSubmodule + public enum Mixed { Mixed } + + public CurrencyRaffleCommands(GamblingConfigService gamblingConfService) : base(gamblingConfService) { - public enum Mixed { Mixed } + } - public CurrencyRaffleCommands(GamblingConfigService gamblingConfService) : base(gamblingConfService) + [NadekoCommand, Aliases] + [RequireContext(ContextType.Guild)] + [Priority(0)] + public Task RaffleCur(Mixed _, ShmartNumber amount) => + RaffleCur(amount, true); + + [NadekoCommand, Aliases] + [RequireContext(ContextType.Guild)] + [Priority(1)] + public async Task RaffleCur(ShmartNumber amount, bool mixed = false) + { + if (!await CheckBetMandatory(amount).ConfigureAwait(false)) + return; + async Task OnEnded(IUser arg, long won) { + await SendConfirmAsync(GetText(strs.rafflecur_ended(CurrencyName, Format.Bold(arg.ToString()), won + CurrencySign))); } - - [NadekoCommand, Aliases] - [RequireContext(ContextType.Guild)] - [Priority(0)] - public Task RaffleCur(Mixed _, ShmartNumber amount) => - RaffleCur(amount, true); - - [NadekoCommand, Aliases] - [RequireContext(ContextType.Guild)] - [Priority(1)] - public async Task RaffleCur(ShmartNumber amount, bool mixed = false) - { - if (!await CheckBetMandatory(amount).ConfigureAwait(false)) - return; - async Task OnEnded(IUser arg, long won) - { - await SendConfirmAsync(GetText(strs.rafflecur_ended(CurrencyName, Format.Bold(arg.ToString()), won + CurrencySign))); - } - var res = await _service.JoinOrCreateGame(ctx.Channel.Id, + var res = await _service.JoinOrCreateGame(ctx.Channel.Id, ctx.User, amount, mixed, OnEnded) - .ConfigureAwait(false); + .ConfigureAwait(false); - if (res.Item1 != null) - { - await SendConfirmAsync(GetText(strs.rafflecur(res.Item1.GameType.ToString())), - string.Join("\n", res.Item1.Users.Select(x => $"{x.DiscordUser} ({x.Amount})")), - footer: GetText(strs.rafflecur_joined(ctx.User.ToString()))); - } - else - { - if (res.Item2 == CurrencyRaffleService.JoinErrorType.AlreadyJoinedOrInvalidAmount) - await ReplyErrorLocalizedAsync(strs.rafflecur_already_joined).ConfigureAwait(false); - else if (res.Item2 == CurrencyRaffleService.JoinErrorType.NotEnoughCurrency) - await ReplyErrorLocalizedAsync(strs.not_enough(CurrencySign)); - } + if (res.Item1 != null) + { + await SendConfirmAsync(GetText(strs.rafflecur(res.Item1.GameType.ToString())), + string.Join("\n", res.Item1.Users.Select(x => $"{x.DiscordUser} ({x.Amount})")), + footer: GetText(strs.rafflecur_joined(ctx.User.ToString()))); + } + else + { + if (res.Item2 == CurrencyRaffleService.JoinErrorType.AlreadyJoinedOrInvalidAmount) + await ReplyErrorLocalizedAsync(strs.rafflecur_already_joined).ConfigureAwait(false); + else if (res.Item2 == CurrencyRaffleService.JoinErrorType.NotEnoughCurrency) + await ReplyErrorLocalizedAsync(strs.not_enough(CurrencySign)); } } } -} +} \ No newline at end of file diff --git a/src/NadekoBot/Modules/Gambling/DiceRollCommands.cs b/src/NadekoBot/Modules/Gambling/DiceRollCommands.cs index 7f5cf58b5..06a2722c2 100644 --- a/src/NadekoBot/Modules/Gambling/DiceRollCommands.cs +++ b/src/NadekoBot/Modules/Gambling/DiceRollCommands.cs @@ -5,230 +5,226 @@ using NadekoBot.Common.Attributes; using NadekoBot.Extensions; using SixLabors.ImageSharp; using SixLabors.ImageSharp.PixelFormats; -using System; -using System.Collections.Generic; -using System.Linq; using System.Text.RegularExpressions; using System.Threading.Tasks; using NadekoBot.Services; using Image = SixLabors.ImageSharp.Image; -namespace NadekoBot.Modules.Gambling +namespace NadekoBot.Modules.Gambling; + +public partial class Gambling { - public partial class Gambling + [Group] + public class DiceRollCommands : NadekoSubmodule { - [Group] - public class DiceRollCommands : NadekoSubmodule + private static readonly Regex dndRegex = new Regex(@"^(?\d+)d(?\d+)(?:\+(?\d+))?(?:\-(?\d+))?$", RegexOptions.Compiled); + private static readonly Regex fudgeRegex = new Regex(@"^(?\d+)d(?:F|f)$", RegexOptions.Compiled); + + private static readonly char[] _fateRolls = { '-', ' ', '+' }; + private readonly IImageCache _images; + + public DiceRollCommands(IDataCache data) { - private static readonly Regex dndRegex = new Regex(@"^(?\d+)d(?\d+)(?:\+(?\d+))?(?:\-(?\d+))?$", RegexOptions.Compiled); - private static readonly Regex fudgeRegex = new Regex(@"^(?\d+)d(?:F|f)$", RegexOptions.Compiled); + _images = data.LocalImages; + } - private static readonly char[] _fateRolls = { '-', ' ', '+' }; - private readonly IImageCache _images; + [NadekoCommand, Aliases] + public async Task Roll() + { + var rng = new NadekoRandom(); + var gen = rng.Next(1, 101); - public DiceRollCommands(IDataCache data) + var num1 = gen / 10; + var num2 = gen % 10; + + using (var img1 = GetDice(num1)) + using (var img2 = GetDice(num2)) + using (var img = new[] { img1, img2 }.Merge(out var format)) + using (var ms = img.ToStream(format)) { - _images = data.LocalImages; + await ctx.Channel.SendFileAsync(ms, + $"dice.{format.FileExtensions.First()}", + Format.Bold(ctx.User.ToString()) + " " + GetText(strs.dice_rolled(Format.Code(gen.ToString())))); + } + } + + [NadekoCommand, Aliases] + [Priority(1)] + public async Task Roll(int num) + { + await InternalRoll(num, true).ConfigureAwait(false); + } + + + [NadekoCommand, Aliases] + [Priority(1)] + public async Task Rolluo(int num = 1) + { + await InternalRoll(num, false).ConfigureAwait(false); + } + + [NadekoCommand, Aliases] + [Priority(0)] + public async Task Roll(string arg) + { + await InternallDndRoll(arg, true).ConfigureAwait(false); + } + + [NadekoCommand, Aliases] + [Priority(0)] + public async Task Rolluo(string arg) + { + await InternallDndRoll(arg, false).ConfigureAwait(false); + } + + private async Task InternalRoll(int num, bool ordered) + { + if (num < 1 || num > 30) + { + await ReplyErrorLocalizedAsync(strs.dice_invalid_number(1, 30)); + return; } - [NadekoCommand, Aliases] - public async Task Roll() + var rng = new NadekoRandom(); + + var dice = new List>(num); + var values = new List(num); + for (var i = 0; i < num; i++) { - var rng = new NadekoRandom(); - var gen = rng.Next(1, 101); - - var num1 = gen / 10; - var num2 = gen % 10; - - using (var img1 = GetDice(num1)) - using (var img2 = GetDice(num2)) - using (var img = new[] { img1, img2 }.Merge(out var format)) - using (var ms = img.ToStream(format)) + var randomNumber = rng.Next(1, 7); + var toInsert = dice.Count; + if (ordered) { - await ctx.Channel.SendFileAsync(ms, - $"dice.{format.FileExtensions.First()}", - Format.Bold(ctx.User.ToString()) + " " + GetText(strs.dice_rolled(Format.Code(gen.ToString())))); - } - } - - [NadekoCommand, Aliases] - [Priority(1)] - public async Task Roll(int num) - { - await InternalRoll(num, true).ConfigureAwait(false); - } - - - [NadekoCommand, Aliases] - [Priority(1)] - public async Task Rolluo(int num = 1) - { - await InternalRoll(num, false).ConfigureAwait(false); - } - - [NadekoCommand, Aliases] - [Priority(0)] - public async Task Roll(string arg) - { - await InternallDndRoll(arg, true).ConfigureAwait(false); - } - - [NadekoCommand, Aliases] - [Priority(0)] - public async Task Rolluo(string arg) - { - await InternallDndRoll(arg, false).ConfigureAwait(false); - } - - private async Task InternalRoll(int num, bool ordered) - { - if (num < 1 || num > 30) - { - await ReplyErrorLocalizedAsync(strs.dice_invalid_number(1, 30)); - return; - } - - var rng = new NadekoRandom(); - - var dice = new List>(num); - var values = new List(num); - for (var i = 0; i < num; i++) - { - var randomNumber = rng.Next(1, 7); - var toInsert = dice.Count; - if (ordered) - { - if (randomNumber == 6 || dice.Count == 0) - toInsert = 0; - else if (randomNumber != 1) - for (var j = 0; j < dice.Count; j++) - { - if (values[j] < randomNumber) - { - toInsert = j; - break; - } - } - } - else - { - toInsert = dice.Count; - } - dice.Insert(toInsert, GetDice(randomNumber)); - values.Insert(toInsert, randomNumber); - } - - using (var bitmap = dice.Merge(out var format)) - using (var ms = bitmap.ToStream(format)) - { - foreach (var d in dice) - { - d.Dispose(); - } - - await ctx.Channel.SendFileAsync(ms, $"dice.{format.FileExtensions.First()}", - Format.Bold(ctx.User.ToString()) + " " + - GetText(strs.dice_rolled_num(Format.Bold(values.Count.ToString()))) + - " " + GetText(strs.total_average( - Format.Bold(values.Sum().ToString()), - Format.Bold((values.Sum() / (1.0f * values.Count)).ToString("N2"))))); - } - } - - private async Task InternallDndRoll(string arg, bool ordered) - { - Match match; - if ((match = fudgeRegex.Match(arg)).Length != 0 && - int.TryParse(match.Groups["n1"].ToString(), out int n1) && - n1 > 0 && n1 < 500) - { - var rng = new NadekoRandom(); - - var rolls = new List(); - - for (int i = 0; i < n1; i++) - { - rolls.Add(_fateRolls[rng.Next(0, _fateRolls.Length)]); - } - var embed = _eb.Create() - .WithOkColor() - .WithDescription(ctx.User.Mention + " " + GetText(strs.dice_rolled_num(Format.Bold(n1.ToString())))) - .AddField(Format.Bold("Result"), string.Join(" ", rolls.Select(c => Format.Code($"[{c}]")))); - - await ctx.Channel.EmbedAsync(embed).ConfigureAwait(false); - } - else if ((match = dndRegex.Match(arg)).Length != 0) - { - var rng = new NadekoRandom(); - if (int.TryParse(match.Groups["n1"].ToString(), out n1) && - int.TryParse(match.Groups["n2"].ToString(), out int n2) && - n1 <= 50 && n2 <= 100000 && n1 > 0 && n2 > 0) - { - if (!int.TryParse(match.Groups["add"].Value, out int add)) - add = 0; - if (!int.TryParse(match.Groups["sub"].Value, out int sub)) - sub = 0; - - var arr = new int[n1]; - for (int i = 0; i < n1; i++) + if (randomNumber == 6 || dice.Count == 0) + toInsert = 0; + else if (randomNumber != 1) + for (var j = 0; j < dice.Count; j++) { - arr[i] = rng.Next(1, n2 + 1); + if (values[j] < randomNumber) + { + toInsert = j; + break; + } } - - var sum = arr.Sum(); - var embed = _eb.Create().WithOkColor() - .WithDescription(ctx.User.Mention + " " + GetText(strs.dice_rolled_num(n1 + $"`1 - {n2}`"))) - .AddField(Format.Bold("Rolls"), string.Join(" ", - (ordered ? arr.OrderBy(x => x).AsEnumerable() : arr).Select(x => - Format.Code(x.ToString())))) - .AddField(Format.Bold("Sum"), - sum + " + " + add + " - " + sub + " = " + (sum + add - sub)); - await ctx.Channel.EmbedAsync(embed).ConfigureAwait(false); - } - } - } - - [NadekoCommand, Aliases] - public async Task NRoll([Leftover] string range) - { - int rolled; - if (range.Contains("-")) - { - var arr = range.Split('-') - .Take(2) - .Select(int.Parse) - .ToArray(); - if (arr[0] > arr[1]) - { - await ReplyErrorLocalizedAsync(strs.second_larger_than_first).ConfigureAwait(false); - return; - } - rolled = new NadekoRandom().Next(arr[0], arr[1] + 1); } else { - rolled = new NadekoRandom().Next(0, int.Parse(range) + 1); + toInsert = dice.Count; } - - await ReplyConfirmLocalizedAsync(strs.dice_rolled(Format.Bold(rolled.ToString()))).ConfigureAwait(false); + dice.Insert(toInsert, GetDice(randomNumber)); + values.Insert(toInsert, randomNumber); } - private Image GetDice(int num) + using (var bitmap = dice.Merge(out var format)) + using (var ms = bitmap.ToStream(format)) { - if (num < 0 || num > 10) - throw new ArgumentOutOfRangeException(nameof(num)); - - if (num == 10) + foreach (var d in dice) { - var images = _images.Dice; - using (var imgOne = Image.Load(images[1])) - using (var imgZero = Image.Load(images[0])) - { - return new[] { imgOne, imgZero }.Merge(); - } + d.Dispose(); } - return Image.Load(_images.Dice[num]); + + await ctx.Channel.SendFileAsync(ms, $"dice.{format.FileExtensions.First()}", + Format.Bold(ctx.User.ToString()) + " " + + GetText(strs.dice_rolled_num(Format.Bold(values.Count.ToString()))) + + " " + GetText(strs.total_average( + Format.Bold(values.Sum().ToString()), + Format.Bold((values.Sum() / (1.0f * values.Count)).ToString("N2"))))); } } + + private async Task InternallDndRoll(string arg, bool ordered) + { + Match match; + if ((match = fudgeRegex.Match(arg)).Length != 0 && + int.TryParse(match.Groups["n1"].ToString(), out int n1) && + n1 > 0 && n1 < 500) + { + var rng = new NadekoRandom(); + + var rolls = new List(); + + for (int i = 0; i < n1; i++) + { + rolls.Add(_fateRolls[rng.Next(0, _fateRolls.Length)]); + } + var embed = _eb.Create() + .WithOkColor() + .WithDescription(ctx.User.Mention + " " + GetText(strs.dice_rolled_num(Format.Bold(n1.ToString())))) + .AddField(Format.Bold("Result"), string.Join(" ", rolls.Select(c => Format.Code($"[{c}]")))); + + await ctx.Channel.EmbedAsync(embed).ConfigureAwait(false); + } + else if ((match = dndRegex.Match(arg)).Length != 0) + { + var rng = new NadekoRandom(); + if (int.TryParse(match.Groups["n1"].ToString(), out n1) && + int.TryParse(match.Groups["n2"].ToString(), out int n2) && + n1 <= 50 && n2 <= 100000 && n1 > 0 && n2 > 0) + { + if (!int.TryParse(match.Groups["add"].Value, out int add)) + add = 0; + if (!int.TryParse(match.Groups["sub"].Value, out int sub)) + sub = 0; + + var arr = new int[n1]; + for (int i = 0; i < n1; i++) + { + arr[i] = rng.Next(1, n2 + 1); + } + + var sum = arr.Sum(); + var embed = _eb.Create().WithOkColor() + .WithDescription(ctx.User.Mention + " " + GetText(strs.dice_rolled_num(n1 + $"`1 - {n2}`"))) + .AddField(Format.Bold("Rolls"), string.Join(" ", + (ordered ? arr.OrderBy(x => x).AsEnumerable() : arr).Select(x => + Format.Code(x.ToString())))) + .AddField(Format.Bold("Sum"), + sum + " + " + add + " - " + sub + " = " + (sum + add - sub)); + await ctx.Channel.EmbedAsync(embed).ConfigureAwait(false); + } + } + } + + [NadekoCommand, Aliases] + public async Task NRoll([Leftover] string range) + { + int rolled; + if (range.Contains("-")) + { + var arr = range.Split('-') + .Take(2) + .Select(int.Parse) + .ToArray(); + if (arr[0] > arr[1]) + { + await ReplyErrorLocalizedAsync(strs.second_larger_than_first).ConfigureAwait(false); + return; + } + rolled = new NadekoRandom().Next(arr[0], arr[1] + 1); + } + else + { + rolled = new NadekoRandom().Next(0, int.Parse(range) + 1); + } + + await ReplyConfirmLocalizedAsync(strs.dice_rolled(Format.Bold(rolled.ToString()))).ConfigureAwait(false); + } + + private Image GetDice(int num) + { + if (num < 0 || num > 10) + throw new ArgumentOutOfRangeException(nameof(num)); + + if (num == 10) + { + var images = _images.Dice; + using (var imgOne = Image.Load(images[1])) + using (var imgZero = Image.Load(images[0])) + { + return new[] { imgOne, imgZero }.Merge(); + } + } + return Image.Load(_images.Dice[num]); + } } } \ No newline at end of file diff --git a/src/NadekoBot/Modules/Gambling/DrawCommands.cs b/src/NadekoBot/Modules/Gambling/DrawCommands.cs index 91e311d20..a77fcb4e4 100644 --- a/src/NadekoBot/Modules/Gambling/DrawCommands.cs +++ b/src/NadekoBot/Modules/Gambling/DrawCommands.cs @@ -1,9 +1,7 @@ using Discord; using Discord.Commands; using NadekoBot.Extensions; -using System; using System.Collections.Concurrent; -using System.Collections.Generic; using System.IO; using System.Threading.Tasks; using NadekoBot.Common.Attributes; @@ -13,112 +11,111 @@ using Image = SixLabors.ImageSharp.Image; using SixLabors.ImageSharp; using SixLabors.ImageSharp.PixelFormats; -namespace NadekoBot.Modules.Gambling +namespace NadekoBot.Modules.Gambling; + +public partial class Gambling { - public partial class Gambling + [Group] + public class DrawCommands : NadekoSubmodule { - [Group] - public class DrawCommands : NadekoSubmodule + private static readonly ConcurrentDictionary _allDecks = new ConcurrentDictionary(); + private readonly IImageCache _images; + + public DrawCommands(IDataCache data) { - private static readonly ConcurrentDictionary _allDecks = new ConcurrentDictionary(); - private readonly IImageCache _images; + _images = data.LocalImages; + } - public DrawCommands(IDataCache data) + private async Task<(Stream ImageStream, string ToSend)> InternalDraw(int num, ulong? guildId = null) + { + if (num < 1 || num > 10) + throw new ArgumentOutOfRangeException(nameof(num)); + + Deck cards = guildId is null ? new Deck() : _allDecks.GetOrAdd(ctx.Guild, (s) => new Deck()); + var images = new List>(); + var cardObjects = new List(); + for (var i = 0; i < num; i++) { - _images = data.LocalImages; - } - - private async Task<(Stream ImageStream, string ToSend)> InternalDraw(int num, ulong? guildId = null) - { - if (num < 1 || num > 10) - throw new ArgumentOutOfRangeException(nameof(num)); - - Deck cards = guildId is null ? new Deck() : _allDecks.GetOrAdd(ctx.Guild, (s) => new Deck()); - var images = new List>(); - var cardObjects = new List(); - for (var i = 0; i < num; i++) + if (cards.CardPool.Count == 0 && i != 0) { - if (cards.CardPool.Count == 0 && i != 0) + try { - try - { - await ReplyErrorLocalizedAsync(strs.no_more_cards).ConfigureAwait(false); - } - catch - { - // ignored - } - break; + await ReplyErrorLocalizedAsync(strs.no_more_cards).ConfigureAwait(false); } - var currentCard = cards.Draw(); - cardObjects.Add(currentCard); - images.Add(Image.Load(_images.GetCard(currentCard.ToString().ToLowerInvariant().Replace(' ', '_')))); - } - using (var img = images.Merge()) - { - foreach (var i in images) + catch { - i.Dispose(); + // ignored } - - var toSend = $"{Format.Bold(ctx.User.ToString())}"; - if (cardObjects.Count == 5) - toSend += $" drew `{Deck.GetHandValue(cardObjects)}`"; - - if (guildId != null) - toSend += "\n" + GetText(strs.cards_left(Format.Bold(cards.CardPool.Count.ToString()))); - - return (img.ToStream(), toSend); + break; } + var currentCard = cards.Draw(); + cardObjects.Add(currentCard); + images.Add(Image.Load(_images.GetCard(currentCard.ToString().ToLowerInvariant().Replace(' ', '_')))); } - - [NadekoCommand, Aliases] - [RequireContext(ContextType.Guild)] - public async Task Draw(int num = 1) + using (var img = images.Merge()) { - if (num < 1) - num = 1; - if (num > 10) - num = 10; - - var (ImageStream, ToSend) = await InternalDraw(num, ctx.Guild.Id).ConfigureAwait(false); - using (ImageStream) + foreach (var i in images) { - await ctx.Channel.SendFileAsync(ImageStream, num + " cards.jpg", ToSend).ConfigureAwait(false); + i.Dispose(); } + + var toSend = $"{Format.Bold(ctx.User.ToString())}"; + if (cardObjects.Count == 5) + toSend += $" drew `{Deck.GetHandValue(cardObjects)}`"; + + if (guildId != null) + toSend += "\n" + GetText(strs.cards_left(Format.Bold(cards.CardPool.Count.ToString()))); + + return (img.ToStream(), toSend); } + } - [NadekoCommand, Aliases] - public async Task DrawNew(int num = 1) + [NadekoCommand, Aliases] + [RequireContext(ContextType.Guild)] + public async Task Draw(int num = 1) + { + if (num < 1) + num = 1; + if (num > 10) + num = 10; + + var (ImageStream, ToSend) = await InternalDraw(num, ctx.Guild.Id).ConfigureAwait(false); + using (ImageStream) { - if (num < 1) - num = 1; - if (num > 10) - num = 10; + await ctx.Channel.SendFileAsync(ImageStream, num + " cards.jpg", ToSend).ConfigureAwait(false); + } + } - var (ImageStream, ToSend) = await InternalDraw(num).ConfigureAwait(false); - using (ImageStream) + [NadekoCommand, Aliases] + public async Task DrawNew(int num = 1) + { + if (num < 1) + num = 1; + if (num > 10) + num = 10; + + var (ImageStream, ToSend) = await InternalDraw(num).ConfigureAwait(false); + using (ImageStream) + { + await ctx.Channel.SendFileAsync(ImageStream, num + " cards.jpg", ToSend).ConfigureAwait(false); + } + } + + [NadekoCommand, Aliases] + [RequireContext(ContextType.Guild)] + public async Task DeckShuffle() + { + //var channel = (ITextChannel)ctx.Channel; + + _allDecks.AddOrUpdate(ctx.Guild, + (g) => new Deck(), + (g, c) => { - await ctx.Channel.SendFileAsync(ImageStream, num + " cards.jpg", ToSend).ConfigureAwait(false); - } - } + c.Restart(); + return c; + }); - [NadekoCommand, Aliases] - [RequireContext(ContextType.Guild)] - public async Task DeckShuffle() - { - //var channel = (ITextChannel)ctx.Channel; - - _allDecks.AddOrUpdate(ctx.Guild, - (g) => new Deck(), - (g, c) => - { - c.Restart(); - return c; - }); - - await ReplyConfirmLocalizedAsync(strs.deck_reshuffled).ConfigureAwait(false); - } + await ReplyConfirmLocalizedAsync(strs.deck_reshuffled).ConfigureAwait(false); } } } \ No newline at end of file diff --git a/src/NadekoBot/Modules/Gambling/FlipCoinCommands.cs b/src/NadekoBot/Modules/Gambling/FlipCoinCommands.cs index 28b39912d..e78c46ada 100644 --- a/src/NadekoBot/Modules/Gambling/FlipCoinCommands.cs +++ b/src/NadekoBot/Modules/Gambling/FlipCoinCommands.cs @@ -7,124 +7,121 @@ using NadekoBot.Extensions; using NadekoBot.Modules.Gambling.Services; using SixLabors.ImageSharp; using SixLabors.ImageSharp.PixelFormats; -using System; -using System.Linq; using System.Threading.Tasks; using NadekoBot.Services; using Image = SixLabors.ImageSharp.Image; -namespace NadekoBot.Modules.Gambling +namespace NadekoBot.Modules.Gambling; + +public partial class Gambling { - public partial class Gambling + [Group] + public class FlipCoinCommands : GamblingSubmodule { - [Group] - public class FlipCoinCommands : GamblingSubmodule + private readonly IImageCache _images; + private readonly ICurrencyService _cs; + private static readonly NadekoRandom rng = new NadekoRandom(); + + public FlipCoinCommands(IDataCache data, ICurrencyService cs, GamblingConfigService gss) : base(gss) { - private readonly IImageCache _images; - private readonly ICurrencyService _cs; - private static readonly NadekoRandom rng = new NadekoRandom(); + _images = data.LocalImages; + _cs = cs; + } - public FlipCoinCommands(IDataCache data, ICurrencyService cs, GamblingConfigService gss) : base(gss) + [NadekoCommand, Aliases] + public async Task Flip(int count = 1) + { + if (count > 10 || count < 1) { - _images = data.LocalImages; - _cs = cs; + await ReplyErrorLocalizedAsync(strs.flip_invalid(10)); + return; } - - [NadekoCommand, Aliases] - public async Task Flip(int count = 1) + var headCount = 0; + var tailCount = 0; + var imgs = new Image[count]; + for (var i = 0; i < count; i++) { - if (count > 10 || count < 1) + var headsArr = _images.Heads[rng.Next(0, _images.Heads.Count)]; + var tailsArr = _images.Tails[rng.Next(0, _images.Tails.Count)]; + if (rng.Next(0, 10) < 5) { - await ReplyErrorLocalizedAsync(strs.flip_invalid(10)); - return; - } - var headCount = 0; - var tailCount = 0; - var imgs = new Image[count]; - for (var i = 0; i < count; i++) - { - var headsArr = _images.Heads[rng.Next(0, _images.Heads.Count)]; - var tailsArr = _images.Tails[rng.Next(0, _images.Tails.Count)]; - if (rng.Next(0, 10) < 5) - { - imgs[i] = Image.Load(headsArr); - headCount++; - } - else - { - imgs[i] = Image.Load(tailsArr); - tailCount++; - } - } - using (var img = imgs.Merge(out var format)) - using (var stream = img.ToStream(format)) - { - foreach (var i in imgs) - { - i.Dispose(); - } - var msg = count != 1 - ? Format.Bold(ctx.User.ToString()) + " " + GetText(strs.flip_results(count, headCount, tailCount)) - : Format.Bold(ctx.User.ToString()) + " " + GetText(strs.flipped(headCount > 0 - ? Format.Bold(GetText(strs.heads)) - : Format.Bold(GetText(strs.tails)))); - await ctx.Channel.SendFileAsync(stream, $"{count} coins.{format.FileExtensions.First()}", msg).ConfigureAwait(false); - } - } - - public enum BetFlipGuess - { - H = 1, - Head = 1, - Heads = 1, - T = 2, - Tail = 2, - Tails = 2 - } - - [NadekoCommand, Aliases] - public async Task Betflip(ShmartNumber amount, BetFlipGuess guess) - { - if (!await CheckBetMandatory(amount).ConfigureAwait(false) || amount == 1) - return; - - var removed = await _cs.RemoveAsync(ctx.User, "Betflip Gamble", amount, false, gamble: true).ConfigureAwait(false); - if (!removed) - { - await ReplyErrorLocalizedAsync(strs.not_enough(CurrencySign)); - return; - } - BetFlipGuess result; - Uri imageToSend; - var coins = _images.ImageUrls.Coins; - if (rng.Next(0, 1000) <= 499) - { - imageToSend = coins.Heads[rng.Next(0, coins.Heads.Length)]; - result = BetFlipGuess.Heads; + imgs[i] = Image.Load(headsArr); + headCount++; } else { - imageToSend = coins.Tails[rng.Next(0, coins.Tails.Length)]; - result = BetFlipGuess.Tails; + imgs[i] = Image.Load(tailsArr); + tailCount++; } - - string str; - if (guess == result) - { - var toWin = (long)(amount * _config.BetFlip.Multiplier); - str = Format.Bold(ctx.User.ToString()) + " " + GetText(strs.flip_guess(toWin + CurrencySign)); - await _cs.AddAsync(ctx.User, "Betflip Gamble", toWin, false, gamble: true).ConfigureAwait(false); - } - else - { - str = ctx.User.ToString() + " " + GetText(strs.better_luck); - } - - await ctx.Channel.EmbedAsync(_eb.Create() - .WithDescription(str) - .WithOkColor() - .WithImageUrl(imageToSend.ToString())).ConfigureAwait(false); } + using (var img = imgs.Merge(out var format)) + using (var stream = img.ToStream(format)) + { + foreach (var i in imgs) + { + i.Dispose(); + } + var msg = count != 1 + ? Format.Bold(ctx.User.ToString()) + " " + GetText(strs.flip_results(count, headCount, tailCount)) + : Format.Bold(ctx.User.ToString()) + " " + GetText(strs.flipped(headCount > 0 + ? Format.Bold(GetText(strs.heads)) + : Format.Bold(GetText(strs.tails)))); + await ctx.Channel.SendFileAsync(stream, $"{count} coins.{format.FileExtensions.First()}", msg).ConfigureAwait(false); + } + } + + public enum BetFlipGuess + { + H = 1, + Head = 1, + Heads = 1, + T = 2, + Tail = 2, + Tails = 2 + } + + [NadekoCommand, Aliases] + public async Task Betflip(ShmartNumber amount, BetFlipGuess guess) + { + if (!await CheckBetMandatory(amount).ConfigureAwait(false) || amount == 1) + return; + + var removed = await _cs.RemoveAsync(ctx.User, "Betflip Gamble", amount, false, gamble: true).ConfigureAwait(false); + if (!removed) + { + await ReplyErrorLocalizedAsync(strs.not_enough(CurrencySign)); + return; + } + BetFlipGuess result; + Uri imageToSend; + var coins = _images.ImageUrls.Coins; + if (rng.Next(0, 1000) <= 499) + { + imageToSend = coins.Heads[rng.Next(0, coins.Heads.Length)]; + result = BetFlipGuess.Heads; + } + else + { + imageToSend = coins.Tails[rng.Next(0, coins.Tails.Length)]; + result = BetFlipGuess.Tails; + } + + string str; + if (guess == result) + { + var toWin = (long)(amount * _config.BetFlip.Multiplier); + str = Format.Bold(ctx.User.ToString()) + " " + GetText(strs.flip_guess(toWin + CurrencySign)); + await _cs.AddAsync(ctx.User, "Betflip Gamble", toWin, false, gamble: true).ConfigureAwait(false); + } + else + { + str = ctx.User.ToString() + " " + GetText(strs.better_luck); + } + + await ctx.Channel.EmbedAsync(_eb.Create() + .WithDescription(str) + .WithOkColor() + .WithImageUrl(imageToSend.ToString())).ConfigureAwait(false); } } } \ No newline at end of file diff --git a/src/NadekoBot/Modules/Gambling/Gambling.cs b/src/NadekoBot/Modules/Gambling/Gambling.cs index a092e7731..46612eef8 100644 --- a/src/NadekoBot/Modules/Gambling/Gambling.cs +++ b/src/NadekoBot/Modules/Gambling/Gambling.cs @@ -7,670 +7,666 @@ using NadekoBot.Modules.Gambling.Common; using NadekoBot.Db.Models; using NadekoBot.Extensions; using NadekoBot.Modules.Gambling.Services; -using System; -using System.Collections.Generic; using System.Globalization; -using System.Linq; using System.Numerics; using System.Threading.Tasks; using NadekoBot.Services; using NadekoBot.Services.Database.Models; using NadekoBot.Db; -namespace NadekoBot.Modules.Gambling +namespace NadekoBot.Modules.Gambling; + +public partial class Gambling : GamblingModule { - public partial class Gambling : GamblingModule + private readonly DbService _db; + private readonly ICurrencyService _cs; + private readonly IDataCache _cache; + private readonly DiscordSocketClient _client; + private readonly NumberFormatInfo _enUsCulture; + private readonly DownloadTracker _tracker; + private readonly GamblingConfigService _configService; + + public Gambling(DbService db, ICurrencyService currency, + IDataCache cache, DiscordSocketClient client, + DownloadTracker tracker, GamblingConfigService configService) : base(configService) { - private readonly DbService _db; - private readonly ICurrencyService _cs; - private readonly IDataCache _cache; - private readonly DiscordSocketClient _client; - private readonly NumberFormatInfo _enUsCulture; - private readonly DownloadTracker _tracker; - private readonly GamblingConfigService _configService; + _db = db; + _cs = currency; + _cache = cache; + _client = client; + _enUsCulture = new CultureInfo("en-US", false).NumberFormat; + _enUsCulture.NumberDecimalDigits = 0; + _enUsCulture.NumberGroupSeparator = " "; + _tracker = tracker; + _configService = configService; + } - public Gambling(DbService db, ICurrencyService currency, - IDataCache cache, DiscordSocketClient client, - DownloadTracker tracker, GamblingConfigService configService) : base(configService) + private string n(long cur) => cur.ToString("N", _enUsCulture); + + public string GetCurrency(ulong id) + { + using (var uow = _db.GetDbContext()) { - _db = db; - _cs = currency; - _cache = cache; - _client = client; - _enUsCulture = new CultureInfo("en-US", false).NumberFormat; - _enUsCulture.NumberDecimalDigits = 0; - _enUsCulture.NumberGroupSeparator = " "; - _tracker = tracker; - _configService = configService; + return n(uow.DiscordUser.GetUserCurrency(id)); + } + } + + [NadekoCommand, Aliases] + public async Task Economy() + { + var ec = _service.GetEconomy(); + decimal onePercent = 0; + if (ec.Cash > 0) + { + onePercent = ec.OnePercent / (ec.Cash-ec.Bot); // This stops the top 1% from owning more than 100% of the money + // [21:03] Bob Page: Kinda remids me of US economy + } + var embed = _eb.Create() + .WithTitle(GetText(strs.economy_state)) + .AddField(GetText(strs.currency_owned), ((BigInteger)(ec.Cash - ec.Bot)).ToString("N", _enUsCulture) + CurrencySign) + .AddField(GetText(strs.currency_one_percent), (onePercent * 100).ToString("F2") + "%") + .AddField(GetText(strs.currency_planted), ((BigInteger)ec.Planted) + CurrencySign) + .AddField(GetText(strs.owned_waifus_total), ((BigInteger)ec.Waifus) + CurrencySign) + .AddField(GetText(strs.bot_currency), ec.Bot.ToString("N", _enUsCulture) + CurrencySign) + .AddField(GetText(strs.total), ((BigInteger)(ec.Cash + ec.Planted + ec.Waifus)).ToString("N", _enUsCulture) + CurrencySign) + .WithOkColor(); + // ec.Cash already contains ec.Bot as it's the total of all values in the CurrencyAmount column of the DiscordUser table + await ctx.Channel.EmbedAsync(embed).ConfigureAwait(false); + } + + [NadekoCommand, Aliases] + public async Task Timely() + { + var val = _config.Timely.Amount; + var period = _config.Timely.Cooldown; + if (val <= 0 || period <= 0) + { + await ReplyErrorLocalizedAsync(strs.timely_none).ConfigureAwait(false); + return; } - private string n(long cur) => cur.ToString("N", _enUsCulture); - - public string GetCurrency(ulong id) + TimeSpan? rem; + if ((rem = _cache.AddTimelyClaim(ctx.User.Id, period)) != null) { - using (var uow = _db.GetDbContext()) - { - return n(uow.DiscordUser.GetUserCurrency(id)); - } + await ReplyErrorLocalizedAsync(strs.timely_already_claimed(rem?.ToString(@"dd\d\ hh\h\ mm\m\ ss\s"))).ConfigureAwait(false); + return; } - [NadekoCommand, Aliases] - public async Task Economy() - { - var ec = _service.GetEconomy(); - decimal onePercent = 0; - if (ec.Cash > 0) - { - onePercent = ec.OnePercent / (ec.Cash-ec.Bot); // This stops the top 1% from owning more than 100% of the money - // [21:03] Bob Page: Kinda remids me of US economy - } - var embed = _eb.Create() - .WithTitle(GetText(strs.economy_state)) - .AddField(GetText(strs.currency_owned), ((BigInteger)(ec.Cash - ec.Bot)).ToString("N", _enUsCulture) + CurrencySign) - .AddField(GetText(strs.currency_one_percent), (onePercent * 100).ToString("F2") + "%") - .AddField(GetText(strs.currency_planted), ((BigInteger)ec.Planted) + CurrencySign) - .AddField(GetText(strs.owned_waifus_total), ((BigInteger)ec.Waifus) + CurrencySign) - .AddField(GetText(strs.bot_currency), ec.Bot.ToString("N", _enUsCulture) + CurrencySign) - .AddField(GetText(strs.total), ((BigInteger)(ec.Cash + ec.Planted + ec.Waifus)).ToString("N", _enUsCulture) + CurrencySign) - .WithOkColor(); - // ec.Cash already contains ec.Bot as it's the total of all values in the CurrencyAmount column of the DiscordUser table - await ctx.Channel.EmbedAsync(embed).ConfigureAwait(false); - } + await _cs.AddAsync(ctx.User.Id, "Timely claim", val).ConfigureAwait(false); - [NadekoCommand, Aliases] - public async Task Timely() - { - var val = _config.Timely.Amount; - var period = _config.Timely.Cooldown; - if (val <= 0 || period <= 0) - { - await ReplyErrorLocalizedAsync(strs.timely_none).ConfigureAwait(false); - return; - } + await ReplyConfirmLocalizedAsync(strs.timely(n(val) + CurrencySign, period)); + } - TimeSpan? rem; - if ((rem = _cache.AddTimelyClaim(ctx.User.Id, period)) != null) - { - await ReplyErrorLocalizedAsync(strs.timely_already_claimed(rem?.ToString(@"dd\d\ hh\h\ mm\m\ ss\s"))).ConfigureAwait(false); - return; - } + [NadekoCommand, Aliases] + [OwnerOnly] + public async Task TimelyReset() + { + _cache.RemoveAllTimelyClaims(); + await ReplyConfirmLocalizedAsync(strs.timely_reset).ConfigureAwait(false); + } - await _cs.AddAsync(ctx.User.Id, "Timely claim", val).ConfigureAwait(false); - - await ReplyConfirmLocalizedAsync(strs.timely(n(val) + CurrencySign, period)); - } - - [NadekoCommand, Aliases] - [OwnerOnly] - public async Task TimelyReset() - { - _cache.RemoveAllTimelyClaims(); - await ReplyConfirmLocalizedAsync(strs.timely_reset).ConfigureAwait(false); - } - - [NadekoCommand, Aliases] - [OwnerOnly] - public async Task TimelySet(int amount, int period = 24) - { - if (amount < 0 || period < 0) - return; + [NadekoCommand, Aliases] + [OwnerOnly] + public async Task TimelySet(int amount, int period = 24) + { + if (amount < 0 || period < 0) + return; - _configService.ModifyConfig(gs => - { - gs.Timely.Amount = amount; - gs.Timely.Cooldown = period; - }); + _configService.ModifyConfig(gs => + { + gs.Timely.Amount = amount; + gs.Timely.Cooldown = period; + }); - if (amount == 0) - await ReplyConfirmLocalizedAsync(strs.timely_set_none).ConfigureAwait(false); - else - await ReplyConfirmLocalizedAsync(strs.timely_set(Format.Bold(n(amount) + CurrencySign), Format.Bold(period.ToString()))).ConfigureAwait(false); + if (amount == 0) + await ReplyConfirmLocalizedAsync(strs.timely_set_none).ConfigureAwait(false); + else + await ReplyConfirmLocalizedAsync(strs.timely_set(Format.Bold(n(amount) + CurrencySign), Format.Bold(period.ToString()))).ConfigureAwait(false); + } + + [NadekoCommand, Aliases] + [RequireContext(ContextType.Guild)] + public async Task Raffle([Leftover] IRole role = null) + { + role = role ?? ctx.Guild.EveryoneRole; + + var members = (await role.GetMembersAsync().ConfigureAwait(false)).Where(u => u.Status != UserStatus.Offline); + var membersArray = members as IUser[] ?? members.ToArray(); + if (membersArray.Length == 0) + { + return; + } + var usr = membersArray[new NadekoRandom().Next(0, membersArray.Length)]; + await SendConfirmAsync("🎟 " + GetText(strs.raffled_user), $"**{usr.Username}#{usr.Discriminator}**", footer: $"ID: {usr.Id}").ConfigureAwait(false); + } + + [NadekoCommand, Aliases] + [RequireContext(ContextType.Guild)] + public async Task RaffleAny([Leftover] IRole role = null) + { + role = role ?? ctx.Guild.EveryoneRole; + + var members = (await role.GetMembersAsync().ConfigureAwait(false)); + var membersArray = members as IUser[] ?? members.ToArray(); + if (membersArray.Length == 0) + { + return; + } + var usr = membersArray[new NadekoRandom().Next(0, membersArray.Length)]; + await SendConfirmAsync("🎟 " + GetText(strs.raffled_user), $"**{usr.Username}#{usr.Discriminator}**", footer: $"ID: {usr.Id}").ConfigureAwait(false); + } + + [NadekoCommand, Aliases] + [Priority(1)] + public async Task Cash([Leftover] IUser user = null) + { + user = user ?? ctx.User; + await ConfirmLocalizedAsync(strs.has(Format.Bold(user.ToString()), $"{GetCurrency(user.Id)} {CurrencySign}")); + } + + [NadekoCommand, Aliases] + [Priority(2)] + public Task CurrencyTransactions(int page = 1) => + InternalCurrencyTransactions(ctx.User.Id, page); + + [NadekoCommand, Aliases] + [OwnerOnly] + [Priority(0)] + public Task CurrencyTransactions([Leftover] IUser usr) => + InternalCurrencyTransactions(usr.Id, 1); + + [NadekoCommand, Aliases] + [OwnerOnly] + [Priority(1)] + public Task CurrencyTransactions(IUser usr, int page) => + InternalCurrencyTransactions(usr.Id, page); + + private async Task InternalCurrencyTransactions(ulong userId, int page) + { + if (--page < 0) + return; + + var trs = new List(); + using (var uow = _db.GetDbContext()) + { + trs = uow.CurrencyTransactions.GetPageFor(userId, page); } - [NadekoCommand, Aliases] - [RequireContext(ContextType.Guild)] - public async Task Raffle([Leftover] IRole role = null) - { - role = role ?? ctx.Guild.EveryoneRole; + var embed = _eb.Create() + .WithTitle(GetText(strs.transactions( + ((SocketGuild)ctx.Guild)?.GetUser(userId)?.ToString() ?? $"{userId}"))) + .WithOkColor(); - var members = (await role.GetMembersAsync().ConfigureAwait(false)).Where(u => u.Status != UserStatus.Offline); - var membersArray = members as IUser[] ?? members.ToArray(); - if (membersArray.Length == 0) - { - return; - } - var usr = membersArray[new NadekoRandom().Next(0, membersArray.Length)]; - await SendConfirmAsync("🎟 " + GetText(strs.raffled_user), $"**{usr.Username}#{usr.Discriminator}**", footer: $"ID: {usr.Id}").ConfigureAwait(false); + var desc = ""; + foreach (var tr in trs) + { + var type = tr.Amount > 0 ? "🔵" : "🔴"; + var date = Format.Code($"〖{tr.DateAdded:HH:mm yyyy-MM-dd}〗"); + desc += $"\\{type} {date} {Format.Bold(n(tr.Amount))}\n\t{tr.Reason?.Trim()}\n"; } - [NadekoCommand, Aliases] - [RequireContext(ContextType.Guild)] - public async Task RaffleAny([Leftover] IRole role = null) - { - role = role ?? ctx.Guild.EveryoneRole; + embed.WithDescription(desc); + embed.WithFooter(GetText(strs.page(page + 1))); + await ctx.Channel.EmbedAsync(embed).ConfigureAwait(false); + } - var members = (await role.GetMembersAsync().ConfigureAwait(false)); - var membersArray = members as IUser[] ?? members.ToArray(); - if (membersArray.Length == 0) - { - return; - } - var usr = membersArray[new NadekoRandom().Next(0, membersArray.Length)]; - await SendConfirmAsync("🎟 " + GetText(strs.raffled_user), $"**{usr.Username}#{usr.Discriminator}**", footer: $"ID: {usr.Id}").ConfigureAwait(false); + [NadekoCommand, Aliases] + [Priority(0)] + public async Task Cash(ulong userId) + { + await ReplyConfirmLocalizedAsync(strs.has(Format.Code(userId.ToString()), $"{GetCurrency(userId)} {CurrencySign}")); + } + + [NadekoCommand, Aliases] + [RequireContext(ContextType.Guild)] + [Priority(0)] + public async Task Give(ShmartNumber amount, IGuildUser receiver, [Leftover] string msg = null) + { + if (amount <= 0 || ctx.User.Id == receiver.Id || receiver.IsBot) + return; + var success = await _cs.RemoveAsync((IGuildUser)ctx.User, $"Gift to {receiver.Username} ({receiver.Id}).", amount, false).ConfigureAwait(false); + if (!success) + { + await ReplyErrorLocalizedAsync(strs.not_enough(CurrencySign)); + return; + } + await _cs.AddAsync(receiver, $"Gift from {ctx.User.Username} ({ctx.User.Id}) - {msg}.", amount, true).ConfigureAwait(false); + await ReplyConfirmLocalizedAsync(strs.gifted(n(amount) + CurrencySign, Format.Bold(receiver.ToString()))); + } + + [NadekoCommand, Aliases] + [RequireContext(ContextType.Guild)] + [Priority(1)] + public Task Give(ShmartNumber amount, [Leftover] IGuildUser receiver) + => Give(amount, receiver, null); + + [NadekoCommand, Aliases] + [RequireContext(ContextType.Guild)] + [OwnerOnly] + [Priority(0)] + public Task Award(long amount, IGuildUser usr, [Leftover] string msg) => + Award(amount, usr.Id, msg); + + [NadekoCommand, Aliases] + [RequireContext(ContextType.Guild)] + [OwnerOnly] + [Priority(1)] + public Task Award(long amount, [Leftover] IGuildUser usr) => + Award(amount, usr.Id); + + [NadekoCommand, Aliases] + [OwnerOnly] + [Priority(2)] + public async Task Award(long amount, ulong usrId, [Leftover] string msg = null) + { + if (amount <= 0) + return; + + var usr = await ((DiscordSocketClient)Context.Client).Rest.GetUserAsync(usrId); + + if(usr is null) + { + await ReplyErrorLocalizedAsync(strs.user_not_found).ConfigureAwait(false); + return; } - [NadekoCommand, Aliases] - [Priority(1)] - public async Task Cash([Leftover] IUser user = null) - { - user = user ?? ctx.User; - await ConfirmLocalizedAsync(strs.has(Format.Bold(user.ToString()), $"{GetCurrency(user.Id)} {CurrencySign}")); - } + await _cs.AddAsync(usr, + $"Awarded by bot owner. ({ctx.User.Username}/{ctx.User.Id}) {(msg ?? "")}", + amount, + gamble: (ctx.Client.CurrentUser.Id != usrId)).ConfigureAwait(false); + await ReplyConfirmLocalizedAsync(strs.awarded(n(amount) + CurrencySign, $"<@{usrId}>")); + } - [NadekoCommand, Aliases] - [Priority(2)] - public Task CurrencyTransactions(int page = 1) => - InternalCurrencyTransactions(ctx.User.Id, page); + [NadekoCommand, Aliases] + [RequireContext(ContextType.Guild)] + [OwnerOnly] + [Priority(3)] + public async Task Award(long amount, [Leftover] IRole role) + { + var users = (await ctx.Guild.GetUsersAsync().ConfigureAwait(false)) + .Where(u => u.GetRoles().Contains(role)) + .ToList(); - [NadekoCommand, Aliases] - [OwnerOnly] - [Priority(0)] - public Task CurrencyTransactions([Leftover] IUser usr) => - InternalCurrencyTransactions(usr.Id, 1); - - [NadekoCommand, Aliases] - [OwnerOnly] - [Priority(1)] - public Task CurrencyTransactions(IUser usr, int page) => - InternalCurrencyTransactions(usr.Id, page); - - private async Task InternalCurrencyTransactions(ulong userId, int page) - { - if (--page < 0) - return; - - var trs = new List(); - using (var uow = _db.GetDbContext()) - { - trs = uow.CurrencyTransactions.GetPageFor(userId, page); - } - - var embed = _eb.Create() - .WithTitle(GetText(strs.transactions( - ((SocketGuild)ctx.Guild)?.GetUser(userId)?.ToString() ?? $"{userId}"))) - .WithOkColor(); - - var desc = ""; - foreach (var tr in trs) - { - var type = tr.Amount > 0 ? "🔵" : "🔴"; - var date = Format.Code($"〖{tr.DateAdded:HH:mm yyyy-MM-dd}〗"); - desc += $"\\{type} {date} {Format.Bold(n(tr.Amount))}\n\t{tr.Reason?.Trim()}\n"; - } - - embed.WithDescription(desc); - embed.WithFooter(GetText(strs.page(page + 1))); - await ctx.Channel.EmbedAsync(embed).ConfigureAwait(false); - } - - [NadekoCommand, Aliases] - [Priority(0)] - public async Task Cash(ulong userId) - { - await ReplyConfirmLocalizedAsync(strs.has(Format.Code(userId.ToString()), $"{GetCurrency(userId)} {CurrencySign}")); - } - - [NadekoCommand, Aliases] - [RequireContext(ContextType.Guild)] - [Priority(0)] - public async Task Give(ShmartNumber amount, IGuildUser receiver, [Leftover] string msg = null) - { - if (amount <= 0 || ctx.User.Id == receiver.Id || receiver.IsBot) - return; - var success = await _cs.RemoveAsync((IGuildUser)ctx.User, $"Gift to {receiver.Username} ({receiver.Id}).", amount, false).ConfigureAwait(false); - if (!success) - { - await ReplyErrorLocalizedAsync(strs.not_enough(CurrencySign)); - return; - } - await _cs.AddAsync(receiver, $"Gift from {ctx.User.Username} ({ctx.User.Id}) - {msg}.", amount, true).ConfigureAwait(false); - await ReplyConfirmLocalizedAsync(strs.gifted(n(amount) + CurrencySign, Format.Bold(receiver.ToString()))); - } - - [NadekoCommand, Aliases] - [RequireContext(ContextType.Guild)] - [Priority(1)] - public Task Give(ShmartNumber amount, [Leftover] IGuildUser receiver) - => Give(amount, receiver, null); - - [NadekoCommand, Aliases] - [RequireContext(ContextType.Guild)] - [OwnerOnly] - [Priority(0)] - public Task Award(long amount, IGuildUser usr, [Leftover] string msg) => - Award(amount, usr.Id, msg); - - [NadekoCommand, Aliases] - [RequireContext(ContextType.Guild)] - [OwnerOnly] - [Priority(1)] - public Task Award(long amount, [Leftover] IGuildUser usr) => - Award(amount, usr.Id); - - [NadekoCommand, Aliases] - [OwnerOnly] - [Priority(2)] - public async Task Award(long amount, ulong usrId, [Leftover] string msg = null) - { - if (amount <= 0) - return; - - var usr = await ((DiscordSocketClient)Context.Client).Rest.GetUserAsync(usrId); - - if(usr is null) - { - await ReplyErrorLocalizedAsync(strs.user_not_found).ConfigureAwait(false); - return; - } - - await _cs.AddAsync(usr, - $"Awarded by bot owner. ({ctx.User.Username}/{ctx.User.Id}) {(msg ?? "")}", - amount, - gamble: (ctx.Client.CurrentUser.Id != usrId)).ConfigureAwait(false); - await ReplyConfirmLocalizedAsync(strs.awarded(n(amount) + CurrencySign, $"<@{usrId}>")); - } - - [NadekoCommand, Aliases] - [RequireContext(ContextType.Guild)] - [OwnerOnly] - [Priority(3)] - public async Task Award(long amount, [Leftover] IRole role) - { - var users = (await ctx.Guild.GetUsersAsync().ConfigureAwait(false)) - .Where(u => u.GetRoles().Contains(role)) - .ToList(); - - await _cs.AddBulkAsync(users.Select(x => x.Id), + await _cs.AddBulkAsync(users.Select(x => x.Id), users.Select(x => $"Awarded by bot owner to **{role.Name}** role. ({ctx.User.Username}/{ctx.User.Id})"), users.Select(x => amount), gamble: true) - .ConfigureAwait(false); + .ConfigureAwait(false); - await ReplyConfirmLocalizedAsync(strs.mass_award( - n(amount) + CurrencySign, - Format.Bold(users.Count.ToString()), - Format.Bold(role.Name))); - } + await ReplyConfirmLocalizedAsync(strs.mass_award( + n(amount) + CurrencySign, + Format.Bold(users.Count.ToString()), + Format.Bold(role.Name))); + } - [NadekoCommand, Aliases] - [RequireContext(ContextType.Guild)] - [OwnerOnly] - [Priority(0)] - public async Task Take(long amount, [Leftover] IRole role) - { - var users = (await role.GetMembersAsync()).ToList(); + [NadekoCommand, Aliases] + [RequireContext(ContextType.Guild)] + [OwnerOnly] + [Priority(0)] + public async Task Take(long amount, [Leftover] IRole role) + { + var users = (await role.GetMembersAsync()).ToList(); - await _cs.RemoveBulkAsync(users.Select(x => x.Id), - users.Select(x => $"Taken by bot owner from **{role.Name}** role. ({ctx.User.Username}/{ctx.User.Id})"), - users.Select(x => amount), - gamble: true) - .ConfigureAwait(false); + await _cs.RemoveBulkAsync(users.Select(x => x.Id), + users.Select(x => $"Taken by bot owner from **{role.Name}** role. ({ctx.User.Username}/{ctx.User.Id})"), + users.Select(x => amount), + gamble: true) + .ConfigureAwait(false); - await ReplyConfirmLocalizedAsync(strs.mass_take( - n(amount) + CurrencySign, - Format.Bold(users.Count.ToString()), - Format.Bold(role.Name))); - } + await ReplyConfirmLocalizedAsync(strs.mass_take( + n(amount) + CurrencySign, + Format.Bold(users.Count.ToString()), + Format.Bold(role.Name))); + } - [NadekoCommand, Aliases] - [RequireContext(ContextType.Guild)] - [OwnerOnly] - [Priority(1)] - public async Task Take(long amount, [Leftover] IGuildUser user) - { - if (amount <= 0) - return; + [NadekoCommand, Aliases] + [RequireContext(ContextType.Guild)] + [OwnerOnly] + [Priority(1)] + public async Task Take(long amount, [Leftover] IGuildUser user) + { + if (amount <= 0) + return; - if (await _cs.RemoveAsync(user, $"Taken by bot owner.({ctx.User.Username}/{ctx.User.Id})", amount, + if (await _cs.RemoveAsync(user, $"Taken by bot owner.({ctx.User.Username}/{ctx.User.Id})", amount, gamble: (ctx.Client.CurrentUser.Id != user.Id)).ConfigureAwait(false)) - await ReplyConfirmLocalizedAsync(strs.take(n(amount) + CurrencySign, Format.Bold(user.ToString()))).ConfigureAwait(false); - else - await ReplyErrorLocalizedAsync(strs.take_fail(n(amount) + CurrencySign, Format.Bold(user.ToString()), CurrencySign)); - } + await ReplyConfirmLocalizedAsync(strs.take(n(amount) + CurrencySign, Format.Bold(user.ToString()))).ConfigureAwait(false); + else + await ReplyErrorLocalizedAsync(strs.take_fail(n(amount) + CurrencySign, Format.Bold(user.ToString()), CurrencySign)); + } - [NadekoCommand, Aliases] - [OwnerOnly] - public async Task Take(long amount, [Leftover] ulong usrId) - { - if (amount <= 0) - return; + [NadekoCommand, Aliases] + [OwnerOnly] + public async Task Take(long amount, [Leftover] ulong usrId) + { + if (amount <= 0) + return; - if (await _cs.RemoveAsync(usrId, $"Taken by bot owner.({ctx.User.Username}/{ctx.User.Id})", amount, + if (await _cs.RemoveAsync(usrId, $"Taken by bot owner.({ctx.User.Username}/{ctx.User.Id})", amount, gamble: (ctx.Client.CurrentUser.Id != usrId)).ConfigureAwait(false)) - await ReplyConfirmLocalizedAsync(strs.take(amount + CurrencySign, $"<@{usrId}>")); + await ReplyConfirmLocalizedAsync(strs.take(amount + CurrencySign, $"<@{usrId}>")); + else + await ReplyErrorLocalizedAsync(strs.take_fail(amount + CurrencySign, Format.Code(usrId.ToString()), CurrencySign)); + } + + private IUserMessage rdMsg = null; + + [NadekoCommand, Aliases] + [RequireContext(ContextType.Guild)] + public async Task RollDuel(IUser u) + { + if (ctx.User.Id == u.Id) + return; + + //since the challenge is created by another user, we need to reverse the ids + //if it gets removed, means challenge is accepted + if (_service.Duels.TryRemove((ctx.User.Id, u.Id), out var game)) + { + await game.StartGame().ConfigureAwait(false); + } + } + + [NadekoCommand, Aliases] + [RequireContext(ContextType.Guild)] + public async Task RollDuel(ShmartNumber amount, IUser u) + { + if (ctx.User.Id == u.Id) + return; + + if (amount <= 0) + return; + + var embed = _eb.Create() + .WithOkColor() + .WithTitle(GetText(strs.roll_duel)); + + var description = string.Empty; + + var game = new RollDuelGame(_cs, _client.CurrentUser.Id, ctx.User.Id, u.Id, amount); + //means challenge is just created + if (_service.Duels.TryGetValue((ctx.User.Id, u.Id), out var other)) + { + if (other.Amount != amount) + { + await ReplyErrorLocalizedAsync(strs.roll_duel_already_challenged).ConfigureAwait(false); + } else - await ReplyErrorLocalizedAsync(strs.take_fail(amount + CurrencySign, Format.Code(usrId.ToString()), CurrencySign)); + { + await RollDuel(u).ConfigureAwait(false); + } + return; + } + if (_service.Duels.TryAdd((u.Id, ctx.User.Id), game)) + { + game.OnGameTick += Game_OnGameTick; + game.OnEnded += Game_OnEnded; + + await ReplyConfirmLocalizedAsync(strs.roll_duel_challenge( + Format.Bold(ctx.User.ToString()), + Format.Bold(u.ToString()), + Format.Bold(amount + CurrencySign))); } - private IUserMessage rdMsg = null; - - [NadekoCommand, Aliases] - [RequireContext(ContextType.Guild)] - public async Task RollDuel(IUser u) + async Task Game_OnGameTick(RollDuelGame arg) { - if (ctx.User.Id == u.Id) - return; - - //since the challenge is created by another user, we need to reverse the ids - //if it gets removed, means challenge is accepted - if (_service.Duels.TryRemove((ctx.User.Id, u.Id), out var game)) - { - await game.StartGame().ConfigureAwait(false); - } - } - - [NadekoCommand, Aliases] - [RequireContext(ContextType.Guild)] - public async Task RollDuel(ShmartNumber amount, IUser u) - { - if (ctx.User.Id == u.Id) - return; - - if (amount <= 0) - return; - - var embed = _eb.Create() - .WithOkColor() - .WithTitle(GetText(strs.roll_duel)); - - var description = string.Empty; - - var game = new RollDuelGame(_cs, _client.CurrentUser.Id, ctx.User.Id, u.Id, amount); - //means challenge is just created - if (_service.Duels.TryGetValue((ctx.User.Id, u.Id), out var other)) - { - if (other.Amount != amount) - { - await ReplyErrorLocalizedAsync(strs.roll_duel_already_challenged).ConfigureAwait(false); - } - else - { - await RollDuel(u).ConfigureAwait(false); - } - return; - } - if (_service.Duels.TryAdd((u.Id, ctx.User.Id), game)) - { - game.OnGameTick += Game_OnGameTick; - game.OnEnded += Game_OnEnded; - - await ReplyConfirmLocalizedAsync(strs.roll_duel_challenge( - Format.Bold(ctx.User.ToString()), - Format.Bold(u.ToString()), - Format.Bold(amount + CurrencySign))); - } - - async Task Game_OnGameTick(RollDuelGame arg) - { - var rolls = arg.Rolls.Last(); - description += $@"{Format.Bold(ctx.User.ToString())} rolled **{rolls.Item1}** + var rolls = arg.Rolls.Last(); + description += $@"{Format.Bold(ctx.User.ToString())} rolled **{rolls.Item1}** {Format.Bold(u.ToString())} rolled **{rolls.Item2}** -- "; - embed = embed.WithDescription(description); + embed = embed.WithDescription(description); - if (rdMsg is null) - { - rdMsg = await ctx.Channel.EmbedAsync(embed) - .ConfigureAwait(false); - } - else - { - await rdMsg.ModifyAsync(x => - { - x.Embed = embed.Build(); - }).ConfigureAwait(false); - } - } - - async Task Game_OnEnded(RollDuelGame rdGame, RollDuelGame.Reason reason) + if (rdMsg is null) { - try + rdMsg = await ctx.Channel.EmbedAsync(embed) + .ConfigureAwait(false); + } + else + { + await rdMsg.ModifyAsync(x => { - if (reason == RollDuelGame.Reason.Normal) - { - var winner = rdGame.Winner == rdGame.P1 - ? ctx.User - : u; - description += $"\n**{winner}** Won {n(((long)(rdGame.Amount * 2 * 0.98))) + CurrencySign}"; - - embed = embed.WithDescription(description); - - await rdMsg.ModifyAsync(x => x.Embed = embed.Build()) - .ConfigureAwait(false); - } - else if (reason == RollDuelGame.Reason.Timeout) - { - await ReplyErrorLocalizedAsync(strs.roll_duel_timeout).ConfigureAwait(false); - } - else if (reason == RollDuelGame.Reason.NoFunds) - { - await ReplyErrorLocalizedAsync(strs.roll_duel_no_funds).ConfigureAwait(false); - } - } - finally - { - _service.Duels.TryRemove((u.Id, ctx.User.Id), out var _); - } + x.Embed = embed.Build(); + }).ConfigureAwait(false); } } - private async Task InternallBetroll(long amount) + async Task Game_OnEnded(RollDuelGame rdGame, RollDuelGame.Reason reason) { - if (!await CheckBetMandatory(amount).ConfigureAwait(false)) - return; + try + { + if (reason == RollDuelGame.Reason.Normal) + { + var winner = rdGame.Winner == rdGame.P1 + ? ctx.User + : u; + description += $"\n**{winner}** Won {n(((long)(rdGame.Amount * 2 * 0.98))) + CurrencySign}"; - if (!await _cs.RemoveAsync(ctx.User, "Betroll Gamble", amount, false, gamble: true).ConfigureAwait(false)) + embed = embed.WithDescription(description); + + await rdMsg.ModifyAsync(x => x.Embed = embed.Build()) + .ConfigureAwait(false); + } + else if (reason == RollDuelGame.Reason.Timeout) + { + await ReplyErrorLocalizedAsync(strs.roll_duel_timeout).ConfigureAwait(false); + } + else if (reason == RollDuelGame.Reason.NoFunds) + { + await ReplyErrorLocalizedAsync(strs.roll_duel_no_funds).ConfigureAwait(false); + } + } + finally + { + _service.Duels.TryRemove((u.Id, ctx.User.Id), out var _); + } + } + } + + private async Task InternallBetroll(long amount) + { + if (!await CheckBetMandatory(amount).ConfigureAwait(false)) + return; + + if (!await _cs.RemoveAsync(ctx.User, "Betroll Gamble", amount, false, gamble: true).ConfigureAwait(false)) + { + await ReplyErrorLocalizedAsync(strs.not_enough(CurrencySign)); + return; + } + + var br = new Betroll(base._config.BetRoll); + + var result = br.Roll(); + + + var str = Format.Bold(ctx.User.ToString()) + Format.Code(GetText(strs.roll(result.Roll))); + if (result.Multiplier > 0) + { + var win = (long)(amount * result.Multiplier); + str += GetText(strs.br_win( + n(win) + CurrencySign, + result.Threshold + (result.Roll == 100 ? " 👑" : ""))); + await _cs.AddAsync(ctx.User, "Betroll Gamble", + win, false, gamble: true).ConfigureAwait(false); + } + else + { + str += GetText(strs.better_luck); + } + + await SendConfirmAsync(str).ConfigureAwait(false); + } + + [NadekoCommand, Aliases] + public Task BetRoll(ShmartNumber amount) + => InternallBetroll(amount); + + [NadekoCommand, Aliases] + [NadekoOptions(typeof(LbOpts))] + [Priority(0)] + public Task Leaderboard(params string[] args) + => Leaderboard(1, args); + + [NadekoCommand, Aliases] + [NadekoOptions(typeof(LbOpts))] + [Priority(1)] + public async Task Leaderboard(int page = 1, params string[] args) + { + if (--page < 0) + return; + + var (opts, _) = OptionsParser.ParseFrom(new LbOpts(), args); + + List cleanRichest = new List(); + // it's pointless to have clean on dm context + if (ctx.Guild is null) + { + opts.Clean = false; + } + + if (opts.Clean) + { + var now = DateTime.UtcNow; + + using (var uow = _db.GetDbContext()) + { + cleanRichest = uow.DiscordUser.GetTopRichest(_client.CurrentUser.Id, 10_000); + } + + await ctx.Channel.TriggerTypingAsync().ConfigureAwait(false); + await _tracker.EnsureUsersDownloadedAsync(ctx.Guild).ConfigureAwait(false); + + var sg = (SocketGuild)ctx.Guild; + cleanRichest = cleanRichest.Where(x => sg.GetUser(x.UserId) != null) + .ToList(); + } + else + { + using (var uow = _db.GetDbContext()) + { + cleanRichest = uow.DiscordUser.GetTopRichest(_client.CurrentUser.Id, 9, page).ToList(); + } + } + + await ctx.SendPaginatedConfirmAsync(page, curPage => + { + var embed = _eb.Create() + .WithOkColor() + .WithTitle(CurrencySign + " " + GetText(strs.leaderboard)); + + List toSend; + if (!opts.Clean) + { + using (var uow = _db.GetDbContext()) + { + toSend = uow.DiscordUser.GetTopRichest(_client.CurrentUser.Id, 9, curPage); + } + } + else + { + toSend = cleanRichest.Skip(curPage * 9).Take(9).ToList(); + } + if (!toSend.Any()) + { + embed.WithDescription(GetText(strs.no_user_on_this_page)); + return embed; + } + + for (var i = 0; i < toSend.Count; i++) + { + var x = toSend[i]; + var usrStr = x.ToString().TrimTo(20, true); + + var j = i; + embed.AddField("#" + (9 * curPage + j + 1) + " " + usrStr, n(x.CurrencyAmount) + " " + CurrencySign, true); + } + + return embed; + }, opts.Clean ? cleanRichest.Count() : 9000, 9, opts.Clean); + } + + + public enum RpsPick + { + R = 0, + Rock = 0, + Rocket = 0, + P = 1, + Paper = 1, + Paperclip = 1, + S = 2, + Scissors = 2 + } + + public enum RpsResult + { + Win, + Loss, + Draw, + } + + [NadekoCommand, Aliases] + public async Task Rps(RpsPick pick, ShmartNumber amount = default) + { + long oldAmount = amount; + if (!await CheckBetOptional(amount).ConfigureAwait(false) || (amount == 1)) + return; + + string getRpsPick(RpsPick p) + { + switch (p) + { + case RpsPick.R: + return "🚀"; + case RpsPick.P: + return "📎"; + default: + return "✂️"; + } + } + var embed = _eb.Create(); + + var nadekoPick = (RpsPick)new NadekoRandom().Next(0, 3); + + if (amount > 0) + { + if (!await _cs.RemoveAsync(ctx.User.Id, + "Rps-bet", amount, gamble: true).ConfigureAwait(false)) { await ReplyErrorLocalizedAsync(strs.not_enough(CurrencySign)); return; } - - var br = new Betroll(base._config.BetRoll); - - var result = br.Roll(); - - - var str = Format.Bold(ctx.User.ToString()) + Format.Code(GetText(strs.roll(result.Roll))); - if (result.Multiplier > 0) - { - var win = (long)(amount * result.Multiplier); - str += GetText(strs.br_win( - n(win) + CurrencySign, - result.Threshold + (result.Roll == 100 ? " 👑" : ""))); - await _cs.AddAsync(ctx.User, "Betroll Gamble", - win, false, gamble: true).ConfigureAwait(false); - } - else - { - str += GetText(strs.better_luck); - } - - await SendConfirmAsync(str).ConfigureAwait(false); } - [NadekoCommand, Aliases] - public Task BetRoll(ShmartNumber amount) - => InternallBetroll(amount); - - [NadekoCommand, Aliases] - [NadekoOptions(typeof(LbOpts))] - [Priority(0)] - public Task Leaderboard(params string[] args) - => Leaderboard(1, args); - - [NadekoCommand, Aliases] - [NadekoOptions(typeof(LbOpts))] - [Priority(1)] - public async Task Leaderboard(int page = 1, params string[] args) + string msg; + if (pick == nadekoPick) { - if (--page < 0) - return; - - var (opts, _) = OptionsParser.ParseFrom(new LbOpts(), args); - - List cleanRichest = new List(); - // it's pointless to have clean on dm context - if (ctx.Guild is null) - { - opts.Clean = false; - } - - if (opts.Clean) - { - var now = DateTime.UtcNow; - - using (var uow = _db.GetDbContext()) - { - cleanRichest = uow.DiscordUser.GetTopRichest(_client.CurrentUser.Id, 10_000); - } - - await ctx.Channel.TriggerTypingAsync().ConfigureAwait(false); - await _tracker.EnsureUsersDownloadedAsync(ctx.Guild).ConfigureAwait(false); - - var sg = (SocketGuild)ctx.Guild; - cleanRichest = cleanRichest.Where(x => sg.GetUser(x.UserId) != null) - .ToList(); - } - else - { - using (var uow = _db.GetDbContext()) - { - cleanRichest = uow.DiscordUser.GetTopRichest(_client.CurrentUser.Id, 9, page).ToList(); - } - } - - await ctx.SendPaginatedConfirmAsync(page, curPage => - { - var embed = _eb.Create() - .WithOkColor() - .WithTitle(CurrencySign + " " + GetText(strs.leaderboard)); - - List toSend; - if (!opts.Clean) - { - using (var uow = _db.GetDbContext()) - { - toSend = uow.DiscordUser.GetTopRichest(_client.CurrentUser.Id, 9, curPage); - } - } - else - { - toSend = cleanRichest.Skip(curPage * 9).Take(9).ToList(); - } - if (!toSend.Any()) - { - embed.WithDescription(GetText(strs.no_user_on_this_page)); - return embed; - } - - for (var i = 0; i < toSend.Count; i++) - { - var x = toSend[i]; - var usrStr = x.ToString().TrimTo(20, true); - - var j = i; - embed.AddField("#" + (9 * curPage + j + 1) + " " + usrStr, n(x.CurrencyAmount) + " " + CurrencySign, true); - } - - return embed; - }, opts.Clean ? cleanRichest.Count() : 9000, 9, opts.Clean); + await _cs.AddAsync(ctx.User.Id, + "Rps-draw", amount, gamble: true).ConfigureAwait(false); + embed.WithOkColor(); + msg = GetText(strs.rps_draw(getRpsPick(pick))); } - - - public enum RpsPick + else if ((pick == RpsPick.Paper && nadekoPick == RpsPick.Rock) || + (pick == RpsPick.Rock && nadekoPick == RpsPick.Scissors) || + (pick == RpsPick.Scissors && nadekoPick == RpsPick.Paper)) { - R = 0, - Rock = 0, - Rocket = 0, - P = 1, - Paper = 1, - Paperclip = 1, - S = 2, - Scissors = 2 + amount = (long)(amount * base._config.BetFlip.Multiplier); + await _cs.AddAsync(ctx.User.Id, + "Rps-win", amount, gamble: true).ConfigureAwait(false); + embed.WithOkColor(); + embed.AddField(GetText(strs.won), n(amount)); + msg = GetText(strs.rps_win(ctx.User.Mention, getRpsPick(pick), getRpsPick(nadekoPick))); } - - public enum RpsResult + else { - Win, - Loss, - Draw, + embed.WithErrorColor(); + amount = 0; + msg = GetText(strs.rps_win(ctx.Client.CurrentUser.Mention, getRpsPick(nadekoPick), getRpsPick(pick))); } - [NadekoCommand, Aliases] - public async Task Rps(RpsPick pick, ShmartNumber amount = default) - { - long oldAmount = amount; - if (!await CheckBetOptional(amount).ConfigureAwait(false) || (amount == 1)) - return; + embed + .WithDescription(msg); - string getRpsPick(RpsPick p) - { - switch (p) - { - case RpsPick.R: - return "🚀"; - case RpsPick.P: - return "📎"; - default: - return "✂️"; - } - } - var embed = _eb.Create(); - - var nadekoPick = (RpsPick)new NadekoRandom().Next(0, 3); - - if (amount > 0) - { - if (!await _cs.RemoveAsync(ctx.User.Id, - "Rps-bet", amount, gamble: true).ConfigureAwait(false)) - { - await ReplyErrorLocalizedAsync(strs.not_enough(CurrencySign)); - return; - } - } - - string msg; - if (pick == nadekoPick) - { - await _cs.AddAsync(ctx.User.Id, - "Rps-draw", amount, gamble: true).ConfigureAwait(false); - embed.WithOkColor(); - msg = GetText(strs.rps_draw(getRpsPick(pick))); - } - else if ((pick == RpsPick.Paper && nadekoPick == RpsPick.Rock) || - (pick == RpsPick.Rock && nadekoPick == RpsPick.Scissors) || - (pick == RpsPick.Scissors && nadekoPick == RpsPick.Paper)) - { - amount = (long)(amount * base._config.BetFlip.Multiplier); - await _cs.AddAsync(ctx.User.Id, - "Rps-win", amount, gamble: true).ConfigureAwait(false); - embed.WithOkColor(); - embed.AddField(GetText(strs.won), n(amount)); - msg = GetText(strs.rps_win(ctx.User.Mention, getRpsPick(pick), getRpsPick(nadekoPick))); - } - else - { - embed.WithErrorColor(); - amount = 0; - msg = GetText(strs.rps_win(ctx.Client.CurrentUser.Mention, getRpsPick(nadekoPick), getRpsPick(pick))); - } - - embed - .WithDescription(msg); - - await ctx.Channel.EmbedAsync(embed).ConfigureAwait(false); - } + await ctx.Channel.EmbedAsync(embed).ConfigureAwait(false); } -} +} \ No newline at end of file diff --git a/src/NadekoBot/Modules/Gambling/PlantAndPickCommands.cs b/src/NadekoBot/Modules/Gambling/PlantAndPickCommands.cs index a386b5cd1..17009ad15 100644 --- a/src/NadekoBot/Modules/Gambling/PlantAndPickCommands.cs +++ b/src/NadekoBot/Modules/Gambling/PlantAndPickCommands.cs @@ -5,121 +5,119 @@ using NadekoBot.Common.Attributes; using NadekoBot.Extensions; using NadekoBot.Modules.Administration.Services; using NadekoBot.Modules.Gambling.Services; -using System.Linq; using System.Threading.Tasks; using NadekoBot.Modules.Gambling.Common; using NadekoBot.Common; -namespace NadekoBot.Modules.Gambling +namespace NadekoBot.Modules.Gambling; + +public partial class Gambling { - public partial class Gambling + [Group] + public class PlantPickCommands : GamblingSubmodule { - [Group] - public class PlantPickCommands : GamblingSubmodule + private readonly ILogCommandService logService; + + public PlantPickCommands(ILogCommandService logService, GamblingConfigService gss) : base(gss) { - private readonly ILogCommandService logService; + this.logService = logService; + } - public PlantPickCommands(ILogCommandService logService, GamblingConfigService gss) : base(gss) + [NadekoCommand, Aliases] + [RequireContext(ContextType.Guild)] + public async Task Pick(string pass = null) + { + if (!string.IsNullOrWhiteSpace(pass) && !pass.IsAlphaNumeric()) { - this.logService = logService; + return; } - [NadekoCommand, Aliases] - [RequireContext(ContextType.Guild)] - public async Task Pick(string pass = null) + var picked = await _service.PickAsync(ctx.Guild.Id, (ITextChannel)ctx.Channel, ctx.User.Id, pass); + + if (picked > 0) { - if (!string.IsNullOrWhiteSpace(pass) && !pass.IsAlphaNumeric()) - { - return; - } - - var picked = await _service.PickAsync(ctx.Guild.Id, (ITextChannel)ctx.Channel, ctx.User.Id, pass); - - if (picked > 0) - { - var msg = await ReplyConfirmLocalizedAsync(strs.picked(picked + CurrencySign)); - msg.DeleteAfter(10); - } - - if (((SocketGuild)ctx.Guild).CurrentUser.GuildPermissions.ManageMessages) - { - try - { - logService.AddDeleteIgnore(ctx.Message.Id); - await ctx.Message.DeleteAsync().ConfigureAwait(false); - } - catch { } - } + var msg = await ReplyConfirmLocalizedAsync(strs.picked(picked + CurrencySign)); + msg.DeleteAfter(10); } - [NadekoCommand, Aliases] - [RequireContext(ContextType.Guild)] - public async Task Plant(ShmartNumber amount, string pass = null) + if (((SocketGuild)ctx.Guild).CurrentUser.GuildPermissions.ManageMessages) { - if (amount < 1) - return; - - if (!string.IsNullOrWhiteSpace(pass) && !pass.IsAlphaNumeric()) - { - return; - } - - if (((SocketGuild)ctx.Guild).CurrentUser.GuildPermissions.ManageMessages) + try { logService.AddDeleteIgnore(ctx.Message.Id); await ctx.Message.DeleteAsync().ConfigureAwait(false); } + catch { } + } + } - var success = await _service.PlantAsync(ctx.Guild.Id, ctx.Channel, ctx.User.Id, ctx.User.ToString(), amount, pass); - if (!success) - { - await ReplyErrorLocalizedAsync(strs.not_enough( CurrencySign)); - } + [NadekoCommand, Aliases] + [RequireContext(ContextType.Guild)] + public async Task Plant(ShmartNumber amount, string pass = null) + { + if (amount < 1) + return; + + if (!string.IsNullOrWhiteSpace(pass) && !pass.IsAlphaNumeric()) + { + return; } - [NadekoCommand, Aliases] - [RequireContext(ContextType.Guild)] - [UserPerm(GuildPerm.ManageMessages)] + if (((SocketGuild)ctx.Guild).CurrentUser.GuildPermissions.ManageMessages) + { + logService.AddDeleteIgnore(ctx.Message.Id); + await ctx.Message.DeleteAsync().ConfigureAwait(false); + } + + var success = await _service.PlantAsync(ctx.Guild.Id, ctx.Channel, ctx.User.Id, ctx.User.ToString(), amount, pass); + if (!success) + { + await ReplyErrorLocalizedAsync(strs.not_enough( CurrencySign)); + } + } + + [NadekoCommand, Aliases] + [RequireContext(ContextType.Guild)] + [UserPerm(GuildPerm.ManageMessages)] #if GLOBAL_NADEKO [OwnerOnly] #endif - public async Task GenCurrency() + public async Task GenCurrency() + { + bool enabled = _service.ToggleCurrencyGeneration(ctx.Guild.Id, ctx.Channel.Id); + if (enabled) { - bool enabled = _service.ToggleCurrencyGeneration(ctx.Guild.Id, ctx.Channel.Id); - if (enabled) - { - await ReplyConfirmLocalizedAsync(strs.curgen_enabled).ConfigureAwait(false); - } - else - { - await ReplyConfirmLocalizedAsync(strs.curgen_disabled).ConfigureAwait(false); - } + await ReplyConfirmLocalizedAsync(strs.curgen_enabled).ConfigureAwait(false); } - - [NadekoCommand, Aliases] - [RequireContext(ContextType.Guild)] - [UserPerm(GuildPerm.ManageMessages)] - [OwnerOnly] - public Task GenCurList(int page = 1) + else { - if (--page < 0) - return Task.CompletedTask; - var enabledIn = _service.GetAllGeneratingChannels(); - - return ctx.SendPaginatedConfirmAsync(page, (cur) => - { - var items = enabledIn.Skip(page * 9).Take(9); - - if (!items.Any()) - { - return _eb.Create().WithErrorColor() - .WithDescription("-"); - } - - return items.Aggregate(_eb.Create().WithOkColor(), - (eb, i) => eb.AddField(i.GuildId.ToString(), i.ChannelId)); - }, enabledIn.Count(), 9); + await ReplyConfirmLocalizedAsync(strs.curgen_disabled).ConfigureAwait(false); } } + + [NadekoCommand, Aliases] + [RequireContext(ContextType.Guild)] + [UserPerm(GuildPerm.ManageMessages)] + [OwnerOnly] + public Task GenCurList(int page = 1) + { + if (--page < 0) + return Task.CompletedTask; + var enabledIn = _service.GetAllGeneratingChannels(); + + return ctx.SendPaginatedConfirmAsync(page, (cur) => + { + var items = enabledIn.Skip(page * 9).Take(9); + + if (!items.Any()) + { + return _eb.Create().WithErrorColor() + .WithDescription("-"); + } + + return items.Aggregate(_eb.Create().WithOkColor(), + (eb, i) => eb.AddField(i.GuildId.ToString(), i.ChannelId)); + }, enabledIn.Count(), 9); + } } -} +} \ No newline at end of file diff --git a/src/NadekoBot/Modules/Gambling/Services/AnimalRaceService.cs b/src/NadekoBot/Modules/Gambling/Services/AnimalRaceService.cs index 8d3c6c520..e76ecc35f 100644 --- a/src/NadekoBot/Modules/Gambling/Services/AnimalRaceService.cs +++ b/src/NadekoBot/Modules/Gambling/Services/AnimalRaceService.cs @@ -1,12 +1,10 @@ -using System.Threading.Tasks; -using NadekoBot.Services; +using NadekoBot.Services; using System.Collections.Concurrent; using NadekoBot.Modules.Gambling.Common.AnimalRacing; -namespace NadekoBot.Modules.Gambling.Services +namespace NadekoBot.Modules.Gambling.Services; + +public class AnimalRaceService : INService { - public class AnimalRaceService : INService - { - public ConcurrentDictionary AnimalRaces { get; } = new ConcurrentDictionary(); - } -} + public ConcurrentDictionary AnimalRaces { get; } = new ConcurrentDictionary(); +} \ No newline at end of file diff --git a/src/NadekoBot/Modules/Gambling/Services/BlackJackService.cs b/src/NadekoBot/Modules/Gambling/Services/BlackJackService.cs index a6d80eae6..7bb5afcbe 100644 --- a/src/NadekoBot/Modules/Gambling/Services/BlackJackService.cs +++ b/src/NadekoBot/Modules/Gambling/Services/BlackJackService.cs @@ -2,10 +2,9 @@ using NadekoBot.Services; using System.Collections.Concurrent; -namespace NadekoBot.Modules.Gambling.Services +namespace NadekoBot.Modules.Gambling.Services; + +public class BlackJackService : INService { - public class BlackJackService : INService - { - public ConcurrentDictionary Games { get; } = new ConcurrentDictionary(); - } -} + public ConcurrentDictionary Games { get; } = new ConcurrentDictionary(); +} \ No newline at end of file diff --git a/src/NadekoBot/Modules/Gambling/Services/CurrencyEventsService.cs b/src/NadekoBot/Modules/Gambling/Services/CurrencyEventsService.cs index 3bd3ad1a2..db4f68aac 100644 --- a/src/NadekoBot/Modules/Gambling/Services/CurrencyEventsService.cs +++ b/src/NadekoBot/Modules/Gambling/Services/CurrencyEventsService.cs @@ -2,83 +2,77 @@ using NadekoBot.Modules.Gambling.Common.Events; using System.Collections.Concurrent; using NadekoBot.Modules.Gambling.Common; -using Discord; using Discord.WebSocket; using System.Threading.Tasks; -using System; using NadekoBot.Services.Database.Models; -using System.Net.Http; -using NadekoBot.Modules.Gambling.Services; -using Serilog; -namespace NadekoBot.Modules.Gambling.Services +namespace NadekoBot.Modules.Gambling.Services; + +public class CurrencyEventsService : INService { - public class CurrencyEventsService : INService + private readonly DiscordSocketClient _client; + private readonly ICurrencyService _cs; + private readonly GamblingConfigService _configService; + + private readonly ConcurrentDictionary _events = + new ConcurrentDictionary(); + + + public CurrencyEventsService( + DiscordSocketClient client, + ICurrencyService cs, + GamblingConfigService configService) { - private readonly DiscordSocketClient _client; - private readonly ICurrencyService _cs; - private readonly GamblingConfigService _configService; + _client = client; + _cs = cs; + _configService = configService; + } - private readonly ConcurrentDictionary _events = - new ConcurrentDictionary(); + public async Task TryCreateEventAsync(ulong guildId, ulong channelId, CurrencyEvent.Type type, + EventOptions opts, Func embed) + { + SocketGuild g = _client.GetGuild(guildId); + SocketTextChannel ch = g?.GetChannel(channelId) as SocketTextChannel; + if (ch is null) + return false; + ICurrencyEvent ce; - public CurrencyEventsService( - DiscordSocketClient client, - ICurrencyService cs, - GamblingConfigService configService) + if (type == CurrencyEvent.Type.Reaction) { - _client = client; - _cs = cs; - _configService = configService; + ce = new ReactionEvent(_client, _cs, g, ch, opts, _configService.Data, embed); + } + else if (type == CurrencyEvent.Type.GameStatus) + { + ce = new GameStatusEvent(_client, _cs, g, ch, opts, embed); + } + else + { + return false; } - public async Task TryCreateEventAsync(ulong guildId, ulong channelId, CurrencyEvent.Type type, - EventOptions opts, Func embed) + var added = _events.TryAdd(guildId, ce); + if (added) { - SocketGuild g = _client.GetGuild(guildId); - SocketTextChannel ch = g?.GetChannel(channelId) as SocketTextChannel; - if (ch is null) - return false; - - ICurrencyEvent ce; - - if (type == CurrencyEvent.Type.Reaction) + try { - ce = new ReactionEvent(_client, _cs, g, ch, opts, _configService.Data, embed); + ce.OnEnded += OnEventEnded; + await ce.StartEvent().ConfigureAwait(false); } - else if (type == CurrencyEvent.Type.GameStatus) - { - ce = new GameStatusEvent(_client, _cs, g, ch, opts, embed); - } - else + catch (Exception ex) { + Log.Warning(ex, "Error starting event"); + _events.TryRemove(guildId, out ce); return false; } - - var added = _events.TryAdd(guildId, ce); - if (added) - { - try - { - ce.OnEnded += OnEventEnded; - await ce.StartEvent().ConfigureAwait(false); - } - catch (Exception ex) - { - Log.Warning(ex, "Error starting event"); - _events.TryRemove(guildId, out ce); - return false; - } - } - - return added; } - private Task OnEventEnded(ulong gid) - { - _events.TryRemove(gid, out _); - return Task.CompletedTask; - } + return added; + } + + private Task OnEventEnded(ulong gid) + { + _events.TryRemove(gid, out _); + return Task.CompletedTask; } } \ No newline at end of file diff --git a/src/NadekoBot/Modules/Gambling/Services/CurrencyRaffleService.cs b/src/NadekoBot/Modules/Gambling/Services/CurrencyRaffleService.cs index 97f52a215..371c77c65 100644 --- a/src/NadekoBot/Modules/Gambling/Services/CurrencyRaffleService.cs +++ b/src/NadekoBot/Modules/Gambling/Services/CurrencyRaffleService.cs @@ -2,87 +2,83 @@ using NadekoBot.Services; using NadekoBot.Modules.Gambling.Common; using System.Threading; -using System.Linq; -using System.Collections.Generic; using Discord; -using System; -namespace NadekoBot.Modules.Gambling.Services +namespace NadekoBot.Modules.Gambling.Services; + +public class CurrencyRaffleService : INService { - public class CurrencyRaffleService : INService + public enum JoinErrorType { - public enum JoinErrorType - { - NotEnoughCurrency, - AlreadyJoinedOrInvalidAmount - } - private readonly SemaphoreSlim _locker = new SemaphoreSlim(1, 1); - private readonly DbService _db; - private readonly ICurrencyService _cs; + NotEnoughCurrency, + AlreadyJoinedOrInvalidAmount + } + private readonly SemaphoreSlim _locker = new SemaphoreSlim(1, 1); + private readonly DbService _db; + private readonly ICurrencyService _cs; - public Dictionary Games { get; } = new Dictionary(); + public Dictionary Games { get; } = new Dictionary(); - public CurrencyRaffleService(DbService db, ICurrencyService cs) - { - _db = db; - _cs = cs; - } + public CurrencyRaffleService(DbService db, ICurrencyService cs) + { + _db = db; + _cs = cs; + } - public async Task<(CurrencyRaffleGame, JoinErrorType?)> JoinOrCreateGame(ulong channelId, IUser user, long amount, bool mixed, Func onEnded) + public async Task<(CurrencyRaffleGame, JoinErrorType?)> JoinOrCreateGame(ulong channelId, IUser user, long amount, bool mixed, Func onEnded) + { + await _locker.WaitAsync().ConfigureAwait(false); + try { - await _locker.WaitAsync().ConfigureAwait(false); - try + var newGame = false; + if (!Games.TryGetValue(channelId, out var crg)) { - var newGame = false; - if (!Games.TryGetValue(channelId, out var crg)) - { - newGame = true; - crg = new CurrencyRaffleGame(mixed - ? CurrencyRaffleGame.Type.Mixed - : CurrencyRaffleGame.Type.Normal); - Games.Add(channelId, crg); - } + newGame = true; + crg = new CurrencyRaffleGame(mixed + ? CurrencyRaffleGame.Type.Mixed + : CurrencyRaffleGame.Type.Normal); + Games.Add(channelId, crg); + } - //remove money, and stop the game if this - // user created it and doesn't have the money - if (!await _cs.RemoveAsync(user.Id, "Currency Raffle Join", amount).ConfigureAwait(false)) - { - if (newGame) - Games.Remove(channelId); - return (null, JoinErrorType.NotEnoughCurrency); - } - - if (!crg.AddUser(user, amount)) - { - await _cs.AddAsync(user.Id, "Curency Raffle Refund", amount).ConfigureAwait(false); - return (null, JoinErrorType.AlreadyJoinedOrInvalidAmount); - } + //remove money, and stop the game if this + // user created it and doesn't have the money + if (!await _cs.RemoveAsync(user.Id, "Currency Raffle Join", amount).ConfigureAwait(false)) + { if (newGame) - { - var _t = Task.Run(async () => - { - await Task.Delay(60000).ConfigureAwait(false); - await _locker.WaitAsync().ConfigureAwait(false); - try - { - var winner = crg.GetWinner(); - var won = crg.Users.Sum(x => x.Amount); + Games.Remove(channelId); + return (null, JoinErrorType.NotEnoughCurrency); + } - await _cs.AddAsync(winner.DiscordUser.Id, "Currency Raffle Win", - won).ConfigureAwait(false); - Games.Remove(channelId, out _); - var oe = onEnded(winner.DiscordUser, won); - } - catch { } - finally { _locker.Release(); } - }); - } - return (crg, null); - } - finally + if (!crg.AddUser(user, amount)) { - _locker.Release(); + await _cs.AddAsync(user.Id, "Curency Raffle Refund", amount).ConfigureAwait(false); + return (null, JoinErrorType.AlreadyJoinedOrInvalidAmount); } + if (newGame) + { + var _t = Task.Run(async () => + { + await Task.Delay(60000).ConfigureAwait(false); + await _locker.WaitAsync().ConfigureAwait(false); + try + { + var winner = crg.GetWinner(); + var won = crg.Users.Sum(x => x.Amount); + + await _cs.AddAsync(winner.DiscordUser.Id, "Currency Raffle Win", + won).ConfigureAwait(false); + Games.Remove(channelId, out _); + var oe = onEnded(winner.DiscordUser, won); + } + catch { } + finally { _locker.Release(); } + }); + } + return (crg, null); + } + finally + { + _locker.Release(); } } } \ No newline at end of file diff --git a/src/NadekoBot/Modules/Gambling/Services/GamblingConfigService.cs b/src/NadekoBot/Modules/Gambling/Services/GamblingConfigService.cs index 7b728f2d9..1b0f9237e 100644 --- a/src/NadekoBot/Modules/Gambling/Services/GamblingConfigService.cs +++ b/src/NadekoBot/Modules/Gambling/Services/GamblingConfigService.cs @@ -1,85 +1,80 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using NadekoBot.Common; +using NadekoBot.Common; using NadekoBot.Common.Configs; using NadekoBot.Modules.Gambling.Common; using NadekoBot.Services; -using NadekoBot.Services.Database.Models; -namespace NadekoBot.Modules.Gambling.Services +namespace NadekoBot.Modules.Gambling.Services; + +public sealed class GamblingConfigService : ConfigServiceBase { - public sealed class GamblingConfigService : ConfigServiceBase - { - public override string Name { get; } = "gambling"; - private const string FilePath = "data/gambling.yml"; - private static TypedKey changeKey = new TypedKey("config.gambling.updated"); + public override string Name { get; } = "gambling"; + private const string FilePath = "data/gambling.yml"; + private static TypedKey changeKey = new TypedKey("config.gambling.updated"); - public GamblingConfigService(IConfigSeria serializer, IPubSub pubSub) - : base(FilePath, serializer, pubSub, changeKey) - { - AddParsedProp("currency.name", gs => gs.Currency.Name, ConfigParsers.String, ConfigPrinters.ToString); - AddParsedProp("currency.sign", gs => gs.Currency.Sign, ConfigParsers.String, ConfigPrinters.ToString); + public GamblingConfigService(IConfigSeria serializer, IPubSub pubSub) + : base(FilePath, serializer, pubSub, changeKey) + { + AddParsedProp("currency.name", gs => gs.Currency.Name, ConfigParsers.String, ConfigPrinters.ToString); + AddParsedProp("currency.sign", gs => gs.Currency.Sign, ConfigParsers.String, ConfigPrinters.ToString); - AddParsedProp("minbet", gs => gs.MinBet, int.TryParse, ConfigPrinters.ToString, val => val >= 0); - AddParsedProp("maxbet", gs => gs.MaxBet, int.TryParse, ConfigPrinters.ToString, val => val >= 0); + AddParsedProp("minbet", gs => gs.MinBet, int.TryParse, ConfigPrinters.ToString, val => val >= 0); + AddParsedProp("maxbet", gs => gs.MaxBet, int.TryParse, ConfigPrinters.ToString, val => val >= 0); - AddParsedProp("gen.min", gs => gs.Generation.MinAmount, int.TryParse, ConfigPrinters.ToString, val => val >= 1); - AddParsedProp("gen.max", gs => gs.Generation.MaxAmount, int.TryParse, ConfigPrinters.ToString, val => val >= 1); - AddParsedProp("gen.cd", gs => gs.Generation.GenCooldown, int.TryParse, ConfigPrinters.ToString, val => val > 0); - AddParsedProp("gen.chance", gs => gs.Generation.Chance, decimal.TryParse, ConfigPrinters.ToString, val => val >= 0 && val <= 1); - AddParsedProp("gen.has_pw", gs => gs.Generation.HasPassword, bool.TryParse, ConfigPrinters.ToString); - AddParsedProp("bf.multi", gs => gs.BetFlip.Multiplier, decimal.TryParse, ConfigPrinters.ToString, val => val >= 1); - AddParsedProp("waifu.min_price", gs => gs.Waifu.MinPrice, int.TryParse, ConfigPrinters.ToString, val => val >= 0); - AddParsedProp("waifu.multi.reset", gs => gs.Waifu.Multipliers.WaifuReset, int.TryParse, ConfigPrinters.ToString, val => val >= 0); - AddParsedProp("waifu.multi.crush_claim", gs => gs.Waifu.Multipliers.CrushClaim, decimal.TryParse, ConfigPrinters.ToString, val => val >= 0); - AddParsedProp("waifu.multi.normal_claim", gs => gs.Waifu.Multipliers.NormalClaim, decimal.TryParse, ConfigPrinters.ToString, val => val > 0); - AddParsedProp("waifu.multi.divorce_value", gs => gs.Waifu.Multipliers.DivorceNewValue, decimal.TryParse, ConfigPrinters.ToString, val => val > 0); - AddParsedProp("waifu.multi.all_gifts", gs => gs.Waifu.Multipliers.AllGiftPrices, decimal.TryParse, ConfigPrinters.ToString, val => val > 0); - AddParsedProp("waifu.multi.gift_effect", gs => gs.Waifu.Multipliers.GiftEffect, decimal.TryParse, ConfigPrinters.ToString, val => val >= 0); - AddParsedProp("waifu.multi.negative_gift_effect", gs => gs.Waifu.Multipliers.NegativeGiftEffect, decimal.TryParse, ConfigPrinters.ToString, val => val >= 0); - AddParsedProp("decay.percent", gs => gs.Decay.Percent, decimal.TryParse, ConfigPrinters.ToString, val => val >= 0 && val <= 1); - AddParsedProp("decay.maxdecay", gs => gs.Decay.MaxDecay, int.TryParse, ConfigPrinters.ToString, val => val >= 0); - AddParsedProp("decay.threshold", gs => gs.Decay.MinThreshold, int.TryParse, ConfigPrinters.ToString, val => val >= 0); + AddParsedProp("gen.min", gs => gs.Generation.MinAmount, int.TryParse, ConfigPrinters.ToString, val => val >= 1); + AddParsedProp("gen.max", gs => gs.Generation.MaxAmount, int.TryParse, ConfigPrinters.ToString, val => val >= 1); + AddParsedProp("gen.cd", gs => gs.Generation.GenCooldown, int.TryParse, ConfigPrinters.ToString, val => val > 0); + AddParsedProp("gen.chance", gs => gs.Generation.Chance, decimal.TryParse, ConfigPrinters.ToString, val => val >= 0 && val <= 1); + AddParsedProp("gen.has_pw", gs => gs.Generation.HasPassword, bool.TryParse, ConfigPrinters.ToString); + AddParsedProp("bf.multi", gs => gs.BetFlip.Multiplier, decimal.TryParse, ConfigPrinters.ToString, val => val >= 1); + AddParsedProp("waifu.min_price", gs => gs.Waifu.MinPrice, int.TryParse, ConfigPrinters.ToString, val => val >= 0); + AddParsedProp("waifu.multi.reset", gs => gs.Waifu.Multipliers.WaifuReset, int.TryParse, ConfigPrinters.ToString, val => val >= 0); + AddParsedProp("waifu.multi.crush_claim", gs => gs.Waifu.Multipliers.CrushClaim, decimal.TryParse, ConfigPrinters.ToString, val => val >= 0); + AddParsedProp("waifu.multi.normal_claim", gs => gs.Waifu.Multipliers.NormalClaim, decimal.TryParse, ConfigPrinters.ToString, val => val > 0); + AddParsedProp("waifu.multi.divorce_value", gs => gs.Waifu.Multipliers.DivorceNewValue, decimal.TryParse, ConfigPrinters.ToString, val => val > 0); + AddParsedProp("waifu.multi.all_gifts", gs => gs.Waifu.Multipliers.AllGiftPrices, decimal.TryParse, ConfigPrinters.ToString, val => val > 0); + AddParsedProp("waifu.multi.gift_effect", gs => gs.Waifu.Multipliers.GiftEffect, decimal.TryParse, ConfigPrinters.ToString, val => val >= 0); + AddParsedProp("waifu.multi.negative_gift_effect", gs => gs.Waifu.Multipliers.NegativeGiftEffect, decimal.TryParse, ConfigPrinters.ToString, val => val >= 0); + AddParsedProp("decay.percent", gs => gs.Decay.Percent, decimal.TryParse, ConfigPrinters.ToString, val => val >= 0 && val <= 1); + AddParsedProp("decay.maxdecay", gs => gs.Decay.MaxDecay, int.TryParse, ConfigPrinters.ToString, val => val >= 0); + AddParsedProp("decay.threshold", gs => gs.Decay.MinThreshold, int.TryParse, ConfigPrinters.ToString, val => val >= 0); - Migrate(); + Migrate(); + } + + private readonly IEnumerable antiGiftSeed = new[] + { + new WaifuItemModel("🥀", 100, "WiltedRose", true), + new WaifuItemModel("✂️", 1000, "Haircut", true), + new WaifuItemModel("🧻", 10000, "ToiletPaper", true), + }; + + public void Migrate() + { + if (_data.Version < 2) + { + ModifyConfig(c => + { + c.Waifu.Items = c.Waifu.Items.Concat(antiGiftSeed).ToList(); + c.Version = 2; + }); } - private readonly IEnumerable antiGiftSeed = new[] + if (_data.Version < 3) { - new WaifuItemModel("🥀", 100, "WiltedRose", true), - new WaifuItemModel("✂️", 1000, "Haircut", true), - new WaifuItemModel("🧻", 10000, "ToiletPaper", true), - }; - - public void Migrate() + ModifyConfig(c => + { + c.Version = 3; + c.VoteReward = 100; + }); + } + + if (_data.Version < 4) { - if (_data.Version < 2) + ModifyConfig(c => { - ModifyConfig(c => - { - c.Waifu.Items = c.Waifu.Items.Concat(antiGiftSeed).ToList(); - c.Version = 2; - }); - } - - if (_data.Version < 3) - { - ModifyConfig(c => - { - c.Version = 3; - c.VoteReward = 100; - }); - } - - if (_data.Version < 4) - { - ModifyConfig(c => - { - c.Version = 4; - }); - } + c.Version = 4; + }); } } } \ No newline at end of file diff --git a/src/NadekoBot/Modules/Gambling/Services/GamblingService.cs b/src/NadekoBot/Modules/Gambling/Services/GamblingService.cs index 8286f3599..683c605d1 100644 --- a/src/NadekoBot/Modules/Gambling/Services/GamblingService.cs +++ b/src/NadekoBot/Modules/Gambling/Services/GamblingService.cs @@ -4,69 +4,63 @@ using NadekoBot.Services; using NadekoBot.Modules.Gambling.Common.Connect4; using NadekoBot.Modules.Gambling.Common.WheelOfFortune; using Newtonsoft.Json; -using System; using System.Collections.Concurrent; -using System.Collections.Generic; -using System.Linq; using System.Threading; using System.Threading.Tasks; using Microsoft.EntityFrameworkCore; -using NadekoBot.Common; using NadekoBot.Db; using NadekoBot.Modules.Gambling.Common.Slot; -using NadekoBot.Modules.Gambling.Services; -using Serilog; -namespace NadekoBot.Modules.Gambling.Services +namespace NadekoBot.Modules.Gambling.Services; + +public class GamblingService : INService { - public class GamblingService : INService + private readonly DbService _db; + private readonly ICurrencyService _cs; + private readonly Bot _bot; + private readonly DiscordSocketClient _client; + private readonly IDataCache _cache; + private readonly GamblingConfigService _gss; + + public ConcurrentDictionary<(ulong, ulong), RollDuelGame> Duels { get; } = new ConcurrentDictionary<(ulong, ulong), RollDuelGame>(); + public ConcurrentDictionary Connect4Games { get; } = new ConcurrentDictionary(); + + private readonly Timer _decayTimer; + + public GamblingService(DbService db, Bot bot, ICurrencyService cs, + DiscordSocketClient client, IDataCache cache, GamblingConfigService gss) { - private readonly DbService _db; - private readonly ICurrencyService _cs; - private readonly Bot _bot; - private readonly DiscordSocketClient _client; - private readonly IDataCache _cache; - private readonly GamblingConfigService _gss; - - public ConcurrentDictionary<(ulong, ulong), RollDuelGame> Duels { get; } = new ConcurrentDictionary<(ulong, ulong), RollDuelGame>(); - public ConcurrentDictionary Connect4Games { get; } = new ConcurrentDictionary(); - - private readonly Timer _decayTimer; - - public GamblingService(DbService db, Bot bot, ICurrencyService cs, - DiscordSocketClient client, IDataCache cache, GamblingConfigService gss) - { - _db = db; - _cs = cs; - _bot = bot; - _client = client; - _cache = cache; - _gss = gss; + _db = db; + _cs = cs; + _bot = bot; + _client = client; + _cache = cache; + _gss = gss; - if (_bot.Client.ShardId == 0) + if (_bot.Client.ShardId == 0) + { + _decayTimer = new Timer(_ => { - _decayTimer = new Timer(_ => - { - var config = _gss.Data; - var maxDecay = config.Decay.MaxDecay; - if (config.Decay.Percent <= 0 || config.Decay.Percent > 1 || maxDecay < 0) - return; + var config = _gss.Data; + var maxDecay = config.Decay.MaxDecay; + if (config.Decay.Percent <= 0 || config.Decay.Percent > 1 || maxDecay < 0) + return; - using (var uow = _db.GetDbContext()) - { - var lastCurrencyDecay = _cache.GetLastCurrencyDecay(); + using (var uow = _db.GetDbContext()) + { + var lastCurrencyDecay = _cache.GetLastCurrencyDecay(); - if (DateTime.UtcNow - lastCurrencyDecay < TimeSpan.FromHours(config.Decay.HourInterval)) - return; + if (DateTime.UtcNow - lastCurrencyDecay < TimeSpan.FromHours(config.Decay.HourInterval)) + return; - Log.Information($"Decaying users' currency - decay: {config.Decay.Percent * 100}% " + - $"| max: {maxDecay} " + - $"| threshold: {config.Decay.MinThreshold}"); + Log.Information($"Decaying users' currency - decay: {config.Decay.Percent * 100}% " + + $"| max: {maxDecay} " + + $"| threshold: {config.Decay.MinThreshold}"); - if (maxDecay == 0) - maxDecay = int.MaxValue; + if (maxDecay == 0) + maxDecay = int.MaxValue; - uow.Database.ExecuteSqlInterpolated($@" + uow.Database.ExecuteSqlInterpolated($@" UPDATE DiscordUser SET CurrencyAmount= CASE WHEN @@ -78,99 +72,98 @@ SET CurrencyAmount= END WHERE CurrencyAmount > {config.Decay.MinThreshold} AND UserId!={_client.CurrentUser.Id};"); - _cache.SetLastCurrencyDecay(); - uow.SaveChanges(); - } - }, null, TimeSpan.FromMinutes(5), TimeSpan.FromMinutes(5)); - } - } - - public async Task SlotAsync(ulong userId, long amount) - { - var takeRes = await _cs.RemoveAsync(userId, "Slot Machine", amount, true); - - if (!takeRes) - { - return new SlotResponse - { - Error = GamblingError.NotEnough - }; - } - - var game = new SlotGame(); - var result = game.Spin(); - long won = 0; - - if (result.Multiplier > 0) - { - won = (long)(result.Multiplier * amount); - - await _cs.AddAsync(userId, $"Slot Machine x{result.Multiplier}", won, true); - } - - var toReturn = new SlotResponse - { - Multiplier = result.Multiplier, - Won = won, - }; - - toReturn.Rolls.AddRange(result.Rolls); - - return toReturn; - } - - - public struct EconomyResult - { - public decimal Cash { get; set; } - public decimal Planted { get; set; } - public decimal Waifus { get; set; } - public decimal OnePercent { get; set; } - public long Bot { get; set; } - } - - public EconomyResult GetEconomy() - { - if (_cache.TryGetEconomy(out var data)) - { - try - { - return JsonConvert.DeserializeObject(data); + _cache.SetLastCurrencyDecay(); + uow.SaveChanges(); } - catch { } - } - - decimal cash; - decimal onePercent; - decimal planted; - decimal waifus; - long bot; - - using (var uow = _db.GetDbContext()) - { - cash = uow.DiscordUser.GetTotalCurrency(); - onePercent = uow.DiscordUser.GetTopOnePercentCurrency(_client.CurrentUser.Id); - planted = uow.PlantedCurrency.AsQueryable().Sum(x => x.Amount); - waifus = uow.WaifuInfo.GetTotalValue(); - bot = uow.DiscordUser.GetUserCurrency(_client.CurrentUser.Id); - } - - var result = new EconomyResult - { - Cash = cash, - Planted = planted, - Bot = bot, - Waifus = waifus, - OnePercent = onePercent, - }; - - _cache.SetEconomy(JsonConvert.SerializeObject(result)); - return result; - } - - public Task WheelOfFortuneSpinAsync(ulong userId, long bet) - { - return new WheelOfFortuneGame(userId, bet, _gss.Data, _cs).SpinAsync(); + }, null, TimeSpan.FromMinutes(5), TimeSpan.FromMinutes(5)); } } -} + + public async Task SlotAsync(ulong userId, long amount) + { + var takeRes = await _cs.RemoveAsync(userId, "Slot Machine", amount, true); + + if (!takeRes) + { + return new SlotResponse + { + Error = GamblingError.NotEnough + }; + } + + var game = new SlotGame(); + var result = game.Spin(); + long won = 0; + + if (result.Multiplier > 0) + { + won = (long)(result.Multiplier * amount); + + await _cs.AddAsync(userId, $"Slot Machine x{result.Multiplier}", won, true); + } + + var toReturn = new SlotResponse + { + Multiplier = result.Multiplier, + Won = won, + }; + + toReturn.Rolls.AddRange(result.Rolls); + + return toReturn; + } + + + public struct EconomyResult + { + public decimal Cash { get; set; } + public decimal Planted { get; set; } + public decimal Waifus { get; set; } + public decimal OnePercent { get; set; } + public long Bot { get; set; } + } + + public EconomyResult GetEconomy() + { + if (_cache.TryGetEconomy(out var data)) + { + try + { + return JsonConvert.DeserializeObject(data); + } + catch { } + } + + decimal cash; + decimal onePercent; + decimal planted; + decimal waifus; + long bot; + + using (var uow = _db.GetDbContext()) + { + cash = uow.DiscordUser.GetTotalCurrency(); + onePercent = uow.DiscordUser.GetTopOnePercentCurrency(_client.CurrentUser.Id); + planted = uow.PlantedCurrency.AsQueryable().Sum(x => x.Amount); + waifus = uow.WaifuInfo.GetTotalValue(); + bot = uow.DiscordUser.GetUserCurrency(_client.CurrentUser.Id); + } + + var result = new EconomyResult + { + Cash = cash, + Planted = planted, + Bot = bot, + Waifus = waifus, + OnePercent = onePercent, + }; + + _cache.SetEconomy(JsonConvert.SerializeObject(result)); + return result; + } + + public Task WheelOfFortuneSpinAsync(ulong userId, long bet) + { + return new WheelOfFortuneGame(userId, bet, _gss.Data, _cs).SpinAsync(); + } +} \ No newline at end of file diff --git a/src/NadekoBot/Modules/Gambling/Services/IShopService.cs b/src/NadekoBot/Modules/Gambling/Services/IShopService.cs index e9b33d67b..b261013f1 100644 --- a/src/NadekoBot/Modules/Gambling/Services/IShopService.cs +++ b/src/NadekoBot/Modules/Gambling/Services/IShopService.cs @@ -1,43 +1,42 @@ using System.Threading.Tasks; -namespace NadekoBot.Modules.Gambling.Services +namespace NadekoBot.Modules.Gambling.Services; + +public interface IShopService { - public interface IShopService - { - /// - /// Changes the price of a shop item - /// - /// Id of the guild in which the shop is - /// Index of the item - /// New item price - /// Success status - Task ChangeEntryPriceAsync(ulong guildId, int index, int newPrice); + /// + /// Changes the price of a shop item + /// + /// Id of the guild in which the shop is + /// Index of the item + /// New item price + /// Success status + Task ChangeEntryPriceAsync(ulong guildId, int index, int newPrice); - /// - /// Changes the name of a shop item - /// - /// Id of the guild in which the shop is - /// Index of the item - /// New item name - /// Success status - Task ChangeEntryNameAsync(ulong guildId, int index, string newName); + /// + /// Changes the name of a shop item + /// + /// Id of the guild in which the shop is + /// Index of the item + /// New item name + /// Success status + Task ChangeEntryNameAsync(ulong guildId, int index, string newName); - /// - /// Swaps indexes of 2 items in the shop - /// - /// Id of the guild in which the shop is - /// First entry's index - /// Second entry's index - /// Whether swap was successful - Task SwapEntriesAsync(ulong guildId, int index1, int index2); + /// + /// Swaps indexes of 2 items in the shop + /// + /// Id of the guild in which the shop is + /// First entry's index + /// Second entry's index + /// Whether swap was successful + Task SwapEntriesAsync(ulong guildId, int index1, int index2); - /// - /// Swaps indexes of 2 items in the shop - /// - /// Id of the guild in which the shop is - /// Current index of the entry to move - /// Destination index of the entry - /// Whether swap was successful - Task MoveEntryAsync(ulong guildId, int fromIndex, int toIndex); - } + /// + /// Swaps indexes of 2 items in the shop + /// + /// Id of the guild in which the shop is + /// Current index of the entry to move + /// Destination index of the entry + /// Whether swap was successful + Task MoveEntryAsync(ulong guildId, int fromIndex, int toIndex); } \ No newline at end of file diff --git a/src/NadekoBot/Modules/Gambling/Services/Impl/ShopService.cs b/src/NadekoBot/Modules/Gambling/Services/Impl/ShopService.cs index 49fa41fca..da2c0113d 100644 --- a/src/NadekoBot/Modules/Gambling/Services/Impl/ShopService.cs +++ b/src/NadekoBot/Modules/Gambling/Services/Impl/ShopService.cs @@ -1,5 +1,4 @@ -using System; -using System.Threading.Tasks; +using System.Threading.Tasks; using Microsoft.EntityFrameworkCore; using NadekoBot.Common.Collections; using NadekoBot.Services; @@ -7,102 +6,100 @@ using NadekoBot.Services.Database; using NadekoBot.Services.Database.Models; using NadekoBot.Db; using NadekoBot.Extensions; -using NadekoBot.Modules.Administration; -namespace NadekoBot.Modules.Gambling.Services +namespace NadekoBot.Modules.Gambling.Services; + +public class ShopService : IShopService, INService { - public class ShopService : IShopService, INService + private readonly DbService _db; + + public ShopService(DbService db) { - private readonly DbService _db; + _db = db; + } - public ShopService(DbService db) - { - _db = db; - } + private IndexedCollection GetEntriesInternal(NadekoContext uow, ulong guildId) => + uow.GuildConfigsForId( + guildId, + set => set.Include(x => x.ShopEntries).ThenInclude(x => x.Items) + ) + .ShopEntries + .ToIndexed(); - private IndexedCollection GetEntriesInternal(NadekoContext uow, ulong guildId) => - uow.GuildConfigsForId( - guildId, - set => set.Include(x => x.ShopEntries).ThenInclude(x => x.Items) - ) - .ShopEntries - .ToIndexed(); + public async Task ChangeEntryPriceAsync(ulong guildId, int index, int newPrice) + { + if (index < 0) + throw new ArgumentOutOfRangeException(nameof(index)); + if (newPrice <= 0) + throw new ArgumentOutOfRangeException(nameof(newPrice)); - public async Task ChangeEntryPriceAsync(ulong guildId, int index, int newPrice) - { - if (index < 0) - throw new ArgumentOutOfRangeException(nameof(index)); - if (newPrice <= 0) - throw new ArgumentOutOfRangeException(nameof(newPrice)); + using var uow = _db.GetDbContext(); + var entries = GetEntriesInternal(uow, guildId); - using var uow = _db.GetDbContext(); - var entries = GetEntriesInternal(uow, guildId); + if (index >= entries.Count) + return false; - if (index >= entries.Count) - return false; + entries[index].Price = newPrice; + await uow.SaveChangesAsync(); + return true; + } - entries[index].Price = newPrice; - await uow.SaveChangesAsync(); - return true; - } + public async Task ChangeEntryNameAsync(ulong guildId, int index, string newName) + { + if (index < 0) + throw new ArgumentOutOfRangeException(nameof(index)); + if (string.IsNullOrWhiteSpace(newName)) + throw new ArgumentNullException(nameof(newName)); - public async Task ChangeEntryNameAsync(ulong guildId, int index, string newName) - { - if (index < 0) - throw new ArgumentOutOfRangeException(nameof(index)); - if (string.IsNullOrWhiteSpace(newName)) - throw new ArgumentNullException(nameof(newName)); + using var uow = _db.GetDbContext(); + var entries = GetEntriesInternal(uow, guildId); - using var uow = _db.GetDbContext(); - var entries = GetEntriesInternal(uow, guildId); + if (index >= entries.Count) + return false; - if (index >= entries.Count) - return false; + entries[index].Name = newName.TrimTo(100); + await uow.SaveChangesAsync(); + return true; + } - entries[index].Name = newName.TrimTo(100); - await uow.SaveChangesAsync(); - return true; - } + public async Task SwapEntriesAsync(ulong guildId, int index1, int index2) + { + if (index1 < 0) + throw new ArgumentOutOfRangeException(nameof(index1)); + if (index2 < 0) + throw new ArgumentOutOfRangeException(nameof(index2)); - public async Task SwapEntriesAsync(ulong guildId, int index1, int index2) - { - if (index1 < 0) - throw new ArgumentOutOfRangeException(nameof(index1)); - if (index2 < 0) - throw new ArgumentOutOfRangeException(nameof(index2)); + using var uow = _db.GetDbContext(); + var entries = GetEntriesInternal(uow, guildId); - using var uow = _db.GetDbContext(); - var entries = GetEntriesInternal(uow, guildId); + if (index1 >= entries.Count || index2 >= entries.Count || index1 == index2) + return false; - if (index1 >= entries.Count || index2 >= entries.Count || index1 == index2) - return false; + entries[index1].Index = index2; + entries[index2].Index = index1; - entries[index1].Index = index2; - entries[index2].Index = index1; + await uow.SaveChangesAsync(); + return true; + } - await uow.SaveChangesAsync(); - return true; - } + public async Task MoveEntryAsync(ulong guildId, int fromIndex, int toIndex) + { + if (fromIndex < 0) + throw new ArgumentOutOfRangeException(nameof(fromIndex)); + if (toIndex < 0) + throw new ArgumentOutOfRangeException(nameof(toIndex)); - public async Task MoveEntryAsync(ulong guildId, int fromIndex, int toIndex) - { - if (fromIndex < 0) - throw new ArgumentOutOfRangeException(nameof(fromIndex)); - if (toIndex < 0) - throw new ArgumentOutOfRangeException(nameof(toIndex)); + using var uow = _db.GetDbContext(); + var entries = GetEntriesInternal(uow, guildId); - using var uow = _db.GetDbContext(); - var entries = GetEntriesInternal(uow, guildId); + if (fromIndex >= entries.Count || toIndex >= entries.Count || fromIndex == toIndex) + return false; - if (fromIndex >= entries.Count || toIndex >= entries.Count || fromIndex == toIndex) - return false; + var entry = entries[fromIndex]; + entries.RemoveAt(fromIndex); + entries.Insert(toIndex, entry); - var entry = entries[fromIndex]; - entries.RemoveAt(fromIndex); - entries.Insert(toIndex, entry); - - await uow.SaveChangesAsync(); - return true; - } + await uow.SaveChangesAsync(); + return true; } } \ No newline at end of file diff --git a/src/NadekoBot/Modules/Gambling/Services/PlantPickService.cs b/src/NadekoBot/Modules/Gambling/Services/PlantPickService.cs index a4f9bd2f1..e449b6765 100644 --- a/src/NadekoBot/Modules/Gambling/Services/PlantPickService.cs +++ b/src/NadekoBot/Modules/Gambling/Services/PlantPickService.cs @@ -11,373 +11,369 @@ using SixLabors.ImageSharp; using SixLabors.ImageSharp.Drawing.Processing; using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.Processing; -using System; using System.Collections.Concurrent; -using System.Collections.Generic; using System.IO; -using System.Linq; using System.Threading; using System.Threading.Tasks; using NadekoBot.Db; using Image = SixLabors.ImageSharp.Image; using Color = SixLabors.ImageSharp.Color; -namespace NadekoBot.Modules.Gambling.Services +namespace NadekoBot.Modules.Gambling.Services; + +public class PlantPickService : INService { - public class PlantPickService : INService + private readonly DbService _db; + private readonly IBotStrings _strings; + private readonly IImageCache _images; + private readonly FontProvider _fonts; + private readonly ICurrencyService _cs; + private readonly CommandHandler _cmdHandler; + private readonly NadekoRandom _rng; + private readonly DiscordSocketClient _client; + private readonly GamblingConfigService _gss; + + public readonly ConcurrentHashSet _generationChannels = new ConcurrentHashSet(); + //channelId/last generation + public ConcurrentDictionary LastGenerations { get; } = new ConcurrentDictionary(); + private readonly SemaphoreSlim pickLock = new SemaphoreSlim(1, 1); + + public PlantPickService(DbService db, CommandHandler cmd, IBotStrings strings, + IDataCache cache, FontProvider fonts, ICurrencyService cs, + CommandHandler cmdHandler, DiscordSocketClient client, GamblingConfigService gss) { - private readonly DbService _db; - private readonly IBotStrings _strings; - private readonly IImageCache _images; - private readonly FontProvider _fonts; - private readonly ICurrencyService _cs; - private readonly CommandHandler _cmdHandler; - private readonly NadekoRandom _rng; - private readonly DiscordSocketClient _client; - private readonly GamblingConfigService _gss; + _db = db; + _strings = strings; + _images = cache.LocalImages; + _fonts = fonts; + _cs = cs; + _cmdHandler = cmdHandler; + _rng = new NadekoRandom(); + _client = client; + _gss = gss; - public readonly ConcurrentHashSet _generationChannels = new ConcurrentHashSet(); - //channelId/last generation - public ConcurrentDictionary LastGenerations { get; } = new ConcurrentDictionary(); - private readonly SemaphoreSlim pickLock = new SemaphoreSlim(1, 1); - - public PlantPickService(DbService db, CommandHandler cmd, IBotStrings strings, - IDataCache cache, FontProvider fonts, ICurrencyService cs, - CommandHandler cmdHandler, DiscordSocketClient client, GamblingConfigService gss) + cmd.OnMessageNoTrigger += PotentialFlowerGeneration; + using (var uow = db.GetDbContext()) { - _db = db; - _strings = strings; - _images = cache.LocalImages; - _fonts = fonts; - _cs = cs; - _cmdHandler = cmdHandler; - _rng = new NadekoRandom(); - _client = client; - _gss = gss; - - cmd.OnMessageNoTrigger += PotentialFlowerGeneration; - using (var uow = db.GetDbContext()) - { - var guildIds = client.Guilds.Select(x => x.Id).ToList(); - var configs = uow.Set() - .AsQueryable() - .Include(x => x.GenerateCurrencyChannelIds) - .Where(x => guildIds.Contains(x.GuildId)) - .ToList(); + var guildIds = client.Guilds.Select(x => x.Id).ToList(); + var configs = uow.Set() + .AsQueryable() + .Include(x => x.GenerateCurrencyChannelIds) + .Where(x => guildIds.Contains(x.GuildId)) + .ToList(); - _generationChannels = new ConcurrentHashSet(configs - .SelectMany(c => c.GenerateCurrencyChannelIds.Select(obj => obj.ChannelId))); - } + _generationChannels = new ConcurrentHashSet(configs + .SelectMany(c => c.GenerateCurrencyChannelIds.Select(obj => obj.ChannelId))); } + } - private string GetText(ulong gid, LocStr str) - => _strings.GetText(str, gid); + private string GetText(ulong gid, LocStr str) + => _strings.GetText(str, gid); - public bool ToggleCurrencyGeneration(ulong gid, ulong cid) + public bool ToggleCurrencyGeneration(ulong gid, ulong cid) + { + bool enabled; + using (var uow = _db.GetDbContext()) { - bool enabled; - using (var uow = _db.GetDbContext()) - { - var guildConfig = uow.GuildConfigsForId(gid, set => set.Include(gc => gc.GenerateCurrencyChannelIds)); + var guildConfig = uow.GuildConfigsForId(gid, set => set.Include(gc => gc.GenerateCurrencyChannelIds)); - var toAdd = new GCChannelId() { ChannelId = cid }; - if (!guildConfig.GenerateCurrencyChannelIds.Contains(toAdd)) - { - guildConfig.GenerateCurrencyChannelIds.Add(toAdd); - _generationChannels.Add(cid); - enabled = true; - } - else - { - var toDelete = guildConfig.GenerateCurrencyChannelIds.FirstOrDefault(x => x.Equals(toAdd)); - if (toDelete != null) - { - uow.Remove(toDelete); - } - _generationChannels.TryRemove(cid); - enabled = false; - } - uow.SaveChanges(); + var toAdd = new GCChannelId() { ChannelId = cid }; + if (!guildConfig.GenerateCurrencyChannelIds.Contains(toAdd)) + { + guildConfig.GenerateCurrencyChannelIds.Add(toAdd); + _generationChannels.Add(cid); + enabled = true; } - return enabled; + else + { + var toDelete = guildConfig.GenerateCurrencyChannelIds.FirstOrDefault(x => x.Equals(toAdd)); + if (toDelete != null) + { + uow.Remove(toDelete); + } + _generationChannels.TryRemove(cid); + enabled = false; + } + uow.SaveChanges(); } + return enabled; + } - public IEnumerable GetAllGeneratingChannels() + public IEnumerable GetAllGeneratingChannels() + { + using (var uow = _db.GetDbContext()) { - using (var uow = _db.GetDbContext()) - { - var chs = uow.GuildConfigs.GetGeneratingChannels(); - return chs; - } + var chs = uow.GuildConfigs.GetGeneratingChannels(); + return chs; } + } - /// - /// Get a random currency image stream, with an optional password sticked onto it. - /// - /// Optional password to add to top left corner. - /// Stream of the currency image - public Stream GetRandomCurrencyImage(string pass, out string extension) + /// + /// Get a random currency image stream, with an optional password sticked onto it. + /// + /// Optional password to add to top left corner. + /// Stream of the currency image + public Stream GetRandomCurrencyImage(string pass, out string extension) + { + // get a random currency image bytes + var rng = new NadekoRandom(); + var curImg = _images.Currency[rng.Next(0, _images.Currency.Count)]; + + if (string.IsNullOrWhiteSpace(pass)) { - // get a random currency image bytes - var rng = new NadekoRandom(); - var curImg = _images.Currency[rng.Next(0, _images.Currency.Count)]; - - if (string.IsNullOrWhiteSpace(pass)) + // determine the extension + using (var img = Image.Load(curImg, out var format)) { - // determine the extension - using (var img = Image.Load(curImg, out var format)) - { - extension = format.FileExtensions.FirstOrDefault() ?? "png"; - } - // return the image - return curImg.ToStream(); + extension = format.FileExtensions.FirstOrDefault() ?? "png"; } - - // get the image stream and extension - var (s, ext) = AddPassword(curImg, pass); - // set the out extension parameter to the extension we've got - extension = ext; // return the image - return s; + return curImg.ToStream(); } - /// - /// Add a password to the image. - /// - /// Image to add password to. - /// Password to add to top left corner. - /// Image with the password in the top left corner. - private (Stream, string) AddPassword(byte[] curImg, string pass) + // get the image stream and extension + var (s, ext) = AddPassword(curImg, pass); + // set the out extension parameter to the extension we've got + extension = ext; + // return the image + return s; + } + + /// + /// Add a password to the image. + /// + /// Image to add password to. + /// Password to add to top left corner. + /// Image with the password in the top left corner. + private (Stream, string) AddPassword(byte[] curImg, string pass) + { + // draw lower, it looks better + pass = pass.TrimTo(10, true).ToLowerInvariant(); + using (var img = Image.Load(curImg, out var format)) { - // draw lower, it looks better - pass = pass.TrimTo(10, true).ToLowerInvariant(); - using (var img = Image.Load(curImg, out var format)) + // choose font size based on the image height, so that it's visible + var font = _fonts.NotoSans.CreateFont(img.Height / 12, FontStyle.Bold); + img.Mutate(x => { - // choose font size based on the image height, so that it's visible - var font = _fonts.NotoSans.CreateFont(img.Height / 12, FontStyle.Bold); - img.Mutate(x => - { - // measure the size of the text to be drawing - var size = TextMeasurer.Measure(pass, new RendererOptions(font, new PointF(0, 0))); + // measure the size of the text to be drawing + var size = TextMeasurer.Measure(pass, new RendererOptions(font, new PointF(0, 0))); - // fill the background with black, add 5 pixels on each side to make it look better - x.FillPolygon(Color.ParseHex("00000080"), - new PointF(0, 0), - new PointF(size.Width + 5, 0), - new PointF(size.Width + 5, size.Height + 10), - new PointF(0, size.Height + 10)); + // fill the background with black, add 5 pixels on each side to make it look better + x.FillPolygon(Color.ParseHex("00000080"), + new PointF(0, 0), + new PointF(size.Width + 5, 0), + new PointF(size.Width + 5, size.Height + 10), + new PointF(0, size.Height + 10)); - // draw the password over the background - x.DrawText(pass, - font, - SixLabors.ImageSharp.Color.White, - new PointF(0, 0)); - }); - // return image as a stream for easy sending - return (img.ToStream(format), format.FileExtensions.FirstOrDefault() ?? "png"); - } - } - - private Task PotentialFlowerGeneration(IUserMessage imsg) - { - var msg = imsg as SocketUserMessage; - if (msg is null || msg.Author.IsBot) - return Task.CompletedTask; - - if (!(imsg.Channel is ITextChannel channel)) - return Task.CompletedTask; - - if (!_generationChannels.Contains(channel.Id)) - return Task.CompletedTask; - - var _ = Task.Run(async () => - { - try - { - var config = _gss.Data; - var lastGeneration = LastGenerations.GetOrAdd(channel.Id, DateTime.MinValue); - var rng = new NadekoRandom(); - - if (DateTime.UtcNow - TimeSpan.FromSeconds(config.Generation.GenCooldown) < lastGeneration) //recently generated in this channel, don't generate again - return; - - var num = rng.Next(1, 101) + config.Generation.Chance * 100; - if (num > 100 && LastGenerations.TryUpdate(channel.Id, DateTime.UtcNow, lastGeneration)) - { - var dropAmount = config.Generation.MinAmount; - var dropAmountMax = config.Generation.MaxAmount; - - if (dropAmountMax > dropAmount) - dropAmount = new NadekoRandom().Next(dropAmount, dropAmountMax + 1); - - if (dropAmount > 0) - { - var prefix = _cmdHandler.GetPrefix(channel.Guild.Id); - var toSend = dropAmount == 1 - ? GetText(channel.GuildId, strs.curgen_sn(config.Currency.Sign)) - + " " + GetText(channel.GuildId, strs.pick_sn(prefix)) - : GetText(channel.GuildId, strs.curgen_pl(dropAmount, config.Currency.Sign)) - + " " + GetText(channel.GuildId, strs.pick_pl(prefix)); - - var pw = config.Generation.HasPassword ? GenerateCurrencyPassword().ToUpperInvariant() : null; - - IUserMessage sent; - using (var stream = GetRandomCurrencyImage(pw, out var ext)) - { - sent = await channel.SendFileAsync(stream, $"currency_image.{ext}", toSend).ConfigureAwait(false); - } - - await AddPlantToDatabase(channel.GuildId, - channel.Id, - _client.CurrentUser.Id, - sent.Id, - dropAmount, - pw).ConfigureAwait(false); - } - } - } - catch - { - } + // draw the password over the background + x.DrawText(pass, + font, + SixLabors.ImageSharp.Color.White, + new PointF(0, 0)); }); + // return image as a stream for easy sending + return (img.ToStream(format), format.FileExtensions.FirstOrDefault() ?? "png"); + } + } + + private Task PotentialFlowerGeneration(IUserMessage imsg) + { + var msg = imsg as SocketUserMessage; + if (msg is null || msg.Author.IsBot) return Task.CompletedTask; - } - /// - /// Generate a hexadecimal string from 1000 to ffff. - /// - /// A hexadecimal string from 1000 to ffff - private string GenerateCurrencyPassword() - { - // generate a number from 1000 to ffff - var num = _rng.Next(4096, 65536); - // convert it to hexadecimal - return num.ToString("x4"); - } + if (!(imsg.Channel is ITextChannel channel)) + return Task.CompletedTask; - public async Task PickAsync(ulong gid, ITextChannel ch, ulong uid, string pass) + if (!_generationChannels.Contains(channel.Id)) + return Task.CompletedTask; + + var _ = Task.Run(async () => { - await pickLock.WaitAsync(); try { - long amount; - ulong[] ids; - using (var uow = _db.GetDbContext()) + var config = _gss.Data; + var lastGeneration = LastGenerations.GetOrAdd(channel.Id, DateTime.MinValue); + var rng = new NadekoRandom(); + + if (DateTime.UtcNow - TimeSpan.FromSeconds(config.Generation.GenCooldown) < lastGeneration) //recently generated in this channel, don't generate again + return; + + var num = rng.Next(1, 101) + config.Generation.Chance * 100; + if (num > 100 && LastGenerations.TryUpdate(channel.Id, DateTime.UtcNow, lastGeneration)) { - // this method will sum all plants with that password, - // remove them, and get messageids of the removed plants + var dropAmount = config.Generation.MinAmount; + var dropAmountMax = config.Generation.MaxAmount; - pass = pass?.Trim().TrimTo(10, hideDots: true).ToUpperInvariant(); - // gets all plants in this channel with the same password - var entries = uow.PlantedCurrency - .AsQueryable() - .Where(x => x.ChannelId == ch.Id && pass == x.Password) - .ToList(); - // sum how much currency that is, and get all of the message ids (so that i can delete them) - amount = entries.Sum(x => x.Amount); - ids = entries.Select(x => x.MessageId).ToArray(); - // remove them from the database - uow.RemoveRange(entries); + if (dropAmountMax > dropAmount) + dropAmount = new NadekoRandom().Next(dropAmount, dropAmountMax + 1); - - if (amount > 0) + if (dropAmount > 0) { - // give the picked currency to the user - await _cs.AddAsync(uid, "Picked currency", amount, gamble: false); + var prefix = _cmdHandler.GetPrefix(channel.Guild.Id); + var toSend = dropAmount == 1 + ? GetText(channel.GuildId, strs.curgen_sn(config.Currency.Sign)) + + " " + GetText(channel.GuildId, strs.pick_sn(prefix)) + : GetText(channel.GuildId, strs.curgen_pl(dropAmount, config.Currency.Sign)) + + " " + GetText(channel.GuildId, strs.pick_pl(prefix)); + + var pw = config.Generation.HasPassword ? GenerateCurrencyPassword().ToUpperInvariant() : null; + + IUserMessage sent; + using (var stream = GetRandomCurrencyImage(pw, out var ext)) + { + sent = await channel.SendFileAsync(stream, $"currency_image.{ext}", toSend).ConfigureAwait(false); + } + + await AddPlantToDatabase(channel.GuildId, + channel.Id, + _client.CurrentUser.Id, + sent.Id, + dropAmount, + pw).ConfigureAwait(false); } - uow.SaveChanges(); - } - - try - { - // delete all of the plant messages which have just been picked - var _ = ch.DeleteMessagesAsync(ids); - } - catch { } - - // return the amount of currency the user picked - return amount; - } - finally - { - pickLock.Release(); - } - } - - public async Task SendPlantMessageAsync(ulong gid, IMessageChannel ch, string user, long amount, string pass) - { - try - { - // get the text - var prefix = _cmdHandler.GetPrefix(gid); - var msgToSend = GetText(gid, - strs.planted( - Format.Bold(user), - amount + _gss.Data.Currency.Sign)); - - if (amount > 1) - msgToSend += " " + GetText(gid, strs.pick_pl(prefix)); - else - msgToSend += " " + GetText(gid, strs.pick_sn(prefix)); - - //get the image - using (var stream = GetRandomCurrencyImage(pass, out var ext)) - { - // send it - var msg = await ch.SendFileAsync(stream, $"img.{ext}", msgToSend).ConfigureAwait(false); - // return sent message's id (in order to be able to delete it when it's picked) - return msg.Id; } } catch { - // if sending fails, return null as message id - return null; } - } + }); + return Task.CompletedTask; + } - public async Task PlantAsync(ulong gid, IMessageChannel ch, ulong uid, string user, long amount, string pass) - { - // normalize it - no more than 10 chars, uppercase - pass = pass?.Trim().TrimTo(10, hideDots: true).ToUpperInvariant(); - // has to be either null or alphanumeric - if (!string.IsNullOrWhiteSpace(pass) && !pass.IsAlphaNumeric()) - return false; - - // remove currency from the user who's planting - if (await _cs.RemoveAsync(uid, "Planted currency", amount, gamble: false)) - { - // try to send the message with the currency image - var msgId = await SendPlantMessageAsync(gid, ch, user, amount, pass).ConfigureAwait(false); - if (msgId is null) - { - // if it fails it will return null, if it returns null, refund - await _cs.AddAsync(uid, "Planted currency refund", amount, gamble: false); - return false; - } - // if it doesn't fail, put the plant in the database for other people to pick - await AddPlantToDatabase(gid, ch.Id, uid, msgId.Value, amount, pass).ConfigureAwait(false); - return true; - } - // if user doesn't have enough currency, fail - return false; - } - - private async Task AddPlantToDatabase(ulong gid, ulong cid, ulong uid, ulong mid, long amount, string pass) + /// + /// Generate a hexadecimal string from 1000 to ffff. + /// + /// A hexadecimal string from 1000 to ffff + private string GenerateCurrencyPassword() + { + // generate a number from 1000 to ffff + var num = _rng.Next(4096, 65536); + // convert it to hexadecimal + return num.ToString("x4"); + } + + public async Task PickAsync(ulong gid, ITextChannel ch, ulong uid, string pass) + { + await pickLock.WaitAsync(); + try { + long amount; + ulong[] ids; using (var uow = _db.GetDbContext()) { - uow.PlantedCurrency.Add(new PlantedCurrency + // this method will sum all plants with that password, + // remove them, and get messageids of the removed plants + + pass = pass?.Trim().TrimTo(10, hideDots: true).ToUpperInvariant(); + // gets all plants in this channel with the same password + var entries = uow.PlantedCurrency + .AsQueryable() + .Where(x => x.ChannelId == ch.Id && pass == x.Password) + .ToList(); + // sum how much currency that is, and get all of the message ids (so that i can delete them) + amount = entries.Sum(x => x.Amount); + ids = entries.Select(x => x.MessageId).ToArray(); + // remove them from the database + uow.RemoveRange(entries); + + + if (amount > 0) { - Amount = amount, - GuildId = gid, - ChannelId = cid, - Password = pass, - UserId = uid, - MessageId = mid, - }); - await uow.SaveChangesAsync(); + // give the picked currency to the user + await _cs.AddAsync(uid, "Picked currency", amount, gamble: false); + } + uow.SaveChanges(); } + + try + { + // delete all of the plant messages which have just been picked + var _ = ch.DeleteMessagesAsync(ids); + } + catch { } + + // return the amount of currency the user picked + return amount; + } + finally + { + pickLock.Release(); } } -} + + public async Task SendPlantMessageAsync(ulong gid, IMessageChannel ch, string user, long amount, string pass) + { + try + { + // get the text + var prefix = _cmdHandler.GetPrefix(gid); + var msgToSend = GetText(gid, + strs.planted( + Format.Bold(user), + amount + _gss.Data.Currency.Sign)); + + if (amount > 1) + msgToSend += " " + GetText(gid, strs.pick_pl(prefix)); + else + msgToSend += " " + GetText(gid, strs.pick_sn(prefix)); + + //get the image + using (var stream = GetRandomCurrencyImage(pass, out var ext)) + { + // send it + var msg = await ch.SendFileAsync(stream, $"img.{ext}", msgToSend).ConfigureAwait(false); + // return sent message's id (in order to be able to delete it when it's picked) + return msg.Id; + } + } + catch + { + // if sending fails, return null as message id + return null; + } + } + + public async Task PlantAsync(ulong gid, IMessageChannel ch, ulong uid, string user, long amount, string pass) + { + // normalize it - no more than 10 chars, uppercase + pass = pass?.Trim().TrimTo(10, hideDots: true).ToUpperInvariant(); + // has to be either null or alphanumeric + if (!string.IsNullOrWhiteSpace(pass) && !pass.IsAlphaNumeric()) + return false; + + // remove currency from the user who's planting + if (await _cs.RemoveAsync(uid, "Planted currency", amount, gamble: false)) + { + // try to send the message with the currency image + var msgId = await SendPlantMessageAsync(gid, ch, user, amount, pass).ConfigureAwait(false); + if (msgId is null) + { + // if it fails it will return null, if it returns null, refund + await _cs.AddAsync(uid, "Planted currency refund", amount, gamble: false); + return false; + } + // if it doesn't fail, put the plant in the database for other people to pick + await AddPlantToDatabase(gid, ch.Id, uid, msgId.Value, amount, pass).ConfigureAwait(false); + return true; + } + // if user doesn't have enough currency, fail + return false; + } + + private async Task AddPlantToDatabase(ulong gid, ulong cid, ulong uid, ulong mid, long amount, string pass) + { + using (var uow = _db.GetDbContext()) + { + uow.PlantedCurrency.Add(new PlantedCurrency + { + Amount = amount, + GuildId = gid, + ChannelId = cid, + Password = pass, + UserId = uid, + MessageId = mid, + }); + await uow.SaveChangesAsync(); + } + } +} \ No newline at end of file diff --git a/src/NadekoBot/Modules/Gambling/Services/VoteRewardService.cs b/src/NadekoBot/Modules/Gambling/Services/VoteRewardService.cs index cae63f93c..7603ff585 100644 --- a/src/NadekoBot/Modules/Gambling/Services/VoteRewardService.cs +++ b/src/NadekoBot/Modules/Gambling/Services/VoteRewardService.cs @@ -1,121 +1,116 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Net.Http; +using System.Net.Http; using System.Text.Json; using System.Text.Json.Serialization; using System.Threading.Tasks; using NadekoBot.Common.ModuleBehaviors; using NadekoBot.Services; using Discord.WebSocket; -using Serilog; -namespace NadekoBot.Modules.Gambling.Services +namespace NadekoBot.Modules.Gambling.Services; + +public class VoteModel { - public class VoteModel - { - [JsonPropertyName("userId")] - public ulong UserId { get; set; } - } + [JsonPropertyName("userId")] + public ulong UserId { get; set; } +} - public class VoteRewardService : INService, IReadyExecutor +public class VoteRewardService : INService, IReadyExecutor +{ + private readonly DiscordSocketClient _client; + private readonly IBotCredentials _creds; + private readonly IHttpClientFactory _httpClientFactory; + private readonly ICurrencyService _currencyService; + private readonly GamblingConfigService _gamb; + private HttpClient _http; + + public VoteRewardService( + DiscordSocketClient client, + IBotCredentials creds, + IHttpClientFactory httpClientFactory, + ICurrencyService currencyService, + GamblingConfigService gamb) { - private readonly DiscordSocketClient _client; - private readonly IBotCredentials _creds; - private readonly IHttpClientFactory _httpClientFactory; - private readonly ICurrencyService _currencyService; - private readonly GamblingConfigService _gamb; - private HttpClient _http; - - public VoteRewardService( - DiscordSocketClient client, - IBotCredentials creds, - IHttpClientFactory httpClientFactory, - ICurrencyService currencyService, - GamblingConfigService gamb) - { - _client = client; - _creds = creds; - _httpClientFactory = httpClientFactory; - _currencyService = currencyService; - _gamb = gamb; - } + _client = client; + _creds = creds; + _httpClientFactory = httpClientFactory; + _currencyService = currencyService; + _gamb = gamb; + } - public async Task OnReadyAsync() + public async Task OnReadyAsync() + { + if (_client.ShardId != 0) + return; + + _http = new HttpClient(new HttpClientHandler() { - if (_client.ShardId != 0) - return; + AllowAutoRedirect = false, + ServerCertificateCustomValidationCallback = delegate { return true; } + }); - _http = new HttpClient(new HttpClientHandler() - { - AllowAutoRedirect = false, - ServerCertificateCustomValidationCallback = delegate { return true; } - }); - - while (true) - { - await Task.Delay(30000); + while (true) + { + await Task.Delay(30000); - var topggKey = _creds.Votes?.TopggKey; - var topggServiceUrl = _creds.Votes?.TopggServiceUrl; + var topggKey = _creds.Votes?.TopggKey; + var topggServiceUrl = _creds.Votes?.TopggServiceUrl; - try + try + { + if (!string.IsNullOrWhiteSpace(topggKey) + && !string.IsNullOrWhiteSpace(topggServiceUrl)) { - if (!string.IsNullOrWhiteSpace(topggKey) - && !string.IsNullOrWhiteSpace(topggServiceUrl)) + _http.DefaultRequestHeaders.Authorization = new(topggKey); + var uri = new Uri(new(topggServiceUrl), "topgg/new"); + var res = await _http.GetStringAsync(uri); + var data = JsonSerializer.Deserialize>(res); + + if (data is { Count: > 0 }) { - _http.DefaultRequestHeaders.Authorization = new(topggKey); - var uri = new Uri(new(topggServiceUrl), "topgg/new"); - var res = await _http.GetStringAsync(uri); - var data = JsonSerializer.Deserialize>(res); + var ids = data.Select(x => x.UserId).ToList(); - if (data is { Count: > 0 }) - { - var ids = data.Select(x => x.UserId).ToList(); + await _currencyService.AddBulkAsync(ids, + data.Select(_ => "top.gg vote reward"), + data.Select(x => _gamb.Data.VoteReward), + true); - await _currencyService.AddBulkAsync(ids, - data.Select(_ => "top.gg vote reward"), - data.Select(x => _gamb.Data.VoteReward), - true); - - Log.Information("Rewarding {Count} top.gg voters", ids.Count()); - } + Log.Information("Rewarding {Count} top.gg voters", ids.Count()); } } - catch (Exception ex) - { - Log.Error(ex, "Critical error loading top.gg vote rewards."); - } + } + catch (Exception ex) + { + Log.Error(ex, "Critical error loading top.gg vote rewards."); + } - var discordsKey = _creds.Votes?.DiscordsKey; - var discordsServiceUrl = _creds.Votes?.DiscordsServiceUrl; + var discordsKey = _creds.Votes?.DiscordsKey; + var discordsServiceUrl = _creds.Votes?.DiscordsServiceUrl; - try + try + { + if (!string.IsNullOrWhiteSpace(discordsKey) + && !string.IsNullOrWhiteSpace(discordsServiceUrl)) { - if (!string.IsNullOrWhiteSpace(discordsKey) - && !string.IsNullOrWhiteSpace(discordsServiceUrl)) + _http.DefaultRequestHeaders.Authorization = new(discordsKey); + var res = await _http.GetStringAsync(new Uri(new(discordsServiceUrl), "discords/new")); + var data = JsonSerializer.Deserialize>(res); + + if (data is { Count: > 0 }) { - _http.DefaultRequestHeaders.Authorization = new(discordsKey); - var res = await _http.GetStringAsync(new Uri(new(discordsServiceUrl), "discords/new")); - var data = JsonSerializer.Deserialize>(res); - - if (data is { Count: > 0 }) - { - var ids = data.Select(x => x.UserId).ToList(); + var ids = data.Select(x => x.UserId).ToList(); - await _currencyService.AddBulkAsync(ids, - data.Select(_ => "discords.com vote reward"), - data.Select(x => _gamb.Data.VoteReward), - true); + await _currencyService.AddBulkAsync(ids, + data.Select(_ => "discords.com vote reward"), + data.Select(x => _gamb.Data.VoteReward), + true); - Log.Information("Rewarding {Count} discords.com voters", ids.Count()); - } + Log.Information("Rewarding {Count} discords.com voters", ids.Count()); } } - catch (Exception ex) - { - Log.Error(ex, "Critical error loading discords.com vote rewards."); - } + } + catch (Exception ex) + { + Log.Error(ex, "Critical error loading discords.com vote rewards."); } } } diff --git a/src/NadekoBot/Modules/Gambling/Services/WaifuService.cs b/src/NadekoBot/Modules/Gambling/Services/WaifuService.cs index 51021a959..e632684ff 100644 --- a/src/NadekoBot/Modules/Gambling/Services/WaifuService.cs +++ b/src/NadekoBot/Modules/Gambling/Services/WaifuService.cs @@ -2,527 +2,523 @@ using NadekoBot.Modules.Gambling.Common.Waifu; using NadekoBot.Services; using NadekoBot.Services.Database.Models; -using System; -using System.Collections.Generic; -using System.Linq; using System.Threading.Tasks; using Microsoft.EntityFrameworkCore; using NadekoBot.Db; using NadekoBot.Modules.Gambling.Common; using NadekoBot.Db.Models; -namespace NadekoBot.Modules.Gambling.Services +namespace NadekoBot.Modules.Gambling.Services; + +public class WaifuService : INService { - public class WaifuService : INService + public class FullWaifuInfo { - public class FullWaifuInfo - { - public WaifuInfo Waifu { get; set; } - public IEnumerable Claims { get; set; } - public int Divorces { get; set; } - } + public WaifuInfo Waifu { get; set; } + public IEnumerable Claims { get; set; } + public int Divorces { get; set; } + } - private readonly DbService _db; - private readonly ICurrencyService _cs; - private readonly IDataCache _cache; - private readonly GamblingConfigService _gss; + private readonly DbService _db; + private readonly ICurrencyService _cs; + private readonly IDataCache _cache; + private readonly GamblingConfigService _gss; - public WaifuService(DbService db, ICurrencyService cs, IDataCache cache, - GamblingConfigService gss) - { - _db = db; - _cs = cs; - _cache = cache; - _gss = gss; - } + public WaifuService(DbService db, ICurrencyService cs, IDataCache cache, + GamblingConfigService gss) + { + _db = db; + _cs = cs; + _cache = cache; + _gss = gss; + } - public async Task WaifuTransfer(IUser owner, ulong waifuId, IUser newOwner) - { - if (owner.Id == newOwner.Id || waifuId == newOwner.Id) - return false; + public async Task WaifuTransfer(IUser owner, ulong waifuId, IUser newOwner) + { + if (owner.Id == newOwner.Id || waifuId == newOwner.Id) + return false; - var settings = _gss.Data; + var settings = _gss.Data; - using (var uow = _db.GetDbContext()) - { - var waifu = uow.WaifuInfo.ByWaifuUserId(waifuId); - var ownerUser = uow.GetOrCreateUser(owner); + using (var uow = _db.GetDbContext()) + { + var waifu = uow.WaifuInfo.ByWaifuUserId(waifuId); + var ownerUser = uow.GetOrCreateUser(owner); - // owner has to be the owner of the waifu - if (waifu is null || waifu.ClaimerId != ownerUser.Id) - return false; + // owner has to be the owner of the waifu + if (waifu is null || waifu.ClaimerId != ownerUser.Id) + return false; - // if waifu likes the person, gotta pay the penalty - if (waifu.AffinityId == ownerUser.Id) - { - if (!await _cs.RemoveAsync(owner.Id, + // if waifu likes the person, gotta pay the penalty + if (waifu.AffinityId == ownerUser.Id) + { + if (!await _cs.RemoveAsync(owner.Id, "Waifu Transfer - affinity penalty", (int)(waifu.Price * 0.6), true)) - { - // unable to pay 60% penalty - return false; - } - - waifu.Price = (int)(waifu.Price * 0.7); // half of 60% = 30% price reduction - if (waifu.Price < settings.Waifu.MinPrice) - waifu.Price = settings.Waifu.MinPrice; - } - else // if not, pay 10% fee { - if (!await _cs.RemoveAsync(owner.Id, "Waifu Transfer", waifu.Price / 10, gamble: true)) - { - return false; - } - - waifu.Price = (int) (waifu.Price * 0.95); // half of 10% = 5% price reduction - if (waifu.Price < settings.Waifu.MinPrice) - waifu.Price = settings.Waifu.MinPrice; - } - - //new claimerId is the id of the new owner - var newOwnerUser = uow.GetOrCreateUser(newOwner); - waifu.ClaimerId = newOwnerUser.Id; - - await uow.SaveChangesAsync(); - } - - return true; - } - - public int GetResetPrice(IUser user) - { - var settings = _gss.Data; - using (var uow = _db.GetDbContext()) - { - var waifu = uow.WaifuInfo.ByWaifuUserId(user.Id); - - if (waifu is null) - return settings.Waifu.MinPrice; - - var divorces = uow.WaifuUpdates.Count(x => x.Old != null && - x.Old.UserId == user.Id && - x.UpdateType == WaifuUpdateType.Claimed && - x.New == null); - var affs = uow.WaifuUpdates - .AsQueryable() - .Where(w => w.User.UserId == user.Id && w.UpdateType == WaifuUpdateType.AffinityChanged && - w.New != null) - .ToList() - .GroupBy(x => x.New) - .Count(); - - return (int) Math.Ceiling(waifu.Price * 1.25f) + - ((divorces + affs + 2) * settings.Waifu.Multipliers.WaifuReset); - } - } - - public async Task TryReset(IUser user) - { - using (var uow = _db.GetDbContext()) - { - var price = GetResetPrice(user); - if (!await _cs.RemoveAsync(user.Id, "Waifu Reset", price, gamble: true)) + // unable to pay 60% penalty return false; + } - var affs = uow.WaifuUpdates - .AsQueryable() - .Where(w => w.User.UserId == user.Id - && w.UpdateType == WaifuUpdateType.AffinityChanged - && w.New != null); + waifu.Price = (int)(waifu.Price * 0.7); // half of 60% = 30% price reduction + if (waifu.Price < settings.Waifu.MinPrice) + waifu.Price = settings.Waifu.MinPrice; + } + else // if not, pay 10% fee + { + if (!await _cs.RemoveAsync(owner.Id, "Waifu Transfer", waifu.Price / 10, gamble: true)) + { + return false; + } - var divorces = uow.WaifuUpdates - .AsQueryable() - .Where(x => x.Old != null && - x.Old.UserId == user.Id && - x.UpdateType == WaifuUpdateType.Claimed && - x.New == null); - - //reset changes of heart to 0 - uow.WaifuUpdates.RemoveRange(affs); - //reset divorces to 0 - uow.WaifuUpdates.RemoveRange(divorces); - var waifu = uow.WaifuInfo.ByWaifuUserId(user.Id); - //reset price, remove items - //remove owner, remove affinity - waifu.Price = 50; - waifu.Items.Clear(); - waifu.ClaimerId = null; - waifu.AffinityId = null; - - //wives stay though - - uow.SaveChanges(); + waifu.Price = (int) (waifu.Price * 0.95); // half of 10% = 5% price reduction + if (waifu.Price < settings.Waifu.MinPrice) + waifu.Price = settings.Waifu.MinPrice; } - return true; + //new claimerId is the id of the new owner + var newOwnerUser = uow.GetOrCreateUser(newOwner); + waifu.ClaimerId = newOwnerUser.Id; + + await uow.SaveChangesAsync(); } - public async Task<(WaifuInfo, bool, WaifuClaimResult)> ClaimWaifuAsync(IUser user, IUser target, int amount) + return true; + } + + public int GetResetPrice(IUser user) + { + var settings = _gss.Data; + using (var uow = _db.GetDbContext()) { - var settings = _gss.Data; - WaifuClaimResult result; - WaifuInfo w; - bool isAffinity; - using (var uow = _db.GetDbContext()) + var waifu = uow.WaifuInfo.ByWaifuUserId(user.Id); + + if (waifu is null) + return settings.Waifu.MinPrice; + + var divorces = uow.WaifuUpdates.Count(x => x.Old != null && + x.Old.UserId == user.Id && + x.UpdateType == WaifuUpdateType.Claimed && + x.New == null); + var affs = uow.WaifuUpdates + .AsQueryable() + .Where(w => w.User.UserId == user.Id && w.UpdateType == WaifuUpdateType.AffinityChanged && + w.New != null) + .ToList() + .GroupBy(x => x.New) + .Count(); + + return (int) Math.Ceiling(waifu.Price * 1.25f) + + ((divorces + affs + 2) * settings.Waifu.Multipliers.WaifuReset); + } + } + + public async Task TryReset(IUser user) + { + using (var uow = _db.GetDbContext()) + { + var price = GetResetPrice(user); + if (!await _cs.RemoveAsync(user.Id, "Waifu Reset", price, gamble: true)) + return false; + + var affs = uow.WaifuUpdates + .AsQueryable() + .Where(w => w.User.UserId == user.Id + && w.UpdateType == WaifuUpdateType.AffinityChanged + && w.New != null); + + var divorces = uow.WaifuUpdates + .AsQueryable() + .Where(x => x.Old != null && + x.Old.UserId == user.Id && + x.UpdateType == WaifuUpdateType.Claimed && + x.New == null); + + //reset changes of heart to 0 + uow.WaifuUpdates.RemoveRange(affs); + //reset divorces to 0 + uow.WaifuUpdates.RemoveRange(divorces); + var waifu = uow.WaifuInfo.ByWaifuUserId(user.Id); + //reset price, remove items + //remove owner, remove affinity + waifu.Price = 50; + waifu.Items.Clear(); + waifu.ClaimerId = null; + waifu.AffinityId = null; + + //wives stay though + + uow.SaveChanges(); + } + + return true; + } + + public async Task<(WaifuInfo, bool, WaifuClaimResult)> ClaimWaifuAsync(IUser user, IUser target, int amount) + { + var settings = _gss.Data; + WaifuClaimResult result; + WaifuInfo w; + bool isAffinity; + using (var uow = _db.GetDbContext()) + { + w = uow.WaifuInfo.ByWaifuUserId(target.Id); + isAffinity = (w?.Affinity?.UserId == user.Id); + if (w is null) { - w = uow.WaifuInfo.ByWaifuUserId(target.Id); - isAffinity = (w?.Affinity?.UserId == user.Id); - if (w is null) + var claimer = uow.GetOrCreateUser(user); + var waifu = uow.GetOrCreateUser(target); + if (!await _cs.RemoveAsync(user.Id, "Claimed Waifu", amount, gamble: true)) { - var claimer = uow.GetOrCreateUser(user); - var waifu = uow.GetOrCreateUser(target); - if (!await _cs.RemoveAsync(user.Id, "Claimed Waifu", amount, gamble: true)) - { - result = WaifuClaimResult.NotEnoughFunds; - } - else - { - uow.WaifuInfo.Add(w = new WaifuInfo() - { - Waifu = waifu, - Claimer = claimer, - Affinity = null, - Price = amount - }); - uow.WaifuUpdates.Add(new WaifuUpdate() - { - User = waifu, - Old = null, - New = claimer, - UpdateType = WaifuUpdateType.Claimed - }); - result = WaifuClaimResult.Success; - } - } - else if (isAffinity && amount > w.Price * settings.Waifu.Multipliers.CrushClaim) - { - if (!await _cs.RemoveAsync(user.Id, "Claimed Waifu", amount, gamble: true)) - { - result = WaifuClaimResult.NotEnoughFunds; - } - else - { - var oldClaimer = w.Claimer; - w.Claimer = uow.GetOrCreateUser(user); - w.Price = amount + (amount / 4); - result = WaifuClaimResult.Success; - - uow.WaifuUpdates.Add(new WaifuUpdate() - { - User = w.Waifu, - Old = oldClaimer, - New = w.Claimer, - UpdateType = WaifuUpdateType.Claimed - }); - } - } - else if (amount >= w.Price * settings.Waifu.Multipliers.NormalClaim) // if no affinity - { - if (!await _cs.RemoveAsync(user.Id, "Claimed Waifu", amount, gamble: true)) - { - result = WaifuClaimResult.NotEnoughFunds; - } - else - { - var oldClaimer = w.Claimer; - w.Claimer = uow.GetOrCreateUser(user); - w.Price = amount; - result = WaifuClaimResult.Success; - - uow.WaifuUpdates.Add(new WaifuUpdate() - { - User = w.Waifu, - Old = oldClaimer, - New = w.Claimer, - UpdateType = WaifuUpdateType.Claimed - }); - } + result = WaifuClaimResult.NotEnoughFunds; } else - result = WaifuClaimResult.InsufficientAmount; - - - await uow.SaveChangesAsync(); - } - - return (w, isAffinity, result); - } - - public async Task<(DiscordUser, bool, TimeSpan?)> ChangeAffinityAsync(IUser user, IGuildUser target) - { - DiscordUser oldAff = null; - var success = false; - TimeSpan? remaining = null; - using (var uow = _db.GetDbContext()) - { - var w = uow.WaifuInfo.ByWaifuUserId(user.Id); - var newAff = target is null ? null : uow.GetOrCreateUser(target); - if (w?.Affinity?.UserId == target?.Id) { - } - else if (!_cache.TryAddAffinityCooldown(user.Id, out remaining)) - { - } - else if (w is null) - { - var thisUser = uow.GetOrCreateUser(user); - uow.WaifuInfo.Add(new WaifuInfo() + uow.WaifuInfo.Add(w = new WaifuInfo() { - Affinity = newAff, - Waifu = thisUser, - Price = 1, - Claimer = null + Waifu = waifu, + Claimer = claimer, + Affinity = null, + Price = amount }); - success = true; - uow.WaifuUpdates.Add(new WaifuUpdate() { - User = thisUser, + User = waifu, Old = null, - New = newAff, - UpdateType = WaifuUpdateType.AffinityChanged + New = claimer, + UpdateType = WaifuUpdateType.Claimed }); + result = WaifuClaimResult.Success; + } + } + else if (isAffinity && amount > w.Price * settings.Waifu.Multipliers.CrushClaim) + { + if (!await _cs.RemoveAsync(user.Id, "Claimed Waifu", amount, gamble: true)) + { + result = WaifuClaimResult.NotEnoughFunds; } else { - if (w.Affinity != null) - oldAff = w.Affinity; - w.Affinity = newAff; - success = true; - - uow.WaifuUpdates.Add(new WaifuUpdate() - { - User = w.Waifu, - Old = oldAff, - New = newAff, - UpdateType = WaifuUpdateType.AffinityChanged - }); - } - - await uow.SaveChangesAsync(); - } - - return (oldAff, success, remaining); - } - - public IEnumerable GetTopWaifusAtPage(int page) - { - using (var uow = _db.GetDbContext()) - { - return uow.WaifuInfo.GetTop(9, page * 9); - } - } - - public ulong GetWaifuUserId(ulong ownerId, string name) - { - using var uow = _db.GetDbContext(); - return uow.WaifuInfo.GetWaifuUserId(ownerId, name); - } - - public async Task<(WaifuInfo, DivorceResult, long, TimeSpan?)> DivorceWaifuAsync(IUser user, ulong targetId) - { - DivorceResult result; - TimeSpan? remaining = null; - long amount = 0; - WaifuInfo w = null; - using (var uow = _db.GetDbContext()) - { - w = uow.WaifuInfo.ByWaifuUserId(targetId); - var now = DateTime.UtcNow; - if (w?.Claimer is null || w.Claimer.UserId != user.Id) - result = DivorceResult.NotYourWife; - else if (!_cache.TryAddDivorceCooldown(user.Id, out remaining)) - { - result = DivorceResult.Cooldown; - } - else - { - amount = w.Price / 2; - - if (w.Affinity?.UserId == user.Id) - { - await _cs.AddAsync(w.Waifu.UserId, "Waifu Compensation", amount, gamble: true); - w.Price = (int) Math.Floor(w.Price * _gss.Data.Waifu.Multipliers.DivorceNewValue); - result = DivorceResult.SucessWithPenalty; - } - else - { - await _cs.AddAsync(user.Id, "Waifu Refund", amount, gamble: true); - - result = DivorceResult.Success; - } - var oldClaimer = w.Claimer; - w.Claimer = null; + w.Claimer = uow.GetOrCreateUser(user); + w.Price = amount + (amount / 4); + result = WaifuClaimResult.Success; uow.WaifuUpdates.Add(new WaifuUpdate() { User = w.Waifu, Old = oldClaimer, - New = null, + New = w.Claimer, UpdateType = WaifuUpdateType.Claimed }); } - - await uow.SaveChangesAsync(); } - - return (w, result, amount, remaining); - } - - public async Task GiftWaifuAsync(IUser from, IUser giftedWaifu, WaifuItemModel itemObj) - { - if (!await _cs.RemoveAsync(from, "Bought waifu item", itemObj.Price, gamble: true)) + else if (amount >= w.Price * settings.Waifu.Multipliers.NormalClaim) // if no affinity { - return false; - } - - using (var uow = _db.GetDbContext()) - { - var w = uow.WaifuInfo.ByWaifuUserId(giftedWaifu.Id, - set => set.Include(x => x.Items) - .Include(x => x.Claimer)); - if (w is null) + if (!await _cs.RemoveAsync(user.Id, "Claimed Waifu", amount, gamble: true)) { - uow.WaifuInfo.Add(w = new WaifuInfo() - { - Affinity = null, - Claimer = null, - Price = 1, - Waifu = uow.GetOrCreateUser(giftedWaifu), - }); - } - - if (!itemObj.Negative) - { - w.Items.Add(new WaifuItem() - { - Name = itemObj.Name.ToLowerInvariant(), - ItemEmoji = itemObj.ItemEmoji, - }); - - if (w.Claimer?.UserId == from.Id) - { - w.Price += (int)(itemObj.Price * _gss.Data.Waifu.Multipliers.GiftEffect); - } - else - { - w.Price += itemObj.Price / 2; - } + result = WaifuClaimResult.NotEnoughFunds; } else { - w.Price -= (int)(itemObj.Price * _gss.Data.Waifu.Multipliers.NegativeGiftEffect); - if (w.Price < 1) - w.Price = 1; - } + var oldClaimer = w.Claimer; + w.Claimer = uow.GetOrCreateUser(user); + w.Price = amount; + result = WaifuClaimResult.Success; - await uow.SaveChangesAsync(); - } - - return true; - } - - public WaifuInfoStats GetFullWaifuInfoAsync(ulong targetId) - { - using (var uow = _db.GetDbContext()) - { - var wi = uow.GetWaifuInfo(targetId); - if (wi is null) - { - wi = new WaifuInfoStats + uow.WaifuUpdates.Add(new WaifuUpdate() { - AffinityCount = 0, - AffinityName = null, - ClaimCount = 0, - ClaimerName = null, - Claims = new List(), - Fans = new List(), - DivorceCount = 0, - FullName = null, - Items = new List(), - Price = 1 - }; + User = w.Waifu, + Old = oldClaimer, + New = w.Claimer, + UpdateType = WaifuUpdateType.Claimed + }); } - - return wi; } + else + result = WaifuClaimResult.InsufficientAmount; + + + await uow.SaveChangesAsync(); } - public WaifuInfoStats GetFullWaifuInfoAsync(IGuildUser target) + + return (w, isAffinity, result); + } + + public async Task<(DiscordUser, bool, TimeSpan?)> ChangeAffinityAsync(IUser user, IGuildUser target) + { + DiscordUser oldAff = null; + var success = false; + TimeSpan? remaining = null; + using (var uow = _db.GetDbContext()) { - using (var uow = _db.GetDbContext()) + var w = uow.WaifuInfo.ByWaifuUserId(user.Id); + var newAff = target is null ? null : uow.GetOrCreateUser(target); + if (w?.Affinity?.UserId == target?.Id) { - var du = uow.GetOrCreateUser(target); - - return GetFullWaifuInfoAsync(target.Id); } - } + else if (!_cache.TryAddAffinityCooldown(user.Id, out remaining)) + { + } + else if (w is null) + { + var thisUser = uow.GetOrCreateUser(user); + uow.WaifuInfo.Add(new WaifuInfo() + { + Affinity = newAff, + Waifu = thisUser, + Price = 1, + Claimer = null + }); + success = true; - public string GetClaimTitle(int count) - { - ClaimTitle title; - if (count == 0) - title = ClaimTitle.Lonely; - else if (count == 1) - title = ClaimTitle.Devoted; - else if (count < 3) - title = ClaimTitle.Rookie; - else if (count < 6) - title = ClaimTitle.Schemer; - else if (count < 10) - title = ClaimTitle.Dilettante; - else if (count < 17) - title = ClaimTitle.Intermediate; - else if (count < 25) - title = ClaimTitle.Seducer; - else if (count < 35) - title = ClaimTitle.Expert; - else if (count < 50) - title = ClaimTitle.Veteran; - else if (count < 75) - title = ClaimTitle.Incubis; - else if (count < 100) - title = ClaimTitle.Harem_King; + uow.WaifuUpdates.Add(new WaifuUpdate() + { + User = thisUser, + Old = null, + New = newAff, + UpdateType = WaifuUpdateType.AffinityChanged + }); + } else - title = ClaimTitle.Harem_God; + { + if (w.Affinity != null) + oldAff = w.Affinity; + w.Affinity = newAff; + success = true; - return title.ToString().Replace('_', ' '); + uow.WaifuUpdates.Add(new WaifuUpdate() + { + User = w.Waifu, + Old = oldAff, + New = newAff, + UpdateType = WaifuUpdateType.AffinityChanged + }); + } + + await uow.SaveChangesAsync(); } - public string GetAffinityTitle(int count) - { - AffinityTitle title; - if (count < 1) - title = AffinityTitle.Pure; - else if (count < 2) - title = AffinityTitle.Faithful; - else if (count < 4) - title = AffinityTitle.Playful; - else if (count < 8) - title = AffinityTitle.Cheater; - else if (count < 11) - title = AffinityTitle.Tainted; - else if (count < 15) - title = AffinityTitle.Corrupted; - else if (count < 20) - title = AffinityTitle.Lewd; - else if (count < 25) - title = AffinityTitle.Sloot; - else if (count < 35) - title = AffinityTitle.Depraved; - else - title = AffinityTitle.Harlot; + return (oldAff, success, remaining); + } - return title.ToString().Replace('_', ' '); - } - - public IReadOnlyList GetWaifuItems() + public IEnumerable GetTopWaifusAtPage(int page) + { + using (var uow = _db.GetDbContext()) { - var conf = _gss.Data; - return conf.Waifu.Items - .Select(x => new WaifuItemModel(x.ItemEmoji, (int)(x.Price * conf.Waifu.Multipliers.AllGiftPrices), x.Name, x.Negative)) - .ToList(); + return uow.WaifuInfo.GetTop(9, page * 9); } } + + public ulong GetWaifuUserId(ulong ownerId, string name) + { + using var uow = _db.GetDbContext(); + return uow.WaifuInfo.GetWaifuUserId(ownerId, name); + } + + public async Task<(WaifuInfo, DivorceResult, long, TimeSpan?)> DivorceWaifuAsync(IUser user, ulong targetId) + { + DivorceResult result; + TimeSpan? remaining = null; + long amount = 0; + WaifuInfo w = null; + using (var uow = _db.GetDbContext()) + { + w = uow.WaifuInfo.ByWaifuUserId(targetId); + var now = DateTime.UtcNow; + if (w?.Claimer is null || w.Claimer.UserId != user.Id) + result = DivorceResult.NotYourWife; + else if (!_cache.TryAddDivorceCooldown(user.Id, out remaining)) + { + result = DivorceResult.Cooldown; + } + else + { + amount = w.Price / 2; + + if (w.Affinity?.UserId == user.Id) + { + await _cs.AddAsync(w.Waifu.UserId, "Waifu Compensation", amount, gamble: true); + w.Price = (int) Math.Floor(w.Price * _gss.Data.Waifu.Multipliers.DivorceNewValue); + result = DivorceResult.SucessWithPenalty; + } + else + { + await _cs.AddAsync(user.Id, "Waifu Refund", amount, gamble: true); + + result = DivorceResult.Success; + } + + var oldClaimer = w.Claimer; + w.Claimer = null; + + uow.WaifuUpdates.Add(new WaifuUpdate() + { + User = w.Waifu, + Old = oldClaimer, + New = null, + UpdateType = WaifuUpdateType.Claimed + }); + } + + await uow.SaveChangesAsync(); + } + + return (w, result, amount, remaining); + } + + public async Task GiftWaifuAsync(IUser from, IUser giftedWaifu, WaifuItemModel itemObj) + { + if (!await _cs.RemoveAsync(from, "Bought waifu item", itemObj.Price, gamble: true)) + { + return false; + } + + using (var uow = _db.GetDbContext()) + { + var w = uow.WaifuInfo.ByWaifuUserId(giftedWaifu.Id, + set => set.Include(x => x.Items) + .Include(x => x.Claimer)); + if (w is null) + { + uow.WaifuInfo.Add(w = new WaifuInfo() + { + Affinity = null, + Claimer = null, + Price = 1, + Waifu = uow.GetOrCreateUser(giftedWaifu), + }); + } + + if (!itemObj.Negative) + { + w.Items.Add(new WaifuItem() + { + Name = itemObj.Name.ToLowerInvariant(), + ItemEmoji = itemObj.ItemEmoji, + }); + + if (w.Claimer?.UserId == from.Id) + { + w.Price += (int)(itemObj.Price * _gss.Data.Waifu.Multipliers.GiftEffect); + } + else + { + w.Price += itemObj.Price / 2; + } + } + else + { + w.Price -= (int)(itemObj.Price * _gss.Data.Waifu.Multipliers.NegativeGiftEffect); + if (w.Price < 1) + w.Price = 1; + } + + await uow.SaveChangesAsync(); + } + + return true; + } + + public WaifuInfoStats GetFullWaifuInfoAsync(ulong targetId) + { + using (var uow = _db.GetDbContext()) + { + var wi = uow.GetWaifuInfo(targetId); + if (wi is null) + { + wi = new WaifuInfoStats + { + AffinityCount = 0, + AffinityName = null, + ClaimCount = 0, + ClaimerName = null, + Claims = new List(), + Fans = new List(), + DivorceCount = 0, + FullName = null, + Items = new List(), + Price = 1 + }; + } + + return wi; + } + } + public WaifuInfoStats GetFullWaifuInfoAsync(IGuildUser target) + { + using (var uow = _db.GetDbContext()) + { + var du = uow.GetOrCreateUser(target); + + return GetFullWaifuInfoAsync(target.Id); + } + } + + public string GetClaimTitle(int count) + { + ClaimTitle title; + if (count == 0) + title = ClaimTitle.Lonely; + else if (count == 1) + title = ClaimTitle.Devoted; + else if (count < 3) + title = ClaimTitle.Rookie; + else if (count < 6) + title = ClaimTitle.Schemer; + else if (count < 10) + title = ClaimTitle.Dilettante; + else if (count < 17) + title = ClaimTitle.Intermediate; + else if (count < 25) + title = ClaimTitle.Seducer; + else if (count < 35) + title = ClaimTitle.Expert; + else if (count < 50) + title = ClaimTitle.Veteran; + else if (count < 75) + title = ClaimTitle.Incubis; + else if (count < 100) + title = ClaimTitle.Harem_King; + else + title = ClaimTitle.Harem_God; + + return title.ToString().Replace('_', ' '); + } + + public string GetAffinityTitle(int count) + { + AffinityTitle title; + if (count < 1) + title = AffinityTitle.Pure; + else if (count < 2) + title = AffinityTitle.Faithful; + else if (count < 4) + title = AffinityTitle.Playful; + else if (count < 8) + title = AffinityTitle.Cheater; + else if (count < 11) + title = AffinityTitle.Tainted; + else if (count < 15) + title = AffinityTitle.Corrupted; + else if (count < 20) + title = AffinityTitle.Lewd; + else if (count < 25) + title = AffinityTitle.Sloot; + else if (count < 35) + title = AffinityTitle.Depraved; + else + title = AffinityTitle.Harlot; + + return title.ToString().Replace('_', ' '); + } + + public IReadOnlyList GetWaifuItems() + { + var conf = _gss.Data; + return conf.Waifu.Items + .Select(x => new WaifuItemModel(x.ItemEmoji, (int)(x.Price * conf.Waifu.Multipliers.AllGiftPrices), x.Name, x.Negative)) + .ToList(); + } } \ No newline at end of file diff --git a/src/NadekoBot/Modules/Gambling/ShopCommands.cs b/src/NadekoBot/Modules/Gambling/ShopCommands.cs index 45e466d72..d00d3af6b 100644 --- a/src/NadekoBot/Modules/Gambling/ShopCommands.cs +++ b/src/NadekoBot/Modules/Gambling/ShopCommands.cs @@ -1,7 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; +using System.Threading.Tasks; using Discord; using Discord.Commands; using Microsoft.EntityFrameworkCore; @@ -14,462 +11,459 @@ using NadekoBot.Services; using NadekoBot.Services.Database.Models; using NadekoBot.Db; using NadekoBot.Extensions; -using NadekoBot.Modules.Administration; -using Serilog; -namespace NadekoBot.Modules.Gambling +namespace NadekoBot.Modules.Gambling; + +public partial class Gambling { - public partial class Gambling + [Group] + public class ShopCommands : GamblingSubmodule { - [Group] - public class ShopCommands : GamblingSubmodule + private readonly DbService _db; + private readonly ICurrencyService _cs; + + public enum Role { - private readonly DbService _db; - private readonly ICurrencyService _cs; + Role + } - public enum Role - { - Role - } + public enum List + { + List + } - public enum List - { - List - } - - public ShopCommands(DbService db, ICurrencyService cs, GamblingConfigService gamblingConf) - : base(gamblingConf) - { - _db = db; - _cs = cs; - } + public ShopCommands(DbService db, ICurrencyService cs, GamblingConfigService gamblingConf) + : base(gamblingConf) + { + _db = db; + _cs = cs; + } - private Task ShopInternalAsync(int page = 0) - { - if (page < 0) - throw new ArgumentOutOfRangeException(nameof(page)); + private Task ShopInternalAsync(int page = 0) + { + if (page < 0) + throw new ArgumentOutOfRangeException(nameof(page)); - using var uow = _db.GetDbContext(); - var entries = uow.GuildConfigsForId(ctx.Guild.Id, - set => set.Include(x => x.ShopEntries) - .ThenInclude(x => x.Items)).ShopEntries - .ToIndexed(); - return ctx.SendPaginatedConfirmAsync(page, (curPage) => + using var uow = _db.GetDbContext(); + var entries = uow.GuildConfigsForId(ctx.Guild.Id, + set => set.Include(x => x.ShopEntries) + .ThenInclude(x => x.Items)).ShopEntries + .ToIndexed(); + return ctx.SendPaginatedConfirmAsync(page, (curPage) => + { + var theseEntries = entries.Skip(curPage * 9).Take(9).ToArray(); + + if (!theseEntries.Any()) + return _eb.Create().WithErrorColor() + .WithDescription(GetText(strs.shop_none)); + var embed = _eb.Create().WithOkColor() + .WithTitle(GetText(strs.shop)); + + for (int i = 0; i < theseEntries.Length; i++) { - var theseEntries = entries.Skip(curPage * 9).Take(9).ToArray(); + var entry = theseEntries[i]; + embed.AddField( + $"#{curPage * 9 + i + 1} - {entry.Price}{CurrencySign}", + EntryToString(entry), + true); + } + return embed; + }, entries.Count, 9, true); + } - if (!theseEntries.Any()) - return _eb.Create().WithErrorColor() - .WithDescription(GetText(strs.shop_none)); - var embed = _eb.Create().WithOkColor() - .WithTitle(GetText(strs.shop)); + [NadekoCommand, Aliases] + [RequireContext(ContextType.Guild)] + public Task Shop(int page = 1) + { + if (--page < 0) + return Task.CompletedTask; + + return ShopInternalAsync(page); + } - for (int i = 0; i < theseEntries.Length; i++) - { - var entry = theseEntries[i]; - embed.AddField( - $"#{curPage * 9 + i + 1} - {entry.Price}{CurrencySign}", - EntryToString(entry), - true); - } - return embed; - }, entries.Count, 9, true); + [NadekoCommand, Aliases] + [RequireContext(ContextType.Guild)] + public async Task Buy(int index) + { + index -= 1; + if (index < 0) + return; + ShopEntry entry; + using (var uow = _db.GetDbContext()) + { + var config = uow.GuildConfigsForId(ctx.Guild.Id, set => set + .Include(x => x.ShopEntries) + .ThenInclude(x => x.Items)); + var entries = new IndexedCollection(config.ShopEntries); + entry = entries.ElementAtOrDefault(index); + uow.SaveChanges(); } - [NadekoCommand, Aliases] - [RequireContext(ContextType.Guild)] - public Task Shop(int page = 1) + if (entry is null) { - if (--page < 0) - return Task.CompletedTask; - - return ShopInternalAsync(page); + await ReplyErrorLocalizedAsync(strs.shop_item_not_found).ConfigureAwait(false); + return; } - [NadekoCommand, Aliases] - [RequireContext(ContextType.Guild)] - public async Task Buy(int index) + if (entry.Type == ShopEntryType.Role) { - index -= 1; - if (index < 0) - return; - ShopEntry entry; - using (var uow = _db.GetDbContext()) - { - var config = uow.GuildConfigsForId(ctx.Guild.Id, set => set - .Include(x => x.ShopEntries) - .ThenInclude(x => x.Items)); - var entries = new IndexedCollection(config.ShopEntries); - entry = entries.ElementAtOrDefault(index); - uow.SaveChanges(); - } + var guser = (IGuildUser)ctx.User; + var role = ctx.Guild.GetRole(entry.RoleId); - if (entry is null) + if (role is null) { - await ReplyErrorLocalizedAsync(strs.shop_item_not_found).ConfigureAwait(false); + await ReplyErrorLocalizedAsync(strs.shop_role_not_found).ConfigureAwait(false); return; } - - if (entry.Type == ShopEntryType.Role) - { - var guser = (IGuildUser)ctx.User; - var role = ctx.Guild.GetRole(entry.RoleId); - - if (role is null) - { - await ReplyErrorLocalizedAsync(strs.shop_role_not_found).ConfigureAwait(false); - return; - } - if (guser.RoleIds.Any(id => id == role.Id)) - { - await ReplyErrorLocalizedAsync(strs.shop_role_already_bought).ConfigureAwait(false); - return; - } - - if (await _cs.RemoveAsync(ctx.User.Id, $"Shop purchase - {entry.Type}", entry.Price).ConfigureAwait(false)) - { - try - { - await guser.AddRoleAsync(role).ConfigureAwait(false); - } - catch (Exception ex) - { - Log.Warning(ex, "Error adding shop role"); - await _cs.AddAsync(ctx.User.Id, $"Shop error refund", entry.Price).ConfigureAwait(false); - await ReplyErrorLocalizedAsync(strs.shop_role_purchase_error).ConfigureAwait(false); - return; - } - var profit = GetProfitAmount(entry.Price); - await _cs.AddAsync(entry.AuthorId, $"Shop sell item - {entry.Type}", profit).ConfigureAwait(false); - await _cs.AddAsync(ctx.Client.CurrentUser.Id, $"Shop sell item - cut", entry.Price - profit).ConfigureAwait(false); - await ReplyConfirmLocalizedAsync(strs.shop_role_purchase(Format.Bold(role.Name))).ConfigureAwait(false); - return; - } - else - { - await ReplyErrorLocalizedAsync(strs.not_enough(CurrencySign)); - return; - } - } - else if (entry.Type == ShopEntryType.List) + if (guser.RoleIds.Any(id => id == role.Id)) { - if (entry.Items.Count == 0) + await ReplyErrorLocalizedAsync(strs.shop_role_already_bought).ConfigureAwait(false); + return; + } + + if (await _cs.RemoveAsync(ctx.User.Id, $"Shop purchase - {entry.Type}", entry.Price).ConfigureAwait(false)) + { + try { - await ReplyErrorLocalizedAsync(strs.out_of_stock).ConfigureAwait(false); + await guser.AddRoleAsync(role).ConfigureAwait(false); + } + catch (Exception ex) + { + Log.Warning(ex, "Error adding shop role"); + await _cs.AddAsync(ctx.User.Id, $"Shop error refund", entry.Price).ConfigureAwait(false); + await ReplyErrorLocalizedAsync(strs.shop_role_purchase_error).ConfigureAwait(false); return; } + var profit = GetProfitAmount(entry.Price); + await _cs.AddAsync(entry.AuthorId, $"Shop sell item - {entry.Type}", profit).ConfigureAwait(false); + await _cs.AddAsync(ctx.Client.CurrentUser.Id, $"Shop sell item - cut", entry.Price - profit).ConfigureAwait(false); + await ReplyConfirmLocalizedAsync(strs.shop_role_purchase(Format.Bold(role.Name))).ConfigureAwait(false); + return; + } + else + { + await ReplyErrorLocalizedAsync(strs.not_enough(CurrencySign)); + return; + } + } + else if (entry.Type == ShopEntryType.List) + { + if (entry.Items.Count == 0) + { + await ReplyErrorLocalizedAsync(strs.out_of_stock).ConfigureAwait(false); + return; + } - var item = entry.Items.ToArray()[new NadekoRandom().Next(0, entry.Items.Count)]; + var item = entry.Items.ToArray()[new NadekoRandom().Next(0, entry.Items.Count)]; - if (await _cs.RemoveAsync(ctx.User.Id, $"Shop purchase - {entry.Type}", entry.Price).ConfigureAwait(false)) + if (await _cs.RemoveAsync(ctx.User.Id, $"Shop purchase - {entry.Type}", entry.Price).ConfigureAwait(false)) + { + using (var uow = _db.GetDbContext()) { - using (var uow = _db.GetDbContext()) - { - var x = uow.Set().Remove(item); - uow.SaveChanges(); - } - try - { - await (await ctx.User.GetOrCreateDMChannelAsync().ConfigureAwait(false)) - .EmbedAsync(_eb.Create().WithOkColor() + var x = uow.Set().Remove(item); + uow.SaveChanges(); + } + try + { + await (await ctx.User.GetOrCreateDMChannelAsync().ConfigureAwait(false)) + .EmbedAsync(_eb.Create().WithOkColor() .WithTitle(GetText(strs.shop_purchase(ctx.Guild.Name))) .AddField(GetText(strs.item), item.Text, false) .AddField(GetText(strs.price), entry.Price.ToString(), true) .AddField(GetText(strs.name), entry.Name, true)) - .ConfigureAwait(false); + .ConfigureAwait(false); - await _cs.AddAsync(entry.AuthorId, - $"Shop sell item - {entry.Name}", - GetProfitAmount(entry.Price)).ConfigureAwait(false); - } - catch + await _cs.AddAsync(entry.AuthorId, + $"Shop sell item - {entry.Name}", + GetProfitAmount(entry.Price)).ConfigureAwait(false); + } + catch + { + await _cs.AddAsync(ctx.User.Id, + $"Shop error refund - {entry.Name}", + entry.Price).ConfigureAwait(false); + using (var uow = _db.GetDbContext()) { - await _cs.AddAsync(ctx.User.Id, - $"Shop error refund - {entry.Name}", - entry.Price).ConfigureAwait(false); - using (var uow = _db.GetDbContext()) + var entries = new IndexedCollection(uow.GuildConfigsForId(ctx.Guild.Id, + set => set.Include(x => x.ShopEntries) + .ThenInclude(x => x.Items)).ShopEntries); + entry = entries.ElementAtOrDefault(index); + if (entry != null) { - var entries = new IndexedCollection(uow.GuildConfigsForId(ctx.Guild.Id, - set => set.Include(x => x.ShopEntries) - .ThenInclude(x => x.Items)).ShopEntries); - entry = entries.ElementAtOrDefault(index); - if (entry != null) + if (entry.Items.Add(item)) { - if (entry.Items.Add(item)) - { - uow.SaveChanges(); - } + uow.SaveChanges(); } } - await ReplyErrorLocalizedAsync(strs.shop_buy_error).ConfigureAwait(false); - return; } - await ReplyConfirmLocalizedAsync(strs.shop_item_purchase).ConfigureAwait(false); - } - else - { - await ReplyErrorLocalizedAsync(strs.not_enough(CurrencySign)); + await ReplyErrorLocalizedAsync(strs.shop_buy_error).ConfigureAwait(false); return; } + await ReplyConfirmLocalizedAsync(strs.shop_item_purchase).ConfigureAwait(false); } - - } - - private static long GetProfitAmount(int price) => - (int)(Math.Ceiling(0.90 * price)); - - [NadekoCommand, Aliases] - [RequireContext(ContextType.Guild)] - [UserPerm(GuildPerm.Administrator)] - [BotPerm(GuildPerm.ManageRoles)] - public async Task ShopAdd(Role _, int price, [Leftover] IRole role) - { - if (price < 1) - return; - - var entry = new ShopEntry() - { - Name = "-", - Price = price, - Type = ShopEntryType.Role, - AuthorId = ctx.User.Id, - RoleId = role.Id, - RoleName = role.Name - }; - using (var uow = _db.GetDbContext()) - { - var entries = new IndexedCollection(uow.GuildConfigsForId(ctx.Guild.Id, - set => set.Include(x => x.ShopEntries) - .ThenInclude(x => x.Items)).ShopEntries) - { - entry - }; - uow.GuildConfigsForId(ctx.Guild.Id, set => set).ShopEntries = entries; - uow.SaveChanges(); - } - await ctx.Channel.EmbedAsync(EntryToEmbed(entry) - .WithTitle(GetText(strs.shop_item_add))).ConfigureAwait(false); - } - - [NadekoCommand, Aliases] - [RequireContext(ContextType.Guild)] - [UserPerm(GuildPerm.Administrator)] - public async Task ShopAdd(List _, int price, [Leftover] string name) - { - if (price < 1) - return; - - var entry = new ShopEntry() - { - Name = name.TrimTo(100), - Price = price, - Type = ShopEntryType.List, - AuthorId = ctx.User.Id, - Items = new HashSet(), - }; - using (var uow = _db.GetDbContext()) - { - var entries = new IndexedCollection(uow.GuildConfigsForId(ctx.Guild.Id, - set => set.Include(x => x.ShopEntries) - .ThenInclude(x => x.Items)).ShopEntries) - { - entry - }; - uow.GuildConfigsForId(ctx.Guild.Id, set => set).ShopEntries = entries; - uow.SaveChanges(); - } - - await ctx.Channel.EmbedAsync(EntryToEmbed(entry) - .WithTitle(GetText(strs.shop_item_add))).ConfigureAwait(false); - } - - [NadekoCommand, Aliases] - [RequireContext(ContextType.Guild)] - [UserPerm(GuildPerm.Administrator)] - public async Task ShopListAdd(int index, [Leftover] string itemText) - { - index -= 1; - if (index < 0) - return; - var item = new ShopEntryItem() - { - Text = itemText - }; - ShopEntry entry; - bool rightType = false; - bool added = false; - using (var uow = _db.GetDbContext()) - { - var entries = new IndexedCollection(uow.GuildConfigsForId(ctx.Guild.Id, - set => set.Include(x => x.ShopEntries) - .ThenInclude(x => x.Items)).ShopEntries); - entry = entries.ElementAtOrDefault(index); - if (entry != null && (rightType = (entry.Type == ShopEntryType.List))) - { - if (added = entry.Items.Add(item)) - { - uow.SaveChanges(); - } - } - } - if (entry is null) - await ReplyErrorLocalizedAsync(strs.shop_item_not_found).ConfigureAwait(false); - else if (!rightType) - await ReplyErrorLocalizedAsync(strs.shop_item_wrong_type).ConfigureAwait(false); - else if (added == false) - await ReplyErrorLocalizedAsync(strs.shop_list_item_not_unique).ConfigureAwait(false); else - await ReplyConfirmLocalizedAsync(strs.shop_list_item_added).ConfigureAwait(false); + { + await ReplyErrorLocalizedAsync(strs.not_enough(CurrencySign)); + return; + } } - [NadekoCommand, Aliases] - [RequireContext(ContextType.Guild)] - [UserPerm(GuildPerm.Administrator)] - public async Task ShopRemove(int index) - { - index -= 1; - if (index < 0) - return; - ShopEntry removed; - using (var uow = _db.GetDbContext()) - { - var config = uow.GuildConfigsForId(ctx.Guild.Id, set => set - .Include(x => x.ShopEntries) - .ThenInclude(x => x.Items)); + } - var entries = new IndexedCollection(config.ShopEntries); - removed = entries.ElementAtOrDefault(index); - if (removed != null) + private static long GetProfitAmount(int price) => + (int)(Math.Ceiling(0.90 * price)); + + [NadekoCommand, Aliases] + [RequireContext(ContextType.Guild)] + [UserPerm(GuildPerm.Administrator)] + [BotPerm(GuildPerm.ManageRoles)] + public async Task ShopAdd(Role _, int price, [Leftover] IRole role) + { + if (price < 1) + return; + + var entry = new ShopEntry() + { + Name = "-", + Price = price, + Type = ShopEntryType.Role, + AuthorId = ctx.User.Id, + RoleId = role.Id, + RoleName = role.Name + }; + using (var uow = _db.GetDbContext()) + { + var entries = new IndexedCollection(uow.GuildConfigsForId(ctx.Guild.Id, + set => set.Include(x => x.ShopEntries) + .ThenInclude(x => x.Items)).ShopEntries) + { + entry + }; + uow.GuildConfigsForId(ctx.Guild.Id, set => set).ShopEntries = entries; + uow.SaveChanges(); + } + await ctx.Channel.EmbedAsync(EntryToEmbed(entry) + .WithTitle(GetText(strs.shop_item_add))).ConfigureAwait(false); + } + + [NadekoCommand, Aliases] + [RequireContext(ContextType.Guild)] + [UserPerm(GuildPerm.Administrator)] + public async Task ShopAdd(List _, int price, [Leftover] string name) + { + if (price < 1) + return; + + var entry = new ShopEntry() + { + Name = name.TrimTo(100), + Price = price, + Type = ShopEntryType.List, + AuthorId = ctx.User.Id, + Items = new HashSet(), + }; + using (var uow = _db.GetDbContext()) + { + var entries = new IndexedCollection(uow.GuildConfigsForId(ctx.Guild.Id, + set => set.Include(x => x.ShopEntries) + .ThenInclude(x => x.Items)).ShopEntries) + { + entry + }; + uow.GuildConfigsForId(ctx.Guild.Id, set => set).ShopEntries = entries; + uow.SaveChanges(); + } + + await ctx.Channel.EmbedAsync(EntryToEmbed(entry) + .WithTitle(GetText(strs.shop_item_add))).ConfigureAwait(false); + } + + [NadekoCommand, Aliases] + [RequireContext(ContextType.Guild)] + [UserPerm(GuildPerm.Administrator)] + public async Task ShopListAdd(int index, [Leftover] string itemText) + { + index -= 1; + if (index < 0) + return; + var item = new ShopEntryItem() + { + Text = itemText + }; + ShopEntry entry; + bool rightType = false; + bool added = false; + using (var uow = _db.GetDbContext()) + { + var entries = new IndexedCollection(uow.GuildConfigsForId(ctx.Guild.Id, + set => set.Include(x => x.ShopEntries) + .ThenInclude(x => x.Items)).ShopEntries); + entry = entries.ElementAtOrDefault(index); + if (entry != null && (rightType = (entry.Type == ShopEntryType.List))) + { + if (added = entry.Items.Add(item)) { - uow.RemoveRange(removed.Items); - uow.Remove(removed); uow.SaveChanges(); } } - - if (removed is null) - await ReplyErrorLocalizedAsync(strs.shop_item_not_found).ConfigureAwait(false); - else - await ctx.Channel.EmbedAsync(EntryToEmbed(removed) - .WithTitle(GetText(strs.shop_item_rm))).ConfigureAwait(false); } + if (entry is null) + await ReplyErrorLocalizedAsync(strs.shop_item_not_found).ConfigureAwait(false); + else if (!rightType) + await ReplyErrorLocalizedAsync(strs.shop_item_wrong_type).ConfigureAwait(false); + else if (added == false) + await ReplyErrorLocalizedAsync(strs.shop_list_item_not_unique).ConfigureAwait(false); + else + await ReplyConfirmLocalizedAsync(strs.shop_list_item_added).ConfigureAwait(false); + } - [NadekoCommand, Aliases] - [RequireContext(ContextType.Guild)] - [UserPerm(GuildPerm.Administrator)] - public async Task ShopChangePrice(int index, int price) + [NadekoCommand, Aliases] + [RequireContext(ContextType.Guild)] + [UserPerm(GuildPerm.Administrator)] + public async Task ShopRemove(int index) + { + index -= 1; + if (index < 0) + return; + ShopEntry removed; + using (var uow = _db.GetDbContext()) { - if (--index < 0 || price <= 0) - return; + var config = uow.GuildConfigsForId(ctx.Guild.Id, set => set + .Include(x => x.ShopEntries) + .ThenInclude(x => x.Items)); - var succ = await _service.ChangeEntryPriceAsync(ctx.Guild.Id, index, price); - if (succ) + var entries = new IndexedCollection(config.ShopEntries); + removed = entries.ElementAtOrDefault(index); + if (removed != null) { - await ShopInternalAsync(index / 9); - await ctx.OkAsync(); - } - else - { - await ctx.ErrorAsync(); + uow.RemoveRange(removed.Items); + uow.Remove(removed); + uow.SaveChanges(); } } - [NadekoCommand, Aliases] - [RequireContext(ContextType.Guild)] - [UserPerm(GuildPerm.Administrator)] - public async Task ShopChangeName(int index, [Leftover] string newName) + if (removed is null) + await ReplyErrorLocalizedAsync(strs.shop_item_not_found).ConfigureAwait(false); + else + await ctx.Channel.EmbedAsync(EntryToEmbed(removed) + .WithTitle(GetText(strs.shop_item_rm))).ConfigureAwait(false); + } + + [NadekoCommand, Aliases] + [RequireContext(ContextType.Guild)] + [UserPerm(GuildPerm.Administrator)] + public async Task ShopChangePrice(int index, int price) + { + if (--index < 0 || price <= 0) + return; + + var succ = await _service.ChangeEntryPriceAsync(ctx.Guild.Id, index, price); + if (succ) { - if (--index < 0 || string.IsNullOrWhiteSpace(newName)) - return; + await ShopInternalAsync(index / 9); + await ctx.OkAsync(); + } + else + { + await ctx.ErrorAsync(); + } + } + + [NadekoCommand, Aliases] + [RequireContext(ContextType.Guild)] + [UserPerm(GuildPerm.Administrator)] + public async Task ShopChangeName(int index, [Leftover] string newName) + { + if (--index < 0 || string.IsNullOrWhiteSpace(newName)) + return; - var succ = await _service.ChangeEntryNameAsync(ctx.Guild.Id, index, newName); - if (succ) - { - await ShopInternalAsync(index / 9); - await ctx.OkAsync(); - } - else - { - await ctx.ErrorAsync(); - } - } - - [NadekoCommand, Aliases] - [RequireContext(ContextType.Guild)] - [UserPerm(GuildPerm.Administrator)] - public async Task ShopSwap(int index1, int index2) + var succ = await _service.ChangeEntryNameAsync(ctx.Guild.Id, index, newName); + if (succ) { - if (--index1 < 0 || --index2 < 0 || index1 == index2) - return; + await ShopInternalAsync(index / 9); + await ctx.OkAsync(); + } + else + { + await ctx.ErrorAsync(); + } + } + + [NadekoCommand, Aliases] + [RequireContext(ContextType.Guild)] + [UserPerm(GuildPerm.Administrator)] + public async Task ShopSwap(int index1, int index2) + { + if (--index1 < 0 || --index2 < 0 || index1 == index2) + return; - var succ = await _service.SwapEntriesAsync(ctx.Guild.Id, index1, index2); - if (succ) - { - await ShopInternalAsync(index1 / 9); - await ctx.OkAsync(); - } - else - { - await ctx.ErrorAsync(); - } + var succ = await _service.SwapEntriesAsync(ctx.Guild.Id, index1, index2); + if (succ) + { + await ShopInternalAsync(index1 / 9); + await ctx.OkAsync(); } + else + { + await ctx.ErrorAsync(); + } + } - [NadekoCommand, Aliases] - [RequireContext(ContextType.Guild)] - [UserPerm(GuildPerm.Administrator)] - public async Task ShopMove(int fromIndex, int toIndex) - { - if (--fromIndex < 0 || --toIndex < 0 || fromIndex == toIndex) - return; + [NadekoCommand, Aliases] + [RequireContext(ContextType.Guild)] + [UserPerm(GuildPerm.Administrator)] + public async Task ShopMove(int fromIndex, int toIndex) + { + if (--fromIndex < 0 || --toIndex < 0 || fromIndex == toIndex) + return; - var succ = await _service.MoveEntryAsync(ctx.Guild.Id, fromIndex, toIndex); - if (succ) - { - await ShopInternalAsync(toIndex / 9); - await ctx.OkAsync(); - } - else - { - await ctx.ErrorAsync(); - } + var succ = await _service.MoveEntryAsync(ctx.Guild.Id, fromIndex, toIndex); + if (succ) + { + await ShopInternalAsync(toIndex / 9); + await ctx.OkAsync(); } + else + { + await ctx.ErrorAsync(); + } + } - public IEmbedBuilder EntryToEmbed(ShopEntry entry) + public IEmbedBuilder EntryToEmbed(ShopEntry entry) + { + var embed = _eb.Create().WithOkColor(); + + if (entry.Type == ShopEntryType.Role) + return embed.AddField(GetText(strs.name), GetText(strs.shop_role(Format.Bold(ctx.Guild.GetRole(entry.RoleId)?.Name ?? "MISSING_ROLE"))), true) + .AddField(GetText(strs.price), entry.Price.ToString(), true) + .AddField(GetText(strs.type), entry.Type.ToString(), true); + else if (entry.Type == ShopEntryType.List) + return embed.AddField(GetText(strs.name), entry.Name, true) + .AddField(GetText(strs.price), entry.Price.ToString(), true) + .AddField(GetText(strs.type), GetText(strs.random_unique_item), true); + //else if (entry.Type == ShopEntryType.Infinite_List) + // return embed.AddField(GetText(strs.name), GetText(strs.shop_role(Format.Bold(entry.RoleName)), true)) + // .AddField(GetText(strs.price), entry.Price.ToString(), true) + // .AddField(GetText(strs.type), entry.Type.ToString(), true); + else return null; + } + + public string EntryToString(ShopEntry entry) + { + if (entry.Type == ShopEntryType.Role) { - var embed = _eb.Create().WithOkColor(); - - if (entry.Type == ShopEntryType.Role) - return embed.AddField(GetText(strs.name), GetText(strs.shop_role(Format.Bold(ctx.Guild.GetRole(entry.RoleId)?.Name ?? "MISSING_ROLE"))), true) - .AddField(GetText(strs.price), entry.Price.ToString(), true) - .AddField(GetText(strs.type), entry.Type.ToString(), true); - else if (entry.Type == ShopEntryType.List) - return embed.AddField(GetText(strs.name), entry.Name, true) - .AddField(GetText(strs.price), entry.Price.ToString(), true) - .AddField(GetText(strs.type), GetText(strs.random_unique_item), true); - //else if (entry.Type == ShopEntryType.Infinite_List) - // return embed.AddField(GetText(strs.name), GetText(strs.shop_role(Format.Bold(entry.RoleName)), true)) - // .AddField(GetText(strs.price), entry.Price.ToString(), true) - // .AddField(GetText(strs.type), entry.Type.ToString(), true); - else return null; + return GetText(strs.shop_role(Format.Bold(ctx.Guild.GetRole(entry.RoleId)?.Name ?? "MISSING_ROLE"))); } - - public string EntryToString(ShopEntry entry) + else if (entry.Type == ShopEntryType.List) { - if (entry.Type == ShopEntryType.Role) - { - return GetText(strs.shop_role(Format.Bold(ctx.Guild.GetRole(entry.RoleId)?.Name ?? "MISSING_ROLE"))); - } - else if (entry.Type == ShopEntryType.List) - { - return GetText(strs.unique_items_left(entry.Items.Count)) + "\n" + entry.Name; - } - //else if (entry.Type == ShopEntryType.Infinite_List) - //{ - - //} - return ""; + return GetText(strs.unique_items_left(entry.Items.Count)) + "\n" + entry.Name; } + //else if (entry.Type == ShopEntryType.Infinite_List) + //{ + + //} + return ""; } } } \ No newline at end of file diff --git a/src/NadekoBot/Modules/Gambling/SlotCommands.cs b/src/NadekoBot/Modules/Gambling/SlotCommands.cs index 75784d801..899329bb5 100644 --- a/src/NadekoBot/Modules/Gambling/SlotCommands.cs +++ b/src/NadekoBot/Modules/Gambling/SlotCommands.cs @@ -1,9 +1,6 @@ using Discord; using Discord.Commands; using NadekoBot.Extensions; -using System; -using System.Collections.Generic; -using System.Linq; using System.Text; using System.Threading; using System.Threading.Tasks; @@ -21,248 +18,247 @@ using SixLabors.ImageSharp.Drawing.Processing; using SixLabors.ImageSharp.PixelFormats; using Color = SixLabors.ImageSharp.Color; -namespace NadekoBot.Modules.Gambling +namespace NadekoBot.Modules.Gambling; + +public partial class Gambling { - public partial class Gambling + [Group] + public class SlotCommands : GamblingSubmodule { - [Group] - public class SlotCommands : GamblingSubmodule + private static long _totalBet; + private static long _totalPaidOut; + + private static readonly HashSet _runningUsers = new HashSet(); + + //here is a payout chart + //https://lh6.googleusercontent.com/-i1hjAJy_kN4/UswKxmhrbPI/AAAAAAAAB1U/82wq_4ZZc-Y/DE6B0895-6FC1-48BE-AC4F-14D1B91AB75B.jpg + //thanks to judge for helping me with this + + private readonly IImageCache _images; + private FontProvider _fonts; + private readonly DbService _db; + + public SlotCommands(IDataCache data, + FontProvider fonts, DbService db, + GamblingConfigService gamb) : base(gamb) { - private static long _totalBet; - private static long _totalPaidOut; + _images = data.LocalImages; + _fonts = fonts; + _db = db; + } - private static readonly HashSet _runningUsers = new HashSet(); + public sealed class SlotMachine + { + public const int MaxValue = 5; - //here is a payout chart - //https://lh6.googleusercontent.com/-i1hjAJy_kN4/UswKxmhrbPI/AAAAAAAAB1U/82wq_4ZZc-Y/DE6B0895-6FC1-48BE-AC4F-14D1B91AB75B.jpg - //thanks to judge for helping me with this - - private readonly IImageCache _images; - private FontProvider _fonts; - private readonly DbService _db; - - public SlotCommands(IDataCache data, - FontProvider fonts, DbService db, - GamblingConfigService gamb) : base(gamb) + static readonly List> _winningCombos = new List>() { - _images = data.LocalImages; - _fonts = fonts; - _db = db; + //three flowers + (arr) => arr.All(a=>a==MaxValue) ? 30 : 0, + //three of the same + (arr) => !arr.Any(a => a != arr[0]) ? 10 : 0, + //two flowers + (arr) => arr.Count(a => a == MaxValue) == 2 ? 4 : 0, + //one flower + (arr) => arr.Any(a => a == MaxValue) ? 1 : 0, + }; + + public static SlotResult Pull() + { + var numbers = new int[3]; + for (var i = 0; i < numbers.Length; i++) + { + numbers[i] = new NadekoRandom().Next(0, MaxValue + 1); + } + var multi = 0; + foreach (var t in _winningCombos) + { + multi = t(numbers); + if (multi != 0) + break; + } + + return new SlotResult(numbers, multi); } - public sealed class SlotMachine + public struct SlotResult { - public const int MaxValue = 5; - - static readonly List> _winningCombos = new List>() + public int[] Numbers { get; } + public int Multiplier { get; } + public SlotResult(int[] nums, int multi) { - //three flowers - (arr) => arr.All(a=>a==MaxValue) ? 30 : 0, - //three of the same - (arr) => !arr.Any(a => a != arr[0]) ? 10 : 0, - //two flowers - (arr) => arr.Count(a => a == MaxValue) == 2 ? 4 : 0, - //one flower - (arr) => arr.Any(a => a == MaxValue) ? 1 : 0, - }; + Numbers = nums; + Multiplier = multi; + } + } + } - public static SlotResult Pull() + [NadekoCommand, Aliases] + [OwnerOnly] + public async Task SlotStats() + { + //i remembered to not be a moron + var paid = _totalPaidOut; + var bet = _totalBet; + + if (bet <= 0) + bet = 1; + + var embed = _eb.Create() + .WithOkColor() + .WithTitle("Slot Stats") + .AddField("Total Bet", bet.ToString(), true) + .AddField("Paid Out", paid.ToString(), true) + .WithFooter($"Payout Rate: {paid * 1.0 / bet * 100:f4}%"); + + await ctx.Channel.EmbedAsync(embed).ConfigureAwait(false); + } + + [NadekoCommand, Aliases] + [OwnerOnly] + public async Task SlotTest(int tests = 1000) + { + if (tests <= 0) + return; + //multi vs how many times it occured + var dict = new Dictionary(); + for (int i = 0; i < tests; i++) + { + var res = SlotMachine.Pull(); + if (dict.ContainsKey(res.Multiplier)) + dict[res.Multiplier] += 1; + else + dict.Add(res.Multiplier, 1); + } + + var sb = new StringBuilder(); + const int bet = 1; + int payout = 0; + foreach (var key in dict.Keys.OrderByDescending(x => x)) + { + sb.AppendLine($"x{key} occured {dict[key]} times. {dict[key] * 1.0f / tests * 100}%"); + payout += key * dict[key]; + } + await SendConfirmAsync("Slot Test Results", sb.ToString(), + footer: $"Total Bet: {tests * bet} | Payout: {payout * bet} | {payout * 1.0f / tests * 100}%").ConfigureAwait(false); + } + + [NadekoCommand, Aliases] + public async Task Slot(ShmartNumber amount) + { + if (!_runningUsers.Add(ctx.User.Id)) + return; + + try + { + if (!await CheckBetMandatory(amount).ConfigureAwait(false)) + return; + + await ctx.Channel.TriggerTypingAsync().ConfigureAwait(false); + + var result = await _service.SlotAsync(ctx.User.Id, amount); + + if (result.Error != GamblingError.None) + { + if (result.Error == GamblingError.NotEnough) + { + await ReplyErrorLocalizedAsync(strs.not_enough(CurrencySign)); + } + + return; + } + + Interlocked.Add(ref _totalBet, amount); + Interlocked.Add(ref _totalPaidOut, result.Won); + + long ownedAmount; + using (var uow = _db.GetDbContext()) + { + ownedAmount = uow.Set() + .FirstOrDefault(x => x.UserId == ctx.User.Id) + ?.CurrencyAmount ?? 0; + } + + using (var bgImage = Image.Load(_images.SlotBackground, out var format)) { var numbers = new int[3]; - for (var i = 0; i < numbers.Length; i++) - { - numbers[i] = new NadekoRandom().Next(0, MaxValue + 1); - } - var multi = 0; - foreach (var t in _winningCombos) - { - multi = t(numbers); - if (multi != 0) - break; - } + result.Rolls.CopyTo(numbers, 0); - return new SlotResult(numbers, multi); - } - - public struct SlotResult - { - public int[] Numbers { get; } - public int Multiplier { get; } - public SlotResult(int[] nums, int multi) - { - Numbers = nums; - Multiplier = multi; - } - } - } - - [NadekoCommand, Aliases] - [OwnerOnly] - public async Task SlotStats() - { - //i remembered to not be a moron - var paid = _totalPaidOut; - var bet = _totalBet; - - if (bet <= 0) - bet = 1; - - var embed = _eb.Create() - .WithOkColor() - .WithTitle("Slot Stats") - .AddField("Total Bet", bet.ToString(), true) - .AddField("Paid Out", paid.ToString(), true) - .WithFooter($"Payout Rate: {paid * 1.0 / bet * 100:f4}%"); - - await ctx.Channel.EmbedAsync(embed).ConfigureAwait(false); - } - - [NadekoCommand, Aliases] - [OwnerOnly] - public async Task SlotTest(int tests = 1000) - { - if (tests <= 0) - return; - //multi vs how many times it occured - var dict = new Dictionary(); - for (int i = 0; i < tests; i++) - { - var res = SlotMachine.Pull(); - if (dict.ContainsKey(res.Multiplier)) - dict[res.Multiplier] += 1; - else - dict.Add(res.Multiplier, 1); - } - - var sb = new StringBuilder(); - const int bet = 1; - int payout = 0; - foreach (var key in dict.Keys.OrderByDescending(x => x)) - { - sb.AppendLine($"x{key} occured {dict[key]} times. {dict[key] * 1.0f / tests * 100}%"); - payout += key * dict[key]; - } - await SendConfirmAsync("Slot Test Results", sb.ToString(), - footer: $"Total Bet: {tests * bet} | Payout: {payout * bet} | {payout * 1.0f / tests * 100}%").ConfigureAwait(false); - } - - [NadekoCommand, Aliases] - public async Task Slot(ShmartNumber amount) - { - if (!_runningUsers.Add(ctx.User.Id)) - return; - - try - { - if (!await CheckBetMandatory(amount).ConfigureAwait(false)) - return; - - await ctx.Channel.TriggerTypingAsync().ConfigureAwait(false); - - var result = await _service.SlotAsync(ctx.User.Id, amount); - - if (result.Error != GamblingError.None) - { - if (result.Error == GamblingError.NotEnough) - { - await ReplyErrorLocalizedAsync(strs.not_enough(CurrencySign)); - } - - return; - } - - Interlocked.Add(ref _totalBet, amount); - Interlocked.Add(ref _totalPaidOut, result.Won); - - long ownedAmount; - using (var uow = _db.GetDbContext()) - { - ownedAmount = uow.Set() - .FirstOrDefault(x => x.UserId == ctx.User.Id) - ?.CurrencyAmount ?? 0; - } - - using (var bgImage = Image.Load(_images.SlotBackground, out var format)) - { - var numbers = new int[3]; - result.Rolls.CopyTo(numbers, 0); - - Color fontColor = _config.Slots.CurrencyFontColor; + Color fontColor = _config.Slots.CurrencyFontColor; - bgImage.Mutate(x => x.DrawText(new TextGraphicsOptions - { - TextOptions = new TextOptions() - { - HorizontalAlignment = HorizontalAlignment.Center, - VerticalAlignment = VerticalAlignment.Center, - WrapTextWidth = 140, - } - }, result.Won.ToString(), _fonts.DottyFont.CreateFont(65), fontColor, - new PointF(227, 92))); + bgImage.Mutate(x => x.DrawText(new TextGraphicsOptions + { + TextOptions = new TextOptions() + { + HorizontalAlignment = HorizontalAlignment.Center, + VerticalAlignment = VerticalAlignment.Center, + WrapTextWidth = 140, + } + }, result.Won.ToString(), _fonts.DottyFont.CreateFont(65), fontColor, + new PointF(227, 92))); - var bottomFont = _fonts.DottyFont.CreateFont(50); + var bottomFont = _fonts.DottyFont.CreateFont(50); - bgImage.Mutate(x => x.DrawText(new TextGraphicsOptions - { - TextOptions = new TextOptions() - { - HorizontalAlignment = HorizontalAlignment.Center, - VerticalAlignment = VerticalAlignment.Center, - WrapTextWidth = 135, - } - }, amount.ToString(), bottomFont, fontColor, - new PointF(129, 472))); + bgImage.Mutate(x => x.DrawText(new TextGraphicsOptions + { + TextOptions = new TextOptions() + { + HorizontalAlignment = HorizontalAlignment.Center, + VerticalAlignment = VerticalAlignment.Center, + WrapTextWidth = 135, + } + }, amount.ToString(), bottomFont, fontColor, + new PointF(129, 472))); - bgImage.Mutate(x => x.DrawText(new TextGraphicsOptions - { - TextOptions = new TextOptions() - { - HorizontalAlignment = HorizontalAlignment.Center, - VerticalAlignment = VerticalAlignment.Center, - WrapTextWidth = 135, - } - }, ownedAmount.ToString(), bottomFont, fontColor, - new PointF(325, 472))); - //sw.PrintLap("drew red text"); + bgImage.Mutate(x => x.DrawText(new TextGraphicsOptions + { + TextOptions = new TextOptions() + { + HorizontalAlignment = HorizontalAlignment.Center, + VerticalAlignment = VerticalAlignment.Center, + WrapTextWidth = 135, + } + }, ownedAmount.ToString(), bottomFont, fontColor, + new PointF(325, 472))); + //sw.PrintLap("drew red text"); - for (var i = 0; i < 3; i++) - { - using (var img = Image.Load(_images.SlotEmojis[numbers[i]])) - { - bgImage.Mutate(x => x.DrawImage(img, new Point(148 + 105 * i, 217), 1f)); - } - } + for (var i = 0; i < 3; i++) + { + using (var img = Image.Load(_images.SlotEmojis[numbers[i]])) + { + bgImage.Mutate(x => x.DrawImage(img, new Point(148 + 105 * i, 217), 1f)); + } + } - var msg = GetText(strs.better_luck); - if (result.Multiplier > 0) - { - if (result.Multiplier == 1f) - msg = GetText(strs.slot_single(CurrencySign, 1)); - else if (result.Multiplier == 4f) - msg = GetText(strs.slot_two(CurrencySign, 4)); - else if (result.Multiplier == 10f) - msg = GetText(strs.slot_three(10)); - else if (result.Multiplier == 30f) - msg = GetText(strs.slot_jackpot(30)); - } + var msg = GetText(strs.better_luck); + if (result.Multiplier > 0) + { + if (result.Multiplier == 1f) + msg = GetText(strs.slot_single(CurrencySign, 1)); + else if (result.Multiplier == 4f) + msg = GetText(strs.slot_two(CurrencySign, 4)); + else if (result.Multiplier == 10f) + msg = GetText(strs.slot_three(10)); + else if (result.Multiplier == 30f) + msg = GetText(strs.slot_jackpot(30)); + } - using (var imgStream = bgImage.ToStream()) - { - await ctx.Channel.SendFileAsync(imgStream, - filename: "result.png", - text: Format.Bold(ctx.User.ToString()) + " " + msg).ConfigureAwait(false); - } - } - } - finally - { - var _ = Task.Run(async () => - { - await Task.Delay(1000).ConfigureAwait(false); - _runningUsers.Remove(ctx.User.Id); - }); - } + using (var imgStream = bgImage.ToStream()) + { + await ctx.Channel.SendFileAsync(imgStream, + filename: "result.png", + text: Format.Bold(ctx.User.ToString()) + " " + msg).ConfigureAwait(false); + } + } + } + finally + { + var _ = Task.Run(async () => + { + await Task.Delay(1000).ConfigureAwait(false); + _runningUsers.Remove(ctx.User.Id); + }); } } } -} +} \ No newline at end of file diff --git a/src/NadekoBot/Modules/Gambling/WaifuClaimCommands.cs b/src/NadekoBot/Modules/Gambling/WaifuClaimCommands.cs index 30be8faaf..dd3a27202 100644 --- a/src/NadekoBot/Modules/Gambling/WaifuClaimCommands.cs +++ b/src/NadekoBot/Modules/Gambling/WaifuClaimCommands.cs @@ -4,359 +4,356 @@ using NadekoBot.Common.Attributes; using NadekoBot.Modules.Gambling.Common.Waifu; using NadekoBot.Extensions; using NadekoBot.Modules.Gambling.Services; -using System; -using System.Linq; using System.Threading.Tasks; using NadekoBot.Modules.Gambling.Common; -namespace NadekoBot.Modules.Gambling +namespace NadekoBot.Modules.Gambling; + +public partial class Gambling { - public partial class Gambling + [Group] + public class WaifuClaimCommands : GamblingSubmodule { - [Group] - public class WaifuClaimCommands : GamblingSubmodule + + public WaifuClaimCommands(GamblingConfigService gamblingConfService) : base(gamblingConfService) { + } - public WaifuClaimCommands(GamblingConfigService gamblingConfService) : base(gamblingConfService) + [NadekoCommand, Aliases] + public async Task WaifuReset() + { + var price = _service.GetResetPrice(ctx.User); + var embed = _eb.Create() + .WithTitle(GetText(strs.waifu_reset_confirm)) + .WithDescription(GetText(strs.waifu_reset_price(Format.Bold(price + CurrencySign)))); + + if (!await PromptUserConfirmAsync(embed)) + return; + + if (await _service.TryReset(ctx.User)) { + await ReplyConfirmLocalizedAsync(strs.waifu_reset); + return; + } + await ReplyErrorLocalizedAsync(strs.waifu_reset_fail); + } + + [NadekoCommand, Aliases] + [RequireContext(ContextType.Guild)] + public async Task WaifuClaim(int amount, [Leftover]IUser target) + { + if (amount < _config.Waifu.MinPrice) + { + await ReplyErrorLocalizedAsync(strs.waifu_isnt_cheap(_config.Waifu.MinPrice + CurrencySign)); + return; } - [NadekoCommand, Aliases] - public async Task WaifuReset() + if (target.Id == ctx.User.Id) { - var price = _service.GetResetPrice(ctx.User); - var embed = _eb.Create() - .WithTitle(GetText(strs.waifu_reset_confirm)) - .WithDescription(GetText(strs.waifu_reset_price(Format.Bold(price + CurrencySign)))); - - if (!await PromptUserConfirmAsync(embed)) - return; - - if (await _service.TryReset(ctx.User)) - { - await ReplyConfirmLocalizedAsync(strs.waifu_reset); - return; - } - await ReplyErrorLocalizedAsync(strs.waifu_reset_fail); + await ReplyErrorLocalizedAsync(strs.waifu_not_yourself); + return; } - [NadekoCommand, Aliases] - [RequireContext(ContextType.Guild)] - public async Task WaifuClaim(int amount, [Leftover]IUser target) + var (w, isAffinity, result) = await _service.ClaimWaifuAsync(ctx.User, target, amount); + + if (result == WaifuClaimResult.InsufficientAmount) { - if (amount < _config.Waifu.MinPrice) - { - await ReplyErrorLocalizedAsync(strs.waifu_isnt_cheap(_config.Waifu.MinPrice + CurrencySign)); - return; - } - - if (target.Id == ctx.User.Id) - { - await ReplyErrorLocalizedAsync(strs.waifu_not_yourself); - return; - } - - var (w, isAffinity, result) = await _service.ClaimWaifuAsync(ctx.User, target, amount); - - if (result == WaifuClaimResult.InsufficientAmount) - { - await ReplyErrorLocalizedAsync(strs.waifu_not_enough(Math.Ceiling(w.Price * (isAffinity ? 0.88f : 1.1f)))); - return; - } - if (result == WaifuClaimResult.NotEnoughFunds) - { - await ReplyErrorLocalizedAsync(strs.not_enough(CurrencySign)); - return; - } - var msg = GetText(strs.waifu_claimed( - Format.Bold(target.ToString()), - amount + CurrencySign)); - if (w.Affinity?.UserId == ctx.User.Id) - msg += "\n" + GetText(strs.waifu_fulfilled(target, w.Price + CurrencySign)); - else - msg = " " + msg; - await SendConfirmAsync(ctx.User.Mention + msg); + await ReplyErrorLocalizedAsync(strs.waifu_not_enough(Math.Ceiling(w.Price * (isAffinity ? 0.88f : 1.1f)))); + return; } + if (result == WaifuClaimResult.NotEnoughFunds) + { + await ReplyErrorLocalizedAsync(strs.not_enough(CurrencySign)); + return; + } + var msg = GetText(strs.waifu_claimed( + Format.Bold(target.ToString()), + amount + CurrencySign)); + if (w.Affinity?.UserId == ctx.User.Id) + msg += "\n" + GetText(strs.waifu_fulfilled(target, w.Price + CurrencySign)); + else + msg = " " + msg; + await SendConfirmAsync(ctx.User.Mention + msg); + } - [NadekoCommand, Aliases] - [RequireContext(ContextType.Guild)] - [Priority(0)] - public async Task WaifuTransfer(ulong waifuId, IUser newOwner) + [NadekoCommand, Aliases] + [RequireContext(ContextType.Guild)] + [Priority(0)] + public async Task WaifuTransfer(ulong waifuId, IUser newOwner) + { + if (!await _service.WaifuTransfer(ctx.User, waifuId, newOwner) + ) { - if (!await _service.WaifuTransfer(ctx.User, waifuId, newOwner) - ) - { - await ReplyErrorLocalizedAsync(strs.waifu_transfer_fail); - return; - } - - await ReplyConfirmLocalizedAsync(strs.waifu_transfer_success( - Format.Bold(waifuId.ToString()), - Format.Bold(ctx.User.ToString()), - Format.Bold(newOwner.ToString()))); + await ReplyErrorLocalizedAsync(strs.waifu_transfer_fail); + return; } - [NadekoCommand, Aliases] - [RequireContext(ContextType.Guild)] - [Priority(1)] - public async Task WaifuTransfer(IUser waifu, IUser newOwner) - { - if (!await _service.WaifuTransfer(ctx.User, waifu.Id, newOwner)) - { - await ReplyErrorLocalizedAsync(strs.waifu_transfer_fail); - return; - } + await ReplyConfirmLocalizedAsync(strs.waifu_transfer_success( + Format.Bold(waifuId.ToString()), + Format.Bold(ctx.User.ToString()), + Format.Bold(newOwner.ToString()))); + } - await ReplyConfirmLocalizedAsync(strs.waifu_transfer_success( - Format.Bold(waifu.ToString()), - Format.Bold(ctx.User.ToString()), - Format.Bold(newOwner.ToString()))); + [NadekoCommand, Aliases] + [RequireContext(ContextType.Guild)] + [Priority(1)] + public async Task WaifuTransfer(IUser waifu, IUser newOwner) + { + if (!await _service.WaifuTransfer(ctx.User, waifu.Id, newOwner)) + { + await ReplyErrorLocalizedAsync(strs.waifu_transfer_fail); + return; } - [NadekoCommand, Aliases] - [RequireContext(ContextType.Guild)] - [Priority(-1)] - public Task Divorce([Leftover] string target) - { - var waifuUserId = _service.GetWaifuUserId(ctx.User.Id, target); - if (waifuUserId == default) - { - return ReplyErrorLocalizedAsync(strs.waifu_not_yours); - } + await ReplyConfirmLocalizedAsync(strs.waifu_transfer_success( + Format.Bold(waifu.ToString()), + Format.Bold(ctx.User.ToString()), + Format.Bold(newOwner.ToString()))); + } - return Divorce(waifuUserId); + [NadekoCommand, Aliases] + [RequireContext(ContextType.Guild)] + [Priority(-1)] + public Task Divorce([Leftover] string target) + { + var waifuUserId = _service.GetWaifuUserId(ctx.User.Id, target); + if (waifuUserId == default) + { + return ReplyErrorLocalizedAsync(strs.waifu_not_yours); } + + return Divorce(waifuUserId); + } - [NadekoCommand, Aliases] - [RequireContext(ContextType.Guild)] - [Priority(0)] - public Task Divorce([Leftover]IGuildUser target) - => Divorce(target.Id); + [NadekoCommand, Aliases] + [RequireContext(ContextType.Guild)] + [Priority(0)] + public Task Divorce([Leftover]IGuildUser target) + => Divorce(target.Id); - [NadekoCommand, Aliases] - [RequireContext(ContextType.Guild)] - [Priority(1)] - public async Task Divorce([Leftover]ulong targetId) + [NadekoCommand, Aliases] + [RequireContext(ContextType.Guild)] + [Priority(1)] + public async Task Divorce([Leftover]ulong targetId) + { + if (targetId == ctx.User.Id) + return; + + var (w, result, amount, remaining) = await _service.DivorceWaifuAsync(ctx.User, targetId); + + if (result == DivorceResult.SucessWithPenalty) { - if (targetId == ctx.User.Id) - return; + await ReplyConfirmLocalizedAsync(strs.waifu_divorced_like(Format.Bold(w.Waifu.ToString()), amount + CurrencySign)); + } + else if (result == DivorceResult.Success) + { + await ReplyConfirmLocalizedAsync(strs.waifu_divorced_notlike(amount + CurrencySign)); + } + else if (result == DivorceResult.NotYourWife) + { + await ReplyErrorLocalizedAsync(strs.waifu_not_yours); + } + else + { + await ReplyErrorLocalizedAsync(strs.waifu_recent_divorce( + Format.Bold(((int)remaining?.TotalHours).ToString()), + Format.Bold(remaining?.Minutes.ToString()))); + } + } - var (w, result, amount, remaining) = await _service.DivorceWaifuAsync(ctx.User, targetId); - - if (result == DivorceResult.SucessWithPenalty) + [NadekoCommand, Aliases] + [RequireContext(ContextType.Guild)] + public async Task Affinity([Leftover]IGuildUser u = null) + { + if (u?.Id == ctx.User.Id) + { + await ReplyErrorLocalizedAsync(strs.waifu_egomaniac); + return; + } + var (oldAff, sucess, remaining) = await _service.ChangeAffinityAsync(ctx.User, u); + if (!sucess) + { + if (remaining != null) { - await ReplyConfirmLocalizedAsync(strs.waifu_divorced_like(Format.Bold(w.Waifu.ToString()), amount + CurrencySign)); - } - else if (result == DivorceResult.Success) - { - await ReplyConfirmLocalizedAsync(strs.waifu_divorced_notlike(amount + CurrencySign)); - } - else if (result == DivorceResult.NotYourWife) - { - await ReplyErrorLocalizedAsync(strs.waifu_not_yours); - } - else - { - await ReplyErrorLocalizedAsync(strs.waifu_recent_divorce( + await ReplyErrorLocalizedAsync(strs.waifu_affinity_cooldown( Format.Bold(((int)remaining?.TotalHours).ToString()), Format.Bold(remaining?.Minutes.ToString()))); } - } - - [NadekoCommand, Aliases] - [RequireContext(ContextType.Guild)] - public async Task Affinity([Leftover]IGuildUser u = null) - { - if (u?.Id == ctx.User.Id) - { - await ReplyErrorLocalizedAsync(strs.waifu_egomaniac); - return; - } - var (oldAff, sucess, remaining) = await _service.ChangeAffinityAsync(ctx.User, u); - if (!sucess) - { - if (remaining != null) - { - await ReplyErrorLocalizedAsync(strs.waifu_affinity_cooldown( - Format.Bold(((int)remaining?.TotalHours).ToString()), - Format.Bold(remaining?.Minutes.ToString()))); - } - else - { - await ReplyErrorLocalizedAsync(strs.waifu_affinity_already); - } - return; - } - if (u is null) - { - await ReplyConfirmLocalizedAsync(strs.waifu_affinity_reset); - } - else if (oldAff is null) - { - await ReplyConfirmLocalizedAsync(strs.waifu_affinity_set(Format.Bold(u.ToString()))); - } else { - await ReplyConfirmLocalizedAsync(strs.waifu_affinity_changed(Format.Bold(oldAff.ToString()), Format.Bold(u.ToString()))); + await ReplyErrorLocalizedAsync(strs.waifu_affinity_already); } + return; + } + if (u is null) + { + await ReplyConfirmLocalizedAsync(strs.waifu_affinity_reset); + } + else if (oldAff is null) + { + await ReplyConfirmLocalizedAsync(strs.waifu_affinity_set(Format.Bold(u.ToString()))); + } + else + { + await ReplyConfirmLocalizedAsync(strs.waifu_affinity_changed(Format.Bold(oldAff.ToString()), Format.Bold(u.ToString()))); + } + } + + [NadekoCommand, Aliases] + [RequireContext(ContextType.Guild)] + public async Task WaifuLb(int page = 1) + { + page--; + + if (page < 0) + return; + + if (page > 100) + page = 100; + + var waifus = _service.GetTopWaifusAtPage(page); + + if (waifus.Count() == 0) + { + await ReplyConfirmLocalizedAsync(strs.waifus_none); + return; } - [NadekoCommand, Aliases] - [RequireContext(ContextType.Guild)] - public async Task WaifuLb(int page = 1) + var embed = _eb.Create() + .WithTitle(GetText(strs.waifus_top_waifus)) + .WithOkColor(); + + var i = 0; + foreach (var w in waifus) { - page--; + var j = i++; + embed.AddField("#" + ((page * 9) + j + 1) + " - " + w.Price + CurrencySign, w.ToString(), false); + } - if (page < 0) - return; + await ctx.Channel.EmbedAsync(embed); + } - if (page > 100) - page = 100; + [NadekoCommand, Aliases] + [RequireContext(ContextType.Guild)] + [Priority(1)] + public Task WaifuInfo([Leftover]IUser target = null) + { + if (target is null) + target = ctx.User; - var waifus = _service.GetTopWaifusAtPage(page); + return InternalWaifuInfo(target.Id, target.ToString()); + } + + [NadekoCommand, Aliases] + [RequireContext(ContextType.Guild)] + [Priority(0)] + public Task WaifuInfo(ulong targetId) + => InternalWaifuInfo(targetId); - if (waifus.Count() == 0) - { - await ReplyConfirmLocalizedAsync(strs.waifus_none); - return; - } + private Task InternalWaifuInfo(ulong targetId, string name = null) + { + var wi = _service.GetFullWaifuInfoAsync(targetId); + var affInfo = _service.GetAffinityTitle(wi.AffinityCount); + var waifuItems = _service.GetWaifuItems() + .ToDictionary(x => x.ItemEmoji, x => x); + + + var nobody = GetText(strs.nobody); + var itemsStr = !wi.Items.Any() + ? "-" + : string.Join("\n", wi.Items + .Where(x => waifuItems.TryGetValue(x.ItemEmoji, out _)) + .OrderBy(x => waifuItems[x.ItemEmoji].Price) + .GroupBy(x => x.ItemEmoji) + .Select(x => $"{x.Key} x{x.Count(),-3}") + .Chunk(2) + .Select(x => string.Join(" ", x))); + + var fansStr = wi + .Fans + .Shuffle() + .Take(30) + .Select(x => wi.Claims.Contains(x) ? $"{x} 💞" : x) + .JoinWith('\n'); + + if (string.IsNullOrWhiteSpace(fansStr)) + fansStr = "-"; + + var embed = _eb.Create() + .WithOkColor() + .WithTitle(GetText(strs.waifu) + " " + (wi.FullName ?? name ?? targetId.ToString()) + " - \"the " + + _service.GetClaimTitle(wi.ClaimCount) + "\"") + .AddField(GetText(strs.price), wi.Price.ToString(), true) + .AddField(GetText(strs.claimed_by), wi.ClaimerName ?? nobody, true) + .AddField(GetText(strs.likes), wi.AffinityName ?? nobody, true) + .AddField(GetText(strs.changes_of_heart), $"{wi.AffinityCount} - \"the {affInfo}\"", true) + .AddField(GetText(strs.divorces), wi.DivorceCount.ToString(), true) + .AddField("\u200B", "\u200B", true) + .AddField(GetText(strs.fans(wi.Fans.Count)), fansStr, true) + .AddField($"Waifus ({wi.ClaimCount})", wi.ClaimCount == 0 + ? nobody + : string.Join("\n", wi.Claims.Shuffle().Take(30)), true) + .AddField(GetText(strs.gifts), itemsStr, true); + + return ctx.Channel.EmbedAsync(embed); + } + + [NadekoCommand, Aliases] + [RequireContext(ContextType.Guild)] + [Priority(1)] + public async Task WaifuGift(int page = 1) + { + if (--page < 0 || page > (_config.Waifu.Items.Count - 1) / 9) + return; + + var waifuItems = _service.GetWaifuItems(); + await ctx.SendPaginatedConfirmAsync(page, (cur) => + { var embed = _eb.Create() - .WithTitle(GetText(strs.waifus_top_waifus)) + .WithTitle(GetText(strs.waifu_gift_shop)) .WithOkColor(); - var i = 0; - foreach (var w in waifus) - { - var j = i++; - embed.AddField("#" + ((page * 9) + j + 1) + " - " + w.Price + CurrencySign, w.ToString(), false); - } + waifuItems + .OrderBy(x => x.Negative) + .ThenBy(x => x.Price) + .Skip(9 * cur) + .Take(9) + .ForEach(x => embed + .AddField($"{(!x.Negative ? string.Empty : "\\💔")} {x.ItemEmoji} {x.Name}", + Format.Bold(x.Price.ToString()) + _config.Currency.Sign, + true)); - await ctx.Channel.EmbedAsync(embed); - } + return embed; + }, waifuItems.Count, 9); + } - [NadekoCommand, Aliases] - [RequireContext(ContextType.Guild)] - [Priority(1)] - public Task WaifuInfo([Leftover]IUser target = null) + [NadekoCommand, Aliases] + [RequireContext(ContextType.Guild)] + [Priority(0)] + public async Task WaifuGift(string itemName, [Leftover] IUser waifu) + { + if (waifu.Id == ctx.User.Id) + return; + + var allItems = _service.GetWaifuItems(); + var item = allItems.FirstOrDefault(x => x.Name.ToLowerInvariant() == itemName.ToLowerInvariant()); + if (item is null) { - if (target is null) - target = ctx.User; - - return InternalWaifuInfo(target.Id, target.ToString()); + await ReplyErrorLocalizedAsync(strs.waifu_gift_not_exist); + return; } - - [NadekoCommand, Aliases] - [RequireContext(ContextType.Guild)] - [Priority(0)] - public Task WaifuInfo(ulong targetId) - => InternalWaifuInfo(targetId); + var sucess = await _service.GiftWaifuAsync(ctx.User, waifu, item); - private Task InternalWaifuInfo(ulong targetId, string name = null) + if (sucess) { - var wi = _service.GetFullWaifuInfoAsync(targetId); - var affInfo = _service.GetAffinityTitle(wi.AffinityCount); - - var waifuItems = _service.GetWaifuItems() - .ToDictionary(x => x.ItemEmoji, x => x); - - - var nobody = GetText(strs.nobody); - var itemsStr = !wi.Items.Any() - ? "-" - : string.Join("\n", wi.Items - .Where(x => waifuItems.TryGetValue(x.ItemEmoji, out _)) - .OrderBy(x => waifuItems[x.ItemEmoji].Price) - .GroupBy(x => x.ItemEmoji) - .Select(x => $"{x.Key} x{x.Count(),-3}") - .Chunk(2) - .Select(x => string.Join(" ", x))); - - var fansStr = wi - .Fans - .Shuffle() - .Take(30) - .Select(x => wi.Claims.Contains(x) ? $"{x} 💞" : x) - .JoinWith('\n'); - - if (string.IsNullOrWhiteSpace(fansStr)) - fansStr = "-"; - - var embed = _eb.Create() - .WithOkColor() - .WithTitle(GetText(strs.waifu) + " " + (wi.FullName ?? name ?? targetId.ToString()) + " - \"the " + - _service.GetClaimTitle(wi.ClaimCount) + "\"") - .AddField(GetText(strs.price), wi.Price.ToString(), true) - .AddField(GetText(strs.claimed_by), wi.ClaimerName ?? nobody, true) - .AddField(GetText(strs.likes), wi.AffinityName ?? nobody, true) - .AddField(GetText(strs.changes_of_heart), $"{wi.AffinityCount} - \"the {affInfo}\"", true) - .AddField(GetText(strs.divorces), wi.DivorceCount.ToString(), true) - .AddField("\u200B", "\u200B", true) - .AddField(GetText(strs.fans(wi.Fans.Count)), fansStr, true) - .AddField($"Waifus ({wi.ClaimCount})", wi.ClaimCount == 0 - ? nobody - : string.Join("\n", wi.Claims.Shuffle().Take(30)), true) - .AddField(GetText(strs.gifts), itemsStr, true); - - return ctx.Channel.EmbedAsync(embed); + await ReplyConfirmLocalizedAsync(strs.waifu_gift( + Format.Bold(item.ToString() + " " + item.ItemEmoji), + Format.Bold(waifu.ToString()))); } - - [NadekoCommand, Aliases] - [RequireContext(ContextType.Guild)] - [Priority(1)] - public async Task WaifuGift(int page = 1) + else { - if (--page < 0 || page > (_config.Waifu.Items.Count - 1) / 9) - return; - - var waifuItems = _service.GetWaifuItems(); - await ctx.SendPaginatedConfirmAsync(page, (cur) => - { - var embed = _eb.Create() - .WithTitle(GetText(strs.waifu_gift_shop)) - .WithOkColor(); - - waifuItems - .OrderBy(x => x.Negative) - .ThenBy(x => x.Price) - .Skip(9 * cur) - .Take(9) - .ForEach(x => embed - .AddField($"{(!x.Negative ? string.Empty : "\\💔")} {x.ItemEmoji} {x.Name}", - Format.Bold(x.Price.ToString()) + _config.Currency.Sign, - true)); - - return embed; - }, waifuItems.Count, 9); - } - - [NadekoCommand, Aliases] - [RequireContext(ContextType.Guild)] - [Priority(0)] - public async Task WaifuGift(string itemName, [Leftover] IUser waifu) - { - if (waifu.Id == ctx.User.Id) - return; - - var allItems = _service.GetWaifuItems(); - var item = allItems.FirstOrDefault(x => x.Name.ToLowerInvariant() == itemName.ToLowerInvariant()); - if (item is null) - { - await ReplyErrorLocalizedAsync(strs.waifu_gift_not_exist); - return; - } - var sucess = await _service.GiftWaifuAsync(ctx.User, waifu, item); - - if (sucess) - { - await ReplyConfirmLocalizedAsync(strs.waifu_gift( - Format.Bold(item.ToString() + " " + item.ItemEmoji), - Format.Bold(waifu.ToString()))); - } - else - { - await ReplyErrorLocalizedAsync(strs.not_enough(CurrencySign)); - } + await ReplyErrorLocalizedAsync(strs.not_enough(CurrencySign)); } } } diff --git a/src/NadekoBot/Modules/Gambling/WheelOfFortuneCommands.cs b/src/NadekoBot/Modules/Gambling/WheelOfFortuneCommands.cs index 6c8bf3f20..1fd469db9 100644 --- a/src/NadekoBot/Modules/Gambling/WheelOfFortuneCommands.cs +++ b/src/NadekoBot/Modules/Gambling/WheelOfFortuneCommands.cs @@ -1,6 +1,5 @@ using Discord; using NadekoBot.Common.Attributes; -using NadekoBot.Extensions; using System.Threading.Tasks; using Wof = NadekoBot.Modules.Gambling.Common.WheelOfFortune.WheelOfFortuneGame; using NadekoBot.Modules.Gambling.Services; @@ -9,13 +8,13 @@ using System.Collections.Immutable; using NadekoBot.Common; using NadekoBot.Services; -namespace NadekoBot.Modules.Gambling +namespace NadekoBot.Modules.Gambling; + +public partial class Gambling { - public partial class Gambling + public class WheelOfFortuneCommands : GamblingSubmodule { - public class WheelOfFortuneCommands : GamblingSubmodule - { - private static readonly ImmutableArray _emojis = new string[] { + private static readonly ImmutableArray _emojis = new string[] { "⬆", "↖", "⬅", @@ -25,40 +24,39 @@ namespace NadekoBot.Modules.Gambling "➡", "↗" }.ToImmutableArray(); - private readonly ICurrencyService _cs; - private readonly DbService _db; + private readonly ICurrencyService _cs; + private readonly DbService _db; - public WheelOfFortuneCommands(ICurrencyService cs, DbService db, GamblingConfigService gamblingConfService) - : base(gamblingConfService) + public WheelOfFortuneCommands(ICurrencyService cs, DbService db, GamblingConfigService gamblingConfService) + : base(gamblingConfService) + { + _cs = cs; + _db = db; + } + + [NadekoCommand, Aliases] + public async Task WheelOfFortune(ShmartNumber amount) + { + if (!await CheckBetMandatory(amount).ConfigureAwait(false)) + return; + + if (!await _cs.RemoveAsync(ctx.User.Id, "Wheel Of Fortune - bet", amount, gamble: true).ConfigureAwait(false)) { - _cs = cs; - _db = db; + await ReplyErrorLocalizedAsync(strs.not_enough(CurrencySign)); + return; } - [NadekoCommand, Aliases] - public async Task WheelOfFortune(ShmartNumber amount) - { - if (!await CheckBetMandatory(amount).ConfigureAwait(false)) - return; + var result = await _service.WheelOfFortuneSpinAsync(ctx.User.Id, amount).ConfigureAwait(false); - if (!await _cs.RemoveAsync(ctx.User.Id, "Wheel Of Fortune - bet", amount, gamble: true).ConfigureAwait(false)) - { - await ReplyErrorLocalizedAsync(strs.not_enough(CurrencySign)); - return; - } - - var result = await _service.WheelOfFortuneSpinAsync(ctx.User.Id, amount).ConfigureAwait(false); - - var wofMultipliers = _config.WheelOfFortune.Multipliers; - await SendConfirmAsync( -Format.Bold($@"{ctx.User.ToString()} won: {result.Amount + CurrencySign} + var wofMultipliers = _config.WheelOfFortune.Multipliers; + await SendConfirmAsync( + Format.Bold($@"{ctx.User.ToString()} won: {result.Amount + CurrencySign} 『{wofMultipliers[1]}』 『{wofMultipliers[0]}』 『{wofMultipliers[7]}』 『{wofMultipliers[2]}』 {_emojis[result.Index]} 『{wofMultipliers[6]}』 『{wofMultipliers[3]}』 『{wofMultipliers[4]}』 『{wofMultipliers[5]}』")).ConfigureAwait(false); - } } } } \ No newline at end of file diff --git a/src/NadekoBot/Modules/Games/AcropobiaCommands.cs b/src/NadekoBot/Modules/Games/AcropobiaCommands.cs index e9b802b6e..b39b1792d 100644 --- a/src/NadekoBot/Modules/Games/AcropobiaCommands.cs +++ b/src/NadekoBot/Modules/Games/AcropobiaCommands.cs @@ -2,146 +2,143 @@ using Discord.Commands; using Discord.WebSocket; using NadekoBot.Extensions; -using System.Collections.Generic; using System.Collections.Immutable; -using System.Linq; using System.Threading.Tasks; using NadekoBot.Common.Attributes; using NadekoBot.Modules.Games.Common.Acrophobia; using NadekoBot.Modules.Games.Services; using NadekoBot.Common; -namespace NadekoBot.Modules.Games -{ - public partial class Games - { - [Group] - public class AcropobiaCommands : NadekoSubmodule - { - private readonly DiscordSocketClient _client; +namespace NadekoBot.Modules.Games; - public AcropobiaCommands(DiscordSocketClient client) +public partial class Games +{ + [Group] + public class AcropobiaCommands : NadekoSubmodule + { + private readonly DiscordSocketClient _client; + + public AcropobiaCommands(DiscordSocketClient client) + { + _client = client; + } + + [NadekoCommand, Aliases] + [RequireContext(ContextType.Guild)] + [NadekoOptions(typeof(AcrophobiaGame.Options))] + public async Task Acrophobia(params string[] args) + { + var (options, _) = OptionsParser.ParseFrom(new AcrophobiaGame.Options(), args); + var channel = (ITextChannel)ctx.Channel; + + var game = new AcrophobiaGame(options); + if (_service.AcrophobiaGames.TryAdd(channel.Id, game)) { - _client = client; + try + { + game.OnStarted += Game_OnStarted; + game.OnEnded += Game_OnEnded; + game.OnVotingStarted += Game_OnVotingStarted; + game.OnUserVoted += Game_OnUserVoted; + _client.MessageReceived += _client_MessageReceived; + await game.Run().ConfigureAwait(false); + } + finally + { + _client.MessageReceived -= _client_MessageReceived; + _service.AcrophobiaGames.TryRemove(channel.Id, out game); + game.Dispose(); + } + } + else + { + await ReplyErrorLocalizedAsync(strs.acro_running).ConfigureAwait(false); } - [NadekoCommand, Aliases] - [RequireContext(ContextType.Guild)] - [NadekoOptions(typeof(AcrophobiaGame.Options))] - public async Task Acrophobia(params string[] args) + Task _client_MessageReceived(SocketMessage msg) { - var (options, _) = OptionsParser.ParseFrom(new AcrophobiaGame.Options(), args); - var channel = (ITextChannel)ctx.Channel; + if (msg.Channel.Id != ctx.Channel.Id) + return Task.CompletedTask; - var game = new AcrophobiaGame(options); - if (_service.AcrophobiaGames.TryAdd(channel.Id, game)) + var _ = Task.Run(async () => { try { - game.OnStarted += Game_OnStarted; - game.OnEnded += Game_OnEnded; - game.OnVotingStarted += Game_OnVotingStarted; - game.OnUserVoted += Game_OnUserVoted; - _client.MessageReceived += _client_MessageReceived; - await game.Run().ConfigureAwait(false); + var success = await game.UserInput(msg.Author.Id, msg.Author.ToString(), msg.Content) + .ConfigureAwait(false); + if (success) + await msg.DeleteAsync().ConfigureAwait(false); } - finally - { - _client.MessageReceived -= _client_MessageReceived; - _service.AcrophobiaGames.TryRemove(channel.Id, out game); - game.Dispose(); - } - } - else - { - await ReplyErrorLocalizedAsync(strs.acro_running).ConfigureAwait(false); - } + catch { } + }); - Task _client_MessageReceived(SocketMessage msg) - { - if (msg.Channel.Id != ctx.Channel.Id) - return Task.CompletedTask; + return Task.CompletedTask; + } + } - var _ = Task.Run(async () => - { - try - { - var success = await game.UserInput(msg.Author.Id, msg.Author.ToString(), msg.Content) - .ConfigureAwait(false); - if (success) - await msg.DeleteAsync().ConfigureAwait(false); - } - catch { } - }); + private Task Game_OnStarted(AcrophobiaGame game) + { + var embed = _eb.Create().WithOkColor() + .WithTitle(GetText(strs.acrophobia)) + .WithDescription(GetText(strs.acro_started(Format.Bold(string.Join(".", game.StartingLetters))))) + .WithFooter(GetText(strs.acro_started_footer(game.Opts.SubmissionTime))); - return Task.CompletedTask; - } + return ctx.Channel.EmbedAsync(embed); + } + + private Task Game_OnUserVoted(string user) + { + return SendConfirmAsync( + GetText(strs.acrophobia), + GetText(strs.acro_vote_cast(Format.Bold(user)))); + } + + private async Task Game_OnVotingStarted(AcrophobiaGame game, ImmutableArray> submissions) + { + if (submissions.Length == 0) + { + await SendErrorAsync(GetText(strs.acrophobia), GetText(strs.acro_ended_no_sub)).ConfigureAwait(false); + return; + } + if (submissions.Length == 1) + { + await ctx.Channel.EmbedAsync(_eb.Create().WithOkColor() + .WithDescription(GetText(strs.acro_winner_only(Format.Bold(submissions.First().Key.UserName)))) + .WithFooter(submissions.First().Key.Input)) + .ConfigureAwait(false); + return; } - private Task Game_OnStarted(AcrophobiaGame game) - { - var embed = _eb.Create().WithOkColor() - .WithTitle(GetText(strs.acrophobia)) - .WithDescription(GetText(strs.acro_started(Format.Bold(string.Join(".", game.StartingLetters))))) - .WithFooter(GetText(strs.acro_started_footer(game.Opts.SubmissionTime))); - return ctx.Channel.EmbedAsync(embed); - } - - private Task Game_OnUserVoted(string user) - { - return SendConfirmAsync( - GetText(strs.acrophobia), - GetText(strs.acro_vote_cast(Format.Bold(user)))); - } - - private async Task Game_OnVotingStarted(AcrophobiaGame game, ImmutableArray> submissions) - { - if (submissions.Length == 0) - { - await SendErrorAsync(GetText(strs.acrophobia), GetText(strs.acro_ended_no_sub)).ConfigureAwait(false); - return; - } - if (submissions.Length == 1) - { - await ctx.Channel.EmbedAsync(_eb.Create().WithOkColor() - .WithDescription(GetText(strs.acro_winner_only(Format.Bold(submissions.First().Key.UserName)))) - .WithFooter(submissions.First().Key.Input)) - .ConfigureAwait(false); - return; - } - - - var i = 0; - var embed = _eb.Create() - .WithOkColor() - .WithTitle(GetText(strs.acrophobia) + " - " + GetText(strs.submissions_closed)) - .WithDescription(GetText(strs.acro_nym_was(Format.Bold(string.Join(".", game.StartingLetters)) + "\n" + -$@"-- + var i = 0; + var embed = _eb.Create() + .WithOkColor() + .WithTitle(GetText(strs.acrophobia) + " - " + GetText(strs.submissions_closed)) + .WithDescription(GetText(strs.acro_nym_was(Format.Bold(string.Join(".", game.StartingLetters)) + "\n" + + $@"-- {submissions.Aggregate("", (agg, cur) => agg + $"`{++i}.` **{cur.Key.Input}**\n")} --"))) - .WithFooter(GetText(strs.acro_vote)); + .WithFooter(GetText(strs.acro_vote)); - await ctx.Channel.EmbedAsync(embed).ConfigureAwait(false); - } + await ctx.Channel.EmbedAsync(embed).ConfigureAwait(false); + } - private async Task Game_OnEnded(AcrophobiaGame game, ImmutableArray> votes) + private async Task Game_OnEnded(AcrophobiaGame game, ImmutableArray> votes) + { + if (!votes.Any() || votes.All(x => x.Value == 0)) { - if (!votes.Any() || votes.All(x => x.Value == 0)) - { - await SendErrorAsync(GetText(strs.acrophobia), GetText(strs.acro_no_votes_cast)).ConfigureAwait(false); - return; - } - var table = votes.OrderByDescending(v => v.Value); - var winner = table.First(); - var embed = _eb.Create().WithOkColor() - .WithTitle(GetText(strs.acrophobia)) - .WithDescription(GetText(strs.acro_winner(Format.Bold(winner.Key.UserName), - Format.Bold(winner.Value.ToString())))) - .WithFooter(winner.Key.Input); - - await ctx.Channel.EmbedAsync(embed).ConfigureAwait(false); + await SendErrorAsync(GetText(strs.acrophobia), GetText(strs.acro_no_votes_cast)).ConfigureAwait(false); + return; } + var table = votes.OrderByDescending(v => v.Value); + var winner = table.First(); + var embed = _eb.Create().WithOkColor() + .WithTitle(GetText(strs.acrophobia)) + .WithDescription(GetText(strs.acro_winner(Format.Bold(winner.Key.UserName), + Format.Bold(winner.Value.ToString())))) + .WithFooter(winner.Key.Input); + + await ctx.Channel.EmbedAsync(embed).ConfigureAwait(false); } } } \ No newline at end of file diff --git a/src/NadekoBot/Modules/Games/CleverBotCommands.cs b/src/NadekoBot/Modules/Games/CleverBotCommands.cs index 66524a0bf..eb109a213 100644 --- a/src/NadekoBot/Modules/Games/CleverBotCommands.cs +++ b/src/NadekoBot/Modules/Games/CleverBotCommands.cs @@ -1,60 +1,57 @@ using Discord; using Discord.Commands; -using System; using System.Threading.Tasks; using NadekoBot.Common; using NadekoBot.Common.Attributes; using NadekoBot.Services; using NadekoBot.Db; -using NadekoBot.Modules.Administration; using NadekoBot.Modules.Games.Services; using NadekoBot.Modules.Games.Common.ChatterBot; -namespace NadekoBot.Modules.Games +namespace NadekoBot.Modules.Games; + +public partial class Games { - public partial class Games + [Group] + public class ChatterBotCommands : NadekoSubmodule { - [Group] - public class ChatterBotCommands : NadekoSubmodule + private readonly DbService _db; + + public ChatterBotCommands(DbService db) { - private readonly DbService _db; - - public ChatterBotCommands(DbService db) - { - _db = db; - } - - [NoPublicBot] - [NadekoCommand, Aliases] - [RequireContext(ContextType.Guild)] - [UserPerm(GuildPerm.ManageMessages)] - public async Task Cleverbot() - { - var channel = (ITextChannel)ctx.Channel; - - if (_service.ChatterBotGuilds.TryRemove(channel.Guild.Id, out _)) - { - using (var uow = _db.GetDbContext()) - { - uow.GuildConfigs.SetCleverbotEnabled(ctx.Guild.Id, false); - await uow.SaveChangesAsync(); - } - await ReplyConfirmLocalizedAsync(strs.cleverbot_disabled).ConfigureAwait(false); - return; - } - - _service.ChatterBotGuilds.TryAdd(channel.Guild.Id, new Lazy(() => _service.CreateSession(), true)); - - using (var uow = _db.GetDbContext()) - { - uow.GuildConfigs.SetCleverbotEnabled(ctx.Guild.Id, true); - await uow.SaveChangesAsync(); - } - - await ReplyConfirmLocalizedAsync(strs.cleverbot_enabled).ConfigureAwait(false); - } + _db = db; } - + [NoPublicBot] + [NadekoCommand, Aliases] + [RequireContext(ContextType.Guild)] + [UserPerm(GuildPerm.ManageMessages)] + public async Task Cleverbot() + { + var channel = (ITextChannel)ctx.Channel; + + if (_service.ChatterBotGuilds.TryRemove(channel.Guild.Id, out _)) + { + using (var uow = _db.GetDbContext()) + { + uow.GuildConfigs.SetCleverbotEnabled(ctx.Guild.Id, false); + await uow.SaveChangesAsync(); + } + await ReplyConfirmLocalizedAsync(strs.cleverbot_disabled).ConfigureAwait(false); + return; + } + + _service.ChatterBotGuilds.TryAdd(channel.Guild.Id, new Lazy(() => _service.CreateSession(), true)); + + using (var uow = _db.GetDbContext()) + { + uow.GuildConfigs.SetCleverbotEnabled(ctx.Guild.Id, true); + await uow.SaveChangesAsync(); + } + + await ReplyConfirmLocalizedAsync(strs.cleverbot_enabled).ConfigureAwait(false); + } } + + } \ No newline at end of file diff --git a/src/NadekoBot/Modules/Games/Common/Acrophobia/Acrophobia.cs b/src/NadekoBot/Modules/Games/Common/Acrophobia/Acrophobia.cs index 1ab473df8..6e69d71a0 100644 --- a/src/NadekoBot/Modules/Games/Common/Acrophobia/Acrophobia.cs +++ b/src/NadekoBot/Modules/Games/Common/Acrophobia/Acrophobia.cs @@ -1,189 +1,185 @@ -using System; -using System.Collections.Generic; -using System.Collections.Immutable; -using System.Linq; +using System.Collections.Immutable; using System.Threading; using System.Threading.Tasks; using CommandLine; using NadekoBot.Common; using NadekoBot.Extensions; -namespace NadekoBot.Modules.Games.Common.Acrophobia +namespace NadekoBot.Modules.Games.Common.Acrophobia; + +public sealed class AcrophobiaGame : IDisposable { - public sealed class AcrophobiaGame : IDisposable + public class Options : INadekoCommandOptions { - public class Options : INadekoCommandOptions + [Option('s', "submission-time", Required = false, Default = 60, HelpText = "Time after which the submissions are closed and voting starts.")] + public int SubmissionTime { get; set; } = 60; + + [Option('v', "vote-time", Required = false, Default = 60, HelpText = "Time after which the voting is closed and the winner is declared.")] + public int VoteTime { get; set; } = 30; + + public void NormalizeOptions() { - [Option('s', "submission-time", Required = false, Default = 60, HelpText = "Time after which the submissions are closed and voting starts.")] - public int SubmissionTime { get; set; } = 60; - - [Option('v', "vote-time", Required = false, Default = 60, HelpText = "Time after which the voting is closed and the winner is declared.")] - public int VoteTime { get; set; } = 30; - - public void NormalizeOptions() - { - if (SubmissionTime < 15 || SubmissionTime > 300) - SubmissionTime = 60; - if (VoteTime < 15 || VoteTime > 120) - VoteTime = 30; - } - } - - public enum Phase - { - Submission, - Voting, - Ended - } - - public enum UserInputResult - { - Submitted, - SubmissionFailed, - Voted, - VotingFailed, - Failed - } - - public Phase CurrentPhase { get; private set; } = Phase.Submission; - public ImmutableArray StartingLetters { get; private set; } - - private readonly Dictionary submissions = new Dictionary(); - private readonly SemaphoreSlim locker = new SemaphoreSlim(1, 1); - public Options Opts { get; } - private readonly NadekoRandom _rng; - - public event Func OnStarted = delegate { return Task.CompletedTask; }; - public event Func>, Task> OnVotingStarted = delegate { return Task.CompletedTask; }; - public event Func OnUserVoted = delegate { return Task.CompletedTask; }; - public event Func>, Task> OnEnded = delegate { return Task.CompletedTask; }; - - private readonly HashSet _usersWhoVoted = new HashSet(); - - public AcrophobiaGame(Options options) - { - Opts = options; - _rng = new NadekoRandom(); - InitializeStartingLetters(); - } - - public async Task Run() - { - await OnStarted(this).ConfigureAwait(false); - await Task.Delay(Opts.SubmissionTime * 1000).ConfigureAwait(false); - await locker.WaitAsync().ConfigureAwait(false); - try - { - if (submissions.Count == 0) - { - CurrentPhase = Phase.Ended; - await OnVotingStarted(this, ImmutableArray.Create>()).ConfigureAwait(false); - return; - } - if (submissions.Count == 1) - { - CurrentPhase = Phase.Ended; - await OnVotingStarted(this, submissions.ToArray().ToImmutableArray()).ConfigureAwait(false); - return; - } - - CurrentPhase = Phase.Voting; - - await OnVotingStarted(this, submissions.ToArray().ToImmutableArray()).ConfigureAwait(false); - } - finally { locker.Release(); } - - await Task.Delay(Opts.VoteTime * 1000).ConfigureAwait(false); - await locker.WaitAsync().ConfigureAwait(false); - try - { - CurrentPhase = Phase.Ended; - await OnEnded(this, submissions.ToArray().ToImmutableArray()).ConfigureAwait(false); - } - finally { locker.Release(); } - } - - private void InitializeStartingLetters() - { - var wordCount = _rng.Next(3, 6); - - var lettersArr = new char[wordCount]; - - for (int i = 0; i < wordCount; i++) - { - var randChar = (char)_rng.Next(65, 91); - lettersArr[i] = randChar == 'X' ? (char)_rng.Next(65, 88) : randChar; - } - StartingLetters = lettersArr.ToImmutableArray(); - } - - public async Task UserInput(ulong userId, string userName, string input) - { - var user = new AcrophobiaUser(userId, userName, input.ToLowerInvariant().ToTitleCase()); - - await locker.WaitAsync().ConfigureAwait(false); - try - { - switch (CurrentPhase) - { - case Phase.Submission: - if (submissions.ContainsKey(user) || !IsValidAnswer(input)) - break; - - submissions.Add(user, 0); - return true; - case Phase.Voting: - AcrophobiaUser toVoteFor; - if (!int.TryParse(input, out var index) - || --index < 0 - || index >= submissions.Count - || (toVoteFor = submissions.ToArray()[index].Key).UserId == user.UserId - || !_usersWhoVoted.Add(userId)) - break; - ++submissions[toVoteFor]; - var _ = Task.Run(() => OnUserVoted(userName)); - return true; - default: - break; - } - return false; - } - finally - { - locker.Release(); - } - } - - private bool IsValidAnswer(string input) - { - input = input.ToUpperInvariant(); - - var inputWords = input.Split(' '); - - if (inputWords.Length != StartingLetters.Length) // number of words must be the same as the number of the starting letters - return false; - - for (int i = 0; i < StartingLetters.Length; i++) - { - var letter = StartingLetters[i]; - - if (!inputWords[i].StartsWith(letter.ToString(), StringComparison.InvariantCulture)) // all first letters must match - return false; - } - - return true; - } - - public void Dispose() - { - this.CurrentPhase = Phase.Ended; - OnStarted = null; - OnEnded = null; - OnUserVoted = null; - OnVotingStarted = null; - _usersWhoVoted.Clear(); - submissions.Clear(); - locker.Dispose(); + if (SubmissionTime < 15 || SubmissionTime > 300) + SubmissionTime = 60; + if (VoteTime < 15 || VoteTime > 120) + VoteTime = 30; } } -} + + public enum Phase + { + Submission, + Voting, + Ended + } + + public enum UserInputResult + { + Submitted, + SubmissionFailed, + Voted, + VotingFailed, + Failed + } + + public Phase CurrentPhase { get; private set; } = Phase.Submission; + public ImmutableArray StartingLetters { get; private set; } + + private readonly Dictionary submissions = new Dictionary(); + private readonly SemaphoreSlim locker = new SemaphoreSlim(1, 1); + public Options Opts { get; } + private readonly NadekoRandom _rng; + + public event Func OnStarted = delegate { return Task.CompletedTask; }; + public event Func>, Task> OnVotingStarted = delegate { return Task.CompletedTask; }; + public event Func OnUserVoted = delegate { return Task.CompletedTask; }; + public event Func>, Task> OnEnded = delegate { return Task.CompletedTask; }; + + private readonly HashSet _usersWhoVoted = new HashSet(); + + public AcrophobiaGame(Options options) + { + Opts = options; + _rng = new NadekoRandom(); + InitializeStartingLetters(); + } + + public async Task Run() + { + await OnStarted(this).ConfigureAwait(false); + await Task.Delay(Opts.SubmissionTime * 1000).ConfigureAwait(false); + await locker.WaitAsync().ConfigureAwait(false); + try + { + if (submissions.Count == 0) + { + CurrentPhase = Phase.Ended; + await OnVotingStarted(this, ImmutableArray.Create>()).ConfigureAwait(false); + return; + } + if (submissions.Count == 1) + { + CurrentPhase = Phase.Ended; + await OnVotingStarted(this, submissions.ToArray().ToImmutableArray()).ConfigureAwait(false); + return; + } + + CurrentPhase = Phase.Voting; + + await OnVotingStarted(this, submissions.ToArray().ToImmutableArray()).ConfigureAwait(false); + } + finally { locker.Release(); } + + await Task.Delay(Opts.VoteTime * 1000).ConfigureAwait(false); + await locker.WaitAsync().ConfigureAwait(false); + try + { + CurrentPhase = Phase.Ended; + await OnEnded(this, submissions.ToArray().ToImmutableArray()).ConfigureAwait(false); + } + finally { locker.Release(); } + } + + private void InitializeStartingLetters() + { + var wordCount = _rng.Next(3, 6); + + var lettersArr = new char[wordCount]; + + for (int i = 0; i < wordCount; i++) + { + var randChar = (char)_rng.Next(65, 91); + lettersArr[i] = randChar == 'X' ? (char)_rng.Next(65, 88) : randChar; + } + StartingLetters = lettersArr.ToImmutableArray(); + } + + public async Task UserInput(ulong userId, string userName, string input) + { + var user = new AcrophobiaUser(userId, userName, input.ToLowerInvariant().ToTitleCase()); + + await locker.WaitAsync().ConfigureAwait(false); + try + { + switch (CurrentPhase) + { + case Phase.Submission: + if (submissions.ContainsKey(user) || !IsValidAnswer(input)) + break; + + submissions.Add(user, 0); + return true; + case Phase.Voting: + AcrophobiaUser toVoteFor; + if (!int.TryParse(input, out var index) + || --index < 0 + || index >= submissions.Count + || (toVoteFor = submissions.ToArray()[index].Key).UserId == user.UserId + || !_usersWhoVoted.Add(userId)) + break; + ++submissions[toVoteFor]; + var _ = Task.Run(() => OnUserVoted(userName)); + return true; + default: + break; + } + return false; + } + finally + { + locker.Release(); + } + } + + private bool IsValidAnswer(string input) + { + input = input.ToUpperInvariant(); + + var inputWords = input.Split(' '); + + if (inputWords.Length != StartingLetters.Length) // number of words must be the same as the number of the starting letters + return false; + + for (int i = 0; i < StartingLetters.Length; i++) + { + var letter = StartingLetters[i]; + + if (!inputWords[i].StartsWith(letter.ToString(), StringComparison.InvariantCulture)) // all first letters must match + return false; + } + + return true; + } + + public void Dispose() + { + this.CurrentPhase = Phase.Ended; + OnStarted = null; + OnEnded = null; + OnUserVoted = null; + OnVotingStarted = null; + _usersWhoVoted.Clear(); + submissions.Clear(); + locker.Dispose(); + } +} \ No newline at end of file diff --git a/src/NadekoBot/Modules/Games/Common/Acrophobia/AcrophobiaUser.cs b/src/NadekoBot/Modules/Games/Common/Acrophobia/AcrophobiaUser.cs index 8801e7008..a50613d6d 100644 --- a/src/NadekoBot/Modules/Games/Common/Acrophobia/AcrophobiaUser.cs +++ b/src/NadekoBot/Modules/Games/Common/Acrophobia/AcrophobiaUser.cs @@ -1,28 +1,27 @@ -namespace NadekoBot.Modules.Games.Common.Acrophobia +namespace NadekoBot.Modules.Games.Common.Acrophobia; + +public class AcrophobiaUser { - public class AcrophobiaUser + public string UserName { get; } + public ulong UserId { get; } + public string Input { get; } + + public AcrophobiaUser(ulong userId, string userName, string input) { - public string UserName { get; } - public ulong UserId { get; } - public string Input { get; } - - public AcrophobiaUser(ulong userId, string userName, string input) - { - this.UserName = userName; - this.UserId = userId; - this.Input = input; - } - - public override int GetHashCode() - { - return UserId.GetHashCode(); - } - - public override bool Equals(object obj) - { - return obj is AcrophobiaUser x - ? x.UserId == this.UserId - : false; - } + this.UserName = userName; + this.UserId = userId; + this.Input = input; } -} + + public override int GetHashCode() + { + return UserId.GetHashCode(); + } + + public override bool Equals(object obj) + { + return obj is AcrophobiaUser x + ? x.UserId == this.UserId + : false; + } +} \ No newline at end of file diff --git a/src/NadekoBot/Modules/Games/Common/ChatterBot/ChatterBotResponse.cs b/src/NadekoBot/Modules/Games/Common/ChatterBot/ChatterBotResponse.cs index acf5c3def..c047f8a2b 100644 --- a/src/NadekoBot/Modules/Games/Common/ChatterBot/ChatterBotResponse.cs +++ b/src/NadekoBot/Modules/Games/Common/ChatterBot/ChatterBotResponse.cs @@ -1,8 +1,7 @@ -namespace NadekoBot.Modules.Games.Common.ChatterBot +namespace NadekoBot.Modules.Games.Common.ChatterBot; + +public class ChatterBotResponse { - public class ChatterBotResponse - { - public string Convo_id { get; set; } - public string BotSay { get; set; } - } -} + public string Convo_id { get; set; } + public string BotSay { get; set; } +} \ No newline at end of file diff --git a/src/NadekoBot/Modules/Games/Common/ChatterBot/ChatterBotSession.cs b/src/NadekoBot/Modules/Games/Common/ChatterBot/ChatterBotSession.cs index cea2fb810..a84695169 100644 --- a/src/NadekoBot/Modules/Games/Common/ChatterBot/ChatterBotSession.cs +++ b/src/NadekoBot/Modules/Games/Common/ChatterBot/ChatterBotSession.cs @@ -1,40 +1,38 @@ -using System; -using System.Net.Http; +using System.Net.Http; using System.Threading.Tasks; using NadekoBot.Common; using NadekoBot.Extensions; using Newtonsoft.Json; -namespace NadekoBot.Modules.Games.Common.ChatterBot +namespace NadekoBot.Modules.Games.Common.ChatterBot; + +public class ChatterBotSession : IChatterBotSession { - public class ChatterBotSession : IChatterBotSession + private static NadekoRandom Rng { get; } = new NadekoRandom(); + + private readonly string _chatterBotId; + private readonly IHttpClientFactory _httpFactory; + private readonly int _botId = 6; + + public ChatterBotSession(IHttpClientFactory httpFactory) { - private static NadekoRandom Rng { get; } = new NadekoRandom(); + _chatterBotId = Rng.Next(0, 1000000).ToString().ToBase64(); + _httpFactory = httpFactory; + } - private readonly string _chatterBotId; - private readonly IHttpClientFactory _httpFactory; - private readonly int _botId = 6; + private string ApiEndpoint => "http://api.program-o.com/v2/chatbot/" + + $"?bot_id={_botId}&" + + "say={0}&" + + $"convo_id=nadekobot_{_chatterBotId}&" + + "format=json"; - public ChatterBotSession(IHttpClientFactory httpFactory) + public async Task Think(string message) + { + using (var http = _httpFactory.CreateClient()) { - _chatterBotId = Rng.Next(0, 1000000).ToString().ToBase64(); - _httpFactory = httpFactory; - } - - private string ApiEndpoint => "http://api.program-o.com/v2/chatbot/" + - $"?bot_id={_botId}&" + - "say={0}&" + - $"convo_id=nadekobot_{_chatterBotId}&" + - "format=json"; - - public async Task Think(string message) - { - using (var http = _httpFactory.CreateClient()) - { - var res = await http.GetStringAsync(string.Format(ApiEndpoint, message)).ConfigureAwait(false); - var cbr = JsonConvert.DeserializeObject(res); - return cbr.BotSay.Replace("
", "\n", StringComparison.InvariantCulture); - } + var res = await http.GetStringAsync(string.Format(ApiEndpoint, message)).ConfigureAwait(false); + var cbr = JsonConvert.DeserializeObject(res); + return cbr.BotSay.Replace("
", "\n", StringComparison.InvariantCulture); } } -} +} \ No newline at end of file diff --git a/src/NadekoBot/Modules/Games/Common/ChatterBot/CleverbotResponse.cs b/src/NadekoBot/Modules/Games/Common/ChatterBot/CleverbotResponse.cs index ab04e7665..dfac63b30 100644 --- a/src/NadekoBot/Modules/Games/Common/ChatterBot/CleverbotResponse.cs +++ b/src/NadekoBot/Modules/Games/Common/ChatterBot/CleverbotResponse.cs @@ -1,20 +1,19 @@ -namespace NadekoBot.Modules.Games.Common.ChatterBot +namespace NadekoBot.Modules.Games.Common.ChatterBot; + +public class CleverbotResponse { - public class CleverbotResponse - { - public string Cs { get; set; } - public string Output { get; set; } - } - - public class CleverbotIOCreateResponse - { - public string Status { get; set; } - public string Nick { get; set; } - } - - public class CleverbotIOAskResponse - { - public string Status { get; set; } - public string Response { get; set; } - } + public string Cs { get; set; } + public string Output { get; set; } } + +public class CleverbotIOCreateResponse +{ + public string Status { get; set; } + public string Nick { get; set; } +} + +public class CleverbotIOAskResponse +{ + public string Status { get; set; } + public string Response { get; set; } +} \ No newline at end of file diff --git a/src/NadekoBot/Modules/Games/Common/ChatterBot/IChatterBotSession.cs b/src/NadekoBot/Modules/Games/Common/ChatterBot/IChatterBotSession.cs index 980c9e389..97fbdb8c0 100644 --- a/src/NadekoBot/Modules/Games/Common/ChatterBot/IChatterBotSession.cs +++ b/src/NadekoBot/Modules/Games/Common/ChatterBot/IChatterBotSession.cs @@ -1,9 +1,8 @@ using System.Threading.Tasks; -namespace NadekoBot.Modules.Games.Common.ChatterBot +namespace NadekoBot.Modules.Games.Common.ChatterBot; + +public interface IChatterBotSession { - public interface IChatterBotSession - { - Task Think(string input); - } -} + Task Think(string input); +} \ No newline at end of file diff --git a/src/NadekoBot/Modules/Games/Common/ChatterBot/OfficialCleverbotSession.cs b/src/NadekoBot/Modules/Games/Common/ChatterBot/OfficialCleverbotSession.cs index 322cbe6b0..f828e097e 100644 --- a/src/NadekoBot/Modules/Games/Common/ChatterBot/OfficialCleverbotSession.cs +++ b/src/NadekoBot/Modules/Games/Common/ChatterBot/OfficialCleverbotSession.cs @@ -1,109 +1,105 @@ using NadekoBot.Common; using Newtonsoft.Json; -using System; -using System.Collections.Generic; using System.Net.Http; using System.Threading.Tasks; -using Serilog; -namespace NadekoBot.Modules.Games.Common.ChatterBot +namespace NadekoBot.Modules.Games.Common.ChatterBot; + +public class OfficialCleverbotSession : IChatterBotSession { - public class OfficialCleverbotSession : IChatterBotSession + private readonly string _apiKey; + private readonly IHttpClientFactory _httpFactory; + private string _cs = null; + + private string QueryString => $"https://www.cleverbot.com/getreply?key={_apiKey}" + + "&wrapper=nadekobot" + + "&input={0}" + + "&cs={1}"; + + public OfficialCleverbotSession(string apiKey, IHttpClientFactory factory) { - private readonly string _apiKey; - private readonly IHttpClientFactory _httpFactory; - private string _cs = null; - - private string QueryString => $"https://www.cleverbot.com/getreply?key={_apiKey}" + - "&wrapper=nadekobot" + - "&input={0}" + - "&cs={1}"; - - public OfficialCleverbotSession(string apiKey, IHttpClientFactory factory) - { - this._apiKey = apiKey; - this._httpFactory = factory; - } - - public async Task Think(string input) - { - using (var http = _httpFactory.CreateClient()) - { - var dataString = await http.GetStringAsync(string.Format(QueryString, input, _cs ?? "")).ConfigureAwait(false); - try - { - var data = JsonConvert.DeserializeObject(dataString); - - _cs = data?.Cs; - return data?.Output; - } - catch - { - Log.Warning("Unexpected cleverbot response received: "); - Log.Warning(dataString); - return null; - } - } - } + this._apiKey = apiKey; + this._httpFactory = factory; } - public class CleverbotIOSession : IChatterBotSession + public async Task Think(string input) { - private readonly string _key; - private readonly string _user; - private readonly IHttpClientFactory _httpFactory; - private readonly AsyncLazy _nick; - - private readonly string _createEndpoint = $"https://cleverbot.io/1.0/create"; - private readonly string _askEndpoint = $"https://cleverbot.io/1.0/ask"; - - public CleverbotIOSession(string user, string key, IHttpClientFactory factory) + using (var http = _httpFactory.CreateClient()) { - this._key = key; - this._user = user; - this._httpFactory = factory; - - _nick = new AsyncLazy((Func>)GetNick); - } - - private async Task GetNick() - { - using (var _http = _httpFactory.CreateClient()) - using (var msg = new FormUrlEncodedContent(new[] + var dataString = await http.GetStringAsync(string.Format(QueryString, input, _cs ?? "")).ConfigureAwait(false); + try { - new KeyValuePair("user", _user), - new KeyValuePair("key", _key), - })) - using (var data = await _http.PostAsync(_createEndpoint, msg).ConfigureAwait(false)) - { - var str = await data.Content.ReadAsStringAsync().ConfigureAwait(false); - var obj = JsonConvert.DeserializeObject(str); - if (obj.Status != "success") - throw new OperationCanceledException(obj.Status); + var data = JsonConvert.DeserializeObject(dataString); - return obj.Nick; + _cs = data?.Cs; + return data?.Output; } - } - - public async Task Think(string input) - { - using (var _http = _httpFactory.CreateClient()) - using (var msg = new FormUrlEncodedContent(new[] + catch { - new KeyValuePair("user", _user), - new KeyValuePair("key", _key), - new KeyValuePair("nick", await _nick), - new KeyValuePair("text", input), - })) - using (var data = await _http.PostAsync(_askEndpoint, msg).ConfigureAwait(false)) - { - var str = await data.Content.ReadAsStringAsync().ConfigureAwait(false); - var obj = JsonConvert.DeserializeObject(str); - if (obj.Status != "success") - throw new OperationCanceledException(obj.Status); - - return obj.Response; + Log.Warning("Unexpected cleverbot response received: "); + Log.Warning(dataString); + return null; } } } } + +public class CleverbotIOSession : IChatterBotSession +{ + private readonly string _key; + private readonly string _user; + private readonly IHttpClientFactory _httpFactory; + private readonly AsyncLazy _nick; + + private readonly string _createEndpoint = $"https://cleverbot.io/1.0/create"; + private readonly string _askEndpoint = $"https://cleverbot.io/1.0/ask"; + + public CleverbotIOSession(string user, string key, IHttpClientFactory factory) + { + this._key = key; + this._user = user; + this._httpFactory = factory; + + _nick = new AsyncLazy((Func>)GetNick); + } + + private async Task GetNick() + { + using (var _http = _httpFactory.CreateClient()) + using (var msg = new FormUrlEncodedContent(new[] + { + new KeyValuePair("user", _user), + new KeyValuePair("key", _key), + })) + using (var data = await _http.PostAsync(_createEndpoint, msg).ConfigureAwait(false)) + { + var str = await data.Content.ReadAsStringAsync().ConfigureAwait(false); + var obj = JsonConvert.DeserializeObject(str); + if (obj.Status != "success") + throw new OperationCanceledException(obj.Status); + + return obj.Nick; + } + } + + public async Task Think(string input) + { + using (var _http = _httpFactory.CreateClient()) + using (var msg = new FormUrlEncodedContent(new[] + { + new KeyValuePair("user", _user), + new KeyValuePair("key", _key), + new KeyValuePair("nick", await _nick), + new KeyValuePair("text", input), + })) + using (var data = await _http.PostAsync(_askEndpoint, msg).ConfigureAwait(false)) + { + var str = await data.Content.ReadAsStringAsync().ConfigureAwait(false); + var obj = JsonConvert.DeserializeObject(str); + if (obj.Status != "success") + throw new OperationCanceledException(obj.Status); + + return obj.Response; + } + } +} \ No newline at end of file diff --git a/src/NadekoBot/Modules/Games/Common/GamesConfig.cs b/src/NadekoBot/Modules/Games/Common/GamesConfig.cs index 297c105ed..ef3e06154 100644 --- a/src/NadekoBot/Modules/Games/Common/GamesConfig.cs +++ b/src/NadekoBot/Modules/Games/Common/GamesConfig.cs @@ -1,93 +1,91 @@ -using System.Collections.Generic; -using Cloneable; +using Cloneable; using NadekoBot.Common; using NadekoBot.Common.Yml; -namespace NadekoBot.Modules.Games.Common +namespace NadekoBot.Modules.Games.Common; + +[Cloneable] +public sealed partial class GamesConfig : ICloneable { - [Cloneable] - public sealed partial class GamesConfig : ICloneable - { - [Comment("DO NOT CHANGE")] - public int Version { get; set; } + [Comment("DO NOT CHANGE")] + public int Version { get; set; } - [Comment("Hangman related settings (.hangman command)")] - public HangmanConfig Hangman { get; set; } = new HangmanConfig() - { - CurrencyReward = 0 - }; + [Comment("Hangman related settings (.hangman command)")] + public HangmanConfig Hangman { get; set; } = new HangmanConfig() + { + CurrencyReward = 0 + }; - [Comment("Trivia related settings (.t command)")] - public TriviaConfig Trivia { get; set; } = new TriviaConfig() - { - CurrencyReward = 0, - MinimumWinReq = 1, - }; - - [Comment("List of responses for the .8ball command. A random one will be selected every time")] - public List EightBallResponses { get; set; } = new List() - { - "Most definitely yes.", - "For sure.", - "Totally!", - "Of course!", - "As I see it, yes.", - "My sources say yes.", - "Yes.", - "Most likely.", - "Perhaps...", - "Maybe...", - "Hm, not sure.", - "It is uncertain.", - "Ask me again later.", - "Don't count on it.", - "Probably not.", - "Very doubtful.", - "Most likely no.", - "Nope.", - "No.", - "My sources say no.", - "Don't even think about it.", - "Definitely no.", - "NO - It may cause disease contraction!" - }; - - [Comment("List of animals which will be used for the animal race game (.race)")] - public List RaceAnimals { get; set; } = new List() - { - new RaceAnimal { Icon = "🐼", Name = "Panda" }, - new RaceAnimal { Icon = "🐻", Name = "Bear" }, - new RaceAnimal { Icon = "🐧", Name = "Pengu" }, - new RaceAnimal { Icon = "🐨", Name = "Koala" }, - new RaceAnimal { Icon = "🐬", Name = "Dolphin" }, - new RaceAnimal { Icon = "🐞", Name = "Ladybird" }, - new RaceAnimal { Icon = "🦀", Name = "Crab" }, - new RaceAnimal { Icon = "🦄", Name = "Unicorn" } - }; - } - - [Cloneable] - public sealed partial class HangmanConfig + [Comment("Trivia related settings (.t command)")] + public TriviaConfig Trivia { get; set; } = new TriviaConfig() { - [Comment("The amount of currency awarded to the winner of a hangman game")] - public long CurrencyReward { get; set; } - } + CurrencyReward = 0, + MinimumWinReq = 1, + }; - [Cloneable] - public sealed partial class TriviaConfig + [Comment("List of responses for the .8ball command. A random one will be selected every time")] + public List EightBallResponses { get; set; } = new List() { - [Comment("The amount of currency awarded to the winner of the trivia game.")] - public long CurrencyReward { get; set; } = 0; + "Most definitely yes.", + "For sure.", + "Totally!", + "Of course!", + "As I see it, yes.", + "My sources say yes.", + "Yes.", + "Most likely.", + "Perhaps...", + "Maybe...", + "Hm, not sure.", + "It is uncertain.", + "Ask me again later.", + "Don't count on it.", + "Probably not.", + "Very doubtful.", + "Most likely no.", + "Nope.", + "No.", + "My sources say no.", + "Don't even think about it.", + "Definitely no.", + "NO - It may cause disease contraction!" + }; - [Comment(@"Users won't be able to start trivia games which have + [Comment("List of animals which will be used for the animal race game (.race)")] + public List RaceAnimals { get; set; } = new List() + { + new RaceAnimal { Icon = "🐼", Name = "Panda" }, + new RaceAnimal { Icon = "🐻", Name = "Bear" }, + new RaceAnimal { Icon = "🐧", Name = "Pengu" }, + new RaceAnimal { Icon = "🐨", Name = "Koala" }, + new RaceAnimal { Icon = "🐬", Name = "Dolphin" }, + new RaceAnimal { Icon = "🐞", Name = "Ladybird" }, + new RaceAnimal { Icon = "🦀", Name = "Crab" }, + new RaceAnimal { Icon = "🦄", Name = "Unicorn" } + }; +} + +[Cloneable] +public sealed partial class HangmanConfig +{ + [Comment("The amount of currency awarded to the winner of a hangman game")] + public long CurrencyReward { get; set; } +} + +[Cloneable] +public sealed partial class TriviaConfig +{ + [Comment("The amount of currency awarded to the winner of the trivia game.")] + public long CurrencyReward { get; set; } = 0; + + [Comment(@"Users won't be able to start trivia games which have a smaller win requirement than the one specified by this setting.")] - public int MinimumWinReq { get; set; } = 1; - } + public int MinimumWinReq { get; set; } = 1; +} - [Cloneable] - public sealed partial class RaceAnimal - { - public string Icon { get; set; } - public string Name { get; set; } - } +[Cloneable] +public sealed partial class RaceAnimal +{ + public string Icon { get; set; } + public string Name { get; set; } } \ No newline at end of file diff --git a/src/NadekoBot/Modules/Games/Common/GirlRating.cs b/src/NadekoBot/Modules/Games/Common/GirlRating.cs index b6646806a..9ebd2249a 100644 --- a/src/NadekoBot/Modules/Games/Common/GirlRating.cs +++ b/src/NadekoBot/Modules/Games/Common/GirlRating.cs @@ -1,75 +1,72 @@ -using System; -using System.IO; +using System.IO; using System.Net.Http; using NadekoBot.Common; using NadekoBot.Services; -using Serilog; using Image = SixLabors.ImageSharp.Image; using SixLabors.ImageSharp; using SixLabors.ImageSharp.Processing; -namespace NadekoBot.Modules.Games.Common +namespace NadekoBot.Modules.Games.Common; + +public class GirlRating { - public class GirlRating + private readonly IImageCache _images; + + public double Crazy { get; } + public double Hot { get; } + public int Roll { get; } + public string Advice { get; } + + private readonly IHttpClientFactory _httpFactory; + + public AsyncLazy Stream { get; } + + public GirlRating(IImageCache images, IHttpClientFactory factory, double crazy, double hot, int roll, string advice) { - private readonly IImageCache _images; - - public double Crazy { get; } - public double Hot { get; } - public int Roll { get; } - public string Advice { get; } - - private readonly IHttpClientFactory _httpFactory; - - public AsyncLazy Stream { get; } - - public GirlRating(IImageCache images, IHttpClientFactory factory, double crazy, double hot, int roll, string advice) - { - _images = images; - Crazy = crazy; - Hot = hot; - Roll = roll; - Advice = advice; // convenient to have it here, even though atm there are only few different ones. - _httpFactory = factory; + _images = images; + Crazy = crazy; + Hot = hot; + Roll = roll; + Advice = advice; // convenient to have it here, even though atm there are only few different ones. + _httpFactory = factory; - Stream = new AsyncLazy(() => + Stream = new AsyncLazy(() => + { + try { - try + using (var img = Image.Load(_images.RategirlMatrix)) { - using (var img = Image.Load(_images.RategirlMatrix)) + const int minx = 35; + const int miny = 385; + const int length = 345; + + var pointx = (int)(minx + length * (Hot / 10)); + var pointy = (int)(miny - length * ((Crazy - 4) / 6)); + + using (var pointImg = Image.Load(_images.RategirlDot)) { - const int minx = 35; - const int miny = 385; - const int length = 345; - - var pointx = (int)(minx + length * (Hot / 10)); - var pointy = (int)(miny - length * ((Crazy - 4) / 6)); - - using (var pointImg = Image.Load(_images.RategirlDot)) - { - img.Mutate(x => x.DrawImage(pointImg, new Point(pointx - 10, pointy - 10), new GraphicsOptions())); - } - - var imgStream = new MemoryStream(); - img.SaveAsPng(imgStream); - return imgStream; - //using (var byteContent = new ByteArrayContent(imgStream.ToArray())) - //{ - // http.AddFakeHeaders(); - - // using (var reponse = await http.PutAsync("https://transfer.sh/img.png", byteContent).ConfigureAwait(false)) - // { - // url = await reponse.Content.ReadAsStringAsync().ConfigureAwait(false); - // } - //} + img.Mutate(x => x.DrawImage(pointImg, new Point(pointx - 10, pointy - 10), new GraphicsOptions())); } + + var imgStream = new MemoryStream(); + img.SaveAsPng(imgStream); + return imgStream; + //using (var byteContent = new ByteArrayContent(imgStream.ToArray())) + //{ + // http.AddFakeHeaders(); + + // using (var reponse = await http.PutAsync("https://transfer.sh/img.png", byteContent).ConfigureAwait(false)) + // { + // url = await reponse.Content.ReadAsStringAsync().ConfigureAwait(false); + // } + //} } - catch (Exception ex) - { - Log.Warning(ex, "Error getting RateGirl image"); - return null; - } - }); - } + } + catch (Exception ex) + { + Log.Warning(ex, "Error getting RateGirl image"); + return null; + } + }); } -} +} \ No newline at end of file diff --git a/src/NadekoBot/Modules/Games/Common/Hangman/DefaultHangmanSource.cs b/src/NadekoBot/Modules/Games/Common/Hangman/DefaultHangmanSource.cs index 73d09965e..f606ed771 100644 --- a/src/NadekoBot/Modules/Games/Common/Hangman/DefaultHangmanSource.cs +++ b/src/NadekoBot/Modules/Games/Common/Hangman/DefaultHangmanSource.cs @@ -1,72 +1,67 @@ #nullable enable -using System; -using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.IO; -using System.Linq; using NadekoBot.Common; using NadekoBot.Common.Yml; -using Serilog; -namespace NadekoBot.Modules.Games.Hangman +namespace NadekoBot.Modules.Games.Hangman; + +public sealed class DefaultHangmanSource : IHangmanSource { - public sealed class DefaultHangmanSource : IHangmanSource + private IReadOnlyDictionary _terms = new Dictionary(); + private readonly Random _rng; + + public DefaultHangmanSource() { - private IReadOnlyDictionary _terms = new Dictionary(); - private readonly Random _rng; + _rng = new NadekoRandom(); + Reload(); + } - public DefaultHangmanSource() + public void Reload() + { + if (!Directory.Exists("data/hangman")) { - _rng = new NadekoRandom(); - Reload(); + Log.Error("Hangman game won't work. Folder 'data/hangman' is missing."); + return; } - public void Reload() + var qs = new Dictionary(); + foreach (var file in Directory.EnumerateFiles("data/hangman/", "*.yml")) { - if (!Directory.Exists("data/hangman")) + try { - Log.Error("Hangman game won't work. Folder 'data/hangman' is missing."); - return; + var data = Yaml.Deserializer.Deserialize(File.ReadAllText(file)); + qs[Path.GetFileNameWithoutExtension(file).ToLowerInvariant()] = data; } - - var qs = new Dictionary(); - foreach (var file in Directory.EnumerateFiles("data/hangman/", "*.yml")) + catch (Exception ex) { - try - { - var data = Yaml.Deserializer.Deserialize(File.ReadAllText(file)); - qs[Path.GetFileNameWithoutExtension(file).ToLowerInvariant()] = data; - } - catch (Exception ex) - { - Log.Error(ex, "Loading {HangmanFile} failed.", file); - } + Log.Error(ex, "Loading {HangmanFile} failed.", file); } + } - _terms = qs; + _terms = qs; - Log.Information("Loaded {HangmanCategoryCount} hangman categories.", qs.Count); - } + Log.Information("Loaded {HangmanCategoryCount} hangman categories.", qs.Count); + } - public IReadOnlyCollection GetCategories() - => _terms.Keys.ToList(); + public IReadOnlyCollection GetCategories() + => _terms.Keys.ToList(); - public bool GetTerm(string? category, [NotNullWhen(true)] out HangmanTerm? term) + public bool GetTerm(string? category, [NotNullWhen(true)] out HangmanTerm? term) + { + if (category is null) { - if (category is null) - { - var cats = GetCategories(); - category = cats.ElementAt(_rng.Next(0, cats.Count)); - } - - if (_terms.TryGetValue(category, out var terms)) - { - term = terms[_rng.Next(0, terms.Length)]; - return true; - } - - term = null; - return false; + var cats = GetCategories(); + category = cats.ElementAt(_rng.Next(0, cats.Count)); } + + if (_terms.TryGetValue(category, out var terms)) + { + term = terms[_rng.Next(0, terms.Length)]; + return true; + } + + term = null; + return false; } } \ No newline at end of file diff --git a/src/NadekoBot/Modules/Games/Common/Hangman/HangmanGame.cs b/src/NadekoBot/Modules/Games/Common/Hangman/HangmanGame.cs index 006ab3ec9..2e74379f8 100644 --- a/src/NadekoBot/Modules/Games/Common/Hangman/HangmanGame.cs +++ b/src/NadekoBot/Modules/Games/Common/Hangman/HangmanGame.cs @@ -1,121 +1,117 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using AngleSharp.Text; +using AngleSharp.Text; -namespace NadekoBot.Modules.Games.Hangman +namespace NadekoBot.Modules.Games.Hangman; + +public sealed class HangmanGame { - public sealed class HangmanGame + public enum Phase { Running, Ended } + public enum GuessResult { NoAction, AlreadyTried, Incorrect, Guess, Win } + + public record State( + int Errors, + Phase Phase, + string Word, + GuessResult GuessResult, + List missedLetters, + string ImageUrl) { - public enum Phase { Running, Ended } - public enum GuessResult { NoAction, AlreadyTried, Incorrect, Guess, Win } + public bool Failed => Errors > 5; + } - public record State( - int Errors, - Phase Phase, - string Word, - GuessResult GuessResult, - List missedLetters, - string ImageUrl) - { - public bool Failed => Errors > 5; - } - - private Phase CurrentPhase { get; set; } + private Phase CurrentPhase { get; set; } - private readonly HashSet _incorrect = new(); - private readonly HashSet _correct = new(); - private readonly HashSet _remaining = new(); + private readonly HashSet _incorrect = new(); + private readonly HashSet _correct = new(); + private readonly HashSet _remaining = new(); - private readonly string _word; - private readonly string _imageUrl; + private readonly string _word; + private readonly string _imageUrl; - public HangmanGame(HangmanTerm term) - { - _word = term.Word; - _imageUrl = term.ImageUrl; + public HangmanGame(HangmanTerm term) + { + _word = term.Word; + _imageUrl = term.ImageUrl; - _remaining = _word - .ToLowerInvariant() - .Where(x => x.IsLetter()) - .Select(char.ToLowerInvariant) - .ToHashSet(); + _remaining = _word + .ToLowerInvariant() + .Where(x => x.IsLetter()) + .Select(char.ToLowerInvariant) + .ToHashSet(); + } + + public State GetState(GuessResult guessResult = GuessResult.NoAction) + => new State(_incorrect.Count, + CurrentPhase, + CurrentPhase == Phase.Ended + ? _word + : GetScrambledWord(), + guessResult, + _incorrect.ToList(), + CurrentPhase == Phase.Ended + ? _imageUrl + : string.Empty); + + private string GetScrambledWord() + { + Span output = stackalloc char[_word.Length * 2]; + for (var i = 0; i < _word.Length; i++) + { + var ch = _word[i]; + if (ch == ' ') + output[i*2] = ' '; + if (!ch.IsLetter() || !_remaining.Contains(char.ToLowerInvariant(ch))) + output[i*2] = ch; + else + output[i*2] = '_'; + + output[i * 2 + 1] = ' '; } - public State GetState(GuessResult guessResult = GuessResult.NoAction) - => new State(_incorrect.Count, - CurrentPhase, - CurrentPhase == Phase.Ended - ? _word - : GetScrambledWord(), - guessResult, - _incorrect.ToList(), - CurrentPhase == Phase.Ended - ? _imageUrl - : string.Empty); + return new(output); + } - private string GetScrambledWord() + public State Guess(string guess) + { + if (CurrentPhase != Phase.Running) + return GetState(GuessResult.NoAction); + + guess = guess.Trim(); + if (guess.Length > 1) { - Span output = stackalloc char[_word.Length * 2]; - for (var i = 0; i < _word.Length; i++) - { - var ch = _word[i]; - if (ch == ' ') - output[i*2] = ' '; - if (!ch.IsLetter() || !_remaining.Contains(char.ToLowerInvariant(ch))) - output[i*2] = ch; - else - output[i*2] = '_'; - - output[i * 2 + 1] = ' '; - } - - return new(output); - } - - public State Guess(string guess) - { - if (CurrentPhase != Phase.Running) - return GetState(GuessResult.NoAction); - - guess = guess.Trim(); - if (guess.Length > 1) - { - if (guess.Equals(_word, StringComparison.InvariantCultureIgnoreCase)) - { - CurrentPhase = Phase.Ended; - return GetState(GuessResult.Win); - } - - return GetState(GuessResult.NoAction); - } - - var charGuess = guess[0]; - if (!char.IsLetter(charGuess)) - return GetState(GuessResult.NoAction); - - if (_incorrect.Contains(charGuess) || _correct.Contains(charGuess)) - return GetState(GuessResult.AlreadyTried); - - if (_remaining.Remove(charGuess)) - { - if (_remaining.Count == 0) - { - CurrentPhase = Phase.Ended; - return GetState(GuessResult.Win); - } - - return GetState(GuessResult.Guess); - } - - _incorrect.Add(charGuess); - if (_incorrect.Count > 5) + if (guess.Equals(_word, StringComparison.InvariantCultureIgnoreCase)) { CurrentPhase = Phase.Ended; - return GetState(GuessResult.Incorrect); + return GetState(GuessResult.Win); } + return GetState(GuessResult.NoAction); + } + + var charGuess = guess[0]; + if (!char.IsLetter(charGuess)) + return GetState(GuessResult.NoAction); + + if (_incorrect.Contains(charGuess) || _correct.Contains(charGuess)) + return GetState(GuessResult.AlreadyTried); + + if (_remaining.Remove(charGuess)) + { + if (_remaining.Count == 0) + { + CurrentPhase = Phase.Ended; + return GetState(GuessResult.Win); + } + + return GetState(GuessResult.Guess); + } + + _incorrect.Add(charGuess); + if (_incorrect.Count > 5) + { + CurrentPhase = Phase.Ended; return GetState(GuessResult.Incorrect); } + + return GetState(GuessResult.Incorrect); } } \ No newline at end of file diff --git a/src/NadekoBot/Modules/Games/Common/Hangman/HangmanService.cs b/src/NadekoBot/Modules/Games/Common/Hangman/HangmanService.cs index b287bea88..1273afbe8 100644 --- a/src/NadekoBot/Modules/Games/Common/Hangman/HangmanService.cs +++ b/src/NadekoBot/Modules/Games/Common/Hangman/HangmanService.cs @@ -1,7 +1,5 @@ #nullable enable -using System; using System.Collections.Concurrent; -using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Threading.Tasks; using Discord; @@ -11,137 +9,136 @@ using NadekoBot.Extensions; using NadekoBot.Modules.Games.Services; using NadekoBot.Services; -namespace NadekoBot.Modules.Games.Hangman +namespace NadekoBot.Modules.Games.Hangman; + +public sealed class HangmanService : IHangmanService, ILateExecutor { - public sealed class HangmanService : IHangmanService, ILateExecutor + private readonly ConcurrentDictionary _hangmanGames = new(); + private readonly IHangmanSource _source; + private readonly IEmbedBuilderService _eb; + private readonly GamesConfigService _gcs; + private readonly ICurrencyService _cs; + private readonly IMemoryCache _cdCache; + private readonly object _locker = new(); + + public HangmanService(IHangmanSource source, IEmbedBuilderService eb, GamesConfigService gcs, + ICurrencyService cs, IMemoryCache cdCache) { - private readonly ConcurrentDictionary _hangmanGames = new(); - private readonly IHangmanSource _source; - private readonly IEmbedBuilderService _eb; - private readonly GamesConfigService _gcs; - private readonly ICurrencyService _cs; - private readonly IMemoryCache _cdCache; - private readonly object _locker = new(); + _source = source; + _eb = eb; + _gcs = gcs; + _cs = cs; + _cdCache = cdCache; + } - public HangmanService(IHangmanSource source, IEmbedBuilderService eb, GamesConfigService gcs, - ICurrencyService cs, IMemoryCache cdCache) + public bool StartHangman( + ulong channelId, + string? category, + [NotNullWhen(true)] out HangmanGame.State? state) + { + state = null; + if (!_source.GetTerm(category, out var term)) + return false; + + + var game = new HangmanGame(term); + lock (_locker) { - _source = source; - _eb = eb; - _gcs = gcs; - _cs = cs; - _cdCache = cdCache; - } - - public bool StartHangman( - ulong channelId, - string? category, - [NotNullWhen(true)] out HangmanGame.State? state) - { - state = null; - if (!_source.GetTerm(category, out var term)) - return false; - - - var game = new HangmanGame(term); - lock (_locker) + var hc = _hangmanGames.GetOrAdd(channelId, game); + if (hc == game) { - var hc = _hangmanGames.GetOrAdd(channelId, game); - if (hc == game) - { - state = hc.GetState(); - return true; - } - - return false; - } - } - - public ValueTask StopHangman(ulong channelId) - { - lock (_locker) - { - if (_hangmanGames.TryRemove(channelId, out var game)) - { - return new(true); - } + state = hc.GetState(); + return true; } - return new(false); - } - - public IReadOnlyCollection GetHangmanTypes() - => _source.GetCategories(); - - public async Task LateExecute(IGuild guild, IUserMessage msg) - { - if (_hangmanGames.ContainsKey(msg.Channel.Id)) - { - if (string.IsNullOrWhiteSpace(msg.Content)) - return; - - if (_cdCache.TryGetValue(msg.Author.Id, out _)) - return; - - HangmanGame.State state; - long rew = 0; - lock (_locker) - { - if (!_hangmanGames.TryGetValue(msg.Channel.Id, out var game)) - return; - - state = game.Guess(msg.Content.ToLowerInvariant()); - - if (state.GuessResult == HangmanGame.GuessResult.NoAction) - return; - - if (state.GuessResult == HangmanGame.GuessResult.Incorrect - || state.GuessResult == HangmanGame.GuessResult.AlreadyTried) - { - _cdCache.Set(msg.Author.Id, string.Empty, new MemoryCacheEntryOptions() - { - AbsoluteExpirationRelativeToNow = TimeSpan.FromSeconds(3) - }); - } - - if (state.Phase == HangmanGame.Phase.Ended) - if (_hangmanGames.TryRemove(msg.Channel.Id, out _)) - rew = _gcs.Data.Hangman.CurrencyReward; - } - - if (rew > 0) - await _cs.AddAsync(msg.Author, "hangman win", rew, gamble: true); - - await SendState((ITextChannel)msg.Channel, msg.Author, msg.Content, state); - } - } - - private Task SendState(ITextChannel channel, IUser user, string content, HangmanGame.State state) - { - var embed = Games.HangmanCommands.GetEmbed(_eb, state); - if (state.GuessResult == HangmanGame.GuessResult.Guess) - embed.WithDescription($"{user} guessed the letter {content}!") - .WithOkColor(); - else if (state.GuessResult == HangmanGame.GuessResult.Incorrect && state.Failed) - embed.WithDescription($"{user} Letter {content} doesn't exist! Game over!") - .WithErrorColor(); - else if (state.GuessResult == HangmanGame.GuessResult.Incorrect) - embed.WithDescription($"{user} Letter {content} doesn't exist!") - .WithErrorColor(); - else if (state.GuessResult == HangmanGame.GuessResult.AlreadyTried) - embed.WithDescription($"{user} Letter {content} has already been used.") - .WithPendingColor(); - else if (state.GuessResult == HangmanGame.GuessResult.Win) - embed.WithDescription($"{user} won!") - .WithOkColor(); - - if (!string.IsNullOrWhiteSpace(state.ImageUrl) - && Uri.IsWellFormedUriString(state.ImageUrl, UriKind.Absolute)) - { - embed.WithImageUrl(state.ImageUrl); - } - - return channel.EmbedAsync(embed); + return false; } } + + public ValueTask StopHangman(ulong channelId) + { + lock (_locker) + { + if (_hangmanGames.TryRemove(channelId, out var game)) + { + return new(true); + } + } + + return new(false); + } + + public IReadOnlyCollection GetHangmanTypes() + => _source.GetCategories(); + + public async Task LateExecute(IGuild guild, IUserMessage msg) + { + if (_hangmanGames.ContainsKey(msg.Channel.Id)) + { + if (string.IsNullOrWhiteSpace(msg.Content)) + return; + + if (_cdCache.TryGetValue(msg.Author.Id, out _)) + return; + + HangmanGame.State state; + long rew = 0; + lock (_locker) + { + if (!_hangmanGames.TryGetValue(msg.Channel.Id, out var game)) + return; + + state = game.Guess(msg.Content.ToLowerInvariant()); + + if (state.GuessResult == HangmanGame.GuessResult.NoAction) + return; + + if (state.GuessResult == HangmanGame.GuessResult.Incorrect + || state.GuessResult == HangmanGame.GuessResult.AlreadyTried) + { + _cdCache.Set(msg.Author.Id, string.Empty, new MemoryCacheEntryOptions() + { + AbsoluteExpirationRelativeToNow = TimeSpan.FromSeconds(3) + }); + } + + if (state.Phase == HangmanGame.Phase.Ended) + if (_hangmanGames.TryRemove(msg.Channel.Id, out _)) + rew = _gcs.Data.Hangman.CurrencyReward; + } + + if (rew > 0) + await _cs.AddAsync(msg.Author, "hangman win", rew, gamble: true); + + await SendState((ITextChannel)msg.Channel, msg.Author, msg.Content, state); + } + } + + private Task SendState(ITextChannel channel, IUser user, string content, HangmanGame.State state) + { + var embed = Games.HangmanCommands.GetEmbed(_eb, state); + if (state.GuessResult == HangmanGame.GuessResult.Guess) + embed.WithDescription($"{user} guessed the letter {content}!") + .WithOkColor(); + else if (state.GuessResult == HangmanGame.GuessResult.Incorrect && state.Failed) + embed.WithDescription($"{user} Letter {content} doesn't exist! Game over!") + .WithErrorColor(); + else if (state.GuessResult == HangmanGame.GuessResult.Incorrect) + embed.WithDescription($"{user} Letter {content} doesn't exist!") + .WithErrorColor(); + else if (state.GuessResult == HangmanGame.GuessResult.AlreadyTried) + embed.WithDescription($"{user} Letter {content} has already been used.") + .WithPendingColor(); + else if (state.GuessResult == HangmanGame.GuessResult.Win) + embed.WithDescription($"{user} won!") + .WithOkColor(); + + if (!string.IsNullOrWhiteSpace(state.ImageUrl) + && Uri.IsWellFormedUriString(state.ImageUrl, UriKind.Absolute)) + { + embed.WithImageUrl(state.ImageUrl); + } + + return channel.EmbedAsync(embed); + } } \ No newline at end of file diff --git a/src/NadekoBot/Modules/Games/Common/Hangman/HangmanTerm.cs b/src/NadekoBot/Modules/Games/Common/Hangman/HangmanTerm.cs index 014038105..01c5815bd 100644 --- a/src/NadekoBot/Modules/Games/Common/Hangman/HangmanTerm.cs +++ b/src/NadekoBot/Modules/Games/Common/Hangman/HangmanTerm.cs @@ -1,8 +1,7 @@ -namespace NadekoBot.Modules.Games.Hangman +namespace NadekoBot.Modules.Games.Hangman; + +public sealed class HangmanTerm { - public sealed class HangmanTerm - { - public string Word { get; set; } - public string ImageUrl { get; set; } - } -} + public string Word { get; set; } + public string ImageUrl { get; set; } +} \ No newline at end of file diff --git a/src/NadekoBot/Modules/Games/Common/Hangman/IHangmanService.cs b/src/NadekoBot/Modules/Games/Common/Hangman/IHangmanService.cs index a48c3a796..1e63a7bbd 100644 --- a/src/NadekoBot/Modules/Games/Common/Hangman/IHangmanService.cs +++ b/src/NadekoBot/Modules/Games/Common/Hangman/IHangmanService.cs @@ -1,14 +1,12 @@ #nullable enable -using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Threading.Tasks; -namespace NadekoBot.Modules.Games.Hangman +namespace NadekoBot.Modules.Games.Hangman; + +public interface IHangmanService { - public interface IHangmanService - { - bool StartHangman(ulong channelId, string? category, [NotNullWhen(true)] out HangmanGame.State? hangmanController); - ValueTask StopHangman(ulong channelId); - IReadOnlyCollection GetHangmanTypes(); - } + bool StartHangman(ulong channelId, string? category, [NotNullWhen(true)] out HangmanGame.State? hangmanController); + ValueTask StopHangman(ulong channelId); + IReadOnlyCollection GetHangmanTypes(); } \ No newline at end of file diff --git a/src/NadekoBot/Modules/Games/Common/Hangman/IHangmanSource.cs b/src/NadekoBot/Modules/Games/Common/Hangman/IHangmanSource.cs index 340e976f3..55913bce0 100644 --- a/src/NadekoBot/Modules/Games/Common/Hangman/IHangmanSource.cs +++ b/src/NadekoBot/Modules/Games/Common/Hangman/IHangmanSource.cs @@ -1,14 +1,12 @@ #nullable enable -using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using NadekoBot.Services; -namespace NadekoBot.Modules.Games.Hangman +namespace NadekoBot.Modules.Games.Hangman; + +public interface IHangmanSource : INService { - public interface IHangmanSource : INService - { - public IReadOnlyCollection GetCategories(); - public void Reload(); - public bool GetTerm(string? category, [NotNullWhen(true)] out HangmanTerm? term); - } + public IReadOnlyCollection GetCategories(); + public void Reload(); + public bool GetTerm(string? category, [NotNullWhen(true)] out HangmanTerm? term); } \ No newline at end of file diff --git a/src/NadekoBot/Modules/Games/Common/Nunchi/Nunchi.cs b/src/NadekoBot/Modules/Games/Common/Nunchi/Nunchi.cs index 72f91b886..061ca52c1 100644 --- a/src/NadekoBot/Modules/Games/Common/Nunchi/Nunchi.cs +++ b/src/NadekoBot/Modules/Games/Common/Nunchi/Nunchi.cs @@ -1,185 +1,181 @@ using NadekoBot.Common; -using System; -using System.Collections.Generic; using System.Collections.Immutable; -using System.Linq; using System.Threading; using System.Threading.Tasks; -namespace NadekoBot.Modules.Games.Common.Nunchi +namespace NadekoBot.Modules.Games.Common.Nunchi; + +public sealed class NunchiGame : IDisposable { - public sealed class NunchiGame : IDisposable + public enum Phase { - public enum Phase + Joining, + Playing, + WaitingForNextRound, + Ended, + } + + public int CurrentNumber { get; private set; } = new NadekoRandom().Next(0, 100); + public Phase CurrentPhase { get; private set; } = Phase.Joining; + + public event Func OnGameStarted; + public event Func OnRoundStarted; + public event Func OnUserGuessed; + public event Func OnRoundEnded; // tuple of the user who failed + public event Func OnGameEnded; // name of the user who won + + private readonly SemaphoreSlim _locker = new SemaphoreSlim(1, 1); + + private HashSet<(ulong Id, string Name)> _participants = new HashSet<(ulong Id, string Name)>(); + private HashSet<(ulong Id, string Name)> _passed = new HashSet<(ulong Id, string Name)>(); + + public ImmutableArray<(ulong Id, string Name)> Participants => _participants.ToImmutableArray(); + public int ParticipantCount => _participants.Count; + + private const int _killTimeout = 20 * 1000; + private const int _nextRoundTimeout = 5 * 1000; + private Timer _killTimer; + + public NunchiGame(ulong creatorId, string creatorName) + { + _participants.Add((creatorId, creatorName)); + } + + public async Task Join(ulong userId, string userName) + { + await _locker.WaitAsync().ConfigureAwait(false); + try { - Joining, - Playing, - WaitingForNextRound, - Ended, + if (CurrentPhase != Phase.Joining) + return false; + + return _participants.Add((userId, userName)); } + finally { _locker.Release(); } + } - public int CurrentNumber { get; private set; } = new NadekoRandom().Next(0, 100); - public Phase CurrentPhase { get; private set; } = Phase.Joining; - - public event Func OnGameStarted; - public event Func OnRoundStarted; - public event Func OnUserGuessed; - public event Func OnRoundEnded; // tuple of the user who failed - public event Func OnGameEnded; // name of the user who won - - private readonly SemaphoreSlim _locker = new SemaphoreSlim(1, 1); - - private HashSet<(ulong Id, string Name)> _participants = new HashSet<(ulong Id, string Name)>(); - private HashSet<(ulong Id, string Name)> _passed = new HashSet<(ulong Id, string Name)>(); - - public ImmutableArray<(ulong Id, string Name)> Participants => _participants.ToImmutableArray(); - public int ParticipantCount => _participants.Count; - - private const int _killTimeout = 20 * 1000; - private const int _nextRoundTimeout = 5 * 1000; - private Timer _killTimer; - - public NunchiGame(ulong creatorId, string creatorName) + public async Task Initialize() + { + CurrentPhase = Phase.Joining; + await Task.Delay(30000).ConfigureAwait(false); + await _locker.WaitAsync().ConfigureAwait(false); + try { - _participants.Add((creatorId, creatorName)); - } - - public async Task Join(ulong userId, string userName) - { - await _locker.WaitAsync().ConfigureAwait(false); - try + if (_participants.Count < 3) { - if (CurrentPhase != Phase.Joining) - return false; - - return _participants.Add((userId, userName)); - } - finally { _locker.Release(); } - } - - public async Task Initialize() - { - CurrentPhase = Phase.Joining; - await Task.Delay(30000).ConfigureAwait(false); - await _locker.WaitAsync().ConfigureAwait(false); - try - { - if (_participants.Count < 3) - { - CurrentPhase = Phase.Ended; - return false; - } - - _killTimer = new Timer(async state => - { - await _locker.WaitAsync().ConfigureAwait(false); - try - { - if (CurrentPhase != Phase.Playing) - return; - - //if some players took too long to type a number, boot them all out and start a new round - _participants = new HashSet<(ulong, string)>(_passed); - EndRound(); - } - finally { _locker.Release(); } - }, null, _killTimeout, _killTimeout); - - CurrentPhase = Phase.Playing; - var _ = OnGameStarted?.Invoke(this); - var __ = OnRoundStarted?.Invoke(this, CurrentNumber); - return true; - } - finally { _locker.Release(); } - } - - public async Task Input(ulong userId, string userName, int input) - { - await _locker.WaitAsync().ConfigureAwait(false); - try - { - if (CurrentPhase != Phase.Playing) - return; - - var userTuple = (Id: userId, Name: userName); - - // if the user is not a member of the race, - // or he already successfully typed the number - // ignore the input - if (!_participants.Contains(userTuple) || !_passed.Add(userTuple)) - return; - - //if the number is correct - if (CurrentNumber == input - 1) - { - //increment current number - ++CurrentNumber; - if (_passed.Count == _participants.Count - 1) - { - // if only n players are left, and n - 1 type the correct number, round is over - - // if only 2 players are left, game is over - if (_participants.Count == 2) - { - _killTimer.Change(Timeout.Infinite, Timeout.Infinite); - CurrentPhase = Phase.Ended; - var _ = OnGameEnded?.Invoke(this, userTuple.Name); - } - else // else just start the new round without the user who was the last - { - var failure = _participants.Except(_passed).First(); - - OnUserGuessed?.Invoke(this); - EndRound(failure); - return; - } - } - - OnUserGuessed?.Invoke(this); - } - else - { - //if the user failed - - EndRound(userTuple); - } - } - finally { _locker.Release(); } - } - - private void EndRound((ulong, string)? failure = null) - { - _killTimer.Change(_killTimeout, _killTimeout); - CurrentNumber = new NadekoRandom().Next(0, 100); // reset the counter - _passed.Clear(); // reset all users who passed (new round starts) - if(failure != null) - _participants.Remove(failure.Value); // remove the dude who failed from the list of players - - var __ = OnRoundEnded?.Invoke(this, failure); - if (_participants.Count <= 1) // means we have a winner or everyone was booted out - { - _killTimer.Change(Timeout.Infinite, Timeout.Infinite); CurrentPhase = Phase.Ended; - var _ = OnGameEnded?.Invoke(this, _participants.Count > 0 ? _participants.First().Name : null); - return; + return false; } - CurrentPhase = Phase.WaitingForNextRound; - var throwawayDelay = Task.Run(async () => - { - await Task.Delay(_nextRoundTimeout).ConfigureAwait(false); - CurrentPhase = Phase.Playing; - var ___ = OnRoundStarted?.Invoke(this, CurrentNumber); - }); - - } - public void Dispose() - { - OnGameEnded = null; - OnGameStarted = null; - OnRoundEnded = null; - OnRoundStarted = null; - OnUserGuessed = null; + _killTimer = new Timer(async state => + { + await _locker.WaitAsync().ConfigureAwait(false); + try + { + if (CurrentPhase != Phase.Playing) + return; + + //if some players took too long to type a number, boot them all out and start a new round + _participants = new HashSet<(ulong, string)>(_passed); + EndRound(); + } + finally { _locker.Release(); } + }, null, _killTimeout, _killTimeout); + + CurrentPhase = Phase.Playing; + var _ = OnGameStarted?.Invoke(this); + var __ = OnRoundStarted?.Invoke(this, CurrentNumber); + return true; } + finally { _locker.Release(); } + } + + public async Task Input(ulong userId, string userName, int input) + { + await _locker.WaitAsync().ConfigureAwait(false); + try + { + if (CurrentPhase != Phase.Playing) + return; + + var userTuple = (Id: userId, Name: userName); + + // if the user is not a member of the race, + // or he already successfully typed the number + // ignore the input + if (!_participants.Contains(userTuple) || !_passed.Add(userTuple)) + return; + + //if the number is correct + if (CurrentNumber == input - 1) + { + //increment current number + ++CurrentNumber; + if (_passed.Count == _participants.Count - 1) + { + // if only n players are left, and n - 1 type the correct number, round is over + + // if only 2 players are left, game is over + if (_participants.Count == 2) + { + _killTimer.Change(Timeout.Infinite, Timeout.Infinite); + CurrentPhase = Phase.Ended; + var _ = OnGameEnded?.Invoke(this, userTuple.Name); + } + else // else just start the new round without the user who was the last + { + var failure = _participants.Except(_passed).First(); + + OnUserGuessed?.Invoke(this); + EndRound(failure); + return; + } + } + + OnUserGuessed?.Invoke(this); + } + else + { + //if the user failed + + EndRound(userTuple); + } + } + finally { _locker.Release(); } + } + + private void EndRound((ulong, string)? failure = null) + { + _killTimer.Change(_killTimeout, _killTimeout); + CurrentNumber = new NadekoRandom().Next(0, 100); // reset the counter + _passed.Clear(); // reset all users who passed (new round starts) + if(failure != null) + _participants.Remove(failure.Value); // remove the dude who failed from the list of players + + var __ = OnRoundEnded?.Invoke(this, failure); + if (_participants.Count <= 1) // means we have a winner or everyone was booted out + { + _killTimer.Change(Timeout.Infinite, Timeout.Infinite); + CurrentPhase = Phase.Ended; + var _ = OnGameEnded?.Invoke(this, _participants.Count > 0 ? _participants.First().Name : null); + return; + } + CurrentPhase = Phase.WaitingForNextRound; + var throwawayDelay = Task.Run(async () => + { + await Task.Delay(_nextRoundTimeout).ConfigureAwait(false); + CurrentPhase = Phase.Playing; + var ___ = OnRoundStarted?.Invoke(this, CurrentNumber); + }); + + } + + public void Dispose() + { + OnGameEnded = null; + OnGameStarted = null; + OnRoundEnded = null; + OnRoundStarted = null; + OnUserGuessed = null; } } \ No newline at end of file diff --git a/src/NadekoBot/Modules/Games/Common/PollRunner.cs b/src/NadekoBot/Modules/Games/Common/PollRunner.cs index 1301e7f39..2a6d4e13c 100644 --- a/src/NadekoBot/Modules/Games/Common/PollRunner.cs +++ b/src/NadekoBot/Modules/Games/Common/PollRunner.cs @@ -1,74 +1,70 @@ using System.Threading.Tasks; using Discord; -using NadekoBot.Db.Models; -using System; -using System.Linq; using System.Threading; using NadekoBot.Services; using NadekoBot.Services.Database.Models; -namespace NadekoBot.Modules.Games.Common +namespace NadekoBot.Modules.Games.Common; + +public class PollRunner { - public class PollRunner + public Poll Poll { get; } + private readonly DbService _db; + + public event Func OnVoted; + + private readonly SemaphoreSlim _locker = new SemaphoreSlim(1, 1); + + public PollRunner(DbService db, Poll poll) { - public Poll Poll { get; } - private readonly DbService _db; - - public event Func OnVoted; - - private readonly SemaphoreSlim _locker = new SemaphoreSlim(1, 1); - - public PollRunner(DbService db, Poll poll) - { - _db = db; - Poll = poll; - } - - public async Task TryVote(IUserMessage msg) - { - PollVote voteObj; - await _locker.WaitAsync().ConfigureAwait(false); - try - { - // has to be a user message - // channel must be the same the poll started in - if (msg is null || msg.Author.IsBot || msg.Channel.Id != Poll.ChannelId) - return false; - - // has to be an integer - if (!int.TryParse(msg.Content, out int vote)) - return false; - --vote; - if (vote < 0 || vote >= Poll.Answers.Count) - return false; - - var usr = msg.Author as IGuildUser; - if (usr is null) - return false; - - voteObj = new PollVote() - { - UserId = msg.Author.Id, - VoteIndex = vote, - }; - if (!Poll.Votes.Add(voteObj)) - return false; - - var _ = OnVoted?.Invoke(msg, usr); - } - finally { _locker.Release(); } - using (var uow = _db.GetDbContext()) - { - var trackedPoll = uow.Poll.FirstOrDefault(x => x.Id == Poll.Id); - trackedPoll.Votes.Add(voteObj); - uow.SaveChanges(); - } - return true; - } - - public void End() - { - OnVoted = null; - } + _db = db; + Poll = poll; } -} + + public async Task TryVote(IUserMessage msg) + { + PollVote voteObj; + await _locker.WaitAsync().ConfigureAwait(false); + try + { + // has to be a user message + // channel must be the same the poll started in + if (msg is null || msg.Author.IsBot || msg.Channel.Id != Poll.ChannelId) + return false; + + // has to be an integer + if (!int.TryParse(msg.Content, out int vote)) + return false; + --vote; + if (vote < 0 || vote >= Poll.Answers.Count) + return false; + + var usr = msg.Author as IGuildUser; + if (usr is null) + return false; + + voteObj = new PollVote() + { + UserId = msg.Author.Id, + VoteIndex = vote, + }; + if (!Poll.Votes.Add(voteObj)) + return false; + + var _ = OnVoted?.Invoke(msg, usr); + } + finally { _locker.Release(); } + using (var uow = _db.GetDbContext()) + { + var trackedPoll = uow.Poll.FirstOrDefault(x => x.Id == Poll.Id); + trackedPoll.Votes.Add(voteObj); + uow.SaveChanges(); + } + return true; + } + + public void End() + { + OnVoted = null; + } +} \ No newline at end of file diff --git a/src/NadekoBot/Modules/Games/Common/TicTacToe.cs b/src/NadekoBot/Modules/Games/Common/TicTacToe.cs index 4d8ddd498..89ebd7392 100644 --- a/src/NadekoBot/Modules/Games/Common/TicTacToe.cs +++ b/src/NadekoBot/Modules/Games/Common/TicTacToe.cs @@ -1,7 +1,6 @@ using Discord; using Discord.WebSocket; using NadekoBot.Extensions; -using System; using System.Text; using System.Threading; using System.Threading.Tasks; @@ -9,285 +8,284 @@ using CommandLine; using NadekoBot.Common; using NadekoBot.Services; -namespace NadekoBot.Modules.Games.Common +namespace NadekoBot.Modules.Games.Common; + +public class TicTacToe { - public class TicTacToe + public class Options : INadekoCommandOptions { - public class Options : INadekoCommandOptions + public void NormalizeOptions() { - public void NormalizeOptions() + if (TurnTimer < 5 || TurnTimer > 60) + TurnTimer = 15; + } + + [Option('t', "turn-timer", Required = false, Default = 15, HelpText = "Turn time in seconds. Default 15.")] + public int TurnTimer { get; set; } = 15; + } + + enum Phase + { + Starting, + Started, + Ended + } + + private readonly ITextChannel _channel; + private readonly IGuildUser[] _users; + private readonly int?[,] _state; + private Phase _phase; + private int _curUserIndex; + private readonly SemaphoreSlim _moveLock; + + private IGuildUser _winner; + + private readonly string[] _numbers = { ":one:", ":two:", ":three:", ":four:", ":five:", ":six:", ":seven:", ":eight:", ":nine:" }; + + public event Action OnEnded; + + private IUserMessage _previousMessage; + private Timer _timeoutTimer; + private readonly IBotStrings _strings; + private readonly DiscordSocketClient _client; + private readonly Options _options; + private readonly IEmbedBuilderService _eb; + + public TicTacToe(IBotStrings strings, DiscordSocketClient client, ITextChannel channel, + IGuildUser firstUser, Options options, IEmbedBuilderService eb) + { + _channel = channel; + _strings = strings; + _client = client; + _options = options; + _eb = eb; + + _users = new[] { firstUser, null }; + _state = new int?[,] { + { null, null, null }, + { null, null, null }, + { null, null, null }, + }; + + _phase = Phase.Starting; + _moveLock = new SemaphoreSlim(1, 1); + } + + private string GetText(LocStr key) + => _strings.GetText(key, _channel.GuildId); + + public string GetState() + { + var sb = new StringBuilder(); + for (var i = 0; i < _state.GetLength(0); i++) + { + for (var j = 0; j < _state.GetLength(1); j++) { - if (TurnTimer < 5 || TurnTimer > 60) - TurnTimer = 15; + sb.Append(_state[i, j] is null ? _numbers[i * 3 + j] : GetIcon(_state[i, j])); + if (j < _state.GetLength(1) - 1) + sb.Append("┃"); } - - [Option('t', "turn-timer", Required = false, Default = 15, HelpText = "Turn time in seconds. Default 15.")] - public int TurnTimer { get; set; } = 15; + if (i < _state.GetLength(0) - 1) + sb.AppendLine("\n──────────"); } - enum Phase + return sb.ToString(); + } + + public IEmbedBuilder GetEmbed(string title = null) + { + var embed = _eb.Create() + .WithOkColor() + .WithDescription(Environment.NewLine + GetState()) + .WithAuthor(GetText(strs.vs(_users[0], _users[1]))); + + if (!string.IsNullOrWhiteSpace(title)) + embed.WithTitle(title); + + if (_winner is null) { - Starting, - Started, - Ended - } - - private readonly ITextChannel _channel; - private readonly IGuildUser[] _users; - private readonly int?[,] _state; - private Phase _phase; - private int _curUserIndex; - private readonly SemaphoreSlim _moveLock; - - private IGuildUser _winner; - - private readonly string[] _numbers = { ":one:", ":two:", ":three:", ":four:", ":five:", ":six:", ":seven:", ":eight:", ":nine:" }; - - public event Action OnEnded; - - private IUserMessage _previousMessage; - private Timer _timeoutTimer; - private readonly IBotStrings _strings; - private readonly DiscordSocketClient _client; - private readonly Options _options; - private readonly IEmbedBuilderService _eb; - - public TicTacToe(IBotStrings strings, DiscordSocketClient client, ITextChannel channel, - IGuildUser firstUser, Options options, IEmbedBuilderService eb) - { - _channel = channel; - _strings = strings; - _client = client; - _options = options; - _eb = eb; - - _users = new[] { firstUser, null }; - _state = new int?[,] { - { null, null, null }, - { null, null, null }, - { null, null, null }, - }; - - _phase = Phase.Starting; - _moveLock = new SemaphoreSlim(1, 1); - } - - private string GetText(LocStr key) - => _strings.GetText(key, _channel.GuildId); - - public string GetState() - { - var sb = new StringBuilder(); - for (var i = 0; i < _state.GetLength(0); i++) - { - for (var j = 0; j < _state.GetLength(1); j++) - { - sb.Append(_state[i, j] is null ? _numbers[i * 3 + j] : GetIcon(_state[i, j])); - if (j < _state.GetLength(1) - 1) - sb.Append("┃"); - } - if (i < _state.GetLength(0) - 1) - sb.AppendLine("\n──────────"); - } - - return sb.ToString(); - } - - public IEmbedBuilder GetEmbed(string title = null) - { - var embed = _eb.Create() - .WithOkColor() - .WithDescription(Environment.NewLine + GetState()) - .WithAuthor(GetText(strs.vs(_users[0], _users[1]))); - - if (!string.IsNullOrWhiteSpace(title)) - embed.WithTitle(title); - - if (_winner is null) - { - if (_phase == Phase.Ended) - embed.WithFooter(GetText(strs.ttt_no_moves)); - else - embed.WithFooter(GetText(strs.ttt_users_move(_users[_curUserIndex]))); - } + if (_phase == Phase.Ended) + embed.WithFooter(GetText(strs.ttt_no_moves)); else - embed.WithFooter(GetText(strs.ttt_has_won(_winner))); - - return embed; + embed.WithFooter(GetText(strs.ttt_users_move(_users[_curUserIndex]))); } + else + embed.WithFooter(GetText(strs.ttt_has_won(_winner))); - private static string GetIcon(int? val) + return embed; + } + + private static string GetIcon(int? val) + { + switch (val) { - switch (val) - { - case 0: - return "❌"; - case 1: - return "⭕"; - case 2: - return "❎"; - case 3: - return "🅾"; - default: - return "⬛"; - } - } - - public async Task Start(IGuildUser user) - { - if (_phase == Phase.Started || _phase == Phase.Ended) - { - await _channel.SendErrorAsync(_eb, user.Mention + GetText(strs.ttt_already_running)).ConfigureAwait(false); - return; - } - else if (_users[0] == user) - { - await _channel.SendErrorAsync(_eb, user.Mention + GetText(strs.ttt_against_yourself)).ConfigureAwait(false); - return; - } - - _users[1] = user; - - _phase = Phase.Started; - - _timeoutTimer = new Timer(async (_) => - { - await _moveLock.WaitAsync().ConfigureAwait(false); - try - { - if (_phase == Phase.Ended) - return; - - _phase = Phase.Ended; - if (_users[1] != null) - { - _winner = _users[_curUserIndex ^= 1]; - var del = _previousMessage?.DeleteAsync(); - try - { - await _channel.EmbedAsync(GetEmbed(GetText(strs.ttt_time_expired))).ConfigureAwait(false); - if (del != null) - await del.ConfigureAwait(false); - } - catch { } - } - - OnEnded?.Invoke(this); - } - catch { } - finally - { - _moveLock.Release(); - } - }, null, _options.TurnTimer * 1000, Timeout.Infinite); - - _client.MessageReceived += Client_MessageReceived; - - - _previousMessage = await _channel.EmbedAsync(GetEmbed(GetText(strs.game_started))).ConfigureAwait(false); - } - - private bool IsDraw() - { - for (var i = 0; i < 3; i++) - { - for (var j = 0; j < 3; j++) - { - if (_state[i, j] is null) - return false; - } - } - return true; - } - - private Task Client_MessageReceived(SocketMessage msg) - { - var _ = Task.Run(async () => - { - await _moveLock.WaitAsync().ConfigureAwait(false); - try - { - var curUser = _users[_curUserIndex]; - if (_phase == Phase.Ended || msg.Author?.Id != curUser.Id) - return; - - if (int.TryParse(msg.Content, out var index) && - --index >= 0 && - index <= 9 && - _state[index / 3, index % 3] is null) - { - _state[index / 3, index % 3] = _curUserIndex; - - // i'm lazy - if (_state[index / 3, 0] == _state[index / 3, 1] && _state[index / 3, 1] == _state[index / 3, 2]) - { - _state[index / 3, 0] = _curUserIndex + 2; - _state[index / 3, 1] = _curUserIndex + 2; - _state[index / 3, 2] = _curUserIndex + 2; - - _phase = Phase.Ended; - } - else if (_state[0, index % 3] == _state[1, index % 3] && _state[1, index % 3] == _state[2, index % 3]) - { - _state[0, index % 3] = _curUserIndex + 2; - _state[1, index % 3] = _curUserIndex + 2; - _state[2, index % 3] = _curUserIndex + 2; - - _phase = Phase.Ended; - } - else if (_curUserIndex == _state[0, 0] && _state[0, 0] == _state[1, 1] && _state[1, 1] == _state[2, 2]) - { - _state[0, 0] = _curUserIndex + 2; - _state[1, 1] = _curUserIndex + 2; - _state[2, 2] = _curUserIndex + 2; - - _phase = Phase.Ended; - } - else if (_curUserIndex == _state[0, 2] && _state[0, 2] == _state[1, 1] && _state[1, 1] == _state[2, 0]) - { - _state[0, 2] = _curUserIndex + 2; - _state[1, 1] = _curUserIndex + 2; - _state[2, 0] = _curUserIndex + 2; - - _phase = Phase.Ended; - } - var reason = ""; - - if (_phase == Phase.Ended) // if user won, stop receiving moves - { - reason = GetText(strs.ttt_matched_three); - _winner = _users[_curUserIndex]; - _client.MessageReceived -= Client_MessageReceived; - OnEnded?.Invoke(this); - } - else if (IsDraw()) - { - reason = GetText(strs.ttt_a_draw); - _phase = Phase.Ended; - _client.MessageReceived -= Client_MessageReceived; - OnEnded?.Invoke(this); - } - - var sendstate = Task.Run(async () => - { - var del1 = msg.DeleteAsync(); - var del2 = _previousMessage?.DeleteAsync(); - try { _previousMessage = await _channel.EmbedAsync(GetEmbed(reason)).ConfigureAwait(false); } catch { } - try { await del1.ConfigureAwait(false); } catch { } - try { if (del2 != null) await del2.ConfigureAwait(false); } catch { } - }); - _curUserIndex ^= 1; - - _timeoutTimer.Change(_options.TurnTimer * 1000, Timeout.Infinite); - } - } - finally - { - _moveLock.Release(); - } - }); - - return Task.CompletedTask; + case 0: + return "❌"; + case 1: + return "⭕"; + case 2: + return "❎"; + case 3: + return "🅾"; + default: + return "⬛"; } } -} + + public async Task Start(IGuildUser user) + { + if (_phase == Phase.Started || _phase == Phase.Ended) + { + await _channel.SendErrorAsync(_eb, user.Mention + GetText(strs.ttt_already_running)).ConfigureAwait(false); + return; + } + else if (_users[0] == user) + { + await _channel.SendErrorAsync(_eb, user.Mention + GetText(strs.ttt_against_yourself)).ConfigureAwait(false); + return; + } + + _users[1] = user; + + _phase = Phase.Started; + + _timeoutTimer = new Timer(async (_) => + { + await _moveLock.WaitAsync().ConfigureAwait(false); + try + { + if (_phase == Phase.Ended) + return; + + _phase = Phase.Ended; + if (_users[1] != null) + { + _winner = _users[_curUserIndex ^= 1]; + var del = _previousMessage?.DeleteAsync(); + try + { + await _channel.EmbedAsync(GetEmbed(GetText(strs.ttt_time_expired))).ConfigureAwait(false); + if (del != null) + await del.ConfigureAwait(false); + } + catch { } + } + + OnEnded?.Invoke(this); + } + catch { } + finally + { + _moveLock.Release(); + } + }, null, _options.TurnTimer * 1000, Timeout.Infinite); + + _client.MessageReceived += Client_MessageReceived; + + + _previousMessage = await _channel.EmbedAsync(GetEmbed(GetText(strs.game_started))).ConfigureAwait(false); + } + + private bool IsDraw() + { + for (var i = 0; i < 3; i++) + { + for (var j = 0; j < 3; j++) + { + if (_state[i, j] is null) + return false; + } + } + return true; + } + + private Task Client_MessageReceived(SocketMessage msg) + { + var _ = Task.Run(async () => + { + await _moveLock.WaitAsync().ConfigureAwait(false); + try + { + var curUser = _users[_curUserIndex]; + if (_phase == Phase.Ended || msg.Author?.Id != curUser.Id) + return; + + if (int.TryParse(msg.Content, out var index) && + --index >= 0 && + index <= 9 && + _state[index / 3, index % 3] is null) + { + _state[index / 3, index % 3] = _curUserIndex; + + // i'm lazy + if (_state[index / 3, 0] == _state[index / 3, 1] && _state[index / 3, 1] == _state[index / 3, 2]) + { + _state[index / 3, 0] = _curUserIndex + 2; + _state[index / 3, 1] = _curUserIndex + 2; + _state[index / 3, 2] = _curUserIndex + 2; + + _phase = Phase.Ended; + } + else if (_state[0, index % 3] == _state[1, index % 3] && _state[1, index % 3] == _state[2, index % 3]) + { + _state[0, index % 3] = _curUserIndex + 2; + _state[1, index % 3] = _curUserIndex + 2; + _state[2, index % 3] = _curUserIndex + 2; + + _phase = Phase.Ended; + } + else if (_curUserIndex == _state[0, 0] && _state[0, 0] == _state[1, 1] && _state[1, 1] == _state[2, 2]) + { + _state[0, 0] = _curUserIndex + 2; + _state[1, 1] = _curUserIndex + 2; + _state[2, 2] = _curUserIndex + 2; + + _phase = Phase.Ended; + } + else if (_curUserIndex == _state[0, 2] && _state[0, 2] == _state[1, 1] && _state[1, 1] == _state[2, 0]) + { + _state[0, 2] = _curUserIndex + 2; + _state[1, 1] = _curUserIndex + 2; + _state[2, 0] = _curUserIndex + 2; + + _phase = Phase.Ended; + } + var reason = ""; + + if (_phase == Phase.Ended) // if user won, stop receiving moves + { + reason = GetText(strs.ttt_matched_three); + _winner = _users[_curUserIndex]; + _client.MessageReceived -= Client_MessageReceived; + OnEnded?.Invoke(this); + } + else if (IsDraw()) + { + reason = GetText(strs.ttt_a_draw); + _phase = Phase.Ended; + _client.MessageReceived -= Client_MessageReceived; + OnEnded?.Invoke(this); + } + + var sendstate = Task.Run(async () => + { + var del1 = msg.DeleteAsync(); + var del2 = _previousMessage?.DeleteAsync(); + try { _previousMessage = await _channel.EmbedAsync(GetEmbed(reason)).ConfigureAwait(false); } catch { } + try { await del1.ConfigureAwait(false); } catch { } + try { if (del2 != null) await del2.ConfigureAwait(false); } catch { } + }); + _curUserIndex ^= 1; + + _timeoutTimer.Change(_options.TurnTimer * 1000, Timeout.Infinite); + } + } + finally + { + _moveLock.Release(); + } + }); + + return Task.CompletedTask; + } +} \ No newline at end of file diff --git a/src/NadekoBot/Modules/Games/Common/Trivia/TriviaGame.cs b/src/NadekoBot/Modules/Games/Common/Trivia/TriviaGame.cs index dd625a70c..8a40f7072 100644 --- a/src/NadekoBot/Modules/Games/Common/Trivia/TriviaGame.cs +++ b/src/NadekoBot/Modules/Games/Common/Trivia/TriviaGame.cs @@ -1,7 +1,4 @@ -using System; -using System.Collections.Concurrent; -using System.Collections.Generic; -using System.Linq; +using System.Collections.Concurrent; using System.Text; using System.Threading; using System.Threading.Tasks; @@ -10,277 +7,273 @@ using Discord.Net; using Discord.WebSocket; using NadekoBot.Extensions; using NadekoBot.Services; -using NadekoBot.Modules.Games.Common.Trivia; -using NadekoBot.Modules.Games.Services; -using Serilog; -namespace NadekoBot.Modules.Games.Common.Trivia +namespace NadekoBot.Modules.Games.Common.Trivia; + +public class TriviaGame { - public class TriviaGame + private readonly SemaphoreSlim _guessLock = new SemaphoreSlim(1, 1); + private readonly IDataCache _cache; + private readonly IBotStrings _strings; + private readonly DiscordSocketClient _client; + private readonly GamesConfig _config; + private readonly ICurrencyService _cs; + private readonly TriviaOptions _options; + + public IGuild Guild { get; } + public ITextChannel Channel { get; } + + private CancellationTokenSource _triviaCancelSource; + + public TriviaQuestion CurrentQuestion { get; private set; } + public HashSet OldQuestions { get; } = new HashSet(); + + public ConcurrentDictionary Users { get; } = new ConcurrentDictionary(); + + public bool GameActive { get; private set; } + public bool ShouldStopGame { get; private set; } + + private readonly TriviaQuestionPool _questionPool; + private int _timeoutCount = 0; + private readonly string _quitCommand; + private readonly IEmbedBuilderService _eb; + + public TriviaGame(IBotStrings strings, DiscordSocketClient client, GamesConfig config, + IDataCache cache, ICurrencyService cs, IGuild guild, ITextChannel channel, + TriviaOptions options, string quitCommand, IEmbedBuilderService eb) { - private readonly SemaphoreSlim _guessLock = new SemaphoreSlim(1, 1); - private readonly IDataCache _cache; - private readonly IBotStrings _strings; - private readonly DiscordSocketClient _client; - private readonly GamesConfig _config; - private readonly ICurrencyService _cs; - private readonly TriviaOptions _options; + _cache = cache; + _questionPool = new TriviaQuestionPool(_cache); + _strings = strings; + _client = client; + _config = config; + _cs = cs; + _options = options; + _quitCommand = quitCommand; + _eb = eb; - public IGuild Guild { get; } - public ITextChannel Channel { get; } + Guild = guild; + Channel = channel; + } - private CancellationTokenSource _triviaCancelSource; + private string GetText(in LocStr key) + => _strings.GetText(key, Channel.GuildId); - public TriviaQuestion CurrentQuestion { get; private set; } - public HashSet OldQuestions { get; } = new HashSet(); - - public ConcurrentDictionary Users { get; } = new ConcurrentDictionary(); - - public bool GameActive { get; private set; } - public bool ShouldStopGame { get; private set; } - - private readonly TriviaQuestionPool _questionPool; - private int _timeoutCount = 0; - private readonly string _quitCommand; - private readonly IEmbedBuilderService _eb; - - public TriviaGame(IBotStrings strings, DiscordSocketClient client, GamesConfig config, - IDataCache cache, ICurrencyService cs, IGuild guild, ITextChannel channel, - TriviaOptions options, string quitCommand, IEmbedBuilderService eb) + public async Task StartGame() + { + var showHowToQuit = false; + while (!ShouldStopGame) { - _cache = cache; - _questionPool = new TriviaQuestionPool(_cache); - _strings = strings; - _client = client; - _config = config; - _cs = cs; - _options = options; - _quitCommand = quitCommand; - _eb = eb; + // reset the cancellation source + _triviaCancelSource = new CancellationTokenSource(); + showHowToQuit = !showHowToQuit; - Guild = guild; - Channel = channel; - } - - private string GetText(in LocStr key) - => _strings.GetText(key, Channel.GuildId); - - public async Task StartGame() - { - var showHowToQuit = false; - while (!ShouldStopGame) + // load question + CurrentQuestion = _questionPool.GetRandomQuestion(OldQuestions, _options.IsPokemon); + if (string.IsNullOrWhiteSpace(CurrentQuestion?.Answer) || string.IsNullOrWhiteSpace(CurrentQuestion.Question)) { - // reset the cancellation source - _triviaCancelSource = new CancellationTokenSource(); - showHowToQuit = !showHowToQuit; + await Channel.SendErrorAsync(_eb, GetText(strs.trivia_game), GetText(strs.failed_loading_question)).ConfigureAwait(false); + return; + } + OldQuestions.Add(CurrentQuestion); //add it to exclusion list so it doesn't show up again - // load question - CurrentQuestion = _questionPool.GetRandomQuestion(OldQuestions, _options.IsPokemon); - if (string.IsNullOrWhiteSpace(CurrentQuestion?.Answer) || string.IsNullOrWhiteSpace(CurrentQuestion.Question)) - { - await Channel.SendErrorAsync(_eb, GetText(strs.trivia_game), GetText(strs.failed_loading_question)).ConfigureAwait(false); - return; - } - OldQuestions.Add(CurrentQuestion); //add it to exclusion list so it doesn't show up again + IEmbedBuilder questionEmbed; + IUserMessage questionMessage; + try + { + questionEmbed = _eb.Create().WithOkColor() + .WithTitle(GetText(strs.trivia_game)) + .AddField(GetText(strs.category), CurrentQuestion.Category) + .AddField(GetText(strs.question), CurrentQuestion.Question); - IEmbedBuilder questionEmbed; - IUserMessage questionMessage; - try - { - questionEmbed = _eb.Create().WithOkColor() - .WithTitle(GetText(strs.trivia_game)) - .AddField(GetText(strs.category), CurrentQuestion.Category) - .AddField(GetText(strs.question), CurrentQuestion.Question); - - if (showHowToQuit) - questionEmbed.WithFooter(GetText(strs.trivia_quit(_quitCommand))); + if (showHowToQuit) + questionEmbed.WithFooter(GetText(strs.trivia_quit(_quitCommand))); - if (Uri.IsWellFormedUriString(CurrentQuestion.ImageUrl, UriKind.Absolute)) - questionEmbed.WithImageUrl(CurrentQuestion.ImageUrl); + if (Uri.IsWellFormedUriString(CurrentQuestion.ImageUrl, UriKind.Absolute)) + questionEmbed.WithImageUrl(CurrentQuestion.ImageUrl); - questionMessage = await Channel.EmbedAsync(questionEmbed).ConfigureAwait(false); - } - catch (HttpException ex) when (ex.HttpCode == System.Net.HttpStatusCode.NotFound || - ex.HttpCode == System.Net.HttpStatusCode.Forbidden || - ex.HttpCode == System.Net.HttpStatusCode.BadRequest) - { - return; - } - catch (Exception ex) - { - Log.Warning(ex, "Error sending trivia embed"); - await Task.Delay(2000).ConfigureAwait(false); - continue; - } - - //receive messages - try - { - _client.MessageReceived += PotentialGuess; - - //allow people to guess - GameActive = true; - try - { - //hint - await Task.Delay(_options.QuestionTimer * 1000 / 2, _triviaCancelSource.Token).ConfigureAwait(false); - if (!_options.NoHint) - try - { - await questionMessage.ModifyAsync(m => m.Embed = questionEmbed.WithFooter(CurrentQuestion.GetHint()).Build()) - .ConfigureAwait(false); - } - catch (HttpException ex) when (ex.HttpCode == System.Net.HttpStatusCode.NotFound || ex.HttpCode == System.Net.HttpStatusCode.Forbidden) - { - break; - } - catch (Exception ex) { Log.Warning(ex, "Error editing triva message"); } - - //timeout - await Task.Delay(_options.QuestionTimer * 1000 / 2, _triviaCancelSource.Token).ConfigureAwait(false); - - } - catch (TaskCanceledException) { _timeoutCount = 0; } //means someone guessed the answer - } - finally - { - GameActive = false; - _client.MessageReceived -= PotentialGuess; - } - if (!_triviaCancelSource.IsCancellationRequested) - { - try - { - var embed = _eb.Create().WithErrorColor() - .WithTitle(GetText(strs.trivia_game)) - .WithDescription(GetText(strs.trivia_times_up(Format.Bold(CurrentQuestion.Answer)))); - if (Uri.IsWellFormedUriString(CurrentQuestion.AnswerImageUrl, UriKind.Absolute)) - embed.WithImageUrl(CurrentQuestion.AnswerImageUrl); - - await Channel.EmbedAsync(embed).ConfigureAwait(false); - - if (_options.Timeout != 0 && ++_timeoutCount >= _options.Timeout) - await StopGame().ConfigureAwait(false); - } - catch (Exception ex) - { - Log.Warning(ex, "Error sending trivia time's up message"); - } - } - await Task.Delay(5000).ConfigureAwait(false); + questionMessage = await Channel.EmbedAsync(questionEmbed).ConfigureAwait(false); } - } - - public async Task EnsureStopped() - { - ShouldStopGame = true; - - await Channel.EmbedAsync(_eb.Create().WithOkColor() - .WithAuthor("Trivia Game Ended") - .WithTitle("Final Results") - .WithDescription(GetLeaderboard())).ConfigureAwait(false); - } - - public async Task StopGame() - { - var old = ShouldStopGame; - ShouldStopGame = true; - if (!old) + catch (HttpException ex) when (ex.HttpCode == System.Net.HttpStatusCode.NotFound || + ex.HttpCode == System.Net.HttpStatusCode.Forbidden || + ex.HttpCode == System.Net.HttpStatusCode.BadRequest) { - try - { - await Channel.SendConfirmAsync(_eb, - GetText(strs.trivia_game), - GetText(strs.trivia_stopping)); - - } - catch (Exception ex) - { - Log.Warning(ex, "Error sending trivia stopping message"); - } + return; } - } - - private Task PotentialGuess(SocketMessage imsg) - { - var _ = Task.Run(async () => + catch (Exception ex) { + Log.Warning(ex, "Error sending trivia embed"); + await Task.Delay(2000).ConfigureAwait(false); + continue; + } + + //receive messages + try + { + _client.MessageReceived += PotentialGuess; + + //allow people to guess + GameActive = true; try { - if (imsg.Author.IsBot) - return; - - var umsg = imsg as SocketUserMessage; - - var textChannel = umsg?.Channel as ITextChannel; - if (textChannel is null || textChannel.Guild != Guild) - return; - - var guildUser = (IGuildUser)umsg.Author; - - var guess = false; - await _guessLock.WaitAsync().ConfigureAwait(false); - try - { - if (GameActive && CurrentQuestion.IsAnswerCorrect(umsg.Content) && !_triviaCancelSource.IsCancellationRequested) - { - Users.AddOrUpdate(guildUser, 1, (gu, old) => ++old); - guess = true; - } - } - finally { _guessLock.Release(); } - if (!guess) return; - _triviaCancelSource.Cancel(); - - - if (_options.WinRequirement != 0 && Users[guildUser] == _options.WinRequirement) - { - ShouldStopGame = true; + //hint + await Task.Delay(_options.QuestionTimer * 1000 / 2, _triviaCancelSource.Token).ConfigureAwait(false); + if (!_options.NoHint) try { - var embedS = _eb.Create().WithOkColor() - .WithTitle(GetText(strs.trivia_game)) - .WithDescription(GetText(strs.trivia_win( - guildUser.Mention, - Format.Bold(CurrentQuestion.Answer)))); - if (Uri.IsWellFormedUriString(CurrentQuestion.AnswerImageUrl, UriKind.Absolute)) - embedS.WithImageUrl(CurrentQuestion.AnswerImageUrl); - await Channel.EmbedAsync(embedS).ConfigureAwait(false); + await questionMessage.ModifyAsync(m => m.Embed = questionEmbed.WithFooter(CurrentQuestion.GetHint()).Build()) + .ConfigureAwait(false); } - catch + catch (HttpException ex) when (ex.HttpCode == System.Net.HttpStatusCode.NotFound || ex.HttpCode == System.Net.HttpStatusCode.Forbidden) { - // ignored + break; } - var reward = _config.Trivia.CurrencyReward; - if (reward > 0) - await _cs.AddAsync(guildUser, "Won trivia", reward, true).ConfigureAwait(false); - return; - } - var embed = _eb.Create().WithOkColor() + catch (Exception ex) { Log.Warning(ex, "Error editing triva message"); } + + //timeout + await Task.Delay(_options.QuestionTimer * 1000 / 2, _triviaCancelSource.Token).ConfigureAwait(false); + + } + catch (TaskCanceledException) { _timeoutCount = 0; } //means someone guessed the answer + } + finally + { + GameActive = false; + _client.MessageReceived -= PotentialGuess; + } + if (!_triviaCancelSource.IsCancellationRequested) + { + try + { + var embed = _eb.Create().WithErrorColor() .WithTitle(GetText(strs.trivia_game)) - .WithDescription(GetText(strs.trivia_guess(guildUser.Mention, Format.Bold(CurrentQuestion.Answer)))); + .WithDescription(GetText(strs.trivia_times_up(Format.Bold(CurrentQuestion.Answer)))); if (Uri.IsWellFormedUriString(CurrentQuestion.AnswerImageUrl, UriKind.Absolute)) embed.WithImageUrl(CurrentQuestion.AnswerImageUrl); + await Channel.EmbedAsync(embed).ConfigureAwait(false); + + if (_options.Timeout != 0 && ++_timeoutCount >= _options.Timeout) + await StopGame().ConfigureAwait(false); + } + catch (Exception ex) + { + Log.Warning(ex, "Error sending trivia time's up message"); } - catch (Exception ex) { Log.Warning(ex.ToString()); } - }); - return Task.CompletedTask; - } - - public string GetLeaderboard() - { - if (Users.Count == 0) - return GetText(strs.no_results); - - var sb = new StringBuilder(); - - foreach (var kvp in Users.OrderByDescending(kvp => kvp.Value)) - { - sb.AppendLine(GetText(strs.trivia_points(Format.Bold(kvp.Key.ToString()), kvp.Value)).SnPl(kvp.Value)); } - - return sb.ToString(); + await Task.Delay(5000).ConfigureAwait(false); } } + + public async Task EnsureStopped() + { + ShouldStopGame = true; + + await Channel.EmbedAsync(_eb.Create().WithOkColor() + .WithAuthor("Trivia Game Ended") + .WithTitle("Final Results") + .WithDescription(GetLeaderboard())).ConfigureAwait(false); + } + + public async Task StopGame() + { + var old = ShouldStopGame; + ShouldStopGame = true; + if (!old) + { + try + { + await Channel.SendConfirmAsync(_eb, + GetText(strs.trivia_game), + GetText(strs.trivia_stopping)); + + } + catch (Exception ex) + { + Log.Warning(ex, "Error sending trivia stopping message"); + } + } + } + + private Task PotentialGuess(SocketMessage imsg) + { + var _ = Task.Run(async () => + { + try + { + if (imsg.Author.IsBot) + return; + + var umsg = imsg as SocketUserMessage; + + var textChannel = umsg?.Channel as ITextChannel; + if (textChannel is null || textChannel.Guild != Guild) + return; + + var guildUser = (IGuildUser)umsg.Author; + + var guess = false; + await _guessLock.WaitAsync().ConfigureAwait(false); + try + { + if (GameActive && CurrentQuestion.IsAnswerCorrect(umsg.Content) && !_triviaCancelSource.IsCancellationRequested) + { + Users.AddOrUpdate(guildUser, 1, (gu, old) => ++old); + guess = true; + } + } + finally { _guessLock.Release(); } + if (!guess) return; + _triviaCancelSource.Cancel(); + + + if (_options.WinRequirement != 0 && Users[guildUser] == _options.WinRequirement) + { + ShouldStopGame = true; + try + { + var embedS = _eb.Create().WithOkColor() + .WithTitle(GetText(strs.trivia_game)) + .WithDescription(GetText(strs.trivia_win( + guildUser.Mention, + Format.Bold(CurrentQuestion.Answer)))); + if (Uri.IsWellFormedUriString(CurrentQuestion.AnswerImageUrl, UriKind.Absolute)) + embedS.WithImageUrl(CurrentQuestion.AnswerImageUrl); + await Channel.EmbedAsync(embedS).ConfigureAwait(false); + } + catch + { + // ignored + } + var reward = _config.Trivia.CurrencyReward; + if (reward > 0) + await _cs.AddAsync(guildUser, "Won trivia", reward, true).ConfigureAwait(false); + return; + } + var embed = _eb.Create().WithOkColor() + .WithTitle(GetText(strs.trivia_game)) + .WithDescription(GetText(strs.trivia_guess(guildUser.Mention, Format.Bold(CurrentQuestion.Answer)))); + if (Uri.IsWellFormedUriString(CurrentQuestion.AnswerImageUrl, UriKind.Absolute)) + embed.WithImageUrl(CurrentQuestion.AnswerImageUrl); + await Channel.EmbedAsync(embed).ConfigureAwait(false); + } + catch (Exception ex) { Log.Warning(ex.ToString()); } + }); + return Task.CompletedTask; + } + + public string GetLeaderboard() + { + if (Users.Count == 0) + return GetText(strs.no_results); + + var sb = new StringBuilder(); + + foreach (var kvp in Users.OrderByDescending(kvp => kvp.Value)) + { + sb.AppendLine(GetText(strs.trivia_points(Format.Bold(kvp.Key.ToString()), kvp.Value)).SnPl(kvp.Value)); + } + + return sb.ToString(); + } } \ No newline at end of file diff --git a/src/NadekoBot/Modules/Games/Common/Trivia/TriviaOptions.cs b/src/NadekoBot/Modules/Games/Common/Trivia/TriviaOptions.cs index dff0b8619..00e634684 100644 --- a/src/NadekoBot/Modules/Games/Common/Trivia/TriviaOptions.cs +++ b/src/NadekoBot/Modules/Games/Common/Trivia/TriviaOptions.cs @@ -1,30 +1,29 @@ using CommandLine; using NadekoBot.Common; -namespace NadekoBot.Modules.Games.Common.Trivia +namespace NadekoBot.Modules.Games.Common.Trivia; + +public class TriviaOptions : INadekoCommandOptions { - public class TriviaOptions : INadekoCommandOptions + [Option('p', "pokemon", Required = false, Default = false, HelpText = "Whether it's 'Who's that pokemon?' trivia.")] + public bool IsPokemon { get; set; } = false; + [Option("nohint", Required = false, Default = false, HelpText = "Don't show any hints.")] + public bool NoHint { get; set; } = false; + [Option('w', "win-req", Required = false, Default = 10, HelpText = "Winning requirement. Set 0 for an infinite game. Default 10.")] + public int WinRequirement { get; set; } = 10; + [Option('q', "question-timer", Required = false, Default = 30, HelpText = "How long until the question ends. Default 30.")] + public int QuestionTimer { get; set; } = 30; + [Option('t', "timeout", Required = false, Default = 10, HelpText = "Number of questions of inactivity in order stop. Set 0 for never. Default 10.")] + public int Timeout { get; set; } = 10; + + public void NormalizeOptions() { - [Option('p', "pokemon", Required = false, Default = false, HelpText = "Whether it's 'Who's that pokemon?' trivia.")] - public bool IsPokemon { get; set; } = false; - [Option("nohint", Required = false, Default = false, HelpText = "Don't show any hints.")] - public bool NoHint { get; set; } = false; - [Option('w', "win-req", Required = false, Default = 10, HelpText = "Winning requirement. Set 0 for an infinite game. Default 10.")] - public int WinRequirement { get; set; } = 10; - [Option('q', "question-timer", Required = false, Default = 30, HelpText = "How long until the question ends. Default 30.")] - public int QuestionTimer { get; set; } = 30; - [Option('t', "timeout", Required = false, Default = 10, HelpText = "Number of questions of inactivity in order stop. Set 0 for never. Default 10.")] - public int Timeout { get; set; } = 10; + if (WinRequirement < 0) + WinRequirement = 10; + if (QuestionTimer < 10 || QuestionTimer > 300) + QuestionTimer = 30; + if (Timeout < 0 || Timeout > 20) + Timeout = 10; - public void NormalizeOptions() - { - if (WinRequirement < 0) - WinRequirement = 10; - if (QuestionTimer < 10 || QuestionTimer > 300) - QuestionTimer = 30; - if (Timeout < 0 || Timeout > 20) - Timeout = 10; - - } } -} +} \ No newline at end of file diff --git a/src/NadekoBot/Modules/Games/Common/Trivia/TriviaQuestion.cs b/src/NadekoBot/Modules/Games/Common/Trivia/TriviaQuestion.cs index 296947177..4c9aab9e1 100644 --- a/src/NadekoBot/Modules/Games/Common/Trivia/TriviaQuestion.cs +++ b/src/NadekoBot/Modules/Games/Common/Trivia/TriviaQuestion.cs @@ -1,110 +1,106 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text.RegularExpressions; +using System.Text.RegularExpressions; using NadekoBot.Extensions; // THANKS @ShoMinamimoto for suggestions and coding help -namespace NadekoBot.Modules.Games.Common.Trivia +namespace NadekoBot.Modules.Games.Common.Trivia; + +public class TriviaQuestion { - public class TriviaQuestion + //represents the min size to judge levDistance with + private static readonly HashSet> strictness = new HashSet> { + new Tuple(9, 0), + new Tuple(14, 1), + new Tuple(19, 2), + new Tuple(22, 3), + }; + public const int maxStringLength = 22; + + public string Category { get; set; } + public string Question { get; set; } + public string ImageUrl { get; set; } + public string AnswerImageUrl { get; set; } + public string Answer { get; set; } + private string _cleanAnswer; + public string CleanAnswer => _cleanAnswer ?? (_cleanAnswer = Clean(Answer)); + + public TriviaQuestion(string q, string a, string c, string img = null, string answerImage = null) { - //represents the min size to judge levDistance with - private static readonly HashSet> strictness = new HashSet> { - new Tuple(9, 0), - new Tuple(14, 1), - new Tuple(19, 2), - new Tuple(22, 3), - }; - public const int maxStringLength = 22; - - public string Category { get; set; } - public string Question { get; set; } - public string ImageUrl { get; set; } - public string AnswerImageUrl { get; set; } - public string Answer { get; set; } - private string _cleanAnswer; - public string CleanAnswer => _cleanAnswer ?? (_cleanAnswer = Clean(Answer)); - - public TriviaQuestion(string q, string a, string c, string img = null, string answerImage = null) - { - this.Question = q; - this.Answer = a; - this.Category = c; - this.ImageUrl = img; - this.AnswerImageUrl = answerImage ?? img; - } - - public string GetHint() => Scramble(Answer); - - public bool IsAnswerCorrect(string guess) - { - if (Answer.Equals(guess, StringComparison.InvariantCulture)) - { - return true; - } - var cleanGuess = Clean(guess); - if (CleanAnswer.Equals(cleanGuess, StringComparison.InvariantCulture)) - { - return true; - } - - int levDistanceClean = CleanAnswer.LevenshteinDistance(cleanGuess); - int levDistanceNormal = Answer.LevenshteinDistance(guess); - return JudgeGuess(CleanAnswer.Length, cleanGuess.Length, levDistanceClean) - || JudgeGuess(Answer.Length, guess.Length, levDistanceNormal); - } - - private static bool JudgeGuess(int guessLength, int answerLength, int levDistance) - { - foreach (Tuple level in strictness) - { - if (guessLength <= level.Item1 || answerLength <= level.Item1) - { - if (levDistance <= level.Item2) - return true; - else - return false; - } - } - return false; - } - - private static string Clean(string str) - { - str = " " + str.ToLowerInvariant() + " "; - str = Regex.Replace(str, "\\s+", " "); - str = Regex.Replace(str, "[^\\w\\d\\s]", ""); - //Here's where custom modification can be done - str = Regex.Replace(str, "\\s(a|an|the|of|in|for|to|as|at|be)\\s", " "); - //End custom mod and cleanup whitespace - str = Regex.Replace(str, "^\\s+", ""); - str = Regex.Replace(str, "\\s+$", ""); - //Trim the really long answers - str = str.Length <= maxStringLength ? str : str.Substring(0, maxStringLength); - return str; - } - - private static string Scramble(string word) - { - var letters = word.ToCharArray(); - var count = 0; - for (var i = 0; i < letters.Length; i++) - { - if (letters[i] == ' ') - continue; - - count++; - if (count <= letters.Length / 5) - continue; - - if (count % 3 == 0) - continue; - - if (letters[i] != ' ') - letters[i] = '_'; - } - return string.Join(" ", new string(letters).Replace(" ", " \u2000", StringComparison.InvariantCulture).AsEnumerable()); - } + this.Question = q; + this.Answer = a; + this.Category = c; + this.ImageUrl = img; + this.AnswerImageUrl = answerImage ?? img; } -} + + public string GetHint() => Scramble(Answer); + + public bool IsAnswerCorrect(string guess) + { + if (Answer.Equals(guess, StringComparison.InvariantCulture)) + { + return true; + } + var cleanGuess = Clean(guess); + if (CleanAnswer.Equals(cleanGuess, StringComparison.InvariantCulture)) + { + return true; + } + + int levDistanceClean = CleanAnswer.LevenshteinDistance(cleanGuess); + int levDistanceNormal = Answer.LevenshteinDistance(guess); + return JudgeGuess(CleanAnswer.Length, cleanGuess.Length, levDistanceClean) + || JudgeGuess(Answer.Length, guess.Length, levDistanceNormal); + } + + private static bool JudgeGuess(int guessLength, int answerLength, int levDistance) + { + foreach (Tuple level in strictness) + { + if (guessLength <= level.Item1 || answerLength <= level.Item1) + { + if (levDistance <= level.Item2) + return true; + else + return false; + } + } + return false; + } + + private static string Clean(string str) + { + str = " " + str.ToLowerInvariant() + " "; + str = Regex.Replace(str, "\\s+", " "); + str = Regex.Replace(str, "[^\\w\\d\\s]", ""); + //Here's where custom modification can be done + str = Regex.Replace(str, "\\s(a|an|the|of|in|for|to|as|at|be)\\s", " "); + //End custom mod and cleanup whitespace + str = Regex.Replace(str, "^\\s+", ""); + str = Regex.Replace(str, "\\s+$", ""); + //Trim the really long answers + str = str.Length <= maxStringLength ? str : str.Substring(0, maxStringLength); + return str; + } + + private static string Scramble(string word) + { + var letters = word.ToCharArray(); + var count = 0; + for (var i = 0; i < letters.Length; i++) + { + if (letters[i] == ' ') + continue; + + count++; + if (count <= letters.Length / 5) + continue; + + if (count % 3 == 0) + continue; + + if (letters[i] != ' ') + letters[i] = '_'; + } + return string.Join(" ", new string(letters).Replace(" ", " \u2000", StringComparison.InvariantCulture).AsEnumerable()); + } +} \ No newline at end of file diff --git a/src/NadekoBot/Modules/Games/Common/Trivia/TriviaQuestionPool.cs b/src/NadekoBot/Modules/Games/Common/Trivia/TriviaQuestionPool.cs index aa8fd5bda..b1f99deca 100644 --- a/src/NadekoBot/Modules/Games/Common/Trivia/TriviaQuestionPool.cs +++ b/src/NadekoBot/Modules/Games/Common/Trivia/TriviaQuestionPool.cs @@ -1,44 +1,42 @@ using NadekoBot.Common; using NadekoBot.Services; using NadekoBot.Extensions; -using System.Collections.Generic; -namespace NadekoBot.Modules.Games.Common.Trivia +namespace NadekoBot.Modules.Games.Common.Trivia; + +public class TriviaQuestionPool { - public class TriviaQuestionPool + private readonly IDataCache _cache; + private readonly int maxPokemonId; + + private readonly NadekoRandom _rng = new NadekoRandom(); + + private TriviaQuestion[] Pool => _cache.LocalData.TriviaQuestions; + private IReadOnlyDictionary Map => _cache.LocalData.PokemonMap; + + public TriviaQuestionPool(IDataCache cache) { - private readonly IDataCache _cache; - private readonly int maxPokemonId; - - private readonly NadekoRandom _rng = new NadekoRandom(); - - private TriviaQuestion[] Pool => _cache.LocalData.TriviaQuestions; - private IReadOnlyDictionary Map => _cache.LocalData.PokemonMap; - - public TriviaQuestionPool(IDataCache cache) - { - _cache = cache; - maxPokemonId = 721; //xd - } - - public TriviaQuestion GetRandomQuestion(HashSet exclude, bool isPokemon) - { - if (Pool.Length == 0) - return null; - - if (isPokemon) - { - var num = _rng.Next(1, maxPokemonId + 1); - return new TriviaQuestion("Who's That Pokémon?", - Map[num].ToTitleCase(), - "Pokemon", - $@"https://nadeko.bot/images/pokemon/shadows/{num}.png", - $@"https://nadeko.bot/images/pokemon/real/{num}.png"); - } - TriviaQuestion randomQuestion; - while (exclude.Contains(randomQuestion = Pool[_rng.Next(0, Pool.Length)])) ; - - return randomQuestion; - } + _cache = cache; + maxPokemonId = 721; //xd } -} + + public TriviaQuestion GetRandomQuestion(HashSet exclude, bool isPokemon) + { + if (Pool.Length == 0) + return null; + + if (isPokemon) + { + var num = _rng.Next(1, maxPokemonId + 1); + return new TriviaQuestion("Who's That Pokémon?", + Map[num].ToTitleCase(), + "Pokemon", + $@"https://nadeko.bot/images/pokemon/shadows/{num}.png", + $@"https://nadeko.bot/images/pokemon/real/{num}.png"); + } + TriviaQuestion randomQuestion; + while (exclude.Contains(randomQuestion = Pool[_rng.Next(0, Pool.Length)])) ; + + return randomQuestion; + } +} \ No newline at end of file diff --git a/src/NadekoBot/Modules/Games/Common/TypingArticle.cs b/src/NadekoBot/Modules/Games/Common/TypingArticle.cs index 3e4435130..77e9abc8b 100644 --- a/src/NadekoBot/Modules/Games/Common/TypingArticle.cs +++ b/src/NadekoBot/Modules/Games/Common/TypingArticle.cs @@ -1,9 +1,8 @@ -namespace NadekoBot.Modules.Games.Common +namespace NadekoBot.Modules.Games.Common; + +public class TypingArticle { - public class TypingArticle - { - public string Source { get; set; } - public string Extra { get; set; } - public string Text { get; set; } - } -} + public string Source { get; set; } + public string Extra { get; set; } + public string Text { get; set; } +} \ No newline at end of file diff --git a/src/NadekoBot/Modules/Games/Common/TypingGame.cs b/src/NadekoBot/Modules/Games/Common/TypingGame.cs index 389e66058..03463b15f 100644 --- a/src/NadekoBot/Modules/Games/Common/TypingGame.cs +++ b/src/NadekoBot/Modules/Games/Common/TypingGame.cs @@ -1,7 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.Linq; +using System.Diagnostics; using System.Threading.Tasks; using Discord; using Discord.WebSocket; @@ -10,180 +7,178 @@ using NadekoBot.Extensions; using NadekoBot.Modules.Games.Services; using CommandLine; using NadekoBot.Services; -using Serilog; -namespace NadekoBot.Modules.Games.Common +namespace NadekoBot.Modules.Games.Common; + +public class TypingGame { - public class TypingGame + public class Options : INadekoCommandOptions { - public class Options : INadekoCommandOptions - { - [Option('s', "start-time", Default = 5, Required = false, HelpText = "How long does it take for the race to start. Default 5.")] - public int StartTime { get; set; } = 5; + [Option('s', "start-time", Default = 5, Required = false, HelpText = "How long does it take for the race to start. Default 5.")] + public int StartTime { get; set; } = 5; - public void NormalizeOptions() + public void NormalizeOptions() + { + if (StartTime < 3 || StartTime > 30) + StartTime = 5; + } + } + + public const float WORD_VALUE = 4.5f; + public ITextChannel Channel { get; } + public string CurrentSentence { get; private set; } + public bool IsActive { get; private set; } + private readonly Stopwatch sw; + private readonly List finishedUserIds; + private readonly DiscordSocketClient _client; + private readonly GamesService _games; + private readonly string _prefix; + private readonly Options _options; + private readonly IEmbedBuilderService _eb; + + public TypingGame(GamesService games, DiscordSocketClient client, ITextChannel channel, + string prefix, Options options, IEmbedBuilderService eb) + { + _games = games; + _client = client; + _prefix = prefix; + _options = options; + _eb = eb; + + this.Channel = channel; + IsActive = false; + sw = new Stopwatch(); + finishedUserIds = new List(); + } + + public async Task Stop() + { + if (!IsActive) return false; + _client.MessageReceived -= AnswerReceived; + finishedUserIds.Clear(); + IsActive = false; + sw.Stop(); + sw.Reset(); + try + { + await Channel.SendConfirmAsync(_eb, "Typing contest stopped."); + } + catch (Exception ex) + { + Log.Warning(ex.ToString()); + } + + return true; + } + + public async Task Start() + { + if (IsActive) return; // can't start running game + IsActive = true; + CurrentSentence = GetRandomSentence(); + var i = (int)(CurrentSentence.Length / WORD_VALUE * 1.7f); + try + { + await Channel.SendConfirmAsync(_eb, + $@":clock2: Next contest will last for {i} seconds. Type the bolded text as fast as you can."); + + + var time = _options.StartTime; + + var msg = await Channel.SendMessageAsync($"Starting new typing contest in **{time}**...", options: new RequestOptions() { - if (StartTime < 3 || StartTime > 30) - StartTime = 5; + RetryMode = RetryMode.AlwaysRetry + }).ConfigureAwait(false); + + do + { + await Task.Delay(2000).ConfigureAwait(false); + time -= 2; + try { await msg.ModifyAsync(m => m.Content = $"Starting new typing contest in **{time}**..").ConfigureAwait(false); } catch { } + } while (time > 2); + + await msg.ModifyAsync(m => { + m.Content = CurrentSentence.Replace(" ", " \x200B", StringComparison.InvariantCulture); + }).ConfigureAwait(false); + sw.Start(); + HandleAnswers(); + + while (i > 0) + { + await Task.Delay(1000).ConfigureAwait(false); + i--; + if (!IsActive) + return; } + } - - public const float WORD_VALUE = 4.5f; - public ITextChannel Channel { get; } - public string CurrentSentence { get; private set; } - public bool IsActive { get; private set; } - private readonly Stopwatch sw; - private readonly List finishedUserIds; - private readonly DiscordSocketClient _client; - private readonly GamesService _games; - private readonly string _prefix; - private readonly Options _options; - private readonly IEmbedBuilderService _eb; - - public TypingGame(GamesService games, DiscordSocketClient client, ITextChannel channel, - string prefix, Options options, IEmbedBuilderService eb) + catch { } + finally { - _games = games; - _client = client; - _prefix = prefix; - _options = options; - _eb = eb; - - this.Channel = channel; - IsActive = false; - sw = new Stopwatch(); - finishedUserIds = new List(); + await Stop().ConfigureAwait(false); } + } - public async Task Stop() + public string GetRandomSentence() + { + if (_games.TypingArticles.Any()) + return _games.TypingArticles[new NadekoRandom().Next(0, _games.TypingArticles.Count)].Text; + else + return $"No typing articles found. Use {_prefix}typeadd command to add a new article for typing."; + + } + + private void HandleAnswers() + { + _client.MessageReceived += AnswerReceived; + } + + private Task AnswerReceived(SocketMessage imsg) + { + var _ = Task.Run(async () => { - if (!IsActive) return false; - _client.MessageReceived -= AnswerReceived; - finishedUserIds.Clear(); - IsActive = false; - sw.Stop(); - sw.Reset(); try { - await Channel.SendConfirmAsync(_eb, "Typing contest stopped."); + if (imsg.Author.IsBot) + return; + var msg = imsg as SocketUserMessage; + if (msg is null) + return; + + if (this.Channel is null || this.Channel.Id != msg.Channel.Id) return; + + var guess = msg.Content; + + var distance = CurrentSentence.LevenshteinDistance(guess); + var decision = Judge(distance, guess.Length); + if (decision && !finishedUserIds.Contains(msg.Author.Id)) + { + var elapsed = sw.Elapsed; + var wpm = CurrentSentence.Length / WORD_VALUE / elapsed.TotalSeconds * 60; + finishedUserIds.Add(msg.Author.Id); + await this.Channel.EmbedAsync(_eb.Create().WithOkColor() + .WithTitle($"{msg.Author} finished the race!") + .AddField("Place", $"#{finishedUserIds.Count}", true) + .AddField("WPM", $"{wpm:F1} *[{elapsed.TotalSeconds:F2}sec]*", true) + .AddField("Errors", distance.ToString(), true)); + + if (finishedUserIds.Count % 4 == 0) + { + await this.Channel.SendConfirmAsync(_eb, + $":exclamation: A lot of people finished, here is the text for those still typing:" + + $"\n\n**{Format.Sanitize(CurrentSentence.Replace(" ", " \x200B", StringComparison.InvariantCulture)).SanitizeMentions(true)}**") + .ConfigureAwait(false); + } + } } catch (Exception ex) { Log.Warning(ex.ToString()); } - - return true; - } - - public async Task Start() - { - if (IsActive) return; // can't start running game - IsActive = true; - CurrentSentence = GetRandomSentence(); - var i = (int)(CurrentSentence.Length / WORD_VALUE * 1.7f); - try - { - await Channel.SendConfirmAsync(_eb, - $@":clock2: Next contest will last for {i} seconds. Type the bolded text as fast as you can."); - - - var time = _options.StartTime; - - var msg = await Channel.SendMessageAsync($"Starting new typing contest in **{time}**...", options: new RequestOptions() - { - RetryMode = RetryMode.AlwaysRetry - }).ConfigureAwait(false); - - do - { - await Task.Delay(2000).ConfigureAwait(false); - time -= 2; - try { await msg.ModifyAsync(m => m.Content = $"Starting new typing contest in **{time}**..").ConfigureAwait(false); } catch { } - } while (time > 2); - - await msg.ModifyAsync(m => { - m.Content = CurrentSentence.Replace(" ", " \x200B", StringComparison.InvariantCulture); - }).ConfigureAwait(false); - sw.Start(); - HandleAnswers(); - - while (i > 0) - { - await Task.Delay(1000).ConfigureAwait(false); - i--; - if (!IsActive) - return; - } - - } - catch { } - finally - { - await Stop().ConfigureAwait(false); - } - } - - public string GetRandomSentence() - { - if (_games.TypingArticles.Any()) - return _games.TypingArticles[new NadekoRandom().Next(0, _games.TypingArticles.Count)].Text; - else - return $"No typing articles found. Use {_prefix}typeadd command to add a new article for typing."; - - } - - private void HandleAnswers() - { - _client.MessageReceived += AnswerReceived; - } - - private Task AnswerReceived(SocketMessage imsg) - { - var _ = Task.Run(async () => - { - try - { - if (imsg.Author.IsBot) - return; - var msg = imsg as SocketUserMessage; - if (msg is null) - return; - - if (this.Channel is null || this.Channel.Id != msg.Channel.Id) return; - - var guess = msg.Content; - - var distance = CurrentSentence.LevenshteinDistance(guess); - var decision = Judge(distance, guess.Length); - if (decision && !finishedUserIds.Contains(msg.Author.Id)) - { - var elapsed = sw.Elapsed; - var wpm = CurrentSentence.Length / WORD_VALUE / elapsed.TotalSeconds * 60; - finishedUserIds.Add(msg.Author.Id); - await this.Channel.EmbedAsync(_eb.Create().WithOkColor() - .WithTitle($"{msg.Author} finished the race!") - .AddField("Place", $"#{finishedUserIds.Count}", true) - .AddField("WPM", $"{wpm:F1} *[{elapsed.TotalSeconds:F2}sec]*", true) - .AddField("Errors", distance.ToString(), true)); - - if (finishedUserIds.Count % 4 == 0) - { - await this.Channel.SendConfirmAsync(_eb, - $":exclamation: A lot of people finished, here is the text for those still typing:" + - $"\n\n**{Format.Sanitize(CurrentSentence.Replace(" ", " \x200B", StringComparison.InvariantCulture)).SanitizeMentions(true)}**") - .ConfigureAwait(false); - } - } - } - catch (Exception ex) - { - Log.Warning(ex.ToString()); - } - }); - return Task.CompletedTask; - } - - private static bool Judge(int errors, int textLength) => errors <= textLength / 25; - + }); + return Task.CompletedTask; } + + private static bool Judge(int errors, int textLength) => errors <= textLength / 25; + } \ No newline at end of file diff --git a/src/NadekoBot/Modules/Games/Games.cs b/src/NadekoBot/Modules/Games/Games.cs index b4949b7e3..bad28bf12 100644 --- a/src/NadekoBot/Modules/Games/Games.cs +++ b/src/NadekoBot/Modules/Games/Games.cs @@ -6,159 +6,157 @@ using NadekoBot.Services; using NadekoBot.Extensions; using NadekoBot.Modules.Games.Common; using NadekoBot.Modules.Games.Services; -using System; using System.IO; using System.Net.Http; using System.Threading.Tasks; -namespace NadekoBot.Modules.Games +namespace NadekoBot.Modules.Games; + +/* more games +- Shiritori +- Simple RPG adventure +*/ +public partial class Games : NadekoModule { - /* more games - - Shiritori - - Simple RPG adventure - */ - public partial class Games : NadekoModule + private readonly IImageCache _images; + private readonly IHttpClientFactory _httpFactory; + private readonly Random _rng = new Random(); + + public Games(IDataCache data, IHttpClientFactory factory) { - private readonly IImageCache _images; - private readonly IHttpClientFactory _httpFactory; - private readonly Random _rng = new Random(); + _images = data.LocalImages; + _httpFactory = factory; + } - public Games(IDataCache data, IHttpClientFactory factory) + [NadekoCommand, Aliases] + public async Task Choose([Leftover] string list = null) + { + if (string.IsNullOrWhiteSpace(list)) + return; + var listArr = list.Split(';'); + if (listArr.Length < 2) + return; + var rng = new NadekoRandom(); + await SendConfirmAsync("🤔", listArr[rng.Next(0, listArr.Length)]).ConfigureAwait(false); + } + + [NadekoCommand, Aliases] + public async Task EightBall([Leftover] string question = null) + { + if (string.IsNullOrWhiteSpace(question)) + return; + + var res = _service.GetEightballResponse(ctx.User.Id, question); + await ctx.Channel.EmbedAsync(_eb.Create().WithOkColor() + .WithDescription(ctx.User.ToString()) + .AddField("❓ " + GetText(strs.question), question, false) + .AddField("🎱 " + GetText(strs._8ball), res, false)); + } + + [NadekoCommand, Aliases] + [RequireContext(ContextType.Guild)] + public async Task RateGirl([Leftover] IGuildUser usr) + { + var gr = _service.GirlRatings.GetOrAdd(usr.Id, GetGirl); + var originalStream = await gr.Stream; + + if (originalStream is null) { - _images = data.LocalImages; - _httpFactory = factory; + await ReplyErrorLocalizedAsync(strs.something_went_wrong).ConfigureAwait(false); + return; } - [NadekoCommand, Aliases] - public async Task Choose([Leftover] string list = null) + using (var imgStream = new MemoryStream()) { - if (string.IsNullOrWhiteSpace(list)) - return; - var listArr = list.Split(';'); - if (listArr.Length < 2) - return; - var rng = new NadekoRandom(); - await SendConfirmAsync("🤔", listArr[rng.Next(0, listArr.Length)]).ConfigureAwait(false); + lock (gr) + { + originalStream.Position = 0; + originalStream.CopyTo(imgStream); + } + imgStream.Position = 0; + await ctx.Channel.SendFileAsync(stream: imgStream, + filename: $"girl_{usr}.png", + text: Format.Bold($"{ctx.User.Mention} Girl Rating For {usr}"), + embed: _eb.Create() + .WithOkColor() + .AddField("Hot", gr.Hot.ToString("F2"), true) + .AddField("Crazy", gr.Crazy.ToString("F2"), true) + .AddField("Advice", gr.Advice, false) + .Build()).ConfigureAwait(false); + } + } + + private double NextDouble(double x, double y) + { + return _rng.NextDouble() * (y - x) + x; + } + + private GirlRating GetGirl(ulong uid) + { + var rng = new NadekoRandom(); + + var roll = rng.Next(1, 1001); + + var ratings = _service.Ratings.GetAwaiter().GetResult(); + + double hot; + double crazy; + string advice; + if (roll < 500) + { + hot = NextDouble(0, 5); + crazy = NextDouble(4, 10); + advice = ratings.Nog; + } + else if (roll < 750) + { + hot = NextDouble(5, 8); + crazy = NextDouble(4, .6 * hot + 4); + advice = ratings.Fun; + } + else if (roll < 900) + { + hot = NextDouble(5, 10); + crazy = NextDouble(.61 * hot + 4, 10); + advice = ratings.Dan; + } + else if (roll < 951) + { + hot = NextDouble(8, 10); + crazy = NextDouble(7, .6 * hot + 4); + advice = ratings.Dat; + } + else if (roll < 990) + { + hot = NextDouble(8, 10); + crazy = NextDouble(5, 7); + advice = ratings.Wif; + } + else if (roll < 999) + { + hot = NextDouble(8, 10); + crazy = NextDouble(2, 3.99d); + advice = ratings.Tra; + } + else + { + hot = NextDouble(8, 10); + crazy = NextDouble(4, 5); + advice = ratings.Uni; } - [NadekoCommand, Aliases] - public async Task EightBall([Leftover] string question = null) - { - if (string.IsNullOrWhiteSpace(question)) - return; + return new GirlRating(_images, _httpFactory, crazy, hot, roll, advice); + } - var res = _service.GetEightballResponse(ctx.User.Id, question); - await ctx.Channel.EmbedAsync(_eb.Create().WithOkColor() - .WithDescription(ctx.User.ToString()) - .AddField("❓ " + GetText(strs.question), question, false) - .AddField("🎱 " + GetText(strs._8ball), res, false)); - } - - [NadekoCommand, Aliases] - [RequireContext(ContextType.Guild)] - public async Task RateGirl([Leftover] IGuildUser usr) - { - var gr = _service.GirlRatings.GetOrAdd(usr.Id, GetGirl); - var originalStream = await gr.Stream; - - if (originalStream is null) - { - await ReplyErrorLocalizedAsync(strs.something_went_wrong).ConfigureAwait(false); - return; - } - - using (var imgStream = new MemoryStream()) - { - lock (gr) - { - originalStream.Position = 0; - originalStream.CopyTo(imgStream); - } - imgStream.Position = 0; - await ctx.Channel.SendFileAsync(stream: imgStream, - filename: $"girl_{usr}.png", - text: Format.Bold($"{ctx.User.Mention} Girl Rating For {usr}"), - embed: _eb.Create() - .WithOkColor() - .AddField("Hot", gr.Hot.ToString("F2"), true) - .AddField("Crazy", gr.Crazy.ToString("F2"), true) - .AddField("Advice", gr.Advice, false) - .Build()).ConfigureAwait(false); - } - } - - private double NextDouble(double x, double y) - { - return _rng.NextDouble() * (y - x) + x; - } - - private GirlRating GetGirl(ulong uid) - { - var rng = new NadekoRandom(); - - var roll = rng.Next(1, 1001); - - var ratings = _service.Ratings.GetAwaiter().GetResult(); - - double hot; - double crazy; - string advice; - if (roll < 500) - { - hot = NextDouble(0, 5); - crazy = NextDouble(4, 10); - advice = ratings.Nog; - } - else if (roll < 750) - { - hot = NextDouble(5, 8); - crazy = NextDouble(4, .6 * hot + 4); - advice = ratings.Fun; - } - else if (roll < 900) - { - hot = NextDouble(5, 10); - crazy = NextDouble(.61 * hot + 4, 10); - advice = ratings.Dan; - } - else if (roll < 951) - { - hot = NextDouble(8, 10); - crazy = NextDouble(7, .6 * hot + 4); - advice = ratings.Dat; - } - else if (roll < 990) - { - hot = NextDouble(8, 10); - crazy = NextDouble(5, 7); - advice = ratings.Wif; - } - else if (roll < 999) - { - hot = NextDouble(8, 10); - crazy = NextDouble(2, 3.99d); - advice = ratings.Tra; - } - else - { - hot = NextDouble(8, 10); - crazy = NextDouble(4, 5); - advice = ratings.Uni; - } - - return new GirlRating(_images, _httpFactory, crazy, hot, roll, advice); - } - - [NadekoCommand, Aliases] - public async Task Linux(string guhnoo, string loonix) - { - await SendConfirmAsync( -$@"I'd just like to interject for moment. What you're refering to as {loonix}, is in fact, {guhnoo}/{loonix}, or as I've recently taken to calling it, {guhnoo} plus {loonix}. {loonix} is not an operating system unto itself, but rather another free component of a fully functioning {guhnoo} system made useful by the {guhnoo} corelibs, shell utilities and vital system components comprising a full OS as defined by POSIX. + [NadekoCommand, Aliases] + public async Task Linux(string guhnoo, string loonix) + { + await SendConfirmAsync( + $@"I'd just like to interject for moment. What you're refering to as {loonix}, is in fact, {guhnoo}/{loonix}, or as I've recently taken to calling it, {guhnoo} plus {loonix}. {loonix} is not an operating system unto itself, but rather another free component of a fully functioning {guhnoo} system made useful by the {guhnoo} corelibs, shell utilities and vital system components comprising a full OS as defined by POSIX. Many computer users run a modified version of the {guhnoo} system every day, without realizing it. Through a peculiar turn of events, the version of {guhnoo} which is widely used today is often called {loonix}, and many of its users are not aware that it is basically the {guhnoo} system, developed by the {guhnoo} Project. There really is a {loonix}, and these people are using it, but it is just a part of the system they use. {loonix} is the kernel: the program in the system that allocates the machine's resources to the other programs that you run. The kernel is an essential part of an operating system, but useless by itself; it can only function in the context of a complete operating system. {loonix} is normally used in combination with the {guhnoo} operating system: the whole system is basically {guhnoo} with {loonix} added, or {guhnoo}/{loonix}. All the so-called {loonix} distributions are really distributions of {guhnoo}/{loonix}." - ).ConfigureAwait(false); - } + ).ConfigureAwait(false); } -} +} \ No newline at end of file diff --git a/src/NadekoBot/Modules/Games/HangmanCommands.cs b/src/NadekoBot/Modules/Games/HangmanCommands.cs index a6224d26f..95f61d44a 100644 --- a/src/NadekoBot/Modules/Games/HangmanCommands.cs +++ b/src/NadekoBot/Modules/Games/HangmanCommands.cs @@ -7,82 +7,81 @@ using NadekoBot.Extensions; using NadekoBot.Modules.Games.Hangman; using NadekoBot.Services; -namespace NadekoBot.Modules.Games -{ - public partial class Games - { - [Group] - public class HangmanCommands : NadekoSubmodule - { - [NadekoCommand, Aliases] - [RequireContext(ContextType.Guild)] - public async Task Hangmanlist() - { - await SendConfirmAsync( - GetText(strs.hangman_types(Prefix)), - _service.GetHangmanTypes().JoinWith('\n')); - } +namespace NadekoBot.Modules.Games; - private static string Draw(HangmanGame.State state) - { - return $@". ┌─────┐ +public partial class Games +{ + [Group] + public class HangmanCommands : NadekoSubmodule + { + [NadekoCommand, Aliases] + [RequireContext(ContextType.Guild)] + public async Task Hangmanlist() + { + await SendConfirmAsync( + GetText(strs.hangman_types(Prefix)), + _service.GetHangmanTypes().JoinWith('\n')); + } + + private static string Draw(HangmanGame.State state) + { + return $@". ┌─────┐ .┃...............┋ .┃...............┋ .┃{(state.Errors > 0 ? ".............😲" : "")} .┃{(state.Errors > 1 ? "............./" : "")} {(state.Errors > 2 ? "|" : "")} {(state.Errors > 3 ? "\\" : "")} .┃{(state.Errors > 4 ? "............../" : "")} {(state.Errors > 5 ? "\\" : "")} /-\"; - } + } - public static IEmbedBuilder GetEmbed(IEmbedBuilderService eb, HangmanGame.State state) - { - if (state.Phase == HangmanGame.Phase.Running) - return eb.Create() - .WithOkColor() - .AddField("Hangman", Draw(state)) - .AddField("Guess", Format.Code(state.Word)) - .WithFooter(state.missedLetters.JoinWith(' ')); + public static IEmbedBuilder GetEmbed(IEmbedBuilderService eb, HangmanGame.State state) + { + if (state.Phase == HangmanGame.Phase.Running) + return eb.Create() + .WithOkColor() + .AddField("Hangman", Draw(state)) + .AddField("Guess", Format.Code(state.Word)) + .WithFooter(state.missedLetters.JoinWith(' ')); - if (state.Phase == HangmanGame.Phase.Ended && state.Failed) - return eb.Create() - .WithErrorColor() - .AddField("Hangman", Draw(state)) - .AddField("Guess", Format.Code(state.Word)) - .WithFooter(state.missedLetters.JoinWith(' ')); - else - { - return eb.Create() - .WithOkColor() - .AddField("Hangman", Draw(state)) - .AddField("Guess", Format.Code(state.Word)) - .WithFooter(state.missedLetters.JoinWith(' ')); - } + if (state.Phase == HangmanGame.Phase.Ended && state.Failed) + return eb.Create() + .WithErrorColor() + .AddField("Hangman", Draw(state)) + .AddField("Guess", Format.Code(state.Word)) + .WithFooter(state.missedLetters.JoinWith(' ')); + else + { + return eb.Create() + .WithOkColor() + .AddField("Hangman", Draw(state)) + .AddField("Guess", Format.Code(state.Word)) + .WithFooter(state.missedLetters.JoinWith(' ')); + } + } + + [NadekoCommand, Aliases] + [RequireContext(ContextType.Guild)] + public async Task Hangman([Leftover] string? type = null) + { + if (!_service.StartHangman(ctx.Channel.Id, type, out var hangman)) + { + await ReplyErrorLocalizedAsync(strs.hangman_running); + return; } - [NadekoCommand, Aliases] - [RequireContext(ContextType.Guild)] - public async Task Hangman([Leftover] string? type = null) - { - if (!_service.StartHangman(ctx.Channel.Id, type, out var hangman)) - { - await ReplyErrorLocalizedAsync(strs.hangman_running); - return; - } + var eb = GetEmbed(_eb, hangman); + eb.WithDescription(GetText(strs.hangman_game_started)); + await ctx.Channel.EmbedAsync(eb); + } - var eb = GetEmbed(_eb, hangman); - eb.WithDescription(GetText(strs.hangman_game_started)); - await ctx.Channel.EmbedAsync(eb); - } - - [NadekoCommand, Aliases] - [RequireContext(ContextType.Guild)] - public async Task HangmanStop() + [NadekoCommand, Aliases] + [RequireContext(ContextType.Guild)] + public async Task HangmanStop() + { + if (await _service.StopHangman(ctx.Channel.Id)) { - if (await _service.StopHangman(ctx.Channel.Id)) - { - await ReplyConfirmLocalizedAsync(strs.hangman_stopped).ConfigureAwait(false); - } + await ReplyConfirmLocalizedAsync(strs.hangman_stopped).ConfigureAwait(false); } } } -} +} \ No newline at end of file diff --git a/src/NadekoBot/Modules/Games/NunchiCommands.cs b/src/NadekoBot/Modules/Games/NunchiCommands.cs index e2dde3652..e67b9b194 100644 --- a/src/NadekoBot/Modules/Games/NunchiCommands.cs +++ b/src/NadekoBot/Modules/Games/NunchiCommands.cs @@ -4,123 +4,121 @@ using Discord.WebSocket; using NadekoBot.Common.Attributes; using NadekoBot.Modules.Games.Common.Nunchi; using NadekoBot.Modules.Games.Services; -using System.Linq; using System.Threading.Tasks; -namespace NadekoBot.Modules.Games +namespace NadekoBot.Modules.Games; + +public partial class Games { - public partial class Games + [Group] + public class NunchiCommands : NadekoSubmodule { - [Group] - public class NunchiCommands : NadekoSubmodule + private readonly DiscordSocketClient _client; + + public NunchiCommands(DiscordSocketClient client) { - private readonly DiscordSocketClient _client; + _client = client; + } - public NunchiCommands(DiscordSocketClient client) + [NadekoCommand, Aliases] + [RequireContext(ContextType.Guild)] + public async Task Nunchi() + { + var newNunchi = new NunchiGame(ctx.User.Id, ctx.User.ToString()); + NunchiGame nunchi; + + //if a game was already active + if ((nunchi = _service.NunchiGames.GetOrAdd(ctx.Guild.Id, newNunchi)) != newNunchi) { - _client = client; - } - - [NadekoCommand, Aliases] - [RequireContext(ContextType.Guild)] - public async Task Nunchi() - { - var newNunchi = new NunchiGame(ctx.User.Id, ctx.User.ToString()); - NunchiGame nunchi; - - //if a game was already active - if ((nunchi = _service.NunchiGames.GetOrAdd(ctx.Guild.Id, newNunchi)) != newNunchi) + // join it + if (!await nunchi.Join(ctx.User.Id, ctx.User.ToString()).ConfigureAwait(false)) { - // join it - if (!await nunchi.Join(ctx.User.Id, ctx.User.ToString()).ConfigureAwait(false)) - { - // if you failed joining, that means game is running or just ended - // await ReplyErrorLocalized("nunchi_already_started").ConfigureAwait(false); - return; - } - - await ReplyErrorLocalizedAsync(strs.nunchi_joined(nunchi.ParticipantCount)); + // if you failed joining, that means game is running or just ended + // await ReplyErrorLocalized("nunchi_already_started").ConfigureAwait(false); return; } + await ReplyErrorLocalizedAsync(strs.nunchi_joined(nunchi.ParticipantCount)); + return; + } - try { await ConfirmLocalizedAsync(strs.nunchi_created); } catch { } - nunchi.OnGameEnded += Nunchi_OnGameEnded; - //nunchi.OnGameStarted += Nunchi_OnGameStarted; - nunchi.OnRoundEnded += Nunchi_OnRoundEnded; - nunchi.OnUserGuessed += Nunchi_OnUserGuessed; - nunchi.OnRoundStarted += Nunchi_OnRoundStarted; - _client.MessageReceived += _client_MessageReceived; + try { await ConfirmLocalizedAsync(strs.nunchi_created); } catch { } + + nunchi.OnGameEnded += Nunchi_OnGameEnded; + //nunchi.OnGameStarted += Nunchi_OnGameStarted; + nunchi.OnRoundEnded += Nunchi_OnRoundEnded; + nunchi.OnUserGuessed += Nunchi_OnUserGuessed; + nunchi.OnRoundStarted += Nunchi_OnRoundStarted; + _client.MessageReceived += _client_MessageReceived; - var success = await nunchi.Initialize().ConfigureAwait(false); - if (!success) - { - if (_service.NunchiGames.TryRemove(ctx.Guild.Id, out var game)) - game.Dispose(); - await ConfirmLocalizedAsync(strs.nunchi_failed_to_start).ConfigureAwait(false); - } + var success = await nunchi.Initialize().ConfigureAwait(false); + if (!success) + { + if (_service.NunchiGames.TryRemove(ctx.Guild.Id, out var game)) + game.Dispose(); + await ConfirmLocalizedAsync(strs.nunchi_failed_to_start).ConfigureAwait(false); + } - Task _client_MessageReceived(SocketMessage arg) + Task _client_MessageReceived(SocketMessage arg) + { + var _ = Task.Run(async () => { - var _ = Task.Run(async () => + if (arg.Channel.Id != ctx.Channel.Id) + return; + + if (!int.TryParse(arg.Content, out var number)) + return; + try { - if (arg.Channel.Id != ctx.Channel.Id) - return; - - if (!int.TryParse(arg.Content, out var number)) - return; - try - { - await nunchi.Input(arg.Author.Id, arg.Author.ToString(), number).ConfigureAwait(false); - } - catch - { - } - }); - return Task.CompletedTask; - } - - Task Nunchi_OnGameEnded(NunchiGame arg1, string arg2) - { - if (_service.NunchiGames.TryRemove(ctx.Guild.Id, out var game)) - { - _client.MessageReceived -= _client_MessageReceived; - game.Dispose(); + await nunchi.Input(arg.Author.Id, arg.Author.ToString(), number).ConfigureAwait(false); } + catch + { + } + }); + return Task.CompletedTask; + } - if (arg2 is null) - return ConfirmLocalizedAsync(strs.nunchi_ended_no_winner); - else - return ConfirmLocalizedAsync(strs.nunchi_ended(Format.Bold(arg2))); + Task Nunchi_OnGameEnded(NunchiGame arg1, string arg2) + { + if (_service.NunchiGames.TryRemove(ctx.Guild.Id, out var game)) + { + _client.MessageReceived -= _client_MessageReceived; + game.Dispose(); } - } - private Task Nunchi_OnRoundStarted(NunchiGame arg, int cur) - { - return ConfirmLocalizedAsync(strs.nunchi_round_started( - Format.Bold(arg.ParticipantCount.ToString()), - Format.Bold(cur.ToString()))); - } - - private Task Nunchi_OnUserGuessed(NunchiGame arg) - { - return ConfirmLocalizedAsync(strs.nunchi_next_number(Format.Bold(arg.CurrentNumber.ToString()))); - } - - private Task Nunchi_OnRoundEnded(NunchiGame arg1, (ulong Id, string Name)? arg2) - { - if(arg2.HasValue) - return ConfirmLocalizedAsync(strs.nunchi_round_ended(Format.Bold(arg2.Value.Name))); + if (arg2 is null) + return ConfirmLocalizedAsync(strs.nunchi_ended_no_winner); else - return ConfirmLocalizedAsync(strs.nunchi_round_ended_boot( - Format.Bold("\n" + string.Join("\n, ", arg1.Participants.Select(x => x.Name))))); // this won't work if there are too many users + return ConfirmLocalizedAsync(strs.nunchi_ended(Format.Bold(arg2))); } + } - private Task Nunchi_OnGameStarted(NunchiGame arg) - { - return ConfirmLocalizedAsync(strs.nunchi_started(Format.Bold(arg.ParticipantCount.ToString()))); - } + private Task Nunchi_OnRoundStarted(NunchiGame arg, int cur) + { + return ConfirmLocalizedAsync(strs.nunchi_round_started( + Format.Bold(arg.ParticipantCount.ToString()), + Format.Bold(cur.ToString()))); + } + + private Task Nunchi_OnUserGuessed(NunchiGame arg) + { + return ConfirmLocalizedAsync(strs.nunchi_next_number(Format.Bold(arg.CurrentNumber.ToString()))); + } + + private Task Nunchi_OnRoundEnded(NunchiGame arg1, (ulong Id, string Name)? arg2) + { + if(arg2.HasValue) + return ConfirmLocalizedAsync(strs.nunchi_round_ended(Format.Bold(arg2.Value.Name))); + else + return ConfirmLocalizedAsync(strs.nunchi_round_ended_boot( + Format.Bold("\n" + string.Join("\n, ", arg1.Participants.Select(x => x.Name))))); // this won't work if there are too many users + } + + private Task Nunchi_OnGameStarted(NunchiGame arg) + { + return ConfirmLocalizedAsync(strs.nunchi_started(Format.Bold(arg.ParticipantCount.ToString()))); } } } \ No newline at end of file diff --git a/src/NadekoBot/Modules/Games/PollCommands.cs b/src/NadekoBot/Modules/Games/PollCommands.cs index 5d589b3b8..0f0a708f2 100644 --- a/src/NadekoBot/Modules/Games/PollCommands.cs +++ b/src/NadekoBot/Modules/Games/PollCommands.cs @@ -7,118 +7,116 @@ using NadekoBot.Common.Attributes; using NadekoBot.Modules.Games.Services; using NadekoBot.Services.Database.Models; using System.Text; -using System.Linq; -namespace NadekoBot.Modules.Games +namespace NadekoBot.Modules.Games; + +public partial class Games { - public partial class Games + [Group] + public class PollCommands : NadekoSubmodule { - [Group] - public class PollCommands : NadekoSubmodule + private readonly DiscordSocketClient _client; + + public PollCommands(DiscordSocketClient client) { - private readonly DiscordSocketClient _client; + _client = client; + } - public PollCommands(DiscordSocketClient client) + [NadekoCommand, Aliases] + [UserPerm(GuildPerm.ManageMessages)] + [RequireContext(ContextType.Guild)] + public async Task Poll([Leftover] string arg) + { + if (string.IsNullOrWhiteSpace(arg)) + return; + + var poll = _service.CreatePoll(ctx.Guild.Id, + ctx.Channel.Id, arg); + if(poll is null) { - _client = client; + await ReplyErrorLocalizedAsync(strs.poll_invalid_input).ConfigureAwait(false); + return; } - - [NadekoCommand, Aliases] - [UserPerm(GuildPerm.ManageMessages)] - [RequireContext(ContextType.Guild)] - public async Task Poll([Leftover] string arg) + if (_service.StartPoll(poll)) { - if (string.IsNullOrWhiteSpace(arg)) - return; - - var poll = _service.CreatePoll(ctx.Guild.Id, - ctx.Channel.Id, arg); - if(poll is null) - { - await ReplyErrorLocalizedAsync(strs.poll_invalid_input).ConfigureAwait(false); - return; - } - if (_service.StartPoll(poll)) - { - await ctx.Channel - .EmbedAsync(_eb.Create() - .WithOkColor() - .WithTitle(GetText(strs.poll_created(ctx.User.ToString()))) - .WithDescription( - Format.Bold(poll.Question) + "\n\n" + + await ctx.Channel + .EmbedAsync(_eb.Create() + .WithOkColor() + .WithTitle(GetText(strs.poll_created(ctx.User.ToString()))) + .WithDescription( + Format.Bold(poll.Question) + "\n\n" + string.Join("\n", poll.Answers .Select(x => $"`{x.Index + 1}.` {Format.Bold(x.Text)}")))) - .ConfigureAwait(false); - } - else - { - await ReplyErrorLocalizedAsync(strs.poll_already_running).ConfigureAwait(false); - } - } - - [NadekoCommand, Aliases] - [UserPerm(GuildPerm.ManageMessages)] - [RequireContext(ContextType.Guild)] - public async Task PollStats() - { - if (!_service.ActivePolls.TryGetValue(ctx.Guild.Id, out var pr)) - return; - - await ctx.Channel.EmbedAsync(GetStats(pr.Poll, GetText(strs.current_poll_results))).ConfigureAwait(false); - } - - [NadekoCommand, Aliases] - [UserPerm(GuildPerm.ManageMessages)] - [RequireContext(ContextType.Guild)] - public async Task Pollend() - { - var channel = (ITextChannel)ctx.Channel; - - Poll p; - if ((p = _service.StopPoll(ctx.Guild.Id)) is null) - return; - - var embed = GetStats(p, GetText(strs.poll_closed)); - await ctx.Channel.EmbedAsync(embed) .ConfigureAwait(false); } - - public IEmbedBuilder GetStats(Poll poll, string title) + else { - var results = poll.Votes.GroupBy(kvp => kvp.VoteIndex) - .ToDictionary(x => x.Key, x => x.Sum(kvp => 1)); - - var totalVotesCast = results.Sum(x => x.Value); - - var eb = _eb.Create().WithTitle(title); - - var sb = new StringBuilder() - .AppendLine(Format.Bold(poll.Question)) - .AppendLine(); - - var stats = poll.Answers - .Select(x => - { - results.TryGetValue(x.Index, out var votes); - - return (x.Index, votes, x.Text); - }) - .OrderByDescending(x => x.votes) - .ToArray(); - - for (int i = 0; i < stats.Length; i++) - { - var (Index, votes, Text) = stats[i]; - sb.AppendLine(GetText(strs.poll_result( - Index + 1, - Format.Bold(Text), - Format.Bold(votes.ToString())))); - } - - return eb.WithDescription(sb.ToString()) - .WithFooter(GetText(strs.x_votes_cast(totalVotesCast))) - .WithOkColor(); + await ReplyErrorLocalizedAsync(strs.poll_already_running).ConfigureAwait(false); } } + + [NadekoCommand, Aliases] + [UserPerm(GuildPerm.ManageMessages)] + [RequireContext(ContextType.Guild)] + public async Task PollStats() + { + if (!_service.ActivePolls.TryGetValue(ctx.Guild.Id, out var pr)) + return; + + await ctx.Channel.EmbedAsync(GetStats(pr.Poll, GetText(strs.current_poll_results))).ConfigureAwait(false); + } + + [NadekoCommand, Aliases] + [UserPerm(GuildPerm.ManageMessages)] + [RequireContext(ContextType.Guild)] + public async Task Pollend() + { + var channel = (ITextChannel)ctx.Channel; + + Poll p; + if ((p = _service.StopPoll(ctx.Guild.Id)) is null) + return; + + var embed = GetStats(p, GetText(strs.poll_closed)); + await ctx.Channel.EmbedAsync(embed) + .ConfigureAwait(false); + } + + public IEmbedBuilder GetStats(Poll poll, string title) + { + var results = poll.Votes.GroupBy(kvp => kvp.VoteIndex) + .ToDictionary(x => x.Key, x => x.Sum(kvp => 1)); + + var totalVotesCast = results.Sum(x => x.Value); + + var eb = _eb.Create().WithTitle(title); + + var sb = new StringBuilder() + .AppendLine(Format.Bold(poll.Question)) + .AppendLine(); + + var stats = poll.Answers + .Select(x => + { + results.TryGetValue(x.Index, out var votes); + + return (x.Index, votes, x.Text); + }) + .OrderByDescending(x => x.votes) + .ToArray(); + + for (int i = 0; i < stats.Length; i++) + { + var (Index, votes, Text) = stats[i]; + sb.AppendLine(GetText(strs.poll_result( + Index + 1, + Format.Bold(Text), + Format.Bold(votes.ToString())))); + } + + return eb.WithDescription(sb.ToString()) + .WithFooter(GetText(strs.x_votes_cast(totalVotesCast))) + .WithOkColor(); + } } } \ No newline at end of file diff --git a/src/NadekoBot/Modules/Games/Services/ChatterbotService.cs b/src/NadekoBot/Modules/Games/Services/ChatterbotService.cs index ab3409fd2..44b96415a 100644 --- a/src/NadekoBot/Modules/Games/Services/ChatterbotService.cs +++ b/src/NadekoBot/Modules/Games/Services/ChatterbotService.cs @@ -1,6 +1,4 @@ -using System; -using System.Collections.Concurrent; -using System.Linq; +using System.Collections.Concurrent; using System.Threading.Tasks; using Discord; using Discord.WebSocket; @@ -11,142 +9,140 @@ using NadekoBot.Modules.Permissions.Services; using NadekoBot.Services; using NadekoBot.Modules.Games.Common.ChatterBot; using System.Net.Http; -using Serilog; -namespace NadekoBot.Modules.Games.Services +namespace NadekoBot.Modules.Games.Services; + +public class ChatterBotService : IEarlyBehavior { - public class ChatterBotService : IEarlyBehavior + private readonly DiscordSocketClient _client; + private readonly PermissionService _perms; + private readonly CommandHandler _cmd; + private readonly IBotStrings _strings; + private readonly IBotCredentials _creds; + private readonly IEmbedBuilderService _eb; + private readonly IHttpClientFactory _httpFactory; + + public ConcurrentDictionary> ChatterBotGuilds { get; } + + public int Priority => 1; + + public ChatterBotService(DiscordSocketClient client, PermissionService perms, + Bot bot, CommandHandler cmd, IBotStrings strings, IHttpClientFactory factory, + IBotCredentials creds, IEmbedBuilderService eb) { - private readonly DiscordSocketClient _client; - private readonly PermissionService _perms; - private readonly CommandHandler _cmd; - private readonly IBotStrings _strings; - private readonly IBotCredentials _creds; - private readonly IEmbedBuilderService _eb; - private readonly IHttpClientFactory _httpFactory; + _client = client; + _perms = perms; + _cmd = cmd; + _strings = strings; + _creds = creds; + _eb = eb; + _httpFactory = factory; - public ConcurrentDictionary> ChatterBotGuilds { get; } + ChatterBotGuilds = new ConcurrentDictionary>( + bot.AllGuildConfigs + .Where(gc => gc.CleverbotEnabled) + .ToDictionary(gc => gc.GuildId, gc => new Lazy(() => CreateSession(), true))); + } - public int Priority => 1; + public IChatterBotSession CreateSession() + { + if (!string.IsNullOrWhiteSpace(_creds.CleverbotApiKey)) + return new OfficialCleverbotSession(_creds.CleverbotApiKey, _httpFactory); + else + return new CleverbotIOSession("GAh3wUfzDCpDpdpT", "RStKgqn7tcO9blbrv4KbXM8NDlb7H37C", _httpFactory); + } - public ChatterBotService(DiscordSocketClient client, PermissionService perms, - Bot bot, CommandHandler cmd, IBotStrings strings, IHttpClientFactory factory, - IBotCredentials creds, IEmbedBuilderService eb) + public string PrepareMessage(IUserMessage msg, out IChatterBotSession cleverbot) + { + var channel = msg.Channel as ITextChannel; + cleverbot = null; + + if (channel is null) + return null; + + if (!ChatterBotGuilds.TryGetValue(channel.Guild.Id, out Lazy lazyCleverbot)) + return null; + + cleverbot = lazyCleverbot.Value; + + var nadekoId = _client.CurrentUser.Id; + var normalMention = $"<@{nadekoId}> "; + var nickMention = $"<@!{nadekoId}> "; + string message; + if (msg.Content.StartsWith(normalMention, StringComparison.InvariantCulture)) { - _client = client; - _perms = perms; - _cmd = cmd; - _strings = strings; - _creds = creds; - _eb = eb; - _httpFactory = factory; - - ChatterBotGuilds = new ConcurrentDictionary>( - bot.AllGuildConfigs - .Where(gc => gc.CleverbotEnabled) - .ToDictionary(gc => gc.GuildId, gc => new Lazy(() => CreateSession(), true))); + message = msg.Content.Substring(normalMention.Length).Trim(); + } + else if (msg.Content.StartsWith(nickMention, StringComparison.InvariantCulture)) + { + message = msg.Content.Substring(nickMention.Length).Trim(); + } + else + { + return null; } - public IChatterBotSession CreateSession() + return message; + } + + public async Task TryAsk(IChatterBotSession cleverbot, ITextChannel channel, string message) + { + await channel.TriggerTypingAsync().ConfigureAwait(false); + + var response = await cleverbot.Think(message).ConfigureAwait(false); + try { - if (!string.IsNullOrWhiteSpace(_creds.CleverbotApiKey)) - return new OfficialCleverbotSession(_creds.CleverbotApiKey, _httpFactory); - else - return new CleverbotIOSession("GAh3wUfzDCpDpdpT", "RStKgqn7tcO9blbrv4KbXM8NDlb7H37C", _httpFactory); + await channel.SendConfirmAsync(_eb, response.SanitizeMentions(true)).ConfigureAwait(false); } - - public string PrepareMessage(IUserMessage msg, out IChatterBotSession cleverbot) + catch { - var channel = msg.Channel as ITextChannel; - cleverbot = null; - - if (channel is null) - return null; - - if (!ChatterBotGuilds.TryGetValue(channel.Guild.Id, out Lazy lazyCleverbot)) - return null; - - cleverbot = lazyCleverbot.Value; - - var nadekoId = _client.CurrentUser.Id; - var normalMention = $"<@{nadekoId}> "; - var nickMention = $"<@!{nadekoId}> "; - string message; - if (msg.Content.StartsWith(normalMention, StringComparison.InvariantCulture)) - { - message = msg.Content.Substring(normalMention.Length).Trim(); - } - else if (msg.Content.StartsWith(nickMention, StringComparison.InvariantCulture)) - { - message = msg.Content.Substring(nickMention.Length).Trim(); - } - else - { - return null; - } - - return message; + await channel.SendConfirmAsync(_eb, response.SanitizeMentions(true)).ConfigureAwait(false); // try twice :\ } + return true; + } - public async Task TryAsk(IChatterBotSession cleverbot, ITextChannel channel, string message) + public async Task RunBehavior(IGuild guild, IUserMessage usrMsg) + { + if (!(guild is SocketGuild sg)) + return false; + try { - await channel.TriggerTypingAsync().ConfigureAwait(false); - - var response = await cleverbot.Think(message).ConfigureAwait(false); - try - { - await channel.SendConfirmAsync(_eb, response.SanitizeMentions(true)).ConfigureAwait(false); - } - catch - { - await channel.SendConfirmAsync(_eb, response.SanitizeMentions(true)).ConfigureAwait(false); // try twice :\ - } - return true; - } - - public async Task RunBehavior(IGuild guild, IUserMessage usrMsg) - { - if (!(guild is SocketGuild sg)) + var message = PrepareMessage(usrMsg, out IChatterBotSession cbs); + if (message is null || cbs is null) return false; - try - { - var message = PrepareMessage(usrMsg, out IChatterBotSession cbs); - if (message is null || cbs is null) - return false; - var pc = _perms.GetCacheFor(guild.Id); - if (!pc.Permissions.CheckPermissions(usrMsg, + var pc = _perms.GetCacheFor(guild.Id); + if (!pc.Permissions.CheckPermissions(usrMsg, "cleverbot", "Games".ToLowerInvariant(), out int index)) + { + if (pc.Verbose) { - if (pc.Verbose) - { - var returnMsg = _strings.GetText(strs.perm_prevent(index + 1, - Format.Bold(pc.Permissions[index].GetCommand(_cmd.GetPrefix(guild), (SocketGuild)guild)))); + var returnMsg = _strings.GetText(strs.perm_prevent(index + 1, + Format.Bold(pc.Permissions[index].GetCommand(_cmd.GetPrefix(guild), (SocketGuild)guild)))); - try { await usrMsg.Channel.SendErrorAsync(_eb, returnMsg).ConfigureAwait(false); } catch { } - Log.Information(returnMsg); - } - return true; + try { await usrMsg.Channel.SendErrorAsync(_eb, returnMsg).ConfigureAwait(false); } catch { } + Log.Information(returnMsg); } + return true; + } - var cleverbotExecuted = await TryAsk(cbs, (ITextChannel)usrMsg.Channel, message).ConfigureAwait(false); - if (cleverbotExecuted) - { - Log.Information($@"CleverBot Executed + var cleverbotExecuted = await TryAsk(cbs, (ITextChannel)usrMsg.Channel, message).ConfigureAwait(false); + if (cleverbotExecuted) + { + Log.Information($@"CleverBot Executed Server: {guild.Name} [{guild.Id}] Channel: {usrMsg.Channel?.Name} [{usrMsg.Channel?.Id}] UserId: {usrMsg.Author} [{usrMsg.Author.Id}] Message: {usrMsg.Content}"); - return true; - } + return true; } - catch (Exception ex) - { - Log.Warning(ex,"Error in cleverbot"); - } - return false; } + catch (Exception ex) + { + Log.Warning(ex,"Error in cleverbot"); + } + return false; } -} +} \ No newline at end of file diff --git a/src/NadekoBot/Modules/Games/Services/GamesConfigService.cs b/src/NadekoBot/Modules/Games/Services/GamesConfigService.cs index 390c4fb30..8b7e4ae62 100644 --- a/src/NadekoBot/Modules/Games/Services/GamesConfigService.cs +++ b/src/NadekoBot/Modules/Games/Services/GamesConfigService.cs @@ -3,40 +3,39 @@ using NadekoBot.Common.Configs; using NadekoBot.Services; using NadekoBot.Modules.Games.Common; -namespace NadekoBot.Modules.Games.Services -{ - public sealed class GamesConfigService : ConfigServiceBase - { - public override string Name { get; } = "games"; - private const string FilePath = "data/games.yml"; - private static TypedKey changeKey = new TypedKey("config.games.updated"); +namespace NadekoBot.Modules.Games.Services; - public GamesConfigService(IConfigSeria serializer, IPubSub pubSub) - : base(FilePath, serializer, pubSub, changeKey) - { - AddParsedProp("trivia.min_win_req", gs => gs.Trivia.MinimumWinReq, int.TryParse, - ConfigPrinters.ToString, val => val > 0); - AddParsedProp("trivia.currency_reward", gs => gs.Trivia.CurrencyReward, long.TryParse, - ConfigPrinters.ToString, val => val >= 0); - AddParsedProp("hangman.currency_reward", gs => gs.Hangman.CurrencyReward, long.TryParse, - ConfigPrinters.ToString, val => val >= 0); +public sealed class GamesConfigService : ConfigServiceBase +{ + public override string Name { get; } = "games"; + private const string FilePath = "data/games.yml"; + private static TypedKey changeKey = new TypedKey("config.games.updated"); + + public GamesConfigService(IConfigSeria serializer, IPubSub pubSub) + : base(FilePath, serializer, pubSub, changeKey) + { + AddParsedProp("trivia.min_win_req", gs => gs.Trivia.MinimumWinReq, int.TryParse, + ConfigPrinters.ToString, val => val > 0); + AddParsedProp("trivia.currency_reward", gs => gs.Trivia.CurrencyReward, long.TryParse, + ConfigPrinters.ToString, val => val >= 0); + AddParsedProp("hangman.currency_reward", gs => gs.Hangman.CurrencyReward, long.TryParse, + ConfigPrinters.ToString, val => val >= 0); - Migrate(); - } + Migrate(); + } - private void Migrate() + private void Migrate() + { + if (_data.Version < 1) { - if (_data.Version < 1) + ModifyConfig(c => { - ModifyConfig(c => + c.Version = 1; + c.Hangman = new HangmanConfig() { - c.Version = 1; - c.Hangman = new HangmanConfig() - { - CurrencyReward = 0 - }; - }); - } + CurrencyReward = 0 + }; + }); } } } \ No newline at end of file diff --git a/src/NadekoBot/Modules/Games/Services/GamesService.cs b/src/NadekoBot/Modules/Games/Services/GamesService.cs index 154eabb11..49d743eb2 100644 --- a/src/NadekoBot/Modules/Games/Services/GamesService.cs +++ b/src/NadekoBot/Modules/Games/Services/GamesService.cs @@ -7,127 +7,123 @@ using NadekoBot.Modules.Games.Common.Acrophobia; using NadekoBot.Modules.Games.Common.Nunchi; using NadekoBot.Modules.Games.Common.Trivia; using Newtonsoft.Json; -using System; using System.Collections.Concurrent; -using System.Collections.Generic; using System.IO; using System.Net.Http; using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Caching.Memory; -using Serilog; -namespace NadekoBot.Modules.Games.Services +namespace NadekoBot.Modules.Games.Services; + +public class GamesService : INService { - public class GamesService : INService + private readonly GamesConfigService _gamesConfig; + + public ConcurrentDictionary GirlRatings { get; } = new ConcurrentDictionary(); + + public IReadOnlyList EightBallResponses => _gamesConfig.Data.EightBallResponses; + + private readonly Timer _t; + private readonly IHttpClientFactory _httpFactory; + private readonly IMemoryCache _8BallCache; + private readonly Random _rng; + + private const string TypingArticlesPath = "data/typing_articles3.json"; + + public List TypingArticles { get; } = new List(); + + //channelId, game + public ConcurrentDictionary AcrophobiaGames { get; } = new ConcurrentDictionary(); + public ConcurrentDictionary RunningTrivias { get; } = new ConcurrentDictionary(); + public Dictionary TicTacToeGames { get; } = new Dictionary(); + public ConcurrentDictionary RunningContests { get; } = new ConcurrentDictionary(); + public ConcurrentDictionary NunchiGames { get; } = new ConcurrentDictionary(); + + public AsyncLazy Ratings { get; } + + public class RatingTexts { - private readonly GamesConfigService _gamesConfig; + public string Nog { get; set; } + public string Tra { get; set; } + public string Fun { get; set; } + public string Uni { get; set; } + public string Wif { get; set; } + public string Dat { get; set; } + public string Dan { get; set; } + } - public ConcurrentDictionary GirlRatings { get; } = new ConcurrentDictionary(); - - public IReadOnlyList EightBallResponses => _gamesConfig.Data.EightBallResponses; - - private readonly Timer _t; - private readonly IHttpClientFactory _httpFactory; - private readonly IMemoryCache _8BallCache; - private readonly Random _rng; - - private const string TypingArticlesPath = "data/typing_articles3.json"; - - public List TypingArticles { get; } = new List(); - - //channelId, game - public ConcurrentDictionary AcrophobiaGames { get; } = new ConcurrentDictionary(); - public ConcurrentDictionary RunningTrivias { get; } = new ConcurrentDictionary(); - public Dictionary TicTacToeGames { get; } = new Dictionary(); - public ConcurrentDictionary RunningContests { get; } = new ConcurrentDictionary(); - public ConcurrentDictionary NunchiGames { get; } = new ConcurrentDictionary(); - - public AsyncLazy Ratings { get; } - - public class RatingTexts + public GamesService(GamesConfigService gamesConfig, IHttpClientFactory httpFactory) + { + _gamesConfig = gamesConfig; + _httpFactory = httpFactory; + _8BallCache = new MemoryCache(new MemoryCacheOptions() { - public string Nog { get; set; } - public string Tra { get; set; } - public string Fun { get; set; } - public string Uni { get; set; } - public string Wif { get; set; } - public string Dat { get; set; } - public string Dan { get; set; } + SizeLimit = 500_000 + }); + + Ratings = new AsyncLazy(GetRatingTexts); + _rng = new NadekoRandom(); + + //girl ratings + _t = new Timer((_) => + { + GirlRatings.Clear(); + + }, null, TimeSpan.FromDays(1), TimeSpan.FromDays(1)); + + try + { + TypingArticles = JsonConvert.DeserializeObject>(File.ReadAllText(TypingArticlesPath)); } - - public GamesService(GamesConfigService gamesConfig, IHttpClientFactory httpFactory) + catch (Exception ex) { - _gamesConfig = gamesConfig; - _httpFactory = httpFactory; - _8BallCache = new MemoryCache(new MemoryCacheOptions() - { - SizeLimit = 500_000 - }); - - Ratings = new AsyncLazy(GetRatingTexts); - _rng = new NadekoRandom(); - - //girl ratings - _t = new Timer((_) => - { - GirlRatings.Clear(); - - }, null, TimeSpan.FromDays(1), TimeSpan.FromDays(1)); - - try - { - TypingArticles = JsonConvert.DeserializeObject>(File.ReadAllText(TypingArticlesPath)); - } - catch (Exception ex) - { - Log.Warning("Error while loading typing articles {0}", ex.ToString()); - TypingArticles = new List(); - } - } - - private async Task GetRatingTexts() - { - using (var http = _httpFactory.CreateClient()) - { - var text = await http.GetStringAsync("https://nadeko-pictures.nyc3.digitaloceanspaces.com/other/rategirl/rates.json"); - return JsonConvert.DeserializeObject(text); - } - } - - public void AddTypingArticle(IUser user, string text) - { - TypingArticles.Add(new TypingArticle - { - Source = user.ToString(), - Extra = $"Text added on {DateTime.UtcNow} by {user}.", - Text = text.SanitizeMentions(true), - }); - - File.WriteAllText(TypingArticlesPath, JsonConvert.SerializeObject(TypingArticles)); - } - - public string GetEightballResponse(ulong userId, string question) - { - return _8BallCache.GetOrCreate($"8ball:{userId}:{question}", e => - { - e.Size = question.Length; - e.AbsoluteExpirationRelativeToNow = TimeSpan.FromHours(12); - return EightBallResponses[_rng.Next(0, EightBallResponses.Count)];; - }); - } - - public TypingArticle RemoveTypingArticle(int index) - { - var articles = TypingArticles; - if (index < 0 || index >= articles.Count) - return null; - - var removed = articles[index]; - TypingArticles.RemoveAt(index); - - File.WriteAllText(TypingArticlesPath, JsonConvert.SerializeObject(articles)); - return removed; + Log.Warning("Error while loading typing articles {0}", ex.ToString()); + TypingArticles = new List(); } } -} + + private async Task GetRatingTexts() + { + using (var http = _httpFactory.CreateClient()) + { + var text = await http.GetStringAsync("https://nadeko-pictures.nyc3.digitaloceanspaces.com/other/rategirl/rates.json"); + return JsonConvert.DeserializeObject(text); + } + } + + public void AddTypingArticle(IUser user, string text) + { + TypingArticles.Add(new TypingArticle + { + Source = user.ToString(), + Extra = $"Text added on {DateTime.UtcNow} by {user}.", + Text = text.SanitizeMentions(true), + }); + + File.WriteAllText(TypingArticlesPath, JsonConvert.SerializeObject(TypingArticles)); + } + + public string GetEightballResponse(ulong userId, string question) + { + return _8BallCache.GetOrCreate($"8ball:{userId}:{question}", e => + { + e.Size = question.Length; + e.AbsoluteExpirationRelativeToNow = TimeSpan.FromHours(12); + return EightBallResponses[_rng.Next(0, EightBallResponses.Count)];; + }); + } + + public TypingArticle RemoveTypingArticle(int index) + { + var articles = TypingArticles; + if (index < 0 || index >= articles.Count) + return null; + + var removed = articles[index]; + TypingArticles.RemoveAt(index); + + File.WriteAllText(TypingArticlesPath, JsonConvert.SerializeObject(articles)); + return removed; + } +} \ No newline at end of file diff --git a/src/NadekoBot/Modules/Games/Services/PollService.cs b/src/NadekoBot/Modules/Games/Services/PollService.cs index 529754741..556d6837a 100644 --- a/src/NadekoBot/Modules/Games/Services/PollService.cs +++ b/src/NadekoBot/Modules/Games/Services/PollService.cs @@ -1,140 +1,135 @@ -using System; -using System.Collections.Concurrent; -using System.Linq; +using System.Collections.Concurrent; using System.Threading.Tasks; using Discord; using NadekoBot.Common.ModuleBehaviors; using NadekoBot.Modules.Games.Common; -using NadekoBot.Db.Models; using NadekoBot.Common.Collections; using NadekoBot.Services; using NadekoBot.Services.Database.Models; using NadekoBot.Db; using NadekoBot.Extensions; -using Serilog; -namespace NadekoBot.Modules.Games.Services +namespace NadekoBot.Modules.Games.Services; + +public class PollService : IEarlyBehavior { - public class PollService : IEarlyBehavior + public ConcurrentDictionary ActivePolls { get; } = new ConcurrentDictionary(); + + public int Priority => 5; + + private readonly DbService _db; + private readonly IBotStrings _strs; + private readonly IEmbedBuilderService _eb; + + public PollService(DbService db, IBotStrings strs, IEmbedBuilderService eb) { - public ConcurrentDictionary ActivePolls { get; } = new ConcurrentDictionary(); + _db = db; + _strs = strs; + _eb = eb; - public int Priority => 5; - - private readonly DbService _db; - private readonly IBotStrings _strs; - private readonly IEmbedBuilderService _eb; - - public PollService(DbService db, IBotStrings strs, IEmbedBuilderService eb) + using (var uow = db.GetDbContext()) { - _db = db; - _strs = strs; - _eb = eb; - - using (var uow = db.GetDbContext()) - { - ActivePolls = uow.Poll.GetAllPolls() - .ToDictionary(x => x.GuildId, x => - { - var pr = new PollRunner(db, x); - pr.OnVoted += Pr_OnVoted; - return pr; - }) - .ToConcurrent(); - } - } - - public Poll CreatePoll(ulong guildId, ulong channelId, string input) - { - if (string.IsNullOrWhiteSpace(input) || !input.Contains(";")) - return null; - var data = input.Split(';'); - if (data.Length < 3) - return null; - - var col = new IndexedCollection(data.Skip(1) - .Select(x => new PollAnswer() { Text = x })); - - return new Poll() - { - Answers = col, - Question = data[0], - ChannelId = channelId, - GuildId = guildId, - Votes = new System.Collections.Generic.HashSet() - }; - } - - public bool StartPoll(Poll p) - { - var pr = new PollRunner(_db, p); - if (ActivePolls.TryAdd(p.GuildId, pr)) - { - using (var uow = _db.GetDbContext()) + ActivePolls = uow.Poll.GetAllPolls() + .ToDictionary(x => x.GuildId, x => { - uow.Poll.Add(p); - uow.SaveChanges(); - } - - pr.OnVoted += Pr_OnVoted; - return true; - } - return false; - } - - public Poll StopPoll(ulong guildId) - { - if (ActivePolls.TryRemove(guildId, out var pr)) - { - pr.OnVoted -= Pr_OnVoted; - - using var uow = _db.GetDbContext(); - uow.RemovePoll(pr.Poll.Id); - uow.SaveChanges(); - - return pr.Poll; - } - return null; - } - - private async Task Pr_OnVoted(IUserMessage msg, IGuildUser usr) - { - var toDelete = await msg.Channel.SendConfirmAsync(_eb, - _strs.GetText(strs.poll_voted(Format.Bold(usr.ToString())), usr.GuildId)) - .ConfigureAwait(false); - toDelete.DeleteAfter(5); - try { await msg.DeleteAsync().ConfigureAwait(false); } catch { } - } - - public async Task RunBehavior(IGuild guild, IUserMessage msg) - { - if (guild is null) - return false; - - if (!ActivePolls.TryGetValue(guild.Id, out var poll)) - return false; - - try - { - var voted = await poll.TryVote(msg).ConfigureAwait(false); - - if (voted) - { - Log.Information("User {UserName} [{UserId}] voted in a poll on {GuildName} [{GuildId}] server", - msg.Author.ToString(), - msg.Author.Id, - guild.Name, - guild.Id); - } - - return voted; - } - catch (Exception ex) - { - Log.Warning(ex, "Error voting"); - } - - return false; + var pr = new PollRunner(db, x); + pr.OnVoted += Pr_OnVoted; + return pr; + }) + .ToConcurrent(); } } -} + + public Poll CreatePoll(ulong guildId, ulong channelId, string input) + { + if (string.IsNullOrWhiteSpace(input) || !input.Contains(";")) + return null; + var data = input.Split(';'); + if (data.Length < 3) + return null; + + var col = new IndexedCollection(data.Skip(1) + .Select(x => new PollAnswer() { Text = x })); + + return new Poll() + { + Answers = col, + Question = data[0], + ChannelId = channelId, + GuildId = guildId, + Votes = new System.Collections.Generic.HashSet() + }; + } + + public bool StartPoll(Poll p) + { + var pr = new PollRunner(_db, p); + if (ActivePolls.TryAdd(p.GuildId, pr)) + { + using (var uow = _db.GetDbContext()) + { + uow.Poll.Add(p); + uow.SaveChanges(); + } + + pr.OnVoted += Pr_OnVoted; + return true; + } + return false; + } + + public Poll StopPoll(ulong guildId) + { + if (ActivePolls.TryRemove(guildId, out var pr)) + { + pr.OnVoted -= Pr_OnVoted; + + using var uow = _db.GetDbContext(); + uow.RemovePoll(pr.Poll.Id); + uow.SaveChanges(); + + return pr.Poll; + } + return null; + } + + private async Task Pr_OnVoted(IUserMessage msg, IGuildUser usr) + { + var toDelete = await msg.Channel.SendConfirmAsync(_eb, + _strs.GetText(strs.poll_voted(Format.Bold(usr.ToString())), usr.GuildId)) + .ConfigureAwait(false); + toDelete.DeleteAfter(5); + try { await msg.DeleteAsync().ConfigureAwait(false); } catch { } + } + + public async Task RunBehavior(IGuild guild, IUserMessage msg) + { + if (guild is null) + return false; + + if (!ActivePolls.TryGetValue(guild.Id, out var poll)) + return false; + + try + { + var voted = await poll.TryVote(msg).ConfigureAwait(false); + + if (voted) + { + Log.Information("User {UserName} [{UserId}] voted in a poll on {GuildName} [{GuildId}] server", + msg.Author.ToString(), + msg.Author.Id, + guild.Name, + guild.Id); + } + + return voted; + } + catch (Exception ex) + { + Log.Warning(ex, "Error voting"); + } + + return false; + } +} \ No newline at end of file diff --git a/src/NadekoBot/Modules/Games/SpeedTypingCommands.cs b/src/NadekoBot/Modules/Games/SpeedTypingCommands.cs index 0791d2625..ee7683d25 100644 --- a/src/NadekoBot/Modules/Games/SpeedTypingCommands.cs +++ b/src/NadekoBot/Modules/Games/SpeedTypingCommands.cs @@ -2,116 +2,112 @@ using Discord.Commands; using Discord.WebSocket; using NadekoBot.Extensions; -using Newtonsoft.Json; -using System.IO; -using System.Linq; using System.Threading.Tasks; using NadekoBot.Common.Attributes; using NadekoBot.Modules.Games.Common; using NadekoBot.Modules.Games.Services; using NadekoBot.Common; -namespace NadekoBot.Modules.Games +namespace NadekoBot.Modules.Games; + +public partial class Games { - public partial class Games + [Group] + public class SpeedTypingCommands : NadekoSubmodule { - [Group] - public class SpeedTypingCommands : NadekoSubmodule + private readonly GamesService _games; + private readonly DiscordSocketClient _client; + + public SpeedTypingCommands(DiscordSocketClient client, GamesService games) { - private readonly GamesService _games; - private readonly DiscordSocketClient _client; + _games = games; + _client = client; + } - public SpeedTypingCommands(DiscordSocketClient client, GamesService games) + [NadekoCommand, Aliases] + [RequireContext(ContextType.Guild)] + [NadekoOptionsAttribute(typeof(TypingGame.Options))] + public async Task TypeStart(params string[] args) + { + var (options, _) = OptionsParser.ParseFrom(new TypingGame.Options(), args); + var channel = (ITextChannel)ctx.Channel; + + var game = _service.RunningContests.GetOrAdd(ctx.Guild.Id, id => new TypingGame(_games, _client, channel, Prefix, options, _eb)); + + if (game.IsActive) { - _games = games; - _client = client; + await SendErrorAsync($"Contest already running in {game.Channel.Mention} channel."); } - - [NadekoCommand, Aliases] - [RequireContext(ContextType.Guild)] - [NadekoOptionsAttribute(typeof(TypingGame.Options))] - public async Task TypeStart(params string[] args) + else { - var (options, _) = OptionsParser.ParseFrom(new TypingGame.Options(), args); - var channel = (ITextChannel)ctx.Channel; - - var game = _service.RunningContests.GetOrAdd(ctx.Guild.Id, id => new TypingGame(_games, _client, channel, Prefix, options, _eb)); - - if (game.IsActive) - { - await SendErrorAsync($"Contest already running in {game.Channel.Mention} channel."); - } - else - { - await game.Start().ConfigureAwait(false); - } + await game.Start().ConfigureAwait(false); } + } - [NadekoCommand, Aliases] - [RequireContext(ContextType.Guild)] - public async Task TypeStop() + [NadekoCommand, Aliases] + [RequireContext(ContextType.Guild)] + public async Task TypeStop() + { + if (_service.RunningContests.TryRemove(ctx.Guild.Id, out TypingGame game)) { - if (_service.RunningContests.TryRemove(ctx.Guild.Id, out TypingGame game)) - { - await game.Stop().ConfigureAwait(false); - return; - } + await game.Stop().ConfigureAwait(false); + return; + } - await SendErrorAsync("No contest to stop on this channel.").ConfigureAwait(false); - } + await SendErrorAsync("No contest to stop on this channel.").ConfigureAwait(false); + } - [NadekoCommand, Aliases] - [RequireContext(ContextType.Guild)] - [OwnerOnly] - public async Task Typeadd([Leftover] string text) + [NadekoCommand, Aliases] + [RequireContext(ContextType.Guild)] + [OwnerOnly] + public async Task Typeadd([Leftover] string text) + { + if (string.IsNullOrWhiteSpace(text)) + return; + + _games.AddTypingArticle(ctx.User, text); + + await SendConfirmAsync("Added new article for typing game.").ConfigureAwait(false); + } + + [NadekoCommand, Aliases] + [RequireContext(ContextType.Guild)] + public async Task Typelist(int page = 1) + { + if (page < 1) + return; + + var articles = _games.TypingArticles.Skip((page - 1) * 15).Take(15).ToArray(); + + if (!articles.Any()) { - if (string.IsNullOrWhiteSpace(text)) - return; - - _games.AddTypingArticle(ctx.User, text); - - await SendConfirmAsync("Added new article for typing game.").ConfigureAwait(false); + await SendErrorAsync($"{ctx.User.Mention} `No articles found on that page.`").ConfigureAwait(false); + return; } + var i = (page - 1) * 15; + await SendConfirmAsync("List of articles for Type Race", string.Join("\n", articles.Select(a => $"`#{++i}` - {a.Text.TrimTo(50)}"))) + .ConfigureAwait(false); + } - [NadekoCommand, Aliases] - [RequireContext(ContextType.Guild)] - public async Task Typelist(int page = 1) - { - if (page < 1) - return; - - var articles = _games.TypingArticles.Skip((page - 1) * 15).Take(15).ToArray(); - - if (!articles.Any()) - { - await SendErrorAsync($"{ctx.User.Mention} `No articles found on that page.`").ConfigureAwait(false); - return; - } - var i = (page - 1) * 15; - await SendConfirmAsync("List of articles for Type Race", string.Join("\n", articles.Select(a => $"`#{++i}` - {a.Text.TrimTo(50)}"))) - .ConfigureAwait(false); - } - - [NadekoCommand, Aliases] - [RequireContext(ContextType.Guild)] - [OwnerOnly] - public async Task Typedel(int index) - { - var removed = _service.RemoveTypingArticle(--index); + [NadekoCommand, Aliases] + [RequireContext(ContextType.Guild)] + [OwnerOnly] + public async Task Typedel(int index) + { + var removed = _service.RemoveTypingArticle(--index); - if (removed is null) - { - return; - } - - var embed = _eb.Create() - .WithTitle($"Removed typing article #{index + 1}") - .WithDescription(removed.Text.TrimTo(50)) - .WithOkColor(); - - await ctx.Channel.EmbedAsync(embed); + if (removed is null) + { + return; } + + var embed = _eb.Create() + .WithTitle($"Removed typing article #{index + 1}") + .WithDescription(removed.Text.TrimTo(50)) + .WithOkColor(); + + await ctx.Channel.EmbedAsync(embed); } } } \ No newline at end of file diff --git a/src/NadekoBot/Modules/Games/TicTacToeCommands.cs b/src/NadekoBot/Modules/Games/TicTacToeCommands.cs index 8b17e0ff7..3bb88a0c3 100644 --- a/src/NadekoBot/Modules/Games/TicTacToeCommands.cs +++ b/src/NadekoBot/Modules/Games/TicTacToeCommands.cs @@ -8,54 +8,53 @@ using NadekoBot.Modules.Games.Services; using System.Threading; using System.Threading.Tasks; -namespace NadekoBot.Modules.Games +namespace NadekoBot.Modules.Games; + +public partial class Games { - public partial class Games + [Group] + public class TicTacToeCommands : NadekoSubmodule { - [Group] - public class TicTacToeCommands : NadekoSubmodule + private readonly SemaphoreSlim _sem = new SemaphoreSlim(1, 1); + private readonly DiscordSocketClient _client; + + public TicTacToeCommands(DiscordSocketClient client) { - private readonly SemaphoreSlim _sem = new SemaphoreSlim(1, 1); - private readonly DiscordSocketClient _client; + _client = client; + } - public TicTacToeCommands(DiscordSocketClient client) + [NadekoCommand, Aliases] + [RequireContext(ContextType.Guild)] + [NadekoOptions(typeof(TicTacToe.Options))] + public async Task TicTacToe(params string[] args) + { + var (options, _) = OptionsParser.ParseFrom(new TicTacToe.Options(), args); + var channel = (ITextChannel)ctx.Channel; + + await _sem.WaitAsync(1000).ConfigureAwait(false); + try { - _client = client; + if (_service.TicTacToeGames.TryGetValue(channel.Id, out TicTacToe game)) + { + var _ = Task.Run(async () => + { + await game.Start((IGuildUser)ctx.User).ConfigureAwait(false); + }); + return; + } + game = new TicTacToe(base.Strings, this._client, channel, (IGuildUser)ctx.User, options, _eb); + _service.TicTacToeGames.Add(channel.Id, game); + await ReplyConfirmLocalizedAsync(strs.ttt_created).ConfigureAwait(false); + + game.OnEnded += (g) => + { + _service.TicTacToeGames.Remove(channel.Id); + _sem.Dispose(); + }; } - - [NadekoCommand, Aliases] - [RequireContext(ContextType.Guild)] - [NadekoOptions(typeof(TicTacToe.Options))] - public async Task TicTacToe(params string[] args) + finally { - var (options, _) = OptionsParser.ParseFrom(new TicTacToe.Options(), args); - var channel = (ITextChannel)ctx.Channel; - - await _sem.WaitAsync(1000).ConfigureAwait(false); - try - { - if (_service.TicTacToeGames.TryGetValue(channel.Id, out TicTacToe game)) - { - var _ = Task.Run(async () => - { - await game.Start((IGuildUser)ctx.User).ConfigureAwait(false); - }); - return; - } - game = new TicTacToe(base.Strings, this._client, channel, (IGuildUser)ctx.User, options, _eb); - _service.TicTacToeGames.Add(channel.Id, game); - await ReplyConfirmLocalizedAsync(strs.ttt_created).ConfigureAwait(false); - - game.OnEnded += (g) => - { - _service.TicTacToeGames.Remove(channel.Id); - _sem.Dispose(); - }; - } - finally - { - _sem.Release(); - } + _sem.Release(); } } } diff --git a/src/NadekoBot/Modules/Games/TriviaCommands.cs b/src/NadekoBot/Modules/Games/TriviaCommands.cs index 292cf5f08..43a0969ea 100644 --- a/src/NadekoBot/Modules/Games/TriviaCommands.cs +++ b/src/NadekoBot/Modules/Games/TriviaCommands.cs @@ -1,7 +1,6 @@ using Discord; using Discord.Commands; using Discord.WebSocket; -using NadekoBot.Extensions; using NadekoBot.Services; using System.Threading.Tasks; using NadekoBot.Common.Attributes; @@ -9,93 +8,92 @@ using NadekoBot.Modules.Games.Common.Trivia; using NadekoBot.Modules.Games.Services; using NadekoBot.Common; -namespace NadekoBot.Modules.Games +namespace NadekoBot.Modules.Games; + +public partial class Games { - public partial class Games + [Group] + public class TriviaCommands : NadekoSubmodule { - [Group] - public class TriviaCommands : NadekoSubmodule + private readonly IDataCache _cache; + private readonly ICurrencyService _cs; + private readonly GamesConfigService _gamesConfig; + private readonly DiscordSocketClient _client; + + public TriviaCommands(DiscordSocketClient client, IDataCache cache, ICurrencyService cs, + GamesConfigService gamesConfig) { - private readonly IDataCache _cache; - private readonly ICurrencyService _cs; - private readonly GamesConfigService _gamesConfig; - private readonly DiscordSocketClient _client; + _cache = cache; + _cs = cs; + _gamesConfig = gamesConfig; + _client = client; + } - public TriviaCommands(DiscordSocketClient client, IDataCache cache, ICurrencyService cs, - GamesConfigService gamesConfig) + [NadekoCommand, Aliases] + [RequireContext(ContextType.Guild)] + [Priority(0)] + [NadekoOptionsAttribute(typeof(TriviaOptions))] + public Task Trivia(params string[] args) + => InternalTrivia(args); + + public async Task InternalTrivia(params string[] args) + { + var channel = (ITextChannel)ctx.Channel; + + var (opts, _) = OptionsParser.ParseFrom(new TriviaOptions(), args); + + var config = _gamesConfig.Data; + if (config.Trivia.MinimumWinReq > 0 && config.Trivia.MinimumWinReq > opts.WinRequirement) { - _cache = cache; - _cs = cs; - _gamesConfig = gamesConfig; - _client = client; + return; } - [NadekoCommand, Aliases] - [RequireContext(ContextType.Guild)] - [Priority(0)] - [NadekoOptionsAttribute(typeof(TriviaOptions))] - public Task Trivia(params string[] args) - => InternalTrivia(args); - - public async Task InternalTrivia(params string[] args) + var trivia = new TriviaGame(Strings, _client, config, _cache, _cs, channel.Guild, channel, opts, + Prefix + "tq", _eb); + if (_service.RunningTrivias.TryAdd(channel.Guild.Id, trivia)) { - var channel = (ITextChannel)ctx.Channel; - - var (opts, _) = OptionsParser.ParseFrom(new TriviaOptions(), args); - - var config = _gamesConfig.Data; - if (config.Trivia.MinimumWinReq > 0 && config.Trivia.MinimumWinReq > opts.WinRequirement) + try { - return; + await trivia.StartGame().ConfigureAwait(false); } - - var trivia = new TriviaGame(Strings, _client, config, _cache, _cs, channel.Guild, channel, opts, - Prefix + "tq", _eb); - if (_service.RunningTrivias.TryAdd(channel.Guild.Id, trivia)) + finally { - try - { - await trivia.StartGame().ConfigureAwait(false); - } - finally - { - _service.RunningTrivias.TryRemove(channel.Guild.Id, out trivia); - await trivia.EnsureStopped().ConfigureAwait(false); - } - return; + _service.RunningTrivias.TryRemove(channel.Guild.Id, out trivia); + await trivia.EnsureStopped().ConfigureAwait(false); } - - await SendErrorAsync(GetText(strs.trivia_already_running) + "\n" + trivia.CurrentQuestion) - .ConfigureAwait(false); + return; } - [NadekoCommand, Aliases] - [RequireContext(ContextType.Guild)] - public async Task Tl() - { - if (_service.RunningTrivias.TryGetValue(ctx.Guild.Id, out TriviaGame trivia)) - { - await SendConfirmAsync(GetText(strs.leaderboard), trivia.GetLeaderboard()).ConfigureAwait(false); - return; - } + await SendErrorAsync(GetText(strs.trivia_already_running) + "\n" + trivia.CurrentQuestion) + .ConfigureAwait(false); + } - await ReplyErrorLocalizedAsync(strs.trivia_none).ConfigureAwait(false); + [NadekoCommand, Aliases] + [RequireContext(ContextType.Guild)] + public async Task Tl() + { + if (_service.RunningTrivias.TryGetValue(ctx.Guild.Id, out TriviaGame trivia)) + { + await SendConfirmAsync(GetText(strs.leaderboard), trivia.GetLeaderboard()).ConfigureAwait(false); + return; } - [NadekoCommand, Aliases] - [RequireContext(ContextType.Guild)] - public async Task Tq() + await ReplyErrorLocalizedAsync(strs.trivia_none).ConfigureAwait(false); + } + + [NadekoCommand, Aliases] + [RequireContext(ContextType.Guild)] + public async Task Tq() + { + var channel = (ITextChannel)ctx.Channel; + + if (_service.RunningTrivias.TryGetValue(channel.Guild.Id, out TriviaGame trivia)) { - var channel = (ITextChannel)ctx.Channel; - - if (_service.RunningTrivias.TryGetValue(channel.Guild.Id, out TriviaGame trivia)) - { - await trivia.StopGame().ConfigureAwait(false); - return; - } - - await ReplyErrorLocalizedAsync(strs.trivia_none).ConfigureAwait(false); + await trivia.StopGame().ConfigureAwait(false); + return; } + + await ReplyErrorLocalizedAsync(strs.trivia_none).ConfigureAwait(false); } } } \ No newline at end of file diff --git a/src/NadekoBot/Modules/Help/Common/CommandsOptions.cs b/src/NadekoBot/Modules/Help/Common/CommandsOptions.cs index 2fc423bbf..62abb2170 100644 --- a/src/NadekoBot/Modules/Help/Common/CommandsOptions.cs +++ b/src/NadekoBot/Modules/Help/Common/CommandsOptions.cs @@ -1,23 +1,22 @@ using CommandLine; using NadekoBot.Common; -namespace NadekoBot.Modules.Help.Common +namespace NadekoBot.Modules.Help.Common; + +public class CommandsOptions : INadekoCommandOptions { - public class CommandsOptions : INadekoCommandOptions + public enum ViewType { - public enum ViewType - { - Hide, - Cross, - All, - } - - [Option('v', "view", Required = false, Default = ViewType.Hide, HelpText = "Specifies how to output the list of commands. 0 - Hide commands which you can't use, 1 - Cross out commands which you can't use, 2 - Show all.")] - public ViewType View { get; set; } = ViewType.Hide; - - public void NormalizeOptions() - { - - } + Hide, + Cross, + All, } -} + + [Option('v', "view", Required = false, Default = ViewType.Hide, HelpText = "Specifies how to output the list of commands. 0 - Hide commands which you can't use, 1 - Cross out commands which you can't use, 2 - Show all.")] + public ViewType View { get; set; } = ViewType.Hide; + + public void NormalizeOptions() + { + + } +} \ No newline at end of file diff --git a/src/NadekoBot/Modules/Help/Help.cs b/src/NadekoBot/Modules/Help/Help.cs index b33c5d0ed..b6b15e604 100644 --- a/src/NadekoBot/Modules/Help/Help.cs +++ b/src/NadekoBot/Modules/Help/Help.cs @@ -9,451 +9,446 @@ using NadekoBot.Extensions; using NadekoBot.Modules.Help.Services; using NadekoBot.Modules.Permissions.Services; using Newtonsoft.Json; -using System; -using System.Collections.Generic; using System.IO; -using System.Linq; using System.Text; using System.Text.Json; using System.Threading.Tasks; using Amazon.S3; using Amazon.S3.Model; using Discord.WebSocket; -using Serilog; -namespace NadekoBot.Modules.Help +namespace NadekoBot.Modules.Help; + +public class Help : NadekoModule { - public class Help : NadekoModule + public const string PatreonUrl = "https://patreon.com/nadekobot"; + public const string PaypalUrl = "https://paypal.me/Kwoth"; + private readonly CommandService _cmds; + private readonly BotConfigService _bss; + private readonly GlobalPermissionService _perms; + private readonly IServiceProvider _services; + private readonly DiscordSocketClient _client; + private readonly IBotStrings _strings; + + private readonly AsyncLazy _lazyClientId; + + public Help(GlobalPermissionService perms, CommandService cmds, BotConfigService bss, + IServiceProvider services, DiscordSocketClient client, IBotStrings strings) { - public const string PatreonUrl = "https://patreon.com/nadekobot"; - public const string PaypalUrl = "https://paypal.me/Kwoth"; - private readonly CommandService _cmds; - private readonly BotConfigService _bss; - private readonly GlobalPermissionService _perms; - private readonly IServiceProvider _services; - private readonly DiscordSocketClient _client; - private readonly IBotStrings _strings; + _cmds = cmds; + _bss = bss; + _perms = perms; + _services = services; + _client = client; + _strings = strings; - private readonly AsyncLazy _lazyClientId; + _lazyClientId = new AsyncLazy(async () => (await _client.GetApplicationInfoAsync()).Id); + } - public Help(GlobalPermissionService perms, CommandService cmds, BotConfigService bss, - IServiceProvider services, DiscordSocketClient client, IBotStrings strings) - { - _cmds = cmds; - _bss = bss; - _perms = perms; - _services = services; - _client = client; - _strings = strings; - - _lazyClientId = new AsyncLazy(async () => (await _client.GetApplicationInfoAsync()).Id); - } - - public async Task GetHelpString() - { - var botSettings = _bss.Data; - if (string.IsNullOrWhiteSpace(botSettings.HelpText) || botSettings.HelpText == "-") - return default; + public async Task GetHelpString() + { + var botSettings = _bss.Data; + if (string.IsNullOrWhiteSpace(botSettings.HelpText) || botSettings.HelpText == "-") + return default; - var clientId = await _lazyClientId.Value; - var r = new ReplacementBuilder() - .WithDefault(Context) - .WithOverride("{0}", () => clientId.ToString()) - .WithOverride("{1}", () => Prefix) - .WithOverride("%prefix%", () => Prefix) - .WithOverride("%bot.prefix%", () => Prefix) - .Build(); + var clientId = await _lazyClientId.Value; + var r = new ReplacementBuilder() + .WithDefault(Context) + .WithOverride("{0}", () => clientId.ToString()) + .WithOverride("{1}", () => Prefix) + .WithOverride("%prefix%", () => Prefix) + .WithOverride("%bot.prefix%", () => Prefix) + .Build(); - var text = SmartText.CreateFrom(botSettings.HelpText); - return r.Replace(text); - } + var text = SmartText.CreateFrom(botSettings.HelpText); + return r.Replace(text); + } - [NadekoCommand, Aliases] - public async Task Modules(int page = 1) + [NadekoCommand, Aliases] + public async Task Modules(int page = 1) + { + if (--page < 0) + return; + + var topLevelModules = _cmds.Modules.GroupBy(m => m.GetTopLevelModule()) + .Where(m => !_perms.BlockedModules.Contains(m.Key.Name.ToLowerInvariant())) + .Select(x => x.Key) + .ToList(); + + await ctx.SendPaginatedConfirmAsync(page, cur => { - if (--page < 0) - return; + var embed = _eb.Create().WithOkColor() + .WithTitle(GetText(strs.list_of_modules)); - var topLevelModules = _cmds.Modules.GroupBy(m => m.GetTopLevelModule()) - .Where(m => !_perms.BlockedModules.Contains(m.Key.Name.ToLowerInvariant())) - .Select(x => x.Key) + var localModules = topLevelModules.Skip(12 * cur) + .Take(12) .ToList(); - - await ctx.SendPaginatedConfirmAsync(page, cur => + + if (!localModules.Any()) { - var embed = _eb.Create().WithOkColor() - .WithTitle(GetText(strs.list_of_modules)); - - var localModules = topLevelModules.Skip(12 * cur) - .Take(12) - .ToList(); - - if (!localModules.Any()) - { - embed = embed.WithOkColor() - .WithDescription(GetText(strs.module_page_empty)); - return embed; - } - - localModules - .OrderBy(module => module.Name) - .ForEach(module => embed.AddField($"{GetModuleEmoji(module.Name)} {module.Name}", - GetText(GetModuleLocStr(module.Name)) + "\n" + - Format.Code(GetText(strs.module_footer(Prefix, module.Name.ToLowerInvariant()))), - true)); - + embed = embed.WithOkColor() + .WithDescription(GetText(strs.module_page_empty)); return embed; - }, topLevelModules.Count(), 12, false); - } - - private LocStr GetModuleLocStr(string moduleName) - { - switch (moduleName.ToLowerInvariant()) - { - case "help": - return strs.module_description_help; - case "administration": - return strs.module_description_administration; - case "customreactions": - return strs.module_description_customreactions; - case "searches": - return strs.module_description_searches; - case "utility": - return strs.module_description_utility; - case "games": - return strs.module_description_games; - case "gambling": - return strs.module_description_gambling; - case "music": - return strs.module_description_music; - case "nsfw": - return strs.module_description_nsfw; - case "permissions": - return strs.module_description_permissions; - case "xp": - return strs.module_description_xp; - default: - return strs.module_description_missing; + } - } - } - private string GetModuleEmoji(string moduleName) + localModules + .OrderBy(module => module.Name) + .ForEach(module => embed.AddField($"{GetModuleEmoji(module.Name)} {module.Name}", + GetText(GetModuleLocStr(module.Name)) + "\n" + + Format.Code(GetText(strs.module_footer(Prefix, module.Name.ToLowerInvariant()))), + true)); + + return embed; + }, topLevelModules.Count(), 12, false); + } + + private LocStr GetModuleLocStr(string moduleName) + { + switch (moduleName.ToLowerInvariant()) { - moduleName = moduleName.ToLowerInvariant(); - switch (moduleName) - { - case "help": - return "❓"; - case "administration": - return "🛠️"; - case "customreactions": - return "🗣️"; - case "searches": - return "🔍"; - case "utility": - return "🔧"; - case "games": - return "🎲"; - case "gambling": - return "💰"; - case "music": - return "🎶"; - case "nsfw": - return "😳"; - case "permissions": - return "🚓"; - case "xp": - return "📝"; - default: - return "📖"; + case "help": + return strs.module_description_help; + case "administration": + return strs.module_description_administration; + case "customreactions": + return strs.module_description_customreactions; + case "searches": + return strs.module_description_searches; + case "utility": + return strs.module_description_utility; + case "games": + return strs.module_description_games; + case "gambling": + return strs.module_description_gambling; + case "music": + return strs.module_description_music; + case "nsfw": + return strs.module_description_nsfw; + case "permissions": + return strs.module_description_permissions; + case "xp": + return strs.module_description_xp; + default: + return strs.module_description_missing; - } + } + } + private string GetModuleEmoji(string moduleName) + { + moduleName = moduleName.ToLowerInvariant(); + switch (moduleName) + { + case "help": + return "❓"; + case "administration": + return "🛠️"; + case "customreactions": + return "🗣️"; + case "searches": + return "🔍"; + case "utility": + return "🔧"; + case "games": + return "🎲"; + case "gambling": + return "💰"; + case "music": + return "🎶"; + case "nsfw": + return "😳"; + case "permissions": + return "🚓"; + case "xp": + return "📝"; + default: + return "📖"; + + } + } + + [NadekoCommand, Aliases] + [NadekoOptions(typeof(CommandsOptions))] + public async Task Commands(string module = null, params string[] args) + { + var channel = ctx.Channel; + + + module = module?.Trim().ToUpperInvariant(); + if (string.IsNullOrWhiteSpace(module)) + { + await Modules(); + return; } - [NadekoCommand, Aliases] - [NadekoOptions(typeof(CommandsOptions))] - public async Task Commands(string module = null, params string[] args) + var (opts, _) = OptionsParser.ParseFrom(new CommandsOptions(), args); + + // Find commands for that module + // don't show commands which are blocked + // order by name + var cmds = _cmds.Commands.Where(c => c.Module.GetTopLevelModule().Name.ToUpperInvariant().StartsWith(module, StringComparison.InvariantCulture)) + .Where(c => !_perms.BlockedCommands.Contains(c.Aliases[0].ToLowerInvariant())) + .OrderBy(c => c.Aliases[0]) + .Distinct(new CommandTextEqualityComparer()); + + + // check preconditions for all commands, but only if it's not 'all' + // because all will show all commands anyway, no need to check + var succ = new HashSet(); + if (opts.View != CommandsOptions.ViewType.All) { - var channel = ctx.Channel; - - - module = module?.Trim().ToUpperInvariant(); - if (string.IsNullOrWhiteSpace(module)) - { - await Modules(); - return; - } - - var (opts, _) = OptionsParser.ParseFrom(new CommandsOptions(), args); - - // Find commands for that module - // don't show commands which are blocked - // order by name - var cmds = _cmds.Commands.Where(c => c.Module.GetTopLevelModule().Name.ToUpperInvariant().StartsWith(module, StringComparison.InvariantCulture)) - .Where(c => !_perms.BlockedCommands.Contains(c.Aliases[0].ToLowerInvariant())) - .OrderBy(c => c.Aliases[0]) - .Distinct(new CommandTextEqualityComparer()); - - - // check preconditions for all commands, but only if it's not 'all' - // because all will show all commands anyway, no need to check - var succ = new HashSet(); - if (opts.View != CommandsOptions.ViewType.All) - { - succ = new HashSet((await Task.WhenAll(cmds.Select(async x => + succ = new HashSet((await Task.WhenAll(cmds.Select(async x => { var pre = (await x.CheckPreconditionsAsync(Context, _services).ConfigureAwait(false)); return (Cmd: x, Succ: pre.IsSuccess); })).ConfigureAwait(false)) - .Where(x => x.Succ) - .Select(x => x.Cmd)); + .Where(x => x.Succ) + .Select(x => x.Cmd)); - if (opts.View == CommandsOptions.ViewType.Hide) - { - // if hidden is specified, completely remove these commands from the list - cmds = cmds.Where(x => succ.Contains(x)); - } - } - - var cmdsWithGroup = cmds.GroupBy(c => c.Module.Name.Replace("Commands", "", StringComparison.InvariantCulture)) - .OrderBy(x => x.Key == x.First().Module.Name ? int.MaxValue : x.Count()); - - if (!cmds.Any()) + if (opts.View == CommandsOptions.ViewType.Hide) { - if (opts.View != CommandsOptions.ViewType.Hide) - await ReplyErrorLocalizedAsync(strs.module_not_found).ConfigureAwait(false); - else - await ReplyErrorLocalizedAsync(strs.module_not_found_or_cant_exec).ConfigureAwait(false); - return; + // if hidden is specified, completely remove these commands from the list + cmds = cmds.Where(x => succ.Contains(x)); } - var i = 0; - var groups = cmdsWithGroup.GroupBy(x => i++ / 48).ToArray(); - var embed = _eb.Create().WithOkColor(); - foreach (var g in groups) - { - var last = g.Count(); - for (i = 0; i < last; i++) - { - var transformed = g.ElementAt(i).Select(x => - { - //if cross is specified, and the command doesn't satisfy the requirements, cross it out - if (opts.View == CommandsOptions.ViewType.Cross) - { - return $"{(succ.Contains(x) ? "✅" : "❌")}{Prefix + x.Aliases.First(),-15} {"[" + x.Aliases.Skip(1).FirstOrDefault() + "]",-8}"; - } - return $"{Prefix + x.Aliases.First(),-15} {"[" + x.Aliases.Skip(1).FirstOrDefault() + "]",-8}"; - }); + } - if (i == last - 1 && (i + 1) % 2 != 0) + var cmdsWithGroup = cmds.GroupBy(c => c.Module.Name.Replace("Commands", "", StringComparison.InvariantCulture)) + .OrderBy(x => x.Key == x.First().Module.Name ? int.MaxValue : x.Count()); + + if (!cmds.Any()) + { + if (opts.View != CommandsOptions.ViewType.Hide) + await ReplyErrorLocalizedAsync(strs.module_not_found).ConfigureAwait(false); + else + await ReplyErrorLocalizedAsync(strs.module_not_found_or_cant_exec).ConfigureAwait(false); + return; + } + var i = 0; + var groups = cmdsWithGroup.GroupBy(x => i++ / 48).ToArray(); + var embed = _eb.Create().WithOkColor(); + foreach (var g in groups) + { + var last = g.Count(); + for (i = 0; i < last; i++) + { + var transformed = g.ElementAt(i).Select(x => + { + //if cross is specified, and the command doesn't satisfy the requirements, cross it out + if (opts.View == CommandsOptions.ViewType.Cross) { - var grp = 0; - var count = transformed.Count(); - transformed = transformed - .GroupBy(x => grp++ % count / 2) - .Select(x => - { - if (x.Count() == 1) - return $"{x.First()}"; - else - return String.Concat(x); - }); + return $"{(succ.Contains(x) ? "✅" : "❌")}{Prefix + x.Aliases.First(),-15} {"[" + x.Aliases.Skip(1).FirstOrDefault() + "]",-8}"; } - embed.AddField(g.ElementAt(i).Key, "```css\n" + string.Join("\n", transformed) + "\n```", true); - } - } - embed.WithFooter(GetText(strs.commands_instr(Prefix))); - await ctx.Channel.EmbedAsync(embed).ConfigureAwait(false); - } + return $"{Prefix + x.Aliases.First(),-15} {"[" + x.Aliases.Skip(1).FirstOrDefault() + "]",-8}"; + }); - [NadekoCommand, Aliases] - [Priority(0)] - public async Task H([Leftover] string fail) - { - var prefixless = _cmds.Commands.FirstOrDefault(x => x.Aliases.Any(cmdName => cmdName.ToLowerInvariant() == fail)); - if (prefixless != null) - { - await H(prefixless).ConfigureAwait(false); - return; - } - - await ReplyErrorLocalizedAsync(strs.command_not_found).ConfigureAwait(false); - } - - [NadekoCommand, Aliases] - [Priority(1)] - public async Task H([Leftover] CommandInfo com = null) - { - var channel = ctx.Channel; - - if (com is null) - { - IMessageChannel ch = channel is ITextChannel - ? await ((IGuildUser)ctx.User).GetOrCreateDMChannelAsync().ConfigureAwait(false) - : channel; - try + if (i == last - 1 && (i + 1) % 2 != 0) { - var data = await GetHelpString(); - if (data == default) - return; - await ch.SendAsync(data); - try{ await ctx.OkAsync(); } catch { } // ignore if bot can't react - } - catch (Exception) - { - await ReplyErrorLocalizedAsync(strs.cant_dm).ConfigureAwait(false); - } - return; - } - - var embed = _service.GetCommandHelp(com, ctx.Guild); - await channel.EmbedAsync(embed).ConfigureAwait(false); - } - - [NadekoCommand, Aliases] - [OwnerOnly] - public async Task GenCmdList() - { - _ = ctx.Channel.TriggerTypingAsync(); - - // order commands by top level module name - // and make a dictionary of > - var cmdData = _cmds - .Commands - .GroupBy(x => x.Module.GetTopLevelModule().Name) - .OrderBy(x => x.Key) - .ToDictionary( - x => x.Key, - x => x.Distinct(c => c.Aliases.First()) - .Select(com => + var grp = 0; + var count = transformed.Count(); + transformed = transformed + .GroupBy(x => grp++ % count / 2) + .Select(x => { - List optHelpStr = null; - var opt = ((NadekoOptionsAttribute)com.Attributes.FirstOrDefault(x => x is NadekoOptionsAttribute))?.OptionType; - if (opt != null) - { - optHelpStr = HelpService.GetCommandOptionHelpList(opt); - } - - return new CommandJsonObject - { - Aliases = com.Aliases.Select(alias => Prefix + alias).ToArray(), - Description = com.RealSummary(_strings, ctx.Guild?.Id, Prefix), - Usage = com.RealRemarksArr(_strings, ctx.Guild?.Id, Prefix), - Submodule = com.Module.Name, - Module = com.Module.GetTopLevelModule().Name, - Options = optHelpStr, - Requirements = HelpService.GetCommandRequirements(com), - }; - }) - .ToList() - ); + if (x.Count() == 1) + return $"{x.First()}"; + else + return String.Concat(x); + }); + } + embed.AddField(g.ElementAt(i).Key, "```css\n" + string.Join("\n", transformed) + "\n```", true); + } + } + embed.WithFooter(GetText(strs.commands_instr(Prefix))); + await ctx.Channel.EmbedAsync(embed).ConfigureAwait(false); + } - var readableData = JsonConvert.SerializeObject(cmdData, Formatting.Indented); - var uploadData = JsonConvert.SerializeObject(cmdData, Formatting.None); + [NadekoCommand, Aliases] + [Priority(0)] + public async Task H([Leftover] string fail) + { + var prefixless = _cmds.Commands.FirstOrDefault(x => x.Aliases.Any(cmdName => cmdName.ToLowerInvariant() == fail)); + if (prefixless != null) + { + await H(prefixless).ConfigureAwait(false); + return; + } - // for example https://nyc.digitaloceanspaces.com (without your space name) - var serviceUrl = Environment.GetEnvironmentVariable("do_spaces_address"); + await ReplyErrorLocalizedAsync(strs.command_not_found).ConfigureAwait(false); + } - // generate spaces access key on https://cloud.digitalocean.com/account/api/tokens - // you will get 2 keys, first, shorter one is id, longer one is secret - var accessKey = Environment.GetEnvironmentVariable("do_access_key_id"); - var secretAcccessKey = Environment.GetEnvironmentVariable("do_access_key_secret"); + [NadekoCommand, Aliases] + [Priority(1)] + public async Task H([Leftover] CommandInfo com = null) + { + var channel = ctx.Channel; - // if all env vars are set, upload the unindented file (to save space) there - if (!(serviceUrl is null || accessKey is null || secretAcccessKey is null)) + if (com is null) + { + IMessageChannel ch = channel is ITextChannel + ? await ((IGuildUser)ctx.User).GetOrCreateDMChannelAsync().ConfigureAwait(false) + : channel; + try { - var config = new AmazonS3Config {ServiceURL = serviceUrl}; + var data = await GetHelpString(); + if (data == default) + return; + await ch.SendAsync(data); + try{ await ctx.OkAsync(); } catch { } // ignore if bot can't react + } + catch (Exception) + { + await ReplyErrorLocalizedAsync(strs.cant_dm).ConfigureAwait(false); + } + return; + } + + var embed = _service.GetCommandHelp(com, ctx.Guild); + await channel.EmbedAsync(embed).ConfigureAwait(false); + } + + [NadekoCommand, Aliases] + [OwnerOnly] + public async Task GenCmdList() + { + _ = ctx.Channel.TriggerTypingAsync(); + + // order commands by top level module name + // and make a dictionary of > + var cmdData = _cmds + .Commands + .GroupBy(x => x.Module.GetTopLevelModule().Name) + .OrderBy(x => x.Key) + .ToDictionary( + x => x.Key, + x => x.Distinct(c => c.Aliases.First()) + .Select(com => + { + List optHelpStr = null; + var opt = ((NadekoOptionsAttribute)com.Attributes.FirstOrDefault(x => x is NadekoOptionsAttribute))?.OptionType; + if (opt != null) + { + optHelpStr = HelpService.GetCommandOptionHelpList(opt); + } + + return new CommandJsonObject + { + Aliases = com.Aliases.Select(alias => Prefix + alias).ToArray(), + Description = com.RealSummary(_strings, ctx.Guild?.Id, Prefix), + Usage = com.RealRemarksArr(_strings, ctx.Guild?.Id, Prefix), + Submodule = com.Module.Name, + Module = com.Module.GetTopLevelModule().Name, + Options = optHelpStr, + Requirements = HelpService.GetCommandRequirements(com), + }; + }) + .ToList() + ); + + var readableData = JsonConvert.SerializeObject(cmdData, Formatting.Indented); + var uploadData = JsonConvert.SerializeObject(cmdData, Formatting.None); + + // for example https://nyc.digitaloceanspaces.com (without your space name) + var serviceUrl = Environment.GetEnvironmentVariable("do_spaces_address"); + + // generate spaces access key on https://cloud.digitalocean.com/account/api/tokens + // you will get 2 keys, first, shorter one is id, longer one is secret + var accessKey = Environment.GetEnvironmentVariable("do_access_key_id"); + var secretAcccessKey = Environment.GetEnvironmentVariable("do_access_key_secret"); + + // if all env vars are set, upload the unindented file (to save space) there + if (!(serviceUrl is null || accessKey is null || secretAcccessKey is null)) + { + var config = new AmazonS3Config {ServiceURL = serviceUrl}; - using var dlClient = new AmazonS3Client(accessKey, secretAcccessKey, config); - var oldVersionObject = await dlClient.GetObjectAsync(new GetObjectRequest() + using var dlClient = new AmazonS3Client(accessKey, secretAcccessKey, config); + var oldVersionObject = await dlClient.GetObjectAsync(new GetObjectRequest() + { + BucketName = "nadeko-pictures", + Key = "cmds/versions.json", + }); + + using (var client = new AmazonS3Client(accessKey, secretAcccessKey, config)) + { + await client.PutObjectAsync(new PutObjectRequest() { BucketName = "nadeko-pictures", - Key = "cmds/versions.json", + ContentType = "application/json", + ContentBody = uploadData, + // either use a path provided in the argument or the default one for public nadeko, other/cmds.json + Key = $"cmds/{StatsService.BotVersion}.json", + CannedACL = S3CannedACL.PublicRead }); - - using (var client = new AmazonS3Client(accessKey, secretAcccessKey, config)) - { - await client.PutObjectAsync(new PutObjectRequest() - { - BucketName = "nadeko-pictures", - ContentType = "application/json", - ContentBody = uploadData, - // either use a path provided in the argument or the default one for public nadeko, other/cmds.json - Key = $"cmds/{StatsService.BotVersion}.json", - CannedACL = S3CannedACL.PublicRead - }); - } - - using var ms = new MemoryStream(); - await oldVersionObject.ResponseStream.CopyToAsync(ms); - var versionListString = Encoding.UTF8.GetString(ms.ToArray()); - - var versionList = System.Text.Json.JsonSerializer.Deserialize>(versionListString); - if (!versionList.Contains(StatsService.BotVersion)) - { - // save the file with new version added - // versionList.Add(StatsService.BotVersion); - versionListString = System.Text.Json.JsonSerializer.Serialize( - versionList.Prepend(StatsService.BotVersion), - new JsonSerializerOptions() - { - WriteIndented = true - }); - - // upload the updated version list - using var client = new AmazonS3Client(accessKey, secretAcccessKey, config); - await client.PutObjectAsync(new PutObjectRequest() - { - BucketName = "nadeko-pictures", - ContentType = "application/json", - ContentBody = versionListString, - // either use a path provided in the argument or the default one for public nadeko, other/cmds.json - Key = "cmds/versions.json", - CannedACL = S3CannedACL.PublicRead - }); - } - else - { - Log.Warning("Version {Version} already exists in the version file. " + - "Did you forget to increment it?", StatsService.BotVersion); - } } - // also send the file, but indented one, to chat - using var rDataStream = new MemoryStream(Encoding.ASCII.GetBytes(readableData)); - await ctx.Channel.SendFileAsync(rDataStream, "cmds.json", GetText(strs.commandlist_regen)).ConfigureAwait(false); + using var ms = new MemoryStream(); + await oldVersionObject.ResponseStream.CopyToAsync(ms); + var versionListString = Encoding.UTF8.GetString(ms.ToArray()); + + var versionList = System.Text.Json.JsonSerializer.Deserialize>(versionListString); + if (!versionList.Contains(StatsService.BotVersion)) + { + // save the file with new version added + // versionList.Add(StatsService.BotVersion); + versionListString = System.Text.Json.JsonSerializer.Serialize( + versionList.Prepend(StatsService.BotVersion), + new JsonSerializerOptions() + { + WriteIndented = true + }); + + // upload the updated version list + using var client = new AmazonS3Client(accessKey, secretAcccessKey, config); + await client.PutObjectAsync(new PutObjectRequest() + { + BucketName = "nadeko-pictures", + ContentType = "application/json", + ContentBody = versionListString, + // either use a path provided in the argument or the default one for public nadeko, other/cmds.json + Key = "cmds/versions.json", + CannedACL = S3CannedACL.PublicRead + }); + } + else + { + Log.Warning("Version {Version} already exists in the version file. " + + "Did you forget to increment it?", StatsService.BotVersion); + } } - [NadekoCommand, Aliases] - public async Task Guide() - { - await ConfirmLocalizedAsync(strs.guide( - "https://nadeko.bot/commands", - "http://nadekobot.readthedocs.io/en/latest/")); - } - - [NadekoCommand, Aliases] - public async Task Donate() - { - await ReplyConfirmLocalizedAsync(strs.donate(PatreonUrl, PaypalUrl)); - } + // also send the file, but indented one, to chat + using var rDataStream = new MemoryStream(Encoding.ASCII.GetBytes(readableData)); + await ctx.Channel.SendFileAsync(rDataStream, "cmds.json", GetText(strs.commandlist_regen)).ConfigureAwait(false); } - public class CommandTextEqualityComparer : IEqualityComparer + [NadekoCommand, Aliases] + public async Task Guide() { - public bool Equals(CommandInfo x, CommandInfo y) => x.Aliases[0] == y.Aliases[0]; - - public int GetHashCode(CommandInfo obj) => obj.Aliases[0].GetHashCode(StringComparison.InvariantCulture); - + await ConfirmLocalizedAsync(strs.guide( + "https://nadeko.bot/commands", + "http://nadekobot.readthedocs.io/en/latest/")); } - internal class CommandJsonObject + [NadekoCommand, Aliases] + public async Task Donate() { - public string[] Aliases { get; set; } - public string Description { get; set; } - public string[] Usage { get; set; } - public string Submodule { get; set; } - public string Module { get; set; } - public List Options { get; set; } - public string[] Requirements { get; set; } + await ReplyConfirmLocalizedAsync(strs.donate(PatreonUrl, PaypalUrl)); } } + +public class CommandTextEqualityComparer : IEqualityComparer +{ + public bool Equals(CommandInfo x, CommandInfo y) => x.Aliases[0] == y.Aliases[0]; + + public int GetHashCode(CommandInfo obj) => obj.Aliases[0].GetHashCode(StringComparison.InvariantCulture); + +} + +internal class CommandJsonObject +{ + public string[] Aliases { get; set; } + public string Description { get; set; } + public string[] Usage { get; set; } + public string Submodule { get; set; } + public string Module { get; set; } + public List Options { get; set; } + public string[] Requirements { get; set; } +} \ No newline at end of file diff --git a/src/NadekoBot/Modules/Help/Services/HelpService.cs b/src/NadekoBot/Modules/Help/Services/HelpService.cs index d21e34f50..9ae47a0fe 100644 --- a/src/NadekoBot/Modules/Help/Services/HelpService.cs +++ b/src/NadekoBot/Modules/Help/Services/HelpService.cs @@ -1,183 +1,177 @@ using System.Threading.Tasks; using Discord; -using System; using Discord.Commands; using NadekoBot.Extensions; -using System.Linq; using NadekoBot.Common.Attributes; using NadekoBot.Common.ModuleBehaviors; using NadekoBot.Services; -using NadekoBot.Common; using CommandLine; -using System.Collections.Generic; using NadekoBot.Common.Replacements; using NadekoBot.Modules.Administration.Services; -using Serilog; -namespace NadekoBot.Modules.Help.Services +namespace NadekoBot.Modules.Help.Services; + +public class HelpService : ILateExecutor, INService { - public class HelpService : ILateExecutor, INService + private readonly CommandHandler _ch; + private readonly IBotStrings _strings; + private readonly DiscordPermOverrideService _dpos; + private readonly BotConfigService _bss; + private readonly IEmbedBuilderService _eb; + + public HelpService(CommandHandler ch, + IBotStrings strings, + DiscordPermOverrideService dpos, + BotConfigService bss, + IEmbedBuilderService eb) { - private readonly CommandHandler _ch; - private readonly IBotStrings _strings; - private readonly DiscordPermOverrideService _dpos; - private readonly BotConfigService _bss; - private readonly IEmbedBuilderService _eb; + _ch = ch; + _strings = strings; + _dpos = dpos; + _bss = bss; + _eb = eb; + } - public HelpService(CommandHandler ch, - IBotStrings strings, - DiscordPermOverrideService dpos, - BotConfigService bss, - IEmbedBuilderService eb) + public Task LateExecute(IGuild guild, IUserMessage msg) + { + var settings = _bss.Data; + if (guild is null) { - _ch = ch; - _strings = strings; - _dpos = dpos; - _bss = bss; - _eb = eb; - } - - public Task LateExecute(IGuild guild, IUserMessage msg) - { - var settings = _bss.Data; - if (guild is null) - { - if (string.IsNullOrWhiteSpace(settings.DmHelpText) || settings.DmHelpText == "-") - return Task.CompletedTask; + if (string.IsNullOrWhiteSpace(settings.DmHelpText) || settings.DmHelpText == "-") + return Task.CompletedTask; - // only send dm help text if it contains one of the keywords, if they're specified - // if they're not, then reply to every DM - if (settings.DmHelpTextKeywords.Any() && !settings.DmHelpTextKeywords.Any(k => msg.Content.Contains(k))) - return Task.CompletedTask; + // only send dm help text if it contains one of the keywords, if they're specified + // if they're not, then reply to every DM + if (settings.DmHelpTextKeywords.Any() && !settings.DmHelpTextKeywords.Any(k => msg.Content.Contains(k))) + return Task.CompletedTask; - var rep = new ReplacementBuilder() - .WithOverride("%prefix%", () => _bss.Data.Prefix) - .WithOverride("%bot.prefix%", () => _bss.Data.Prefix) - .WithUser(msg.Author) - .Build(); + var rep = new ReplacementBuilder() + .WithOverride("%prefix%", () => _bss.Data.Prefix) + .WithOverride("%bot.prefix%", () => _bss.Data.Prefix) + .WithUser(msg.Author) + .Build(); - var text = SmartText.CreateFrom(settings.DmHelpText); - text = rep.Replace(text); + var text = SmartText.CreateFrom(settings.DmHelpText); + text = rep.Replace(text); - return msg.Channel.SendAsync(text); - } - return Task.CompletedTask; + return msg.Channel.SendAsync(text); + } + return Task.CompletedTask; + } + + public IEmbedBuilder GetCommandHelp(CommandInfo com, IGuild guild) + { + var prefix = _ch.GetPrefix(guild); + + var str = $"**`{prefix + com.Aliases.First()}`**"; + var alias = com.Aliases.Skip(1).FirstOrDefault(); + if (alias != null) + str += $" **/ `{prefix + alias}`**"; + var em = _eb.Create() + .AddField(str, $"{com.RealSummary(_strings, guild?.Id, prefix)}", true); + + _dpos.TryGetOverrides(guild?.Id ?? 0, com.Name, out var overrides); + var reqs = GetCommandRequirements(com, overrides); + if(reqs.Any()) + { + em.AddField(GetText(strs.requires, guild), string.Join("\n", reqs)); } - public IEmbedBuilder GetCommandHelp(CommandInfo com, IGuild guild) + em + .AddField(_strings.GetText(strs.usage), + string.Join("\n", Array.ConvertAll(com.RealRemarksArr(_strings, guild?.Id, prefix), + arg => Format.Code(arg))), + false) + .WithFooter(GetText(strs.module(com.Module.GetTopLevelModule().Name), guild)) + .WithOkColor(); + + var opt = ((NadekoOptionsAttribute)com.Attributes.FirstOrDefault(x => x is NadekoOptionsAttribute))?.OptionType; + if (opt != null) { - var prefix = _ch.GetPrefix(guild); + var hs = GetCommandOptionHelp(opt); + if(!string.IsNullOrWhiteSpace(hs)) + em.AddField(GetText(strs.options, guild), hs, false); + } - var str = $"**`{prefix + com.Aliases.First()}`**"; - var alias = com.Aliases.Skip(1).FirstOrDefault(); - if (alias != null) - str += $" **/ `{prefix + alias}`**"; - var em = _eb.Create() - .AddField(str, $"{com.RealSummary(_strings, guild?.Id, prefix)}", true); + return em; + } - _dpos.TryGetOverrides(guild?.Id ?? 0, com.Name, out var overrides); - var reqs = GetCommandRequirements(com, overrides); - if(reqs.Any()) + public static string GetCommandOptionHelp(Type opt) + { + var strs = GetCommandOptionHelpList(opt); + + return string.Join("\n", strs); + } + + public static List GetCommandOptionHelpList(Type opt) + { + var strs = opt.GetProperties() + .Select(x => x.GetCustomAttributes(true).FirstOrDefault(a => a is OptionAttribute)) + .Where(x => x != null) + .Cast() + .Select(x => { - em.AddField(GetText(strs.requires, guild), string.Join("\n", reqs)); - } + var toReturn = $"`--{x.LongName}`"; - em - .AddField(_strings.GetText(strs.usage), - string.Join("\n", Array.ConvertAll(com.RealRemarksArr(_strings, guild?.Id, prefix), - arg => Format.Code(arg))), - false) - .WithFooter(GetText(strs.module(com.Module.GetTopLevelModule().Name), guild)) - .WithOkColor(); + if (!string.IsNullOrWhiteSpace(x.ShortName)) + toReturn += $" (`-{x.ShortName}`)"; - var opt = ((NadekoOptionsAttribute)com.Attributes.FirstOrDefault(x => x is NadekoOptionsAttribute))?.OptionType; - if (opt != null) - { - var hs = GetCommandOptionHelp(opt); - if(!string.IsNullOrWhiteSpace(hs)) - em.AddField(GetText(strs.options, guild), hs, false); - } + toReturn += $" {x.HelpText} "; + return toReturn; + }) + .ToList(); - return em; - } - - public static string GetCommandOptionHelp(Type opt) - { - var strs = GetCommandOptionHelpList(opt); - - return string.Join("\n", strs); - } - - public static List GetCommandOptionHelpList(Type opt) - { - var strs = opt.GetProperties() - .Select(x => x.GetCustomAttributes(true).FirstOrDefault(a => a is OptionAttribute)) - .Where(x => x != null) - .Cast() - .Select(x => - { - var toReturn = $"`--{x.LongName}`"; - - if (!string.IsNullOrWhiteSpace(x.ShortName)) - toReturn += $" (`-{x.ShortName}`)"; - - toReturn += $" {x.HelpText} "; - return toReturn; - }) - .ToList(); - - return strs; - } + return strs; + } - public static string[] GetCommandRequirements(CommandInfo cmd, GuildPerm? overrides = null) - { - var toReturn = new List(); + public static string[] GetCommandRequirements(CommandInfo cmd, GuildPerm? overrides = null) + { + var toReturn = new List(); - if(cmd.Preconditions.Any(x => x is OwnerOnlyAttribute)) - toReturn.Add("Bot Owner Only"); + if(cmd.Preconditions.Any(x => x is OwnerOnlyAttribute)) + toReturn.Add("Bot Owner Only"); - var userPerm = (UserPermAttribute)cmd.Preconditions - .FirstOrDefault(ca => ca is UserPermAttribute); + var userPerm = (UserPermAttribute)cmd.Preconditions + .FirstOrDefault(ca => ca is UserPermAttribute); - string userPermString = string.Empty; - if (userPerm is not null) - { - if (userPerm.UserPermissionAttribute.ChannelPermission is ChannelPermission cPerm) - userPermString = GetPreconditionString((ChannelPerm) cPerm); - if (userPerm.UserPermissionAttribute.GuildPermission is GuildPermission gPerm) - userPermString = GetPreconditionString((GuildPerm) gPerm); - } + string userPermString = string.Empty; + if (userPerm is not null) + { + if (userPerm.UserPermissionAttribute.ChannelPermission is ChannelPermission cPerm) + userPermString = GetPreconditionString((ChannelPerm) cPerm); + if (userPerm.UserPermissionAttribute.GuildPermission is GuildPermission gPerm) + userPermString = GetPreconditionString((GuildPerm) gPerm); + } - if (overrides is null) - { - if(!string.IsNullOrWhiteSpace(userPermString)) - toReturn.Add(userPermString); - } - else - { - if(!string.IsNullOrWhiteSpace(userPermString)) - toReturn.Add(Format.Strikethrough(userPermString)); + if (overrides is null) + { + if(!string.IsNullOrWhiteSpace(userPermString)) + toReturn.Add(userPermString); + } + else + { + if(!string.IsNullOrWhiteSpace(userPermString)) + toReturn.Add(Format.Strikethrough(userPermString)); - toReturn.Add(GetPreconditionString(overrides.Value)); - } - - return toReturn.ToArray(); + toReturn.Add(GetPreconditionString(overrides.Value)); } - public static string GetPreconditionString(ChannelPerm perm) - { - return (perm.ToString() + " Channel Permission") - .Replace("Guild", "Server", StringComparison.InvariantCulture); - } - - public static string GetPreconditionString(GuildPerm perm) - { - return (perm.ToString() + " Server Permission") - .Replace("Guild", "Server", StringComparison.InvariantCulture); - } - - private string GetText(LocStr str, IGuild guild, params object[] replacements) => - _strings.GetText(str, guild?.Id); + return toReturn.ToArray(); } -} + + public static string GetPreconditionString(ChannelPerm perm) + { + return (perm.ToString() + " Channel Permission") + .Replace("Guild", "Server", StringComparison.InvariantCulture); + } + + public static string GetPreconditionString(GuildPerm perm) + { + return (perm.ToString() + " Server Permission") + .Replace("Guild", "Server", StringComparison.InvariantCulture); + } + + private string GetText(LocStr str, IGuild guild, params object[] replacements) => + _strings.GetText(str, guild?.Id); +} \ No newline at end of file diff --git a/src/NadekoBot/Modules/Music/Common/ICachableTrackData.cs b/src/NadekoBot/Modules/Music/Common/ICachableTrackData.cs index 17e3e79d7..a75646869 100644 --- a/src/NadekoBot/Modules/Music/Common/ICachableTrackData.cs +++ b/src/NadekoBot/Modules/Music/Common/ICachableTrackData.cs @@ -1,14 +1,11 @@ -using System; +namespace NadekoBot.Modules.Music; -namespace NadekoBot.Modules.Music +public interface ICachableTrackData { - public interface ICachableTrackData - { - string Id { get; set; } - string Url { get; set; } - string Thumbnail { get; set; } - public TimeSpan Duration { get; } - MusicPlatform Platform { get; set; } - string Title { get; set; } - } + string Id { get; set; } + string Url { get; set; } + string Thumbnail { get; set; } + public TimeSpan Duration { get; } + MusicPlatform Platform { get; set; } + string Title { get; set; } } \ No newline at end of file diff --git a/src/NadekoBot/Modules/Music/Common/ILocalTrackResolver.cs b/src/NadekoBot/Modules/Music/Common/ILocalTrackResolver.cs index 0d826b716..18f26d457 100644 --- a/src/NadekoBot/Modules/Music/Common/ILocalTrackResolver.cs +++ b/src/NadekoBot/Modules/Music/Common/ILocalTrackResolver.cs @@ -1,9 +1,6 @@ -using System.Collections.Generic; +namespace NadekoBot.Modules.Music; -namespace NadekoBot.Modules.Music +public interface ILocalTrackResolver : IPlatformQueryResolver { - public interface ILocalTrackResolver : IPlatformQueryResolver - { - IAsyncEnumerable ResolveDirectoryAsync(string dirPath); - } + IAsyncEnumerable ResolveDirectoryAsync(string dirPath); } \ No newline at end of file diff --git a/src/NadekoBot/Modules/Music/Common/IMusicPlayer.cs b/src/NadekoBot/Modules/Music/Common/IMusicPlayer.cs index 86e8a4220..1fa2a07a1 100644 --- a/src/NadekoBot/Modules/Music/Common/IMusicPlayer.cs +++ b/src/NadekoBot/Modules/Music/Common/IMusicPlayer.cs @@ -1,39 +1,36 @@ -using System; -using System.Collections.Generic; -using System.Threading.Tasks; +using System.Threading.Tasks; using NadekoBot.Services.Database.Models; #nullable enable -namespace NadekoBot.Modules.Music +namespace NadekoBot.Modules.Music; + +public interface IMusicPlayer : IDisposable { - public interface IMusicPlayer : IDisposable - { - float Volume { get; } - bool IsPaused { get; } - bool IsStopped { get; } - bool IsKilled { get; } - int CurrentIndex { get; } - public PlayerRepeatType Repeat { get; } + float Volume { get; } + bool IsPaused { get; } + bool IsStopped { get; } + bool IsKilled { get; } + int CurrentIndex { get; } + public PlayerRepeatType Repeat { get; } - void Stop(); - void Clear(); - IReadOnlyCollection GetQueuedTracks(); - IQueuedTrackInfo? GetCurrentTrack(out int index); - void Next(); - bool MoveTo(int index); - void SetVolume(int newVolume); + void Stop(); + void Clear(); + IReadOnlyCollection GetQueuedTracks(); + IQueuedTrackInfo? GetCurrentTrack(out int index); + void Next(); + bool MoveTo(int index); + void SetVolume(int newVolume); - void Kill(); - bool TryRemoveTrackAt(int index, out IQueuedTrackInfo? trackInfo); + void Kill(); + bool TryRemoveTrackAt(int index, out IQueuedTrackInfo? trackInfo); - Task<(IQueuedTrackInfo? QueuedTrack, int Index)> TryEnqueueTrackAsync(string query, string queuer, bool asNext, MusicPlatform? forcePlatform = null); - Task EnqueueManyAsync(IEnumerable<(string Query, MusicPlatform Platform)> queries, string queuer); - bool TogglePause(); - IQueuedTrackInfo? MoveTrack(int from, int to); - void EnqueueTrack(ITrackInfo track, string queuer); - void EnqueueTracks(IEnumerable tracks, string queuer); - void SetRepeat(PlayerRepeatType type); - void ShuffleQueue(); - } + Task<(IQueuedTrackInfo? QueuedTrack, int Index)> TryEnqueueTrackAsync(string query, string queuer, bool asNext, MusicPlatform? forcePlatform = null); + Task EnqueueManyAsync(IEnumerable<(string Query, MusicPlatform Platform)> queries, string queuer); + bool TogglePause(); + IQueuedTrackInfo? MoveTrack(int from, int to); + void EnqueueTrack(ITrackInfo track, string queuer); + void EnqueueTracks(IEnumerable tracks, string queuer); + void SetRepeat(PlayerRepeatType type); + void ShuffleQueue(); } \ No newline at end of file diff --git a/src/NadekoBot/Modules/Music/Common/IMusicQueue.cs b/src/NadekoBot/Modules/Music/Common/IMusicQueue.cs index f6ff3c553..1a83c0f95 100644 --- a/src/NadekoBot/Modules/Music/Common/IMusicQueue.cs +++ b/src/NadekoBot/Modules/Music/Common/IMusicQueue.cs @@ -1,27 +1,23 @@ #nullable enable -using System; -using System.Collections.Generic; +namespace NadekoBot.Modules.Music; -namespace NadekoBot.Modules.Music +public interface IMusicQueue { - public interface IMusicQueue - { - IQueuedTrackInfo Enqueue(ITrackInfo trackInfo, string queuer, out int index); - IQueuedTrackInfo EnqueueNext(ITrackInfo song, string queuer, out int index); + IQueuedTrackInfo Enqueue(ITrackInfo trackInfo, string queuer, out int index); + IQueuedTrackInfo EnqueueNext(ITrackInfo song, string queuer, out int index); - void EnqueueMany(IEnumerable tracks, string queuer); + void EnqueueMany(IEnumerable tracks, string queuer); - public IReadOnlyCollection List(); - IQueuedTrackInfo? GetCurrent(out int index); - void Advance(); - void Clear(); - bool SetIndex(int index); - bool TryRemoveAt(int index, out IQueuedTrackInfo? trackInfo, out bool isCurrent); - int Index { get; } - int Count { get; } - void RemoveCurrent(); - IQueuedTrackInfo? MoveTrack(int from, int to); - void Shuffle(Random rng); - bool IsLast(); - } + public IReadOnlyCollection List(); + IQueuedTrackInfo? GetCurrent(out int index); + void Advance(); + void Clear(); + bool SetIndex(int index); + bool TryRemoveAt(int index, out IQueuedTrackInfo? trackInfo, out bool isCurrent); + int Index { get; } + int Count { get; } + void RemoveCurrent(); + IQueuedTrackInfo? MoveTrack(int from, int to); + void Shuffle(Random rng); + bool IsLast(); } \ No newline at end of file diff --git a/src/NadekoBot/Modules/Music/Common/IPlatformQueryResolver.cs b/src/NadekoBot/Modules/Music/Common/IPlatformQueryResolver.cs index 1867762f8..1e7039438 100644 --- a/src/NadekoBot/Modules/Music/Common/IPlatformQueryResolver.cs +++ b/src/NadekoBot/Modules/Music/Common/IPlatformQueryResolver.cs @@ -1,10 +1,9 @@ #nullable enable using System.Threading.Tasks; -namespace NadekoBot.Modules.Music +namespace NadekoBot.Modules.Music; + +public interface IPlatformQueryResolver { - public interface IPlatformQueryResolver - { - Task ResolveByQueryAsync(string query); - } + Task ResolveByQueryAsync(string query); } \ No newline at end of file diff --git a/src/NadekoBot/Modules/Music/Common/IQueuedTrackInfo.cs b/src/NadekoBot/Modules/Music/Common/IQueuedTrackInfo.cs index 7df45a9dc..c5fd569d2 100644 --- a/src/NadekoBot/Modules/Music/Common/IQueuedTrackInfo.cs +++ b/src/NadekoBot/Modules/Music/Common/IQueuedTrackInfo.cs @@ -1,9 +1,8 @@ -namespace NadekoBot.Modules.Music -{ - public interface IQueuedTrackInfo : ITrackInfo - { - public ITrackInfo TrackInfo { get; } +namespace NadekoBot.Modules.Music; - public string Queuer { get; } - } +public interface IQueuedTrackInfo : ITrackInfo +{ + public ITrackInfo TrackInfo { get; } + + public string Queuer { get; } } \ No newline at end of file diff --git a/src/NadekoBot/Modules/Music/Common/IRadioResolver.cs b/src/NadekoBot/Modules/Music/Common/IRadioResolver.cs index cf28a9e06..7a896d9d9 100644 --- a/src/NadekoBot/Modules/Music/Common/IRadioResolver.cs +++ b/src/NadekoBot/Modules/Music/Common/IRadioResolver.cs @@ -1,7 +1,6 @@ -namespace NadekoBot.Modules.Music +namespace NadekoBot.Modules.Music; + +public interface IRadioResolver : IPlatformQueryResolver { - public interface IRadioResolver : IPlatformQueryResolver - { - } } \ No newline at end of file diff --git a/src/NadekoBot/Modules/Music/Common/ISoundcloudResolver.cs b/src/NadekoBot/Modules/Music/Common/ISoundcloudResolver.cs index 1e4b4d739..90d8ac35b 100644 --- a/src/NadekoBot/Modules/Music/Common/ISoundcloudResolver.cs +++ b/src/NadekoBot/Modules/Music/Common/ISoundcloudResolver.cs @@ -1,10 +1,7 @@ -using System.Collections.Generic; +namespace NadekoBot.Modules.Music; -namespace NadekoBot.Modules.Music +public interface ISoundcloudResolver : IPlatformQueryResolver { - public interface ISoundcloudResolver : IPlatformQueryResolver - { - bool IsSoundCloudLink(string url); - IAsyncEnumerable ResolvePlaylistAsync(string playlist); - } + bool IsSoundCloudLink(string url); + IAsyncEnumerable ResolvePlaylistAsync(string playlist); } \ No newline at end of file diff --git a/src/NadekoBot/Modules/Music/Common/ITrackCacher.cs b/src/NadekoBot/Modules/Music/Common/ITrackCacher.cs index 0d14ea65f..d1f1e0702 100644 --- a/src/NadekoBot/Modules/Music/Common/ITrackCacher.cs +++ b/src/NadekoBot/Modules/Music/Common/ITrackCacher.cs @@ -1,26 +1,23 @@ #nullable enable -using System; -using System.Collections.Generic; using System.Threading.Tasks; -namespace NadekoBot.Modules.Music -{ - public interface ITrackCacher - { - Task GetOrCreateStreamLink( - string id, - MusicPlatform platform, - Func> streamUrlFactory - ); +namespace NadekoBot.Modules.Music; - Task CacheTrackDataAsync(ICachableTrackData data); - Task GetCachedDataByIdAsync(string id, MusicPlatform platform); - Task GetCachedDataByQueryAsync(string query, MusicPlatform platform); - Task CacheTrackDataByQueryAsync(string query, ICachableTrackData data); - Task CacheStreamUrlAsync(string id, MusicPlatform platform, string url, TimeSpan expiry); - Task> GetPlaylistTrackIdsAsync(string playlistId, MusicPlatform platform); - Task CachePlaylistTrackIdsAsync(string playlistId, MusicPlatform platform, IEnumerable ids); - Task CachePlaylistIdByQueryAsync(string query, MusicPlatform platform, string playlistId); - Task GetPlaylistIdByQueryAsync(string query, MusicPlatform platform); - } +public interface ITrackCacher +{ + Task GetOrCreateStreamLink( + string id, + MusicPlatform platform, + Func> streamUrlFactory + ); + + Task CacheTrackDataAsync(ICachableTrackData data); + Task GetCachedDataByIdAsync(string id, MusicPlatform platform); + Task GetCachedDataByQueryAsync(string query, MusicPlatform platform); + Task CacheTrackDataByQueryAsync(string query, ICachableTrackData data); + Task CacheStreamUrlAsync(string id, MusicPlatform platform, string url, TimeSpan expiry); + Task> GetPlaylistTrackIdsAsync(string playlistId, MusicPlatform platform); + Task CachePlaylistTrackIdsAsync(string playlistId, MusicPlatform platform, IEnumerable ids); + Task CachePlaylistIdByQueryAsync(string query, MusicPlatform platform, string playlistId); + Task GetPlaylistIdByQueryAsync(string query, MusicPlatform platform); } \ No newline at end of file diff --git a/src/NadekoBot/Modules/Music/Common/ITrackInfo.cs b/src/NadekoBot/Modules/Music/Common/ITrackInfo.cs index b22d98aa0..6db9acbcc 100644 --- a/src/NadekoBot/Modules/Music/Common/ITrackInfo.cs +++ b/src/NadekoBot/Modules/Music/Common/ITrackInfo.cs @@ -1,16 +1,14 @@ #nullable enable -using System; using System.Threading.Tasks; -namespace NadekoBot.Modules.Music +namespace NadekoBot.Modules.Music; + +public interface ITrackInfo { - public interface ITrackInfo - { - public string Title { get; } - public string Url { get; } - public string Thumbnail { get; } - public TimeSpan Duration { get; } - public MusicPlatform Platform { get; } - public ValueTask GetStreamUrl(); - } + public string Title { get; } + public string Url { get; } + public string Thumbnail { get; } + public TimeSpan Duration { get; } + public MusicPlatform Platform { get; } + public ValueTask GetStreamUrl(); } \ No newline at end of file diff --git a/src/NadekoBot/Modules/Music/Common/ITrackResolveProvider.cs b/src/NadekoBot/Modules/Music/Common/ITrackResolveProvider.cs index 5df158184..abb7b5ff3 100644 --- a/src/NadekoBot/Modules/Music/Common/ITrackResolveProvider.cs +++ b/src/NadekoBot/Modules/Music/Common/ITrackResolveProvider.cs @@ -1,10 +1,9 @@ #nullable enable using System.Threading.Tasks; -namespace NadekoBot.Modules.Music +namespace NadekoBot.Modules.Music; + +public interface ITrackResolveProvider { - public interface ITrackResolveProvider - { - Task QuerySongAsync(string query, MusicPlatform? forcePlatform); - } + Task QuerySongAsync(string query, MusicPlatform? forcePlatform); } \ No newline at end of file diff --git a/src/NadekoBot/Modules/Music/Common/IVoiceProxy.cs b/src/NadekoBot/Modules/Music/Common/IVoiceProxy.cs index c5a8262da..34e327b6e 100644 --- a/src/NadekoBot/Modules/Music/Common/IVoiceProxy.cs +++ b/src/NadekoBot/Modules/Music/Common/IVoiceProxy.cs @@ -1,17 +1,15 @@ -using System; -using System.Threading.Tasks; +using System.Threading.Tasks; using Ayu.Discord.Voice; -namespace NadekoBot.Modules.Music +namespace NadekoBot.Modules.Music; + +public interface IVoiceProxy { - public interface IVoiceProxy - { - VoiceProxy.VoiceProxyState State { get; } - public bool SendPcmFrame(VoiceClient vc, Span data, int length); - public void SetGateway(VoiceGateway gateway); - Task StartSpeakingAsync(); - Task StopSpeakingAsync(); - public Task StartGateway(); - Task StopGateway(); - } + VoiceProxy.VoiceProxyState State { get; } + public bool SendPcmFrame(VoiceClient vc, Span data, int length); + public void SetGateway(VoiceGateway gateway); + Task StartSpeakingAsync(); + Task StopSpeakingAsync(); + public Task StartGateway(); + Task StopGateway(); } \ No newline at end of file diff --git a/src/NadekoBot/Modules/Music/Common/IYoutubeResolver.cs b/src/NadekoBot/Modules/Music/Common/IYoutubeResolver.cs index 46486af68..9f6622909 100644 --- a/src/NadekoBot/Modules/Music/Common/IYoutubeResolver.cs +++ b/src/NadekoBot/Modules/Music/Common/IYoutubeResolver.cs @@ -1,15 +1,13 @@ #nullable enable -using System.Collections.Generic; using System.Text.RegularExpressions; using System.Threading.Tasks; -namespace NadekoBot.Modules.Music +namespace NadekoBot.Modules.Music; + +public interface IYoutubeResolver : IPlatformQueryResolver { - public interface IYoutubeResolver : IPlatformQueryResolver - { - public Regex YtVideoIdRegex { get; } - public Task ResolveByIdAsync(string id); - IAsyncEnumerable ResolveTracksFromPlaylistAsync(string query); - Task ResolveByQueryAsync(string query, bool tryExtractingId); - } + public Regex YtVideoIdRegex { get; } + public Task ResolveByIdAsync(string id); + IAsyncEnumerable ResolveTracksFromPlaylistAsync(string query); + Task ResolveByQueryAsync(string query, bool tryExtractingId); } \ No newline at end of file diff --git a/src/NadekoBot/Modules/Music/Common/Impl/CachableTrackData.cs b/src/NadekoBot/Modules/Music/Common/Impl/CachableTrackData.cs index 00f2566b4..7d60be986 100644 --- a/src/NadekoBot/Modules/Music/Common/Impl/CachableTrackData.cs +++ b/src/NadekoBot/Modules/Music/Common/Impl/CachableTrackData.cs @@ -1,17 +1,15 @@ -using System; -using System.Text.Json.Serialization; +using System.Text.Json.Serialization; -namespace NadekoBot.Modules.Music +namespace NadekoBot.Modules.Music; + +public sealed class CachableTrackData : ICachableTrackData { - public sealed class CachableTrackData : ICachableTrackData - { - public string Title { get; set; } = string.Empty; - public string Id { get; set; } = string.Empty; - public string Url { get; set; } = string.Empty; - public string Thumbnail { get; set; } = string.Empty; - public double TotalDurationMs { get; set; } - [JsonIgnore] - public TimeSpan Duration => TimeSpan.FromMilliseconds(TotalDurationMs); - public MusicPlatform Platform { get; set; } - } + public string Title { get; set; } = string.Empty; + public string Id { get; set; } = string.Empty; + public string Url { get; set; } = string.Empty; + public string Thumbnail { get; set; } = string.Empty; + public double TotalDurationMs { get; set; } + [JsonIgnore] + public TimeSpan Duration => TimeSpan.FromMilliseconds(TotalDurationMs); + public MusicPlatform Platform { get; set; } } \ No newline at end of file diff --git a/src/NadekoBot/Modules/Music/Common/Impl/MultimediaTimer.cs b/src/NadekoBot/Modules/Music/Common/Impl/MultimediaTimer.cs index 541aa3195..83c5983cf 100644 --- a/src/NadekoBot/Modules/Music/Common/Impl/MultimediaTimer.cs +++ b/src/NadekoBot/Modules/Music/Common/Impl/MultimediaTimer.cs @@ -1,92 +1,89 @@ -using System; -using System.Runtime.InteropServices; -using Serilog; +using System.Runtime.InteropServices; -namespace NadekoBot.Modules.Music.Common +namespace NadekoBot.Modules.Music.Common; + +public sealed class MultimediaTimer : IDisposable { - public sealed class MultimediaTimer : IDisposable + private delegate void LpTimeProcDelegate(uint uTimerID, uint uMsg, int dwUser, int dw1, int dw2); + + /// + /// The timeSetEvent function starts a specified timer event. The multimedia timer runs in its own thread. + /// After the event is activated, it calls the specified callback function or sets or pulses the specified + /// event object. + /// + /// + /// Event delay, in milliseconds. If this value is not in the range of the minimum and + /// maximum event delays supported by the timer, the function returns an error. + /// + /// + /// Resolution of the timer event, in milliseconds. The resolution increases with + /// smaller values; a resolution of 0 indicates periodic events should occur with the greatest possible accuracy. + /// To reduce system overhead, however, you should use the maximum value appropriate for your application. + /// + /// + /// Pointer to a callback function that is called once upon expiration of a single event or periodically upon + /// expiration of periodic events. If fuEvent specifies the TIME_CALLBACK_EVENT_SET or TIME_CALLBACK_EVENT_PULSE + /// flag, then the lpTimeProc parameter is interpreted as a handle to an event object. The event will be set or + /// pulsed upon completion of a single event or periodically upon completion of periodic events. + /// For any other value of fuEvent, the lpTimeProc parameter is a pointer to a callback function of type + /// LPTIMECALLBACK. + /// + /// User-supplied callback data. + /// + /// Timer event type. This parameter may include one of the following values. + [DllImport("Winmm.dll")] + private static extern uint timeSetEvent( + uint uDelay, + uint uResolution, + LpTimeProcDelegate lpTimeProc, + int dwUser, + TimerMode fuEvent + ); + + private enum TimerMode { - private delegate void LpTimeProcDelegate(uint uTimerID, uint uMsg, int dwUser, int dw1, int dw2); - - /// - /// The timeSetEvent function starts a specified timer event. The multimedia timer runs in its own thread. - /// After the event is activated, it calls the specified callback function or sets or pulses the specified - /// event object. - /// - /// - /// Event delay, in milliseconds. If this value is not in the range of the minimum and - /// maximum event delays supported by the timer, the function returns an error. - /// - /// - /// Resolution of the timer event, in milliseconds. The resolution increases with - /// smaller values; a resolution of 0 indicates periodic events should occur with the greatest possible accuracy. - /// To reduce system overhead, however, you should use the maximum value appropriate for your application. - /// - /// - /// Pointer to a callback function that is called once upon expiration of a single event or periodically upon - /// expiration of periodic events. If fuEvent specifies the TIME_CALLBACK_EVENT_SET or TIME_CALLBACK_EVENT_PULSE - /// flag, then the lpTimeProc parameter is interpreted as a handle to an event object. The event will be set or - /// pulsed upon completion of a single event or periodically upon completion of periodic events. - /// For any other value of fuEvent, the lpTimeProc parameter is a pointer to a callback function of type - /// LPTIMECALLBACK. - /// - /// User-supplied callback data. - /// - /// Timer event type. This parameter may include one of the following values. - [DllImport("Winmm.dll")] - private static extern uint timeSetEvent( - uint uDelay, - uint uResolution, - LpTimeProcDelegate lpTimeProc, - int dwUser, - TimerMode fuEvent - ); - - private enum TimerMode - { - OneShot, - Periodic, - } + OneShot, + Periodic, + } - /// - /// The timeKillEvent function cancels a specified timer event. - /// - /// - /// Identifier of the timer event to cancel. - /// This identifier was returned by the timeSetEvent function when the timer event was set up. - /// - /// Returns TIMERR_NOERROR if successful or MMSYSERR_INVALPARAM if the specified timer event does not exist. - [DllImport("Winmm.dll")] - private static extern int timeKillEvent( - uint uTimerID - ); + /// + /// The timeKillEvent function cancels a specified timer event. + /// + /// + /// Identifier of the timer event to cancel. + /// This identifier was returned by the timeSetEvent function when the timer event was set up. + /// + /// Returns TIMERR_NOERROR if successful or MMSYSERR_INVALPARAM if the specified timer event does not exist. + [DllImport("Winmm.dll")] + private static extern int timeKillEvent( + uint uTimerID + ); - private LpTimeProcDelegate _lpTimeProc; - private readonly uint _eventId; - private readonly Action _callback; - private readonly object _state; + private LpTimeProcDelegate _lpTimeProc; + private readonly uint _eventId; + private readonly Action _callback; + private readonly object _state; - public MultimediaTimer(Action callback, object state, int period) - { - if (period <= 0) - throw new ArgumentOutOfRangeException(nameof(period), "Period must be greater than 0"); + public MultimediaTimer(Action callback, object state, int period) + { + if (period <= 0) + throw new ArgumentOutOfRangeException(nameof(period), "Period must be greater than 0"); - _callback = callback; - _state = state; + _callback = callback; + _state = state; - _lpTimeProc = CallbackInternal; - _eventId = timeSetEvent((uint)period, 1, _lpTimeProc, 0, TimerMode.Periodic); - } + _lpTimeProc = CallbackInternal; + _eventId = timeSetEvent((uint)period, 1, _lpTimeProc, 0, TimerMode.Periodic); + } - private void CallbackInternal(uint uTimerId, uint uMsg, int dwUser, int dw1, int dw2) - { - _callback(_state); - } + private void CallbackInternal(uint uTimerId, uint uMsg, int dwUser, int dw1, int dw2) + { + _callback(_state); + } - public void Dispose() - { - _lpTimeProc = default; - timeKillEvent(_eventId); - } + public void Dispose() + { + _lpTimeProc = default; + timeKillEvent(_eventId); } } \ No newline at end of file diff --git a/src/NadekoBot/Modules/Music/Common/Impl/MusicExtensions.cs b/src/NadekoBot/Modules/Music/Common/Impl/MusicExtensions.cs index de9480a6e..5b48f4c4b 100644 --- a/src/NadekoBot/Modules/Music/Common/Impl/MusicExtensions.cs +++ b/src/NadekoBot/Modules/Music/Common/Impl/MusicExtensions.cs @@ -1,61 +1,59 @@ -using System; -using Discord; +using Discord; using NadekoBot.Extensions; -namespace NadekoBot.Modules.Music +namespace NadekoBot.Modules.Music; + +public static class MusicExtensions { - public static class MusicExtensions + public static string PrettyTotalTime(this IMusicPlayer mp) { - public static string PrettyTotalTime(this IMusicPlayer mp) + long sum = 0; + foreach (var track in mp.GetQueuedTracks()) { - long sum = 0; - foreach (var track in mp.GetQueuedTracks()) - { - if (track.Duration == TimeSpan.MaxValue) - return "∞"; + if (track.Duration == TimeSpan.MaxValue) + return "∞"; - sum += track.Duration.Ticks; - } - - var total = new TimeSpan(sum); - - return total.ToString(@"hh\:mm\:ss"); + sum += track.Duration.Ticks; } - public static string PrettyVolume(this IMusicPlayer mp) - => $"🔉 {(int) (mp.Volume * 100)}%"; + var total = new TimeSpan(sum); + + return total.ToString(@"hh\:mm\:ss"); + } + + public static string PrettyVolume(this IMusicPlayer mp) + => $"🔉 {(int) (mp.Volume * 100)}%"; - public static string PrettyName(this ITrackInfo trackInfo) - => $"**[{trackInfo.Title.TrimTo(60).Replace("[", "\\[").Replace("]", "\\]")}]({trackInfo.Url.TrimTo(50, true)})**"; + public static string PrettyName(this ITrackInfo trackInfo) + => $"**[{trackInfo.Title.TrimTo(60).Replace("[", "\\[").Replace("]", "\\]")}]({trackInfo.Url.TrimTo(50, true)})**"; - public static string PrettyInfo(this IQueuedTrackInfo trackInfo) - => $"{trackInfo.PrettyTotalTime()} | {trackInfo.Platform} | {trackInfo.Queuer}"; + public static string PrettyInfo(this IQueuedTrackInfo trackInfo) + => $"{trackInfo.PrettyTotalTime()} | {trackInfo.Platform} | {trackInfo.Queuer}"; - public static string PrettyFullName(this IQueuedTrackInfo trackInfo) - => $@"{trackInfo.PrettyName()} + public static string PrettyFullName(this IQueuedTrackInfo trackInfo) + => $@"{trackInfo.PrettyName()} `{trackInfo.PrettyTotalTime()} | {trackInfo.Platform} | {Format.Sanitize(trackInfo.Queuer.TrimTo(15))}`"; - public static string PrettyTotalTime(this ITrackInfo trackInfo) - { - if (trackInfo.Duration == TimeSpan.Zero) - return "(?)"; - if (trackInfo.Duration == TimeSpan.MaxValue) - return "∞"; - if (trackInfo.Duration.TotalHours >= 1) - return trackInfo.Duration.ToString(@"hh\:mm\:ss"); + public static string PrettyTotalTime(this ITrackInfo trackInfo) + { + if (trackInfo.Duration == TimeSpan.Zero) + return "(?)"; + if (trackInfo.Duration == TimeSpan.MaxValue) + return "∞"; + if (trackInfo.Duration.TotalHours >= 1) + return trackInfo.Duration.ToString(@"hh\:mm\:ss"); - return trackInfo.Duration.ToString(@"mm\:ss"); - } - - public static ICachableTrackData ToCachedData(this ITrackInfo trackInfo, string id) - => new CachableTrackData() - { - TotalDurationMs = trackInfo.Duration.TotalMilliseconds, - Id = id, - Thumbnail = trackInfo.Thumbnail, - Url = trackInfo.Url, - Platform = trackInfo.Platform, - Title = trackInfo.Title - }; + return trackInfo.Duration.ToString(@"mm\:ss"); } + + public static ICachableTrackData ToCachedData(this ITrackInfo trackInfo, string id) + => new CachableTrackData() + { + TotalDurationMs = trackInfo.Duration.TotalMilliseconds, + Id = id, + Thumbnail = trackInfo.Thumbnail, + Url = trackInfo.Url, + Platform = trackInfo.Platform, + Title = trackInfo.Title + }; } \ No newline at end of file diff --git a/src/NadekoBot/Modules/Music/Common/Impl/MusicPlatform.cs b/src/NadekoBot/Modules/Music/Common/Impl/MusicPlatform.cs index e8f442a70..3005fb467 100644 --- a/src/NadekoBot/Modules/Music/Common/Impl/MusicPlatform.cs +++ b/src/NadekoBot/Modules/Music/Common/Impl/MusicPlatform.cs @@ -1,10 +1,9 @@ -namespace NadekoBot.Modules.Music +namespace NadekoBot.Modules.Music; + +public enum MusicPlatform { - public enum MusicPlatform - { - Radio, - Youtube, - Local, - SoundCloud, - } + Radio, + Youtube, + Local, + SoundCloud, } \ No newline at end of file diff --git a/src/NadekoBot/Modules/Music/Common/Impl/MusicPlayer.cs b/src/NadekoBot/Modules/Music/Common/Impl/MusicPlayer.cs index 91651c19b..4224f850c 100644 --- a/src/NadekoBot/Modules/Music/Common/Impl/MusicPlayer.cs +++ b/src/NadekoBot/Modules/Music/Common/Impl/MusicPlayer.cs @@ -1,540 +1,532 @@ #nullable enable -using System; -using System.Collections.Generic; using System.ComponentModel; using System.Diagnostics; -using System.Linq; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Threading; using System.Threading.Tasks; using Ayu.Discord.Voice; using NadekoBot.Common; -using NadekoBot.Modules.Music.Common; using NadekoBot.Services.Database.Models; -using NadekoBot.Extensions; -using NadekoBot.Modules.Music; -using Serilog; -namespace NadekoBot.Modules.Music +namespace NadekoBot.Modules.Music; + +public sealed class MusicPlayer : IMusicPlayer { - public sealed class MusicPlayer : IMusicPlayer + private delegate void AdjustVolumeDelegate(Span data, float volume); + + private AdjustVolumeDelegate AdjustVolume; + private readonly VoiceClient _vc; + + public bool IsKilled { get; private set; } + public bool IsStopped { get; private set; } + public bool IsPaused { get; private set; } + public PlayerRepeatType Repeat { get; private set; } + + public int CurrentIndex => _queue.Index; + + public float Volume => _volume; + private float _volume = 1.0f; + + private readonly IMusicQueue _queue; + private readonly ITrackResolveProvider _trackResolveProvider; + private readonly IVoiceProxy _proxy; + private readonly ISongBuffer _songBuffer; + + private bool _skipped; + private int? _forceIndex; + private readonly Thread _thread; + private readonly Random _rng; + + public MusicPlayer( + IMusicQueue queue, + ITrackResolveProvider trackResolveProvider, + IVoiceProxy proxy, + QualityPreset qualityPreset) { - private delegate void AdjustVolumeDelegate(Span data, float volume); + _queue = queue; + _trackResolveProvider = trackResolveProvider; + _proxy = proxy; + _rng = new NadekoRandom(); - private AdjustVolumeDelegate AdjustVolume; - private readonly VoiceClient _vc; - - public bool IsKilled { get; private set; } - public bool IsStopped { get; private set; } - public bool IsPaused { get; private set; } - public PlayerRepeatType Repeat { get; private set; } - - public int CurrentIndex => _queue.Index; - - public float Volume => _volume; - private float _volume = 1.0f; - - private readonly IMusicQueue _queue; - private readonly ITrackResolveProvider _trackResolveProvider; - private readonly IVoiceProxy _proxy; - private readonly ISongBuffer _songBuffer; - - private bool _skipped; - private int? _forceIndex; - private readonly Thread _thread; - private readonly Random _rng; - - public MusicPlayer( - IMusicQueue queue, - ITrackResolveProvider trackResolveProvider, - IVoiceProxy proxy, - QualityPreset qualityPreset) - { - _queue = queue; - _trackResolveProvider = trackResolveProvider; - _proxy = proxy; - _rng = new NadekoRandom(); - - _vc = GetVoiceClient(qualityPreset); - if (_vc.BitDepth == 16) - AdjustVolume = AdjustVolumeInt16; - else - AdjustVolume = AdjustVolumeFloat32; + _vc = GetVoiceClient(qualityPreset); + if (_vc.BitDepth == 16) + AdjustVolume = AdjustVolumeInt16; + else + AdjustVolume = AdjustVolumeFloat32; - _songBuffer = new PoopyBufferImmortalized(_vc.InputLength); + _songBuffer = new PoopyBufferImmortalized(_vc.InputLength); - _thread = new Thread(async () => - { - await PlayLoop(); - }); - _thread.Start(); - } - - private static VoiceClient GetVoiceClient(QualityPreset qualityPreset) - => qualityPreset switch - { - QualityPreset.Highest => new VoiceClient( - SampleRate._48k, - Bitrate._192k, - Channels.Two, - FrameDelay.Delay20, - BitDepthEnum.Float32 - ), - QualityPreset.High => new VoiceClient( - SampleRate._48k, - Bitrate._128k, - Channels.Two, - FrameDelay.Delay40, - BitDepthEnum.Float32 - ), - QualityPreset.Medium => new VoiceClient( - SampleRate._48k, - Bitrate._96k, - Channels.Two, - FrameDelay.Delay40, - BitDepthEnum.UInt16 - ), - QualityPreset.Low => new VoiceClient( - SampleRate._48k, - Bitrate._64k, - Channels.Two, - FrameDelay.Delay40, - BitDepthEnum.UInt16 - ), - _ => throw new ArgumentOutOfRangeException(nameof(qualityPreset), qualityPreset, null) - }; - - private async Task PlayLoop() + _thread = new Thread(async () => { - var sw = new Stopwatch(); + await PlayLoop(); + }); + _thread.Start(); + } - while (!IsKilled) - { - // wait until a song is available in the queue - // or until the queue is resumed - var track = _queue.GetCurrent(out int index); + private static VoiceClient GetVoiceClient(QualityPreset qualityPreset) + => qualityPreset switch + { + QualityPreset.Highest => new VoiceClient( + SampleRate._48k, + Bitrate._192k, + Channels.Two, + FrameDelay.Delay20, + BitDepthEnum.Float32 + ), + QualityPreset.High => new VoiceClient( + SampleRate._48k, + Bitrate._128k, + Channels.Two, + FrameDelay.Delay40, + BitDepthEnum.Float32 + ), + QualityPreset.Medium => new VoiceClient( + SampleRate._48k, + Bitrate._96k, + Channels.Two, + FrameDelay.Delay40, + BitDepthEnum.UInt16 + ), + QualityPreset.Low => new VoiceClient( + SampleRate._48k, + Bitrate._64k, + Channels.Two, + FrameDelay.Delay40, + BitDepthEnum.UInt16 + ), + _ => throw new ArgumentOutOfRangeException(nameof(qualityPreset), qualityPreset, null) + }; + + private async Task PlayLoop() + { + var sw = new Stopwatch(); + + while (!IsKilled) + { + // wait until a song is available in the queue + // or until the queue is resumed + var track = _queue.GetCurrent(out int index); - if (track is null || IsStopped) - { - await Task.Delay(500); - continue; - } - - if (_skipped) - { - _skipped = false; - _queue.Advance(); - continue; - } - - using var cancellationTokenSource = new CancellationTokenSource(); - var token = cancellationTokenSource.Token; - try - { - // light up green in vc - _ = _proxy.StartSpeakingAsync(); - - _ = OnStarted?.Invoke(this, track, index); - - // make sure song buffer is ready to be (re)used - _songBuffer.Reset(); - - var streamUrl = await track.GetStreamUrl(); - // start up the data source - using var source = FfmpegTrackDataSource.CreateAsync( - _vc.BitDepth, - streamUrl, - track.Platform == MusicPlatform.Local - ); - - // start moving data from the source into the buffer - // this method will return once the sufficient prebuffering is done - await _songBuffer.BufferAsync(source, token); - - // // Implemenation with multimedia timer. Works but a hassle because no support for switching - // // vcs, as any error in copying will cancel the song. Also no idea how to use this as an option - // // for selfhosters. - // if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) - // { - // var cancelSource = new CancellationTokenSource(); - // var cancelToken = cancelSource.Token; - // using var timer = new MultimediaTimer(_ => - // { - // if (IsStopped || IsKilled) - // { - // cancelSource.Cancel(); - // return; - // } - // - // if (_skipped) - // { - // _skipped = false; - // cancelSource.Cancel(); - // return; - // } - // - // if (IsPaused) - // return; - // - // try - // { - // // this should tolerate certain number of errors - // var result = CopyChunkToOutput(_songBuffer, _vc); - // if (!result) - // cancelSource.Cancel(); - // - // } - // catch (Exception ex) - // { - // Log.Warning(ex, "Something went wrong sending voice data: {ErrorMessage}", ex.Message); - // cancelSource.Cancel(); - // } - // - // }, null, 20); - // - // while(true) - // await Task.Delay(1000, cancelToken); - // } - - // start sending data - var ticksPerMs = 1000f / Stopwatch.Frequency; - sw.Start(); - Thread.Sleep(2); - - var delay = sw.ElapsedTicks * ticksPerMs > 3f - ? _vc.Delay - 16 - : _vc.Delay - 3; - - var errorCount = 0; - while (!IsStopped && !IsKilled) - { - // doing the skip this way instead of in the condition - // ensures that a song will for sure be skipped - if (_skipped) - { - _skipped = false; - break; - } - - if (IsPaused) - { - await Task.Delay(200); - continue; - } - - sw.Restart(); - var ticks = sw.ElapsedTicks; - try - { - var result = CopyChunkToOutput(_songBuffer, _vc); - - // if song is finished - if (result is null) - break; - - if (result is true) - { - if (errorCount > 0) - { - _ = _proxy.StartSpeakingAsync(); - errorCount = 0; - } - - // todo future windows multimedia api - - // wait for slightly less than the latency - Thread.Sleep(delay); - - // and then spin out the rest - while ((sw.ElapsedTicks - ticks) * ticksPerMs <= _vc.Delay - 0.1f) - Thread.SpinWait(100); - } - else - { - // result is false is either when the gateway is being swapped - // or if the bot is reconnecting, or just disconnected for whatever reason - - // tolerate up to 15x200ms of failures (3 seconds) - if (++errorCount <= 15) - { - await Task.Delay(200); - continue; - } - - Log.Warning("Can't send data to voice channel"); - - IsStopped = true; - // if errors are happening for more than 3 seconds - // Stop the player - break; - } - } - catch (Exception ex) - { - Log.Warning(ex, "Something went wrong sending voice data: {ErrorMessage}", ex.Message); - } - } - } - catch (Win32Exception) - { - IsStopped = true; - Log.Error("Please install ffmpeg and make sure it's added to your " + - "PATH environment variable before trying again"); - } - catch (OperationCanceledException) - { - Log.Information("Song skipped"); - } - catch (Exception ex) - { - Log.Error(ex, "Unknown error in music loop: {ErrorMessage}", ex.Message); - } - finally - { - cancellationTokenSource.Cancel(); - // turn off green in vc - _ = OnCompleted?.Invoke(this, track); - - HandleQueuePostTrack(); - _skipped = false; - - _ = _proxy.StopSpeakingAsync();; - - await Task.Delay(100); - } - } - } - - private bool? CopyChunkToOutput(ISongBuffer sb, VoiceClient vc) - { - var data = sb.Read(vc.InputLength, out var length); - - // if nothing is read from the buffer, song is finished - if (data.Length == 0) + if (track is null || IsStopped) { - return null; + await Task.Delay(500); + continue; } - AdjustVolume(data, _volume); - return _proxy.SendPcmFrame(vc, data, length); - } - - private void HandleQueuePostTrack() - { - if (_forceIndex is int forceIndex) + if (_skipped) { - _queue.SetIndex(forceIndex); - _forceIndex = null; - return; - } - - var (repeat, isStopped) = (Repeat, IsStopped); - - if (repeat == PlayerRepeatType.Track || isStopped) - return; - - // if queue is being repeated, advance no matter what - if (repeat == PlayerRepeatType.None) - { - // if this is the last song, - // stop the queue - if (_queue.IsLast()) - { - IsStopped = true; - OnQueueStopped?.Invoke(this); - return; - } - + _skipped = false; _queue.Advance(); - return; + continue; } - - _queue.Advance(); - } - - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static void AdjustVolumeInt16(Span audioSamples, float volume) - { - if (Math.Abs(volume - 1f) < 0.0001f) return; - - var samples = MemoryMarshal.Cast(audioSamples); - - for (var i = 0; i < samples.Length; i++) + using var cancellationTokenSource = new CancellationTokenSource(); + var token = cancellationTokenSource.Token; + try { - ref var sample = ref samples[i]; - sample = (short) (sample * volume); - } - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static void AdjustVolumeFloat32(Span audioSamples, float volume) - { - if (Math.Abs(volume - 1f) < 0.0001f) return; - - var samples = MemoryMarshal.Cast(audioSamples); - - for (var i = 0; i < samples.Length; i++) - { - ref var sample = ref samples[i]; - sample = (float) (sample * volume); - } - } + // light up green in vc + _ = _proxy.StartSpeakingAsync(); - public async Task<(IQueuedTrackInfo? QueuedTrack, int Index)> TryEnqueueTrackAsync( - string query, - string queuer, - bool asNext, - MusicPlatform? forcePlatform = null) - { - var song = await _trackResolveProvider.QuerySongAsync(query, forcePlatform); - if (song is null) - return default; + _ = OnStarted?.Invoke(this, track, index); - int index; + // make sure song buffer is ready to be (re)used + _songBuffer.Reset(); - if (asNext) - return (_queue.EnqueueNext(song, queuer, out index), index); + var streamUrl = await track.GetStreamUrl(); + // start up the data source + using var source = FfmpegTrackDataSource.CreateAsync( + _vc.BitDepth, + streamUrl, + track.Platform == MusicPlatform.Local + ); + + // start moving data from the source into the buffer + // this method will return once the sufficient prebuffering is done + await _songBuffer.BufferAsync(source, token); - return (_queue.Enqueue(song, queuer, out index), index); - } - - public async Task EnqueueManyAsync(IEnumerable<(string Query, MusicPlatform Platform)> queries, string queuer) - { - var errorCount = 0; - foreach (var chunk in queries.Chunk(5)) - { - if (IsKilled) - break; - - var queueTasks = chunk.Select(async data => + // // Implemenation with multimedia timer. Works but a hassle because no support for switching + // // vcs, as any error in copying will cancel the song. Also no idea how to use this as an option + // // for selfhosters. + // if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + // { + // var cancelSource = new CancellationTokenSource(); + // var cancelToken = cancelSource.Token; + // using var timer = new MultimediaTimer(_ => + // { + // if (IsStopped || IsKilled) + // { + // cancelSource.Cancel(); + // return; + // } + // + // if (_skipped) + // { + // _skipped = false; + // cancelSource.Cancel(); + // return; + // } + // + // if (IsPaused) + // return; + // + // try + // { + // // this should tolerate certain number of errors + // var result = CopyChunkToOutput(_songBuffer, _vc); + // if (!result) + // cancelSource.Cancel(); + // + // } + // catch (Exception ex) + // { + // Log.Warning(ex, "Something went wrong sending voice data: {ErrorMessage}", ex.Message); + // cancelSource.Cancel(); + // } + // + // }, null, 20); + // + // while(true) + // await Task.Delay(1000, cancelToken); + // } + + // start sending data + var ticksPerMs = 1000f / Stopwatch.Frequency; + sw.Start(); + Thread.Sleep(2); + + var delay = sw.ElapsedTicks * ticksPerMs > 3f + ? _vc.Delay - 16 + : _vc.Delay - 3; + + var errorCount = 0; + while (!IsStopped && !IsKilled) { - var (query, platform) = data; + // doing the skip this way instead of in the condition + // ensures that a song will for sure be skipped + if (_skipped) + { + _skipped = false; + break; + } + + if (IsPaused) + { + await Task.Delay(200); + continue; + } + + sw.Restart(); + var ticks = sw.ElapsedTicks; try { - await TryEnqueueTrackAsync(query, queuer, false, forcePlatform: platform).ConfigureAwait(false); - errorCount = 0; + var result = CopyChunkToOutput(_songBuffer, _vc); + + // if song is finished + if (result is null) + break; + + if (result is true) + { + if (errorCount > 0) + { + _ = _proxy.StartSpeakingAsync(); + errorCount = 0; + } + + // todo future windows multimedia api + + // wait for slightly less than the latency + Thread.Sleep(delay); + + // and then spin out the rest + while ((sw.ElapsedTicks - ticks) * ticksPerMs <= _vc.Delay - 0.1f) + Thread.SpinWait(100); + } + else + { + // result is false is either when the gateway is being swapped + // or if the bot is reconnecting, or just disconnected for whatever reason + + // tolerate up to 15x200ms of failures (3 seconds) + if (++errorCount <= 15) + { + await Task.Delay(200); + continue; + } + + Log.Warning("Can't send data to voice channel"); + + IsStopped = true; + // if errors are happening for more than 3 seconds + // Stop the player + break; + } } catch (Exception ex) { - Log.Warning(ex, "Error resolving {MusicPlatform} Track {TrackQuery}", platform, query); - ++errorCount; + Log.Warning(ex, "Something went wrong sending voice data: {ErrorMessage}", ex.Message); } - }); - - await Task.WhenAll(queueTasks); - await Task.Delay(1000); - - // > 10 errors in a row = kill - if (errorCount > 10) - break; + } + } + catch (Win32Exception) + { + IsStopped = true; + Log.Error("Please install ffmpeg and make sure it's added to your " + + "PATH environment variable before trying again"); + } + catch (OperationCanceledException) + { + Log.Information("Song skipped"); + } + catch (Exception ex) + { + Log.Error(ex, "Unknown error in music loop: {ErrorMessage}", ex.Message); + } + finally + { + cancellationTokenSource.Cancel(); + // turn off green in vc + _ = OnCompleted?.Invoke(this, track); + + HandleQueuePostTrack(); + _skipped = false; + + _ = _proxy.StopSpeakingAsync();; + + await Task.Delay(100); } } + } + + private bool? CopyChunkToOutput(ISongBuffer sb, VoiceClient vc) + { + var data = sb.Read(vc.InputLength, out var length); + + // if nothing is read from the buffer, song is finished + if (data.Length == 0) + { + return null; + } + + AdjustVolume(data, _volume); + return _proxy.SendPcmFrame(vc, data, length); + } + + private void HandleQueuePostTrack() + { + if (_forceIndex is int forceIndex) + { + _queue.SetIndex(forceIndex); + _forceIndex = null; + return; + } + + var (repeat, isStopped) = (Repeat, IsStopped); + + if (repeat == PlayerRepeatType.Track || isStopped) + return; + + // if queue is being repeated, advance no matter what + if (repeat == PlayerRepeatType.None) + { + // if this is the last song, + // stop the queue + if (_queue.IsLast()) + { + IsStopped = true; + OnQueueStopped?.Invoke(this); + return; + } + + _queue.Advance(); + return; + } + + _queue.Advance(); + } + - public void EnqueueTrack(ITrackInfo track, string queuer) + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void AdjustVolumeInt16(Span audioSamples, float volume) + { + if (Math.Abs(volume - 1f) < 0.0001f) return; + + var samples = MemoryMarshal.Cast(audioSamples); + + for (var i = 0; i < samples.Length; i++) { - _queue.Enqueue(track, queuer, out _); + ref var sample = ref samples[i]; + sample = (short) (sample * volume); } - - public void EnqueueTracks(IEnumerable tracks, string queuer) + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void AdjustVolumeFloat32(Span audioSamples, float volume) + { + if (Math.Abs(volume - 1f) < 0.0001f) return; + + var samples = MemoryMarshal.Cast(audioSamples); + + for (var i = 0; i < samples.Length; i++) { - _queue.EnqueueMany(tracks, queuer); + ref var sample = ref samples[i]; + sample = (float) (sample * volume); } + } - public void SetRepeat(PlayerRepeatType type) + public async Task<(IQueuedTrackInfo? QueuedTrack, int Index)> TryEnqueueTrackAsync( + string query, + string queuer, + bool asNext, + MusicPlatform? forcePlatform = null) + { + var song = await _trackResolveProvider.QuerySongAsync(query, forcePlatform); + if (song is null) + return default; + + int index; + + if (asNext) + return (_queue.EnqueueNext(song, queuer, out index), index); + + return (_queue.Enqueue(song, queuer, out index), index); + } + + public async Task EnqueueManyAsync(IEnumerable<(string Query, MusicPlatform Platform)> queries, string queuer) + { + var errorCount = 0; + foreach (var chunk in queries.Chunk(5)) { - Repeat = type; + if (IsKilled) + break; + + var queueTasks = chunk.Select(async data => + { + var (query, platform) = data; + try + { + await TryEnqueueTrackAsync(query, queuer, false, forcePlatform: platform).ConfigureAwait(false); + errorCount = 0; + } + catch (Exception ex) + { + Log.Warning(ex, "Error resolving {MusicPlatform} Track {TrackQuery}", platform, query); + ++errorCount; + } + }); + + await Task.WhenAll(queueTasks); + await Task.Delay(1000); + + // > 10 errors in a row = kill + if (errorCount > 10) + break; } + } + + public void EnqueueTrack(ITrackInfo track, string queuer) + { + _queue.Enqueue(track, queuer, out _); + } - public void ShuffleQueue() - { - _queue.Shuffle(_rng); - } - - public void Stop() - { - IsStopped = true; - } - - public void Clear() - { - _queue.Clear(); - _skipped = true; - } - - public IReadOnlyCollection GetQueuedTracks() - => _queue.List(); - - public IQueuedTrackInfo? GetCurrentTrack(out int index) - => _queue.GetCurrent(out index); - - public void Next() + public void EnqueueTracks(IEnumerable tracks, string queuer) + { + _queue.EnqueueMany(tracks, queuer); + } + + public void SetRepeat(PlayerRepeatType type) + { + Repeat = type; + } + + public void ShuffleQueue() + { + _queue.Shuffle(_rng); + } + + public void Stop() + { + IsStopped = true; + } + + public void Clear() + { + _queue.Clear(); + _skipped = true; + } + + public IReadOnlyCollection GetQueuedTracks() + => _queue.List(); + + public IQueuedTrackInfo? GetCurrentTrack(out int index) + => _queue.GetCurrent(out index); + + public void Next() + { + _skipped = true; + IsStopped = false; + IsPaused = false; + } + + public bool MoveTo(int index) + { + if (_queue.SetIndex(index)) { + _forceIndex = index; _skipped = true; IsStopped = false; IsPaused = false; - } - - public bool MoveTo(int index) - { - if (_queue.SetIndex(index)) - { - _forceIndex = index; - _skipped = true; - IsStopped = false; - IsPaused = false; - return true; - } - - return false; - } - - public void SetVolume(int newVolume) - { - var normalizedVolume = newVolume / 100f; - if (normalizedVolume < 0f || normalizedVolume > 1f) - throw new ArgumentOutOfRangeException(nameof(newVolume), "Volume must be in range 0-100"); - - _volume = normalizedVolume; - } - - public void Kill() - { - IsKilled = true; - IsStopped = true; - IsPaused = false; - _skipped = true; - } - - public bool TryRemoveTrackAt(int index, out IQueuedTrackInfo? trackInfo) - { - if (!_queue.TryRemoveAt(index, out trackInfo, out var isCurrent)) - return false; - - if (isCurrent) - _skipped = true; - return true; } - - public bool TogglePause() => IsPaused = !IsPaused; - public IQueuedTrackInfo? MoveTrack(int from, int to) => _queue.MoveTrack(from, to); - public void Dispose() - { - IsKilled = true; - OnCompleted = null; - OnStarted = null; - OnQueueStopped = null; - _queue.Clear(); - _songBuffer.Dispose(); - _vc.Dispose(); - } - - public event Func? OnCompleted; - public event Func? OnStarted; - public event Func? OnQueueStopped; + return false; } + + public void SetVolume(int newVolume) + { + var normalizedVolume = newVolume / 100f; + if (normalizedVolume < 0f || normalizedVolume > 1f) + throw new ArgumentOutOfRangeException(nameof(newVolume), "Volume must be in range 0-100"); + + _volume = normalizedVolume; + } + + public void Kill() + { + IsKilled = true; + IsStopped = true; + IsPaused = false; + _skipped = true; + } + + public bool TryRemoveTrackAt(int index, out IQueuedTrackInfo? trackInfo) + { + if (!_queue.TryRemoveAt(index, out trackInfo, out var isCurrent)) + return false; + + if (isCurrent) + _skipped = true; + + return true; + } + + public bool TogglePause() => IsPaused = !IsPaused; + public IQueuedTrackInfo? MoveTrack(int from, int to) => _queue.MoveTrack(from, to); + + public void Dispose() + { + IsKilled = true; + OnCompleted = null; + OnStarted = null; + OnQueueStopped = null; + _queue.Clear(); + _songBuffer.Dispose(); + _vc.Dispose(); + } + + public event Func? OnCompleted; + public event Func? OnStarted; + public event Func? OnQueueStopped; } \ No newline at end of file diff --git a/src/NadekoBot/Modules/Music/Common/Impl/MusicQueue.cs b/src/NadekoBot/Modules/Music/Common/Impl/MusicQueue.cs index c942eeed1..f3a9d3c0e 100644 --- a/src/NadekoBot/Modules/Music/Common/Impl/MusicQueue.cs +++ b/src/NadekoBot/Modules/Music/Common/Impl/MusicQueue.cs @@ -1,323 +1,319 @@ #nullable enable -using System; -using System.Collections.Generic; -using System.Linq; using System.Threading.Tasks; -namespace NadekoBot.Modules.Music +namespace NadekoBot.Modules.Music; + +public sealed partial class MusicQueue { - public sealed partial class MusicQueue + private sealed class QueuedTrackInfo : IQueuedTrackInfo { - private sealed class QueuedTrackInfo : IQueuedTrackInfo + public ITrackInfo TrackInfo { get; } + public string Queuer { get; } + + public string Title => TrackInfo.Title; + public string Url => TrackInfo.Url; + public string Thumbnail => TrackInfo.Thumbnail; + public TimeSpan Duration => TrackInfo.Duration; + public MusicPlatform Platform => TrackInfo.Platform; + + + public QueuedTrackInfo(ITrackInfo trackInfo, string queuer) { - public ITrackInfo TrackInfo { get; } - public string Queuer { get; } + TrackInfo = trackInfo; + Queuer = queuer; + } - public string Title => TrackInfo.Title; - public string Url => TrackInfo.Url; - public string Thumbnail => TrackInfo.Thumbnail; - public TimeSpan Duration => TrackInfo.Duration; - public MusicPlatform Platform => TrackInfo.Platform; + public ValueTask GetStreamUrl() => TrackInfo.GetStreamUrl(); + } +} +public sealed partial class MusicQueue : IMusicQueue +{ + private LinkedList _tracks; - public QueuedTrackInfo(ITrackInfo trackInfo, string queuer) + public int Index + { + get + { + // just make sure the internal logic runs first + // to make sure that some potential indermediate value is not returned + lock (locker) { - TrackInfo = trackInfo; - Queuer = queuer; + return _index; } - - public ValueTask GetStreamUrl() => TrackInfo.GetStreamUrl(); } } - public sealed partial class MusicQueue : IMusicQueue + private int _index; + + public int Count { - private LinkedList _tracks; - - public int Index - { - get - { - // just make sure the internal logic runs first - // to make sure that some potential indermediate value is not returned - lock (locker) - { - return _index; - } - } - } - - private int _index; - - public int Count - { - get - { - lock (locker) - { - return _tracks.Count; - } - } - } - - private readonly object locker = new object(); - - public MusicQueue() - { - _index = 0; - _tracks = new LinkedList(); - } - - public IQueuedTrackInfo Enqueue(ITrackInfo trackInfo, string queuer, out int index) + get { lock (locker) { - var added = new QueuedTrackInfo(trackInfo, queuer); - index = _tracks.Count; - _tracks.AddLast(added); - return added; + return _tracks.Count; } } + } - public IQueuedTrackInfo EnqueueNext(ITrackInfo trackInfo, string queuer, out int index) + private readonly object locker = new object(); + + public MusicQueue() + { + _index = 0; + _tracks = new LinkedList(); + } + + public IQueuedTrackInfo Enqueue(ITrackInfo trackInfo, string queuer, out int index) + { + lock (locker) { - lock (locker) + var added = new QueuedTrackInfo(trackInfo, queuer); + index = _tracks.Count; + _tracks.AddLast(added); + return added; + } + } + + public IQueuedTrackInfo EnqueueNext(ITrackInfo trackInfo, string queuer, out int index) + { + lock (locker) + { + if (_tracks.Count == 0) { - if (_tracks.Count == 0) - { - return Enqueue(trackInfo, queuer, out index); - } - - LinkedListNode currentNode = _tracks.First!; - int i; - for (i = 1; i <= _index; i++) - { - currentNode = currentNode.Next!; // can't be null because index is always in range of the count - } - - var added = new QueuedTrackInfo(trackInfo, queuer); - index = i; - - _tracks.AddAfter(currentNode, added); - - return added; + return Enqueue(trackInfo, queuer, out index); } - } - public void EnqueueMany(IEnumerable tracks, string queuer) - { - lock (locker) - { - foreach (var track in tracks) - { - var added = new QueuedTrackInfo(track, queuer); - _tracks.AddLast(added); - } - } - } - - public IReadOnlyCollection List() - { - lock (locker) - { - return _tracks.ToList(); - } - } - - public IQueuedTrackInfo? GetCurrent(out int index) - { - lock (locker) - { - index = _index; - return _tracks.ElementAtOrDefault(_index); - } - } - - public void Advance() - { - lock (locker) - { - if (++_index >= _tracks.Count) - _index = 0; - } - } - - public void Clear() - { - lock (locker) - { - _tracks.Clear(); - } - } - - public bool SetIndex(int index) - { - lock (locker) - { - if (index < 0 || index >= _tracks.Count) - return false; - - _index = index; - return true; - } - } - - private void RemoveAtInternal(int index, out IQueuedTrackInfo trackInfo) - { - var removedNode = _tracks.First!; + LinkedListNode currentNode = _tracks.First!; int i; - for (i = 0; i < index; i++) + for (i = 1; i <= _index; i++) { - removedNode = removedNode.Next!; + currentNode = currentNode.Next!; // can't be null because index is always in range of the count } - trackInfo = removedNode.Value; - _tracks.Remove(removedNode); + var added = new QueuedTrackInfo(trackInfo, queuer); + index = i; - if (i <= _index) - --_index; + _tracks.AddAfter(currentNode, added); - if (_index < 0) - _index = Count; - - // if it was the last song in the queue - // // wrap back to start - // if (_index == Count) - // _index = 0; - // else if (i <= _index) - // if (_index == 0) - // _index = Count; - // else --_index; + return added; } + } - public void RemoveCurrent() + public void EnqueueMany(IEnumerable tracks, string queuer) + { + lock (locker) { - lock (locker) + foreach (var track in tracks) { - if (_index < _tracks.Count) - RemoveAtInternal(_index, out _); + var added = new QueuedTrackInfo(track, queuer); + _tracks.AddLast(added); } } + } - public IQueuedTrackInfo? MoveTrack(int from, int to) + public IReadOnlyCollection List() + { + lock (locker) { - if (from < 0) - throw new ArgumentOutOfRangeException(nameof(from)); - if (to < 0) - throw new ArgumentOutOfRangeException(nameof(to)); - if (to == from) - throw new ArgumentException($"{nameof(from)} and {nameof(to)} must be different"); + return _tracks.ToList(); + } + } - lock (locker) + public IQueuedTrackInfo? GetCurrent(out int index) + { + lock (locker) + { + index = _index; + return _tracks.ElementAtOrDefault(_index); + } + } + + public void Advance() + { + lock (locker) + { + if (++_index >= _tracks.Count) + _index = 0; + } + } + + public void Clear() + { + lock (locker) + { + _tracks.Clear(); + } + } + + public bool SetIndex(int index) + { + lock (locker) + { + if (index < 0 || index >= _tracks.Count) + return false; + + _index = index; + return true; + } + } + + private void RemoveAtInternal(int index, out IQueuedTrackInfo trackInfo) + { + var removedNode = _tracks.First!; + int i; + for (i = 0; i < index; i++) + { + removedNode = removedNode.Next!; + } + + trackInfo = removedNode.Value; + _tracks.Remove(removedNode); + + if (i <= _index) + --_index; + + if (_index < 0) + _index = Count; + + // if it was the last song in the queue + // // wrap back to start + // if (_index == Count) + // _index = 0; + // else if (i <= _index) + // if (_index == 0) + // _index = Count; + // else --_index; + } + + public void RemoveCurrent() + { + lock (locker) + { + if (_index < _tracks.Count) + RemoveAtInternal(_index, out _); + } + } + + public IQueuedTrackInfo? MoveTrack(int from, int to) + { + if (from < 0) + throw new ArgumentOutOfRangeException(nameof(from)); + if (to < 0) + throw new ArgumentOutOfRangeException(nameof(to)); + if (to == from) + throw new ArgumentException($"{nameof(from)} and {nameof(to)} must be different"); + + lock (locker) + { + if (from >= Count || to >= Count) + return null; + + // update current track index + if (from == _index) { - if (from >= Count || to >= Count) - return null; + // if the song being moved is the current track + // it means that it will for sure end up on the destination + _index = to; + } + else + { + // moving a track from below the current track means + // means it will drop down + if (from < _index) + _index--; - // update current track index - if (from == _index) - { - // if the song being moved is the current track - // it means that it will for sure end up on the destination - _index = to; - } - else - { - // moving a track from below the current track means - // means it will drop down - if (from < _index) - _index--; - - // moving a track to below the current track - // means it will rise up - if (to <= _index) - _index++; + // moving a track to below the current track + // means it will rise up + if (to <= _index) + _index++; - // if both from and to are below _index - net change is + 1 - 1 = 0 - // if from is below and to is above - net change is -1 (as the track is taken and put above) - // if from is above and to is below - net change is 1 (as the track is inserted under) - // if from is above and to is above - net change is 0 - } + // if both from and to are below _index - net change is + 1 - 1 = 0 + // if from is below and to is above - net change is -1 (as the track is taken and put above) + // if from is above and to is below - net change is 1 (as the track is inserted under) + // if from is above and to is above - net change is 0 + } - // get the node which needs to be moved - var fromNode = _tracks.First!; - for (var i = 0; i < from; i++) - fromNode = fromNode.Next!; + // get the node which needs to be moved + var fromNode = _tracks.First!; + for (var i = 0; i < from; i++) + fromNode = fromNode.Next!; - // remove it from the queue - _tracks.Remove(fromNode); + // remove it from the queue + _tracks.Remove(fromNode); - // if it needs to be added as a first node, - // add it directly and return - if (to == 0) - { - _tracks.AddFirst(fromNode); - return fromNode.Value; - } - - // else find the node at the index before the specified target - var addAfterNode = _tracks.First!; - for (var i = 1; i < to; i++) - addAfterNode = addAfterNode.Next!; - - // and add after it - _tracks.AddAfter(addAfterNode, fromNode); + // if it needs to be added as a first node, + // add it directly and return + if (to == 0) + { + _tracks.AddFirst(fromNode); return fromNode.Value; } + + // else find the node at the index before the specified target + var addAfterNode = _tracks.First!; + for (var i = 1; i < to; i++) + addAfterNode = addAfterNode.Next!; + + // and add after it + _tracks.AddAfter(addAfterNode, fromNode); + return fromNode.Value; } + } - public void Shuffle(Random rng) + public void Shuffle(Random rng) + { + lock (locker) { - lock (locker) + var list = _tracks.ToList(); + + for (var i = 0; i < list.Count; i++) { - var list = _tracks.ToList(); + var struck = rng.Next(i, list.Count); + var temp = list[struck]; + list[struck] = list[i]; + list[i] = temp; - for (var i = 0; i < list.Count; i++) - { - var struck = rng.Next(i, list.Count); - var temp = list[struck]; - list[struck] = list[i]; - list[i] = temp; - - // could preserving the index during shuffling be done better? - if (i == _index) - _index = struck; - else if (struck == _index) - _index = i; - } - - _tracks = new LinkedList(list); + // could preserving the index during shuffling be done better? + if (i == _index) + _index = struck; + else if (struck == _index) + _index = i; } + + _tracks = new LinkedList(list); } + } - public bool IsLast() + public bool IsLast() + { + lock (locker) { - lock (locker) - { - return _index == _tracks.Count // if there are no tracks - || _index == _tracks.Count - 1; - } + return _index == _tracks.Count // if there are no tracks + || _index == _tracks.Count - 1; } + } - public bool TryRemoveAt(int index, out IQueuedTrackInfo? trackInfo, out bool isCurrent) + public bool TryRemoveAt(int index, out IQueuedTrackInfo? trackInfo, out bool isCurrent) + { + lock (locker) { - lock (locker) + isCurrent = false; + trackInfo = null; + + if (index < 0 || index >= _tracks.Count) + return false; + + if (index == _index) { - isCurrent = false; - trackInfo = null; - - if (index < 0 || index >= _tracks.Count) - return false; - - if (index == _index) - { - isCurrent = true; - } - - RemoveAtInternal(index, out trackInfo); - - return true; + isCurrent = true; } + + RemoveAtInternal(index, out trackInfo); + + return true; } } } \ No newline at end of file diff --git a/src/NadekoBot/Modules/Music/Common/Impl/RedisTrackCacher.cs b/src/NadekoBot/Modules/Music/Common/Impl/RedisTrackCacher.cs index 25be1fe79..fb39d2597 100644 --- a/src/NadekoBot/Modules/Music/Common/Impl/RedisTrackCacher.cs +++ b/src/NadekoBot/Modules/Music/Common/Impl/RedisTrackCacher.cs @@ -1,213 +1,208 @@ #nullable enable -using System; -using System.Collections.Generic; -using System.Linq; using System.Runtime.CompilerServices; using System.Text.Json; using System.Threading.Tasks; -using Serilog; using StackExchange.Redis; -namespace NadekoBot.Modules.Music +namespace NadekoBot.Modules.Music; + +public sealed class RedisTrackCacher : ITrackCacher { - public sealed class RedisTrackCacher : ITrackCacher + private readonly ConnectionMultiplexer _multiplexer; + + public RedisTrackCacher(ConnectionMultiplexer multiplexer) { - private readonly ConnectionMultiplexer _multiplexer; - - public RedisTrackCacher(ConnectionMultiplexer multiplexer) - { - _multiplexer = multiplexer; - } + _multiplexer = multiplexer; + } - public async Task GetOrCreateStreamLink( - string id, - MusicPlatform platform, - Func> streamUrlFactory - ) + public async Task GetOrCreateStreamLink( + string id, + MusicPlatform platform, + Func> streamUrlFactory + ) + { + var trackStreamKey = CreateStreamKey(id, platform); + + var value = await GetStreamFromCacheInternalAsync(trackStreamKey); + + // if there is no cached value + if (value == default) { - var trackStreamKey = CreateStreamKey(id, platform); - - var value = await GetStreamFromCacheInternalAsync(trackStreamKey); - - // if there is no cached value - if (value == default) - { - // otherwise retrieve and cache a new value, and run this method again - var success = await CreateAndCacheStreamUrlAsync(trackStreamKey, streamUrlFactory); - if (!success) - return null; + // otherwise retrieve and cache a new value, and run this method again + var success = await CreateAndCacheStreamUrlAsync(trackStreamKey, streamUrlFactory); + if (!success) + return null; - return await GetOrCreateStreamLink(id, platform, streamUrlFactory); - } - - // cache new one for future use - _ = Task.Run(() => CreateAndCacheStreamUrlAsync(trackStreamKey, streamUrlFactory)); - - return value; + return await GetOrCreateStreamLink(id, platform, streamUrlFactory); } + + // cache new one for future use + _ = Task.Run(() => CreateAndCacheStreamUrlAsync(trackStreamKey, streamUrlFactory)); + + return value; + } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static string CreateStreamKey(string id, MusicPlatform platform) - => $"track:stream:{platform}:{id}"; + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static string CreateStreamKey(string id, MusicPlatform platform) + => $"track:stream:{platform}:{id}"; - private async Task CreateAndCacheStreamUrlAsync( - string trackStreamKey, - Func> factory) + private async Task CreateAndCacheStreamUrlAsync( + string trackStreamKey, + Func> factory) + { + try { - try - { - var data = await factory(); - if (data == default) - return false; - - await CacheStreamUrlInternalAsync(trackStreamKey, data.StreamUrl, data.Expiry); - return true; - } - catch (Exception ex) - { - Log.Error(ex, "Error resolving stream link for {TrackCacheKey}", trackStreamKey); - return false; - } - } - - public Task CacheStreamUrlAsync(string id, MusicPlatform platform, string url, TimeSpan expiry) - => CacheStreamUrlInternalAsync(CreateStreamKey(id, platform), url, expiry); - - private async Task CacheStreamUrlInternalAsync(string trackStreamKey, string url, TimeSpan expiry) - { - // keys need to be expired after an hour - // to make sure client doesn't get an expired stream url - // to achieve this, track keys will be just pointers to real data - // but that data will expire - - var db = _multiplexer.GetDatabase(); - var dataKey = $"entry:{Guid.NewGuid()}:{trackStreamKey}"; - await db.StringSetAsync(dataKey, url, expiry: expiry); - await db.ListRightPushAsync(trackStreamKey, dataKey); - } - - private async Task GetStreamFromCacheInternalAsync(string trackStreamKey) - { - // Job of the method which retrieves keys is to pop the elements - // from the list of cached trackurls until it finds a non-expired key - - var db = _multiplexer.GetDatabase(); - while(true) - { - string? dataKey = await db.ListLeftPopAsync(trackStreamKey); - if (dataKey == default) - return null; - - var streamUrl = await db.StringGetAsync(dataKey); - if (streamUrl == default) - continue; - - return streamUrl; - } - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static string CreateCachedDataKey(string id, MusicPlatform platform) - => $"track:data:{platform}:{id}"; - - public Task CacheTrackDataAsync(ICachableTrackData data) - { - var db = _multiplexer.GetDatabase(); - - var trackDataKey = CreateCachedDataKey(data.Id, data.Platform); - var dataString = JsonSerializer.Serialize((object)data); - // cache for 1 day - return db.StringSetAsync(trackDataKey, dataString, expiry: TimeSpan.FromDays(1)); - } - - public async Task GetCachedDataByIdAsync(string id, MusicPlatform platform) - { - var db = _multiplexer.GetDatabase(); - - var trackDataKey = CreateCachedDataKey(id, platform); - var data = await db.StringGetAsync(trackDataKey); + var data = await factory(); if (data == default) - return null; + return false; - return JsonSerializer.Deserialize(data); + await CacheStreamUrlInternalAsync(trackStreamKey, data.StreamUrl, data.Expiry); + return true; } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static string CreateCachedQueryDataKey(string query, MusicPlatform platform) - => $"track:query_to_id:{platform}:{query}"; - public async Task GetCachedDataByQueryAsync(string query, MusicPlatform platform) + catch (Exception ex) { - query = Uri.EscapeDataString(query.Trim()); - - var db = _multiplexer.GetDatabase(); - var queryDataKey = CreateCachedQueryDataKey(query, platform); - - var trackId = await db.StringGetAsync(queryDataKey); - if (trackId == default) - return null; - - return await GetCachedDataByIdAsync(trackId, platform); - } - - public async Task CacheTrackDataByQueryAsync(string query, ICachableTrackData data) - { - query = Uri.EscapeDataString(query.Trim()); - - // first cache the data - await CacheTrackDataAsync(data); - - // then map the query to cached data's id - var db = _multiplexer.GetDatabase(); - - var queryDataKey = CreateCachedQueryDataKey(query, data.Platform); - await db.StringSetAsync(queryDataKey, data.Id, TimeSpan.FromDays(7)); - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static string CreateCachedPlaylistKey(string playlistId, MusicPlatform platform) - => $"playlist:{platform}:{playlistId}"; - public async Task> GetPlaylistTrackIdsAsync(string playlistId, MusicPlatform platform) - { - var db = _multiplexer.GetDatabase(); - var key = CreateCachedPlaylistKey(playlistId, platform); - var vals = await db.ListRangeAsync(key); - if (vals == default || vals.Length == 0) - return Array.Empty(); - - return vals.Select(x => x.ToString()).ToList(); - } - - public async Task CachePlaylistTrackIdsAsync(string playlistId, MusicPlatform platform, IEnumerable ids) - { - var db = _multiplexer.GetDatabase(); - var key = CreateCachedPlaylistKey(playlistId, platform); - await db.ListRightPushAsync(key, ids.Select(x => (RedisValue) x).ToArray()); - await db.KeyExpireAsync(key, TimeSpan.FromDays(7)); - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static string CreateCachedPlaylistQueryKey(string query, MusicPlatform platform) - => $"playlist:query:{platform}:{query}"; - public Task CachePlaylistIdByQueryAsync(string query, MusicPlatform platform, string playlistId) - { - query = Uri.EscapeDataString(query.Trim()); - var key = CreateCachedPlaylistQueryKey(query, platform); - var db = _multiplexer.GetDatabase(); - return db.StringSetAsync(key, playlistId, TimeSpan.FromDays(7)); - } - - public async Task GetPlaylistIdByQueryAsync(string query, MusicPlatform platform) - { - query = Uri.EscapeDataString(query.Trim()); - var key = CreateCachedPlaylistQueryKey(query, platform); - - var val = await _multiplexer.GetDatabase().StringGetAsync(key); - if (val == default) - return null; - - return val; + Log.Error(ex, "Error resolving stream link for {TrackCacheKey}", trackStreamKey); + return false; } } + + public Task CacheStreamUrlAsync(string id, MusicPlatform platform, string url, TimeSpan expiry) + => CacheStreamUrlInternalAsync(CreateStreamKey(id, platform), url, expiry); + + private async Task CacheStreamUrlInternalAsync(string trackStreamKey, string url, TimeSpan expiry) + { + // keys need to be expired after an hour + // to make sure client doesn't get an expired stream url + // to achieve this, track keys will be just pointers to real data + // but that data will expire + + var db = _multiplexer.GetDatabase(); + var dataKey = $"entry:{Guid.NewGuid()}:{trackStreamKey}"; + await db.StringSetAsync(dataKey, url, expiry: expiry); + await db.ListRightPushAsync(trackStreamKey, dataKey); + } + + private async Task GetStreamFromCacheInternalAsync(string trackStreamKey) + { + // Job of the method which retrieves keys is to pop the elements + // from the list of cached trackurls until it finds a non-expired key + + var db = _multiplexer.GetDatabase(); + while(true) + { + string? dataKey = await db.ListLeftPopAsync(trackStreamKey); + if (dataKey == default) + return null; + + var streamUrl = await db.StringGetAsync(dataKey); + if (streamUrl == default) + continue; + + return streamUrl; + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static string CreateCachedDataKey(string id, MusicPlatform platform) + => $"track:data:{platform}:{id}"; + + public Task CacheTrackDataAsync(ICachableTrackData data) + { + var db = _multiplexer.GetDatabase(); + + var trackDataKey = CreateCachedDataKey(data.Id, data.Platform); + var dataString = JsonSerializer.Serialize((object)data); + // cache for 1 day + return db.StringSetAsync(trackDataKey, dataString, expiry: TimeSpan.FromDays(1)); + } + + public async Task GetCachedDataByIdAsync(string id, MusicPlatform platform) + { + var db = _multiplexer.GetDatabase(); + + var trackDataKey = CreateCachedDataKey(id, platform); + var data = await db.StringGetAsync(trackDataKey); + if (data == default) + return null; + + return JsonSerializer.Deserialize(data); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static string CreateCachedQueryDataKey(string query, MusicPlatform platform) + => $"track:query_to_id:{platform}:{query}"; + public async Task GetCachedDataByQueryAsync(string query, MusicPlatform platform) + { + query = Uri.EscapeDataString(query.Trim()); + + var db = _multiplexer.GetDatabase(); + var queryDataKey = CreateCachedQueryDataKey(query, platform); + + var trackId = await db.StringGetAsync(queryDataKey); + if (trackId == default) + return null; + + return await GetCachedDataByIdAsync(trackId, platform); + } + + public async Task CacheTrackDataByQueryAsync(string query, ICachableTrackData data) + { + query = Uri.EscapeDataString(query.Trim()); + + // first cache the data + await CacheTrackDataAsync(data); + + // then map the query to cached data's id + var db = _multiplexer.GetDatabase(); + + var queryDataKey = CreateCachedQueryDataKey(query, data.Platform); + await db.StringSetAsync(queryDataKey, data.Id, TimeSpan.FromDays(7)); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static string CreateCachedPlaylistKey(string playlistId, MusicPlatform platform) + => $"playlist:{platform}:{playlistId}"; + public async Task> GetPlaylistTrackIdsAsync(string playlistId, MusicPlatform platform) + { + var db = _multiplexer.GetDatabase(); + var key = CreateCachedPlaylistKey(playlistId, platform); + var vals = await db.ListRangeAsync(key); + if (vals == default || vals.Length == 0) + return Array.Empty(); + + return vals.Select(x => x.ToString()).ToList(); + } + + public async Task CachePlaylistTrackIdsAsync(string playlistId, MusicPlatform platform, IEnumerable ids) + { + var db = _multiplexer.GetDatabase(); + var key = CreateCachedPlaylistKey(playlistId, platform); + await db.ListRightPushAsync(key, ids.Select(x => (RedisValue) x).ToArray()); + await db.KeyExpireAsync(key, TimeSpan.FromDays(7)); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static string CreateCachedPlaylistQueryKey(string query, MusicPlatform platform) + => $"playlist:query:{platform}:{query}"; + public Task CachePlaylistIdByQueryAsync(string query, MusicPlatform platform, string playlistId) + { + query = Uri.EscapeDataString(query.Trim()); + var key = CreateCachedPlaylistQueryKey(query, platform); + var db = _multiplexer.GetDatabase(); + return db.StringSetAsync(key, playlistId, TimeSpan.FromDays(7)); + } + + public async Task GetPlaylistIdByQueryAsync(string query, MusicPlatform platform) + { + query = Uri.EscapeDataString(query.Trim()); + var key = CreateCachedPlaylistQueryKey(query, platform); + + var val = await _multiplexer.GetDatabase().StringGetAsync(key); + if (val == default) + return null; + + return val; + } } \ No newline at end of file diff --git a/src/NadekoBot/Modules/Music/Common/Impl/RemoteTrackInfo.cs b/src/NadekoBot/Modules/Music/Common/Impl/RemoteTrackInfo.cs index 9427ecbed..aecd1a342 100644 --- a/src/NadekoBot/Modules/Music/Common/Impl/RemoteTrackInfo.cs +++ b/src/NadekoBot/Modules/Music/Common/Impl/RemoteTrackInfo.cs @@ -1,30 +1,28 @@ #nullable enable -using System; using System.Threading.Tasks; -namespace NadekoBot.Modules.Music +namespace NadekoBot.Modules.Music; + +public sealed class RemoteTrackInfo : ITrackInfo { - public sealed class RemoteTrackInfo : ITrackInfo + public string Title { get; } + public string Url { get; } + public string Thumbnail { get; } + public TimeSpan Duration { get; } + public MusicPlatform Platform { get; } + + private readonly Func> _streamFactory; + + public RemoteTrackInfo(string title, string url, string thumbnail, TimeSpan duration, MusicPlatform platform, + Func> streamFactory) { - public string Title { get; } - public string Url { get; } - public string Thumbnail { get; } - public TimeSpan Duration { get; } - public MusicPlatform Platform { get; } - - private readonly Func> _streamFactory; - - public RemoteTrackInfo(string title, string url, string thumbnail, TimeSpan duration, MusicPlatform platform, - Func> streamFactory) - { - _streamFactory = streamFactory; - Title = title; - Url = url; - Thumbnail = thumbnail; - Duration = duration; - Platform = platform; - } - - public async ValueTask GetStreamUrl() => await _streamFactory(); + _streamFactory = streamFactory; + Title = title; + Url = url; + Thumbnail = thumbnail; + Duration = duration; + Platform = platform; } + + public async ValueTask GetStreamUrl() => await _streamFactory(); } \ No newline at end of file diff --git a/src/NadekoBot/Modules/Music/Common/Impl/SimpleTrackInfo.cs b/src/NadekoBot/Modules/Music/Common/Impl/SimpleTrackInfo.cs index ec9391aba..f5d4a013e 100644 --- a/src/NadekoBot/Modules/Music/Common/Impl/SimpleTrackInfo.cs +++ b/src/NadekoBot/Modules/Music/Common/Impl/SimpleTrackInfo.cs @@ -1,28 +1,26 @@ #nullable enable -using System; using System.Threading.Tasks; -namespace NadekoBot.Modules.Music -{ - public sealed class SimpleTrackInfo : ITrackInfo - { - public string Title { get; } - public string Url { get; } - public string Thumbnail { get; } - public TimeSpan Duration { get; } - public MusicPlatform Platform { get; } - public string? StreamUrl { get; } - public ValueTask GetStreamUrl() => new ValueTask(StreamUrl); +namespace NadekoBot.Modules.Music; - public SimpleTrackInfo(string title, string url, string thumbnail, TimeSpan duration, - MusicPlatform platform, string streamUrl) - { - Title = title; - Url = url; - Thumbnail = thumbnail; - Duration = duration; - Platform = platform; - StreamUrl = streamUrl; - } +public sealed class SimpleTrackInfo : ITrackInfo +{ + public string Title { get; } + public string Url { get; } + public string Thumbnail { get; } + public TimeSpan Duration { get; } + public MusicPlatform Platform { get; } + public string? StreamUrl { get; } + public ValueTask GetStreamUrl() => new ValueTask(StreamUrl); + + public SimpleTrackInfo(string title, string url, string thumbnail, TimeSpan duration, + MusicPlatform platform, string streamUrl) + { + Title = title; + Url = url; + Thumbnail = thumbnail; + Duration = duration; + Platform = platform; + StreamUrl = streamUrl; } } \ No newline at end of file diff --git a/src/NadekoBot/Modules/Music/Common/Impl/VoiceProxy.cs b/src/NadekoBot/Modules/Music/Common/Impl/VoiceProxy.cs index a2de25919..7fead251b 100644 --- a/src/NadekoBot/Modules/Music/Common/Impl/VoiceProxy.cs +++ b/src/NadekoBot/Modules/Music/Common/Impl/VoiceProxy.cs @@ -1,119 +1,116 @@ -using System; -using System.Threading.Tasks; +using System.Threading.Tasks; using Ayu.Discord.Voice; using Ayu.Discord.Voice.Models; -using Serilog; -namespace NadekoBot.Modules.Music +namespace NadekoBot.Modules.Music; + +public sealed class VoiceProxy : IVoiceProxy { - public sealed class VoiceProxy : IVoiceProxy + public enum VoiceProxyState { - public enum VoiceProxyState - { - Created, - Started, - Stopped - } + Created, + Started, + Stopped + } - private const int MAX_ERROR_COUNT = 20; - private const int DELAY_ON_ERROR_MILISECONDS = 200; + private const int MAX_ERROR_COUNT = 20; + private const int DELAY_ON_ERROR_MILISECONDS = 200; - public VoiceProxyState State - => _gateway switch - { - {Started: true, Stopped: false} => VoiceProxyState.Started, - {Stopped: false} => VoiceProxyState.Created, - _ => VoiceProxyState.Stopped - }; + public VoiceProxyState State + => _gateway switch + { + {Started: true, Stopped: false} => VoiceProxyState.Started, + {Stopped: false} => VoiceProxyState.Created, + _ => VoiceProxyState.Stopped + }; - private VoiceGateway _gateway; + private VoiceGateway _gateway; - public VoiceProxy(VoiceGateway initial) + public VoiceProxy(VoiceGateway initial) + { + _gateway = initial; + } + + public bool SendPcmFrame(VoiceClient vc, Span data, int length) + { + try { - _gateway = initial; - } - - public bool SendPcmFrame(VoiceClient vc, Span data, int length) - { - try - { - var gw = _gateway; - if (gw is null || gw.Stopped || !gw.Started) - { - return false; - } - - vc.SendPcmFrame(gw, data, 0, length); - return true; - } - catch (Exception) + var gw = _gateway; + if (gw is null || gw.Stopped || !gw.Started) { return false; } + + vc.SendPcmFrame(gw, data, 0, length); + return true; } - - public async Task RunGatewayAction(Func action) + catch (Exception) { - var errorCount = 0; - do + return false; + } + } + + public async Task RunGatewayAction(Func action) + { + var errorCount = 0; + do + { + if (State == VoiceProxyState.Stopped) { - if (State == VoiceProxyState.Stopped) - { - break; - } + break; + } - try - { - var gw = _gateway; - if (gw is null || !gw.ConnectingFinished.Task.IsCompleted) - { - ++errorCount; - await Task.Delay(DELAY_ON_ERROR_MILISECONDS); - Log.Debug("Gateway is not ready"); - continue; - } - - await action(gw); - errorCount = 0; - } - catch (Exception ex) + try + { + var gw = _gateway; + if (gw is null || !gw.ConnectingFinished.Task.IsCompleted) { ++errorCount; await Task.Delay(DELAY_ON_ERROR_MILISECONDS); - Log.Debug(ex, "Error performing proxy gateway action"); + Log.Debug("Gateway is not ready"); + continue; } - } while (errorCount > 0 && errorCount <= MAX_ERROR_COUNT); - return State != VoiceProxyState.Stopped && errorCount <= MAX_ERROR_COUNT; - } + await action(gw); + errorCount = 0; + } + catch (Exception ex) + { + ++errorCount; + await Task.Delay(DELAY_ON_ERROR_MILISECONDS); + Log.Debug(ex, "Error performing proxy gateway action"); + } + } while (errorCount > 0 && errorCount <= MAX_ERROR_COUNT); - public void SetGateway(VoiceGateway gateway) - { - _gateway = gateway; - } + return State != VoiceProxyState.Stopped && errorCount <= MAX_ERROR_COUNT; + } - public Task StartSpeakingAsync() - { - return RunGatewayAction((gw) => gw.SendSpeakingAsync(VoiceSpeaking.State.Microphone)); - } + public void SetGateway(VoiceGateway gateway) + { + _gateway = gateway; + } - public Task StopSpeakingAsync() - { - return RunGatewayAction((gw) => gw.SendSpeakingAsync(VoiceSpeaking.State.None)); - } + public Task StartSpeakingAsync() + { + return RunGatewayAction((gw) => gw.SendSpeakingAsync(VoiceSpeaking.State.Microphone)); + } - public async Task StartGateway() - { - await _gateway.Start(); - } + public Task StopSpeakingAsync() + { + return RunGatewayAction((gw) => gw.SendSpeakingAsync(VoiceSpeaking.State.None)); + } - public Task StopGateway() - { - if(_gateway is VoiceGateway gw) - return gw.StopAsync(); + public async Task StartGateway() + { + await _gateway.Start(); + } - return Task.CompletedTask; - } + public Task StopGateway() + { + if(_gateway is VoiceGateway gw) + return gw.StopAsync(); + + return Task.CompletedTask; } } \ No newline at end of file diff --git a/src/NadekoBot/Modules/Music/Common/Resolvers/LocalTrackResolver.cs b/src/NadekoBot/Modules/Music/Common/Resolvers/LocalTrackResolver.cs index c02988189..c9907b890 100644 --- a/src/NadekoBot/Modules/Music/Common/Resolvers/LocalTrackResolver.cs +++ b/src/NadekoBot/Modules/Music/Common/Resolvers/LocalTrackResolver.cs @@ -1,133 +1,126 @@ -using System; -using System.Collections.Generic; -using System.ComponentModel; +using System.ComponentModel; using System.Diagnostics; using System.IO; -using System.Linq; using System.Text; using System.Threading.Tasks; -using NadekoBot.Modules.Music; -using NadekoBot.Extensions; -using Serilog; #nullable enable -namespace NadekoBot.Modules.Music.Resolvers +namespace NadekoBot.Modules.Music.Resolvers; + +public sealed class LocalTrackResolver : ILocalTrackResolver { - public sealed class LocalTrackResolver : ILocalTrackResolver + private static readonly HashSet _musicExtensions = new[] { - private static readonly HashSet _musicExtensions = new[] - { - ".MP4", ".MP3", ".FLAC", ".OGG", ".WAV", ".WMA", ".WMV", - ".AAC", ".MKV", ".WEBM", ".M4A", ".AA", ".AAX", - ".ALAC", ".AIFF", ".MOV", ".FLV", ".OGG", ".M4V" - }.ToHashSet(); + ".MP4", ".MP3", ".FLAC", ".OGG", ".WAV", ".WMA", ".WMV", + ".AAC", ".MKV", ".WEBM", ".M4A", ".AA", ".AAX", + ".ALAC", ".AIFF", ".MOV", ".FLV", ".OGG", ".M4V" + }.ToHashSet(); - public async Task ResolveByQueryAsync(string query) - { - if (!File.Exists(query)) - return null; + public async Task ResolveByQueryAsync(string query) + { + if (!File.Exists(query)) + return null; - var trackDuration = await Ffprobe.GetTrackDurationAsync(query); - return new SimpleTrackInfo( - Path.GetFileNameWithoutExtension(query), - $"https://google.com?q={Uri.EscapeDataString(Path.GetFileNameWithoutExtension(query))}", - "https://cdn.discordapp.com/attachments/155726317222887425/261850914783100928/1482522077_music.png", - trackDuration, - MusicPlatform.Local, - $"\"{Path.GetFullPath(query)}\"" - ); - } - - public async IAsyncEnumerable ResolveDirectoryAsync(string dirPath) - { - DirectoryInfo dir; - try - { - dir = new DirectoryInfo(dirPath); - } - catch (Exception ex) - { - Log.Error(ex, "Specified directory {DirectoryPath} could not be opened", dirPath); - yield break; - } - - var files = dir.EnumerateFiles() - .Where(x => - { - if (!x.Attributes.HasFlag(FileAttributes.Hidden | FileAttributes.System) - && _musicExtensions.Contains(x.Extension.ToUpperInvariant())) return true; - return false; - }); - - var firstFile = files.FirstOrDefault()?.FullName; - if (firstFile is null) - yield break; - - var firstData = await ResolveByQueryAsync(firstFile); - if (firstData is not null) - yield return firstData; - - var fileChunks = files.Skip(1).Chunk(10); - foreach (var chunk in fileChunks) - { - var part = await Task.WhenAll(chunk.Select(x => ResolveByQueryAsync(x.FullName))); - - // nullable reference types being annoying - foreach (var p in part) - { - if (p is null) - continue; - - yield return p; - } - } - } + var trackDuration = await Ffprobe.GetTrackDurationAsync(query); + return new SimpleTrackInfo( + Path.GetFileNameWithoutExtension(query), + $"https://google.com?q={Uri.EscapeDataString(Path.GetFileNameWithoutExtension(query))}", + "https://cdn.discordapp.com/attachments/155726317222887425/261850914783100928/1482522077_music.png", + trackDuration, + MusicPlatform.Local, + $"\"{Path.GetFullPath(query)}\"" + ); } - public static class Ffprobe + public async IAsyncEnumerable ResolveDirectoryAsync(string dirPath) { - public static async Task GetTrackDurationAsync(string query) + DirectoryInfo dir; + try { - query = query.Replace("\"", ""); + dir = new DirectoryInfo(dirPath); + } + catch (Exception ex) + { + Log.Error(ex, "Specified directory {DirectoryPath} could not be opened", dirPath); + yield break; + } - try + var files = dir.EnumerateFiles() + .Where(x => { - using var p = Process.Start(new ProcessStartInfo() - { - FileName = "ffprobe", - Arguments = - $"-v error -show_entries format=duration -of default=noprint_wrappers=1:nokey=1 -- \"{query}\"", - UseShellExecute = false, - RedirectStandardOutput = true, - RedirectStandardError = true, - StandardOutputEncoding = Encoding.UTF8, - StandardErrorEncoding = Encoding.UTF8, - CreateNoWindow = true, - }); + if (!x.Attributes.HasFlag(FileAttributes.Hidden | FileAttributes.System) + && _musicExtensions.Contains(x.Extension.ToUpperInvariant())) return true; + return false; + }); - if (p is null) - return TimeSpan.Zero; - - var data = await p.StandardOutput.ReadToEndAsync(); - if (double.TryParse(data, out var seconds)) - return TimeSpan.FromSeconds(seconds); - - var errorData = await p.StandardError.ReadToEndAsync(); - if (!string.IsNullOrWhiteSpace(errorData)) - Log.Warning("Ffprobe warning for file {FileName}: {ErrorMessage}", query, errorData); - - return TimeSpan.Zero; - } - catch (Win32Exception) - { - Log.Warning("Ffprobe was likely not installed. Local song durations will show as (?)"); - } - catch (Exception ex) - { - Log.Error(ex, "Unknown exception running ffprobe; {ErrorMessage}", ex.Message); - } + var firstFile = files.FirstOrDefault()?.FullName; + if (firstFile is null) + yield break; - return TimeSpan.Zero; + var firstData = await ResolveByQueryAsync(firstFile); + if (firstData is not null) + yield return firstData; + + var fileChunks = files.Skip(1).Chunk(10); + foreach (var chunk in fileChunks) + { + var part = await Task.WhenAll(chunk.Select(x => ResolveByQueryAsync(x.FullName))); + + // nullable reference types being annoying + foreach (var p in part) + { + if (p is null) + continue; + + yield return p; + } } } } + +public static class Ffprobe +{ + public static async Task GetTrackDurationAsync(string query) + { + query = query.Replace("\"", ""); + + try + { + using var p = Process.Start(new ProcessStartInfo() + { + FileName = "ffprobe", + Arguments = + $"-v error -show_entries format=duration -of default=noprint_wrappers=1:nokey=1 -- \"{query}\"", + UseShellExecute = false, + RedirectStandardOutput = true, + RedirectStandardError = true, + StandardOutputEncoding = Encoding.UTF8, + StandardErrorEncoding = Encoding.UTF8, + CreateNoWindow = true, + }); + + if (p is null) + return TimeSpan.Zero; + + var data = await p.StandardOutput.ReadToEndAsync(); + if (double.TryParse(data, out var seconds)) + return TimeSpan.FromSeconds(seconds); + + var errorData = await p.StandardError.ReadToEndAsync(); + if (!string.IsNullOrWhiteSpace(errorData)) + Log.Warning("Ffprobe warning for file {FileName}: {ErrorMessage}", query, errorData); + + return TimeSpan.Zero; + } + catch (Win32Exception) + { + Log.Warning("Ffprobe was likely not installed. Local song durations will show as (?)"); + } + catch (Exception ex) + { + Log.Error(ex, "Unknown exception running ffprobe; {ErrorMessage}", ex.Message); + } + + return TimeSpan.Zero; + } +} \ No newline at end of file diff --git a/src/NadekoBot/Modules/Music/Common/Resolvers/RadioResolveStrategy.cs b/src/NadekoBot/Modules/Music/Common/Resolvers/RadioResolveStrategy.cs index 052c6ee78..c2a616507 100644 --- a/src/NadekoBot/Modules/Music/Common/Resolvers/RadioResolveStrategy.cs +++ b/src/NadekoBot/Modules/Music/Common/Resolvers/RadioResolveStrategy.cs @@ -1,135 +1,131 @@ -using System; -using System.Net.Http; +using System.Net.Http; using System.Text.RegularExpressions; using System.Threading.Tasks; -using NadekoBot.Modules.Music; using NadekoBot.Extensions; -using Serilog; -namespace NadekoBot.Modules.Music.Resolvers +namespace NadekoBot.Modules.Music.Resolvers; + +public class RadioResolver : IRadioResolver { - public class RadioResolver : IRadioResolver + private readonly Regex plsRegex = new Regex("File1=(?.*?)\\n", RegexOptions.Compiled); + private readonly Regex m3uRegex = new Regex("(?^[^#].*)", RegexOptions.Compiled | RegexOptions.Multiline); + private readonly Regex asxRegex = new Regex(".*?)\"", RegexOptions.Compiled); + private readonly Regex xspfRegex = new Regex("(?.*?)", RegexOptions.Compiled); + + public RadioResolver() { - private readonly Regex plsRegex = new Regex("File1=(?.*?)\\n", RegexOptions.Compiled); - private readonly Regex m3uRegex = new Regex("(?^[^#].*)", RegexOptions.Compiled | RegexOptions.Multiline); - private readonly Regex asxRegex = new Regex(".*?)\"", RegexOptions.Compiled); - private readonly Regex xspfRegex = new Regex("(?.*?)", RegexOptions.Compiled); + } - public RadioResolver() + public async Task ResolveByQueryAsync(string query) + { + if (IsRadioLink(query)) + query = await HandleStreamContainers(query).ConfigureAwait(false); + + return new SimpleTrackInfo( + query.TrimTo(50), + query, + "https://cdn.discordapp.com/attachments/155726317222887425/261850925063340032/1482522097_radio.png", + TimeSpan.MaxValue, + MusicPlatform.Radio, + query + ); + } + + public static bool IsRadioLink(string query) => + (query.StartsWith("http", StringComparison.InvariantCulture) || + query.StartsWith("ww", StringComparison.InvariantCulture)) + && + (query.Contains(".pls") || + query.Contains(".m3u") || + query.Contains(".asx") || + query.Contains(".xspf")); + + private async Task HandleStreamContainers(string query) + { + string file = null; + try { + using (var http = new HttpClient()) + { + file = await http.GetStringAsync(query).ConfigureAwait(false); + } } - - public async Task ResolveByQueryAsync(string query) + catch { - if (IsRadioLink(query)) - query = await HandleStreamContainers(query).ConfigureAwait(false); - - return new SimpleTrackInfo( - query.TrimTo(50), - query, - "https://cdn.discordapp.com/attachments/155726317222887425/261850925063340032/1482522097_radio.png", - TimeSpan.MaxValue, - MusicPlatform.Radio, - query - ); + return query; } - - public static bool IsRadioLink(string query) => - (query.StartsWith("http", StringComparison.InvariantCulture) || - query.StartsWith("ww", StringComparison.InvariantCulture)) - && - (query.Contains(".pls") || - query.Contains(".m3u") || - query.Contains(".asx") || - query.Contains(".xspf")); - - private async Task HandleStreamContainers(string query) + if (query.Contains(".pls")) { - string file = null; + //File1=http://armitunes.com:8000/ + //Regex.Match(query) try { - using (var http = new HttpClient()) - { - file = await http.GetStringAsync(query).ConfigureAwait(false); - } + var m = plsRegex.Match(file); + var res = m.Groups["url"]?.ToString(); + return res?.Trim(); } catch { - return query; + Log.Warning($"Failed reading .pls:\n{file}"); + return null; } - if (query.Contains(".pls")) - { - //File1=http://armitunes.com:8000/ - //Regex.Match(query) - try - { - var m = plsRegex.Match(file); - var res = m.Groups["url"]?.ToString(); - return res?.Trim(); - } - catch - { - Log.Warning($"Failed reading .pls:\n{file}"); - return null; - } - } - if (query.Contains(".m3u")) - { - /* -# This is a comment - C:\xxx4xx\xxxxxx3x\xx2xxxx\xx.mp3 - C:\xxx5xx\x6xxxxxx\x7xxxxx\xx.mp3 - */ - try - { - var m = m3uRegex.Match(file); - var res = m.Groups["url"]?.ToString(); - return res?.Trim(); - } - catch - { - Log.Warning($"Failed reading .m3u:\n{file}"); - return null; - } - - } - if (query.Contains(".asx")) - { - // - try - { - var m = asxRegex.Match(file); - var res = m.Groups["url"]?.ToString(); - return res?.Trim(); - } - catch - { - Log.Warning($"Failed reading .asx:\n{file}"); - return null; - } - } - if (query.Contains(".xspf")) - { - /* - - - - file:///mp3s/song_1.mp3 - */ - try - { - var m = xspfRegex.Match(file); - var res = m.Groups["url"]?.ToString(); - return res?.Trim(); - } - catch - { - Log.Warning($"Failed reading .xspf:\n{file}"); - return null; - } - } - - return query; } + if (query.Contains(".m3u")) + { + /* +# This is a comment + C:\xxx4xx\xxxxxx3x\xx2xxxx\xx.mp3 + C:\xxx5xx\x6xxxxxx\x7xxxxx\xx.mp3 + */ + try + { + var m = m3uRegex.Match(file); + var res = m.Groups["url"]?.ToString(); + return res?.Trim(); + } + catch + { + Log.Warning($"Failed reading .m3u:\n{file}"); + return null; + } + + } + if (query.Contains(".asx")) + { + // + try + { + var m = asxRegex.Match(file); + var res = m.Groups["url"]?.ToString(); + return res?.Trim(); + } + catch + { + Log.Warning($"Failed reading .asx:\n{file}"); + return null; + } + } + if (query.Contains(".xspf")) + { + /* + + + + file:///mp3s/song_1.mp3 + */ + try + { + var m = xspfRegex.Match(file); + var res = m.Groups["url"]?.ToString(); + return res?.Trim(); + } + catch + { + Log.Warning($"Failed reading .xspf:\n{file}"); + return null; + } + } + + return query; } } \ No newline at end of file diff --git a/src/NadekoBot/Modules/Music/Common/Resolvers/SoundcloudResolver.cs b/src/NadekoBot/Modules/Music/Common/Resolvers/SoundcloudResolver.cs index bb3f21025..ff3c57021 100644 --- a/src/NadekoBot/Modules/Music/Common/Resolvers/SoundcloudResolver.cs +++ b/src/NadekoBot/Modules/Music/Common/Resolvers/SoundcloudResolver.cs @@ -1,100 +1,95 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Net.Http; +using System.Net.Http; using System.Runtime.CompilerServices; using System.Threading.Tasks; using NadekoBot.Services; -using NadekoBot.Extensions; using Newtonsoft.Json.Linq; #nullable enable -namespace NadekoBot.Modules.Music.Resolvers -{ - public sealed class SoundcloudResolver : ISoundcloudResolver +namespace NadekoBot.Modules.Music.Resolvers; + +public sealed class SoundcloudResolver : ISoundcloudResolver +{ + private readonly SoundCloudApiService _sc; + private readonly ITrackCacher _trackCacher; + private readonly IHttpClientFactory _httpFactory; + + public SoundcloudResolver(SoundCloudApiService sc, ITrackCacher trackCacher, IHttpClientFactory httpFactory) { - private readonly SoundCloudApiService _sc; - private readonly ITrackCacher _trackCacher; - private readonly IHttpClientFactory _httpFactory; + _sc = sc; + _trackCacher = trackCacher; + _httpFactory = httpFactory; + } - public SoundcloudResolver(SoundCloudApiService sc, ITrackCacher trackCacher, IHttpClientFactory httpFactory) + public bool IsSoundCloudLink(string url) => + System.Text.RegularExpressions.Regex.IsMatch(url, "(.*)(soundcloud.com|snd.sc)(.*)"); + + public async IAsyncEnumerable ResolvePlaylistAsync(string playlist) + { + playlist = Uri.EscapeDataString(playlist); + + using var http = _httpFactory.CreateClient(); + var responseString = await http.GetStringAsync($"https://scapi.nadeko.bot/resolve?url={playlist}"); + var scvids = JObject.Parse(responseString)["tracks"]?.ToObject(); + if (scvids is null) { - _sc = sc; - _trackCacher = trackCacher; - _httpFactory = httpFactory; + yield break; } - public bool IsSoundCloudLink(string url) => - System.Text.RegularExpressions.Regex.IsMatch(url, "(.*)(soundcloud.com|snd.sc)(.*)"); - - public async IAsyncEnumerable ResolvePlaylistAsync(string playlist) + foreach (var videosChunk in scvids.Where(x => x.Streamable is true).Chunk(5)) { - playlist = Uri.EscapeDataString(playlist); - - using var http = _httpFactory.CreateClient(); - var responseString = await http.GetStringAsync($"https://scapi.nadeko.bot/resolve?url={playlist}"); - var scvids = JObject.Parse(responseString)["tracks"]?.ToObject(); - if (scvids is null) + var cachableTracks = videosChunk + .Select(VideoModelToCachedData) + .ToList(); + + await Task.WhenAll(cachableTracks.Select(_trackCacher.CacheTrackDataAsync)); + foreach(var info in cachableTracks.Select(CachableDataToTrackInfo)) { - yield break; + yield return info; } - - foreach (var videosChunk in scvids.Where(x => x.Streamable is true).Chunk(5)) - { - var cachableTracks = videosChunk - .Select(VideoModelToCachedData) - .ToList(); - - await Task.WhenAll(cachableTracks.Select(_trackCacher.CacheTrackDataAsync)); - foreach(var info in cachableTracks.Select(CachableDataToTrackInfo)) - { - yield return info; - } - } - } - - private ICachableTrackData VideoModelToCachedData(SoundCloudVideo svideo) - => new CachableTrackData() - { - Title = svideo.FullName, - Url = svideo.TrackLink, - Thumbnail = svideo.ArtworkUrl, - TotalDurationMs = svideo.Duration, - Id = svideo.Id.ToString(), - Platform = MusicPlatform.SoundCloud - }; - - private ITrackInfo CachableDataToTrackInfo(ICachableTrackData trackData) - => new SimpleTrackInfo( - trackData.Title, - trackData.Url, - trackData.Thumbnail, - trackData.Duration, - trackData.Platform, - GetStreamUrl(trackData.Id) - ); - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private string GetStreamUrl(string trackId) - => $"https://api.soundcloud.com/tracks/{trackId}/stream?client_id=368b0c85751007cd588d869d3ae61ac0"; - - public async Task ResolveByQueryAsync(string query) - { - var cached = await _trackCacher.GetCachedDataByQueryAsync(query, MusicPlatform.SoundCloud); - if (cached is not null) - return CachableDataToTrackInfo(cached); - - var svideo = !IsSoundCloudLink(query) - ? await _sc.GetVideoByQueryAsync(query).ConfigureAwait(false) - : await _sc.ResolveVideoAsync(query).ConfigureAwait(false); - - if (svideo is null) - return null; - - var cachableData = VideoModelToCachedData(svideo); - await _trackCacher.CacheTrackDataByQueryAsync(query, cachableData); - - return CachableDataToTrackInfo(cachableData); } } -} + + private ICachableTrackData VideoModelToCachedData(SoundCloudVideo svideo) + => new CachableTrackData() + { + Title = svideo.FullName, + Url = svideo.TrackLink, + Thumbnail = svideo.ArtworkUrl, + TotalDurationMs = svideo.Duration, + Id = svideo.Id.ToString(), + Platform = MusicPlatform.SoundCloud + }; + + private ITrackInfo CachableDataToTrackInfo(ICachableTrackData trackData) + => new SimpleTrackInfo( + trackData.Title, + trackData.Url, + trackData.Thumbnail, + trackData.Duration, + trackData.Platform, + GetStreamUrl(trackData.Id) + ); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private string GetStreamUrl(string trackId) + => $"https://api.soundcloud.com/tracks/{trackId}/stream?client_id=368b0c85751007cd588d869d3ae61ac0"; + + public async Task ResolveByQueryAsync(string query) + { + var cached = await _trackCacher.GetCachedDataByQueryAsync(query, MusicPlatform.SoundCloud); + if (cached is not null) + return CachableDataToTrackInfo(cached); + + var svideo = !IsSoundCloudLink(query) + ? await _sc.GetVideoByQueryAsync(query).ConfigureAwait(false) + : await _sc.ResolveVideoAsync(query).ConfigureAwait(false); + + if (svideo is null) + return null; + + var cachableData = VideoModelToCachedData(svideo); + await _trackCacher.CacheTrackDataByQueryAsync(query, cachableData); + + return CachableDataToTrackInfo(cachableData); + } +} \ No newline at end of file diff --git a/src/NadekoBot/Modules/Music/Common/Resolvers/TrackResolveProvider.cs b/src/NadekoBot/Modules/Music/Common/Resolvers/TrackResolveProvider.cs index 3aa42948f..a05430e40 100644 --- a/src/NadekoBot/Modules/Music/Common/Resolvers/TrackResolveProvider.cs +++ b/src/NadekoBot/Modules/Music/Common/Resolvers/TrackResolveProvider.cs @@ -1,63 +1,60 @@ #nullable enable -using System; using System.Threading.Tasks; -using Serilog; -namespace NadekoBot.Modules.Music +namespace NadekoBot.Modules.Music; + +public sealed class TrackResolveProvider : ITrackResolveProvider { - public sealed class TrackResolveProvider : ITrackResolveProvider + private readonly IYoutubeResolver _ytResolver; + private readonly ILocalTrackResolver _localResolver; + private readonly ISoundcloudResolver _soundcloudResolver; + private readonly IRadioResolver _radioResolver; + + public TrackResolveProvider(IYoutubeResolver ytResolver, ILocalTrackResolver localResolver, + ISoundcloudResolver soundcloudResolver, IRadioResolver radioResolver) { - private readonly IYoutubeResolver _ytResolver; - private readonly ILocalTrackResolver _localResolver; - private readonly ISoundcloudResolver _soundcloudResolver; - private readonly IRadioResolver _radioResolver; - - public TrackResolveProvider(IYoutubeResolver ytResolver, ILocalTrackResolver localResolver, - ISoundcloudResolver soundcloudResolver, IRadioResolver radioResolver) - { - _ytResolver = ytResolver; - _localResolver = localResolver; - _soundcloudResolver = soundcloudResolver; - _radioResolver = radioResolver; - } - - public Task QuerySongAsync(string query, MusicPlatform? forcePlatform) - { - switch (forcePlatform) - { - case MusicPlatform.Radio: - return _radioResolver.ResolveByQueryAsync(query); - case MusicPlatform.Youtube: - return _ytResolver.ResolveByQueryAsync(query); - case MusicPlatform.Local: - return _localResolver.ResolveByQueryAsync(query); - case MusicPlatform.SoundCloud: - return _soundcloudResolver.ResolveByQueryAsync(query); - case null: - var match = _ytResolver.YtVideoIdRegex.Match(query); - if (match.Success) - return _ytResolver.ResolveByIdAsync(match.Groups["id"].Value); - else if (_soundcloudResolver.IsSoundCloudLink(query)) - return _soundcloudResolver.ResolveByQueryAsync(query); - else if (Uri.TryCreate(query, UriKind.Absolute, out var uri) && uri.IsFile) - return _localResolver.ResolveByQueryAsync(uri.AbsolutePath); - else if (IsRadioLink(query)) - return _radioResolver.ResolveByQueryAsync(query); - else - return _ytResolver.ResolveByQueryAsync(query, false); - default: - Log.Error("Unsupported platform: {MusicPlatform}", forcePlatform); - return Task.FromResult(null); - } - } - - public static bool IsRadioLink(string query) => - (query.StartsWith("http", StringComparison.InvariantCulture) || - query.StartsWith("ww", StringComparison.InvariantCulture)) - && - (query.Contains(".pls") || - query.Contains(".m3u") || - query.Contains(".asx") || - query.Contains(".xspf")); + _ytResolver = ytResolver; + _localResolver = localResolver; + _soundcloudResolver = soundcloudResolver; + _radioResolver = radioResolver; } + + public Task QuerySongAsync(string query, MusicPlatform? forcePlatform) + { + switch (forcePlatform) + { + case MusicPlatform.Radio: + return _radioResolver.ResolveByQueryAsync(query); + case MusicPlatform.Youtube: + return _ytResolver.ResolveByQueryAsync(query); + case MusicPlatform.Local: + return _localResolver.ResolveByQueryAsync(query); + case MusicPlatform.SoundCloud: + return _soundcloudResolver.ResolveByQueryAsync(query); + case null: + var match = _ytResolver.YtVideoIdRegex.Match(query); + if (match.Success) + return _ytResolver.ResolveByIdAsync(match.Groups["id"].Value); + else if (_soundcloudResolver.IsSoundCloudLink(query)) + return _soundcloudResolver.ResolveByQueryAsync(query); + else if (Uri.TryCreate(query, UriKind.Absolute, out var uri) && uri.IsFile) + return _localResolver.ResolveByQueryAsync(uri.AbsolutePath); + else if (IsRadioLink(query)) + return _radioResolver.ResolveByQueryAsync(query); + else + return _ytResolver.ResolveByQueryAsync(query, false); + default: + Log.Error("Unsupported platform: {MusicPlatform}", forcePlatform); + return Task.FromResult(null); + } + } + + public static bool IsRadioLink(string query) => + (query.StartsWith("http", StringComparison.InvariantCulture) || + query.StartsWith("ww", StringComparison.InvariantCulture)) + && + (query.Contains(".pls") || + query.Contains(".m3u") || + query.Contains(".asx") || + query.Contains(".xspf")); } \ No newline at end of file diff --git a/src/NadekoBot/Modules/Music/Common/Resolvers/YtdlYoutubeResolver.cs b/src/NadekoBot/Modules/Music/Common/Resolvers/YtdlYoutubeResolver.cs index a7396f024..1d7f41b49 100644 --- a/src/NadekoBot/Modules/Music/Common/Resolvers/YtdlYoutubeResolver.cs +++ b/src/NadekoBot/Modules/Music/Common/Resolvers/YtdlYoutubeResolver.cs @@ -1,353 +1,348 @@ #nullable enable -using System; -using System.Collections.Generic; using System.Globalization; -using System.Linq; using System.Text.RegularExpressions; using System.Threading.Tasks; using NadekoBot.Services; using NadekoBot.Extensions; -using Serilog; -namespace NadekoBot.Modules.Music +namespace NadekoBot.Modules.Music; + +public sealed class YtdlYoutubeResolver : IYoutubeResolver { - public sealed class YtdlYoutubeResolver : IYoutubeResolver + private static readonly string[] durationFormats = new[] + {"ss", "m\\:ss", "mm\\:ss", "h\\:mm\\:ss", "hh\\:mm\\:ss", "hhh\\:mm\\:ss"}; + + public Regex YtVideoIdRegex { get; } + = new Regex( + @"(?:youtube\.com\/\S*(?:(?:\/e(?:mbed))?\/|watch\?(?:\S*?&?v\=))|youtu\.be\/)(?[a-zA-Z0-9_-]{6,11})", + RegexOptions.Compiled + ); + + private readonly ITrackCacher _trackCacher; + + private readonly YtdlOperation _ytdlPlaylistOperation; + private readonly YtdlOperation _ytdlIdOperation; + private readonly YtdlOperation _ytdlSearchOperation; + + private IGoogleApiService _google; + + public YtdlYoutubeResolver(ITrackCacher trackCacher, IGoogleApiService google) { - private static readonly string[] durationFormats = new[] - {"ss", "m\\:ss", "mm\\:ss", "h\\:mm\\:ss", "hh\\:mm\\:ss", "hhh\\:mm\\:ss"}; + _trackCacher = trackCacher; + _google = google; - public Regex YtVideoIdRegex { get; } - = new Regex( - @"(?:youtube\.com\/\S*(?:(?:\/e(?:mbed))?\/|watch\?(?:\S*?&?v\=))|youtu\.be\/)(?[a-zA-Z0-9_-]{6,11})", - RegexOptions.Compiled - ); + _ytdlPlaylistOperation = + new YtdlOperation("-4 " + + "--geo-bypass " + + "--encoding UTF8 " + + "-f bestaudio " + + "-e " + + "--get-url " + + "--get-id " + + "--get-thumbnail " + + "--get-duration " + + "--no-check-certificate " + + "-i " + + "--yes-playlist " + + "-- \"{0}\""); - private readonly ITrackCacher _trackCacher; - - private readonly YtdlOperation _ytdlPlaylistOperation; - private readonly YtdlOperation _ytdlIdOperation; - private readonly YtdlOperation _ytdlSearchOperation; - - private IGoogleApiService _google; - - public YtdlYoutubeResolver(ITrackCacher trackCacher, IGoogleApiService google) - { - _trackCacher = trackCacher; - _google = google; - - _ytdlPlaylistOperation = - new YtdlOperation("-4 " + - "--geo-bypass " + - "--encoding UTF8 " + - "-f bestaudio " + - "-e " + - "--get-url " + - "--get-id " + - "--get-thumbnail " + - "--get-duration " + - "--no-check-certificate " + - "-i " + - "--yes-playlist " + - "-- \"{0}\""); - - _ytdlIdOperation = - new YtdlOperation("-4 " + - "--geo-bypass " + - "--encoding UTF8 " + - "-f bestaudio " + - "-e " + - "--get-url " + - "--get-id " + - "--get-thumbnail " + - "--get-duration " + - "--no-check-certificate " + - "-- \"{0}\""); + _ytdlIdOperation = + new YtdlOperation("-4 " + + "--geo-bypass " + + "--encoding UTF8 " + + "-f bestaudio " + + "-e " + + "--get-url " + + "--get-id " + + "--get-thumbnail " + + "--get-duration " + + "--no-check-certificate " + + "-- \"{0}\""); - _ytdlSearchOperation = - new YtdlOperation("-4 " + - "--geo-bypass " + - "--encoding UTF8 " + - "-f bestaudio " + - "-e " + - "--get-url " + - "--get-id " + - "--get-thumbnail " + - "--get-duration " + - "--no-check-certificate " + - "--default-search " + - "\"ytsearch:\" -- \"{0}\""); - } + _ytdlSearchOperation = + new YtdlOperation("-4 " + + "--geo-bypass " + + "--encoding UTF8 " + + "-f bestaudio " + + "-e " + + "--get-url " + + "--get-id " + + "--get-thumbnail " + + "--get-duration " + + "--no-check-certificate " + + "--default-search " + + "\"ytsearch:\" -- \"{0}\""); + } - private readonly struct YtTrackData + private readonly struct YtTrackData + { + public readonly string Title; + public readonly string Id; + public readonly string Thumbnail; + public readonly string? StreamUrl; + public readonly TimeSpan Duration; + + public YtTrackData(string title, string id, string thumbnail, string? streamUrl, TimeSpan duration) { - public readonly string Title; - public readonly string Id; - public readonly string Thumbnail; - public readonly string? StreamUrl; - public readonly TimeSpan Duration; - - public YtTrackData(string title, string id, string thumbnail, string? streamUrl, TimeSpan duration) - { - Title = title.Trim(); - Id = id.Trim(); - Thumbnail = thumbnail; - StreamUrl = streamUrl; - Duration = duration; - } - } - - private YtTrackData ResolveYtdlData(string ytdlOutputString) - { - if (string.IsNullOrWhiteSpace(ytdlOutputString)) - return default; - - var dataArray = ytdlOutputString.Trim().Split('\n'); - - if (dataArray.Length < 5) - { - Log.Information("Not enough data received: {YtdlData}", ytdlOutputString); - return default; - } - - if (!TimeSpan.TryParseExact(dataArray[4], durationFormats, CultureInfo.InvariantCulture, out var time)) - { - time = TimeSpan.Zero; - } - - var thumbnail = Uri.IsWellFormedUriString(dataArray[3], UriKind.Absolute) - ? dataArray[3].Trim() - : string.Empty; - - return new YtTrackData( - dataArray[0], - dataArray[1], - thumbnail, - dataArray[2], - time - ); - } - - private ITrackInfo DataToInfo(in YtTrackData trackData) - => new RemoteTrackInfo( - trackData.Title, - $"https://youtube.com/watch?v={trackData.Id}", - trackData.Thumbnail, - trackData.Duration, - MusicPlatform.Youtube, - CreateCacherFactory(trackData.Id)); - - private Func> CreateCacherFactory(string id) - => () => _trackCacher.GetOrCreateStreamLink( - id, - MusicPlatform.Youtube, - async () => await ExtractNewStreamUrlAsync(id) - ); - - private static readonly Regex expiryRegex = new Regex(@"(?:[\?\&]expire\=(?\d+))"); - private static TimeSpan GetExpiry(string streamUrl) - { - var match = expiryRegex.Match(streamUrl); - if (match.Success && double.TryParse(match.Groups["timestamp"].ToString(), out var timestamp)) - { - var realExpiry = (timestamp.ToUnixTimestamp() - DateTime.UtcNow); - if (realExpiry > TimeSpan.FromMinutes(60)) - return realExpiry.Subtract(TimeSpan.FromMinutes(30)); - - return realExpiry; - } - - return TimeSpan.FromHours(1); - } - - private async Task<(string StreamUrl, TimeSpan Expiry)> ExtractNewStreamUrlAsync(string id) - { - var data = await _ytdlIdOperation.GetDataAsync(id); - var trackInfo = ResolveYtdlData(data); - if (string.IsNullOrWhiteSpace(trackInfo.StreamUrl)) - return default; - - return (trackInfo.StreamUrl!, GetExpiry(trackInfo.StreamUrl!)); - } - - public async Task ResolveByIdAsync(string id) - { - id = id.Trim(); - - var cachedData = await _trackCacher.GetCachedDataByIdAsync(id, MusicPlatform.Youtube); - if (cachedData is null) - { - Log.Information("Resolving youtube track by Id: {YoutubeId}", id); - - var data = await _ytdlIdOperation.GetDataAsync(id); - - var trackInfo = ResolveYtdlData(data); - if (string.IsNullOrWhiteSpace(trackInfo.Title)) - return default; - - var toReturn = DataToInfo(in trackInfo); - - await Task.WhenAll( - _trackCacher.CacheTrackDataAsync(toReturn.ToCachedData(id)), - CacheStreamUrlAsync(trackInfo) - ); - - return toReturn; - } - - return DataToInfo(new YtTrackData( - cachedData.Title, - cachedData.Id, - cachedData.Thumbnail, - null, - cachedData.Duration - )); - } - - private Task CacheStreamUrlAsync(YtTrackData trackInfo) - => _trackCacher.CacheStreamUrlAsync( - trackInfo.Id, - MusicPlatform.Youtube, - trackInfo.StreamUrl!, - GetExpiry(trackInfo.StreamUrl!) - ); - - - private static readonly Regex _simplePlaylistRegex - = new Regex(@"&list=(?[\w\-]{12,})", RegexOptions.Compiled); - - public async IAsyncEnumerable ResolveTracksByPlaylistIdAsync(string playlistId) - { - Log.Information("Resolving youtube tracks from playlist: {PlaylistId}", playlistId); - var count = 0; - - var ids = await _trackCacher.GetPlaylistTrackIdsAsync(playlistId, MusicPlatform.Youtube); - if (ids.Count > 0) - { - foreach (var id in ids) - { - var trackInfo = await ResolveByIdAsync(id); - if (trackInfo is null) - continue; - - yield return trackInfo; - } - - yield break; - } - - var data = string.Empty; - var trackIds = new List(); - await foreach (var line in _ytdlPlaylistOperation.EnumerateDataAsync(playlistId)) - { - data += line; - - if (++count == 5) - { - var trackData = ResolveYtdlData(data); - data = string.Empty; - count = 0; - if (string.IsNullOrWhiteSpace(trackData.Id)) - continue; - - var info = DataToInfo(in trackData); - await Task.WhenAll( - _trackCacher.CacheTrackDataAsync(info.ToCachedData(trackData.Id)), - CacheStreamUrlAsync(trackData) - ); - - trackIds.Add(trackData.Id); - yield return info; - } - else - { - data += Environment.NewLine; - } - } - - await _trackCacher.CachePlaylistTrackIdsAsync(playlistId, MusicPlatform.Youtube, trackIds); - } - - public async IAsyncEnumerable ResolveTracksFromPlaylistAsync(string query) - { - string? playlistId; - // try to match playlist id inside the query, if a playlist url has been queried - var match = _simplePlaylistRegex.Match(query); - if (match.Success) - { - // if it's a success, just return from that playlist using the id - playlistId = match.Groups["id"].ToString(); - await foreach (var track in ResolveTracksByPlaylistIdAsync(playlistId)) - yield return track; - - yield break; - } - - // if a query is a search term, try the cache - playlistId = await _trackCacher.GetPlaylistIdByQueryAsync(query, MusicPlatform.Youtube); - if (playlistId is null) - { - // if it's not in the cache - // find playlist id by keyword using google api - try - { - var playlistIds = await _google.GetPlaylistIdsByKeywordsAsync(query); - playlistId = playlistIds.FirstOrDefault(); - } - catch (Exception ex) - { - Log.Warning(ex, "Error Getting playlist id via GoogleApi"); - } - - // if query is not a playlist url - // and query result is not in the cache - // and api returns no values - // it means invalid input has been used, - // or google api key is not provided - if (playlistId is null) - yield break; - } - - // cache the query -> playlist id for fast future lookup - await _trackCacher.CachePlaylistIdByQueryAsync(query, MusicPlatform.Youtube, playlistId); - await foreach (var track in ResolveTracksByPlaylistIdAsync(playlistId)) - yield return track; - } - - public Task ResolveByQueryAsync(string query) - => ResolveByQueryAsync(query, true); - - public async Task ResolveByQueryAsync(string query, bool tryResolving) - { - if (tryResolving) - { - var match = YtVideoIdRegex.Match(query); - if (match.Success) - return await ResolveByIdAsync(match.Groups["id"].Value); - } - - Log.Information("Resolving youtube song by search term: {YoutubeQuery}", query); - - var cachedData = await _trackCacher.GetCachedDataByQueryAsync(query, MusicPlatform.Youtube); - if (cachedData is null) - { - var stringData = await _ytdlSearchOperation.GetDataAsync(query); - var trackData = ResolveYtdlData(stringData); - - var trackInfo = DataToInfo(trackData); - await Task.WhenAll( - _trackCacher.CacheTrackDataByQueryAsync(query, trackInfo.ToCachedData(trackData.Id)), - CacheStreamUrlAsync(trackData) - ); - return trackInfo; - } - - return DataToInfo(new YtTrackData( - cachedData.Title, - cachedData.Id, - cachedData.Thumbnail, - null, - cachedData.Duration - )); + Title = title.Trim(); + Id = id.Trim(); + Thumbnail = thumbnail; + StreamUrl = streamUrl; + Duration = duration; } } + + private YtTrackData ResolveYtdlData(string ytdlOutputString) + { + if (string.IsNullOrWhiteSpace(ytdlOutputString)) + return default; + + var dataArray = ytdlOutputString.Trim().Split('\n'); + + if (dataArray.Length < 5) + { + Log.Information("Not enough data received: {YtdlData}", ytdlOutputString); + return default; + } + + if (!TimeSpan.TryParseExact(dataArray[4], durationFormats, CultureInfo.InvariantCulture, out var time)) + { + time = TimeSpan.Zero; + } + + var thumbnail = Uri.IsWellFormedUriString(dataArray[3], UriKind.Absolute) + ? dataArray[3].Trim() + : string.Empty; + + return new YtTrackData( + dataArray[0], + dataArray[1], + thumbnail, + dataArray[2], + time + ); + } + + private ITrackInfo DataToInfo(in YtTrackData trackData) + => new RemoteTrackInfo( + trackData.Title, + $"https://youtube.com/watch?v={trackData.Id}", + trackData.Thumbnail, + trackData.Duration, + MusicPlatform.Youtube, + CreateCacherFactory(trackData.Id)); + + private Func> CreateCacherFactory(string id) + => () => _trackCacher.GetOrCreateStreamLink( + id, + MusicPlatform.Youtube, + async () => await ExtractNewStreamUrlAsync(id) + ); + + private static readonly Regex expiryRegex = new Regex(@"(?:[\?\&]expire\=(?\d+))"); + private static TimeSpan GetExpiry(string streamUrl) + { + var match = expiryRegex.Match(streamUrl); + if (match.Success && double.TryParse(match.Groups["timestamp"].ToString(), out var timestamp)) + { + var realExpiry = (timestamp.ToUnixTimestamp() - DateTime.UtcNow); + if (realExpiry > TimeSpan.FromMinutes(60)) + return realExpiry.Subtract(TimeSpan.FromMinutes(30)); + + return realExpiry; + } + + return TimeSpan.FromHours(1); + } + + private async Task<(string StreamUrl, TimeSpan Expiry)> ExtractNewStreamUrlAsync(string id) + { + var data = await _ytdlIdOperation.GetDataAsync(id); + var trackInfo = ResolveYtdlData(data); + if (string.IsNullOrWhiteSpace(trackInfo.StreamUrl)) + return default; + + return (trackInfo.StreamUrl!, GetExpiry(trackInfo.StreamUrl!)); + } + + public async Task ResolveByIdAsync(string id) + { + id = id.Trim(); + + var cachedData = await _trackCacher.GetCachedDataByIdAsync(id, MusicPlatform.Youtube); + if (cachedData is null) + { + Log.Information("Resolving youtube track by Id: {YoutubeId}", id); + + var data = await _ytdlIdOperation.GetDataAsync(id); + + var trackInfo = ResolveYtdlData(data); + if (string.IsNullOrWhiteSpace(trackInfo.Title)) + return default; + + var toReturn = DataToInfo(in trackInfo); + + await Task.WhenAll( + _trackCacher.CacheTrackDataAsync(toReturn.ToCachedData(id)), + CacheStreamUrlAsync(trackInfo) + ); + + return toReturn; + } + + return DataToInfo(new YtTrackData( + cachedData.Title, + cachedData.Id, + cachedData.Thumbnail, + null, + cachedData.Duration + )); + } + + private Task CacheStreamUrlAsync(YtTrackData trackInfo) + => _trackCacher.CacheStreamUrlAsync( + trackInfo.Id, + MusicPlatform.Youtube, + trackInfo.StreamUrl!, + GetExpiry(trackInfo.StreamUrl!) + ); + + + private static readonly Regex _simplePlaylistRegex + = new Regex(@"&list=(?[\w\-]{12,})", RegexOptions.Compiled); + + public async IAsyncEnumerable ResolveTracksByPlaylistIdAsync(string playlistId) + { + Log.Information("Resolving youtube tracks from playlist: {PlaylistId}", playlistId); + var count = 0; + + var ids = await _trackCacher.GetPlaylistTrackIdsAsync(playlistId, MusicPlatform.Youtube); + if (ids.Count > 0) + { + foreach (var id in ids) + { + var trackInfo = await ResolveByIdAsync(id); + if (trackInfo is null) + continue; + + yield return trackInfo; + } + + yield break; + } + + var data = string.Empty; + var trackIds = new List(); + await foreach (var line in _ytdlPlaylistOperation.EnumerateDataAsync(playlistId)) + { + data += line; + + if (++count == 5) + { + var trackData = ResolveYtdlData(data); + data = string.Empty; + count = 0; + if (string.IsNullOrWhiteSpace(trackData.Id)) + continue; + + var info = DataToInfo(in trackData); + await Task.WhenAll( + _trackCacher.CacheTrackDataAsync(info.ToCachedData(trackData.Id)), + CacheStreamUrlAsync(trackData) + ); + + trackIds.Add(trackData.Id); + yield return info; + } + else + { + data += Environment.NewLine; + } + } + + await _trackCacher.CachePlaylistTrackIdsAsync(playlistId, MusicPlatform.Youtube, trackIds); + } + + public async IAsyncEnumerable ResolveTracksFromPlaylistAsync(string query) + { + string? playlistId; + // try to match playlist id inside the query, if a playlist url has been queried + var match = _simplePlaylistRegex.Match(query); + if (match.Success) + { + // if it's a success, just return from that playlist using the id + playlistId = match.Groups["id"].ToString(); + await foreach (var track in ResolveTracksByPlaylistIdAsync(playlistId)) + yield return track; + + yield break; + } + + // if a query is a search term, try the cache + playlistId = await _trackCacher.GetPlaylistIdByQueryAsync(query, MusicPlatform.Youtube); + if (playlistId is null) + { + // if it's not in the cache + // find playlist id by keyword using google api + try + { + var playlistIds = await _google.GetPlaylistIdsByKeywordsAsync(query); + playlistId = playlistIds.FirstOrDefault(); + } + catch (Exception ex) + { + Log.Warning(ex, "Error Getting playlist id via GoogleApi"); + } + + // if query is not a playlist url + // and query result is not in the cache + // and api returns no values + // it means invalid input has been used, + // or google api key is not provided + if (playlistId is null) + yield break; + } + + // cache the query -> playlist id for fast future lookup + await _trackCacher.CachePlaylistIdByQueryAsync(query, MusicPlatform.Youtube, playlistId); + await foreach (var track in ResolveTracksByPlaylistIdAsync(playlistId)) + yield return track; + } + + public Task ResolveByQueryAsync(string query) + => ResolveByQueryAsync(query, true); + + public async Task ResolveByQueryAsync(string query, bool tryResolving) + { + if (tryResolving) + { + var match = YtVideoIdRegex.Match(query); + if (match.Success) + return await ResolveByIdAsync(match.Groups["id"].Value); + } + + Log.Information("Resolving youtube song by search term: {YoutubeQuery}", query); + + var cachedData = await _trackCacher.GetCachedDataByQueryAsync(query, MusicPlatform.Youtube); + if (cachedData is null) + { + var stringData = await _ytdlSearchOperation.GetDataAsync(query); + var trackData = ResolveYtdlData(stringData); + + var trackInfo = DataToInfo(trackData); + await Task.WhenAll( + _trackCacher.CacheTrackDataByQueryAsync(query, trackInfo.ToCachedData(trackData.Id)), + CacheStreamUrlAsync(trackData) + ); + return trackInfo; + } + + return DataToInfo(new YtTrackData( + cachedData.Title, + cachedData.Id, + cachedData.Thumbnail, + null, + cachedData.Duration + )); + } } \ No newline at end of file diff --git a/src/NadekoBot/Modules/Music/Music.cs b/src/NadekoBot/Modules/Music/Music.cs index 509009256..3775b5524 100644 --- a/src/NadekoBot/Modules/Music/Music.cs +++ b/src/NadekoBot/Modules/Music/Music.cs @@ -1,7 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading; +using System.Threading; using System.Threading.Tasks; using Discord; using Discord.Commands; @@ -12,385 +9,368 @@ using NadekoBot.Extensions; using NadekoBot.Modules.Administration.Services; using NadekoBot.Modules.Music.Services; -namespace NadekoBot.Modules.Music +namespace NadekoBot.Modules.Music; + +[NoPublicBot] +public sealed partial class Music : NadekoModule { - [NoPublicBot] - public sealed partial class Music : NadekoModule + public const string MusicIconUrl = "http://i.imgur.com/nhKS3PT.png"; + private readonly ILogCommandService _logService; + + public Music(ILogCommandService _logService) { - public const string MusicIconUrl = "http://i.imgur.com/nhKS3PT.png"; - private readonly ILogCommandService _logService; - - public Music(ILogCommandService _logService) - { - this._logService = _logService; - } + this._logService = _logService; + } - private async Task ValidateAsync() - { - var user = (IGuildUser) ctx.User; - var userVoiceChannelId = user.VoiceChannel?.Id; + private async Task ValidateAsync() + { + var user = (IGuildUser) ctx.User; + var userVoiceChannelId = user.VoiceChannel?.Id; - if (userVoiceChannelId is null) - { - await ReplyErrorLocalizedAsync(strs.must_be_in_voice); - return false; - } - - var currentUser = await ctx.Guild.GetCurrentUserAsync(); - if (currentUser.VoiceChannel?.Id != userVoiceChannelId) - { - await ReplyErrorLocalizedAsync(strs.not_with_bot_in_voice); - return false; - } - - return true; - } - - private static readonly SemaphoreSlim voiceChannelLock = new SemaphoreSlim(1, 1); - private async Task EnsureBotInVoiceChannelAsync(ulong voiceChannelId, IGuildUser botUser = null) + if (userVoiceChannelId is null) { - botUser ??= await ctx.Guild.GetCurrentUserAsync(); - await voiceChannelLock.WaitAsync(); - try - { - if (botUser.VoiceChannel?.Id is null || !_service.TryGetMusicPlayer(ctx.Guild.Id, out _)) - await _service.JoinVoiceChannelAsync(ctx.Guild.Id, voiceChannelId); - } - finally - { - voiceChannelLock.Release(); - } + await ReplyErrorLocalizedAsync(strs.must_be_in_voice); + return false; } + + var currentUser = await ctx.Guild.GetCurrentUserAsync(); + if (currentUser.VoiceChannel?.Id != userVoiceChannelId) + { + await ReplyErrorLocalizedAsync(strs.not_with_bot_in_voice); + return false; + } + + return true; + } + + private static readonly SemaphoreSlim voiceChannelLock = new SemaphoreSlim(1, 1); + private async Task EnsureBotInVoiceChannelAsync(ulong voiceChannelId, IGuildUser botUser = null) + { + botUser ??= await ctx.Guild.GetCurrentUserAsync(); + await voiceChannelLock.WaitAsync(); + try + { + if (botUser.VoiceChannel?.Id is null || !_service.TryGetMusicPlayer(ctx.Guild.Id, out _)) + await _service.JoinVoiceChannelAsync(ctx.Guild.Id, voiceChannelId); + } + finally + { + voiceChannelLock.Release(); + } + } - private async Task QueuePreconditionInternalAsync() + private async Task QueuePreconditionInternalAsync() + { + var user = (IGuildUser) ctx.User; + var voiceChannelId = user.VoiceChannel?.Id; + + if (voiceChannelId is null) { - var user = (IGuildUser) ctx.User; - var voiceChannelId = user.VoiceChannel?.Id; - - if (voiceChannelId is null) - { - await ReplyErrorLocalizedAsync(strs.must_be_in_voice); - return false; - } - - _ = ctx.Channel.TriggerTypingAsync(); - - var botUser = await ctx.Guild.GetCurrentUserAsync(); - await EnsureBotInVoiceChannelAsync(voiceChannelId!.Value, botUser); - - if (botUser.VoiceChannel?.Id != voiceChannelId) - { - await ReplyErrorLocalizedAsync(strs.not_with_bot_in_voice); - return false; - } - - return true; + await ReplyErrorLocalizedAsync(strs.must_be_in_voice); + return false; } - private async Task QueueByQuery(string query, bool asNext = false, MusicPlatform? forcePlatform = null) + _ = ctx.Channel.TriggerTypingAsync(); + + var botUser = await ctx.Guild.GetCurrentUserAsync(); + await EnsureBotInVoiceChannelAsync(voiceChannelId!.Value, botUser); + + if (botUser.VoiceChannel?.Id != voiceChannelId) { - var succ = await QueuePreconditionInternalAsync(); - if (!succ) - return; + await ReplyErrorLocalizedAsync(strs.not_with_bot_in_voice); + return false; + } + + return true; + } + + private async Task QueueByQuery(string query, bool asNext = false, MusicPlatform? forcePlatform = null) + { + var succ = await QueuePreconditionInternalAsync(); + if (!succ) + return; - var mp = await _service.GetOrCreateMusicPlayerAsync((ITextChannel) ctx.Channel); - if (mp is null) + var mp = await _service.GetOrCreateMusicPlayerAsync((ITextChannel) ctx.Channel); + if (mp is null) + { + await ReplyErrorLocalizedAsync(strs.no_player); + return; + } + + var (trackInfo, index) = await mp.TryEnqueueTrackAsync(query, + ctx.User.ToString(), + asNext, + forcePlatform); + if (trackInfo is null) + { + await ReplyErrorLocalizedAsync(strs.song_not_found); + return; + } + + try + { + var embed = _eb.Create() + .WithOkColor() + .WithAuthor(GetText(strs.queued_song) + " #" + (index + 1), MusicIconUrl) + .WithDescription($"{trackInfo.PrettyName()}\n{GetText(strs.queue)} ") + .WithFooter(trackInfo.Platform.ToString()); + + if (!string.IsNullOrWhiteSpace(trackInfo.Thumbnail)) + embed.WithThumbnailUrl(trackInfo.Thumbnail); + + var queuedMessage = await _service.SendToOutputAsync(ctx.Guild.Id, embed).ConfigureAwait(false); + queuedMessage?.DeleteAfter(10, _logService); + if (mp.IsStopped) { - await ReplyErrorLocalizedAsync(strs.no_player); - return; + var msg = await ReplyPendingLocalizedAsync(strs.queue_stopped(Format.Code(Prefix + "play"))); + msg.DeleteAfter(10, _logService); } + } + catch + { + // ignored + } + } + + private async Task MoveToIndex(int index) + { + if (--index < 0) + return; - var (trackInfo, index) = await mp.TryEnqueueTrackAsync(query, - ctx.User.ToString(), - asNext, - forcePlatform); - if (trackInfo is null) + var succ = await QueuePreconditionInternalAsync(); + if (!succ) + return; + + var mp = await _service.GetOrCreateMusicPlayerAsync((ITextChannel) ctx.Channel); + if (mp is null) + { + await ReplyErrorLocalizedAsync(strs.no_player); + return; + } + + mp.MoveTo(index); + } + + // join vc + [NadekoCommand, Aliases] + [RequireContext(ContextType.Guild)] + public async Task Join() + { + var user = (IGuildUser) ctx.User; + + var voiceChannelId = user.VoiceChannel?.Id; + + if (voiceChannelId is null) + { + await ReplyErrorLocalizedAsync(strs.must_be_in_voice); + return; + } + + await _service.JoinVoiceChannelAsync(user.GuildId, voiceChannelId.Value); + } + + // leave vc (destroy) + [NadekoCommand, Aliases] + [RequireContext(ContextType.Guild)] + public async Task Destroy() + { + var valid = await ValidateAsync(); + if (!valid) + return; + + await _service.LeaveVoiceChannelAsync(ctx.Guild.Id); + } + + // play - no args = next + [NadekoCommand, Aliases] + [RequireContext(ContextType.Guild)] + [Priority(2)] + public Task Play() + => Next(); + + // play - index = skip to that index + [NadekoCommand, Aliases] + [RequireContext(ContextType.Guild)] + [Priority(1)] + public Task Play(int index) + => MoveToIndex(index); + + // play - query = q(query) + [NadekoCommand, Aliases] + [RequireContext(ContextType.Guild)] + [Priority(0)] + public Task Play([Leftover] string query) + => QueueByQuery(query); + + [NadekoCommand, Aliases] + [RequireContext(ContextType.Guild)] + public Task Queue([Leftover] string query) + => QueueByQuery(query); + + [NadekoCommand, Aliases] + [RequireContext(ContextType.Guild)] + public Task QueueNext([Leftover] string query) + => QueueByQuery(query, asNext: true); + + [NadekoCommand, Aliases] + [RequireContext(ContextType.Guild)] + public async Task Volume(int vol) + { + if (vol < 0 || vol > 100) + { + await ReplyErrorLocalizedAsync(strs.volume_input_invalid); + return; + } + + var valid = await ValidateAsync(); + if (!valid) + return; + + await _service.SetVolumeAsync(ctx.Guild.Id, vol); + await ReplyConfirmLocalizedAsync(strs.volume_set(vol)); + } + + [NadekoCommand, Aliases] + [RequireContext(ContextType.Guild)] + public async Task Next() + { + var valid = await ValidateAsync(); + if (!valid) + return; + + var success = await _service.PlayAsync(ctx.Guild.Id, ((IGuildUser)ctx.User).VoiceChannel.Id); + if (!success) + { + await ReplyErrorLocalizedAsync(strs.no_player); + return; + } + } + + private const int LQ_ITEMS_PER_PAGE = 9; + + // list queue, relevant page + [NadekoCommand, Aliases] + [RequireContext(ContextType.Guild)] + public async Task ListQueue() + { + // show page with the current song + if (!_service.TryGetMusicPlayer(ctx.Guild.Id, out var mp)) + { + await ReplyErrorLocalizedAsync(strs.no_player); + return; + } + + await ListQueue(mp.CurrentIndex / LQ_ITEMS_PER_PAGE + 1); + } + + // list queue, specify page + [NadekoCommand, Aliases] + [RequireContext(ContextType.Guild)] + public async Task ListQueue(int page) + { + if (--page < 0) + return; + + IReadOnlyCollection tracks; + if (!_service.TryGetMusicPlayer(ctx.Guild.Id, out var mp) || (tracks = mp.GetQueuedTracks()).Count == 0) + { + await ReplyErrorLocalizedAsync(strs.no_player); + return; + } + + IEmbedBuilder printAction(int curPage) + { + string desc = string.Empty; + var current = mp.GetCurrentTrack(out var currentIndex); + if (current is not null) { - await ReplyErrorLocalizedAsync(strs.song_not_found); - return; + desc = $"`🔊` {current.PrettyFullName()}\n\n" + desc; } - try + var repeatType = mp.Repeat; + var add = ""; + if (mp.IsStopped) + add += Format.Bold(GetText(strs.queue_stopped(Format.Code(Prefix + "play")))) + "\n"; + // var mps = mp.MaxPlaytimeSeconds; + // if (mps > 0) + // add += Format.Bold(GetText(strs.song_skips_after(TimeSpan.FromSeconds(mps).ToString("HH\\:mm\\:ss")))) + "\n"; + if (repeatType == PlayerRepeatType.Track) { - var embed = _eb.Create() - .WithOkColor() - .WithAuthor(GetText(strs.queued_song) + " #" + (index + 1), MusicIconUrl) - .WithDescription($"{trackInfo.PrettyName()}\n{GetText(strs.queue)} ") - .WithFooter(trackInfo.Platform.ToString()); + add += "🔂 " + GetText(strs.repeating_track) + "\n"; + } + else + { + // if (mp.Autoplay) + // add += "↪ " + GetText(strs.autoplaying) + "\n"; + // if (mp.FairPlay && !mp.Autoplay) + // add += " " + GetText(strs.fairplay) + "\n"; + if (repeatType == PlayerRepeatType.Queue) + add += "🔁 " + GetText(strs.repeating_queue) + "\n"; + } - if (!string.IsNullOrWhiteSpace(trackInfo.Thumbnail)) - embed.WithThumbnailUrl(trackInfo.Thumbnail); - var queuedMessage = await _service.SendToOutputAsync(ctx.Guild.Id, embed).ConfigureAwait(false); - queuedMessage?.DeleteAfter(10, _logService); - if (mp.IsStopped) + desc += tracks + .Skip(LQ_ITEMS_PER_PAGE * curPage) + .Take(LQ_ITEMS_PER_PAGE) + .Select((v, index) => { - var msg = await ReplyPendingLocalizedAsync(strs.queue_stopped(Format.Code(Prefix + "play"))); - msg.DeleteAfter(10, _logService); - } - } - catch - { - // ignored - } - } - - private async Task MoveToIndex(int index) - { - if (--index < 0) - return; - - var succ = await QueuePreconditionInternalAsync(); - if (!succ) - return; - - var mp = await _service.GetOrCreateMusicPlayerAsync((ITextChannel) ctx.Channel); - if (mp is null) - { - await ReplyErrorLocalizedAsync(strs.no_player); - return; - } - - mp.MoveTo(index); - } - - // join vc - [NadekoCommand, Aliases] - [RequireContext(ContextType.Guild)] - public async Task Join() - { - var user = (IGuildUser) ctx.User; - - var voiceChannelId = user.VoiceChannel?.Id; - - if (voiceChannelId is null) - { - await ReplyErrorLocalizedAsync(strs.must_be_in_voice); - return; - } - - await _service.JoinVoiceChannelAsync(user.GuildId, voiceChannelId.Value); - } - - // leave vc (destroy) - [NadekoCommand, Aliases] - [RequireContext(ContextType.Guild)] - public async Task Destroy() - { - var valid = await ValidateAsync(); - if (!valid) - return; - - await _service.LeaveVoiceChannelAsync(ctx.Guild.Id); - } - - // play - no args = next - [NadekoCommand, Aliases] - [RequireContext(ContextType.Guild)] - [Priority(2)] - public Task Play() - => Next(); - - // play - index = skip to that index - [NadekoCommand, Aliases] - [RequireContext(ContextType.Guild)] - [Priority(1)] - public Task Play(int index) - => MoveToIndex(index); - - // play - query = q(query) - [NadekoCommand, Aliases] - [RequireContext(ContextType.Guild)] - [Priority(0)] - public Task Play([Leftover] string query) - => QueueByQuery(query); - - [NadekoCommand, Aliases] - [RequireContext(ContextType.Guild)] - public Task Queue([Leftover] string query) - => QueueByQuery(query); - - [NadekoCommand, Aliases] - [RequireContext(ContextType.Guild)] - public Task QueueNext([Leftover] string query) - => QueueByQuery(query, asNext: true); - - [NadekoCommand, Aliases] - [RequireContext(ContextType.Guild)] - public async Task Volume(int vol) - { - if (vol < 0 || vol > 100) - { - await ReplyErrorLocalizedAsync(strs.volume_input_invalid); - return; - } - - var valid = await ValidateAsync(); - if (!valid) - return; - - await _service.SetVolumeAsync(ctx.Guild.Id, vol); - await ReplyConfirmLocalizedAsync(strs.volume_set(vol)); - } - - [NadekoCommand, Aliases] - [RequireContext(ContextType.Guild)] - public async Task Next() - { - var valid = await ValidateAsync(); - if (!valid) - return; - - var success = await _service.PlayAsync(ctx.Guild.Id, ((IGuildUser)ctx.User).VoiceChannel.Id); - if (!success) - { - await ReplyErrorLocalizedAsync(strs.no_player); - return; - } - } - - private const int LQ_ITEMS_PER_PAGE = 9; - - // list queue, relevant page - [NadekoCommand, Aliases] - [RequireContext(ContextType.Guild)] - public async Task ListQueue() - { - // show page with the current song - if (!_service.TryGetMusicPlayer(ctx.Guild.Id, out var mp)) - { - await ReplyErrorLocalizedAsync(strs.no_player); - return; - } - - await ListQueue(mp.CurrentIndex / LQ_ITEMS_PER_PAGE + 1); - } - - // list queue, specify page - [NadekoCommand, Aliases] - [RequireContext(ContextType.Guild)] - public async Task ListQueue(int page) - { - if (--page < 0) - return; - - IReadOnlyCollection tracks; - if (!_service.TryGetMusicPlayer(ctx.Guild.Id, out var mp) || (tracks = mp.GetQueuedTracks()).Count == 0) - { - await ReplyErrorLocalizedAsync(strs.no_player); - return; - } - - IEmbedBuilder printAction(int curPage) - { - string desc = string.Empty; - var current = mp.GetCurrentTrack(out var currentIndex); - if (current is not null) - { - desc = $"`🔊` {current.PrettyFullName()}\n\n" + desc; - } - - var repeatType = mp.Repeat; - var add = ""; - if (mp.IsStopped) - add += Format.Bold(GetText(strs.queue_stopped(Format.Code(Prefix + "play")))) + "\n"; - // var mps = mp.MaxPlaytimeSeconds; - // if (mps > 0) - // add += Format.Bold(GetText(strs.song_skips_after(TimeSpan.FromSeconds(mps).ToString("HH\\:mm\\:ss")))) + "\n"; - if (repeatType == PlayerRepeatType.Track) - { - add += "🔂 " + GetText(strs.repeating_track) + "\n"; - } - else - { - // if (mp.Autoplay) - // add += "↪ " + GetText(strs.autoplaying) + "\n"; - // if (mp.FairPlay && !mp.Autoplay) - // add += " " + GetText(strs.fairplay) + "\n"; - if (repeatType == PlayerRepeatType.Queue) - add += "🔁 " + GetText(strs.repeating_queue) + "\n"; - } - - - desc += tracks - .Skip(LQ_ITEMS_PER_PAGE * curPage) - .Take(LQ_ITEMS_PER_PAGE) - .Select((v, index) => - { - index += LQ_ITEMS_PER_PAGE * curPage; - if (index == currentIndex) - return $"**⇒**`{index + 1}.` {v.PrettyFullName()}"; + index += LQ_ITEMS_PER_PAGE * curPage; + if (index == currentIndex) + return $"**⇒**`{index + 1}.` {v.PrettyFullName()}"; - return $"`{index + 1}.` {v.PrettyFullName()}"; - }) - .JoinWith('\n'); + return $"`{index + 1}.` {v.PrettyFullName()}"; + }) + .JoinWith('\n'); - if (!string.IsNullOrWhiteSpace(add)) - desc = add + "\n" + desc; + if (!string.IsNullOrWhiteSpace(add)) + desc = add + "\n" + desc; - var embed = _eb.Create() - .WithAuthor(GetText(strs.player_queue(curPage + 1, (tracks.Count / LQ_ITEMS_PER_PAGE) + 1)), - MusicIconUrl) - .WithDescription(desc) - .WithFooter($" {mp.PrettyVolume()} | 🎶 {tracks.Count} | ⌛ {mp.PrettyTotalTime()} ") - .WithOkColor(); + var embed = _eb.Create() + .WithAuthor(GetText(strs.player_queue(curPage + 1, (tracks.Count / LQ_ITEMS_PER_PAGE) + 1)), + MusicIconUrl) + .WithDescription(desc) + .WithFooter($" {mp.PrettyVolume()} | 🎶 {tracks.Count} | ⌛ {mp.PrettyTotalTime()} ") + .WithOkColor(); - return embed; - } - - await ctx.SendPaginatedConfirmAsync( - page, - printAction, - tracks.Count, - LQ_ITEMS_PER_PAGE, - false); + return embed; } - // search - [NadekoCommand, Aliases] - [RequireContext(ContextType.Guild)] - public async Task QueueSearch([Leftover] string query) + await ctx.SendPaginatedConfirmAsync( + page, + printAction, + tracks.Count, + LQ_ITEMS_PER_PAGE, + false); + } + + // search + [NadekoCommand, Aliases] + [RequireContext(ContextType.Guild)] + public async Task QueueSearch([Leftover] string query) + { + _ = ctx.Channel.TriggerTypingAsync(); + + var videos = await _service.SearchVideosAsync(query); + + if (videos is null || videos.Count == 0) { - _ = ctx.Channel.TriggerTypingAsync(); + await ReplyErrorLocalizedAsync(strs.song_not_found).ConfigureAwait(false); + return; + } - var videos = await _service.SearchVideosAsync(query); - - if (videos is null || videos.Count == 0) - { - await ReplyErrorLocalizedAsync(strs.song_not_found).ConfigureAwait(false); - return; - } - - var resultsString = videos - .Select((x, i) => $"`{i + 1}.`\n\t{Format.Bold(x.Title)}\n\t{x.Url}") - .JoinWith('\n'); + var resultsString = videos + .Select((x, i) => $"`{i + 1}.`\n\t{Format.Bold(x.Title)}\n\t{x.Url}") + .JoinWith('\n'); - var msg = await SendConfirmAsync(resultsString); + var msg = await SendConfirmAsync(resultsString); - try - { - var input = await GetUserInputAsync(ctx.User.Id, ctx.Channel.Id).ConfigureAwait(false); - if (input is null - || !int.TryParse(input, out var index) - || (index -= 1) < 0 - || index >= videos.Count) - { - _logService.AddDeleteIgnore(msg.Id); - try - { - await msg.DeleteAsync().ConfigureAwait(false); - } - catch - { - } - - return; - } - query = videos[index].Url; - - await Play(query); - } - finally + try + { + var input = await GetUserInputAsync(ctx.User.Id, ctx.Channel.Id).ConfigureAwait(false); + if (input is null + || !int.TryParse(input, out var index) + || (index -= 1) < 0 + || index >= videos.Count) { _logService.AddDeleteIgnore(msg.Id); try @@ -400,383 +380,399 @@ namespace NadekoBot.Modules.Music catch { } - } - } - [NadekoCommand, Aliases] - [RequireContext(ContextType.Guild)] - [Priority(1)] - public async Task TrackRemove(int index) + return; + } + query = videos[index].Url; + + await Play(query); + } + finally { - if (index < 1) + _logService.AddDeleteIgnore(msg.Id); + try { - await ReplyErrorLocalizedAsync(strs.removed_song_error).ConfigureAwait(false); - return; + await msg.DeleteAsync().ConfigureAwait(false); } - - var valid = await ValidateAsync(); - if (!valid) - return; + catch + { + } + } + } - if (!_service.TryGetMusicPlayer(ctx.Guild.Id, out var mp)) - { - await ReplyErrorLocalizedAsync(strs.no_player); - return; - } + [NadekoCommand, Aliases] + [RequireContext(ContextType.Guild)] + [Priority(1)] + public async Task TrackRemove(int index) + { + if (index < 1) + { + await ReplyErrorLocalizedAsync(strs.removed_song_error).ConfigureAwait(false); + return; + } - if (!mp.TryRemoveTrackAt(index - 1, out var song)) - { - await ReplyErrorLocalizedAsync(strs.removed_song_error).ConfigureAwait(false); - return; - } - - var embed = _eb.Create() - .WithAuthor(GetText(strs.removed_song) + " #" + (index), MusicIconUrl) - .WithDescription(song.PrettyName()) - .WithFooter(song.PrettyInfo()) - .WithErrorColor(); + var valid = await ValidateAsync(); + if (!valid) + return; - await _service.SendToOutputAsync(ctx.Guild.Id, embed); + if (!_service.TryGetMusicPlayer(ctx.Guild.Id, out var mp)) + { + await ReplyErrorLocalizedAsync(strs.no_player); + return; + } + + if (!mp.TryRemoveTrackAt(index - 1, out var song)) + { + await ReplyErrorLocalizedAsync(strs.removed_song_error).ConfigureAwait(false); + return; + } + + var embed = _eb.Create() + .WithAuthor(GetText(strs.removed_song) + " #" + (index), MusicIconUrl) + .WithDescription(song.PrettyName()) + .WithFooter(song.PrettyInfo()) + .WithErrorColor(); + + await _service.SendToOutputAsync(ctx.Guild.Id, embed); + } + + public enum All { All = -1 } + [NadekoCommand, Aliases] + [RequireContext(ContextType.Guild)] + [Priority(0)] + public async Task TrackRemove(All _ = All.All) + { + var valid = await ValidateAsync(); + if (!valid) + return; + + if (!_service.TryGetMusicPlayer(ctx.Guild.Id, out var mp)) + { + await ReplyErrorLocalizedAsync(strs.no_player); + return; + } + + mp.Clear(); + await ReplyConfirmLocalizedAsync(strs.queue_cleared).ConfigureAwait(false); + } + + [NadekoCommand, Aliases] + [RequireContext(ContextType.Guild)] + public async Task Stop() + { + var valid = await ValidateAsync(); + if (!valid) + return; + + if (!_service.TryGetMusicPlayer(ctx.Guild.Id, out var mp)) + { + await ReplyErrorLocalizedAsync(strs.no_player); + return; + } + + mp.Stop(); + } + + public enum InputRepeatType + { + N = 0, No = 0, None = 0, + T = 1, Track = 1, S = 1, Song = 1, + Q = 2, Queue = 2, Playlist = 2, Pl = 2, + } + + private PlayerRepeatType InputToDbType(InputRepeatType type) => type switch + { + InputRepeatType.None => PlayerRepeatType.None, + InputRepeatType.Queue => PlayerRepeatType.Queue, + InputRepeatType.Track => PlayerRepeatType.Track, + _ => PlayerRepeatType.Queue + }; + + [NadekoCommand, Aliases] + [RequireContext(ContextType.Guild)] + public async Task QueueRepeat(InputRepeatType type = InputRepeatType.Queue) + { + var valid = await ValidateAsync(); + if (!valid) + return; + + await _service.SetRepeatAsync(ctx.Guild.Id, InputToDbType(type)); + + if (type == InputRepeatType.None) + await ReplyConfirmLocalizedAsync(strs.repeating_none); + else if (type == InputRepeatType.Queue) + await ReplyConfirmLocalizedAsync(strs.repeating_queue); + else + await ReplyConfirmLocalizedAsync(strs.repeating_track); + } + + [NadekoCommand, Aliases] + [RequireContext(ContextType.Guild)] + public async Task ReptCurSong() + { + await ReplyPendingLocalizedAsync(strs.obsolete_use($"`{Prefix}qrp song`")); + await QueueRepeat(InputRepeatType.Song); + } + + [NadekoCommand, Aliases] + [RequireContext(ContextType.Guild)] + public async Task Pause() + { + var valid = await ValidateAsync(); + if (!valid) + return; + + if (!_service.TryGetMusicPlayer(ctx.Guild.Id, out var mp) || mp.GetCurrentTrack(out _) is null) + { + await ReplyErrorLocalizedAsync(strs.no_player); + return; } - public enum All { All = -1 } - [NadekoCommand, Aliases] - [RequireContext(ContextType.Guild)] - [Priority(0)] - public async Task TrackRemove(All _ = All.All) - { - var valid = await ValidateAsync(); - if (!valid) - return; - - if (!_service.TryGetMusicPlayer(ctx.Guild.Id, out var mp)) - { - await ReplyErrorLocalizedAsync(strs.no_player); - return; - } - - mp.Clear(); - await ReplyConfirmLocalizedAsync(strs.queue_cleared).ConfigureAwait(false); - } + mp.TogglePause(); + } - [NadekoCommand, Aliases] - [RequireContext(ContextType.Guild)] - public async Task Stop() - { - var valid = await ValidateAsync(); - if (!valid) - return; + [NadekoCommand, Aliases] + [RequireContext(ContextType.Guild)] + public Task Radio(string radioLink) + => QueueByQuery(radioLink, false, MusicPlatform.Radio); - if (!_service.TryGetMusicPlayer(ctx.Guild.Id, out var mp)) - { - await ReplyErrorLocalizedAsync(strs.no_player); - return; - } - - mp.Stop(); - } + [NadekoCommand, Aliases] + [RequireContext(ContextType.Guild)] + [OwnerOnly] + public Task Local([Leftover] string path) + => QueueByQuery(path, false, MusicPlatform.Local); - public enum InputRepeatType - { - N = 0, No = 0, None = 0, - T = 1, Track = 1, S = 1, Song = 1, - Q = 2, Queue = 2, Playlist = 2, Pl = 2, - } + [NadekoCommand, Aliases] + [RequireContext(ContextType.Guild)] + [OwnerOnly] + public async Task LocalPlaylist([Leftover] string dirPath) + { + if (string.IsNullOrWhiteSpace(dirPath)) + return; - private PlayerRepeatType InputToDbType(InputRepeatType type) => type switch - { - InputRepeatType.None => PlayerRepeatType.None, - InputRepeatType.Queue => PlayerRepeatType.Queue, - InputRepeatType.Track => PlayerRepeatType.Track, - _ => PlayerRepeatType.Queue - }; - - [NadekoCommand, Aliases] - [RequireContext(ContextType.Guild)] - public async Task QueueRepeat(InputRepeatType type = InputRepeatType.Queue) - { - var valid = await ValidateAsync(); - if (!valid) - return; - - await _service.SetRepeatAsync(ctx.Guild.Id, InputToDbType(type)); - - if (type == InputRepeatType.None) - await ReplyConfirmLocalizedAsync(strs.repeating_none); - else if (type == InputRepeatType.Queue) - await ReplyConfirmLocalizedAsync(strs.repeating_queue); - else - await ReplyConfirmLocalizedAsync(strs.repeating_track); - } - - [NadekoCommand, Aliases] - [RequireContext(ContextType.Guild)] - public async Task ReptCurSong() - { - await ReplyPendingLocalizedAsync(strs.obsolete_use($"`{Prefix}qrp song`")); - await QueueRepeat(InputRepeatType.Song); - } - - [NadekoCommand, Aliases] - [RequireContext(ContextType.Guild)] - public async Task Pause() - { - var valid = await ValidateAsync(); - if (!valid) - return; - - if (!_service.TryGetMusicPlayer(ctx.Guild.Id, out var mp) || mp.GetCurrentTrack(out _) is null) - { - await ReplyErrorLocalizedAsync(strs.no_player); - return; - } - - mp.TogglePause(); - } - - [NadekoCommand, Aliases] - [RequireContext(ContextType.Guild)] - public Task Radio(string radioLink) - => QueueByQuery(radioLink, false, MusicPlatform.Radio); - - [NadekoCommand, Aliases] - [RequireContext(ContextType.Guild)] - [OwnerOnly] - public Task Local([Leftover] string path) - => QueueByQuery(path, false, MusicPlatform.Local); - - [NadekoCommand, Aliases] - [RequireContext(ContextType.Guild)] - [OwnerOnly] - public async Task LocalPlaylist([Leftover] string dirPath) - { - if (string.IsNullOrWhiteSpace(dirPath)) - return; - - var user = (IGuildUser) ctx.User; - var voiceChannelId = user.VoiceChannel?.Id; + var user = (IGuildUser) ctx.User; + var voiceChannelId = user.VoiceChannel?.Id; - if (voiceChannelId is null) - { - await ReplyErrorLocalizedAsync(strs.must_be_in_voice); - return; - } + if (voiceChannelId is null) + { + await ReplyErrorLocalizedAsync(strs.must_be_in_voice); + return; + } - _ = ctx.Channel.TriggerTypingAsync(); + _ = ctx.Channel.TriggerTypingAsync(); - var botUser = await ctx.Guild.GetCurrentUserAsync(); - await EnsureBotInVoiceChannelAsync(voiceChannelId!.Value, botUser); + var botUser = await ctx.Guild.GetCurrentUserAsync(); + await EnsureBotInVoiceChannelAsync(voiceChannelId!.Value, botUser); - if (botUser.VoiceChannel?.Id != voiceChannelId) - { - await ReplyErrorLocalizedAsync(strs.not_with_bot_in_voice); - return; - } + if (botUser.VoiceChannel?.Id != voiceChannelId) + { + await ReplyErrorLocalizedAsync(strs.not_with_bot_in_voice); + return; + } - var mp = await _service.GetOrCreateMusicPlayerAsync((ITextChannel) ctx.Channel); - if (mp is null) - { - await ReplyErrorLocalizedAsync(strs.no_player); - return; - } + var mp = await _service.GetOrCreateMusicPlayerAsync((ITextChannel) ctx.Channel); + if (mp is null) + { + await ReplyErrorLocalizedAsync(strs.no_player); + return; + } - await _service.EnqueueDirectoryAsync(mp, dirPath, ctx.User.ToString()); + await _service.EnqueueDirectoryAsync(mp, dirPath, ctx.User.ToString()); - await ReplyConfirmLocalizedAsync(strs.dir_queue_complete).ConfigureAwait(false); - } + await ReplyConfirmLocalizedAsync(strs.dir_queue_complete).ConfigureAwait(false); + } - [NadekoCommand, Aliases] - [RequireContext(ContextType.Guild)] - public async Task MoveSong(int from, int to) - { - if (--from < 0 || --to < 0 || from == to) - { - await ReplyErrorLocalizedAsync(strs.invalid_input).ConfigureAwait(false); - return; - } + [NadekoCommand, Aliases] + [RequireContext(ContextType.Guild)] + public async Task MoveSong(int from, int to) + { + if (--from < 0 || --to < 0 || from == to) + { + await ReplyErrorLocalizedAsync(strs.invalid_input).ConfigureAwait(false); + return; + } - var valid = await ValidateAsync(); - if (!valid) - return; + var valid = await ValidateAsync(); + if (!valid) + return; - var mp = await _service.GetOrCreateMusicPlayerAsync((ITextChannel) ctx.Channel); - if (mp is null) - { - await ReplyErrorLocalizedAsync(strs.no_player); - return; - } + var mp = await _service.GetOrCreateMusicPlayerAsync((ITextChannel) ctx.Channel); + if (mp is null) + { + await ReplyErrorLocalizedAsync(strs.no_player); + return; + } - var track = mp.MoveTrack(from, to); - if (track is null) - { - await ReplyErrorLocalizedAsync(strs.invalid_input).ConfigureAwait(false); - return; - } + var track = mp.MoveTrack(from, to); + if (track is null) + { + await ReplyErrorLocalizedAsync(strs.invalid_input).ConfigureAwait(false); + return; + } - var embed = _eb.Create() - .WithTitle(track.Title.TrimTo(65)) - .WithAuthor(GetText(strs.song_moved), MusicIconUrl) - .AddField(GetText(strs.from_position), $"#{from + 1}", true) - .AddField(GetText(strs.to_position), $"#{to + 1}", true) - .WithOkColor(); + var embed = _eb.Create() + .WithTitle(track.Title.TrimTo(65)) + .WithAuthor(GetText(strs.song_moved), MusicIconUrl) + .AddField(GetText(strs.from_position), $"#{from + 1}", true) + .AddField(GetText(strs.to_position), $"#{to + 1}", true) + .WithOkColor(); - if (Uri.IsWellFormedUriString(track.Url, UriKind.Absolute)) - embed.WithUrl(track.Url); + if (Uri.IsWellFormedUriString(track.Url, UriKind.Absolute)) + embed.WithUrl(track.Url); - await ctx.Channel.EmbedAsync(embed).ConfigureAwait(false); - } + await ctx.Channel.EmbedAsync(embed).ConfigureAwait(false); + } - [NadekoCommand, Aliases] - [RequireContext(ContextType.Guild)] - public Task SoundCloudQueue([Leftover] string query) - => QueueByQuery(query, false, MusicPlatform.SoundCloud); + [NadekoCommand, Aliases] + [RequireContext(ContextType.Guild)] + public Task SoundCloudQueue([Leftover] string query) + => QueueByQuery(query, false, MusicPlatform.SoundCloud); - [NadekoCommand, Aliases] - [RequireContext(ContextType.Guild)] - public async Task SoundCloudPl([Leftover] string playlist) - { - if (string.IsNullOrWhiteSpace(playlist)) - return; + [NadekoCommand, Aliases] + [RequireContext(ContextType.Guild)] + public async Task SoundCloudPl([Leftover] string playlist) + { + if (string.IsNullOrWhiteSpace(playlist)) + return; - var succ = await QueuePreconditionInternalAsync(); - if (!succ) - return; + var succ = await QueuePreconditionInternalAsync(); + if (!succ) + return; - var mp = await _service.GetOrCreateMusicPlayerAsync((ITextChannel) ctx.Channel); - if (mp is null) - { - await ReplyErrorLocalizedAsync(strs.no_player); - return; - } + var mp = await _service.GetOrCreateMusicPlayerAsync((ITextChannel) ctx.Channel); + if (mp is null) + { + await ReplyErrorLocalizedAsync(strs.no_player); + return; + } - _ = ctx.Channel.TriggerTypingAsync(); + _ = ctx.Channel.TriggerTypingAsync(); - await _service.EnqueueSoundcloudPlaylistAsync(mp, playlist, ctx.User.ToString()); + await _service.EnqueueSoundcloudPlaylistAsync(mp, playlist, ctx.User.ToString()); - await ctx.OkAsync(); - } + await ctx.OkAsync(); + } - [NadekoCommand, Aliases] - [RequireContext(ContextType.Guild)] - public async Task Playlist([Leftover] string playlistQuery) - { - if (string.IsNullOrWhiteSpace(playlistQuery)) - return; + [NadekoCommand, Aliases] + [RequireContext(ContextType.Guild)] + public async Task Playlist([Leftover] string playlistQuery) + { + if (string.IsNullOrWhiteSpace(playlistQuery)) + return; - var succ = await QueuePreconditionInternalAsync(); - if (!succ) - return; + var succ = await QueuePreconditionInternalAsync(); + if (!succ) + return; - var mp = await _service.GetOrCreateMusicPlayerAsync((ITextChannel) ctx.Channel); - if (mp is null) - { - await ReplyErrorLocalizedAsync(strs.no_player); - return; - } + var mp = await _service.GetOrCreateMusicPlayerAsync((ITextChannel) ctx.Channel); + if (mp is null) + { + await ReplyErrorLocalizedAsync(strs.no_player); + return; + } - _ = ctx.Channel.TriggerTypingAsync(); + _ = ctx.Channel.TriggerTypingAsync(); - var queuedCount = await _service.EnqueueYoutubePlaylistAsync(mp, playlistQuery, ctx.User.ToString()); - if (queuedCount == 0) - { - await ReplyErrorLocalizedAsync(strs.no_search_results).ConfigureAwait(false); - return; - } - await ctx.OkAsync(); - } + var queuedCount = await _service.EnqueueYoutubePlaylistAsync(mp, playlistQuery, ctx.User.ToString()); + if (queuedCount == 0) + { + await ReplyErrorLocalizedAsync(strs.no_search_results).ConfigureAwait(false); + return; + } + await ctx.OkAsync(); + } - [NadekoCommand, Aliases] - [RequireContext(ContextType.Guild)] - public async Task NowPlaying() - { - var mp = await _service.GetOrCreateMusicPlayerAsync((ITextChannel) ctx.Channel); - if (mp is null) - { - await ReplyErrorLocalizedAsync(strs.no_player); - return; - } + [NadekoCommand, Aliases] + [RequireContext(ContextType.Guild)] + public async Task NowPlaying() + { + var mp = await _service.GetOrCreateMusicPlayerAsync((ITextChannel) ctx.Channel); + if (mp is null) + { + await ReplyErrorLocalizedAsync(strs.no_player); + return; + } - var currentTrack = mp.GetCurrentTrack(out _); - if (currentTrack is null) - return; + var currentTrack = mp.GetCurrentTrack(out _); + if (currentTrack is null) + return; - var embed = _eb.Create().WithOkColor() - .WithAuthor(GetText(strs.now_playing), MusicIconUrl) - .WithDescription(currentTrack.PrettyName()) - .WithThumbnailUrl(currentTrack.Thumbnail) - .WithFooter($"{mp.PrettyVolume()} | {mp.PrettyTotalTime()} | {currentTrack.Platform} | {currentTrack.Queuer}"); + var embed = _eb.Create().WithOkColor() + .WithAuthor(GetText(strs.now_playing), MusicIconUrl) + .WithDescription(currentTrack.PrettyName()) + .WithThumbnailUrl(currentTrack.Thumbnail) + .WithFooter($"{mp.PrettyVolume()} | {mp.PrettyTotalTime()} | {currentTrack.Platform} | {currentTrack.Queuer}"); - await ctx.Channel.EmbedAsync(embed).ConfigureAwait(false); - } + await ctx.Channel.EmbedAsync(embed).ConfigureAwait(false); + } - [NadekoCommand, Aliases] - [RequireContext(ContextType.Guild)] - public async Task PlaylistShuffle() - { - var valid = await ValidateAsync(); - if (!valid) - return; + [NadekoCommand, Aliases] + [RequireContext(ContextType.Guild)] + public async Task PlaylistShuffle() + { + var valid = await ValidateAsync(); + if (!valid) + return; - var mp = await _service.GetOrCreateMusicPlayerAsync((ITextChannel) ctx.Channel); - if (mp is null) - { - await ReplyErrorLocalizedAsync(strs.no_player); - return; - } + var mp = await _service.GetOrCreateMusicPlayerAsync((ITextChannel) ctx.Channel); + if (mp is null) + { + await ReplyErrorLocalizedAsync(strs.no_player); + return; + } - mp.ShuffleQueue(); - await ReplyConfirmLocalizedAsync(strs.queue_shuffled); - } + mp.ShuffleQueue(); + await ReplyConfirmLocalizedAsync(strs.queue_shuffled); + } - [NadekoCommand, Aliases] - [RequireContext(ContextType.Guild)] - [UserPerm(GuildPerm.ManageMessages)] - public async Task SetMusicChannel() - { - await _service.SetMusicChannelAsync(ctx.Guild.Id, ctx.Channel.Id); + [NadekoCommand, Aliases] + [RequireContext(ContextType.Guild)] + [UserPerm(GuildPerm.ManageMessages)] + public async Task SetMusicChannel() + { + await _service.SetMusicChannelAsync(ctx.Guild.Id, ctx.Channel.Id); - await ReplyConfirmLocalizedAsync(strs.set_music_channel); - } + await ReplyConfirmLocalizedAsync(strs.set_music_channel); + } - [NadekoCommand, Aliases] - [RequireContext(ContextType.Guild)] - [UserPerm(GuildPerm.ManageMessages)] - public async Task UnsetMusicChannel() - { - await _service.SetMusicChannelAsync(ctx.Guild.Id, null); + [NadekoCommand, Aliases] + [RequireContext(ContextType.Guild)] + [UserPerm(GuildPerm.ManageMessages)] + public async Task UnsetMusicChannel() + { + await _service.SetMusicChannelAsync(ctx.Guild.Id, null); - await ReplyConfirmLocalizedAsync(strs.unset_music_channel); - } + await ReplyConfirmLocalizedAsync(strs.unset_music_channel); + } - [NadekoCommand, Aliases] - [RequireContext(ContextType.Guild)] - public async Task AutoDisconnect() - { - var newState = await _service.ToggleAutoDisconnectAsync(ctx.Guild.Id); + [NadekoCommand, Aliases] + [RequireContext(ContextType.Guild)] + public async Task AutoDisconnect() + { + var newState = await _service.ToggleAutoDisconnectAsync(ctx.Guild.Id); - if(newState) - await ReplyConfirmLocalizedAsync(strs.autodc_enable); - else - await ReplyConfirmLocalizedAsync(strs.autodc_disable); - } + if(newState) + await ReplyConfirmLocalizedAsync(strs.autodc_enable); + else + await ReplyConfirmLocalizedAsync(strs.autodc_disable); + } - [NadekoCommand, Aliases] - [RequireContext(ContextType.Guild)] - [UserPerm(GuildPerm.Administrator)] - public async Task MusicQuality() - { - var quality = await _service.GetMusicQualityAsync(ctx.Guild.Id); - await ReplyConfirmLocalizedAsync(strs.current_music_quality(Format.Bold(quality.ToString()))); - } + [NadekoCommand, Aliases] + [RequireContext(ContextType.Guild)] + [UserPerm(GuildPerm.Administrator)] + public async Task MusicQuality() + { + var quality = await _service.GetMusicQualityAsync(ctx.Guild.Id); + await ReplyConfirmLocalizedAsync(strs.current_music_quality(Format.Bold(quality.ToString()))); + } - [NadekoCommand, Aliases] - [RequireContext(ContextType.Guild)] - [UserPerm(GuildPerm.Administrator)] - public async Task MusicQuality(QualityPreset preset) - { - await _service.SetMusicQualityAsync(ctx.Guild.Id, preset); - await ReplyConfirmLocalizedAsync(strs.music_quality_set(Format.Bold(preset.ToString()))); - } + [NadekoCommand, Aliases] + [RequireContext(ContextType.Guild)] + [UserPerm(GuildPerm.Administrator)] + public async Task MusicQuality(QualityPreset preset) + { + await _service.SetMusicQualityAsync(ctx.Guild.Id, preset); + await ReplyConfirmLocalizedAsync(strs.music_quality_set(Format.Bold(preset.ToString()))); } } \ No newline at end of file diff --git a/src/NadekoBot/Modules/Music/PlaylistCommands.cs b/src/NadekoBot/Modules/Music/PlaylistCommands.cs index ae7aac8dd..a838d80e6 100644 --- a/src/NadekoBot/Modules/Music/PlaylistCommands.cs +++ b/src/NadekoBot/Modules/Music/PlaylistCommands.cs @@ -1,7 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading; +using System.Threading; using System.Threading.Tasks; using Discord; using Discord.Commands; @@ -9,244 +6,239 @@ using NadekoBot.Common.Attributes; using NadekoBot.Services; using NadekoBot.Services.Database.Models; using NadekoBot.Db; -using NadekoBot.Db.Models; using NadekoBot.Extensions; -using NadekoBot.Modules; -using NadekoBot.Modules.Music; using NadekoBot.Modules.Music.Services; -using Serilog; -namespace NadekoBot.Modules.Music +namespace NadekoBot.Modules.Music; + +public sealed partial class Music { - public sealed partial class Music + [Group] + public sealed class PlaylistCommands : NadekoModule { - [Group] - public sealed class PlaylistCommands : NadekoModule + private readonly DbService _db; + private readonly IBotCredentials _creds; + + public PlaylistCommands(DbService db, IBotCredentials creds) { - private readonly DbService _db; - private readonly IBotCredentials _creds; - - public PlaylistCommands(DbService db, IBotCredentials creds) - { - _db = db; - _creds = creds; - } + _db = db; + _creds = creds; + } - private async Task EnsureBotInVoiceChannelAsync(ulong voiceChannelId, IGuildUser botUser = null) + private async Task EnsureBotInVoiceChannelAsync(ulong voiceChannelId, IGuildUser botUser = null) + { + botUser ??= await ctx.Guild.GetCurrentUserAsync(); + await voiceChannelLock.WaitAsync(); + try { - botUser ??= await ctx.Guild.GetCurrentUserAsync(); - await voiceChannelLock.WaitAsync(); - try - { - if (botUser.VoiceChannel?.Id is null || !_service.TryGetMusicPlayer(ctx.Guild.Id, out _)) - await _service.JoinVoiceChannelAsync(ctx.Guild.Id, voiceChannelId); - } - finally - { - voiceChannelLock.Release(); - } + if (botUser.VoiceChannel?.Id is null || !_service.TryGetMusicPlayer(ctx.Guild.Id, out _)) + await _service.JoinVoiceChannelAsync(ctx.Guild.Id, voiceChannelId); + } + finally + { + voiceChannelLock.Release(); + } + } + + [NadekoCommand, Aliases] + [RequireContext(ContextType.Guild)] + public async Task Playlists([Leftover] int num = 1) + { + if (num <= 0) + return; + + List playlists; + + using (var uow = _db.GetDbContext()) + { + playlists = uow.MusicPlaylists.GetPlaylistsOnPage(num); } - [NadekoCommand, Aliases] - [RequireContext(ContextType.Guild)] - public async Task Playlists([Leftover] int num = 1) + var embed = _eb + .Create(ctx) + .WithAuthor(GetText(strs.playlists_page(num)), MusicIconUrl) + .WithDescription(string.Join("\n", playlists.Select(r => + GetText(strs.playlists(r.Id, r.Name, r.Author, r.Songs.Count))))) + .WithOkColor(); + + await ctx.Channel.EmbedAsync(embed).ConfigureAwait(false); + } + + [NadekoCommand, Aliases] + [RequireContext(ContextType.Guild)] + public async Task DeletePlaylist([Leftover] int id) + { + var success = false; + try { - if (num <= 0) - return; - - List playlists; - using (var uow = _db.GetDbContext()) { - playlists = uow.MusicPlaylists.GetPlaylistsOnPage(num); - } + var pl = uow.MusicPlaylists.FirstOrDefault(x => x.Id == id); - var embed = _eb - .Create(ctx) - .WithAuthor(GetText(strs.playlists_page(num)), MusicIconUrl) - .WithDescription(string.Join("\n", playlists.Select(r => - GetText(strs.playlists(r.Id, r.Name, r.Author, r.Songs.Count))))) - .WithOkColor(); - - await ctx.Channel.EmbedAsync(embed).ConfigureAwait(false); - } - - [NadekoCommand, Aliases] - [RequireContext(ContextType.Guild)] - public async Task DeletePlaylist([Leftover] int id) - { - var success = false; - try - { - using (var uow = _db.GetDbContext()) + if (pl != null) { - var pl = uow.MusicPlaylists.FirstOrDefault(x => x.Id == id); - - if (pl != null) + if (_creds.IsOwner(ctx.User) || pl.AuthorId == ctx.User.Id) { - if (_creds.IsOwner(ctx.User) || pl.AuthorId == ctx.User.Id) - { - uow.MusicPlaylists.Remove(pl); - await uow.SaveChangesAsync(); - success = true; - } + uow.MusicPlaylists.Remove(pl); + await uow.SaveChangesAsync(); + success = true; } } } - catch (Exception ex) - { - Log.Warning(ex, "Error deleting playlist"); - } - - if (!success) - await ReplyErrorLocalizedAsync(strs.playlist_delete_fail).ConfigureAwait(false); - else - await ReplyConfirmLocalizedAsync(strs.playlist_deleted).ConfigureAwait(false); + } + catch (Exception ex) + { + Log.Warning(ex, "Error deleting playlist"); } - [NadekoCommand, Aliases] - [RequireContext(ContextType.Guild)] - public async Task PlaylistShow(int id, int page = 1) - { - if (page-- < 1) - return; + if (!success) + await ReplyErrorLocalizedAsync(strs.playlist_delete_fail).ConfigureAwait(false); + else + await ReplyConfirmLocalizedAsync(strs.playlist_deleted).ConfigureAwait(false); + } + [NadekoCommand, Aliases] + [RequireContext(ContextType.Guild)] + public async Task PlaylistShow(int id, int page = 1) + { + if (page-- < 1) + return; + + MusicPlaylist mpl; + using (var uow = _db.GetDbContext()) + { + mpl = uow.MusicPlaylists.GetWithSongs(id); + } + + await ctx.SendPaginatedConfirmAsync(page, (cur) => + { + var i = 0; + var str = string.Join("\n", mpl.Songs + .Skip(cur * 20) + .Take(20) + .Select(x => $"`{++i}.` [{x.Title.TrimTo(45)}]({x.Query}) `{x.Provider}`")); + return _eb.Create() + .WithTitle($"\"{mpl.Name}\" by {mpl.Author}") + .WithOkColor() + .WithDescription(str); + }, mpl.Songs.Count, 20).ConfigureAwait(false); + } + + [NadekoCommand, Aliases] + [RequireContext(ContextType.Guild)] + public async Task Save([Leftover] string name) + { + if (!_service.TryGetMusicPlayer(ctx.Guild.Id, out var mp)) + { + await ReplyErrorLocalizedAsync(strs.no_player); + return; + } + + var songs = mp.GetQueuedTracks() + .Select(s => new PlaylistSong() + { + Provider = s.Platform.ToString(), + ProviderType = (MusicType)s.Platform, + Title = s.Title, + Query = s.Platform == MusicPlatform.Local ? s.GetStreamUrl().Result!.Trim('"') : s.Url, + }).ToList(); + + MusicPlaylist playlist; + using (var uow = _db.GetDbContext()) + { + playlist = new MusicPlaylist + { + Name = name, + Author = ctx.User.Username, + AuthorId = ctx.User.Id, + Songs = songs.ToList(), + }; + uow.MusicPlaylists.Add(playlist); + await uow.SaveChangesAsync(); + } + + await ctx.Channel.EmbedAsync(_eb.Create() + .WithOkColor() + .WithTitle(GetText(strs.playlist_saved)) + .AddField(GetText(strs.name), name) + .AddField(GetText(strs.id), playlist.Id.ToString())); + } + + + private static readonly SemaphoreSlim _playlistLock = new SemaphoreSlim(1, 1); + + [NadekoCommand, Aliases] + [RequireContext(ContextType.Guild)] + public async Task Load([Leftover] int id) + { + // expensive action, 1 at a time + await _playlistLock.WaitAsync(); + try + { + var user = (IGuildUser) ctx.User; + var voiceChannelId = user.VoiceChannel?.Id; + + if (voiceChannelId is null) + { + await ReplyErrorLocalizedAsync(strs.must_be_in_voice); + return; + } + + _ = ctx.Channel.TriggerTypingAsync(); + + var botUser = await ctx.Guild.GetCurrentUserAsync(); + await EnsureBotInVoiceChannelAsync(voiceChannelId!.Value, botUser); + + if (botUser.VoiceChannel?.Id != voiceChannelId) + { + await ReplyErrorLocalizedAsync(strs.not_with_bot_in_voice); + return; + } + + var mp = await _service.GetOrCreateMusicPlayerAsync((ITextChannel) ctx.Channel); + if (mp is null) + { + await ReplyErrorLocalizedAsync(strs.no_player); + return; + } + MusicPlaylist mpl; using (var uow = _db.GetDbContext()) { mpl = uow.MusicPlaylists.GetWithSongs(id); } - await ctx.SendPaginatedConfirmAsync(page, (cur) => + if (mpl is null) { - var i = 0; - var str = string.Join("\n", mpl.Songs - .Skip(cur * 20) - .Take(20) - .Select(x => $"`{++i}.` [{x.Title.TrimTo(45)}]({x.Query}) `{x.Provider}`")); - return _eb.Create() - .WithTitle($"\"{mpl.Name}\" by {mpl.Author}") - .WithOkColor() - .WithDescription(str); - }, mpl.Songs.Count, 20).ConfigureAwait(false); - } - - [NadekoCommand, Aliases] - [RequireContext(ContextType.Guild)] - public async Task Save([Leftover] string name) - { - if (!_service.TryGetMusicPlayer(ctx.Guild.Id, out var mp)) - { - await ReplyErrorLocalizedAsync(strs.no_player); + await ReplyErrorLocalizedAsync(strs.playlist_id_not_found).ConfigureAwait(false); return; } - var songs = mp.GetQueuedTracks() - .Select(s => new PlaylistSong() - { - Provider = s.Platform.ToString(), - ProviderType = (MusicType)s.Platform, - Title = s.Title, - Query = s.Platform == MusicPlatform.Local ? s.GetStreamUrl().Result!.Trim('"') : s.Url, - }).ToList(); - - MusicPlaylist playlist; - using (var uow = _db.GetDbContext()) - { - playlist = new MusicPlaylist - { - Name = name, - Author = ctx.User.Username, - AuthorId = ctx.User.Id, - Songs = songs.ToList(), - }; - uow.MusicPlaylists.Add(playlist); - await uow.SaveChangesAsync(); - } - - await ctx.Channel.EmbedAsync(_eb.Create() - .WithOkColor() - .WithTitle(GetText(strs.playlist_saved)) - .AddField(GetText(strs.name), name) - .AddField(GetText(strs.id), playlist.Id.ToString())); - } - - - private static readonly SemaphoreSlim _playlistLock = new SemaphoreSlim(1, 1); - - [NadekoCommand, Aliases] - [RequireContext(ContextType.Guild)] - public async Task Load([Leftover] int id) - { - // expensive action, 1 at a time - await _playlistLock.WaitAsync(); + IUserMessage msg = null; try - { - var user = (IGuildUser) ctx.User; - var voiceChannelId = user.VoiceChannel?.Id; - - if (voiceChannelId is null) - { - await ReplyErrorLocalizedAsync(strs.must_be_in_voice); - return; - } - - _ = ctx.Channel.TriggerTypingAsync(); - - var botUser = await ctx.Guild.GetCurrentUserAsync(); - await EnsureBotInVoiceChannelAsync(voiceChannelId!.Value, botUser); - - if (botUser.VoiceChannel?.Id != voiceChannelId) - { - await ReplyErrorLocalizedAsync(strs.not_with_bot_in_voice); - return; - } - - var mp = await _service.GetOrCreateMusicPlayerAsync((ITextChannel) ctx.Channel); - if (mp is null) - { - await ReplyErrorLocalizedAsync(strs.no_player); - return; - } - - MusicPlaylist mpl; - using (var uow = _db.GetDbContext()) - { - mpl = uow.MusicPlaylists.GetWithSongs(id); - } - - if (mpl is null) - { - await ReplyErrorLocalizedAsync(strs.playlist_id_not_found).ConfigureAwait(false); - return; - } - - IUserMessage msg = null; - try - { - msg = await ctx.Channel - .SendMessageAsync(GetText(strs.attempting_to_queue(Format.Bold(mpl.Songs.Count.ToString())))) - .ConfigureAwait(false); - } - catch (Exception) - { - } - - await mp.EnqueueManyAsync( - mpl.Songs.Select(x => (x.Query, (MusicPlatform) x.ProviderType)), - ctx.User.ToString() - ); - - if (msg != null) - { - await msg.ModifyAsync(m => m.Content = GetText(strs.playlist_queue_complete)); - } - } - finally { - _playlistLock.Release(); + msg = await ctx.Channel + .SendMessageAsync(GetText(strs.attempting_to_queue(Format.Bold(mpl.Songs.Count.ToString())))) + .ConfigureAwait(false); } + catch (Exception) + { + } + + await mp.EnqueueManyAsync( + mpl.Songs.Select(x => (x.Query, (MusicPlatform) x.ProviderType)), + ctx.User.ToString() + ); + + if (msg != null) + { + await msg.ModifyAsync(m => m.Content = GetText(strs.playlist_queue_complete)); + } + } + finally + { + _playlistLock.Release(); } } } -} +} \ No newline at end of file diff --git a/src/NadekoBot/Modules/Music/Services/AyuVoiceStateService.cs b/src/NadekoBot/Modules/Music/Services/AyuVoiceStateService.cs index 1ebf396ac..f91287e23 100644 --- a/src/NadekoBot/Modules/Music/Services/AyuVoiceStateService.cs +++ b/src/NadekoBot/Modules/Music/Services/AyuVoiceStateService.cs @@ -1,5 +1,4 @@ using System.Collections.Concurrent; -using System.Linq; using System.Reflection; using System.Threading; using System.Threading.Tasks; @@ -7,215 +6,214 @@ using Ayu.Discord.Voice; using Discord.WebSocket; using NadekoBot.Services; -namespace NadekoBot.Modules.Music.Services +namespace NadekoBot.Modules.Music.Services; + +public sealed class AyuVoiceStateService : INService { - public sealed class AyuVoiceStateService : INService + // public delegate Task VoiceProxyUpdatedDelegate(ulong guildId, IVoiceProxy proxy); + // public event VoiceProxyUpdatedDelegate OnVoiceProxyUpdate = delegate { return Task.CompletedTask; }; + + private readonly ConcurrentDictionary _voiceProxies = new ConcurrentDictionary(); + private readonly ConcurrentDictionary _voiceGatewayLocks = new ConcurrentDictionary(); + + private readonly DiscordSocketClient _client; + private readonly MethodInfo _sendVoiceStateUpdateMethodInfo; + private readonly object _dnetApiClient; + private readonly ulong _currentUserId; + + public AyuVoiceStateService(DiscordSocketClient client) { - // public delegate Task VoiceProxyUpdatedDelegate(ulong guildId, IVoiceProxy proxy); - // public event VoiceProxyUpdatedDelegate OnVoiceProxyUpdate = delegate { return Task.CompletedTask; }; - - private readonly ConcurrentDictionary _voiceProxies = new ConcurrentDictionary(); - private readonly ConcurrentDictionary _voiceGatewayLocks = new ConcurrentDictionary(); - - private readonly DiscordSocketClient _client; - private readonly MethodInfo _sendVoiceStateUpdateMethodInfo; - private readonly object _dnetApiClient; - private readonly ulong _currentUserId; + _client = client; + _currentUserId = _client.CurrentUser.Id; - public AyuVoiceStateService(DiscordSocketClient client) - { - _client = client; - _currentUserId = _client.CurrentUser.Id; - - var prop = _client.GetType() - .GetProperties(BindingFlags.NonPublic | BindingFlags.Instance) - .First(x => x.Name == "ApiClient" && x.PropertyType.Name == "DiscordSocketApiClient"); - _dnetApiClient = prop.GetValue(_client, null); - _sendVoiceStateUpdateMethodInfo = _dnetApiClient.GetType().GetMethod("SendVoiceStateUpdateAsync"); + var prop = _client.GetType() + .GetProperties(BindingFlags.NonPublic | BindingFlags.Instance) + .First(x => x.Name == "ApiClient" && x.PropertyType.Name == "DiscordSocketApiClient"); + _dnetApiClient = prop.GetValue(_client, null); + _sendVoiceStateUpdateMethodInfo = _dnetApiClient.GetType().GetMethod("SendVoiceStateUpdateAsync"); - _client.LeftGuild += ClientOnLeftGuild; + _client.LeftGuild += ClientOnLeftGuild; + } + + private Task ClientOnLeftGuild(SocketGuild guild) + { + if (_voiceProxies.TryRemove(guild.Id, out var proxy)) + { + proxy.StopGateway(); + proxy.SetGateway(null); } - private Task ClientOnLeftGuild(SocketGuild guild) + return Task.CompletedTask; + } + + private Task InvokeSendVoiceStateUpdateAsync(ulong guildId, ulong? channelId = null, bool isDeafened = false, bool isMuted = false) + { + // return _voiceStateUpdate(guildId, channelId, isDeafened, isMuted); + return (Task) _sendVoiceStateUpdateMethodInfo.Invoke(_dnetApiClient, new object[] {guildId, channelId, isMuted, isDeafened, null}); + } + + private Task SendLeaveVoiceChannelInternalAsync(ulong guildId) + => InvokeSendVoiceStateUpdateAsync(guildId); + + private Task SendJoinVoiceChannelInternalAsync(ulong guildId, ulong channelId) + => InvokeSendVoiceStateUpdateAsync(guildId, channelId); + + private SemaphoreSlim GetVoiceGatewayLock(ulong guildId) => _voiceGatewayLocks.GetOrAdd(guildId, new SemaphoreSlim(1, 1)); + + private async Task LeaveVoiceChannelInternalAsync(ulong guildId) + { + var complete = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + Task OnUserVoiceStateUpdated(SocketUser user, SocketVoiceState oldState, SocketVoiceState newState) { - if (_voiceProxies.TryRemove(guild.Id, out var proxy)) + if (user is SocketGuildUser guildUser + && guildUser.Guild.Id == guildId + && newState.VoiceChannel?.Id is null) { - proxy.StopGateway(); - proxy.SetGateway(null); + complete.TrySetResult(true); } return Task.CompletedTask; } - private Task InvokeSendVoiceStateUpdateAsync(ulong guildId, ulong? channelId = null, bool isDeafened = false, bool isMuted = false) + try { - // return _voiceStateUpdate(guildId, channelId, isDeafened, isMuted); - return (Task) _sendVoiceStateUpdateMethodInfo.Invoke(_dnetApiClient, new object[] {guildId, channelId, isMuted, isDeafened, null}); + _client.UserVoiceStateUpdated += OnUserVoiceStateUpdated; + + if (_voiceProxies.TryGetValue(guildId, out var proxy)) + { + _ = proxy.StopGateway(); + proxy.SetGateway(null); + } + + await SendLeaveVoiceChannelInternalAsync(guildId); + await Task.WhenAny(Task.Delay(1500), complete.Task); + } + finally + { + _client.UserVoiceStateUpdated -= OnUserVoiceStateUpdated; + } + } + public async Task LeaveVoiceChannel(ulong guildId) + { + var gwLock = GetVoiceGatewayLock(guildId); + await gwLock.WaitAsync().ConfigureAwait(false); + try + { + await LeaveVoiceChannelInternalAsync(guildId); + } + finally + { + gwLock.Release(); + } + } + + private async Task InternalConnectToVcAsync(ulong guildId, ulong channelId) + { + var voiceStateUpdatedSource = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + var voiceServerUpdatedSource = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + + Task OnUserVoiceStateUpdated(SocketUser user, SocketVoiceState oldState, SocketVoiceState newState) + { + if (user is SocketGuildUser guildUser && guildUser.Guild.Id == guildId) + { + if (newState.VoiceChannel?.Id == channelId) + voiceStateUpdatedSource.TrySetResult(newState.VoiceSessionId); + + voiceStateUpdatedSource.TrySetResult(null); + } + + return Task.CompletedTask; } - private Task SendLeaveVoiceChannelInternalAsync(ulong guildId) - => InvokeSendVoiceStateUpdateAsync(guildId); - - private Task SendJoinVoiceChannelInternalAsync(ulong guildId, ulong channelId) - => InvokeSendVoiceStateUpdateAsync(guildId, channelId); - - private SemaphoreSlim GetVoiceGatewayLock(ulong guildId) => _voiceGatewayLocks.GetOrAdd(guildId, new SemaphoreSlim(1, 1)); - - private async Task LeaveVoiceChannelInternalAsync(ulong guildId) + Task OnVoiceServerUpdated(SocketVoiceServer data) { - var complete = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); - Task OnUserVoiceStateUpdated(SocketUser user, SocketVoiceState oldState, SocketVoiceState newState) + if (data.Guild.Id == guildId) { - if (user is SocketGuildUser guildUser - && guildUser.Guild.Id == guildId - && newState.VoiceChannel?.Id is null) - { - complete.TrySetResult(true); - } - - return Task.CompletedTask; + voiceServerUpdatedSource.TrySetResult(data); } - try - { - _client.UserVoiceStateUpdated += OnUserVoiceStateUpdated; - - if (_voiceProxies.TryGetValue(guildId, out var proxy)) - { - _ = proxy.StopGateway(); - proxy.SetGateway(null); - } - - await SendLeaveVoiceChannelInternalAsync(guildId); - await Task.WhenAny(Task.Delay(1500), complete.Task); - } - finally - { - _client.UserVoiceStateUpdated -= OnUserVoiceStateUpdated; - } + return Task.CompletedTask; } - public async Task LeaveVoiceChannel(ulong guildId) - { - var gwLock = GetVoiceGatewayLock(guildId); - await gwLock.WaitAsync().ConfigureAwait(false); - try - { - await LeaveVoiceChannelInternalAsync(guildId); - } - finally - { - gwLock.Release(); - } - } - - private async Task InternalConnectToVcAsync(ulong guildId, ulong channelId) - { - var voiceStateUpdatedSource = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); - var voiceServerUpdatedSource = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); - - Task OnUserVoiceStateUpdated(SocketUser user, SocketVoiceState oldState, SocketVoiceState newState) - { - if (user is SocketGuildUser guildUser && guildUser.Guild.Id == guildId) - { - if (newState.VoiceChannel?.Id == channelId) - voiceStateUpdatedSource.TrySetResult(newState.VoiceSessionId); - - voiceStateUpdatedSource.TrySetResult(null); - } - - return Task.CompletedTask; - } - - Task OnVoiceServerUpdated(SocketVoiceServer data) - { - if (data.Guild.Id == guildId) - { - voiceServerUpdatedSource.TrySetResult(data); - } - - return Task.CompletedTask; - } - try + try + { + _client.VoiceServerUpdated += OnVoiceServerUpdated; + _client.UserVoiceStateUpdated += OnUserVoiceStateUpdated; + + await SendJoinVoiceChannelInternalAsync(guildId, channelId); + + // create a delay task, how much to wait for gateway response + var delayTask = Task.Delay(2500); + + // either delay or successful voiceStateUpdate + var maybeUpdateTask = Task.WhenAny(delayTask, voiceStateUpdatedSource.Task); + // either delay or successful voiceServerUpdate + var maybeServerTask = Task.WhenAny(delayTask, voiceServerUpdatedSource.Task); + + // wait for both to end (max 1s) and check if either of them is a delay task + var results = await Task.WhenAll(maybeUpdateTask, maybeServerTask); + if (results[0] == delayTask || results[1] == delayTask) { - _client.VoiceServerUpdated += OnVoiceServerUpdated; - _client.UserVoiceStateUpdated += OnUserVoiceStateUpdated; + // if either is delay, return null - connection unsuccessful + return null; + } - await SendJoinVoiceChannelInternalAsync(guildId, channelId); + // if both are succesful, that means we can safely get + // the values from completion sources - // create a delay task, how much to wait for gateway response - var delayTask = Task.Delay(2500); + var session = await voiceStateUpdatedSource.Task; - // either delay or successful voiceStateUpdate - var maybeUpdateTask = Task.WhenAny(delayTask, voiceStateUpdatedSource.Task); - // either delay or successful voiceServerUpdate - var maybeServerTask = Task.WhenAny(delayTask, voiceServerUpdatedSource.Task); + // session can be null. Means we disconnected, or connected to the wrong channel (?!) + if (session is null) + return null; - // wait for both to end (max 1s) and check if either of them is a delay task - var results = await Task.WhenAll(maybeUpdateTask, maybeServerTask); - if (results[0] == delayTask || results[1] == delayTask) - { - // if either is delay, return null - connection unsuccessful - return null; - } - - // if both are succesful, that means we can safely get - // the values from completion sources - - var session = await voiceStateUpdatedSource.Task; - - // session can be null. Means we disconnected, or connected to the wrong channel (?!) - if (session is null) - return null; - - var voiceServerData = await voiceServerUpdatedSource.Task; + var voiceServerData = await voiceServerUpdatedSource.Task; - VoiceGateway CreateVoiceGatewayLocal() => - new VoiceGateway( - guildId, - _currentUserId, - session, - voiceServerData.Token, - voiceServerData.Endpoint - ); - - var current = _voiceProxies.AddOrUpdate( + VoiceGateway CreateVoiceGatewayLocal() => + new VoiceGateway( guildId, - (gid) => new VoiceProxy(CreateVoiceGatewayLocal()), - (gid, currentProxy) => - { - _ = currentProxy.StopGateway(); - currentProxy.SetGateway(CreateVoiceGatewayLocal()); - return currentProxy; - } + _currentUserId, + session, + voiceServerData.Token, + voiceServerData.Endpoint ); - _ = current.StartGateway(); // don't await, this blocks until gateway is closed - return current; - } - finally - { - _client.VoiceServerUpdated -= OnVoiceServerUpdated; - _client.UserVoiceStateUpdated -= OnUserVoiceStateUpdated; - } - } + var current = _voiceProxies.AddOrUpdate( + guildId, + (gid) => new VoiceProxy(CreateVoiceGatewayLocal()), + (gid, currentProxy) => + { + _ = currentProxy.StopGateway(); + currentProxy.SetGateway(CreateVoiceGatewayLocal()); + return currentProxy; + } + ); - public async Task JoinVoiceChannel(ulong guildId, ulong channelId, bool forceReconnect = true) + _ = current.StartGateway(); // don't await, this blocks until gateway is closed + return current; + } + finally { - var gwLock = GetVoiceGatewayLock(guildId); - await gwLock.WaitAsync().ConfigureAwait(false); - try - { - await LeaveVoiceChannelInternalAsync(guildId); - return await InternalConnectToVcAsync(guildId, channelId); - } - finally - { - gwLock.Release(); - } + _client.VoiceServerUpdated -= OnVoiceServerUpdated; + _client.UserVoiceStateUpdated -= OnUserVoiceStateUpdated; } - - public bool TryGetProxy(ulong guildId, out IVoiceProxy proxy) - => _voiceProxies.TryGetValue(guildId, out proxy); } + + public async Task JoinVoiceChannel(ulong guildId, ulong channelId, bool forceReconnect = true) + { + var gwLock = GetVoiceGatewayLock(guildId); + await gwLock.WaitAsync().ConfigureAwait(false); + try + { + await LeaveVoiceChannelInternalAsync(guildId); + return await InternalConnectToVcAsync(guildId, channelId); + } + finally + { + gwLock.Release(); + } + } + + public bool TryGetProxy(ulong guildId, out IVoiceProxy proxy) + => _voiceProxies.TryGetValue(guildId, out proxy); } \ No newline at end of file diff --git a/src/NadekoBot/Modules/Music/Services/IMusicService.cs b/src/NadekoBot/Modules/Music/Services/IMusicService.cs index 251cbb21f..fe8eabc64 100644 --- a/src/NadekoBot/Modules/Music/Services/IMusicService.cs +++ b/src/NadekoBot/Modules/Music/Services/IMusicService.cs @@ -1,42 +1,39 @@ #nullable enable -using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Threading.Tasks; using Discord; using NadekoBot.Common; -using NadekoBot.Modules.Music; using NadekoBot.Services.Database.Models; -namespace NadekoBot.Modules.Music.Services -{ - public interface IMusicService : IPlaceholderProvider - { - /// - /// Leave voice channel in the specified guild if it's connected to one - /// - /// Id of the guild - public Task LeaveVoiceChannelAsync(ulong guildId); +namespace NadekoBot.Modules.Music.Services; - /// - /// Joins the voice channel with the specified id - /// - /// Id of the guild where the voice channel is - /// Id of the voice channel - public Task JoinVoiceChannelAsync(ulong guildId, ulong voiceChannelId); +public interface IMusicService : IPlaceholderProvider +{ + /// + /// Leave voice channel in the specified guild if it's connected to one + /// + /// Id of the guild + public Task LeaveVoiceChannelAsync(ulong guildId); + + /// + /// Joins the voice channel with the specified id + /// + /// Id of the guild where the voice channel is + /// Id of the voice channel + public Task JoinVoiceChannelAsync(ulong guildId, ulong voiceChannelId); - Task GetOrCreateMusicPlayerAsync(ITextChannel contextChannel); - bool TryGetMusicPlayer(ulong guildId, [MaybeNullWhen(false)] out IMusicPlayer musicPlayer); - Task EnqueueYoutubePlaylistAsync(IMusicPlayer mp, string playlistId, string queuer); - Task EnqueueDirectoryAsync(IMusicPlayer mp, string dirPath, string queuer); - Task EnqueueSoundcloudPlaylistAsync(IMusicPlayer mp, string playlist, string queuer); - Task SendToOutputAsync(ulong guildId, IEmbedBuilder embed); - Task PlayAsync(ulong guildId, ulong voiceChannelId); - Task> SearchVideosAsync(string query); - Task SetMusicChannelAsync(ulong guildId, ulong? channelId); - Task SetRepeatAsync(ulong guildId, PlayerRepeatType repeatType); - Task SetVolumeAsync(ulong guildId, int value); - Task ToggleAutoDisconnectAsync(ulong guildId); - Task GetMusicQualityAsync(ulong guildId); - Task SetMusicQualityAsync(ulong guildId, QualityPreset preset); - } -} + Task GetOrCreateMusicPlayerAsync(ITextChannel contextChannel); + bool TryGetMusicPlayer(ulong guildId, [MaybeNullWhen(false)] out IMusicPlayer musicPlayer); + Task EnqueueYoutubePlaylistAsync(IMusicPlayer mp, string playlistId, string queuer); + Task EnqueueDirectoryAsync(IMusicPlayer mp, string dirPath, string queuer); + Task EnqueueSoundcloudPlaylistAsync(IMusicPlayer mp, string playlist, string queuer); + Task SendToOutputAsync(ulong guildId, IEmbedBuilder embed); + Task PlayAsync(ulong guildId, ulong voiceChannelId); + Task> SearchVideosAsync(string query); + Task SetMusicChannelAsync(ulong guildId, ulong? channelId); + Task SetRepeatAsync(ulong guildId, PlayerRepeatType repeatType); + Task SetVolumeAsync(ulong guildId, int value); + Task ToggleAutoDisconnectAsync(ulong guildId); + Task GetMusicQualityAsync(ulong guildId); + Task SetMusicQualityAsync(ulong guildId, QualityPreset preset); +} \ No newline at end of file diff --git a/src/NadekoBot/Modules/Music/Services/MusicService.cs b/src/NadekoBot/Modules/Music/Services/MusicService.cs index 1b7afce69..087d689fd 100644 --- a/src/NadekoBot/Modules/Music/Services/MusicService.cs +++ b/src/NadekoBot/Modules/Music/Services/MusicService.cs @@ -1,462 +1,455 @@ #nullable enable -using System; using System.Collections.Concurrent; -using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; -using System.Linq; using System.Threading.Tasks; using Discord; using Discord.WebSocket; using NadekoBot.Services; using NadekoBot.Services.Database.Models; using NadekoBot.Db; -using NadekoBot.Modules.Music; -using NadekoBot.Db.Models; using NadekoBot.Extensions; -using Serilog; -namespace NadekoBot.Modules.Music.Services +namespace NadekoBot.Modules.Music.Services; + +public sealed class MusicService : IMusicService { - public sealed class MusicService : IMusicService + private readonly AyuVoiceStateService _voiceStateService; + private readonly ITrackResolveProvider _trackResolveProvider; + private readonly DbService _db; + private readonly IYoutubeResolver _ytResolver; + private readonly ILocalTrackResolver _localResolver; + private readonly ISoundcloudResolver _scResolver; + private readonly DiscordSocketClient _client; + private readonly IBotStrings _strings; + private readonly IGoogleApiService _googleApiService; + private readonly YtLoader _ytLoader; + private readonly IEmbedBuilderService _eb; + + private readonly ConcurrentDictionary _players; + private readonly ConcurrentDictionary _outputChannels; + private readonly ConcurrentDictionary _settings; + + public MusicService(AyuVoiceStateService voiceStateService, ITrackResolveProvider trackResolveProvider, + DbService db, IYoutubeResolver ytResolver, ILocalTrackResolver localResolver, ISoundcloudResolver scResolver, + DiscordSocketClient client, IBotStrings strings, IGoogleApiService googleApiService, YtLoader ytLoader, + IEmbedBuilderService eb) { - private readonly AyuVoiceStateService _voiceStateService; - private readonly ITrackResolveProvider _trackResolveProvider; - private readonly DbService _db; - private readonly IYoutubeResolver _ytResolver; - private readonly ILocalTrackResolver _localResolver; - private readonly ISoundcloudResolver _scResolver; - private readonly DiscordSocketClient _client; - private readonly IBotStrings _strings; - private readonly IGoogleApiService _googleApiService; - private readonly YtLoader _ytLoader; - private readonly IEmbedBuilderService _eb; + _voiceStateService = voiceStateService; + _trackResolveProvider = trackResolveProvider; + _db = db; + _ytResolver = ytResolver; + _localResolver = localResolver; + _scResolver = scResolver; + _client = client; + _strings = strings; + _googleApiService = googleApiService; + _ytLoader = ytLoader; + _eb = eb; - private readonly ConcurrentDictionary _players; - private readonly ConcurrentDictionary _outputChannels; - private readonly ConcurrentDictionary _settings; - - public MusicService(AyuVoiceStateService voiceStateService, ITrackResolveProvider trackResolveProvider, - DbService db, IYoutubeResolver ytResolver, ILocalTrackResolver localResolver, ISoundcloudResolver scResolver, - DiscordSocketClient client, IBotStrings strings, IGoogleApiService googleApiService, YtLoader ytLoader, - IEmbedBuilderService eb) - { - _voiceStateService = voiceStateService; - _trackResolveProvider = trackResolveProvider; - _db = db; - _ytResolver = ytResolver; - _localResolver = localResolver; - _scResolver = scResolver; - _client = client; - _strings = strings; - _googleApiService = googleApiService; - _ytLoader = ytLoader; - _eb = eb; - - _players = new ConcurrentDictionary(); - _outputChannels = new ConcurrentDictionary(); - _settings = new ConcurrentDictionary(); + _players = new ConcurrentDictionary(); + _outputChannels = new ConcurrentDictionary(); + _settings = new ConcurrentDictionary(); - _client.LeftGuild += ClientOnLeftGuild; - } + _client.LeftGuild += ClientOnLeftGuild; + } - private void DisposeMusicPlayer(IMusicPlayer musicPlayer) + private void DisposeMusicPlayer(IMusicPlayer musicPlayer) + { + musicPlayer.Kill(); + _ = Task.Delay(10_000).ContinueWith(_ => musicPlayer.Dispose()); + } + + private void RemoveMusicPlayer(ulong guildId) + { + _outputChannels.TryRemove(guildId, out _); + if (_players.TryRemove(guildId, out var mp)) { - musicPlayer.Kill(); - _ = Task.Delay(10_000).ContinueWith(_ => musicPlayer.Dispose()); + DisposeMusicPlayer(mp); } + } - private void RemoveMusicPlayer(ulong guildId) - { - _outputChannels.TryRemove(guildId, out _); - if (_players.TryRemove(guildId, out var mp)) - { - DisposeMusicPlayer(mp); - } - } + private Task ClientOnLeftGuild(SocketGuild guild) + { + RemoveMusicPlayer(guild.Id); + return Task.CompletedTask; + } - private Task ClientOnLeftGuild(SocketGuild guild) - { - RemoveMusicPlayer(guild.Id); - return Task.CompletedTask; - } + public async Task LeaveVoiceChannelAsync(ulong guildId) + { + RemoveMusicPlayer(guildId); + await _voiceStateService.LeaveVoiceChannel(guildId); + } - public async Task LeaveVoiceChannelAsync(ulong guildId) - { - RemoveMusicPlayer(guildId); - await _voiceStateService.LeaveVoiceChannel(guildId); - } + public Task JoinVoiceChannelAsync(ulong guildId, ulong voiceChannelId) + => _voiceStateService.JoinVoiceChannel(guildId, voiceChannelId); - public Task JoinVoiceChannelAsync(ulong guildId, ulong voiceChannelId) - => _voiceStateService.JoinVoiceChannel(guildId, voiceChannelId); - - public async Task GetOrCreateMusicPlayerAsync(ITextChannel contextChannel) - { - var newPLayer = await CreateMusicPlayerInternalAsync(contextChannel.GuildId, contextChannel); - if (newPLayer is null) - return null; + public async Task GetOrCreateMusicPlayerAsync(ITextChannel contextChannel) + { + var newPLayer = await CreateMusicPlayerInternalAsync(contextChannel.GuildId, contextChannel); + if (newPLayer is null) + return null; - return _players.GetOrAdd(contextChannel.GuildId, newPLayer); + return _players.GetOrAdd(contextChannel.GuildId, newPLayer); + } + + public bool TryGetMusicPlayer(ulong guildId, [MaybeNullWhen(false)] out IMusicPlayer musicPlayer) + => _players.TryGetValue(guildId, out musicPlayer); + + public async Task EnqueueYoutubePlaylistAsync(IMusicPlayer mp, string query, string queuer) + { + var count = 0; + await foreach (var track in _ytResolver.ResolveTracksFromPlaylistAsync(query)) + { + if (mp.IsKilled) + break; + + mp.EnqueueTrack(track, queuer); + ++count; } - public bool TryGetMusicPlayer(ulong guildId, [MaybeNullWhen(false)] out IMusicPlayer musicPlayer) - => _players.TryGetValue(guildId, out musicPlayer); + return count; + } - public async Task EnqueueYoutubePlaylistAsync(IMusicPlayer mp, string query, string queuer) + public async Task EnqueueDirectoryAsync(IMusicPlayer mp, string dirPath, string queuer) + { + await foreach (var track in _localResolver.ResolveDirectoryAsync(dirPath)) { - var count = 0; - await foreach (var track in _ytResolver.ResolveTracksFromPlaylistAsync(query)) - { - if (mp.IsKilled) - break; - - mp.EnqueueTrack(track, queuer); - ++count; - } - - return count; - } - - public async Task EnqueueDirectoryAsync(IMusicPlayer mp, string dirPath, string queuer) - { - await foreach (var track in _localResolver.ResolveDirectoryAsync(dirPath)) - { - if (mp.IsKilled) - break; + if (mp.IsKilled) + break; - mp.EnqueueTrack(track, queuer); - } + mp.EnqueueTrack(track, queuer); } + } - public async Task EnqueueSoundcloudPlaylistAsync(IMusicPlayer mp, string playlist, string queuer) + public async Task EnqueueSoundcloudPlaylistAsync(IMusicPlayer mp, string playlist, string queuer) + { + var i = 0; + await foreach (var track in _scResolver.ResolvePlaylistAsync(playlist)) { - var i = 0; - await foreach (var track in _scResolver.ResolvePlaylistAsync(playlist)) - { - if (mp.IsKilled) - break; + if (mp.IsKilled) + break; - mp.EnqueueTrack(track, queuer); - ++i; - } - - return i; + mp.EnqueueTrack(track, queuer); + ++i; } - private async Task CreateMusicPlayerInternalAsync(ulong guildId, ITextChannel defaultChannel) + return i; + } + + private async Task CreateMusicPlayerInternalAsync(ulong guildId, ITextChannel defaultChannel) + { + var queue = new MusicQueue(); + var resolver = _trackResolveProvider; + + if (!_voiceStateService.TryGetProxy(guildId, out var proxy)) { - var queue = new MusicQueue(); - var resolver = _trackResolveProvider; + return null; + } - if (!_voiceStateService.TryGetProxy(guildId, out var proxy)) + var settings = await GetSettingsInternalAsync(guildId); + + ITextChannel? overrideChannel = null; + if (settings.MusicChannelId is ulong channelId) + { + overrideChannel = _client.GetGuild(guildId)?.GetTextChannel(channelId); + + if (overrideChannel is null) { - return null; - } - - var settings = await GetSettingsInternalAsync(guildId); - - ITextChannel? overrideChannel = null; - if (settings.MusicChannelId is ulong channelId) - { - overrideChannel = _client.GetGuild(guildId)?.GetTextChannel(channelId); - - if (overrideChannel is null) - { - Log.Warning("Saved music output channel doesn't exist, falling back to current channel"); - } + Log.Warning("Saved music output channel doesn't exist, falling back to current channel"); } + } - _outputChannels[guildId] = (defaultChannel, overrideChannel); + _outputChannels[guildId] = (defaultChannel, overrideChannel); - var mp = new MusicPlayer( - queue, - resolver, - proxy, - settings.QualityPreset - ); + var mp = new MusicPlayer( + queue, + resolver, + proxy, + settings.QualityPreset + ); - mp.SetRepeat(settings.PlayerRepeat); + mp.SetRepeat(settings.PlayerRepeat); - if (settings.Volume >= 0 && settings.Volume <= 100) - { - mp.SetVolume(settings.Volume); - } - else - { - Log.Error("Saved Volume is outside of valid range >= 0 && <=100 ({Volume})", settings.Volume); - } - - mp.OnCompleted += OnTrackCompleted(guildId); - mp.OnStarted += OnTrackStarted(guildId); - mp.OnQueueStopped += OnQueueStopped(guildId); - - return mp; - } - - public Task SendToOutputAsync(ulong guildId, IEmbedBuilder embed) + if (settings.Volume >= 0 && settings.Volume <= 100) { - if (_outputChannels.TryGetValue(guildId, out var chan)) - return (chan.Default ?? chan.Override).EmbedAsync(embed); - - return Task.FromResult(null); + mp.SetVolume(settings.Volume); } - - private Func OnTrackCompleted(ulong guildId) + else { - IUserMessage? lastFinishedMessage = null; - return async (mp, trackInfo) => - { - _ = lastFinishedMessage?.DeleteAsync(); - var embed = _eb.Create() - .WithOkColor() - .WithAuthor(GetText(guildId, strs.finished_song), Music.MusicIconUrl) - .WithDescription(trackInfo.PrettyName()) - .WithFooter(trackInfo.PrettyTotalTime()); - - lastFinishedMessage = await SendToOutputAsync(guildId, embed); - }; + Log.Error("Saved Volume is outside of valid range >= 0 && <=100 ({Volume})", settings.Volume); } - private Func OnTrackStarted(ulong guildId) + mp.OnCompleted += OnTrackCompleted(guildId); + mp.OnStarted += OnTrackStarted(guildId); + mp.OnQueueStopped += OnQueueStopped(guildId); + + return mp; + } + + public Task SendToOutputAsync(ulong guildId, IEmbedBuilder embed) + { + if (_outputChannels.TryGetValue(guildId, out var chan)) + return (chan.Default ?? chan.Override).EmbedAsync(embed); + + return Task.FromResult(null); + } + + private Func OnTrackCompleted(ulong guildId) + { + IUserMessage? lastFinishedMessage = null; + return async (mp, trackInfo) => { - IUserMessage? lastPlayingMessage = null; - return async (mp, trackInfo, index) => - { - _ = lastPlayingMessage?.DeleteAsync(); - var embed = _eb.Create().WithOkColor() - .WithAuthor(GetText(guildId, strs.playing_song(index + 1)), Music.MusicIconUrl) - .WithDescription(trackInfo.PrettyName()) - .WithFooter($"{mp.PrettyVolume()} | {trackInfo.PrettyInfo()}"); + _ = lastFinishedMessage?.DeleteAsync(); + var embed = _eb.Create() + .WithOkColor() + .WithAuthor(GetText(guildId, strs.finished_song), Music.MusicIconUrl) + .WithDescription(trackInfo.PrettyName()) + .WithFooter(trackInfo.PrettyTotalTime()); - lastPlayingMessage = await SendToOutputAsync(guildId, embed); - }; - } + lastFinishedMessage = await SendToOutputAsync(guildId, embed); + }; + } - private Func OnQueueStopped(ulong guildId) - => (mp) => - { - if (_settings.TryGetValue(guildId, out var settings)) - { - if (settings.AutoDisconnect) - { - return LeaveVoiceChannelAsync(guildId); - } - } - - return Task.CompletedTask; - }; - - // this has to be done because dragging bot to another vc isn't supported yet - public async Task PlayAsync(ulong guildId, ulong voiceChannelId) + private Func OnTrackStarted(ulong guildId) + { + IUserMessage? lastPlayingMessage = null; + return async (mp, trackInfo, index) => { - if (!TryGetMusicPlayer(guildId, out var mp)) - { - return false; - } + _ = lastPlayingMessage?.DeleteAsync(); + var embed = _eb.Create().WithOkColor() + .WithAuthor(GetText(guildId, strs.playing_song(index + 1)), Music.MusicIconUrl) + .WithDescription(trackInfo.PrettyName()) + .WithFooter($"{mp.PrettyVolume()} | {trackInfo.PrettyInfo()}"); - if (mp.IsStopped) - { - if (!_voiceStateService.TryGetProxy(guildId, out var proxy) - || proxy.State == VoiceProxy.VoiceProxyState.Stopped) - { - await JoinVoiceChannelAsync(guildId, voiceChannelId); - } - } + lastPlayingMessage = await SendToOutputAsync(guildId, embed); + }; + } - mp.Next(); - return true; - } - - private async Task> SearchYtLoaderVideosAsync(string query) - { - var result = await _ytLoader.LoadResultsAsync(query); - return result.Select(x => (x.Title, x.Url)).ToList(); - } - - private async Task> SearchGoogleApiVideosAsync(string query) - { - var result = await _googleApiService.GetVideoInfosByKeywordAsync(query, 5); - return result.Select(x => (x.Name, x.Url)).ToList(); - } - - public async Task> SearchVideosAsync(string query) - { - try - { - IList<(string, string)> videos = await SearchYtLoaderVideosAsync(query); - if (videos.Count > 0) - { - return videos; - } - } - catch (Exception ex) - { - Log.Warning("Failed geting videos with YtLoader: {ErrorMessage}", ex.Message); - } - - try - { - return await SearchGoogleApiVideosAsync(query); - } - catch (Exception ex) - { - Log.Warning("Failed getting video results with Google Api. " + - "Probably google api key missing: {ErrorMessage}", ex.Message); - } - - return Array.Empty<(string, string)>(); - } - - private string GetText(ulong guildId, LocStr str) - => _strings.GetText(str, guildId); - - public IEnumerable<(string Name, Func Func)> GetPlaceholders() - { - // random song that's playing - yield return ("%music.playing%", () => - { - var randomPlayingTrack = _players - .Select(x => x.Value.GetCurrentTrack(out _)) - .Where(x => x is not null) - .Shuffle() - .FirstOrDefault(); - - if (randomPlayingTrack is null) - return "-"; - - return randomPlayingTrack.Title; - }); - - // number of servers currently listening to music - yield return ("%music.servers%", () => - { - var count = _players - .Select(x => x.Value.GetCurrentTrack(out _)) - .Count(x => x is not null); - - return count.ToString(); - }); - - yield return ("%music.queued%", () => - { - var count = _players - .Sum(x => x.Value.GetQueuedTracks().Count); - - return count.ToString(); - }); - } - - #region Settings - - private async Task GetSettingsInternalAsync(ulong guildId) + private Func OnQueueStopped(ulong guildId) + => (mp) => { if (_settings.TryGetValue(guildId, out var settings)) - return settings; - - using var uow = _db.GetDbContext(); - var toReturn = _settings[guildId] = await uow.MusicPlayerSettings.ForGuildAsync(guildId); - await uow.SaveChangesAsync(); - - return toReturn; - } - - private async Task ModifySettingsInternalAsync( - ulong guildId, - Action action, - TState state) - { - using var uow = _db.GetDbContext(); - var ms = await uow.MusicPlayerSettings.ForGuildAsync(guildId); - action(ms, state); - await uow.SaveChangesAsync(); - _settings[guildId] = ms; - } - - public async Task SetMusicChannelAsync(ulong guildId, ulong? channelId) - { - if (channelId is null) { - await UnsetMusicChannelAsync(guildId); - return true; + if (settings.AutoDisconnect) + { + return LeaveVoiceChannelAsync(guildId); + } } - - var channel = _client.GetGuild(guildId)?.GetTextChannel(channelId.Value); - if (channel is null) - return false; - await ModifySettingsInternalAsync(guildId, (settings, chId) => + return Task.CompletedTask; + }; + + // this has to be done because dragging bot to another vc isn't supported yet + public async Task PlayAsync(ulong guildId, ulong voiceChannelId) + { + if (!TryGetMusicPlayer(guildId, out var mp)) + { + return false; + } + + if (mp.IsStopped) + { + if (!_voiceStateService.TryGetProxy(guildId, out var proxy) + || proxy.State == VoiceProxy.VoiceProxyState.Stopped) { - settings.MusicChannelId = chId; - }, channelId); + await JoinVoiceChannelAsync(guildId, voiceChannelId); + } + } - _outputChannels.AddOrUpdate(guildId, - (channel, channel), - (key, old) => (old.Default, channel)); + mp.Next(); + return true; + } + + private async Task> SearchYtLoaderVideosAsync(string query) + { + var result = await _ytLoader.LoadResultsAsync(query); + return result.Select(x => (x.Title, x.Url)).ToList(); + } + + private async Task> SearchGoogleApiVideosAsync(string query) + { + var result = await _googleApiService.GetVideoInfosByKeywordAsync(query, 5); + return result.Select(x => (x.Name, x.Url)).ToList(); + } + + public async Task> SearchVideosAsync(string query) + { + try + { + IList<(string, string)> videos = await SearchYtLoaderVideosAsync(query); + if (videos.Count > 0) + { + return videos; + } + } + catch (Exception ex) + { + Log.Warning("Failed geting videos with YtLoader: {ErrorMessage}", ex.Message); + } + + try + { + return await SearchGoogleApiVideosAsync(query); + } + catch (Exception ex) + { + Log.Warning("Failed getting video results with Google Api. " + + "Probably google api key missing: {ErrorMessage}", ex.Message); + } + return Array.Empty<(string, string)>(); + } + + private string GetText(ulong guildId, LocStr str) + => _strings.GetText(str, guildId); + + public IEnumerable<(string Name, Func Func)> GetPlaceholders() + { + // random song that's playing + yield return ("%music.playing%", () => + { + var randomPlayingTrack = _players + .Select(x => x.Value.GetCurrentTrack(out _)) + .Where(x => x is not null) + .Shuffle() + .FirstOrDefault(); + + if (randomPlayingTrack is null) + return "-"; + + return randomPlayingTrack.Title; + }); + + // number of servers currently listening to music + yield return ("%music.servers%", () => + { + var count = _players + .Select(x => x.Value.GetCurrentTrack(out _)) + .Count(x => x is not null); + + return count.ToString(); + }); + + yield return ("%music.queued%", () => + { + var count = _players + .Sum(x => x.Value.GetQueuedTracks().Count); + + return count.ToString(); + }); + } + + #region Settings + + private async Task GetSettingsInternalAsync(ulong guildId) + { + if (_settings.TryGetValue(guildId, out var settings)) + return settings; + + using var uow = _db.GetDbContext(); + var toReturn = _settings[guildId] = await uow.MusicPlayerSettings.ForGuildAsync(guildId); + await uow.SaveChangesAsync(); + + return toReturn; + } + + private async Task ModifySettingsInternalAsync( + ulong guildId, + Action action, + TState state) + { + using var uow = _db.GetDbContext(); + var ms = await uow.MusicPlayerSettings.ForGuildAsync(guildId); + action(ms, state); + await uow.SaveChangesAsync(); + _settings[guildId] = ms; + } + + public async Task SetMusicChannelAsync(ulong guildId, ulong? channelId) + { + if (channelId is null) + { + await UnsetMusicChannelAsync(guildId); return true; } - - public async Task UnsetMusicChannelAsync(ulong guildId) - { - await ModifySettingsInternalAsync(guildId, (settings, _) => - { - settings.MusicChannelId = null; - }, (ulong?)null); - - if (_outputChannels.TryGetValue(guildId, out var old)) - _outputChannels[guildId] = (old.Default, null); - } - - public async Task SetRepeatAsync(ulong guildId, PlayerRepeatType repeatType) - { - await ModifySettingsInternalAsync(guildId, (settings, type) => - { - settings.PlayerRepeat = type; - }, repeatType); - - if (TryGetMusicPlayer(guildId, out var mp)) - mp.SetRepeat(repeatType); - } - - public async Task SetVolumeAsync(ulong guildId, int value) - { - if (value < 0 || value > 100) - throw new ArgumentOutOfRangeException(nameof(value)); - await ModifySettingsInternalAsync(guildId, (settings, newValue) => - { - settings.Volume = newValue; - }, value); + var channel = _client.GetGuild(guildId)?.GetTextChannel(channelId.Value); + if (channel is null) + return false; + + await ModifySettingsInternalAsync(guildId, (settings, chId) => + { + settings.MusicChannelId = chId; + }, channelId); + + _outputChannels.AddOrUpdate(guildId, + (channel, channel), + (key, old) => (old.Default, channel)); - if (TryGetMusicPlayer(guildId, out var mp)) - mp.SetVolume(value); - } - - public async Task ToggleAutoDisconnectAsync(ulong guildId) - { - var newState = false; - await ModifySettingsInternalAsync(guildId, (settings, _) => - { - newState = settings.AutoDisconnect = !settings.AutoDisconnect; - }, default(object)); - - return newState; - } - - public async Task GetMusicQualityAsync(ulong guildId) - { - using var uow = _db.GetDbContext(); - var settings = await uow.MusicPlayerSettings.ForGuildAsync(guildId); - return settings.QualityPreset; - } - - public Task SetMusicQualityAsync(ulong guildId, QualityPreset preset) - { - return ModifySettingsInternalAsync(guildId, (settings, _) => - { - settings.QualityPreset = preset; - }, preset); - } - - #endregion + return true; } + + public async Task UnsetMusicChannelAsync(ulong guildId) + { + await ModifySettingsInternalAsync(guildId, (settings, _) => + { + settings.MusicChannelId = null; + }, (ulong?)null); + + if (_outputChannels.TryGetValue(guildId, out var old)) + _outputChannels[guildId] = (old.Default, null); + } + + public async Task SetRepeatAsync(ulong guildId, PlayerRepeatType repeatType) + { + await ModifySettingsInternalAsync(guildId, (settings, type) => + { + settings.PlayerRepeat = type; + }, repeatType); + + if (TryGetMusicPlayer(guildId, out var mp)) + mp.SetRepeat(repeatType); + } + + public async Task SetVolumeAsync(ulong guildId, int value) + { + if (value < 0 || value > 100) + throw new ArgumentOutOfRangeException(nameof(value)); + + await ModifySettingsInternalAsync(guildId, (settings, newValue) => + { + settings.Volume = newValue; + }, value); + + if (TryGetMusicPlayer(guildId, out var mp)) + mp.SetVolume(value); + } + + public async Task ToggleAutoDisconnectAsync(ulong guildId) + { + var newState = false; + await ModifySettingsInternalAsync(guildId, (settings, _) => + { + newState = settings.AutoDisconnect = !settings.AutoDisconnect; + }, default(object)); + + return newState; + } + + public async Task GetMusicQualityAsync(ulong guildId) + { + using var uow = _db.GetDbContext(); + var settings = await uow.MusicPlayerSettings.ForGuildAsync(guildId); + return settings.QualityPreset; + } + + public Task SetMusicQualityAsync(ulong guildId, QualityPreset preset) + { + return ModifySettingsInternalAsync(guildId, (settings, _) => + { + settings.QualityPreset = preset; + }, preset); + } + + #endregion } \ No newline at end of file diff --git a/src/NadekoBot/Modules/Music/Services/extractor/Misc.cs b/src/NadekoBot/Modules/Music/Services/extractor/Misc.cs index 145afdada..bb5e39e35 100644 --- a/src/NadekoBot/Modules/Music/Services/extractor/Misc.cs +++ b/src/NadekoBot/Modules/Music/Services/extractor/Misc.cs @@ -1,75 +1,71 @@ -using System; +namespace NadekoBot.Modules.Music.Services; -namespace NadekoBot.Modules.Music.Services +public sealed partial class YtLoader { - - public sealed partial class YtLoader + + public class InitRange { + public string Start { get; set; } + public string End { get; set; } + } - public class InitRange + public class IndexRange + { + public string Start { get; set; } + public string End { get; set; } + } + + public class ColorInfo + { + public string Primaries { get; set; } + public string TransferCharacteristics { get; set; } + public string MatrixCoefficients { get; set; } + } + + public class YtAdaptiveFormat + { + public int Itag { get; set; } + public string MimeType { get; set; } + public int Bitrate { get; set; } + public int Width { get; set; } + public int Height { get; set; } + public InitRange InitRange { get; set; } + public IndexRange IndexRange { get; set; } + public string LastModified { get; set; } + public string ContentLength { get; set; } + public string Quality { get; set; } + public int Fps { get; set; } + public string QualityLabel { get; set; } + public string ProjectionType { get; set; } + public int AverageBitrate { get; set; } + public ColorInfo ColorInfo { get; set; } + public string ApproxDurationMs { get; set; } + public string SignatureCipher { get; set; } + } + + public abstract class TrackInfo + { + public abstract string Url { get; } + public abstract string Title { get; } + public abstract TimeSpan Duration { get; } + } + + public sealed class YtTrackInfo : TrackInfo + { + private const string BaseYoutubeUrl = "https://youtube.com/watch?v="; + public override string Url { get; } + public override string Title { get; } + public override TimeSpan Duration { get; } + + private readonly string _videoId; + + public YtTrackInfo(string title, string videoId, TimeSpan duration) { - public string Start { get; set; } - public string End { get; set; } - } + Title = title; + Url = BaseYoutubeUrl + videoId; + Duration = duration; - public class IndexRange - { - public string Start { get; set; } - public string End { get; set; } - } - - public class ColorInfo - { - public string Primaries { get; set; } - public string TransferCharacteristics { get; set; } - public string MatrixCoefficients { get; set; } - } - - public class YtAdaptiveFormat - { - public int Itag { get; set; } - public string MimeType { get; set; } - public int Bitrate { get; set; } - public int Width { get; set; } - public int Height { get; set; } - public InitRange InitRange { get; set; } - public IndexRange IndexRange { get; set; } - public string LastModified { get; set; } - public string ContentLength { get; set; } - public string Quality { get; set; } - public int Fps { get; set; } - public string QualityLabel { get; set; } - public string ProjectionType { get; set; } - public int AverageBitrate { get; set; } - public ColorInfo ColorInfo { get; set; } - public string ApproxDurationMs { get; set; } - public string SignatureCipher { get; set; } - } - - public abstract class TrackInfo - { - public abstract string Url { get; } - public abstract string Title { get; } - public abstract TimeSpan Duration { get; } - } - - public sealed class YtTrackInfo : TrackInfo - { - private const string BaseYoutubeUrl = "https://youtube.com/watch?v="; - public override string Url { get; } - public override string Title { get; } - public override TimeSpan Duration { get; } - - private readonly string _videoId; - - public YtTrackInfo(string title, string videoId, TimeSpan duration) - { - Title = title; - Url = BaseYoutubeUrl + videoId; - Duration = duration; - - _videoId = videoId; - } + _videoId = videoId; } } } \ No newline at end of file diff --git a/src/NadekoBot/Modules/Music/Services/extractor/YtLoader.cs b/src/NadekoBot/Modules/Music/Services/extractor/YtLoader.cs index d71f3422c..57e14b33d 100644 --- a/src/NadekoBot/Modules/Music/Services/extractor/YtLoader.cs +++ b/src/NadekoBot/Modules/Music/Services/extractor/YtLoader.cs @@ -1,140 +1,133 @@ -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.Globalization; -using System.Linq; +using System.Globalization; using System.Net.Http; using System.Text; using System.Text.Json; using System.Threading.Tasks; -using Discord.Net; -using Serilog; -namespace NadekoBot.Modules.Music.Services +namespace NadekoBot.Modules.Music.Services; + +public sealed partial class YtLoader { - public sealed partial class YtLoader - { - private readonly IHttpClientFactory _httpFactory; - private static readonly byte[] YT_RESULT_INITIAL_DATA = Encoding.UTF8.GetBytes("var ytInitialData = "); - private static readonly byte[] YT_RESULT_JSON_END = Encoding.UTF8.GetBytes(";<"); + private readonly IHttpClientFactory _httpFactory; + private static readonly byte[] YT_RESULT_INITIAL_DATA = Encoding.UTF8.GetBytes("var ytInitialData = "); + private static readonly byte[] YT_RESULT_JSON_END = Encoding.UTF8.GetBytes(";<"); - private static readonly string[] durationFormats = new[] - { - @"m\:ss", @"mm\:ss", @"h\:mm\:ss", @"hh\:mm\:ss", @"hhh\:mm\:ss" - }; + private static readonly string[] durationFormats = new[] + { + @"m\:ss", @"mm\:ss", @"h\:mm\:ss", @"hh\:mm\:ss", @"hhh\:mm\:ss" + }; - public YtLoader(IHttpClientFactory httpFactory) + public YtLoader(IHttpClientFactory httpFactory) + { + _httpFactory = httpFactory; + } + + // public async Task LoadTrackByIdAsync(string videoId) + // { + // using var http = new HttpClient(); + // http.DefaultRequestHeaders.Add("X-YouTube-Client-Name", "1"); + // http.DefaultRequestHeaders.Add("X-YouTube-Client-Version", "2.20210520.09.00"); + // http.DefaultRequestHeaders.Add("Cookie", "CONSENT=YES+cb.20210530-19-p0.en+FX+071;"); + // + // var responseString = await http.GetStringAsync($"https://youtube.com?" + + // $"pbj=1" + + // $"&hl=en" + + // $"&v=" + videoId); + // + // var jsonDoc = JsonDocument.Parse(responseString).RootElement; + // var elem = jsonDoc.EnumerateArray() + // .FirstOrDefault(x => x.TryGetProperty("page", out var elem) && elem.GetString() == "watch"); + // + // var formatsJsonArray = elem.GetProperty("streamingdata") + // .GetProperty("formats") + // .GetRawText(); + // + // var formats = JsonSerializer.Deserialize>(formatsJsonArray); + // var result = formats + // .Where(x => x.MimeType.StartsWith("audio/")) + // .OrderByDescending(x => x.Bitrate) + // .FirstOrDefault(); + // + // if (result is null) + // return null; + // + // return new YtTrackInfo("1", "2", TimeSpan.Zero); + // } + + public async Task> LoadResultsAsync(string query) + { + query = Uri.EscapeDataString(query); + + using var http = _httpFactory.CreateClient(); + http.DefaultRequestHeaders.Add("Cookie", "CONSENT=YES+cb.20210530-19-p0.en+FX+071;"); + + byte[] response; + try { - _httpFactory = httpFactory; + response = await http.GetByteArrayAsync($"https://youtube.com/results?hl=en&search_query={query}"); + } + catch (HttpRequestException ex) + { + Log.Warning("Unable to retrieve data with YtLoader: {ErrorMessage}", ex.Message); + return null; } - // public async Task LoadTrackByIdAsync(string videoId) - // { - // using var http = new HttpClient(); - // http.DefaultRequestHeaders.Add("X-YouTube-Client-Name", "1"); - // http.DefaultRequestHeaders.Add("X-YouTube-Client-Version", "2.20210520.09.00"); - // http.DefaultRequestHeaders.Add("Cookie", "CONSENT=YES+cb.20210530-19-p0.en+FX+071;"); - // - // var responseString = await http.GetStringAsync($"https://youtube.com?" + - // $"pbj=1" + - // $"&hl=en" + - // $"&v=" + videoId); - // - // var jsonDoc = JsonDocument.Parse(responseString).RootElement; - // var elem = jsonDoc.EnumerateArray() - // .FirstOrDefault(x => x.TryGetProperty("page", out var elem) && elem.GetString() == "watch"); - // - // var formatsJsonArray = elem.GetProperty("streamingdata") - // .GetProperty("formats") - // .GetRawText(); - // - // var formats = JsonSerializer.Deserialize>(formatsJsonArray); - // var result = formats - // .Where(x => x.MimeType.StartsWith("audio/")) - // .OrderByDescending(x => x.Bitrate) - // .FirstOrDefault(); - // - // if (result is null) - // return null; - // - // return new YtTrackInfo("1", "2", TimeSpan.Zero); - // } + // there is a lot of useless html above the script tag, however if html gets significantly reduced + // this will result in the json being cut off - public async Task> LoadResultsAsync(string query) + var mem = GetScriptResponseSpan(response); + var root = JsonDocument.Parse(mem).RootElement; + + var tracksJsonItems = root + .GetProperty("contents") + .GetProperty("twoColumnSearchResultsRenderer") + .GetProperty("primaryContents") + .GetProperty("sectionListRenderer") + .GetProperty("contents") + [0] + .GetProperty("itemSectionRenderer") + .GetProperty("contents") + .EnumerateArray(); + + var tracks = new List(); + foreach (var track in tracksJsonItems) { - query = Uri.EscapeDataString(query); - - using var http = _httpFactory.CreateClient(); - http.DefaultRequestHeaders.Add("Cookie", "CONSENT=YES+cb.20210530-19-p0.en+FX+071;"); + if(!track.TryGetProperty("videoRenderer", out var elem)) + continue; - byte[] response; - try - { - response = await http.GetByteArrayAsync($"https://youtube.com/results?hl=en&search_query={query}"); - } - catch (HttpRequestException ex) - { - Log.Warning("Unable to retrieve data with YtLoader: {ErrorMessage}", ex.Message); - return null; - } + var videoId = elem.GetProperty("videoId").GetString(); + // var thumb = elem.GetProperty("thumbnail").GetProperty("thumbnails")[0].GetProperty("url").GetString(); + var title = elem.GetProperty("title").GetProperty("runs")[0].GetProperty("text").GetString(); + var durationString = elem.GetProperty("lengthText").GetProperty("simpleText").GetString(); - // there is a lot of useless html above the script tag, however if html gets significantly reduced - // this will result in the json being cut off - - var mem = GetScriptResponseSpan(response); - var root = JsonDocument.Parse(mem).RootElement; - - var tracksJsonItems = root - .GetProperty("contents") - .GetProperty("twoColumnSearchResultsRenderer") - .GetProperty("primaryContents") - .GetProperty("sectionListRenderer") - .GetProperty("contents") - [0] - .GetProperty("itemSectionRenderer") - .GetProperty("contents") - .EnumerateArray(); - - var tracks = new List(); - foreach (var track in tracksJsonItems) - { - if(!track.TryGetProperty("videoRenderer", out var elem)) - continue; - - var videoId = elem.GetProperty("videoId").GetString(); - // var thumb = elem.GetProperty("thumbnail").GetProperty("thumbnails")[0].GetProperty("url").GetString(); - var title = elem.GetProperty("title").GetProperty("runs")[0].GetProperty("text").GetString(); - var durationString = elem.GetProperty("lengthText").GetProperty("simpleText").GetString(); - - if (!TimeSpan.TryParseExact(durationString, durationFormats, CultureInfo.InvariantCulture, + if (!TimeSpan.TryParseExact(durationString, durationFormats, CultureInfo.InvariantCulture, out var duration)) - { - Log.Warning("Cannot parse duration: {DurationString}", durationString); - continue; - } - - tracks.Add(new YtTrackInfo(title, videoId, duration)); - if (tracks.Count >= 5) - break; + { + Log.Warning("Cannot parse duration: {DurationString}", durationString); + continue; } + + tracks.Add(new YtTrackInfo(title, videoId, duration)); + if (tracks.Count >= 5) + break; + } - return tracks; - } + return tracks; + } - private Memory GetScriptResponseSpan(byte[] response) - { - var responseSpan = response.AsSpan().Slice(140_000); - var startIndex = responseSpan.IndexOf(YT_RESULT_INITIAL_DATA); - if (startIndex == -1) - return null; // todo future try selecting html - startIndex += YT_RESULT_INITIAL_DATA.Length; + private Memory GetScriptResponseSpan(byte[] response) + { + var responseSpan = response.AsSpan().Slice(140_000); + var startIndex = responseSpan.IndexOf(YT_RESULT_INITIAL_DATA); + if (startIndex == -1) + return null; // todo future try selecting html + startIndex += YT_RESULT_INITIAL_DATA.Length; - var endIndex = 140_000 + startIndex + responseSpan.Slice(startIndex + 20_000).IndexOf(YT_RESULT_JSON_END) + 20_000; - startIndex += 140_000; - return response.AsMemory( - startIndex, - endIndex - startIndex - ); - } + var endIndex = 140_000 + startIndex + responseSpan.Slice(startIndex + 20_000).IndexOf(YT_RESULT_JSON_END) + 20_000; + startIndex += 140_000; + return response.AsMemory( + startIndex, + endIndex - startIndex + ); } } \ No newline at end of file diff --git a/src/NadekoBot/Modules/Nsfw/Common/Booru.cs b/src/NadekoBot/Modules/Nsfw/Common/Booru.cs index f172746e0..c2378f1a6 100644 --- a/src/NadekoBot/Modules/Nsfw/Common/Booru.cs +++ b/src/NadekoBot/Modules/Nsfw/Common/Booru.cs @@ -1,15 +1,14 @@ -namespace NadekoBot.Modules.Nsfw.Common +namespace NadekoBot.Modules.Nsfw.Common; + +public enum Booru { - public enum Booru - { - Safebooru, - E621, - Derpibooru, - Rule34, - Gelbooru, - Konachan, - Yandere, - Danbooru, - Sankaku - } -} + Safebooru, + E621, + Derpibooru, + Rule34, + Gelbooru, + Konachan, + Yandere, + Danbooru, + Sankaku +} \ No newline at end of file diff --git a/src/NadekoBot/Modules/Nsfw/Common/DapiImageObject.cs b/src/NadekoBot/Modules/Nsfw/Common/DapiImageObject.cs index 9c3198021..cb3596931 100644 --- a/src/NadekoBot/Modules/Nsfw/Common/DapiImageObject.cs +++ b/src/NadekoBot/Modules/Nsfw/Common/DapiImageObject.cs @@ -1,18 +1,17 @@ using System.Text.Json.Serialization; -namespace NadekoBot.Modules.Nsfw.Common +namespace NadekoBot.Modules.Nsfw.Common; + +public class DapiImageObject : IImageData { - public class DapiImageObject : IImageData - { - [JsonPropertyName("File_Url")] - public string FileUrl { get; set; } - public string Tags { get; set; } - [JsonPropertyName("Tag_String")] - public string TagString { get; set; } - public int Score { get; set; } - public string Rating { get; set; } + [JsonPropertyName("File_Url")] + public string FileUrl { get; set; } + public string Tags { get; set; } + [JsonPropertyName("Tag_String")] + public string TagString { get; set; } + public int Score { get; set; } + public string Rating { get; set; } - public ImageData ToCachedImageData(Booru type) - => new ImageData(this.FileUrl, type, this.Tags?.Split(' ') ?? this.TagString?.Split(' '), Score.ToString() ?? Rating); - } -} + public ImageData ToCachedImageData(Booru type) + => new ImageData(this.FileUrl, type, this.Tags?.Split(' ') ?? this.TagString?.Split(' '), Score.ToString() ?? Rating); +} \ No newline at end of file diff --git a/src/NadekoBot/Modules/Nsfw/Common/DapiTag.cs b/src/NadekoBot/Modules/Nsfw/Common/DapiTag.cs index 464f313c0..964663e4f 100644 --- a/src/NadekoBot/Modules/Nsfw/Common/DapiTag.cs +++ b/src/NadekoBot/Modules/Nsfw/Common/DapiTag.cs @@ -1,15 +1,14 @@ using System.Text.Json.Serialization; -namespace NadekoBot.Modules.Nsfw.Common -{ - public readonly struct DapiTag - { - public string Name { get; } +namespace NadekoBot.Modules.Nsfw.Common; - [JsonConstructor] - public DapiTag(string name) - { - Name = name; - } +public readonly struct DapiTag +{ + public string Name { get; } + + [JsonConstructor] + public DapiTag(string name) + { + Name = name; } } \ No newline at end of file diff --git a/src/NadekoBot/Modules/Nsfw/Common/DerpiContainer.cs b/src/NadekoBot/Modules/Nsfw/Common/DerpiContainer.cs index aa1bae6f5..58f825f9e 100644 --- a/src/NadekoBot/Modules/Nsfw/Common/DerpiContainer.cs +++ b/src/NadekoBot/Modules/Nsfw/Common/DerpiContainer.cs @@ -1,19 +1,18 @@ using System.Text.Json.Serialization; -namespace NadekoBot.Modules.Nsfw.Common -{ - public class DerpiContainer - { - public DerpiImageObject[] Images { get; set; } - } +namespace NadekoBot.Modules.Nsfw.Common; - public class DerpiImageObject : IImageData - { - [JsonPropertyName("view_url")] - public string ViewUrl { get; set; } - public string[] Tags { get; set; } - public int Score { get; set; } - public ImageData ToCachedImageData(Booru type) - => new(ViewUrl, type, Tags, Score.ToString("F1")); - } +public class DerpiContainer +{ + public DerpiImageObject[] Images { get; set; } } + +public class DerpiImageObject : IImageData +{ + [JsonPropertyName("view_url")] + public string ViewUrl { get; set; } + public string[] Tags { get; set; } + public int Score { get; set; } + public ImageData ToCachedImageData(Booru type) + => new(ViewUrl, type, Tags, Score.ToString("F1")); +} \ No newline at end of file diff --git a/src/NadekoBot/Modules/Nsfw/Common/Downloaders/DanbooruImageDownloader.cs b/src/NadekoBot/Modules/Nsfw/Common/Downloaders/DanbooruImageDownloader.cs index 88df6e481..55256bcd0 100644 --- a/src/NadekoBot/Modules/Nsfw/Common/Downloaders/DanbooruImageDownloader.cs +++ b/src/NadekoBot/Modules/Nsfw/Common/Downloaders/DanbooruImageDownloader.cs @@ -4,38 +4,37 @@ using System.Net.Http.Json; using System.Threading; using System.Threading.Tasks; -namespace NadekoBot.Modules.Nsfw.Common +namespace NadekoBot.Modules.Nsfw.Common; + +public sealed class DanbooruImageDownloader : DapiImageDownloader { - public sealed class DanbooruImageDownloader : DapiImageDownloader + // using them as concurrent hashsets, value doesn't matter + private static readonly ConcurrentDictionary _existentTags = new(); + private static readonly ConcurrentDictionary _nonexistentTags = new(); + + public override async Task IsTagValid(string tag, CancellationToken cancel = default) { - // using them as concurrent hashsets, value doesn't matter - private static readonly ConcurrentDictionary _existentTags = new(); - private static readonly ConcurrentDictionary _nonexistentTags = new(); + if (_existentTags.ContainsKey(tag)) + return true; - public override async Task IsTagValid(string tag, CancellationToken cancel = default) + if (_nonexistentTags.ContainsKey(tag)) + return false; + + var tags = await _http.GetFromJsonAsync(_baseUrl + + "/tags.json" + + $"?search[name_or_alias_matches]={tag}", + options: this._serializerOptions, + cancellationToken: cancel); + if (tags is {Length: > 0}) { - if (_existentTags.ContainsKey(tag)) - return true; - - if (_nonexistentTags.ContainsKey(tag)) - return false; - - var tags = await _http.GetFromJsonAsync(_baseUrl + - "/tags.json" + - $"?search[name_or_alias_matches]={tag}", - options: this._serializerOptions, - cancellationToken: cancel); - if (tags is {Length: > 0}) - { - return _existentTags[tag] = true; - } - - return _nonexistentTags[tag] = false; + return _existentTags[tag] = true; } + + return _nonexistentTags[tag] = false; + } - public DanbooruImageDownloader(HttpClient http) - : base(Booru.Danbooru, http, "http://danbooru.donmai.us") - { - } + public DanbooruImageDownloader(HttpClient http) + : base(Booru.Danbooru, http, "http://danbooru.donmai.us") + { } } \ No newline at end of file diff --git a/src/NadekoBot/Modules/Nsfw/Common/Downloaders/DapiImageDownloader.cs b/src/NadekoBot/Modules/Nsfw/Common/Downloaders/DapiImageDownloader.cs index bcf6d19e7..84653ada6 100644 --- a/src/NadekoBot/Modules/Nsfw/Common/Downloaders/DapiImageDownloader.cs +++ b/src/NadekoBot/Modules/Nsfw/Common/Downloaders/DapiImageDownloader.cs @@ -1,56 +1,53 @@ -using System.Collections.Generic; -using System.Linq; -using System.Net.Http; +using System.Net.Http; using System.Net.Http.Json; using System.Threading; using System.Threading.Tasks; -namespace NadekoBot.Modules.Nsfw.Common +namespace NadekoBot.Modules.Nsfw.Common; + +public abstract class DapiImageDownloader : ImageDownloader { - public abstract class DapiImageDownloader : ImageDownloader + protected readonly string _baseUrl; + + public DapiImageDownloader(Booru booru, HttpClient http, string baseUrl) : base(booru, http) { - protected readonly string _baseUrl; + _baseUrl = baseUrl; + } - public DapiImageDownloader(Booru booru, HttpClient http, string baseUrl) : base(booru, http) + public abstract Task IsTagValid(string tag, CancellationToken cancel = default); + protected async Task AllTagsValid(string[] tags, CancellationToken cancel = default) + { + var results = await Task.WhenAll(tags.Select(tag => IsTagValid(tag, cancel))); + + // if any of the tags is not valid, the query is not valid + foreach (var result in results) { - _baseUrl = baseUrl; + if (!result) + return false; } - public abstract Task IsTagValid(string tag, CancellationToken cancel = default); - protected async Task AllTagsValid(string[] tags, CancellationToken cancel = default) - { - var results = await Task.WhenAll(tags.Select(tag => IsTagValid(tag, cancel))); + return true; + } - // if any of the tags is not valid, the query is not valid - foreach (var result in results) - { - if (!result) - return false; - } + public override async Task> DownloadImagesAsync(string[] tags, int page, + bool isExplicit = false, CancellationToken cancel = default) + { + // up to 2 tags allowed on danbooru + if (tags.Length > 2) + return new(); - return true; - } + if (!await AllTagsValid(tags, cancel).ConfigureAwait(false)) + return new(); - public override async Task> DownloadImagesAsync(string[] tags, int page, - bool isExplicit = false, CancellationToken cancel = default) - { - // up to 2 tags allowed on danbooru - if (tags.Length > 2) - return new(); + var tagString = ImageDownloaderHelper.GetTagString(tags, isExplicit); - if (!await AllTagsValid(tags, cancel).ConfigureAwait(false)) - return new(); - - var tagString = ImageDownloaderHelper.GetTagString(tags, isExplicit); - - var uri = $"{_baseUrl}/posts.json?limit=200&tags={tagString}&page={page}"; - var imageObjects = await _http.GetFromJsonAsync(uri, _serializerOptions, cancel) - .ConfigureAwait(false); - if (imageObjects is null) - return new(); - return imageObjects - .Where(x => x.FileUrl is not null) - .ToList(); - } + var uri = $"{_baseUrl}/posts.json?limit=200&tags={tagString}&page={page}"; + var imageObjects = await _http.GetFromJsonAsync(uri, _serializerOptions, cancel) + .ConfigureAwait(false); + if (imageObjects is null) + return new(); + return imageObjects + .Where(x => x.FileUrl is not null) + .ToList(); } } \ No newline at end of file diff --git a/src/NadekoBot/Modules/Nsfw/Common/Downloaders/DerpibooruImageDownloader.cs b/src/NadekoBot/Modules/Nsfw/Common/Downloaders/DerpibooruImageDownloader.cs index f0e1569ce..0911182e6 100644 --- a/src/NadekoBot/Modules/Nsfw/Common/Downloaders/DerpibooruImageDownloader.cs +++ b/src/NadekoBot/Modules/Nsfw/Common/Downloaders/DerpibooruImageDownloader.cs @@ -1,35 +1,32 @@ -using System.Collections.Generic; -using System.Linq; -using System.Net.Http; +using System.Net.Http; using System.Net.Http.Json; using System.Threading; using System.Threading.Tasks; using NadekoBot.Extensions; -namespace NadekoBot.Modules.Nsfw.Common -{ - public class DerpibooruImageDownloader : ImageDownloader - { - public DerpibooruImageDownloader(HttpClient http) : base(Booru.Derpibooru, http) - { - } +namespace NadekoBot.Modules.Nsfw.Common; - public override async Task> DownloadImagesAsync(string[] tags, int page, bool isExplicit = false, CancellationToken cancel = default) - { - var tagString = ImageDownloaderHelper.GetTagString(tags, isExplicit); - var uri = $"https://www.derpibooru.org/api/v1/json/search/images?q={tagString.Replace('+', ',')}&per_page=49&page={page}"; - using var req = new HttpRequestMessage(HttpMethod.Get, uri); - req.Headers.AddFakeHeaders(); - using var res = await _http.SendAsync(req, cancel).ConfigureAwait(false); - res.EnsureSuccessStatusCode(); +public class DerpibooruImageDownloader : ImageDownloader +{ + public DerpibooruImageDownloader(HttpClient http) : base(Booru.Derpibooru, http) + { + } + + public override async Task> DownloadImagesAsync(string[] tags, int page, bool isExplicit = false, CancellationToken cancel = default) + { + var tagString = ImageDownloaderHelper.GetTagString(tags, isExplicit); + var uri = $"https://www.derpibooru.org/api/v1/json/search/images?q={tagString.Replace('+', ',')}&per_page=49&page={page}"; + using var req = new HttpRequestMessage(HttpMethod.Get, uri); + req.Headers.AddFakeHeaders(); + using var res = await _http.SendAsync(req, cancel).ConfigureAwait(false); + res.EnsureSuccessStatusCode(); - var container = await res.Content.ReadFromJsonAsync(_serializerOptions, cancel).ConfigureAwait(false); - if (container?.Images is null) - return new(); + var container = await res.Content.ReadFromJsonAsync(_serializerOptions, cancel).ConfigureAwait(false); + if (container?.Images is null) + return new(); - return container.Images - .Where(x => !string.IsNullOrWhiteSpace(x.ViewUrl)) - .ToList(); - } + return container.Images + .Where(x => !string.IsNullOrWhiteSpace(x.ViewUrl)) + .ToList(); } } \ No newline at end of file diff --git a/src/NadekoBot/Modules/Nsfw/Common/Downloaders/E621ImageDownloader.cs b/src/NadekoBot/Modules/Nsfw/Common/Downloaders/E621ImageDownloader.cs index f5d91df4b..084858bb3 100644 --- a/src/NadekoBot/Modules/Nsfw/Common/Downloaders/E621ImageDownloader.cs +++ b/src/NadekoBot/Modules/Nsfw/Common/Downloaders/E621ImageDownloader.cs @@ -1,35 +1,32 @@ -using System.Collections.Generic; -using System.Linq; -using System.Net.Http; +using System.Net.Http; using System.Net.Http.Json; using System.Threading; using System.Threading.Tasks; using NadekoBot.Extensions; -namespace NadekoBot.Modules.Nsfw.Common +namespace NadekoBot.Modules.Nsfw.Common; + +public class E621ImageDownloader : ImageDownloader { - public class E621ImageDownloader : ImageDownloader + public E621ImageDownloader(HttpClient http) : base(Booru.E621, http) { - public E621ImageDownloader(HttpClient http) : base(Booru.E621, http) - { - } + } - public override async Task> DownloadImagesAsync(string[] tags, int page, bool isExplicit = false, CancellationToken cancel = default) - { - var tagString = ImageDownloaderHelper.GetTagString(tags, isExplicit: isExplicit); - var uri = $"https://e621.net/posts.json?limit=32&tags={tagString}&page={page}"; - using var req = new HttpRequestMessage(HttpMethod.Get, uri); - req.Headers.AddFakeHeaders(); - using var res = await _http.SendAsync(req, cancel).ConfigureAwait(false); - res.EnsureSuccessStatusCode(); + public override async Task> DownloadImagesAsync(string[] tags, int page, bool isExplicit = false, CancellationToken cancel = default) + { + var tagString = ImageDownloaderHelper.GetTagString(tags, isExplicit: isExplicit); + var uri = $"https://e621.net/posts.json?limit=32&tags={tagString}&page={page}"; + using var req = new HttpRequestMessage(HttpMethod.Get, uri); + req.Headers.AddFakeHeaders(); + using var res = await _http.SendAsync(req, cancel).ConfigureAwait(false); + res.EnsureSuccessStatusCode(); - var data = await res.Content.ReadFromJsonAsync(_serializerOptions, cancel).ConfigureAwait(false); - if (data?.Posts is null) - return new(); + var data = await res.Content.ReadFromJsonAsync(_serializerOptions, cancel).ConfigureAwait(false); + if (data?.Posts is null) + return new(); - return data.Posts - .Where(x => !string.IsNullOrWhiteSpace(x.File?.Url)) - .ToList(); - } + return data.Posts + .Where(x => !string.IsNullOrWhiteSpace(x.File?.Url)) + .ToList(); } } \ No newline at end of file diff --git a/src/NadekoBot/Modules/Nsfw/Common/Downloaders/E621Response.cs b/src/NadekoBot/Modules/Nsfw/Common/Downloaders/E621Response.cs index 807e2db97..b4d87c3b7 100644 --- a/src/NadekoBot/Modules/Nsfw/Common/Downloaders/E621Response.cs +++ b/src/NadekoBot/Modules/Nsfw/Common/Downloaders/E621Response.cs @@ -1,9 +1,6 @@ -using System.Collections.Generic; +namespace NadekoBot.Modules.Nsfw.Common; -namespace NadekoBot.Modules.Nsfw.Common +public class E621Response { - public class E621Response - { - public List Posts { get; set; } - } + public List Posts { get; set; } } \ No newline at end of file diff --git a/src/NadekoBot/Modules/Nsfw/Common/Downloaders/GelbooruImageDownloader.cs b/src/NadekoBot/Modules/Nsfw/Common/Downloaders/GelbooruImageDownloader.cs index 5a0645f88..47babca14 100644 --- a/src/NadekoBot/Modules/Nsfw/Common/Downloaders/GelbooruImageDownloader.cs +++ b/src/NadekoBot/Modules/Nsfw/Common/Downloaders/GelbooruImageDownloader.cs @@ -1,35 +1,32 @@ -using System.Collections.Generic; -using System.Linq; -using System.Net.Http; +using System.Net.Http; using System.Text.Json; using System.Threading; using System.Threading.Tasks; -namespace NadekoBot.Modules.Nsfw.Common +namespace NadekoBot.Modules.Nsfw.Common; + +public class GelbooruImageDownloader : ImageDownloader { - public class GelbooruImageDownloader : ImageDownloader + public GelbooruImageDownloader(HttpClient http) : base(Booru.Gelbooru, http) { - public GelbooruImageDownloader(HttpClient http) : base(Booru.Gelbooru, http) - { - } + } - public override async Task> DownloadImagesAsync(string[] tags, int page, bool isExplicit = false, CancellationToken cancel = default) - { - var tagString = ImageDownloaderHelper.GetTagString(tags, isExplicit); - var uri = $"http://gelbooru.com/index.php?page=dapi&s=post&json=1&q=index&limit=100" + - $"&tags={tagString}&pid={page}"; - using var req = new HttpRequestMessage(HttpMethod.Get, uri); - using var res = await _http.SendAsync(req, cancel).ConfigureAwait(false); - res.EnsureSuccessStatusCode(); - var resString = await res.Content.ReadAsStringAsync(cancel); - if (string.IsNullOrWhiteSpace(resString)) - return new(); + public override async Task> DownloadImagesAsync(string[] tags, int page, bool isExplicit = false, CancellationToken cancel = default) + { + var tagString = ImageDownloaderHelper.GetTagString(tags, isExplicit); + var uri = $"http://gelbooru.com/index.php?page=dapi&s=post&json=1&q=index&limit=100" + + $"&tags={tagString}&pid={page}"; + using var req = new HttpRequestMessage(HttpMethod.Get, uri); + using var res = await _http.SendAsync(req, cancel).ConfigureAwait(false); + res.EnsureSuccessStatusCode(); + var resString = await res.Content.ReadAsStringAsync(cancel); + if (string.IsNullOrWhiteSpace(resString)) + return new(); - var images = JsonSerializer.Deserialize>(resString, _serializerOptions); - if (images is null) - return new(); + var images = JsonSerializer.Deserialize>(resString, _serializerOptions); + if (images is null) + return new(); - return images.Where(x => x.FileUrl is not null).ToList(); - } + return images.Where(x => x.FileUrl is not null).ToList(); } } \ No newline at end of file diff --git a/src/NadekoBot/Modules/Nsfw/Common/Downloaders/IImageDownloader.cs b/src/NadekoBot/Modules/Nsfw/Common/Downloaders/IImageDownloader.cs index c05bd35b6..354c5c46f 100644 --- a/src/NadekoBot/Modules/Nsfw/Common/Downloaders/IImageDownloader.cs +++ b/src/NadekoBot/Modules/Nsfw/Common/Downloaders/IImageDownloader.cs @@ -1,12 +1,10 @@ -using System.Collections.Generic; -using System.Threading; +using System.Threading; using System.Threading.Tasks; -namespace NadekoBot.Modules.Nsfw.Common +namespace NadekoBot.Modules.Nsfw.Common; + +public interface IImageDownloader { - public interface IImageDownloader - { - Task> DownloadImageDataAsync(string[] tags, int page = 0, - bool isExplicit = false, CancellationToken cancel = default); - } + Task> DownloadImageDataAsync(string[] tags, int page = 0, + bool isExplicit = false, CancellationToken cancel = default); } \ No newline at end of file diff --git a/src/NadekoBot/Modules/Nsfw/Common/Downloaders/ImageDownloader.cs b/src/NadekoBot/Modules/Nsfw/Common/Downloaders/ImageDownloader.cs index fa74a68a9..cc2a79695 100644 --- a/src/NadekoBot/Modules/Nsfw/Common/Downloaders/ImageDownloader.cs +++ b/src/NadekoBot/Modules/Nsfw/Common/Downloaders/ImageDownloader.cs @@ -1,40 +1,37 @@ -using System.Collections.Generic; -using System.Linq; -using System.Net.Http; +using System.Net.Http; using System.Text.Json; using System.Text.Json.Serialization; using System.Threading; using System.Threading.Tasks; -namespace NadekoBot.Modules.Nsfw.Common +namespace NadekoBot.Modules.Nsfw.Common; + +public abstract class ImageDownloader : IImageDownloader + where T : IImageData { - public abstract class ImageDownloader : IImageDownloader - where T : IImageData + protected readonly HttpClient _http; + + protected JsonSerializerOptions _serializerOptions = new JsonSerializerOptions() { - protected readonly HttpClient _http; - - protected JsonSerializerOptions _serializerOptions = new JsonSerializerOptions() - { - PropertyNameCaseInsensitive = true, - NumberHandling = JsonNumberHandling.WriteAsString | JsonNumberHandling.AllowReadingFromString, + PropertyNameCaseInsensitive = true, + NumberHandling = JsonNumberHandling.WriteAsString | JsonNumberHandling.AllowReadingFromString, - }; + }; - public Booru Booru { get; } + public Booru Booru { get; } - public ImageDownloader(Booru booru, HttpClient http) - { - _http = http; - this.Booru = booru; - } + public ImageDownloader(Booru booru, HttpClient http) + { + _http = http; + this.Booru = booru; + } - public abstract Task> DownloadImagesAsync(string[] tags, int page, bool isExplicit = false, CancellationToken cancel = default); + public abstract Task> DownloadImagesAsync(string[] tags, int page, bool isExplicit = false, CancellationToken cancel = default); - public async Task> DownloadImageDataAsync(string[] tags, int page, bool isExplicit = false, - CancellationToken cancel = default) - { - var images = await DownloadImagesAsync(tags, page, isExplicit, cancel).ConfigureAwait(false); - return images.Select(x => x.ToCachedImageData(Booru)).ToList(); - } + public async Task> DownloadImageDataAsync(string[] tags, int page, bool isExplicit = false, + CancellationToken cancel = default) + { + var images = await DownloadImagesAsync(tags, page, isExplicit, cancel).ConfigureAwait(false); + return images.Select(x => x.ToCachedImageData(Booru)).ToList(); } } \ No newline at end of file diff --git a/src/NadekoBot/Modules/Nsfw/Common/Downloaders/ImageDownloaderHelper.cs b/src/NadekoBot/Modules/Nsfw/Common/Downloaders/ImageDownloaderHelper.cs index 592b1defd..854353204 100644 --- a/src/NadekoBot/Modules/Nsfw/Common/Downloaders/ImageDownloaderHelper.cs +++ b/src/NadekoBot/Modules/Nsfw/Common/Downloaders/ImageDownloaderHelper.cs @@ -1,17 +1,12 @@ -using System; -using System.Collections.Generic; -using System.Linq; +namespace NadekoBot.Modules.Nsfw.Common; -namespace NadekoBot.Modules.Nsfw.Common +public static class ImageDownloaderHelper { - public static class ImageDownloaderHelper + public static string GetTagString(IEnumerable tags, bool isExplicit = false) { - public static string GetTagString(IEnumerable tags, bool isExplicit = false) - { - if (isExplicit) - tags = tags.Append("rating:explicit"); + if (isExplicit) + tags = tags.Append("rating:explicit"); - return string.Join('+', tags.Select(x => x.ToLowerInvariant())); - } + return string.Join('+', tags.Select(x => x.ToLowerInvariant())); } } \ No newline at end of file diff --git a/src/NadekoBot/Modules/Nsfw/Common/Downloaders/KonachanImageDownloader.cs b/src/NadekoBot/Modules/Nsfw/Common/Downloaders/KonachanImageDownloader.cs index 292b9898f..b226b4f86 100644 --- a/src/NadekoBot/Modules/Nsfw/Common/Downloaders/KonachanImageDownloader.cs +++ b/src/NadekoBot/Modules/Nsfw/Common/Downloaders/KonachanImageDownloader.cs @@ -1,33 +1,30 @@ -using System.Collections.Generic; -using System.Linq; -using System.Net.Http; +using System.Net.Http; using System.Net.Http.Json; using System.Threading; using System.Threading.Tasks; -namespace NadekoBot.Modules.Nsfw.Common +namespace NadekoBot.Modules.Nsfw.Common; + +public sealed class KonachanImageDownloader : ImageDownloader { - public sealed class KonachanImageDownloader : ImageDownloader + private readonly string _baseUrl; + + public KonachanImageDownloader(HttpClient http) + : base(Booru.Konachan, http) { - private readonly string _baseUrl; + _baseUrl = "https://konachan.com"; + } - public KonachanImageDownloader(HttpClient http) - : base(Booru.Konachan, http) - { - _baseUrl = "https://konachan.com"; - } - - public override async Task> DownloadImagesAsync(string[] tags, int page, bool isExplicit = false, CancellationToken cancel = default) - { - var tagString = ImageDownloaderHelper.GetTagString(tags, isExplicit); - var uri = $"{_baseUrl}/post.json?s=post&q=index&limit=200&tags={tagString}&page={page}"; - var imageObjects = await _http.GetFromJsonAsync(uri, _serializerOptions, cancel) - .ConfigureAwait(false); - if (imageObjects is null) - return new(); - return imageObjects - .Where(x => x.FileUrl is not null) - .ToList(); - } + public override async Task> DownloadImagesAsync(string[] tags, int page, bool isExplicit = false, CancellationToken cancel = default) + { + var tagString = ImageDownloaderHelper.GetTagString(tags, isExplicit); + var uri = $"{_baseUrl}/post.json?s=post&q=index&limit=200&tags={tagString}&page={page}"; + var imageObjects = await _http.GetFromJsonAsync(uri, _serializerOptions, cancel) + .ConfigureAwait(false); + if (imageObjects is null) + return new(); + return imageObjects + .Where(x => x.FileUrl is not null) + .ToList(); } } \ No newline at end of file diff --git a/src/NadekoBot/Modules/Nsfw/Common/Downloaders/Rule34ImageDownloader.cs b/src/NadekoBot/Modules/Nsfw/Common/Downloaders/Rule34ImageDownloader.cs index 880b7a0e6..2d00aeaf3 100644 --- a/src/NadekoBot/Modules/Nsfw/Common/Downloaders/Rule34ImageDownloader.cs +++ b/src/NadekoBot/Modules/Nsfw/Common/Downloaders/Rule34ImageDownloader.cs @@ -1,31 +1,28 @@ -using System.Collections.Generic; -using System.Linq; -using System.Net.Http; +using System.Net.Http; using System.Net.Http.Json; using System.Threading; using System.Threading.Tasks; -namespace NadekoBot.Modules.Nsfw.Common +namespace NadekoBot.Modules.Nsfw.Common; + +public class Rule34ImageDownloader : ImageDownloader { - public class Rule34ImageDownloader : ImageDownloader + public Rule34ImageDownloader(HttpClient http) : base(Booru.Rule34, http) { - public Rule34ImageDownloader(HttpClient http) : base(Booru.Rule34, http) - { - } + } - public override async Task> DownloadImagesAsync(string[] tags, int page, bool isExplicit = false, CancellationToken cancel = default) - { - var tagString = ImageDownloaderHelper.GetTagString(tags); - var uri = $"https://rule34.xxx/index.php?page=dapi&s=post&q=index&json=1&limit=100" + - $"&tags={tagString}&pid={page}"; - var images = await _http.GetFromJsonAsync>(uri, _serializerOptions, cancel).ConfigureAwait(false); + public override async Task> DownloadImagesAsync(string[] tags, int page, bool isExplicit = false, CancellationToken cancel = default) + { + var tagString = ImageDownloaderHelper.GetTagString(tags); + var uri = $"https://rule34.xxx/index.php?page=dapi&s=post&q=index&json=1&limit=100" + + $"&tags={tagString}&pid={page}"; + var images = await _http.GetFromJsonAsync>(uri, _serializerOptions, cancel).ConfigureAwait(false); - if (images is null) - return new(); + if (images is null) + return new(); - return images - .Where(img => !string.IsNullOrWhiteSpace(img.Image)) - .ToList(); - } + return images + .Where(img => !string.IsNullOrWhiteSpace(img.Image)) + .ToList(); } } \ No newline at end of file diff --git a/src/NadekoBot/Modules/Nsfw/Common/Downloaders/SafebooruImageDownloader.cs b/src/NadekoBot/Modules/Nsfw/Common/Downloaders/SafebooruImageDownloader.cs index 081695428..a43203306 100644 --- a/src/NadekoBot/Modules/Nsfw/Common/Downloaders/SafebooruImageDownloader.cs +++ b/src/NadekoBot/Modules/Nsfw/Common/Downloaders/SafebooruImageDownloader.cs @@ -1,26 +1,24 @@ -using System.Collections.Generic; -using System.Net.Http; +using System.Net.Http; using System.Net.Http.Json; using System.Threading; using System.Threading.Tasks; -namespace NadekoBot.Modules.Nsfw.Common +namespace NadekoBot.Modules.Nsfw.Common; + +public class SafebooruImageDownloader : ImageDownloader { - public class SafebooruImageDownloader : ImageDownloader + public SafebooruImageDownloader(HttpClient http) : base(Booru.Safebooru, http) { - public SafebooruImageDownloader(HttpClient http) : base(Booru.Safebooru, http) - { - } + } - public override async Task> DownloadImagesAsync(string[] tags, int page, bool isExplicit = false, CancellationToken cancel = default) - { - var tagString = ImageDownloaderHelper.GetTagString(tags, isExplicit: false); - var uri = $"https://safebooru.org/index.php?page=dapi&s=post&q=index&limit=200&tags={tagString}&json=1&pid={page}"; - var images = await _http.GetFromJsonAsync>(uri, _serializerOptions, cancellationToken: cancel); - if (images is null) - return new(); + public override async Task> DownloadImagesAsync(string[] tags, int page, bool isExplicit = false, CancellationToken cancel = default) + { + var tagString = ImageDownloaderHelper.GetTagString(tags, isExplicit: false); + var uri = $"https://safebooru.org/index.php?page=dapi&s=post&q=index&limit=200&tags={tagString}&json=1&pid={page}"; + var images = await _http.GetFromJsonAsync>(uri, _serializerOptions, cancellationToken: cancel); + if (images is null) + return new(); - return images; - } + return images; } } \ No newline at end of file diff --git a/src/NadekoBot/Modules/Nsfw/Common/Downloaders/SankakuImageDownloader.cs b/src/NadekoBot/Modules/Nsfw/Common/Downloaders/SankakuImageDownloader.cs index 0221b8934..94be3f2f5 100644 --- a/src/NadekoBot/Modules/Nsfw/Common/Downloaders/SankakuImageDownloader.cs +++ b/src/NadekoBot/Modules/Nsfw/Common/Downloaders/SankakuImageDownloader.cs @@ -1,34 +1,31 @@ -using System.Collections.Generic; -using System.Linq; using System.Net.Http; using System.Text.Json; using System.Threading; using System.Threading.Tasks; using NadekoBot.Extensions; -namespace NadekoBot.Modules.Nsfw.Common +namespace NadekoBot.Modules.Nsfw.Common; + +public sealed class SankakuImageDownloader : ImageDownloader { - public sealed class SankakuImageDownloader : ImageDownloader + private readonly string _baseUrl; + + public SankakuImageDownloader(HttpClient http) + : base(Booru.Sankaku, http) { - private readonly string _baseUrl; - - public SankakuImageDownloader(HttpClient http) - : base(Booru.Sankaku, http) - { - _baseUrl = "https://capi-v2.sankakucomplex.com"; - _http.AddFakeHeaders(); - } + _baseUrl = "https://capi-v2.sankakucomplex.com"; + _http.AddFakeHeaders(); + } - public override async Task> DownloadImagesAsync(string[] tags, int page, bool isExplicit = false, CancellationToken cancel = default) - { - // explicit probably not supported - var tagString = ImageDownloaderHelper.GetTagString(tags, false); + public override async Task> DownloadImagesAsync(string[] tags, int page, bool isExplicit = false, CancellationToken cancel = default) + { + // explicit probably not supported + var tagString = ImageDownloaderHelper.GetTagString(tags, false); - var uri = $"{_baseUrl}/posts?tags={tagString}&limit=50"; - var data = await _http.GetStringAsync(uri).ConfigureAwait(false); - return JsonSerializer.Deserialize(data, _serializerOptions) - .Where(x => !string.IsNullOrWhiteSpace(x.FileUrl) && x.FileType.StartsWith("image")) - .ToList(); - } + var uri = $"{_baseUrl}/posts?tags={tagString}&limit=50"; + var data = await _http.GetStringAsync(uri).ConfigureAwait(false); + return JsonSerializer.Deserialize(data, _serializerOptions) + .Where(x => !string.IsNullOrWhiteSpace(x.FileUrl) && x.FileType.StartsWith("image")) + .ToList(); } } \ No newline at end of file diff --git a/src/NadekoBot/Modules/Nsfw/Common/Downloaders/YandereImageDownloader.cs b/src/NadekoBot/Modules/Nsfw/Common/Downloaders/YandereImageDownloader.cs index 5c3bb7aae..ad2122f16 100644 --- a/src/NadekoBot/Modules/Nsfw/Common/Downloaders/YandereImageDownloader.cs +++ b/src/NadekoBot/Modules/Nsfw/Common/Downloaders/YandereImageDownloader.cs @@ -1,34 +1,30 @@ -using System.Collections.Generic; -using System.Linq; -using System.Net.Http; +using System.Net.Http; using System.Net.Http.Json; using System.Threading; using System.Threading.Tasks; -using NadekoBot.Extensions; -namespace NadekoBot.Modules.Nsfw.Common +namespace NadekoBot.Modules.Nsfw.Common; + +public sealed class YandereImageDownloader : ImageDownloader { - public sealed class YandereImageDownloader : ImageDownloader - { - private readonly string _baseUrl; + private readonly string _baseUrl; - public YandereImageDownloader(HttpClient http) - : base(Booru.Yandere, http) - { - _baseUrl = "https://yande.re"; - } - public override async Task> DownloadImagesAsync(string[] tags, int page, bool isExplicit = false, CancellationToken cancel = default) - { - var tagString = ImageDownloaderHelper.GetTagString(tags, isExplicit); + public YandereImageDownloader(HttpClient http) + : base(Booru.Yandere, http) + { + _baseUrl = "https://yande.re"; + } + public override async Task> DownloadImagesAsync(string[] tags, int page, bool isExplicit = false, CancellationToken cancel = default) + { + var tagString = ImageDownloaderHelper.GetTagString(tags, isExplicit); - var uri = $"{_baseUrl}/post.json?limit=200&tags={tagString}&page={page}"; - var imageObjects = await _http.GetFromJsonAsync(uri, _serializerOptions, cancel) - .ConfigureAwait(false); - if (imageObjects is null) - return new(); - return imageObjects - .Where(x => x.FileUrl is not null) - .ToList(); - } + var uri = $"{_baseUrl}/post.json?limit=200&tags={tagString}&page={page}"; + var imageObjects = await _http.GetFromJsonAsync(uri, _serializerOptions, cancel) + .ConfigureAwait(false); + if (imageObjects is null) + return new(); + return imageObjects + .Where(x => x.FileUrl is not null) + .ToList(); } } \ No newline at end of file diff --git a/src/NadekoBot/Modules/Nsfw/Common/E621Object.cs b/src/NadekoBot/Modules/Nsfw/Common/E621Object.cs index 39919b304..5ff0f5f6f 100644 --- a/src/NadekoBot/Modules/Nsfw/Common/E621Object.cs +++ b/src/NadekoBot/Modules/Nsfw/Common/E621Object.cs @@ -1,27 +1,26 @@ -namespace NadekoBot.Modules.Nsfw.Common +namespace NadekoBot.Modules.Nsfw.Common; + +public class E621Object : IImageData { - public class E621Object : IImageData + public class FileData { - public class FileData - { - public string Url { get; set; } - } - - public class TagData - { - public string[] General { get; set; } - } - - public class ScoreData - { - public int Total { get; set; } - } - - public FileData File { get; set; } - public TagData Tags { get; set; } - public ScoreData Score { get; set; } - - public ImageData ToCachedImageData(Booru type) - => new(File.Url, Booru.E621, Tags.General, Score.Total.ToString()); + public string Url { get; set; } } + + public class TagData + { + public string[] General { get; set; } + } + + public class ScoreData + { + public int Total { get; set; } + } + + public FileData File { get; set; } + public TagData Tags { get; set; } + public ScoreData Score { get; set; } + + public ImageData ToCachedImageData(Booru type) + => new(File.Url, Booru.E621, Tags.General, Score.Total.ToString()); } \ No newline at end of file diff --git a/src/NadekoBot/Modules/Nsfw/Common/IImageData.cs b/src/NadekoBot/Modules/Nsfw/Common/IImageData.cs index bb595f038..b0ff40882 100644 --- a/src/NadekoBot/Modules/Nsfw/Common/IImageData.cs +++ b/src/NadekoBot/Modules/Nsfw/Common/IImageData.cs @@ -1,7 +1,6 @@ -namespace NadekoBot.Modules.Nsfw.Common +namespace NadekoBot.Modules.Nsfw.Common; + +public interface IImageData { - public interface IImageData - { - ImageData ToCachedImageData(Booru type); - } + ImageData ToCachedImageData(Booru type); } \ No newline at end of file diff --git a/src/NadekoBot/Modules/Nsfw/Common/ImageData.cs b/src/NadekoBot/Modules/Nsfw/Common/ImageData.cs index a5781c888..7e61d37c2 100644 --- a/src/NadekoBot/Modules/Nsfw/Common/ImageData.cs +++ b/src/NadekoBot/Modules/Nsfw/Common/ImageData.cs @@ -1,43 +1,38 @@ -using System; -using System.Collections.Generic; -using System.Linq; +namespace NadekoBot.Modules.Nsfw.Common; -namespace NadekoBot.Modules.Nsfw.Common +public class ImageData : IComparable { - public class ImageData : IComparable + public Booru SearchType { get; } + public string FileUrl { get; } + public HashSet Tags { get; } + public string Rating { get; } + + public ImageData(string url, Booru type, string[] tags, string rating) { - public Booru SearchType { get; } - public string FileUrl { get; } - public HashSet Tags { get; } - public string Rating { get; } - - public ImageData(string url, Booru type, string[] tags, string rating) + if (type == Booru.Danbooru && !Uri.IsWellFormedUriString(url, UriKind.Absolute)) { - if (type == Booru.Danbooru && !Uri.IsWellFormedUriString(url, UriKind.Absolute)) - { - this.FileUrl = "https://danbooru.donmai.us" + url; - } - else - { - this.FileUrl = url.StartsWith("http", StringComparison.InvariantCulture) ? url : "https:" + url; - } + this.FileUrl = "https://danbooru.donmai.us" + url; + } + else + { + this.FileUrl = url.StartsWith("http", StringComparison.InvariantCulture) ? url : "https:" + url; + } - this.SearchType = type; - this.FileUrl = url; - this.Tags = tags.ToHashSet(); - this.Rating = rating; - } - - public override string ToString() - { - return FileUrl; - } - - public override int GetHashCode() => FileUrl.GetHashCode(); - public override bool Equals(object obj) - => obj is ImageData ico && ico.FileUrl == this.FileUrl; - - public int CompareTo(ImageData other) - => string.Compare(FileUrl, other.FileUrl, StringComparison.InvariantCulture); + this.SearchType = type; + this.FileUrl = url; + this.Tags = tags.ToHashSet(); + this.Rating = rating; } -} + + public override string ToString() + { + return FileUrl; + } + + public override int GetHashCode() => FileUrl.GetHashCode(); + public override bool Equals(object obj) + => obj is ImageData ico && ico.FileUrl == this.FileUrl; + + public int CompareTo(ImageData other) + => string.Compare(FileUrl, other.FileUrl, StringComparison.InvariantCulture); +} \ No newline at end of file diff --git a/src/NadekoBot/Modules/Nsfw/Common/Rule34Object.cs b/src/NadekoBot/Modules/Nsfw/Common/Rule34Object.cs index eaf465d52..d193b9cc5 100644 --- a/src/NadekoBot/Modules/Nsfw/Common/Rule34Object.cs +++ b/src/NadekoBot/Modules/Nsfw/Common/Rule34Object.cs @@ -1,19 +1,18 @@ -namespace NadekoBot.Modules.Nsfw.Common -{ - public class Rule34Object : IImageData - { - public string Image { get; init; } - public int Directory { get; init; } - public string Tags { get; init; } - public int Score { get; init; } +namespace NadekoBot.Modules.Nsfw.Common; - public ImageData ToCachedImageData(Booru type) - { - return new ImageData( - $"https://img.rule34.xxx//images/{Directory}/{Image}", - Booru.Rule34, - Tags.Split(' '), - Score.ToString()); - } +public class Rule34Object : IImageData +{ + public string Image { get; init; } + public int Directory { get; init; } + public string Tags { get; init; } + public int Score { get; init; } + + public ImageData ToCachedImageData(Booru type) + { + return new ImageData( + $"https://img.rule34.xxx//images/{Directory}/{Image}", + Booru.Rule34, + Tags.Split(' '), + Score.ToString()); } } \ No newline at end of file diff --git a/src/NadekoBot/Modules/Nsfw/Common/SafebooruElement.cs b/src/NadekoBot/Modules/Nsfw/Common/SafebooruElement.cs index 98496a854..04cb60d72 100644 --- a/src/NadekoBot/Modules/Nsfw/Common/SafebooruElement.cs +++ b/src/NadekoBot/Modules/Nsfw/Common/SafebooruElement.cs @@ -1,15 +1,14 @@ -namespace NadekoBot.Modules.Nsfw.Common +namespace NadekoBot.Modules.Nsfw.Common; + +public class SafebooruElement : IImageData { - public class SafebooruElement : IImageData - { - public string Directory { get; set; } - public string Image { get; set; } + public string Directory { get; set; } + public string Image { get; set; } - public string FileUrl => $"https://safebooru.org/images/{Directory}/{Image}"; - public string Rating { get; set; } - public string Tags { get; set; } - public ImageData ToCachedImageData(Booru type) - => new ImageData(FileUrl, Booru.Safebooru, this.Tags.Split(' '), Rating); - } -} + public string FileUrl => $"https://safebooru.org/images/{Directory}/{Image}"; + public string Rating { get; set; } + public string Tags { get; set; } + public ImageData ToCachedImageData(Booru type) + => new ImageData(FileUrl, Booru.Safebooru, this.Tags.Split(' '), Rating); +} \ No newline at end of file diff --git a/src/NadekoBot/Modules/Nsfw/Common/SankakuImageObject.cs b/src/NadekoBot/Modules/Nsfw/Common/SankakuImageObject.cs index 978ebd473..084afd244 100644 --- a/src/NadekoBot/Modules/Nsfw/Common/SankakuImageObject.cs +++ b/src/NadekoBot/Modules/Nsfw/Common/SankakuImageObject.cs @@ -1,27 +1,25 @@ -using System.Linq; using System.Text.Json.Serialization; -namespace NadekoBot.Modules.Nsfw.Common -{ - public class SankakuImageObject : IImageData - { - public class Tag - { - public string Name { get; set; } - } - - [JsonPropertyName("file_url")] - public string FileUrl { get; set; } - - [JsonPropertyName("file_type")] - public string FileType { get; set; } - - public Tag[] Tags { get; set; } - - [JsonPropertyName("total_score")] - public int Score { get; set; } +namespace NadekoBot.Modules.Nsfw.Common; - public ImageData ToCachedImageData(Nsfw.Common.Booru type) - => new(FileUrl, Nsfw.Common.Booru.Sankaku, Tags.Select(x => x.Name).ToArray(), Score.ToString()); +public class SankakuImageObject : IImageData +{ + public class Tag + { + public string Name { get; set; } } + + [JsonPropertyName("file_url")] + public string FileUrl { get; set; } + + [JsonPropertyName("file_type")] + public string FileType { get; set; } + + public Tag[] Tags { get; set; } + + [JsonPropertyName("total_score")] + public int Score { get; set; } + + public ImageData ToCachedImageData(Nsfw.Common.Booru type) + => new(FileUrl, Nsfw.Common.Booru.Sankaku, Tags.Select(x => x.Name).ToArray(), Score.ToString()); } \ No newline at end of file diff --git a/src/NadekoBot/Modules/Nsfw/ISearchImagesService.cs b/src/NadekoBot/Modules/Nsfw/ISearchImagesService.cs index 55db22f15..904b66bc6 100644 --- a/src/NadekoBot/Modules/Nsfw/ISearchImagesService.cs +++ b/src/NadekoBot/Modules/Nsfw/ISearchImagesService.cs @@ -1,31 +1,29 @@ using System.Collections.Concurrent; using System.Threading; using System.Threading.Tasks; -using NadekoBot.Modules.Nsfw.Common; using NadekoBot.Modules.Searches.Common; -namespace NadekoBot.Modules.Nsfw +namespace NadekoBot.Modules.Nsfw; + +public interface ISearchImagesService { - public interface ISearchImagesService - { - Task Gelbooru(ulong? guildId, bool forceExplicit, string[] tags); - Task Danbooru(ulong? guildId, bool forceExplicit, string[] tags); - Task Konachan(ulong? guildId, bool forceExplicit, string[] tags); - Task Yandere(ulong? guildId, bool forceExplicit, string[] tags); - Task Rule34(ulong? guildId, bool forceExplicit, string[] tags); - Task E621(ulong? guildId, bool forceExplicit, string[] tags); - Task DerpiBooru(ulong? guildId, bool forceExplicit, string[] tags); - Task Sankaku(ulong? guildId, bool forceExplicit, string[] tags); - Task SafeBooru(ulong? guildId, bool forceExplicit, string[] tags); - Task Hentai(ulong? guildId, bool forceExplicit, string[] tags); - Task Boobs(); - ValueTask ToggleBlacklistTag(ulong guildId, string tag); - ValueTask GetBlacklistedTags(ulong guildId); - Task Butts(); - Task GetNhentaiByIdAsync(uint id); - Task GetNhentaiBySearchAsync(string search); - ConcurrentDictionary AutoHentaiTimers { get; } - ConcurrentDictionary AutoBoobTimers { get; } - ConcurrentDictionary AutoButtTimers { get; } - } + Task Gelbooru(ulong? guildId, bool forceExplicit, string[] tags); + Task Danbooru(ulong? guildId, bool forceExplicit, string[] tags); + Task Konachan(ulong? guildId, bool forceExplicit, string[] tags); + Task Yandere(ulong? guildId, bool forceExplicit, string[] tags); + Task Rule34(ulong? guildId, bool forceExplicit, string[] tags); + Task E621(ulong? guildId, bool forceExplicit, string[] tags); + Task DerpiBooru(ulong? guildId, bool forceExplicit, string[] tags); + Task Sankaku(ulong? guildId, bool forceExplicit, string[] tags); + Task SafeBooru(ulong? guildId, bool forceExplicit, string[] tags); + Task Hentai(ulong? guildId, bool forceExplicit, string[] tags); + Task Boobs(); + ValueTask ToggleBlacklistTag(ulong guildId, string tag); + ValueTask GetBlacklistedTags(ulong guildId); + Task Butts(); + Task GetNhentaiByIdAsync(uint id); + Task GetNhentaiBySearchAsync(string search); + ConcurrentDictionary AutoHentaiTimers { get; } + ConcurrentDictionary AutoBoobTimers { get; } + ConcurrentDictionary AutoButtTimers { get; } } \ No newline at end of file diff --git a/src/NadekoBot/Modules/Nsfw/Nsfw.cs b/src/NadekoBot/Modules/Nsfw/Nsfw.cs index 6a153839a..0bb4fad5c 100644 --- a/src/NadekoBot/Modules/Nsfw/Nsfw.cs +++ b/src/NadekoBot/Modules/Nsfw/Nsfw.cs @@ -6,422 +6,417 @@ using NadekoBot.Common.Collections; using NadekoBot.Extensions; using NadekoBot.Modules.Searches.Common; using Newtonsoft.Json.Linq; -using System; -using System.Linq; using System.Net.Http; using System.Threading; using System.Threading.Tasks; -using CommandLine; -using Serilog; -namespace NadekoBot.Modules.Nsfw +namespace NadekoBot.Modules.Nsfw; + +[NoPublicBot] +public class NSFW : NadekoModule { - [NoPublicBot] - public class NSFW : NadekoModule + private static readonly ConcurrentHashSet _hentaiBombBlacklist = new ConcurrentHashSet(); + private readonly IHttpClientFactory _httpFactory; + private readonly NadekoRandom _rng; + + public NSFW(IHttpClientFactory factory) { - private static readonly ConcurrentHashSet _hentaiBombBlacklist = new ConcurrentHashSet(); - private readonly IHttpClientFactory _httpFactory; - private readonly NadekoRandom _rng; + _httpFactory = factory; + _rng = new NadekoRandom(); + } - public NSFW(IHttpClientFactory factory) + private async Task InternalBoobs() + { + try { - _httpFactory = factory; - _rng = new NadekoRandom(); + JToken obj; + using (var http = _httpFactory.CreateClient()) + { + obj = JArray.Parse(await http + .GetStringAsync($"http://api.oboobs.ru/boobs/{new NadekoRandom().Next(0, 10330)}") + .ConfigureAwait(false))[0]; + } + + await ctx.Channel.SendMessageAsync($"http://media.oboobs.ru/{obj["preview"]}").ConfigureAwait(false); } - - private async Task InternalBoobs() + catch (Exception ex) { - try - { - JToken obj; - using (var http = _httpFactory.CreateClient()) - { - obj = JArray.Parse(await http - .GetStringAsync($"http://api.oboobs.ru/boobs/{new NadekoRandom().Next(0, 10330)}") - .ConfigureAwait(false))[0]; - } - - await ctx.Channel.SendMessageAsync($"http://media.oboobs.ru/{obj["preview"]}").ConfigureAwait(false); - } - catch (Exception ex) - { - await SendErrorAsync(ex.Message).ConfigureAwait(false); - } - } - - private async Task InternalButts(IMessageChannel Channel) - { - try - { - JToken obj; - using (var http = _httpFactory.CreateClient()) - { - obj = JArray.Parse(await http - .GetStringAsync($"http://api.obutts.ru/butts/{new NadekoRandom().Next(0, 4335)}") - .ConfigureAwait(false))[0]; - } - - await Channel.SendMessageAsync($"http://media.obutts.ru/{obj["preview"]}").ConfigureAwait(false); - } - catch (Exception ex) - { - await SendErrorAsync(ex.Message).ConfigureAwait(false); - } - } - - [NadekoCommand, Aliases] - [RequireNsfw] - [RequireContext(ContextType.Guild)] - [UserPerm(ChannelPerm.ManageMessages)] - public async Task AutoHentai(int interval = 0, [Leftover] string tags = null) - { - Timer t; - - if (interval == 0) - { - if (!_service.AutoHentaiTimers.TryRemove(ctx.Channel.Id, out t)) return; - - t.Change(Timeout.Infinite, Timeout.Infinite); //proper way to disable the timer - await ReplyConfirmLocalizedAsync(strs.stopped).ConfigureAwait(false); - return; - } - - if (interval < 20) - return; - - t = new Timer(async (state) => - { - try - { - if (tags is null || tags.Length == 0) - await InternalDapiCommand(null, true, _service.Hentai).ConfigureAwait(false); - else - { - var groups = tags.Split('|'); - var group = groups[_rng.Next(0, groups.Length)]; - await InternalDapiCommand(group.Split(' '), true, _service.Hentai).ConfigureAwait(false); - } - } - catch - { - // ignored - } - }, null, interval * 1000, interval * 1000); - - _service.AutoHentaiTimers.AddOrUpdate(ctx.Channel.Id, t, (key, old) => - { - old.Change(Timeout.Infinite, Timeout.Infinite); - return t; - }); - - await ReplyConfirmLocalizedAsync(strs.autohentai_started( - interval, - string.Join(", ", tags))); - } - - [NadekoCommand, Aliases] - [RequireNsfw] - [RequireContext(ContextType.Guild)] - [UserPerm(ChannelPerm.ManageMessages)] - public async Task AutoBoobs(int interval = 0) - { - Timer t; - - if (interval == 0) - { - if (!_service.AutoBoobTimers.TryRemove(ctx.Channel.Id, out t)) return; - - t.Change(Timeout.Infinite, Timeout.Infinite); - await ReplyConfirmLocalizedAsync(strs.stopped).ConfigureAwait(false); - return; - } - - if (interval < 20) - return; - - t = new Timer(async (state) => - { - try - { - await InternalBoobs().ConfigureAwait(false); - } - catch - { - // ignored - } - }, null, interval * 1000, interval * 1000); - - _service.AutoBoobTimers.AddOrUpdate(ctx.Channel.Id, t, (key, old) => - { - old.Change(Timeout.Infinite, Timeout.Infinite); - return t; - }); - - await ReplyConfirmLocalizedAsync(strs.started(interval)); - } - - [NadekoCommand, Aliases] - [RequireNsfw(Group = "nsfw_or_dm"), RequireContext(ContextType.DM, Group = "nsfw_or_dm")] - [UserPerm(ChannelPerm.ManageMessages)] - public async Task AutoButts(int interval = 0) - { - Timer t; - - if (interval == 0) - { - if (!_service.AutoButtTimers.TryRemove(ctx.Channel.Id, out t)) return; - - t.Change(Timeout.Infinite, Timeout.Infinite); //proper way to disable the timer - await ReplyConfirmLocalizedAsync(strs.stopped).ConfigureAwait(false); - return; - } - - if (interval < 20) - return; - - t = new Timer(async (state) => - { - try - { - await InternalButts(ctx.Channel).ConfigureAwait(false); - } - catch - { - // ignored - } - }, null, interval * 1000, interval * 1000); - - _service.AutoButtTimers.AddOrUpdate(ctx.Channel.Id, t, (key, old) => - { - old.Change(Timeout.Infinite, Timeout.Infinite); - return t; - }); - - await ReplyConfirmLocalizedAsync(strs.started(interval)); - } - - [NadekoCommand, Aliases] - [RequireNsfw(Group = "nsfw_or_dm"), RequireContext(ContextType.DM, Group = "nsfw_or_dm")] - public Task Hentai(params string[] tags) - => InternalDapiCommand(tags, true, _service.Hentai); - - [NadekoCommand, Aliases] - [RequireNsfw(Group = "nsfw_or_dm"), RequireContext(ContextType.DM, Group = "nsfw_or_dm")] - public async Task HentaiBomb(params string[] tags) - { - if (!_hentaiBombBlacklist.Add(ctx.Guild?.Id ?? ctx.User.Id)) - return; - try - { - var images = await Task.WhenAll(_service.Yandere(ctx.Guild?.Id, true, tags), - _service.Danbooru(ctx.Guild?.Id, true, tags), - _service.Konachan(ctx.Guild?.Id, true, tags), - _service.Gelbooru(ctx.Guild?.Id, true, tags)); - - var linksEnum = images?.Where(l => l != null).ToArray(); - if (images is null || !linksEnum.Any()) - { - await ReplyErrorLocalizedAsync(strs.no_results).ConfigureAwait(false); - return; - } - - await ctx.Channel.SendMessageAsync(string.Join("\n\n", linksEnum.Select(x => x.Url))) - .ConfigureAwait(false); - } - finally - { - _hentaiBombBlacklist.TryRemove(ctx.Guild?.Id ?? ctx.User.Id); - } - } - - [NadekoCommand, Aliases] - [RequireNsfw(Group = "nsfw_or_dm"), RequireContext(ContextType.DM, Group = "nsfw_or_dm")] - public Task Yandere(params string[] tags) - => InternalDapiCommand(tags, false, _service.Yandere); - - [NadekoCommand, Aliases] - [RequireNsfw(Group = "nsfw_or_dm"), RequireContext(ContextType.DM, Group = "nsfw_or_dm")] - public Task Konachan(params string[] tags) - => InternalDapiCommand(tags, false, _service.Konachan); - - [NadekoCommand, Aliases] - [RequireNsfw(Group = "nsfw_or_dm"), RequireContext(ContextType.DM, Group = "nsfw_or_dm")] - public Task Sankaku(params string[] tags) - => InternalDapiCommand(tags, false, _service.Sankaku); - - [NadekoCommand, Aliases] - [RequireNsfw(Group = "nsfw_or_dm"), RequireContext(ContextType.DM, Group = "nsfw_or_dm")] - public Task E621(params string[] tags) - => InternalDapiCommand(tags, false, _service.E621); - - [NadekoCommand, Aliases] - [RequireNsfw(Group = "nsfw_or_dm"), RequireContext(ContextType.DM, Group = "nsfw_or_dm")] - public Task Rule34(params string[] tags) - => InternalDapiCommand(tags, false, _service.Rule34); - - [NadekoCommand, Aliases] - [RequireNsfw(Group = "nsfw_or_dm"), RequireContext(ContextType.DM, Group = "nsfw_or_dm")] - public Task Danbooru(params string[] tags) - => InternalDapiCommand(tags, false, _service.Danbooru); - - [NadekoCommand, Aliases] - [RequireNsfw(Group = "nsfw_or_dm"), RequireContext(ContextType.DM, Group = "nsfw_or_dm")] - public Task Gelbooru(params string[] tags) - => InternalDapiCommand(tags, false, _service.Gelbooru); - - [NadekoCommand, Aliases] - [RequireNsfw(Group = "nsfw_or_dm"), RequireContext(ContextType.DM, Group = "nsfw_or_dm")] - public Task Derpibooru(params string[] tags) - => InternalDapiCommand(tags, false, _service.DerpiBooru); - - [NadekoCommand, Aliases] - [RequireNsfw(Group = "nsfw_or_dm"), RequireContext(ContextType.DM, Group = "nsfw_or_dm")] - public Task Safebooru(params string[] tags) - => InternalDapiCommand(tags, false, _service.SafeBooru); - - [NadekoCommand, Aliases] - [RequireNsfw(Group = "nsfw_or_dm"), RequireContext(ContextType.DM, Group = "nsfw_or_dm")] - public async Task Boobs() - { - try - { - JToken obj; - using (var http = _httpFactory.CreateClient()) - { - obj = JArray.Parse(await http - .GetStringAsync($"http://api.oboobs.ru/boobs/{new NadekoRandom().Next(0, 12000)}") - .ConfigureAwait(false))[0]; - } - - await ctx.Channel.SendMessageAsync($"http://media.oboobs.ru/{obj["preview"]}").ConfigureAwait(false); - } - catch (Exception ex) - { - await SendErrorAsync(ex.Message).ConfigureAwait(false); - } - } - - [NadekoCommand, Aliases] - [RequireNsfw(Group = "nsfw_or_dm"), RequireContext(ContextType.DM, Group = "nsfw_or_dm")] - public async Task Butts() - { - try - { - JToken obj; - using (var http = _httpFactory.CreateClient()) - { - obj = JArray.Parse(await http - .GetStringAsync($"http://api.obutts.ru/butts/{new NadekoRandom().Next(0, 6100)}") - .ConfigureAwait(false))[0]; - } - - await ctx.Channel.SendMessageAsync($"http://media.obutts.ru/{obj["preview"]}").ConfigureAwait(false); - } - catch (Exception ex) - { - await SendErrorAsync(ex.Message).ConfigureAwait(false); - } - } - - [NadekoCommand, Aliases] - [RequireContext(ContextType.Guild)] - [UserPerm(GuildPerm.ManageMessages)] - public async Task NsfwTagBlacklist([Leftover] string tag = null) - { - if (string.IsNullOrWhiteSpace(tag)) - { - var blTags = await _service.GetBlacklistedTags(ctx.Guild.Id); - await SendConfirmAsync(GetText(strs.blacklisted_tag_list), - blTags.Any() - ? string.Join(", ", blTags) - : "-").ConfigureAwait(false); - } - else - { - tag = tag.Trim().ToLowerInvariant(); - var added = await _service.ToggleBlacklistTag(ctx.Guild.Id, tag); - - if (added) - await ReplyPendingLocalizedAsync(strs.blacklisted_tag_add(tag)); - else - await ReplyPendingLocalizedAsync(strs.blacklisted_tag_remove(tag)); - } - } - - [NadekoCommand, Aliases] - [RequireContext(ContextType.Guild)] - [RequireNsfw(Group = "nsfw_or_dm"), RequireContext(ContextType.DM, Group = "nsfw_or_dm")] - [Priority(1)] - public async Task Nhentai(uint id) - { - var g = await _service.GetNhentaiByIdAsync(id); - - if (g is null) - { - await ReplyErrorLocalizedAsync(strs.not_found); - return; - } - - await SendNhentaiGalleryInternalAsync(g); - } - - [NadekoCommand, Aliases] - [RequireContext(ContextType.Guild)] - [RequireNsfw(Group = "nsfw_or_dm"), RequireContext(ContextType.DM, Group = "nsfw_or_dm")] - [Priority(0)] - public async Task Nhentai([Leftover] string query) - { - var g = await _service.GetNhentaiBySearchAsync(query); - - if (g is null) - { - await ReplyErrorLocalizedAsync(strs.not_found); - return; - } - - await SendNhentaiGalleryInternalAsync(g); - } - - private async Task SendNhentaiGalleryInternalAsync(Gallery g) - { - var count = 0; - var tagString = g.Tags - .Shuffle() - .Select(tag => $"[{tag.Name}]({tag.Url})") - .TakeWhile(tag => (count += tag.Length) < 1000) - .JoinWith(" "); - - var embed = _eb.Create() - .WithTitle(g.Title) - .WithDescription(g.FullTitle) - .WithImageUrl(g.Thumbnail) - .WithUrl(g.Url) - .AddField(GetText(strs.favorites), g.Likes, true) - .AddField(GetText(strs.pages), g.PageCount, true) - .AddField(GetText(strs.tags), tagString, true) - .WithFooter(g.UploadedAt.ToString("f")) - .WithOkColor(); - - await ctx.Channel.EmbedAsync(embed); - } - - private async Task InternalDapiCommand(string[] tags, - bool forceExplicit, - Func> func) - { - var data = await func(ctx.Guild?.Id, forceExplicit, tags); - - if (data is null || !string.IsNullOrWhiteSpace(data.Error)) - { - await ReplyErrorLocalizedAsync(strs.no_results); - return; - } - await ctx.Channel.EmbedAsync(_eb - .Create(ctx) - .WithOkColor() - .WithImageUrl(data.Url) - .WithDescription($"[link]({data.Url})") - .WithFooter($"{data.Rating} ({data.Provider}) | {string.Join(" | ", data.Tags.Where(x => !string.IsNullOrWhiteSpace(x)).Take(5))}")); + await SendErrorAsync(ex.Message).ConfigureAwait(false); } } + + private async Task InternalButts(IMessageChannel Channel) + { + try + { + JToken obj; + using (var http = _httpFactory.CreateClient()) + { + obj = JArray.Parse(await http + .GetStringAsync($"http://api.obutts.ru/butts/{new NadekoRandom().Next(0, 4335)}") + .ConfigureAwait(false))[0]; + } + + await Channel.SendMessageAsync($"http://media.obutts.ru/{obj["preview"]}").ConfigureAwait(false); + } + catch (Exception ex) + { + await SendErrorAsync(ex.Message).ConfigureAwait(false); + } + } + + [NadekoCommand, Aliases] + [RequireNsfw] + [RequireContext(ContextType.Guild)] + [UserPerm(ChannelPerm.ManageMessages)] + public async Task AutoHentai(int interval = 0, [Leftover] string tags = null) + { + Timer t; + + if (interval == 0) + { + if (!_service.AutoHentaiTimers.TryRemove(ctx.Channel.Id, out t)) return; + + t.Change(Timeout.Infinite, Timeout.Infinite); //proper way to disable the timer + await ReplyConfirmLocalizedAsync(strs.stopped).ConfigureAwait(false); + return; + } + + if (interval < 20) + return; + + t = new Timer(async (state) => + { + try + { + if (tags is null || tags.Length == 0) + await InternalDapiCommand(null, true, _service.Hentai).ConfigureAwait(false); + else + { + var groups = tags.Split('|'); + var group = groups[_rng.Next(0, groups.Length)]; + await InternalDapiCommand(group.Split(' '), true, _service.Hentai).ConfigureAwait(false); + } + } + catch + { + // ignored + } + }, null, interval * 1000, interval * 1000); + + _service.AutoHentaiTimers.AddOrUpdate(ctx.Channel.Id, t, (key, old) => + { + old.Change(Timeout.Infinite, Timeout.Infinite); + return t; + }); + + await ReplyConfirmLocalizedAsync(strs.autohentai_started( + interval, + string.Join(", ", tags))); + } + + [NadekoCommand, Aliases] + [RequireNsfw] + [RequireContext(ContextType.Guild)] + [UserPerm(ChannelPerm.ManageMessages)] + public async Task AutoBoobs(int interval = 0) + { + Timer t; + + if (interval == 0) + { + if (!_service.AutoBoobTimers.TryRemove(ctx.Channel.Id, out t)) return; + + t.Change(Timeout.Infinite, Timeout.Infinite); + await ReplyConfirmLocalizedAsync(strs.stopped).ConfigureAwait(false); + return; + } + + if (interval < 20) + return; + + t = new Timer(async (state) => + { + try + { + await InternalBoobs().ConfigureAwait(false); + } + catch + { + // ignored + } + }, null, interval * 1000, interval * 1000); + + _service.AutoBoobTimers.AddOrUpdate(ctx.Channel.Id, t, (key, old) => + { + old.Change(Timeout.Infinite, Timeout.Infinite); + return t; + }); + + await ReplyConfirmLocalizedAsync(strs.started(interval)); + } + + [NadekoCommand, Aliases] + [RequireNsfw(Group = "nsfw_or_dm"), RequireContext(ContextType.DM, Group = "nsfw_or_dm")] + [UserPerm(ChannelPerm.ManageMessages)] + public async Task AutoButts(int interval = 0) + { + Timer t; + + if (interval == 0) + { + if (!_service.AutoButtTimers.TryRemove(ctx.Channel.Id, out t)) return; + + t.Change(Timeout.Infinite, Timeout.Infinite); //proper way to disable the timer + await ReplyConfirmLocalizedAsync(strs.stopped).ConfigureAwait(false); + return; + } + + if (interval < 20) + return; + + t = new Timer(async (state) => + { + try + { + await InternalButts(ctx.Channel).ConfigureAwait(false); + } + catch + { + // ignored + } + }, null, interval * 1000, interval * 1000); + + _service.AutoButtTimers.AddOrUpdate(ctx.Channel.Id, t, (key, old) => + { + old.Change(Timeout.Infinite, Timeout.Infinite); + return t; + }); + + await ReplyConfirmLocalizedAsync(strs.started(interval)); + } + + [NadekoCommand, Aliases] + [RequireNsfw(Group = "nsfw_or_dm"), RequireContext(ContextType.DM, Group = "nsfw_or_dm")] + public Task Hentai(params string[] tags) + => InternalDapiCommand(tags, true, _service.Hentai); + + [NadekoCommand, Aliases] + [RequireNsfw(Group = "nsfw_or_dm"), RequireContext(ContextType.DM, Group = "nsfw_or_dm")] + public async Task HentaiBomb(params string[] tags) + { + if (!_hentaiBombBlacklist.Add(ctx.Guild?.Id ?? ctx.User.Id)) + return; + try + { + var images = await Task.WhenAll(_service.Yandere(ctx.Guild?.Id, true, tags), + _service.Danbooru(ctx.Guild?.Id, true, tags), + _service.Konachan(ctx.Guild?.Id, true, tags), + _service.Gelbooru(ctx.Guild?.Id, true, tags)); + + var linksEnum = images?.Where(l => l != null).ToArray(); + if (images is null || !linksEnum.Any()) + { + await ReplyErrorLocalizedAsync(strs.no_results).ConfigureAwait(false); + return; + } + + await ctx.Channel.SendMessageAsync(string.Join("\n\n", linksEnum.Select(x => x.Url))) + .ConfigureAwait(false); + } + finally + { + _hentaiBombBlacklist.TryRemove(ctx.Guild?.Id ?? ctx.User.Id); + } + } + + [NadekoCommand, Aliases] + [RequireNsfw(Group = "nsfw_or_dm"), RequireContext(ContextType.DM, Group = "nsfw_or_dm")] + public Task Yandere(params string[] tags) + => InternalDapiCommand(tags, false, _service.Yandere); + + [NadekoCommand, Aliases] + [RequireNsfw(Group = "nsfw_or_dm"), RequireContext(ContextType.DM, Group = "nsfw_or_dm")] + public Task Konachan(params string[] tags) + => InternalDapiCommand(tags, false, _service.Konachan); + + [NadekoCommand, Aliases] + [RequireNsfw(Group = "nsfw_or_dm"), RequireContext(ContextType.DM, Group = "nsfw_or_dm")] + public Task Sankaku(params string[] tags) + => InternalDapiCommand(tags, false, _service.Sankaku); + + [NadekoCommand, Aliases] + [RequireNsfw(Group = "nsfw_or_dm"), RequireContext(ContextType.DM, Group = "nsfw_or_dm")] + public Task E621(params string[] tags) + => InternalDapiCommand(tags, false, _service.E621); + + [NadekoCommand, Aliases] + [RequireNsfw(Group = "nsfw_or_dm"), RequireContext(ContextType.DM, Group = "nsfw_or_dm")] + public Task Rule34(params string[] tags) + => InternalDapiCommand(tags, false, _service.Rule34); + + [NadekoCommand, Aliases] + [RequireNsfw(Group = "nsfw_or_dm"), RequireContext(ContextType.DM, Group = "nsfw_or_dm")] + public Task Danbooru(params string[] tags) + => InternalDapiCommand(tags, false, _service.Danbooru); + + [NadekoCommand, Aliases] + [RequireNsfw(Group = "nsfw_or_dm"), RequireContext(ContextType.DM, Group = "nsfw_or_dm")] + public Task Gelbooru(params string[] tags) + => InternalDapiCommand(tags, false, _service.Gelbooru); + + [NadekoCommand, Aliases] + [RequireNsfw(Group = "nsfw_or_dm"), RequireContext(ContextType.DM, Group = "nsfw_or_dm")] + public Task Derpibooru(params string[] tags) + => InternalDapiCommand(tags, false, _service.DerpiBooru); + + [NadekoCommand, Aliases] + [RequireNsfw(Group = "nsfw_or_dm"), RequireContext(ContextType.DM, Group = "nsfw_or_dm")] + public Task Safebooru(params string[] tags) + => InternalDapiCommand(tags, false, _service.SafeBooru); + + [NadekoCommand, Aliases] + [RequireNsfw(Group = "nsfw_or_dm"), RequireContext(ContextType.DM, Group = "nsfw_or_dm")] + public async Task Boobs() + { + try + { + JToken obj; + using (var http = _httpFactory.CreateClient()) + { + obj = JArray.Parse(await http + .GetStringAsync($"http://api.oboobs.ru/boobs/{new NadekoRandom().Next(0, 12000)}") + .ConfigureAwait(false))[0]; + } + + await ctx.Channel.SendMessageAsync($"http://media.oboobs.ru/{obj["preview"]}").ConfigureAwait(false); + } + catch (Exception ex) + { + await SendErrorAsync(ex.Message).ConfigureAwait(false); + } + } + + [NadekoCommand, Aliases] + [RequireNsfw(Group = "nsfw_or_dm"), RequireContext(ContextType.DM, Group = "nsfw_or_dm")] + public async Task Butts() + { + try + { + JToken obj; + using (var http = _httpFactory.CreateClient()) + { + obj = JArray.Parse(await http + .GetStringAsync($"http://api.obutts.ru/butts/{new NadekoRandom().Next(0, 6100)}") + .ConfigureAwait(false))[0]; + } + + await ctx.Channel.SendMessageAsync($"http://media.obutts.ru/{obj["preview"]}").ConfigureAwait(false); + } + catch (Exception ex) + { + await SendErrorAsync(ex.Message).ConfigureAwait(false); + } + } + + [NadekoCommand, Aliases] + [RequireContext(ContextType.Guild)] + [UserPerm(GuildPerm.ManageMessages)] + public async Task NsfwTagBlacklist([Leftover] string tag = null) + { + if (string.IsNullOrWhiteSpace(tag)) + { + var blTags = await _service.GetBlacklistedTags(ctx.Guild.Id); + await SendConfirmAsync(GetText(strs.blacklisted_tag_list), + blTags.Any() + ? string.Join(", ", blTags) + : "-").ConfigureAwait(false); + } + else + { + tag = tag.Trim().ToLowerInvariant(); + var added = await _service.ToggleBlacklistTag(ctx.Guild.Id, tag); + + if (added) + await ReplyPendingLocalizedAsync(strs.blacklisted_tag_add(tag)); + else + await ReplyPendingLocalizedAsync(strs.blacklisted_tag_remove(tag)); + } + } + + [NadekoCommand, Aliases] + [RequireContext(ContextType.Guild)] + [RequireNsfw(Group = "nsfw_or_dm"), RequireContext(ContextType.DM, Group = "nsfw_or_dm")] + [Priority(1)] + public async Task Nhentai(uint id) + { + var g = await _service.GetNhentaiByIdAsync(id); + + if (g is null) + { + await ReplyErrorLocalizedAsync(strs.not_found); + return; + } + + await SendNhentaiGalleryInternalAsync(g); + } + + [NadekoCommand, Aliases] + [RequireContext(ContextType.Guild)] + [RequireNsfw(Group = "nsfw_or_dm"), RequireContext(ContextType.DM, Group = "nsfw_or_dm")] + [Priority(0)] + public async Task Nhentai([Leftover] string query) + { + var g = await _service.GetNhentaiBySearchAsync(query); + + if (g is null) + { + await ReplyErrorLocalizedAsync(strs.not_found); + return; + } + + await SendNhentaiGalleryInternalAsync(g); + } + + private async Task SendNhentaiGalleryInternalAsync(Gallery g) + { + var count = 0; + var tagString = g.Tags + .Shuffle() + .Select(tag => $"[{tag.Name}]({tag.Url})") + .TakeWhile(tag => (count += tag.Length) < 1000) + .JoinWith(" "); + + var embed = _eb.Create() + .WithTitle(g.Title) + .WithDescription(g.FullTitle) + .WithImageUrl(g.Thumbnail) + .WithUrl(g.Url) + .AddField(GetText(strs.favorites), g.Likes, true) + .AddField(GetText(strs.pages), g.PageCount, true) + .AddField(GetText(strs.tags), tagString, true) + .WithFooter(g.UploadedAt.ToString("f")) + .WithOkColor(); + + await ctx.Channel.EmbedAsync(embed); + } + + private async Task InternalDapiCommand(string[] tags, + bool forceExplicit, + Func> func) + { + var data = await func(ctx.Guild?.Id, forceExplicit, tags); + + if (data is null || !string.IsNullOrWhiteSpace(data.Error)) + { + await ReplyErrorLocalizedAsync(strs.no_results); + return; + } + await ctx.Channel.EmbedAsync(_eb + .Create(ctx) + .WithOkColor() + .WithImageUrl(data.Url) + .WithDescription($"[link]({data.Url})") + .WithFooter($"{data.Rating} ({data.Provider}) | {string.Join(" | ", data.Tags.Where(x => !string.IsNullOrWhiteSpace(x)).Take(5))}")); + } } \ No newline at end of file diff --git a/src/NadekoBot/Modules/Nsfw/NsfwService.cs b/src/NadekoBot/Modules/Nsfw/NsfwService.cs index 1f5bc2d53..797ddda1a 100644 --- a/src/NadekoBot/Modules/Nsfw/NsfwService.cs +++ b/src/NadekoBot/Modules/Nsfw/NsfwService.cs @@ -1,12 +1,11 @@ -namespace NadekoBot.Modules.Nsfw +namespace NadekoBot.Modules.Nsfw; + +public interface INsfwService { - public interface INsfwService - { - } +} - public class NsfwService - { +public class NsfwService +{ - } } \ No newline at end of file diff --git a/src/NadekoBot/Modules/Nsfw/SearchImageCacher.cs b/src/NadekoBot/Modules/Nsfw/SearchImageCacher.cs index df8011c66..69f47547e 100644 --- a/src/NadekoBot/Modules/Nsfw/SearchImageCacher.cs +++ b/src/NadekoBot/Modules/Nsfw/SearchImageCacher.cs @@ -1,8 +1,4 @@ -using Serilog; -using System; -using System.Collections.Concurrent; -using System.Collections.Generic; -using System.Linq; +using System.Collections.Concurrent; using System.Net.Http; using System.Threading; using System.Threading.Tasks; @@ -11,318 +7,317 @@ using NadekoBot.Common; using NadekoBot.Extensions; using NadekoBot.Services; -namespace NadekoBot.Modules.Nsfw.Common +namespace NadekoBot.Modules.Nsfw.Common; + +public class SearchImageCacher : INService { - public class SearchImageCacher : INService + private readonly IHttpClientFactory _httpFactory; + private readonly SemaphoreSlim _lock = new SemaphoreSlim(1, 1); + private readonly Random _rng; + + private static readonly ISet defaultTagBlacklist = new HashSet() { - private readonly IHttpClientFactory _httpFactory; - private readonly SemaphoreSlim _lock = new SemaphoreSlim(1, 1); - private readonly Random _rng; + "loli", + "lolicon", + "shota", + "shotacon", + "cub" + }; - private static readonly ISet defaultTagBlacklist = new HashSet() + private readonly Dictionary _typeLocks = new(); + private readonly Dictionary> _usedTags = new(); + private readonly IMemoryCache _cache; + + public SearchImageCacher(IHttpClientFactory httpFactory, IMemoryCache cache) + { + _httpFactory = httpFactory; + _rng = new NadekoRandom(); + _cache = cache; + + // initialize new cache with empty values + foreach (var type in Enum.GetValues()) { - "loli", - "lolicon", - "shota", - "shotacon", - "cub" - }; - - private readonly Dictionary _typeLocks = new(); - private readonly Dictionary> _usedTags = new(); - private readonly IMemoryCache _cache; - - public SearchImageCacher(IHttpClientFactory httpFactory, IMemoryCache cache) - { - _httpFactory = httpFactory; - _rng = new NadekoRandom(); - _cache = cache; - - // initialize new cache with empty values - foreach (var type in Enum.GetValues()) - { - _typeLocks[type] = new(); - _usedTags[type] = new(); - } - } - - private string Key(Booru boory, string tag) - => $"booru:{boory}__tag:{tag}"; - - /// - /// Download images of the specified type, and cache them. - /// - /// Required tags - /// Whether images will be forced to be explicit - /// Provider type - /// Cancellation token - /// Whether any image is found. - private async Task UpdateImagesInternalAsync(string[] tags, bool forceExplicit, Booru type, CancellationToken cancel) - { - var images = await DownloadImagesAsync(tags, forceExplicit, type, cancel).ConfigureAwait(false); - if (images is null || images.Count == 0) - { - // Log.Warning("Got no images for {0}, tags: {1}", type, string.Join(", ", tags)); - return false; - } - - Log.Information("Updating {0}...", type); - lock (_typeLocks[type]) - { - var typeUsedTags = _usedTags[type]; - foreach (var tag in tags) - typeUsedTags.Add(tag); - - // if user uses no tags for the hentai command and there are no used - // tags atm, just select 50 random tags from downloaded images to seed - if (typeUsedTags.Count == 0) - images.SelectMany(x => x.Tags) - .Distinct() - .Shuffle() - .Take(50) - .ForEach(x => typeUsedTags.Add(x)); - - foreach (var img in images) - { - // if any of the tags is a tag banned by discord - // do not put that image in the cache - if (defaultTagBlacklist.Overlaps(img.Tags)) - continue; - - // if image doesn't have a proper absolute uri, skip it - if (!Uri.IsWellFormedUriString(img.FileUrl, UriKind.Absolute)) - continue; - - // i'm appending current tags because of tag aliasing - // this way, if user uses tag alias, for example 'kissing' - - // both 'kiss' (real tag returned by the image) and 'kissing' will be populated with - // retreived images - foreach (var tag in img.Tags.Concat(tags).Distinct()) - { - if (typeUsedTags.Contains(tag)) - { - var set = _cache.GetOrCreate>(Key(type, tag), e => - { - e.AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(30); - return new(); - }); - - if(set.Count < 100) - set.Add(img); - } - } - } - } - - return true; - } - - private ImageData QueryLocal(string[] tags, bool forceExplicit, Booru type, HashSet blacklistedTags) - { - var setList = new List>(); - - // ofc make sure no changes are happening while we're getting a random one - lock (_typeLocks[type]) - { - // if no tags are provided, get a random tag - if (tags.Length == 0) - { - // get all tags in the cache - if (_usedTags.TryGetValue(type, out var allTags) - && allTags.Count > 0) - { - tags = new[] {allTags.ToList()[_rng.Next(0, allTags.Count)]}; - } - else - { - return null; - } - } - - foreach (var tag in tags) - { - // if any tag is missing from cache, that means there is no result - if (_cache.TryGetValue>(Key(type, tag), out var set)) - setList.Add(set); - else - return null; - } - - if (setList.Count == 0) - return null; - - - List resultList; - // if multiple tags, we need to interesect sets - if (setList.Count > 1) - { - // now that we have sets, interesect them to find eligible items - // make a copy of the 1st set - var resultSet = new HashSet(setList[0]); - - // go through all other sets, and - for (var i = 1; i < setList.Count; ++i) - { - // if any of the elements in result set are not present in the current set - // remove it from the result set - resultSet.IntersectWith(setList[i]); - } - - resultList = resultSet.ToList(); - - } - else - { - // if only one tag, use that set - resultList = setList[0].ToList(); - } - - // return a random one which doesn't have blacklisted tags in it - resultList = resultList.Where(x => !blacklistedTags.Overlaps(x.Tags)).ToList(); - - // if no items in the set -> not found - if (resultList.Count == 0) - return null; - - var toReturn = resultList[_rng.Next(0, resultList.Count)]; - - // remove from cache - foreach (var tag in tags) - { - if (_cache.TryGetValue>(Key(type, tag), out var items)) - { - items.Remove(toReturn); - } - } - return toReturn; - } - } - - public async Task GetImageNew(string[] tags, bool forceExplicit, Booru type, - HashSet blacklistedTags, CancellationToken cancel) - { - // make sure tags are proper - tags = tags - .Where(x => x is not null) - .Select(tag => tag.ToLowerInvariant().Trim()) - .Distinct() - .ToArray(); - - if (tags.Length > 2 && type == Booru.Danbooru) - tags = tags[..2]; - - // use both tags banned by discord and tags banned on the server - if (blacklistedTags.Overlaps(tags) || defaultTagBlacklist.Overlaps(tags)) - return default; - - // query for an image - var image = QueryLocal(tags, forceExplicit, type, blacklistedTags); - if (image is not null) - return image; - - var success = false; - try - { - // if image is not found, update the cache and query again - success = await UpdateImagesInternalAsync(tags, forceExplicit, type, cancel).ConfigureAwait(false); - } - catch (HttpRequestException) - { - return default; - } - - if (!success) - return default; - image = QueryLocal(tags, forceExplicit, type, blacklistedTags); - - return image; - } - - private readonly ConcurrentDictionary<(Booru, string), int> maxPages = new(); - - public async Task> DownloadImagesAsync(string[] tags, bool isExplicit, Booru type, CancellationToken cancel) - { - var tagStr = string.Join(' ', tags.OrderByDescending(x => x)); - var page = 0; - - var attempt = 0; - while (attempt++ <= 10) - { - if (maxPages.TryGetValue((type, tagStr), out var maxPage)) - { - if (maxPage == 0) - { - Log.Information("Tag {0} yields no result on {1}, skipping.", tagStr, type); - return new(); - } - - page = _rng.Next(0, maxPage); - } - else - { - page = _rng.Next(0, 11); - } - - var result = await DownloadImagesAsync(tags, isExplicit, type, page, cancel).ConfigureAwait(false); - - if (result is null or { Count: 0 }) - { - Log.Information("Tag {0}, page {1} has no result on {2}.", string.Join(", ", tags), page, type.ToString()); - continue; - } - - return result; - } - - return new(); - } - - private IImageDownloader GetImageDownloader(Booru booru, HttpClient http) - => booru switch - { - Booru.Danbooru => new DanbooruImageDownloader(http), - Booru.Yandere => new YandereImageDownloader(http), - Booru.Konachan => new KonachanImageDownloader(http), - Booru.Safebooru => new SafebooruImageDownloader(http), - Booru.E621 => new E621ImageDownloader(http), - Booru.Derpibooru => new DerpibooruImageDownloader(http), - Booru.Gelbooru => new GelbooruImageDownloader(http), - Booru.Rule34 => new Rule34ImageDownloader(http), - Booru.Sankaku => new SankakuImageDownloader(http), - _ => throw new NotImplementedException($"{booru} downloader not implemented.") - }; - - private async Task> DownloadImagesAsync(string[] tags, bool isExplicit, Booru type, int page, CancellationToken cancel) - { - try - { - Log.Information("Downloading from {0} (page {1})...", type, page); - - using var http = _httpFactory.CreateClient(); - var downloader = GetImageDownloader(type, http); - - var images = await downloader.DownloadImageDataAsync(tags, page, isExplicit, cancel); - if (images.Count == 0) - { - var tagStr = string.Join(' ', tags.OrderByDescending(x => x)); - maxPages[(type, tagStr)] = page; - } - - return images; - } - catch (OperationCanceledException) - { - throw; - } - catch (Exception ex) - { - Log.Error(ex, "Error downloading an image:\nTags: {0}\nType: {1}\nPage: {2}\nMessage: {3}", - string.Join(", ", tags), - type, - page, - ex.Message); - return new(); - } + _typeLocks[type] = new(); + _usedTags[type] = new(); } } -} + + private string Key(Booru boory, string tag) + => $"booru:{boory}__tag:{tag}"; + + /// + /// Download images of the specified type, and cache them. + /// + /// Required tags + /// Whether images will be forced to be explicit + /// Provider type + /// Cancellation token + /// Whether any image is found. + private async Task UpdateImagesInternalAsync(string[] tags, bool forceExplicit, Booru type, CancellationToken cancel) + { + var images = await DownloadImagesAsync(tags, forceExplicit, type, cancel).ConfigureAwait(false); + if (images is null || images.Count == 0) + { + // Log.Warning("Got no images for {0}, tags: {1}", type, string.Join(", ", tags)); + return false; + } + + Log.Information("Updating {0}...", type); + lock (_typeLocks[type]) + { + var typeUsedTags = _usedTags[type]; + foreach (var tag in tags) + typeUsedTags.Add(tag); + + // if user uses no tags for the hentai command and there are no used + // tags atm, just select 50 random tags from downloaded images to seed + if (typeUsedTags.Count == 0) + images.SelectMany(x => x.Tags) + .Distinct() + .Shuffle() + .Take(50) + .ForEach(x => typeUsedTags.Add(x)); + + foreach (var img in images) + { + // if any of the tags is a tag banned by discord + // do not put that image in the cache + if (defaultTagBlacklist.Overlaps(img.Tags)) + continue; + + // if image doesn't have a proper absolute uri, skip it + if (!Uri.IsWellFormedUriString(img.FileUrl, UriKind.Absolute)) + continue; + + // i'm appending current tags because of tag aliasing + // this way, if user uses tag alias, for example 'kissing' - + // both 'kiss' (real tag returned by the image) and 'kissing' will be populated with + // retreived images + foreach (var tag in img.Tags.Concat(tags).Distinct()) + { + if (typeUsedTags.Contains(tag)) + { + var set = _cache.GetOrCreate>(Key(type, tag), e => + { + e.AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(30); + return new(); + }); + + if(set.Count < 100) + set.Add(img); + } + } + } + } + + return true; + } + + private ImageData QueryLocal(string[] tags, bool forceExplicit, Booru type, HashSet blacklistedTags) + { + var setList = new List>(); + + // ofc make sure no changes are happening while we're getting a random one + lock (_typeLocks[type]) + { + // if no tags are provided, get a random tag + if (tags.Length == 0) + { + // get all tags in the cache + if (_usedTags.TryGetValue(type, out var allTags) + && allTags.Count > 0) + { + tags = new[] {allTags.ToList()[_rng.Next(0, allTags.Count)]}; + } + else + { + return null; + } + } + + foreach (var tag in tags) + { + // if any tag is missing from cache, that means there is no result + if (_cache.TryGetValue>(Key(type, tag), out var set)) + setList.Add(set); + else + return null; + } + + if (setList.Count == 0) + return null; + + + List resultList; + // if multiple tags, we need to interesect sets + if (setList.Count > 1) + { + // now that we have sets, interesect them to find eligible items + // make a copy of the 1st set + var resultSet = new HashSet(setList[0]); + + // go through all other sets, and + for (var i = 1; i < setList.Count; ++i) + { + // if any of the elements in result set are not present in the current set + // remove it from the result set + resultSet.IntersectWith(setList[i]); + } + + resultList = resultSet.ToList(); + + } + else + { + // if only one tag, use that set + resultList = setList[0].ToList(); + } + + // return a random one which doesn't have blacklisted tags in it + resultList = resultList.Where(x => !blacklistedTags.Overlaps(x.Tags)).ToList(); + + // if no items in the set -> not found + if (resultList.Count == 0) + return null; + + var toReturn = resultList[_rng.Next(0, resultList.Count)]; + + // remove from cache + foreach (var tag in tags) + { + if (_cache.TryGetValue>(Key(type, tag), out var items)) + { + items.Remove(toReturn); + } + } + return toReturn; + } + } + + public async Task GetImageNew(string[] tags, bool forceExplicit, Booru type, + HashSet blacklistedTags, CancellationToken cancel) + { + // make sure tags are proper + tags = tags + .Where(x => x is not null) + .Select(tag => tag.ToLowerInvariant().Trim()) + .Distinct() + .ToArray(); + + if (tags.Length > 2 && type == Booru.Danbooru) + tags = tags[..2]; + + // use both tags banned by discord and tags banned on the server + if (blacklistedTags.Overlaps(tags) || defaultTagBlacklist.Overlaps(tags)) + return default; + + // query for an image + var image = QueryLocal(tags, forceExplicit, type, blacklistedTags); + if (image is not null) + return image; + + var success = false; + try + { + // if image is not found, update the cache and query again + success = await UpdateImagesInternalAsync(tags, forceExplicit, type, cancel).ConfigureAwait(false); + } + catch (HttpRequestException) + { + return default; + } + + if (!success) + return default; + image = QueryLocal(tags, forceExplicit, type, blacklistedTags); + + return image; + } + + private readonly ConcurrentDictionary<(Booru, string), int> maxPages = new(); + + public async Task> DownloadImagesAsync(string[] tags, bool isExplicit, Booru type, CancellationToken cancel) + { + var tagStr = string.Join(' ', tags.OrderByDescending(x => x)); + var page = 0; + + var attempt = 0; + while (attempt++ <= 10) + { + if (maxPages.TryGetValue((type, tagStr), out var maxPage)) + { + if (maxPage == 0) + { + Log.Information("Tag {0} yields no result on {1}, skipping.", tagStr, type); + return new(); + } + + page = _rng.Next(0, maxPage); + } + else + { + page = _rng.Next(0, 11); + } + + var result = await DownloadImagesAsync(tags, isExplicit, type, page, cancel).ConfigureAwait(false); + + if (result is null or { Count: 0 }) + { + Log.Information("Tag {0}, page {1} has no result on {2}.", string.Join(", ", tags), page, type.ToString()); + continue; + } + + return result; + } + + return new(); + } + + private IImageDownloader GetImageDownloader(Booru booru, HttpClient http) + => booru switch + { + Booru.Danbooru => new DanbooruImageDownloader(http), + Booru.Yandere => new YandereImageDownloader(http), + Booru.Konachan => new KonachanImageDownloader(http), + Booru.Safebooru => new SafebooruImageDownloader(http), + Booru.E621 => new E621ImageDownloader(http), + Booru.Derpibooru => new DerpibooruImageDownloader(http), + Booru.Gelbooru => new GelbooruImageDownloader(http), + Booru.Rule34 => new Rule34ImageDownloader(http), + Booru.Sankaku => new SankakuImageDownloader(http), + _ => throw new NotImplementedException($"{booru} downloader not implemented.") + }; + + private async Task> DownloadImagesAsync(string[] tags, bool isExplicit, Booru type, int page, CancellationToken cancel) + { + try + { + Log.Information("Downloading from {0} (page {1})...", type, page); + + using var http = _httpFactory.CreateClient(); + var downloader = GetImageDownloader(type, http); + + var images = await downloader.DownloadImageDataAsync(tags, page, isExplicit, cancel); + if (images.Count == 0) + { + var tagStr = string.Join(' ', tags.OrderByDescending(x => x)); + maxPages[(type, tagStr)] = page; + } + + return images; + } + catch (OperationCanceledException) + { + throw; + } + catch (Exception ex) + { + Log.Error(ex, "Error downloading an image:\nTags: {0}\nType: {1}\nPage: {2}\nMessage: {3}", + string.Join(", ", tags), + type, + page, + ex.Message); + return new(); + } + } +} \ No newline at end of file diff --git a/src/NadekoBot/Modules/Nsfw/SearchImagesService.cs b/src/NadekoBot/Modules/Nsfw/SearchImagesService.cs index 08e973cc0..381fcba0c 100644 --- a/src/NadekoBot/Modules/Nsfw/SearchImagesService.cs +++ b/src/NadekoBot/Modules/Nsfw/SearchImagesService.cs @@ -1,9 +1,5 @@ using Newtonsoft.Json.Linq; -using Serilog; -using System; using System.Collections.Concurrent; -using System.Collections.Generic; -using System.Linq; using System.Net.Http; using System.Threading.Tasks; using System.Threading; @@ -17,377 +13,376 @@ using Newtonsoft.Json; using Booru = NadekoBot.Modules.Nsfw.Common.Booru; using SearchImageCacher = NadekoBot.Modules.Nsfw.Common.SearchImageCacher; -namespace NadekoBot.Modules.Nsfw +namespace NadekoBot.Modules.Nsfw; + +public record TagRequest(ulong GuildId, bool ForceExplicit, Booru SearchType, params string[] Tags); +public record UrlReply { - public record TagRequest(ulong GuildId, bool ForceExplicit, Booru SearchType, params string[] Tags); - public record UrlReply + public string Error { get; init; } + public string Url { get; init; } + public string Rating { get; init; } + public string Provider { get; init; } + public List Tags { get; } = new List(); +} + +public class SearchImagesService : ISearchImagesService, INService +{ + private readonly Random _rng; + private readonly HttpClient _http; + private readonly SearchImageCacher _cache; + private readonly IHttpClientFactory _httpFactory; + private readonly DbService _db; + private ConcurrentDictionary> BlacklistedTags { get; } + + public ConcurrentDictionary AutoHentaiTimers { get; } = new ConcurrentDictionary(); + public ConcurrentDictionary AutoBoobTimers { get; } = new ConcurrentDictionary(); + public ConcurrentDictionary AutoButtTimers { get; } = new ConcurrentDictionary(); + + public SearchImagesService(DbService db, + IHttpClientFactory http, + SearchImageCacher cacher, + IHttpClientFactory httpFactory) { - public string Error { get; init; } - public string Url { get; init; } - public string Rating { get; init; } - public string Provider { get; init; } - public List Tags { get; } = new List(); + _db = db; + _rng = new NadekoRandom(); + _http = http.CreateClient(); + _http.AddFakeHeaders(); + _cache = cacher; + _httpFactory = httpFactory; + + using var uow = db.GetDbContext(); + BlacklistedTags = new( + uow.NsfwBlacklistedTags + .AsEnumerable() + .GroupBy(x => x.GuildId) + .ToDictionary( + x => x.Key, + x => new HashSet(x.Select(x => x.Tag)))); } - public class SearchImagesService : ISearchImagesService, INService + private Task GetNsfwImageAsync(ulong? guildId, bool forceExplicit, string[] tags, Booru dapi, CancellationToken cancel = default) { - private readonly Random _rng; - private readonly HttpClient _http; - private readonly SearchImageCacher _cache; - private readonly IHttpClientFactory _httpFactory; - private readonly DbService _db; - private ConcurrentDictionary> BlacklistedTags { get; } + return GetNsfwImageAsync(guildId ?? 0, tags ?? Array.Empty(), forceExplicit, dapi, cancel); + } - public ConcurrentDictionary AutoHentaiTimers { get; } = new ConcurrentDictionary(); - public ConcurrentDictionary AutoBoobTimers { get; } = new ConcurrentDictionary(); - public ConcurrentDictionary AutoButtTimers { get; } = new ConcurrentDictionary(); + private bool IsValidTag(string tag) => tag.All(x => x != '+' && x != '?' && x != '/'); // tags mustn't contain + or ? or / - public SearchImagesService(DbService db, - IHttpClientFactory http, - SearchImageCacher cacher, - IHttpClientFactory httpFactory) + private async Task GetNsfwImageAsync( + ulong guildId, + string[] tags, + bool forceExplicit, + Booru dapi, + CancellationToken cancel) + { + if (!tags.All(x => IsValidTag(x))) { - _db = db; - _rng = new NadekoRandom(); - _http = http.CreateClient(); - _http.AddFakeHeaders(); - _cache = cacher; - _httpFactory = httpFactory; - - using var uow = db.GetDbContext(); - BlacklistedTags = new( - uow.NsfwBlacklistedTags - .AsEnumerable() - .GroupBy(x => x.GuildId) - .ToDictionary( - x => x.Key, - x => new HashSet(x.Select(x => x.Tag)))); + return new UrlReply + { + Error = "One or more tags are invalid.", + Url = "" + }; } - private Task GetNsfwImageAsync(ulong? guildId, bool forceExplicit, string[] tags, Booru dapi, CancellationToken cancel = default) + Log.Information("Getting {V} image for Guild: {GuildId}...", dapi.ToString(), guildId); + try { - return GetNsfwImageAsync(guildId ?? 0, tags ?? Array.Empty(), forceExplicit, dapi, cancel); - } + BlacklistedTags.TryGetValue(guildId, out var blTags); - private bool IsValidTag(string tag) => tag.All(x => x != '+' && x != '?' && x != '/'); // tags mustn't contain + or ? or / + if (dapi == Booru.E621) { + for (var i = 0; i < tags.Length; ++i) + if (tags[i] == "yuri") + tags[i] = "female/female"; + } - private async Task GetNsfwImageAsync( - ulong guildId, - string[] tags, - bool forceExplicit, - Booru dapi, - CancellationToken cancel) - { - if (!tags.All(x => IsValidTag(x))) + if (dapi == Booru.Derpibooru) + { + for (var i = 0; i < tags.Length; ++i) + if (tags[i] == "yuri") + tags[i] = "lesbian"; + } + + var result = await _cache.GetImageNew(tags, forceExplicit, dapi, blTags ?? new HashSet(), cancel) + .ConfigureAwait(false); + + if (result is null) { return new UrlReply { - Error = "One or more tags are invalid.", + Error = "Image not found.", Url = "" }; } - Log.Information("Getting {V} image for Guild: {GuildId}...", dapi.ToString(), guildId); - try + var reply = new UrlReply { - BlacklistedTags.TryGetValue(guildId, out var blTags); + Error = "", + Url = result.FileUrl, + Rating = result.Rating, + Provider = result.SearchType.ToString() + }; - if (dapi == Booru.E621) { - for (var i = 0; i < tags.Length; ++i) - if (tags[i] == "yuri") - tags[i] = "female/female"; - } + reply.Tags.AddRange(result.Tags); - if (dapi == Booru.Derpibooru) - { - for (var i = 0; i < tags.Length; ++i) - if (tags[i] == "yuri") - tags[i] = "lesbian"; - } + return reply; - var result = await _cache.GetImageNew(tags, forceExplicit, dapi, blTags ?? new HashSet(), cancel) - .ConfigureAwait(false); - - if (result is null) - { - return new UrlReply - { - Error = "Image not found.", - Url = "" - }; - } - - var reply = new UrlReply - { - Error = "", - Url = result.FileUrl, - Rating = result.Rating, - Provider = result.SearchType.ToString() - }; - - reply.Tags.AddRange(result.Tags); - - return reply; - - } - catch (Exception ex) - { - Log.Error(ex, "Failed getting {Dapi} image: {Message}", dapi, ex.Message); - return new UrlReply - { - Error = ex.Message, - Url = "" - }; - } } + catch (Exception ex) + { + Log.Error(ex, "Failed getting {Dapi} image: {Message}", dapi, ex.Message); + return new UrlReply + { + Error = ex.Message, + Url = "" + }; + } + } - public Task Gelbooru(ulong? guildId, bool forceExplicit, string[] tags) - => GetNsfwImageAsync(guildId, forceExplicit, tags, Booru.Gelbooru); + public Task Gelbooru(ulong? guildId, bool forceExplicit, string[] tags) + => GetNsfwImageAsync(guildId, forceExplicit, tags, Booru.Gelbooru); - public Task Danbooru(ulong? guildId, bool forceExplicit, string[] tags) - => GetNsfwImageAsync(guildId, forceExplicit, tags, Booru.Danbooru); + public Task Danbooru(ulong? guildId, bool forceExplicit, string[] tags) + => GetNsfwImageAsync(guildId, forceExplicit, tags, Booru.Danbooru); - public Task Konachan(ulong? guildId, bool forceExplicit, string[] tags) - => GetNsfwImageAsync(guildId, forceExplicit, tags, Booru.Konachan); + public Task Konachan(ulong? guildId, bool forceExplicit, string[] tags) + => GetNsfwImageAsync(guildId, forceExplicit, tags, Booru.Konachan); - public Task Yandere(ulong? guildId, bool forceExplicit, string[] tags) - => GetNsfwImageAsync(guildId, forceExplicit, tags, Booru.Yandere); + public Task Yandere(ulong? guildId, bool forceExplicit, string[] tags) + => GetNsfwImageAsync(guildId, forceExplicit, tags, Booru.Yandere); - public Task Rule34(ulong? guildId, bool forceExplicit, string[] tags) - => GetNsfwImageAsync(guildId, forceExplicit, tags, Booru.Rule34); + public Task Rule34(ulong? guildId, bool forceExplicit, string[] tags) + => GetNsfwImageAsync(guildId, forceExplicit, tags, Booru.Rule34); - public Task E621(ulong? guildId, bool forceExplicit, string[] tags) - => GetNsfwImageAsync(guildId, forceExplicit, tags, Booru.E621); + public Task E621(ulong? guildId, bool forceExplicit, string[] tags) + => GetNsfwImageAsync(guildId, forceExplicit, tags, Booru.E621); - public Task DerpiBooru(ulong? guildId, bool forceExplicit, string[] tags) - => GetNsfwImageAsync(guildId, forceExplicit, tags, Booru.Derpibooru); + public Task DerpiBooru(ulong? guildId, bool forceExplicit, string[] tags) + => GetNsfwImageAsync(guildId, forceExplicit, tags, Booru.Derpibooru); - public Task SafeBooru(ulong? guildId, bool forceExplicit, string[] tags) - => GetNsfwImageAsync(guildId, forceExplicit, tags, Booru.Safebooru); + public Task SafeBooru(ulong? guildId, bool forceExplicit, string[] tags) + => GetNsfwImageAsync(guildId, forceExplicit, tags, Booru.Safebooru); - public Task Sankaku(ulong? guildId, bool forceExplicit, string[] tags) - => GetNsfwImageAsync(guildId, forceExplicit, tags, Booru.Sankaku); + public Task Sankaku(ulong? guildId, bool forceExplicit, string[] tags) + => GetNsfwImageAsync(guildId, forceExplicit, tags, Booru.Sankaku); - public async Task Hentai(ulong? guildId, bool forceExplicit, string[] tags) + public async Task Hentai(ulong? guildId, bool forceExplicit, string[] tags) + { + var providers = new[] { + Booru.Danbooru, + Booru.Konachan, + Booru.Gelbooru, + Booru.Yandere + }; + + using var cancelSource = new CancellationTokenSource(); + + // create a task for each type + var tasks = providers.Select(type => GetNsfwImageAsync(guildId, forceExplicit, tags, type)).ToList(); + do { - var providers = new[] { - Booru.Danbooru, - Booru.Konachan, - Booru.Gelbooru, - Booru.Yandere - }; + // wait for any of the tasks to complete + var task = await Task.WhenAny(tasks); - using var cancelSource = new CancellationTokenSource(); - - // create a task for each type - var tasks = providers.Select(type => GetNsfwImageAsync(guildId, forceExplicit, tags, type)).ToList(); - do + // get its result + var result = task.GetAwaiter().GetResult(); + if(result.Error == "") { - // wait for any of the tasks to complete - var task = await Task.WhenAny(tasks); - - // get its result - var result = task.GetAwaiter().GetResult(); - if(result.Error == "") - { - // if we have a non-error result, cancel other searches and return the result - cancelSource.Cancel(); - return result; - } - - // if the result is an error, remove that task from the waiting list, - // and wait for another task to complete - tasks.Remove(task); + // if we have a non-error result, cancel other searches and return the result + cancelSource.Cancel(); + return result; } - while (tasks.Count > 0); // keep looping as long as there is any task remaining to be attempted - // if we ran out of tasks, that means all tasks failed - return an error - return new UrlReply() + // if the result is an error, remove that task from the waiting list, + // and wait for another task to complete + tasks.Remove(task); + } + while (tasks.Count > 0); // keep looping as long as there is any task remaining to be attempted + + // if we ran out of tasks, that means all tasks failed - return an error + return new UrlReply() + { + Error = "No hentai image found." + }; + } + + public async Task Boobs() + { + try + { + JToken obj; + obj = JArray.Parse(await _http.GetStringAsync($"http://api.oboobs.ru/boobs/{_rng.Next(0, 12000)}").ConfigureAwait(false))[0]; + return new UrlReply { - Error = "No hentai image found." + Error = "", + Url = $"http://media.oboobs.ru/{obj["preview"]}", }; } - - public async Task Boobs() + catch (Exception ex) { - try + Log.Error(ex, "Error retreiving boob image: {Message}", ex.Message); + return new UrlReply { - JToken obj; - obj = JArray.Parse(await _http.GetStringAsync($"http://api.oboobs.ru/boobs/{_rng.Next(0, 12000)}").ConfigureAwait(false))[0]; - return new UrlReply - { - Error = "", - Url = $"http://media.oboobs.ru/{obj["preview"]}", - }; - } - catch (Exception ex) - { - Log.Error(ex, "Error retreiving boob image: {Message}", ex.Message); - return new UrlReply - { - Error = ex.Message, - Url = "", - }; - } + Error = ex.Message, + Url = "", + }; } + } - private readonly object taglock = new object(); - public ValueTask ToggleBlacklistTag(ulong guildId, string tag) + private readonly object taglock = new object(); + public ValueTask ToggleBlacklistTag(ulong guildId, string tag) + { + lock (taglock) { - lock (taglock) + tag = tag.Trim().ToLowerInvariant(); + var blacklistedTags = BlacklistedTags.GetOrAdd(guildId, new HashSet()); + var isAdded = blacklistedTags.Add(tag); + + using var uow = _db.GetDbContext(); + if (!isAdded) { - tag = tag.Trim().ToLowerInvariant(); - var blacklistedTags = BlacklistedTags.GetOrAdd(guildId, new HashSet()); - var isAdded = blacklistedTags.Add(tag); - - using var uow = _db.GetDbContext(); - if (!isAdded) - { - blacklistedTags.Remove(tag); - uow.NsfwBlacklistedTags.DeleteAsync(x => x.GuildId == guildId && x.Tag == tag); - uow.SaveChanges(); - } - else - { - uow.NsfwBlacklistedTags.Add(new NsfwBlacklistedTag() - { - Tag = tag, - GuildId = guildId - }); - - uow.SaveChanges(); - } - - return new(isAdded); + blacklistedTags.Remove(tag); + uow.NsfwBlacklistedTags.DeleteAsync(x => x.GuildId == guildId && x.Tag == tag); + uow.SaveChanges(); } + else + { + uow.NsfwBlacklistedTags.Add(new NsfwBlacklistedTag() + { + Tag = tag, + GuildId = guildId + }); + + uow.SaveChanges(); + } + + return new(isAdded); + } - } + } - public ValueTask GetBlacklistedTags(ulong guildId) + public ValueTask GetBlacklistedTags(ulong guildId) + { + lock (taglock) { - lock (taglock) + if (BlacklistedTags.TryGetValue(guildId, out var tags)) { - if (BlacklistedTags.TryGetValue(guildId, out var tags)) - { - return new(tags.ToArray()); - } - - return new(Array.Empty()); + return new(tags.ToArray()); } - } - public async Task Butts() + return new(Array.Empty()); + } + } + + public async Task Butts() + { + try { - try + JToken obj; + obj = JArray.Parse(await _http.GetStringAsync($"http://api.obutts.ru/butts/{_rng.Next(0, 6100)}"))[0]; + return new UrlReply { - JToken obj; - obj = JArray.Parse(await _http.GetStringAsync($"http://api.obutts.ru/butts/{_rng.Next(0, 6100)}"))[0]; - return new UrlReply - { - Error = "", - Url = $"http://media.obutts.ru/{obj["preview"]}", - }; - } - catch (Exception ex) - { - Log.Error(ex, "Error retreiving butt image: {Message}", ex.Message); - return new UrlReply - { - Error = ex.Message, - Url = "", - }; - } - } - - #region Nhentai - private string GetNhentaiExtensionInternal(string s) - => s switch - { - "j" => "jpg", - "p" => "png", - "g" => "gif", - _ => "jpg" + Error = "", + Url = $"http://media.obutts.ru/{obj["preview"]}", }; + } + catch (Exception ex) + { + Log.Error(ex, "Error retreiving butt image: {Message}", ex.Message); + return new UrlReply + { + Error = ex.Message, + Url = "", + }; + } + } - private Gallery ModelToGallery(NhentaiApiModel.Gallery model) + #region Nhentai + private string GetNhentaiExtensionInternal(string s) + => s switch { - var thumbnail = $"https://t.nhentai.net/galleries/{model.MediaId}/thumb." - + GetNhentaiExtensionInternal(model.Images.Thumbnail.T); - - var url = $"https://nhentai.net/g/{model.Id}"; - return new Gallery( - model.Id.ToString(), - url, - model.Title.English, - model.Title.Pretty, - thumbnail, - model.NumPages, - model.NumFavorites, - model.UploadDate.ToUnixTimestamp().UtcDateTime, - model.Tags.Map(x => new Tag() - { - Name = x.Name, - Url = "https://nhentai.com/" + x.Url - })); - } - - private async Task GetNhentaiByIdInternalAsync(uint id) - { - using var http = _httpFactory.CreateClient(); - try - { - var res = await http.GetStringAsync("https://nhentai.net/api/gallery/" + id); - return JsonConvert.DeserializeObject(res); - } - catch (HttpRequestException) - { - Log.Warning("Nhentai with id {NhentaiId} not found", id); - return null; - } - } - - private async Task SearchNhentaiInternalAsync(string search) - { - using var http = _httpFactory.CreateClient(); - try - { - var res = await http.GetStringAsync("https://nhentai.net/api/galleries/search?query=" + search); - return JsonConvert.DeserializeObject(res).Result; - } - catch (HttpRequestException) - { - Log.Warning("Nhentai with search {NhentaiSearch} not found", search); - return null; - } - } - - public async Task GetNhentaiByIdAsync(uint id) - { - var model = await GetNhentaiByIdInternalAsync(id); - - return ModelToGallery(model); - } - - private static readonly string[] _bannedTags = - { - "loli", - "lolicon", - "shota", - "shotacon", - "cub" + "j" => "jpg", + "p" => "png", + "g" => "gif", + _ => "jpg" }; - public async Task GetNhentaiBySearchAsync(string search) - { - var models = await SearchNhentaiInternalAsync(search); + private Gallery ModelToGallery(NhentaiApiModel.Gallery model) + { + var thumbnail = $"https://t.nhentai.net/galleries/{model.MediaId}/thumb." + + GetNhentaiExtensionInternal(model.Images.Thumbnail.T); - models = models.Where(x => !x.Tags.Any(t => _bannedTags.Contains(t.Name))).ToArray(); - - if (models.Length == 0) - return null; - - return ModelToGallery(models[_rng.Next(0, models.Length)]); - } - #endregion + var url = $"https://nhentai.net/g/{model.Id}"; + return new Gallery( + model.Id.ToString(), + url, + model.Title.English, + model.Title.Pretty, + thumbnail, + model.NumPages, + model.NumFavorites, + model.UploadDate.ToUnixTimestamp().UtcDateTime, + model.Tags.Map(x => new Tag() + { + Name = x.Name, + Url = "https://nhentai.com/" + x.Url + })); } -} + + private async Task GetNhentaiByIdInternalAsync(uint id) + { + using var http = _httpFactory.CreateClient(); + try + { + var res = await http.GetStringAsync("https://nhentai.net/api/gallery/" + id); + return JsonConvert.DeserializeObject(res); + } + catch (HttpRequestException) + { + Log.Warning("Nhentai with id {NhentaiId} not found", id); + return null; + } + } + + private async Task SearchNhentaiInternalAsync(string search) + { + using var http = _httpFactory.CreateClient(); + try + { + var res = await http.GetStringAsync("https://nhentai.net/api/galleries/search?query=" + search); + return JsonConvert.DeserializeObject(res).Result; + } + catch (HttpRequestException) + { + Log.Warning("Nhentai with search {NhentaiSearch} not found", search); + return null; + } + } + + public async Task GetNhentaiByIdAsync(uint id) + { + var model = await GetNhentaiByIdInternalAsync(id); + + return ModelToGallery(model); + } + + private static readonly string[] _bannedTags = + { + "loli", + "lolicon", + "shota", + "shotacon", + "cub" + }; + + public async Task GetNhentaiBySearchAsync(string search) + { + var models = await SearchNhentaiInternalAsync(search); + + models = models.Where(x => !x.Tags.Any(t => _bannedTags.Contains(t.Name))).ToArray(); + + if (models.Length == 0) + return null; + + return ModelToGallery(models[_rng.Next(0, models.Length)]); + } + #endregion +} \ No newline at end of file diff --git a/src/NadekoBot/Modules/Permissions/BlacklistCommands.cs b/src/NadekoBot/Modules/Permissions/BlacklistCommands.cs index df0bfd691..e0464d993 100644 --- a/src/NadekoBot/Modules/Permissions/BlacklistCommands.cs +++ b/src/NadekoBot/Modules/Permissions/BlacklistCommands.cs @@ -1,157 +1,153 @@ -using System; -using Discord; +using Discord; using Discord.Commands; using NadekoBot.Common.Attributes; using NadekoBot.Common.TypeReaders; using NadekoBot.Services.Database.Models; using NadekoBot.Modules.Permissions.Services; -using System.Linq; using System.Threading.Tasks; using Discord.WebSocket; using NadekoBot.Extensions; -using Serilog; -namespace NadekoBot.Modules.Permissions +namespace NadekoBot.Modules.Permissions; + +public partial class Permissions { - public partial class Permissions + [Group] + public class BlacklistCommands : NadekoSubmodule { - [Group] - public class BlacklistCommands : NadekoSubmodule + private readonly DiscordSocketClient _client; + + public BlacklistCommands(DiscordSocketClient client) { - private readonly DiscordSocketClient _client; - - public BlacklistCommands(DiscordSocketClient client) - { - _client = client; - } + _client = client; + } - private async Task ListBlacklistInternal(string title, BlacklistType type, int page = 0) - { - if (page < 0) - throw new ArgumentOutOfRangeException(nameof(page)); + private async Task ListBlacklistInternal(string title, BlacklistType type, int page = 0) + { + if (page < 0) + throw new ArgumentOutOfRangeException(nameof(page)); - var list = _service.GetBlacklist(); - var items = await list - .Where(x => x.Type == type) - .Select(async i => - { - try - { - return i.Type switch - { - BlacklistType.Channel => Format.Code(i.ItemId.ToString()) - + " " + (_client.GetChannel(i.ItemId)?.ToString() ?? ""), - BlacklistType.User => Format.Code(i.ItemId.ToString()) - + " " + - ((await _client.Rest.GetUserAsync(i.ItemId))?.ToString() ?? ""), - BlacklistType.Server => Format.Code(i.ItemId.ToString()) - + " " + (_client.GetGuild(i.ItemId)?.ToString() ?? ""), - _ => Format.Code(i.ItemId.ToString()) - }; - } - catch - { - Log.Warning("Can't get {BlacklistType} [{BlacklistItemId}]", i.Type, i.ItemId); - return Format.Code(i.ItemId.ToString()); - } - }) - .WhenAll(); - - await ctx.SendPaginatedConfirmAsync(page, (int curPage) => + var list = _service.GetBlacklist(); + var items = await list + .Where(x => x.Type == type) + .Select(async i => { - var pageItems = items - .Skip(10 * curPage) - .Take(10) - .ToList(); - - if (pageItems.Count == 0) + try { - return _eb.Create() - .WithOkColor() - .WithTitle(title) - .WithDescription(GetText(strs.empty_page)); + return i.Type switch + { + BlacklistType.Channel => Format.Code(i.ItemId.ToString()) + + " " + (_client.GetChannel(i.ItemId)?.ToString() ?? ""), + BlacklistType.User => Format.Code(i.ItemId.ToString()) + + " " + + ((await _client.Rest.GetUserAsync(i.ItemId))?.ToString() ?? ""), + BlacklistType.Server => Format.Code(i.ItemId.ToString()) + + " " + (_client.GetGuild(i.ItemId)?.ToString() ?? ""), + _ => Format.Code(i.ItemId.ToString()) + }; } + catch + { + Log.Warning("Can't get {BlacklistType} [{BlacklistItemId}]", i.Type, i.ItemId); + return Format.Code(i.ItemId.ToString()); + } + }) + .WhenAll(); + + await ctx.SendPaginatedConfirmAsync(page, (int curPage) => + { + var pageItems = items + .Skip(10 * curPage) + .Take(10) + .ToList(); + if (pageItems.Count == 0) + { return _eb.Create() + .WithOkColor() .WithTitle(title) - .WithDescription(pageItems.JoinWith('\n')) - .WithOkColor(); - }, items.Length, 10); - } - - [NadekoCommand, Aliases] - [OwnerOnly] - public Task UserBlacklist(int page = 1) - { - if (--page < 0) - return Task.CompletedTask; - - return ListBlacklistInternal(GetText(strs.blacklisted_users), BlacklistType.User, page); - } - - [NadekoCommand, Aliases] - [OwnerOnly] - public Task ChannelBlacklist(int page = 1) - { - if (--page < 0) - return Task.CompletedTask; - - return ListBlacklistInternal(GetText(strs.blacklisted_channels), BlacklistType.Channel, page); - } - - [NadekoCommand, Aliases] - [OwnerOnly] - public Task ServerBlacklist(int page = 1) - { - if (--page < 0) - return Task.CompletedTask; - - return ListBlacklistInternal(GetText(strs.blacklisted_servers), BlacklistType.Server, page); - } - - [NadekoCommand, Aliases] - [OwnerOnly] - public Task UserBlacklist(AddRemove action, ulong id) - => Blacklist(action, id, BlacklistType.User); - - [NadekoCommand, Aliases] - [OwnerOnly] - public Task UserBlacklist(AddRemove action, IUser usr) - => Blacklist(action, usr.Id, BlacklistType.User); - - [NadekoCommand, Aliases] - [OwnerOnly] - public Task ChannelBlacklist(AddRemove action, ulong id) - => Blacklist(action, id, BlacklistType.Channel); - - [NadekoCommand, Aliases] - [OwnerOnly] - public Task ServerBlacklist(AddRemove action, ulong id) - => Blacklist(action, id, BlacklistType.Server); - - [NadekoCommand, Aliases] - [OwnerOnly] - public Task ServerBlacklist(AddRemove action, IGuild guild) - => Blacklist(action, guild.Id, BlacklistType.Server); - - private async Task Blacklist(AddRemove action, ulong id, BlacklistType type) - { - if (action == AddRemove.Add) - { - _service.Blacklist(type, id); - } - else - { - _service.UnBlacklist(type, id); + .WithDescription(GetText(strs.empty_page)); } + + return _eb.Create() + .WithTitle(title) + .WithDescription(pageItems.JoinWith('\n')) + .WithOkColor(); + }, items.Length, 10); + } + + [NadekoCommand, Aliases] + [OwnerOnly] + public Task UserBlacklist(int page = 1) + { + if (--page < 0) + return Task.CompletedTask; + + return ListBlacklistInternal(GetText(strs.blacklisted_users), BlacklistType.User, page); + } + + [NadekoCommand, Aliases] + [OwnerOnly] + public Task ChannelBlacklist(int page = 1) + { + if (--page < 0) + return Task.CompletedTask; + + return ListBlacklistInternal(GetText(strs.blacklisted_channels), BlacklistType.Channel, page); + } + + [NadekoCommand, Aliases] + [OwnerOnly] + public Task ServerBlacklist(int page = 1) + { + if (--page < 0) + return Task.CompletedTask; + + return ListBlacklistInternal(GetText(strs.blacklisted_servers), BlacklistType.Server, page); + } - if (action == AddRemove.Add) - await ReplyConfirmLocalizedAsync(strs.blacklisted(Format.Code(type.ToString()), - Format.Code(id.ToString()))); - else - await ReplyConfirmLocalizedAsync(strs.unblacklisted(Format.Code(type.ToString()), - Format.Code(id.ToString()))); + [NadekoCommand, Aliases] + [OwnerOnly] + public Task UserBlacklist(AddRemove action, ulong id) + => Blacklist(action, id, BlacklistType.User); + + [NadekoCommand, Aliases] + [OwnerOnly] + public Task UserBlacklist(AddRemove action, IUser usr) + => Blacklist(action, usr.Id, BlacklistType.User); + + [NadekoCommand, Aliases] + [OwnerOnly] + public Task ChannelBlacklist(AddRemove action, ulong id) + => Blacklist(action, id, BlacklistType.Channel); + + [NadekoCommand, Aliases] + [OwnerOnly] + public Task ServerBlacklist(AddRemove action, ulong id) + => Blacklist(action, id, BlacklistType.Server); + + [NadekoCommand, Aliases] + [OwnerOnly] + public Task ServerBlacklist(AddRemove action, IGuild guild) + => Blacklist(action, guild.Id, BlacklistType.Server); + + private async Task Blacklist(AddRemove action, ulong id, BlacklistType type) + { + if (action == AddRemove.Add) + { + _service.Blacklist(type, id); } + else + { + _service.UnBlacklist(type, id); + } + + if (action == AddRemove.Add) + await ReplyConfirmLocalizedAsync(strs.blacklisted(Format.Code(type.ToString()), + Format.Code(id.ToString()))); + else + await ReplyConfirmLocalizedAsync(strs.unblacklisted(Format.Code(type.ToString()), + Format.Code(id.ToString()))); } } -} +} \ No newline at end of file diff --git a/src/NadekoBot/Modules/Permissions/CmdCdsCommands.cs b/src/NadekoBot/Modules/Permissions/CmdCdsCommands.cs index 7c3d830d7..29d9ea015 100644 --- a/src/NadekoBot/Modules/Permissions/CmdCdsCommands.cs +++ b/src/NadekoBot/Modules/Permissions/CmdCdsCommands.cs @@ -5,95 +5,92 @@ using NadekoBot.Extensions; using NadekoBot.Services; using NadekoBot.Services.Database.Models; using System.Collections.Concurrent; -using System.Linq; using System.Threading.Tasks; using NadekoBot.Common.Attributes; using NadekoBot.Common.Collections; using NadekoBot.Common.TypeReaders; using NadekoBot.Db; -using NadekoBot.Modules.Administration; using NadekoBot.Modules.Permissions.Services; -namespace NadekoBot.Modules.Permissions +namespace NadekoBot.Modules.Permissions; + +public partial class Permissions { - public partial class Permissions + [Group] + public class CmdCdsCommands : NadekoSubmodule { - [Group] - public class CmdCdsCommands : NadekoSubmodule + private readonly DbService _db; + private readonly CmdCdService _service; + + private ConcurrentDictionary> CommandCooldowns + => _service.CommandCooldowns; + private ConcurrentDictionary> ActiveCooldowns + => _service.ActiveCooldowns; + + public CmdCdsCommands(CmdCdService service, DbService db) { - private readonly DbService _db; - private readonly CmdCdService _service; + _service = service; + _db = db; + } - private ConcurrentDictionary> CommandCooldowns - => _service.CommandCooldowns; - private ConcurrentDictionary> ActiveCooldowns - => _service.ActiveCooldowns; - - public CmdCdsCommands(CmdCdService service, DbService db) + [NadekoCommand, Aliases] + [RequireContext(ContextType.Guild)] + public async Task CmdCooldown(CommandOrCrInfo command, int secs) + { + var channel = (ITextChannel)ctx.Channel; + if (secs < 0 || secs > 3600) { - _service = service; - _db = db; + await ReplyErrorLocalizedAsync(strs.invalid_second_param_between(0, 3600)); + return; } - [NadekoCommand, Aliases] - [RequireContext(ContextType.Guild)] - public async Task CmdCooldown(CommandOrCrInfo command, int secs) + var name = command.Name.ToLowerInvariant(); + using (var uow = _db.GetDbContext()) { - var channel = (ITextChannel)ctx.Channel; - if (secs < 0 || secs > 3600) - { - await ReplyErrorLocalizedAsync(strs.invalid_second_param_between(0, 3600)); - return; - } - - var name = command.Name.ToLowerInvariant(); - using (var uow = _db.GetDbContext()) - { - var config = uow.GuildConfigsForId(channel.Guild.Id, set => set.Include(gc => gc.CommandCooldowns)); - var localSet = CommandCooldowns.GetOrAdd(channel.Guild.Id, new ConcurrentHashSet()); - - var toDelete = config.CommandCooldowns.FirstOrDefault(cc => cc.CommandName == name); - if (toDelete != null) - uow.Set().Remove(toDelete); - localSet.RemoveWhere(cc => cc.CommandName == name); - if (secs != 0) - { - var cc = new CommandCooldown() - { - CommandName = name, - Seconds = secs, - }; - config.CommandCooldowns.Add(cc); - localSet.Add(cc); - } - await uow.SaveChangesAsync(); - } - if (secs == 0) - { - var activeCds = ActiveCooldowns.GetOrAdd(channel.Guild.Id, new ConcurrentHashSet()); - activeCds.RemoveWhere(ac => ac.Command == name); - await ReplyConfirmLocalizedAsync(strs.cmdcd_cleared(Format.Bold(name))); - } - else - { - await ReplyConfirmLocalizedAsync(strs.cmdcd_add( - Format.Bold(name), - Format.Bold(secs.ToString()))); - } - } - - [NadekoCommand, Aliases] - [RequireContext(ContextType.Guild)] - public async Task AllCmdCooldowns() - { - var channel = (ITextChannel)ctx.Channel; + var config = uow.GuildConfigsForId(channel.Guild.Id, set => set.Include(gc => gc.CommandCooldowns)); var localSet = CommandCooldowns.GetOrAdd(channel.Guild.Id, new ConcurrentHashSet()); - if (!localSet.Any()) - await ReplyConfirmLocalizedAsync(strs.cmdcd_none).ConfigureAwait(false); - else - await channel.SendTableAsync("", localSet.Select(c => c.CommandName + ": " + c.Seconds + GetText(strs.sec)), s => $"{s,-30}", 2).ConfigureAwait(false); + var toDelete = config.CommandCooldowns.FirstOrDefault(cc => cc.CommandName == name); + if (toDelete != null) + uow.Set().Remove(toDelete); + localSet.RemoveWhere(cc => cc.CommandName == name); + if (secs != 0) + { + var cc = new CommandCooldown() + { + CommandName = name, + Seconds = secs, + }; + config.CommandCooldowns.Add(cc); + localSet.Add(cc); + } + await uow.SaveChangesAsync(); + } + if (secs == 0) + { + var activeCds = ActiveCooldowns.GetOrAdd(channel.Guild.Id, new ConcurrentHashSet()); + activeCds.RemoveWhere(ac => ac.Command == name); + await ReplyConfirmLocalizedAsync(strs.cmdcd_cleared(Format.Bold(name))); + } + else + { + await ReplyConfirmLocalizedAsync(strs.cmdcd_add( + Format.Bold(name), + Format.Bold(secs.ToString()))); } } + + [NadekoCommand, Aliases] + [RequireContext(ContextType.Guild)] + public async Task AllCmdCooldowns() + { + var channel = (ITextChannel)ctx.Channel; + var localSet = CommandCooldowns.GetOrAdd(channel.Guild.Id, new ConcurrentHashSet()); + + if (!localSet.Any()) + await ReplyConfirmLocalizedAsync(strs.cmdcd_none).ConfigureAwait(false); + else + await channel.SendTableAsync("", localSet.Select(c => c.CommandName + ": " + c.Seconds + GetText(strs.sec)), s => $"{s,-30}", 2).ConfigureAwait(false); + } } -} +} \ No newline at end of file diff --git a/src/NadekoBot/Modules/Permissions/Common/PermissionCache.cs b/src/NadekoBot/Modules/Permissions/Common/PermissionCache.cs index 346f31be2..f3fca3439 100644 --- a/src/NadekoBot/Modules/Permissions/Common/PermissionCache.cs +++ b/src/NadekoBot/Modules/Permissions/Common/PermissionCache.cs @@ -1,11 +1,10 @@ using NadekoBot.Services.Database.Models; -namespace NadekoBot.Modules.Permissions.Common +namespace NadekoBot.Modules.Permissions.Common; + +public class PermissionCache { - public class PermissionCache - { - public string PermRole { get; set; } - public bool Verbose { get; set; } = true; - public PermissionsCollection Permissions { get; set; } - } -} + public string PermRole { get; set; } + public bool Verbose { get; set; } = true; + public PermissionsCollection Permissions { get; set; } +} \ No newline at end of file diff --git a/src/NadekoBot/Modules/Permissions/Common/PermissionExtensions.cs b/src/NadekoBot/Modules/Permissions/Common/PermissionExtensions.cs index 42c766bb5..8afa0d8f1 100644 --- a/src/NadekoBot/Modules/Permissions/Common/PermissionExtensions.cs +++ b/src/NadekoBot/Modules/Permissions/Common/PermissionExtensions.cs @@ -1,125 +1,122 @@ -using System.Collections.Generic; -using System.Linq; -using Discord; +using Discord; using Discord.WebSocket; using NadekoBot.Services.Database.Models; -namespace NadekoBot.Modules.Permissions.Common +namespace NadekoBot.Modules.Permissions.Common; + +public static class PermissionExtensions { - public static class PermissionExtensions + public static bool CheckPermissions(this IEnumerable permsEnumerable, IUserMessage message, + string commandName, string moduleName, out int permIndex) { - public static bool CheckPermissions(this IEnumerable permsEnumerable, IUserMessage message, - string commandName, string moduleName, out int permIndex) + var perms = permsEnumerable as List ?? permsEnumerable.ToList(); + + for (int i = perms.Count - 1; i >= 0; i--) { - var perms = permsEnumerable as List ?? permsEnumerable.ToList(); + var perm = perms[i]; - for (int i = perms.Count - 1; i >= 0; i--) + var result = perm.CheckPermission(message, commandName, moduleName); + + if (result is null) { - var perm = perms[i]; - - var result = perm.CheckPermission(message, commandName, moduleName); - - if (result is null) - { - continue; - } - permIndex = i; - return result.Value; + continue; } - permIndex = -1; //defaut behaviour - return true; - } - - //null = not applicable - //true = applicable, allowed - //false = applicable, not allowed - public static bool? CheckPermission(this Permissionv2 perm, IUserMessage message, string commandName, string moduleName) - { - if (!((perm.SecondaryTarget == SecondaryPermissionType.Command && - perm.SecondaryTargetName.ToLowerInvariant() == commandName.ToLowerInvariant()) || - (perm.SecondaryTarget == SecondaryPermissionType.Module && - perm.SecondaryTargetName.ToLowerInvariant() == moduleName.ToLowerInvariant()) || - perm.SecondaryTarget == SecondaryPermissionType.AllModules)) - return null; - - var guildUser = message.Author as IGuildUser; - - switch (perm.PrimaryTarget) - { - case PrimaryPermissionType.User: - if (perm.PrimaryTargetId == message.Author.Id) - return perm.State; - break; - case PrimaryPermissionType.Channel: - if (perm.PrimaryTargetId == message.Channel.Id) - return perm.State; - break; - case PrimaryPermissionType.Role: - if (guildUser is null) - break; - if (guildUser.RoleIds.Contains(perm.PrimaryTargetId)) - return perm.State; - break; - case PrimaryPermissionType.Server: - if (guildUser is null) - break; - return perm.State; - } - return null; - } - - public static string GetCommand(this Permissionv2 perm, string prefix, SocketGuild guild = null) - { - var com = ""; - switch (perm.PrimaryTarget) - { - case PrimaryPermissionType.User: - com += "u"; - break; - case PrimaryPermissionType.Channel: - com += "c"; - break; - case PrimaryPermissionType.Role: - com += "r"; - break; - case PrimaryPermissionType.Server: - com += "s"; - break; - } - - switch (perm.SecondaryTarget) - { - case SecondaryPermissionType.Module: - com += "m"; - break; - case SecondaryPermissionType.Command: - com += "c"; - break; - case SecondaryPermissionType.AllModules: - com = "a" + com + "m"; - break; - } - - var secName = perm.SecondaryTarget == SecondaryPermissionType.Command && !perm.IsCustomCommand ? - prefix + perm.SecondaryTargetName : perm.SecondaryTargetName; - com += " " + (perm.SecondaryTargetName != "*" ? secName + " " : "") + (perm.State ? "enable" : "disable") + " "; - - switch (perm.PrimaryTarget) - { - case PrimaryPermissionType.User: - com += guild?.GetUser(perm.PrimaryTargetId)?.ToString() ?? $"<@{perm.PrimaryTargetId}>"; - break; - case PrimaryPermissionType.Channel: - com += $"<#{perm.PrimaryTargetId}>"; - break; - case PrimaryPermissionType.Role: - com += guild?.GetRole(perm.PrimaryTargetId)?.ToString() ?? $"<@&{perm.PrimaryTargetId}>"; - break; - case PrimaryPermissionType.Server: - break; - } - - return prefix + com; + permIndex = i; + return result.Value; } + permIndex = -1; //defaut behaviour + return true; } -} + + //null = not applicable + //true = applicable, allowed + //false = applicable, not allowed + public static bool? CheckPermission(this Permissionv2 perm, IUserMessage message, string commandName, string moduleName) + { + if (!((perm.SecondaryTarget == SecondaryPermissionType.Command && + perm.SecondaryTargetName.ToLowerInvariant() == commandName.ToLowerInvariant()) || + (perm.SecondaryTarget == SecondaryPermissionType.Module && + perm.SecondaryTargetName.ToLowerInvariant() == moduleName.ToLowerInvariant()) || + perm.SecondaryTarget == SecondaryPermissionType.AllModules)) + return null; + + var guildUser = message.Author as IGuildUser; + + switch (perm.PrimaryTarget) + { + case PrimaryPermissionType.User: + if (perm.PrimaryTargetId == message.Author.Id) + return perm.State; + break; + case PrimaryPermissionType.Channel: + if (perm.PrimaryTargetId == message.Channel.Id) + return perm.State; + break; + case PrimaryPermissionType.Role: + if (guildUser is null) + break; + if (guildUser.RoleIds.Contains(perm.PrimaryTargetId)) + return perm.State; + break; + case PrimaryPermissionType.Server: + if (guildUser is null) + break; + return perm.State; + } + return null; + } + + public static string GetCommand(this Permissionv2 perm, string prefix, SocketGuild guild = null) + { + var com = ""; + switch (perm.PrimaryTarget) + { + case PrimaryPermissionType.User: + com += "u"; + break; + case PrimaryPermissionType.Channel: + com += "c"; + break; + case PrimaryPermissionType.Role: + com += "r"; + break; + case PrimaryPermissionType.Server: + com += "s"; + break; + } + + switch (perm.SecondaryTarget) + { + case SecondaryPermissionType.Module: + com += "m"; + break; + case SecondaryPermissionType.Command: + com += "c"; + break; + case SecondaryPermissionType.AllModules: + com = "a" + com + "m"; + break; + } + + var secName = perm.SecondaryTarget == SecondaryPermissionType.Command && !perm.IsCustomCommand ? + prefix + perm.SecondaryTargetName : perm.SecondaryTargetName; + com += " " + (perm.SecondaryTargetName != "*" ? secName + " " : "") + (perm.State ? "enable" : "disable") + " "; + + switch (perm.PrimaryTarget) + { + case PrimaryPermissionType.User: + com += guild?.GetUser(perm.PrimaryTargetId)?.ToString() ?? $"<@{perm.PrimaryTargetId}>"; + break; + case PrimaryPermissionType.Channel: + com += $"<#{perm.PrimaryTargetId}>"; + break; + case PrimaryPermissionType.Role: + com += guild?.GetRole(perm.PrimaryTargetId)?.ToString() ?? $"<@&{perm.PrimaryTargetId}>"; + break; + case PrimaryPermissionType.Server: + break; + } + + return prefix + com; + } +} \ No newline at end of file diff --git a/src/NadekoBot/Modules/Permissions/Common/PermissionsCollection.cs b/src/NadekoBot/Modules/Permissions/Common/PermissionsCollection.cs index 0f6ab6b9e..34ce69527 100644 --- a/src/NadekoBot/Modules/Permissions/Common/PermissionsCollection.cs +++ b/src/NadekoBot/Modules/Permissions/Common/PermissionsCollection.cs @@ -1,74 +1,70 @@ -using System; -using System.Collections.Generic; -using NadekoBot.Common.Collections; +using NadekoBot.Common.Collections; using NadekoBot.Services.Database.Models; -namespace NadekoBot.Modules.Permissions.Common +namespace NadekoBot.Modules.Permissions.Common; + +public class PermissionsCollection : IndexedCollection where T : class, IIndexed { - public class PermissionsCollection : IndexedCollection where T : class, IIndexed + private readonly object _localLocker = new object(); + public PermissionsCollection(IEnumerable source) : base(source) { - private readonly object _localLocker = new object(); - public PermissionsCollection(IEnumerable source) : base(source) + } + + public static implicit operator List(PermissionsCollection x) => + x.Source; + + public override void Clear() + { + lock (_localLocker) { - } - - public static implicit operator List(PermissionsCollection x) => - x.Source; - - public override void Clear() - { - lock (_localLocker) - { - var first = Source[0]; - base.Clear(); - Source[0] = first; - } - } - - public override bool Remove(T item) - { - bool removed; - lock (_localLocker) - { - if(Source.IndexOf(item) == 0) - throw new ArgumentException("You can't remove first permsission (allow all)"); - removed = base.Remove(item); - } - return removed; - } - - public override void Insert(int index, T item) - { - lock (_localLocker) - { - if(index == 0) // can't insert on first place. Last item is always allow all. - throw new IndexOutOfRangeException(nameof(index)); - base.Insert(index, item); - } - } - - public override void RemoveAt(int index) - { - lock (_localLocker) - { - if(index == 0) // you can't remove first permission (allow all) - throw new IndexOutOfRangeException(nameof(index)); - - base.RemoveAt(index); - } - } - - public override T this[int index] { - get => Source[index]; - set { - lock (_localLocker) - { - if(index == 0) // can't set first element. It's always allow all - throw new IndexOutOfRangeException(nameof(index)); - base[index] = value; - } - } + var first = Source[0]; + base.Clear(); + Source[0] = first; } } -} + public override bool Remove(T item) + { + bool removed; + lock (_localLocker) + { + if(Source.IndexOf(item) == 0) + throw new ArgumentException("You can't remove first permsission (allow all)"); + removed = base.Remove(item); + } + return removed; + } + + public override void Insert(int index, T item) + { + lock (_localLocker) + { + if(index == 0) // can't insert on first place. Last item is always allow all. + throw new IndexOutOfRangeException(nameof(index)); + base.Insert(index, item); + } + } + + public override void RemoveAt(int index) + { + lock (_localLocker) + { + if(index == 0) // you can't remove first permission (allow all) + throw new IndexOutOfRangeException(nameof(index)); + + base.RemoveAt(index); + } + } + + public override T this[int index] { + get => Source[index]; + set { + lock (_localLocker) + { + if(index == 0) // can't set first element. It's always allow all + throw new IndexOutOfRangeException(nameof(index)); + base[index] = value; + } + } + } +} \ No newline at end of file diff --git a/src/NadekoBot/Modules/Permissions/FilterCommands.cs b/src/NadekoBot/Modules/Permissions/FilterCommands.cs index 42912f00b..f14e3007d 100644 --- a/src/NadekoBot/Modules/Permissions/FilterCommands.cs +++ b/src/NadekoBot/Modules/Permissions/FilterCommands.cs @@ -3,296 +3,293 @@ using Discord.Commands; using Microsoft.EntityFrameworkCore; using NadekoBot.Extensions; using NadekoBot.Services; -using System.Linq; using System.Threading.Tasks; using NadekoBot.Common.Attributes; using NadekoBot.Common.Collections; using NadekoBot.Modules.Permissions.Services; using NadekoBot.Services.Database.Models; using NadekoBot.Db; -using NadekoBot.Modules.Administration; -namespace NadekoBot.Modules.Permissions +namespace NadekoBot.Modules.Permissions; + +public partial class Permissions { - public partial class Permissions + [Group] + public class FilterCommands : NadekoSubmodule { - [Group] - public class FilterCommands : NadekoSubmodule + private readonly DbService _db; + + public FilterCommands(DbService db) { - private readonly DbService _db; + _db = db; + } - public FilterCommands(DbService db) + [NadekoCommand, Aliases] + [RequireContext(ContextType.Guild)] + [UserPerm(GuildPerm.Administrator)] + public async Task FwClear() + { + _service.ClearFilteredWords(ctx.Guild.Id); + await ReplyConfirmLocalizedAsync(strs.fw_cleared).ConfigureAwait(false); + } + + [NadekoCommand, Aliases] + [RequireContext(ContextType.Guild)] + public async Task SrvrFilterInv() + { + var channel = (ITextChannel)ctx.Channel; + + bool enabled; + using (var uow = _db.GetDbContext()) { - _db = db; + var config = uow.GuildConfigsForId(channel.Guild.Id, set => set); + enabled = config.FilterInvites = !config.FilterInvites; + await uow.SaveChangesAsync(); } - [NadekoCommand, Aliases] - [RequireContext(ContextType.Guild)] - [UserPerm(GuildPerm.Administrator)] - public async Task FwClear() + if (enabled) { - _service.ClearFilteredWords(ctx.Guild.Id); - await ReplyConfirmLocalizedAsync(strs.fw_cleared).ConfigureAwait(false); + _service.InviteFilteringServers.Add(channel.Guild.Id); + await ReplyConfirmLocalizedAsync(strs.invite_filter_server_on).ConfigureAwait(false); } - - [NadekoCommand, Aliases] - [RequireContext(ContextType.Guild)] - public async Task SrvrFilterInv() + else { - var channel = (ITextChannel)ctx.Channel; - - bool enabled; - using (var uow = _db.GetDbContext()) - { - var config = uow.GuildConfigsForId(channel.Guild.Id, set => set); - enabled = config.FilterInvites = !config.FilterInvites; - await uow.SaveChangesAsync(); - } - - if (enabled) - { - _service.InviteFilteringServers.Add(channel.Guild.Id); - await ReplyConfirmLocalizedAsync(strs.invite_filter_server_on).ConfigureAwait(false); - } - else - { - _service.InviteFilteringServers.TryRemove(channel.Guild.Id); - await ReplyConfirmLocalizedAsync(strs.invite_filter_server_off).ConfigureAwait(false); - } - } - - [NadekoCommand, Aliases] - [RequireContext(ContextType.Guild)] - public async Task ChnlFilterInv() - { - var channel = (ITextChannel)ctx.Channel; - - FilterChannelId removed; - using (var uow = _db.GetDbContext()) - { - var config = uow.GuildConfigsForId(channel.Guild.Id, set => set.Include(gc => gc.FilterInvitesChannelIds)); - var match = new FilterChannelId() - { - ChannelId = channel.Id - }; - removed = config.FilterInvitesChannelIds.FirstOrDefault(fc => fc.Equals(match)); - - if (removed is null) - { - config.FilterInvitesChannelIds.Add(match); - } - else - { - uow.Remove(removed); - } - await uow.SaveChangesAsync(); - } - - if (removed is null) - { - _service.InviteFilteringChannels.Add(channel.Id); - await ReplyConfirmLocalizedAsync(strs.invite_filter_channel_on).ConfigureAwait(false); - } - else - { - _service.InviteFilteringChannels.TryRemove(channel.Id); - await ReplyConfirmLocalizedAsync(strs.invite_filter_channel_off).ConfigureAwait(false); - } - } - - [NadekoCommand, Aliases] - [RequireContext(ContextType.Guild)] - public async Task SrvrFilterLin() - { - var channel = (ITextChannel)ctx.Channel; - - bool enabled; - using (var uow = _db.GetDbContext()) - { - var config = uow.GuildConfigsForId(channel.Guild.Id, set => set); - enabled = config.FilterLinks = !config.FilterLinks; - await uow.SaveChangesAsync(); - } - - if (enabled) - { - _service.LinkFilteringServers.Add(channel.Guild.Id); - await ReplyConfirmLocalizedAsync(strs.link_filter_server_on).ConfigureAwait(false); - } - else - { - _service.LinkFilteringServers.TryRemove(channel.Guild.Id); - await ReplyConfirmLocalizedAsync(strs.link_filter_server_off).ConfigureAwait(false); - } - } - - [NadekoCommand, Aliases] - [RequireContext(ContextType.Guild)] - public async Task ChnlFilterLin() - { - var channel = (ITextChannel)ctx.Channel; - - FilterLinksChannelId removed; - using (var uow = _db.GetDbContext()) - { - var config = uow.GuildConfigsForId(channel.Guild.Id, set => set.Include(gc => gc.FilterLinksChannelIds)); - var match = new FilterLinksChannelId() - { - ChannelId = channel.Id - }; - removed = config.FilterLinksChannelIds.FirstOrDefault(fc => fc.Equals(match)); - - if (removed is null) - { - config.FilterLinksChannelIds.Add(match); - } - else - { - uow.Remove(removed); - } - await uow.SaveChangesAsync(); - } - - if (removed is null) - { - _service.LinkFilteringChannels.Add(channel.Id); - await ReplyConfirmLocalizedAsync(strs.link_filter_channel_on).ConfigureAwait(false); - } - else - { - _service.LinkFilteringChannels.TryRemove(channel.Id); - await ReplyConfirmLocalizedAsync(strs.link_filter_channel_off).ConfigureAwait(false); - } - } - - [NadekoCommand, Aliases] - [RequireContext(ContextType.Guild)] - public async Task SrvrFilterWords() - { - var channel = (ITextChannel)ctx.Channel; - - bool enabled; - using (var uow = _db.GetDbContext()) - { - var config = uow.GuildConfigsForId(channel.Guild.Id, set => set); - enabled = config.FilterWords = !config.FilterWords; - await uow.SaveChangesAsync(); - } - - if (enabled) - { - _service.WordFilteringServers.Add(channel.Guild.Id); - await ReplyConfirmLocalizedAsync(strs.word_filter_server_on).ConfigureAwait(false); - } - else - { - _service.WordFilteringServers.TryRemove(channel.Guild.Id); - await ReplyConfirmLocalizedAsync(strs.word_filter_server_off).ConfigureAwait(false); - } - } - - [NadekoCommand, Aliases] - [RequireContext(ContextType.Guild)] - public async Task ChnlFilterWords() - { - var channel = (ITextChannel)ctx.Channel; - - FilterChannelId removed; - using (var uow = _db.GetDbContext()) - { - var config = uow.GuildConfigsForId(channel.Guild.Id, set => set.Include(gc => gc.FilterWordsChannelIds)); - - var match = new FilterChannelId() - { - ChannelId = channel.Id - }; - removed = config.FilterWordsChannelIds.FirstOrDefault(fc => fc.Equals(match)); - if (removed is null) - { - config.FilterWordsChannelIds.Add(match); - } - else - { - uow.Remove(removed); - } - await uow.SaveChangesAsync(); - } - - if (removed is null) - { - _service.WordFilteringChannels.Add(channel.Id); - await ReplyConfirmLocalizedAsync(strs.word_filter_channel_on).ConfigureAwait(false); - } - else - { - _service.WordFilteringChannels.TryRemove(channel.Id); - await ReplyConfirmLocalizedAsync(strs.word_filter_channel_off).ConfigureAwait(false); - } - } - - [NadekoCommand, Aliases] - [RequireContext(ContextType.Guild)] - public async Task FilterWord([Leftover] string word) - { - var channel = (ITextChannel)ctx.Channel; - - word = word?.Trim().ToLowerInvariant(); - - if (string.IsNullOrWhiteSpace(word)) - return; - - FilteredWord removed; - using (var uow = _db.GetDbContext()) - { - var config = uow.GuildConfigsForId(channel.Guild.Id, set => set.Include(gc => gc.FilteredWords)); - - removed = config.FilteredWords.FirstOrDefault(fw => fw.Word.Trim().ToLowerInvariant() == word); - - if (removed is null) - config.FilteredWords.Add(new FilteredWord() { Word = word }); - else - { - uow.Remove(removed); - } - - await uow.SaveChangesAsync(); - } - - var filteredWords = _service.ServerFilteredWords.GetOrAdd(channel.Guild.Id, new ConcurrentHashSet()); - - if (removed is null) - { - filteredWords.Add(word); - await ReplyConfirmLocalizedAsync(strs.filter_word_add(Format.Code(word))).ConfigureAwait(false); - } - else - { - filteredWords.TryRemove(word); - await ReplyConfirmLocalizedAsync(strs.filter_word_remove(Format.Code(word))).ConfigureAwait(false); - } - } - - [NadekoCommand, Aliases] - [RequireContext(ContextType.Guild)] - public async Task LstFilterWords(int page = 1) - { - page--; - if (page < 0) - return; - - var channel = (ITextChannel)ctx.Channel; - - _service.ServerFilteredWords.TryGetValue(channel.Guild.Id, out var fwHash); - - var fws = fwHash.ToArray(); - - await ctx.SendPaginatedConfirmAsync(page, - (curPage) => _eb.Create() - .WithTitle(GetText(strs.filter_word_list)) - .WithDescription(string.Join("\n", fws.Skip(curPage * 10).Take(10))) - .WithOkColor() - , fws.Length, 10).ConfigureAwait(false); + _service.InviteFilteringServers.TryRemove(channel.Guild.Id); + await ReplyConfirmLocalizedAsync(strs.invite_filter_server_off).ConfigureAwait(false); } } + + [NadekoCommand, Aliases] + [RequireContext(ContextType.Guild)] + public async Task ChnlFilterInv() + { + var channel = (ITextChannel)ctx.Channel; + + FilterChannelId removed; + using (var uow = _db.GetDbContext()) + { + var config = uow.GuildConfigsForId(channel.Guild.Id, set => set.Include(gc => gc.FilterInvitesChannelIds)); + var match = new FilterChannelId() + { + ChannelId = channel.Id + }; + removed = config.FilterInvitesChannelIds.FirstOrDefault(fc => fc.Equals(match)); + + if (removed is null) + { + config.FilterInvitesChannelIds.Add(match); + } + else + { + uow.Remove(removed); + } + await uow.SaveChangesAsync(); + } + + if (removed is null) + { + _service.InviteFilteringChannels.Add(channel.Id); + await ReplyConfirmLocalizedAsync(strs.invite_filter_channel_on).ConfigureAwait(false); + } + else + { + _service.InviteFilteringChannels.TryRemove(channel.Id); + await ReplyConfirmLocalizedAsync(strs.invite_filter_channel_off).ConfigureAwait(false); + } + } + + [NadekoCommand, Aliases] + [RequireContext(ContextType.Guild)] + public async Task SrvrFilterLin() + { + var channel = (ITextChannel)ctx.Channel; + + bool enabled; + using (var uow = _db.GetDbContext()) + { + var config = uow.GuildConfigsForId(channel.Guild.Id, set => set); + enabled = config.FilterLinks = !config.FilterLinks; + await uow.SaveChangesAsync(); + } + + if (enabled) + { + _service.LinkFilteringServers.Add(channel.Guild.Id); + await ReplyConfirmLocalizedAsync(strs.link_filter_server_on).ConfigureAwait(false); + } + else + { + _service.LinkFilteringServers.TryRemove(channel.Guild.Id); + await ReplyConfirmLocalizedAsync(strs.link_filter_server_off).ConfigureAwait(false); + } + } + + [NadekoCommand, Aliases] + [RequireContext(ContextType.Guild)] + public async Task ChnlFilterLin() + { + var channel = (ITextChannel)ctx.Channel; + + FilterLinksChannelId removed; + using (var uow = _db.GetDbContext()) + { + var config = uow.GuildConfigsForId(channel.Guild.Id, set => set.Include(gc => gc.FilterLinksChannelIds)); + var match = new FilterLinksChannelId() + { + ChannelId = channel.Id + }; + removed = config.FilterLinksChannelIds.FirstOrDefault(fc => fc.Equals(match)); + + if (removed is null) + { + config.FilterLinksChannelIds.Add(match); + } + else + { + uow.Remove(removed); + } + await uow.SaveChangesAsync(); + } + + if (removed is null) + { + _service.LinkFilteringChannels.Add(channel.Id); + await ReplyConfirmLocalizedAsync(strs.link_filter_channel_on).ConfigureAwait(false); + } + else + { + _service.LinkFilteringChannels.TryRemove(channel.Id); + await ReplyConfirmLocalizedAsync(strs.link_filter_channel_off).ConfigureAwait(false); + } + } + + [NadekoCommand, Aliases] + [RequireContext(ContextType.Guild)] + public async Task SrvrFilterWords() + { + var channel = (ITextChannel)ctx.Channel; + + bool enabled; + using (var uow = _db.GetDbContext()) + { + var config = uow.GuildConfigsForId(channel.Guild.Id, set => set); + enabled = config.FilterWords = !config.FilterWords; + await uow.SaveChangesAsync(); + } + + if (enabled) + { + _service.WordFilteringServers.Add(channel.Guild.Id); + await ReplyConfirmLocalizedAsync(strs.word_filter_server_on).ConfigureAwait(false); + } + else + { + _service.WordFilteringServers.TryRemove(channel.Guild.Id); + await ReplyConfirmLocalizedAsync(strs.word_filter_server_off).ConfigureAwait(false); + } + } + + [NadekoCommand, Aliases] + [RequireContext(ContextType.Guild)] + public async Task ChnlFilterWords() + { + var channel = (ITextChannel)ctx.Channel; + + FilterChannelId removed; + using (var uow = _db.GetDbContext()) + { + var config = uow.GuildConfigsForId(channel.Guild.Id, set => set.Include(gc => gc.FilterWordsChannelIds)); + + var match = new FilterChannelId() + { + ChannelId = channel.Id + }; + removed = config.FilterWordsChannelIds.FirstOrDefault(fc => fc.Equals(match)); + if (removed is null) + { + config.FilterWordsChannelIds.Add(match); + } + else + { + uow.Remove(removed); + } + await uow.SaveChangesAsync(); + } + + if (removed is null) + { + _service.WordFilteringChannels.Add(channel.Id); + await ReplyConfirmLocalizedAsync(strs.word_filter_channel_on).ConfigureAwait(false); + } + else + { + _service.WordFilteringChannels.TryRemove(channel.Id); + await ReplyConfirmLocalizedAsync(strs.word_filter_channel_off).ConfigureAwait(false); + } + } + + [NadekoCommand, Aliases] + [RequireContext(ContextType.Guild)] + public async Task FilterWord([Leftover] string word) + { + var channel = (ITextChannel)ctx.Channel; + + word = word?.Trim().ToLowerInvariant(); + + if (string.IsNullOrWhiteSpace(word)) + return; + + FilteredWord removed; + using (var uow = _db.GetDbContext()) + { + var config = uow.GuildConfigsForId(channel.Guild.Id, set => set.Include(gc => gc.FilteredWords)); + + removed = config.FilteredWords.FirstOrDefault(fw => fw.Word.Trim().ToLowerInvariant() == word); + + if (removed is null) + config.FilteredWords.Add(new FilteredWord() { Word = word }); + else + { + uow.Remove(removed); + } + + await uow.SaveChangesAsync(); + } + + var filteredWords = _service.ServerFilteredWords.GetOrAdd(channel.Guild.Id, new ConcurrentHashSet()); + + if (removed is null) + { + filteredWords.Add(word); + await ReplyConfirmLocalizedAsync(strs.filter_word_add(Format.Code(word))).ConfigureAwait(false); + } + else + { + filteredWords.TryRemove(word); + await ReplyConfirmLocalizedAsync(strs.filter_word_remove(Format.Code(word))).ConfigureAwait(false); + } + } + + [NadekoCommand, Aliases] + [RequireContext(ContextType.Guild)] + public async Task LstFilterWords(int page = 1) + { + page--; + if (page < 0) + return; + + var channel = (ITextChannel)ctx.Channel; + + _service.ServerFilteredWords.TryGetValue(channel.Guild.Id, out var fwHash); + + var fws = fwHash.ToArray(); + + await ctx.SendPaginatedConfirmAsync(page, + (curPage) => _eb.Create() + .WithTitle(GetText(strs.filter_word_list)) + .WithDescription(string.Join("\n", fws.Skip(curPage * 10).Take(10))) + .WithOkColor() + , fws.Length, 10).ConfigureAwait(false); + } } -} +} \ No newline at end of file diff --git a/src/NadekoBot/Modules/Permissions/GlobalPermissionCommands.cs b/src/NadekoBot/Modules/Permissions/GlobalPermissionCommands.cs index 01b60732c..18c8b4ea2 100644 --- a/src/NadekoBot/Modules/Permissions/GlobalPermissionCommands.cs +++ b/src/NadekoBot/Modules/Permissions/GlobalPermissionCommands.cs @@ -5,84 +5,82 @@ using NadekoBot.Common.TypeReaders; using NadekoBot.Services; using NadekoBot.Extensions; using NadekoBot.Modules.Permissions.Services; -using System.Linq; using System.Threading.Tasks; -namespace NadekoBot.Modules.Permissions +namespace NadekoBot.Modules.Permissions; + +public partial class Permissions { - public partial class Permissions + [Group] + public class GlobalPermissionCommands : NadekoSubmodule { - [Group] - public class GlobalPermissionCommands : NadekoSubmodule + private GlobalPermissionService _service; + private readonly DbService _db; + + public GlobalPermissionCommands(GlobalPermissionService service, DbService db) { - private GlobalPermissionService _service; - private readonly DbService _db; + _service = service; + _db = db; + } - public GlobalPermissionCommands(GlobalPermissionService service, DbService db) + [NadekoCommand, Aliases] + [OwnerOnly] + public async Task GlobalPermList() + { + var blockedModule = _service.BlockedModules; + var blockedCommands = _service.BlockedCommands; + if (!blockedModule.Any() && !blockedCommands.Any()) { - _service = service; - _db = db; + await ReplyErrorLocalizedAsync(strs.lgp_none).ConfigureAwait(false); + return; } - [NadekoCommand, Aliases] - [OwnerOnly] - public async Task GlobalPermList() + var embed = _eb.Create().WithOkColor(); + + if (blockedModule.Any()) + embed.AddField(GetText(strs.blocked_modules) + , string.Join("\n", _service.BlockedModules) + , false); + + if (blockedCommands.Any()) + embed.AddField(GetText(strs.blocked_commands) + , string.Join("\n", _service.BlockedCommands) + , false); + + await ctx.Channel.EmbedAsync(embed).ConfigureAwait(false); + } + + [NadekoCommand, Aliases] + [OwnerOnly] + public async Task GlobalModule(ModuleOrCrInfo module) + { + var moduleName = module.Name.ToLowerInvariant(); + + var added = _service.ToggleModule(moduleName); + + if (added) { - var blockedModule = _service.BlockedModules; - var blockedCommands = _service.BlockedCommands; - if (!blockedModule.Any() && !blockedCommands.Any()) - { - await ReplyErrorLocalizedAsync(strs.lgp_none).ConfigureAwait(false); - return; - } - - var embed = _eb.Create().WithOkColor(); - - if (blockedModule.Any()) - embed.AddField(GetText(strs.blocked_modules) - , string.Join("\n", _service.BlockedModules) - , false); - - if (blockedCommands.Any()) - embed.AddField(GetText(strs.blocked_commands) - , string.Join("\n", _service.BlockedCommands) - , false); - - await ctx.Channel.EmbedAsync(embed).ConfigureAwait(false); + await ReplyConfirmLocalizedAsync(strs.gmod_add(Format.Bold(module.Name))).ConfigureAwait(false); + return; } + + await ReplyConfirmLocalizedAsync(strs.gmod_remove(Format.Bold(module.Name))).ConfigureAwait(false); + } - [NadekoCommand, Aliases] - [OwnerOnly] - public async Task GlobalModule(ModuleOrCrInfo module) + [NadekoCommand, Aliases] + [OwnerOnly] + public async Task GlobalCommand(CommandOrCrInfo cmd) + { + var commandName = cmd.Name.ToLowerInvariant(); + var added = _service.ToggleCommand(commandName); + + if (added) { - var moduleName = module.Name.ToLowerInvariant(); - - var added = _service.ToggleModule(moduleName); - - if (added) - { - await ReplyConfirmLocalizedAsync(strs.gmod_add(Format.Bold(module.Name))).ConfigureAwait(false); - return; - } - - await ReplyConfirmLocalizedAsync(strs.gmod_remove(Format.Bold(module.Name))).ConfigureAwait(false); + await ReplyConfirmLocalizedAsync(strs.gcmd_add(Format.Bold(cmd.Name))).ConfigureAwait(false); + return; } - - [NadekoCommand, Aliases] - [OwnerOnly] - public async Task GlobalCommand(CommandOrCrInfo cmd) - { - var commandName = cmd.Name.ToLowerInvariant(); - var added = _service.ToggleCommand(commandName); - if (added) - { - await ReplyConfirmLocalizedAsync(strs.gcmd_add(Format.Bold(cmd.Name))).ConfigureAwait(false); - return; - } - - await ReplyConfirmLocalizedAsync(strs.gcmd_remove(Format.Bold(cmd.Name))).ConfigureAwait(false); - } + await ReplyConfirmLocalizedAsync(strs.gcmd_remove(Format.Bold(cmd.Name))).ConfigureAwait(false); } } -} +} \ No newline at end of file diff --git a/src/NadekoBot/Modules/Permissions/Permissions.cs b/src/NadekoBot/Modules/Permissions/Permissions.cs index 6e3c9ac91..a2e412459 100644 --- a/src/NadekoBot/Modules/Permissions/Permissions.cs +++ b/src/NadekoBot/Modules/Permissions/Permissions.cs @@ -1,576 +1,571 @@ -using System; -using System.Linq; -using System.Threading.Tasks; +using System.Threading.Tasks; using Discord.Commands; using NadekoBot.Services; using Discord; using NadekoBot.Services.Database.Models; -using System.Collections.Generic; using Discord.WebSocket; using NadekoBot.Common.Attributes; using NadekoBot.Common.TypeReaders; using NadekoBot.Common.TypeReaders.Models; using NadekoBot.Db; -using NadekoBot.Modules.Administration; using NadekoBot.Modules.Permissions.Common; using NadekoBot.Modules.Permissions.Services; -namespace NadekoBot.Modules.Permissions +namespace NadekoBot.Modules.Permissions; + +public partial class Permissions : NadekoModule { - public partial class Permissions : NadekoModule + private readonly DbService _db; + + public Permissions(DbService db) { - private readonly DbService _db; + _db = db; + } - public Permissions(DbService db) + [NadekoCommand, Aliases] + [RequireContext(ContextType.Guild)] + public async Task Verbose(PermissionAction action = null) + { + using (var uow = _db.GetDbContext()) { - _db = db; + var config = uow.GcWithPermissionsv2For(ctx.Guild.Id); + if (action is null) action = new PermissionAction(!config.VerbosePermissions); // New behaviour, can toggle. + config.VerbosePermissions = action.Value; + await uow.SaveChangesAsync(); + _service.UpdateCache(config); } - - [NadekoCommand, Aliases] - [RequireContext(ContextType.Guild)] - public async Task Verbose(PermissionAction action = null) + if (action.Value) { - using (var uow = _db.GetDbContext()) - { - var config = uow.GcWithPermissionsv2For(ctx.Guild.Id); - if (action is null) action = new PermissionAction(!config.VerbosePermissions); // New behaviour, can toggle. - config.VerbosePermissions = action.Value; - await uow.SaveChangesAsync(); - _service.UpdateCache(config); - } - if (action.Value) - { - await ReplyConfirmLocalizedAsync(strs.verbose_true).ConfigureAwait(false); - } - else - { - await ReplyConfirmLocalizedAsync(strs.verbose_false).ConfigureAwait(false); - } + await ReplyConfirmLocalizedAsync(strs.verbose_true).ConfigureAwait(false); } - - [NadekoCommand, Aliases] - [RequireContext(ContextType.Guild)] - [UserPerm(GuildPerm.Administrator)] - [Priority(0)] - public async Task PermRole([Leftover] IRole role = null) + else { - if (role != null && role == role.Guild.EveryoneRole) - return; + await ReplyConfirmLocalizedAsync(strs.verbose_false).ConfigureAwait(false); + } + } + + [NadekoCommand, Aliases] + [RequireContext(ContextType.Guild)] + [UserPerm(GuildPerm.Administrator)] + [Priority(0)] + public async Task PermRole([Leftover] IRole role = null) + { + if (role != null && role == role.Guild.EveryoneRole) + return; - if (role is null) + if (role is null) + { + var cache = _service.GetCacheFor(ctx.Guild.Id); + if (!ulong.TryParse(cache.PermRole, out var roleId) || + (role = ((SocketGuild)ctx.Guild).GetRole(roleId)) is null) { - var cache = _service.GetCacheFor(ctx.Guild.Id); - if (!ulong.TryParse(cache.PermRole, out var roleId) || - (role = ((SocketGuild)ctx.Guild).GetRole(roleId)) is null) - { - await ReplyConfirmLocalizedAsync(strs.permrole_not_set).ConfigureAwait(false); - } - else - { - await ReplyConfirmLocalizedAsync(strs.permrole(Format.Bold(role.ToString()))).ConfigureAwait(false); - } - return; + await ReplyConfirmLocalizedAsync(strs.permrole_not_set).ConfigureAwait(false); } - - using (var uow = _db.GetDbContext()) + else { - var config = uow.GcWithPermissionsv2For(ctx.Guild.Id); - config.PermissionRole = role.Id.ToString(); - uow.SaveChanges(); - _service.UpdateCache(config); + await ReplyConfirmLocalizedAsync(strs.permrole(Format.Bold(role.ToString()))).ConfigureAwait(false); } - - await ReplyConfirmLocalizedAsync(strs.permrole_changed(Format.Bold(role.Name))).ConfigureAwait(false); + return; } - public enum Reset { Reset }; - - [NadekoCommand, Aliases] - [RequireContext(ContextType.Guild)] - [UserPerm(GuildPerm.Administrator)] - [Priority(1)] - public async Task PermRole(Reset _) + using (var uow = _db.GetDbContext()) { + var config = uow.GcWithPermissionsv2For(ctx.Guild.Id); + config.PermissionRole = role.Id.ToString(); + uow.SaveChanges(); + _service.UpdateCache(config); + } + + await ReplyConfirmLocalizedAsync(strs.permrole_changed(Format.Bold(role.Name))).ConfigureAwait(false); + } + + public enum Reset { Reset }; + + [NadekoCommand, Aliases] + [RequireContext(ContextType.Guild)] + [UserPerm(GuildPerm.Administrator)] + [Priority(1)] + public async Task PermRole(Reset _) + { + using (var uow = _db.GetDbContext()) + { + var config = uow.GcWithPermissionsv2For(ctx.Guild.Id); + config.PermissionRole = null; + await uow.SaveChangesAsync(); + _service.UpdateCache(config); + } + + await ReplyConfirmLocalizedAsync(strs.permrole_reset).ConfigureAwait(false); + } + + [NadekoCommand, Aliases] + [RequireContext(ContextType.Guild)] + public async Task ListPerms(int page = 1) + { + if (page < 1) + return; + + IList perms; + + if (_service.Cache.TryGetValue(ctx.Guild.Id, out var permCache)) + { + perms = permCache.Permissions.Source.ToList(); + } + else + { + perms = Permissionv2.GetDefaultPermlist; + } + + var startPos = 20 * (page - 1); + var toSend = Format.Bold(GetText(strs.page(page))) + "\n\n" + string.Join("\n", + perms.Reverse() + .Skip(startPos) + .Take(20) + .Select(p => + { + var str = + $"`{p.Index + 1}.` {Format.Bold(p.GetCommand(Prefix, (SocketGuild)ctx.Guild))}"; + if (p.Index == 0) + str += $" [{GetText(strs.uneditable)}]"; + return str; + })); + + await ctx.Channel.SendMessageAsync(toSend).ConfigureAwait(false); + } + + [NadekoCommand, Aliases] + [RequireContext(ContextType.Guild)] + public async Task RemovePerm(int index) + { + index -= 1; + if (index < 0) + return; + try + { + Permissionv2 p; using (var uow = _db.GetDbContext()) { var config = uow.GcWithPermissionsv2For(ctx.Guild.Id); - config.PermissionRole = null; + var permsCol = new PermissionsCollection(config.Permissions); + p = permsCol[index]; + permsCol.RemoveAt(index); + uow.Remove(p); await uow.SaveChangesAsync(); _service.UpdateCache(config); } - await ReplyConfirmLocalizedAsync(strs.permrole_reset).ConfigureAwait(false); + await ReplyConfirmLocalizedAsync(strs.removed( + index + 1, + Format.Code(p.GetCommand(Prefix, (SocketGuild)ctx.Guild)))); } - - [NadekoCommand, Aliases] - [RequireContext(ContextType.Guild)] - public async Task ListPerms(int page = 1) + catch (IndexOutOfRangeException) { - if (page < 1) - return; - - IList perms; - - if (_service.Cache.TryGetValue(ctx.Guild.Id, out var permCache)) - { - perms = permCache.Permissions.Source.ToList(); - } - else - { - perms = Permissionv2.GetDefaultPermlist; - } - - var startPos = 20 * (page - 1); - var toSend = Format.Bold(GetText(strs.page(page))) + "\n\n" + string.Join("\n", - perms.Reverse() - .Skip(startPos) - .Take(20) - .Select(p => - { - var str = - $"`{p.Index + 1}.` {Format.Bold(p.GetCommand(Prefix, (SocketGuild)ctx.Guild))}"; - if (p.Index == 0) - str += $" [{GetText(strs.uneditable)}]"; - return str; - })); - - await ctx.Channel.SendMessageAsync(toSend).ConfigureAwait(false); + await ReplyErrorLocalizedAsync(strs.perm_out_of_range).ConfigureAwait(false); } + } - [NadekoCommand, Aliases] - [RequireContext(ContextType.Guild)] - public async Task RemovePerm(int index) + [NadekoCommand, Aliases] + [RequireContext(ContextType.Guild)] + public async Task MovePerm(int from, int to) + { + from -= 1; + to -= 1; + if (!(from == to || from < 0 || to < 0)) { - index -= 1; - if (index < 0) - return; try { - Permissionv2 p; + Permissionv2 fromPerm; using (var uow = _db.GetDbContext()) { var config = uow.GcWithPermissionsv2For(ctx.Guild.Id); var permsCol = new PermissionsCollection(config.Permissions); - p = permsCol[index]; - permsCol.RemoveAt(index); - uow.Remove(p); + + var fromFound = from < permsCol.Count; + var toFound = to < permsCol.Count; + + if (!fromFound) + { + await ReplyErrorLocalizedAsync(strs.perm_not_found(++from)); + return; + } + + if (!toFound) + { + await ReplyErrorLocalizedAsync(strs.perm_not_found(++to)); + return; + } + fromPerm = permsCol[from]; + + permsCol.RemoveAt(from); + permsCol.Insert(to, fromPerm); await uow.SaveChangesAsync(); _service.UpdateCache(config); } - await ReplyConfirmLocalizedAsync(strs.removed( - index + 1, - Format.Code(p.GetCommand(Prefix, (SocketGuild)ctx.Guild)))); - } - catch (IndexOutOfRangeException) - { - await ReplyErrorLocalizedAsync(strs.perm_out_of_range).ConfigureAwait(false); - } - } - - [NadekoCommand, Aliases] - [RequireContext(ContextType.Guild)] - public async Task MovePerm(int from, int to) - { - from -= 1; - to -= 1; - if (!(from == to || from < 0 || to < 0)) - { - try - { - Permissionv2 fromPerm; - using (var uow = _db.GetDbContext()) - { - var config = uow.GcWithPermissionsv2For(ctx.Guild.Id); - var permsCol = new PermissionsCollection(config.Permissions); - - var fromFound = from < permsCol.Count; - var toFound = to < permsCol.Count; - - if (!fromFound) - { - await ReplyErrorLocalizedAsync(strs.perm_not_found(++from)); - return; - } - - if (!toFound) - { - await ReplyErrorLocalizedAsync(strs.perm_not_found(++to)); - return; - } - fromPerm = permsCol[from]; - - permsCol.RemoveAt(from); - permsCol.Insert(to, fromPerm); - await uow.SaveChangesAsync(); - _service.UpdateCache(config); - } - - await ReplyConfirmLocalizedAsync(strs.moved_permission( - Format.Code(fromPerm.GetCommand(Prefix, (SocketGuild)ctx.Guild)), - ++from, - ++to)); + await ReplyConfirmLocalizedAsync(strs.moved_permission( + Format.Code(fromPerm.GetCommand(Prefix, (SocketGuild)ctx.Guild)), + ++from, + ++to)); - return; - } - catch (Exception e) when (e is ArgumentOutOfRangeException || e is IndexOutOfRangeException) - { - } - } - await ReplyConfirmLocalizedAsync(strs.perm_out_of_range).ConfigureAwait(false); - } - - [NadekoCommand, Aliases] - [RequireContext(ContextType.Guild)] - public async Task SrvrCmd(CommandOrCrInfo command, PermissionAction action) - { - await _service.AddPermissions(ctx.Guild.Id, new Permissionv2 - { - PrimaryTarget = PrimaryPermissionType.Server, - PrimaryTargetId = 0, - SecondaryTarget = SecondaryPermissionType.Command, - SecondaryTargetName = command.Name.ToLowerInvariant(), - State = action.Value, - IsCustomCommand = command.IsCustom, - }).ConfigureAwait(false); - - if (action.Value) - { - await ReplyConfirmLocalizedAsync(strs.sx_enable( - Format.Code(command.Name), - GetText(strs.of_command))); - } - else - { - await ReplyConfirmLocalizedAsync(strs.sx_disable( - Format.Code(command.Name), - GetText(strs.of_command))); - } - } - - [NadekoCommand, Aliases] - [RequireContext(ContextType.Guild)] - public async Task SrvrMdl(ModuleOrCrInfo module, PermissionAction action) - { - await _service.AddPermissions(ctx.Guild.Id, new Permissionv2 - { - PrimaryTarget = PrimaryPermissionType.Server, - PrimaryTargetId = 0, - SecondaryTarget = SecondaryPermissionType.Module, - SecondaryTargetName = module.Name.ToLowerInvariant(), - State = action.Value, - }).ConfigureAwait(false); - - if (action.Value) - { - await ReplyConfirmLocalizedAsync(strs.sx_enable( - Format.Code(module.Name), - GetText(strs.of_module))); - } - else - { - await ReplyConfirmLocalizedAsync(strs.sx_disable( - Format.Code(module.Name), - GetText(strs.of_module))); - } - } - - [NadekoCommand, Aliases] - [RequireContext(ContextType.Guild)] - public async Task UsrCmd(CommandOrCrInfo command, PermissionAction action, [Leftover] IGuildUser user) - { - await _service.AddPermissions(ctx.Guild.Id, new Permissionv2 - { - PrimaryTarget = PrimaryPermissionType.User, - PrimaryTargetId = user.Id, - SecondaryTarget = SecondaryPermissionType.Command, - SecondaryTargetName = command.Name.ToLowerInvariant(), - State = action.Value, - IsCustomCommand = command.IsCustom, - }).ConfigureAwait(false); - - if (action.Value) - { - await ReplyConfirmLocalizedAsync(strs.ux_enable( - Format.Code(command.Name), - GetText(strs.of_command), - Format.Code(user.ToString()))); - } - else - { - await ReplyConfirmLocalizedAsync(strs.ux_disable( - Format.Code(command.Name), - GetText(strs.of_command), - Format.Code(user.ToString()))); - } - } - - [NadekoCommand, Aliases] - [RequireContext(ContextType.Guild)] - public async Task UsrMdl(ModuleOrCrInfo module, PermissionAction action, [Leftover] IGuildUser user) - { - await _service.AddPermissions(ctx.Guild.Id, new Permissionv2 - { - PrimaryTarget = PrimaryPermissionType.User, - PrimaryTargetId = user.Id, - SecondaryTarget = SecondaryPermissionType.Module, - SecondaryTargetName = module.Name.ToLowerInvariant(), - State = action.Value, - }).ConfigureAwait(false); - - if (action.Value) - { - await ReplyConfirmLocalizedAsync(strs.ux_enable( - Format.Code(module.Name), - GetText(strs.of_module), - Format.Code(user.ToString()))); - } - else - { - await ReplyConfirmLocalizedAsync(strs.ux_disable( - Format.Code(module.Name), - GetText(strs.of_module), - Format.Code(user.ToString()))); - } - } - - [NadekoCommand, Aliases] - [RequireContext(ContextType.Guild)] - public async Task RoleCmd(CommandOrCrInfo command, PermissionAction action, [Leftover] IRole role) - { - if (role == role.Guild.EveryoneRole) return; - - await _service.AddPermissions(ctx.Guild.Id, new Permissionv2 - { - PrimaryTarget = PrimaryPermissionType.Role, - PrimaryTargetId = role.Id, - SecondaryTarget = SecondaryPermissionType.Command, - SecondaryTargetName = command.Name.ToLowerInvariant(), - State = action.Value, - IsCustomCommand = command.IsCustom, - }).ConfigureAwait(false); - - if (action.Value) - { - await ReplyConfirmLocalizedAsync(strs.rx_enable( - Format.Code(command.Name), - GetText(strs.of_command), - Format.Code(role.Name))); } - else + catch (Exception e) when (e is ArgumentOutOfRangeException || e is IndexOutOfRangeException) { - await ReplyConfirmLocalizedAsync(strs.rx_disable( - Format.Code(command.Name), - GetText(strs.of_command), - Format.Code(role.Name))); } } + await ReplyConfirmLocalizedAsync(strs.perm_out_of_range).ConfigureAwait(false); + } - [NadekoCommand, Aliases] - [RequireContext(ContextType.Guild)] - public async Task RoleMdl(ModuleOrCrInfo module, PermissionAction action, [Leftover] IRole role) + [NadekoCommand, Aliases] + [RequireContext(ContextType.Guild)] + public async Task SrvrCmd(CommandOrCrInfo command, PermissionAction action) + { + await _service.AddPermissions(ctx.Guild.Id, new Permissionv2 { - if (role == role.Guild.EveryoneRole) - return; + PrimaryTarget = PrimaryPermissionType.Server, + PrimaryTargetId = 0, + SecondaryTarget = SecondaryPermissionType.Command, + SecondaryTargetName = command.Name.ToLowerInvariant(), + State = action.Value, + IsCustomCommand = command.IsCustom, + }).ConfigureAwait(false); - await _service.AddPermissions(ctx.Guild.Id, new Permissionv2 - { - PrimaryTarget = PrimaryPermissionType.Role, - PrimaryTargetId = role.Id, - SecondaryTarget = SecondaryPermissionType.Module, - SecondaryTargetName = module.Name.ToLowerInvariant(), - State = action.Value, - }).ConfigureAwait(false); - - - if (action.Value) - { - await ReplyConfirmLocalizedAsync(strs.rx_enable( - Format.Code(module.Name), - GetText(strs.of_module), - Format.Code(role.Name))); - } - else - { - await ReplyConfirmLocalizedAsync(strs.rx_disable( - Format.Code(module.Name), - GetText(strs.of_module), - Format.Code(role.Name))); - } + if (action.Value) + { + await ReplyConfirmLocalizedAsync(strs.sx_enable( + Format.Code(command.Name), + GetText(strs.of_command))); } - - [NadekoCommand, Aliases] - [RequireContext(ContextType.Guild)] - public async Task ChnlCmd(CommandOrCrInfo command, PermissionAction action, [Leftover] ITextChannel chnl) + else { - await _service.AddPermissions(ctx.Guild.Id, new Permissionv2 - { - PrimaryTarget = PrimaryPermissionType.Channel, - PrimaryTargetId = chnl.Id, - SecondaryTarget = SecondaryPermissionType.Command, - SecondaryTargetName = command.Name.ToLowerInvariant(), - State = action.Value, - IsCustomCommand = command.IsCustom, - }).ConfigureAwait(false); - - if (action.Value) - { - await ReplyConfirmLocalizedAsync(strs.cx_enable( - Format.Code(command.Name), - GetText(strs.of_command), - Format.Code(chnl.Name))); - } - else - { - await ReplyConfirmLocalizedAsync(strs.cx_disable( - Format.Code(command.Name), - GetText(strs.of_command), - Format.Code(chnl.Name))); - } - } - - [NadekoCommand, Aliases] - [RequireContext(ContextType.Guild)] - public async Task ChnlMdl(ModuleOrCrInfo module, PermissionAction action, [Leftover] ITextChannel chnl) - { - await _service.AddPermissions(ctx.Guild.Id, new Permissionv2 - { - PrimaryTarget = PrimaryPermissionType.Channel, - PrimaryTargetId = chnl.Id, - SecondaryTarget = SecondaryPermissionType.Module, - SecondaryTargetName = module.Name.ToLowerInvariant(), - State = action.Value, - }).ConfigureAwait(false); - - if (action.Value) - { - await ReplyConfirmLocalizedAsync(strs.cx_enable( - Format.Code(module.Name), - GetText(strs.of_module), - Format.Code(chnl.Name))); - } - else - { - await ReplyConfirmLocalizedAsync(strs.cx_disable( - Format.Code(module.Name), - GetText(strs.of_module), - Format.Code(chnl.Name))); - } - } - - [NadekoCommand, Aliases] - [RequireContext(ContextType.Guild)] - public async Task AllChnlMdls(PermissionAction action, [Leftover] ITextChannel chnl) - { - await _service.AddPermissions(ctx.Guild.Id, new Permissionv2 - { - PrimaryTarget = PrimaryPermissionType.Channel, - PrimaryTargetId = chnl.Id, - SecondaryTarget = SecondaryPermissionType.AllModules, - SecondaryTargetName = "*", - State = action.Value, - }).ConfigureAwait(false); - - if (action.Value) - { - await ReplyConfirmLocalizedAsync(strs.acm_enable( - Format.Code(chnl.Name))); - } - else - { - await ReplyConfirmLocalizedAsync(strs.acm_disable( - Format.Code(chnl.Name))); - } - } - - [NadekoCommand, Aliases] - [RequireContext(ContextType.Guild)] - public async Task AllRoleMdls(PermissionAction action, [Leftover] IRole role) - { - if (role == role.Guild.EveryoneRole) - return; - - await _service.AddPermissions(ctx.Guild.Id, new Permissionv2 - { - PrimaryTarget = PrimaryPermissionType.Role, - PrimaryTargetId = role.Id, - SecondaryTarget = SecondaryPermissionType.AllModules, - SecondaryTargetName = "*", - State = action.Value, - }).ConfigureAwait(false); - - if (action.Value) - { - await ReplyConfirmLocalizedAsync(strs.arm_enable( - Format.Code(role.Name))); - } - else - { - await ReplyConfirmLocalizedAsync(strs.arm_disable( - Format.Code(role.Name))); - } - } - - [NadekoCommand, Aliases] - [RequireContext(ContextType.Guild)] - public async Task AllUsrMdls(PermissionAction action, [Leftover] IUser user) - { - await _service.AddPermissions(ctx.Guild.Id, new Permissionv2 - { - PrimaryTarget = PrimaryPermissionType.User, - PrimaryTargetId = user.Id, - SecondaryTarget = SecondaryPermissionType.AllModules, - SecondaryTargetName = "*", - State = action.Value, - }).ConfigureAwait(false); - - if (action.Value) - { - await ReplyConfirmLocalizedAsync(strs.aum_enable( - Format.Code(user.ToString()))); - } - else - { - await ReplyConfirmLocalizedAsync(strs.aum_disable( - Format.Code(user.ToString()))); - } - } - - [NadekoCommand, Aliases] - [RequireContext(ContextType.Guild)] - public async Task AllSrvrMdls(PermissionAction action) - { - var newPerm = new Permissionv2 - { - PrimaryTarget = PrimaryPermissionType.Server, - PrimaryTargetId = 0, - SecondaryTarget = SecondaryPermissionType.AllModules, - SecondaryTargetName = "*", - State = action.Value, - }; - - var allowUser = new Permissionv2 - { - PrimaryTarget = PrimaryPermissionType.User, - PrimaryTargetId = ctx.User.Id, - SecondaryTarget = SecondaryPermissionType.AllModules, - SecondaryTargetName = "*", - State = true, - }; - - await _service.AddPermissions(ctx.Guild.Id, - newPerm, - allowUser).ConfigureAwait(false); - - if (action.Value) - { - await ReplyConfirmLocalizedAsync(strs.asm_enable).ConfigureAwait(false); - } - else - { - await ReplyConfirmLocalizedAsync(strs.asm_disable).ConfigureAwait(false); - } + await ReplyConfirmLocalizedAsync(strs.sx_disable( + Format.Code(command.Name), + GetText(strs.of_command))); } } -} + + [NadekoCommand, Aliases] + [RequireContext(ContextType.Guild)] + public async Task SrvrMdl(ModuleOrCrInfo module, PermissionAction action) + { + await _service.AddPermissions(ctx.Guild.Id, new Permissionv2 + { + PrimaryTarget = PrimaryPermissionType.Server, + PrimaryTargetId = 0, + SecondaryTarget = SecondaryPermissionType.Module, + SecondaryTargetName = module.Name.ToLowerInvariant(), + State = action.Value, + }).ConfigureAwait(false); + + if (action.Value) + { + await ReplyConfirmLocalizedAsync(strs.sx_enable( + Format.Code(module.Name), + GetText(strs.of_module))); + } + else + { + await ReplyConfirmLocalizedAsync(strs.sx_disable( + Format.Code(module.Name), + GetText(strs.of_module))); + } + } + + [NadekoCommand, Aliases] + [RequireContext(ContextType.Guild)] + public async Task UsrCmd(CommandOrCrInfo command, PermissionAction action, [Leftover] IGuildUser user) + { + await _service.AddPermissions(ctx.Guild.Id, new Permissionv2 + { + PrimaryTarget = PrimaryPermissionType.User, + PrimaryTargetId = user.Id, + SecondaryTarget = SecondaryPermissionType.Command, + SecondaryTargetName = command.Name.ToLowerInvariant(), + State = action.Value, + IsCustomCommand = command.IsCustom, + }).ConfigureAwait(false); + + if (action.Value) + { + await ReplyConfirmLocalizedAsync(strs.ux_enable( + Format.Code(command.Name), + GetText(strs.of_command), + Format.Code(user.ToString()))); + } + else + { + await ReplyConfirmLocalizedAsync(strs.ux_disable( + Format.Code(command.Name), + GetText(strs.of_command), + Format.Code(user.ToString()))); + } + } + + [NadekoCommand, Aliases] + [RequireContext(ContextType.Guild)] + public async Task UsrMdl(ModuleOrCrInfo module, PermissionAction action, [Leftover] IGuildUser user) + { + await _service.AddPermissions(ctx.Guild.Id, new Permissionv2 + { + PrimaryTarget = PrimaryPermissionType.User, + PrimaryTargetId = user.Id, + SecondaryTarget = SecondaryPermissionType.Module, + SecondaryTargetName = module.Name.ToLowerInvariant(), + State = action.Value, + }).ConfigureAwait(false); + + if (action.Value) + { + await ReplyConfirmLocalizedAsync(strs.ux_enable( + Format.Code(module.Name), + GetText(strs.of_module), + Format.Code(user.ToString()))); + } + else + { + await ReplyConfirmLocalizedAsync(strs.ux_disable( + Format.Code(module.Name), + GetText(strs.of_module), + Format.Code(user.ToString()))); + } + } + + [NadekoCommand, Aliases] + [RequireContext(ContextType.Guild)] + public async Task RoleCmd(CommandOrCrInfo command, PermissionAction action, [Leftover] IRole role) + { + if (role == role.Guild.EveryoneRole) + return; + + await _service.AddPermissions(ctx.Guild.Id, new Permissionv2 + { + PrimaryTarget = PrimaryPermissionType.Role, + PrimaryTargetId = role.Id, + SecondaryTarget = SecondaryPermissionType.Command, + SecondaryTargetName = command.Name.ToLowerInvariant(), + State = action.Value, + IsCustomCommand = command.IsCustom, + }).ConfigureAwait(false); + + if (action.Value) + { + await ReplyConfirmLocalizedAsync(strs.rx_enable( + Format.Code(command.Name), + GetText(strs.of_command), + Format.Code(role.Name))); + } + else + { + await ReplyConfirmLocalizedAsync(strs.rx_disable( + Format.Code(command.Name), + GetText(strs.of_command), + Format.Code(role.Name))); + } + } + + [NadekoCommand, Aliases] + [RequireContext(ContextType.Guild)] + public async Task RoleMdl(ModuleOrCrInfo module, PermissionAction action, [Leftover] IRole role) + { + if (role == role.Guild.EveryoneRole) + return; + + await _service.AddPermissions(ctx.Guild.Id, new Permissionv2 + { + PrimaryTarget = PrimaryPermissionType.Role, + PrimaryTargetId = role.Id, + SecondaryTarget = SecondaryPermissionType.Module, + SecondaryTargetName = module.Name.ToLowerInvariant(), + State = action.Value, + }).ConfigureAwait(false); + + + if (action.Value) + { + await ReplyConfirmLocalizedAsync(strs.rx_enable( + Format.Code(module.Name), + GetText(strs.of_module), + Format.Code(role.Name))); + } + else + { + await ReplyConfirmLocalizedAsync(strs.rx_disable( + Format.Code(module.Name), + GetText(strs.of_module), + Format.Code(role.Name))); + } + } + + [NadekoCommand, Aliases] + [RequireContext(ContextType.Guild)] + public async Task ChnlCmd(CommandOrCrInfo command, PermissionAction action, [Leftover] ITextChannel chnl) + { + await _service.AddPermissions(ctx.Guild.Id, new Permissionv2 + { + PrimaryTarget = PrimaryPermissionType.Channel, + PrimaryTargetId = chnl.Id, + SecondaryTarget = SecondaryPermissionType.Command, + SecondaryTargetName = command.Name.ToLowerInvariant(), + State = action.Value, + IsCustomCommand = command.IsCustom, + }).ConfigureAwait(false); + + if (action.Value) + { + await ReplyConfirmLocalizedAsync(strs.cx_enable( + Format.Code(command.Name), + GetText(strs.of_command), + Format.Code(chnl.Name))); + } + else + { + await ReplyConfirmLocalizedAsync(strs.cx_disable( + Format.Code(command.Name), + GetText(strs.of_command), + Format.Code(chnl.Name))); + } + } + + [NadekoCommand, Aliases] + [RequireContext(ContextType.Guild)] + public async Task ChnlMdl(ModuleOrCrInfo module, PermissionAction action, [Leftover] ITextChannel chnl) + { + await _service.AddPermissions(ctx.Guild.Id, new Permissionv2 + { + PrimaryTarget = PrimaryPermissionType.Channel, + PrimaryTargetId = chnl.Id, + SecondaryTarget = SecondaryPermissionType.Module, + SecondaryTargetName = module.Name.ToLowerInvariant(), + State = action.Value, + }).ConfigureAwait(false); + + if (action.Value) + { + await ReplyConfirmLocalizedAsync(strs.cx_enable( + Format.Code(module.Name), + GetText(strs.of_module), + Format.Code(chnl.Name))); + } + else + { + await ReplyConfirmLocalizedAsync(strs.cx_disable( + Format.Code(module.Name), + GetText(strs.of_module), + Format.Code(chnl.Name))); + } + } + + [NadekoCommand, Aliases] + [RequireContext(ContextType.Guild)] + public async Task AllChnlMdls(PermissionAction action, [Leftover] ITextChannel chnl) + { + await _service.AddPermissions(ctx.Guild.Id, new Permissionv2 + { + PrimaryTarget = PrimaryPermissionType.Channel, + PrimaryTargetId = chnl.Id, + SecondaryTarget = SecondaryPermissionType.AllModules, + SecondaryTargetName = "*", + State = action.Value, + }).ConfigureAwait(false); + + if (action.Value) + { + await ReplyConfirmLocalizedAsync(strs.acm_enable( + Format.Code(chnl.Name))); + } + else + { + await ReplyConfirmLocalizedAsync(strs.acm_disable( + Format.Code(chnl.Name))); + } + } + + [NadekoCommand, Aliases] + [RequireContext(ContextType.Guild)] + public async Task AllRoleMdls(PermissionAction action, [Leftover] IRole role) + { + if (role == role.Guild.EveryoneRole) + return; + + await _service.AddPermissions(ctx.Guild.Id, new Permissionv2 + { + PrimaryTarget = PrimaryPermissionType.Role, + PrimaryTargetId = role.Id, + SecondaryTarget = SecondaryPermissionType.AllModules, + SecondaryTargetName = "*", + State = action.Value, + }).ConfigureAwait(false); + + if (action.Value) + { + await ReplyConfirmLocalizedAsync(strs.arm_enable( + Format.Code(role.Name))); + } + else + { + await ReplyConfirmLocalizedAsync(strs.arm_disable( + Format.Code(role.Name))); + } + } + + [NadekoCommand, Aliases] + [RequireContext(ContextType.Guild)] + public async Task AllUsrMdls(PermissionAction action, [Leftover] IUser user) + { + await _service.AddPermissions(ctx.Guild.Id, new Permissionv2 + { + PrimaryTarget = PrimaryPermissionType.User, + PrimaryTargetId = user.Id, + SecondaryTarget = SecondaryPermissionType.AllModules, + SecondaryTargetName = "*", + State = action.Value, + }).ConfigureAwait(false); + + if (action.Value) + { + await ReplyConfirmLocalizedAsync(strs.aum_enable( + Format.Code(user.ToString()))); + } + else + { + await ReplyConfirmLocalizedAsync(strs.aum_disable( + Format.Code(user.ToString()))); + } + } + + [NadekoCommand, Aliases] + [RequireContext(ContextType.Guild)] + public async Task AllSrvrMdls(PermissionAction action) + { + var newPerm = new Permissionv2 + { + PrimaryTarget = PrimaryPermissionType.Server, + PrimaryTargetId = 0, + SecondaryTarget = SecondaryPermissionType.AllModules, + SecondaryTargetName = "*", + State = action.Value, + }; + + var allowUser = new Permissionv2 + { + PrimaryTarget = PrimaryPermissionType.User, + PrimaryTargetId = ctx.User.Id, + SecondaryTarget = SecondaryPermissionType.AllModules, + SecondaryTargetName = "*", + State = true, + }; + + await _service.AddPermissions(ctx.Guild.Id, + newPerm, + allowUser).ConfigureAwait(false); + + if (action.Value) + { + await ReplyConfirmLocalizedAsync(strs.asm_enable).ConfigureAwait(false); + } + else + { + await ReplyConfirmLocalizedAsync(strs.asm_disable).ConfigureAwait(false); + } + } +} \ No newline at end of file diff --git a/src/NadekoBot/Modules/Permissions/ResetPermissionsCommands.cs b/src/NadekoBot/Modules/Permissions/ResetPermissionsCommands.cs index 3837545a6..f58a5937c 100644 --- a/src/NadekoBot/Modules/Permissions/ResetPermissionsCommands.cs +++ b/src/NadekoBot/Modules/Permissions/ResetPermissionsCommands.cs @@ -4,38 +4,37 @@ using System.Threading.Tasks; using NadekoBot.Common.Attributes; using NadekoBot.Modules.Permissions.Services; -namespace NadekoBot.Modules.Permissions +namespace NadekoBot.Modules.Permissions; + +public partial class Permissions { - public partial class Permissions + [Group] + public class ResetPermissionsCommands : NadekoSubmodule { - [Group] - public class ResetPermissionsCommands : NadekoSubmodule + private readonly GlobalPermissionService _gps; + private readonly PermissionService _perms; + + public ResetPermissionsCommands(GlobalPermissionService gps, PermissionService perms) { - private readonly GlobalPermissionService _gps; - private readonly PermissionService _perms; + _gps = gps; + _perms = perms; + } - public ResetPermissionsCommands(GlobalPermissionService gps, PermissionService perms) - { - _gps = gps; - _perms = perms; - } + [NadekoCommand, Aliases] + [RequireContext(ContextType.Guild)] + [UserPerm(GuildPerm.Administrator)] + public async Task ResetPerms() + { + await _perms.Reset(ctx.Guild.Id).ConfigureAwait(false); + await ReplyConfirmLocalizedAsync(strs.perms_reset).ConfigureAwait(false); + } - [NadekoCommand, Aliases] - [RequireContext(ContextType.Guild)] - [UserPerm(GuildPerm.Administrator)] - public async Task ResetPerms() - { - await _perms.Reset(ctx.Guild.Id).ConfigureAwait(false); - await ReplyConfirmLocalizedAsync(strs.perms_reset).ConfigureAwait(false); - } - - [NadekoCommand, Aliases] - [OwnerOnly] - public async Task ResetGlobalPerms() - { - await _gps.Reset(); - await ReplyConfirmLocalizedAsync(strs.global_perms_reset).ConfigureAwait(false); - } + [NadekoCommand, Aliases] + [OwnerOnly] + public async Task ResetGlobalPerms() + { + await _gps.Reset(); + await ReplyConfirmLocalizedAsync(strs.global_perms_reset).ConfigureAwait(false); } } -} +} \ No newline at end of file diff --git a/src/NadekoBot/Modules/Permissions/Services/BlacklistService.cs b/src/NadekoBot/Modules/Permissions/Services/BlacklistService.cs index 4a3093324..9256bb40b 100644 --- a/src/NadekoBot/Modules/Permissions/Services/BlacklistService.cs +++ b/src/NadekoBot/Modules/Permissions/Services/BlacklistService.cs @@ -1,6 +1,4 @@ -using System.Collections.Generic; -using System.Linq; -using Discord; +using Discord; using NadekoBot.Common.ModuleBehaviors; using NadekoBot.Services; using NadekoBot.Services.Database.Models; @@ -8,130 +6,128 @@ using System.Threading.Tasks; using Microsoft.EntityFrameworkCore; using NadekoBot.Common; using NadekoBot.Db; -using Serilog; -namespace NadekoBot.Modules.Permissions.Services +namespace NadekoBot.Modules.Permissions.Services; + +public sealed class BlacklistService : IEarlyBehavior { - public sealed class BlacklistService : IEarlyBehavior + private readonly DbService _db; + private readonly IPubSub _pubSub; + private readonly IBotCredentials _creds; + private IReadOnlyList _blacklist; + public int Priority => int.MaxValue; + + private readonly TypedKey blPubKey = new TypedKey("blacklist.reload"); + public BlacklistService(DbService db, IPubSub pubSub, IBotCredentials creds) { - private readonly DbService _db; - private readonly IPubSub _pubSub; - private readonly IBotCredentials _creds; - private IReadOnlyList _blacklist; - public int Priority => int.MaxValue; + _db = db; + _pubSub = pubSub; + _creds = creds; - private readonly TypedKey blPubKey = new TypedKey("blacklist.reload"); - public BlacklistService(DbService db, IPubSub pubSub, IBotCredentials creds) + Reload(false); + _pubSub.Sub(blPubKey, OnReload); + } + + private ValueTask OnReload(BlacklistEntry[] blacklist) + { + _blacklist = blacklist; + return default; + } + + public Task RunBehavior(IGuild guild, IUserMessage usrMsg) + { + foreach (var bl in _blacklist) { - _db = db; - _pubSub = pubSub; - _creds = creds; - - Reload(false); - _pubSub.Sub(blPubKey, OnReload); - } - - private ValueTask OnReload(BlacklistEntry[] blacklist) - { - _blacklist = blacklist; - return default; - } - - public Task RunBehavior(IGuild guild, IUserMessage usrMsg) - { - foreach (var bl in _blacklist) + if (guild != null && bl.Type == BlacklistType.Server && bl.ItemId == guild.Id) { - if (guild != null && bl.Type == BlacklistType.Server && bl.ItemId == guild.Id) - { - Log.Information("Blocked input from blacklisted guild: {GuildName} [{GuildId}]", - guild.Name, - guild.Id); + Log.Information("Blocked input from blacklisted guild: {GuildName} [{GuildId}]", + guild.Name, + guild.Id); - return Task.FromResult(true); - } - - if (bl.Type == BlacklistType.Channel && bl.ItemId == usrMsg.Channel.Id) - { - Log.Information("Blocked input from blacklisted channel: {ChannelName} [{ChannelId}]", - usrMsg.Channel.Name, - usrMsg.Channel.Id); - - return Task.FromResult(true); - } - - if (bl.Type == BlacklistType.User && bl.ItemId == usrMsg.Author.Id) - { - Log.Information("Blocked input from blacklisted user: {UserName} [{UserId}]", - usrMsg.Author.ToString(), - usrMsg.Author.Id); - - return Task.FromResult(true); - } + return Task.FromResult(true); } - return Task.FromResult(false); - } - - public IReadOnlyList GetBlacklist() - => _blacklist; - - public void Reload(bool publish = true) - { - using var uow = _db.GetDbContext(); - var toPublish = uow.Blacklist.AsNoTracking().ToArray(); - _blacklist = toPublish; - if (publish) + if (bl.Type == BlacklistType.Channel && bl.ItemId == usrMsg.Channel.Id) { - _pubSub.Pub(blPubKey, toPublish); + Log.Information("Blocked input from blacklisted channel: {ChannelName} [{ChannelId}]", + usrMsg.Channel.Name, + usrMsg.Channel.Id); + + return Task.FromResult(true); + } + + if (bl.Type == BlacklistType.User && bl.ItemId == usrMsg.Author.Id) + { + Log.Information("Blocked input from blacklisted user: {UserName} [{UserId}]", + usrMsg.Author.ToString(), + usrMsg.Author.Id); + + return Task.FromResult(true); } } - public void Blacklist(BlacklistType type, ulong id) + return Task.FromResult(false); + } + + public IReadOnlyList GetBlacklist() + => _blacklist; + + public void Reload(bool publish = true) + { + using var uow = _db.GetDbContext(); + var toPublish = uow.Blacklist.AsNoTracking().ToArray(); + _blacklist = toPublish; + if (publish) { - if (_creds.OwnerIds.Contains(id)) - return; - - using var uow = _db.GetDbContext(); - var item = new BlacklistEntry { ItemId = id, Type = type }; - uow.Blacklist.Add(item); - uow.SaveChanges(); - - Reload(true); - } - - public void UnBlacklist(BlacklistType type, ulong id) - { - using var uow = _db.GetDbContext(); - var toRemove = uow.Blacklist - .FirstOrDefault(bi => bi.ItemId == id && bi.Type == type); - - if (toRemove is not null) - uow.Blacklist.Remove(toRemove); - - uow.SaveChanges(); - - Reload(true); - } - - public void BlacklistUsers(IReadOnlyCollection toBlacklist) - { - using (var uow = _db.GetDbContext()) - { - var bc = uow.Blacklist; - //blacklist the users - bc.AddRange(toBlacklist.Select(x => - new BlacklistEntry - { - ItemId = x, - Type = BlacklistType.User, - })); - - //clear their currencies - uow.DiscordUser.RemoveFromMany(toBlacklist); - uow.SaveChanges(); - } - - Reload(true); + _pubSub.Pub(blPubKey, toPublish); } } -} + + public void Blacklist(BlacklistType type, ulong id) + { + if (_creds.OwnerIds.Contains(id)) + return; + + using var uow = _db.GetDbContext(); + var item = new BlacklistEntry { ItemId = id, Type = type }; + uow.Blacklist.Add(item); + uow.SaveChanges(); + + Reload(true); + } + + public void UnBlacklist(BlacklistType type, ulong id) + { + using var uow = _db.GetDbContext(); + var toRemove = uow.Blacklist + .FirstOrDefault(bi => bi.ItemId == id && bi.Type == type); + + if (toRemove is not null) + uow.Blacklist.Remove(toRemove); + + uow.SaveChanges(); + + Reload(true); + } + + public void BlacklistUsers(IReadOnlyCollection toBlacklist) + { + using (var uow = _db.GetDbContext()) + { + var bc = uow.Blacklist; + //blacklist the users + bc.AddRange(toBlacklist.Select(x => + new BlacklistEntry + { + ItemId = x, + Type = BlacklistType.User, + })); + + //clear their currencies + uow.DiscordUser.RemoveFromMany(toBlacklist); + uow.SaveChanges(); + } + + Reload(true); + } +} \ No newline at end of file diff --git a/src/NadekoBot/Modules/Permissions/Services/CmdCdService.cs b/src/NadekoBot/Modules/Permissions/Services/CmdCdService.cs index e6fc44b80..ea3a168d6 100644 --- a/src/NadekoBot/Modules/Permissions/Services/CmdCdService.cs +++ b/src/NadekoBot/Modules/Permissions/Services/CmdCdService.cs @@ -1,81 +1,78 @@ using System.Collections.Concurrent; -using System.Linq; using System.Threading.Tasks; using Discord; using Discord.Commands; -using Discord.WebSocket; using NadekoBot.Common.Collections; using NadekoBot.Common.ModuleBehaviors; using NadekoBot.Services; using NadekoBot.Services.Database.Models; -namespace NadekoBot.Modules.Permissions.Services +namespace NadekoBot.Modules.Permissions.Services; + +public class CmdCdService : ILateBlocker, INService { - public class CmdCdService : ILateBlocker, INService + public ConcurrentDictionary> CommandCooldowns { get; } + public ConcurrentDictionary> ActiveCooldowns { get; } = new ConcurrentDictionary>(); + + public int Priority { get; } = 0; + + public CmdCdService(Bot bot) { - public ConcurrentDictionary> CommandCooldowns { get; } - public ConcurrentDictionary> ActiveCooldowns { get; } = new ConcurrentDictionary>(); - - public int Priority { get; } = 0; - - public CmdCdService(Bot bot) - { - CommandCooldowns = new ConcurrentDictionary>( - bot.AllGuildConfigs.ToDictionary(k => k.GuildId, - v => new ConcurrentHashSet(v.CommandCooldowns))); - } - - public Task TryBlock(IGuild guild, IUser user, string commandName) - { - if (guild is null) - return Task.FromResult(false); - - var cmdcds = CommandCooldowns.GetOrAdd(guild.Id, new ConcurrentHashSet()); - CommandCooldown cdRule; - if ((cdRule = cmdcds.FirstOrDefault(cc => cc.CommandName == commandName)) != null) - { - var activeCdsForGuild = ActiveCooldowns.GetOrAdd(guild.Id, new ConcurrentHashSet()); - if (activeCdsForGuild.FirstOrDefault(ac => ac.UserId == user.Id && ac.Command == commandName) != null) - { - return Task.FromResult(true); - } - - activeCdsForGuild.Add(new ActiveCooldown() - { - UserId = user.Id, - Command = commandName, - }); - - var _ = Task.Run(async () => - { - try - { - await Task.Delay(cdRule.Seconds * 1000).ConfigureAwait(false); - activeCdsForGuild.RemoveWhere(ac => ac.Command == commandName && ac.UserId == user.Id); - } - catch - { - // ignored - } - }); - } - - return Task.FromResult(false); - } - - public Task TryBlockLate(ICommandContext ctx, string moduleName, CommandInfo command) - { - var guild = ctx.Guild; - var user = ctx.User; - var commandName = command.Name.ToLowerInvariant(); - - return TryBlock(guild, user, commandName); - } + CommandCooldowns = new ConcurrentDictionary>( + bot.AllGuildConfigs.ToDictionary(k => k.GuildId, + v => new ConcurrentHashSet(v.CommandCooldowns))); } - public class ActiveCooldown + public Task TryBlock(IGuild guild, IUser user, string commandName) { - public string Command { get; set; } - public ulong UserId { get; set; } + if (guild is null) + return Task.FromResult(false); + + var cmdcds = CommandCooldowns.GetOrAdd(guild.Id, new ConcurrentHashSet()); + CommandCooldown cdRule; + if ((cdRule = cmdcds.FirstOrDefault(cc => cc.CommandName == commandName)) != null) + { + var activeCdsForGuild = ActiveCooldowns.GetOrAdd(guild.Id, new ConcurrentHashSet()); + if (activeCdsForGuild.FirstOrDefault(ac => ac.UserId == user.Id && ac.Command == commandName) != null) + { + return Task.FromResult(true); + } + + activeCdsForGuild.Add(new ActiveCooldown() + { + UserId = user.Id, + Command = commandName, + }); + + var _ = Task.Run(async () => + { + try + { + await Task.Delay(cdRule.Seconds * 1000).ConfigureAwait(false); + activeCdsForGuild.RemoveWhere(ac => ac.Command == commandName && ac.UserId == user.Id); + } + catch + { + // ignored + } + }); + } + + return Task.FromResult(false); + } + + public Task TryBlockLate(ICommandContext ctx, string moduleName, CommandInfo command) + { + var guild = ctx.Guild; + var user = ctx.User; + var commandName = command.Name.ToLowerInvariant(); + + return TryBlock(guild, user, commandName); } } + +public class ActiveCooldown +{ + public string Command { get; set; } + public ulong UserId { get; set; } +} \ No newline at end of file diff --git a/src/NadekoBot/Modules/Permissions/Services/FilterService.cs b/src/NadekoBot/Modules/Permissions/Services/FilterService.cs index 137f888a7..90a056212 100644 --- a/src/NadekoBot/Modules/Permissions/Services/FilterService.cs +++ b/src/NadekoBot/Modules/Permissions/Services/FilterService.cs @@ -1,5 +1,4 @@ using System.Collections.Concurrent; -using System.Linq; using System.Threading.Tasks; using Discord; using Discord.Net; @@ -9,227 +8,223 @@ using NadekoBot.Common.ModuleBehaviors; using NadekoBot.Extensions; using NadekoBot.Services; using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Internal; using NadekoBot.Services.Database.Models; using NadekoBot.Db; -using NadekoBot.Modules.Administration; -using Serilog; -namespace NadekoBot.Modules.Permissions.Services +namespace NadekoBot.Modules.Permissions.Services; + +public sealed class FilterService : IEarlyBehavior { - public sealed class FilterService : IEarlyBehavior + private readonly DbService _db; + + public ConcurrentHashSet InviteFilteringChannels { get; } + public ConcurrentHashSet InviteFilteringServers { get; } + + //serverid, filteredwords + public ConcurrentDictionary> ServerFilteredWords { get; } + + public ConcurrentHashSet WordFilteringChannels { get; } + public ConcurrentHashSet WordFilteringServers { get; } + + public ConcurrentHashSet LinkFilteringChannels { get; } + public ConcurrentHashSet LinkFilteringServers { get; } + + public int Priority => int.MaxValue - 1; + + public ConcurrentHashSet FilteredWordsForChannel(ulong channelId, ulong guildId) { - private readonly DbService _db; + ConcurrentHashSet words = new ConcurrentHashSet(); + if (WordFilteringChannels.Contains(channelId)) + ServerFilteredWords.TryGetValue(guildId, out words); + return words; + } - public ConcurrentHashSet InviteFilteringChannels { get; } - public ConcurrentHashSet InviteFilteringServers { get; } - - //serverid, filteredwords - public ConcurrentDictionary> ServerFilteredWords { get; } - - public ConcurrentHashSet WordFilteringChannels { get; } - public ConcurrentHashSet WordFilteringServers { get; } - - public ConcurrentHashSet LinkFilteringChannels { get; } - public ConcurrentHashSet LinkFilteringServers { get; } - - public int Priority => int.MaxValue - 1; - - public ConcurrentHashSet FilteredWordsForChannel(ulong channelId, ulong guildId) + public void ClearFilteredWords(ulong guildId) + { + using (var uow = _db.GetDbContext()) { - ConcurrentHashSet words = new ConcurrentHashSet(); - if (WordFilteringChannels.Contains(channelId)) - ServerFilteredWords.TryGetValue(guildId, out words); - return words; - } + var gc = uow.GuildConfigsForId(guildId, + set => set.Include(x => x.FilteredWords) + .Include(x => x.FilterWordsChannelIds)); - public void ClearFilteredWords(ulong guildId) - { - using (var uow = _db.GetDbContext()) + WordFilteringServers.TryRemove(guildId); + ServerFilteredWords.TryRemove(guildId, out _); + + foreach (var c in gc.FilterWordsChannelIds) { - var gc = uow.GuildConfigsForId(guildId, - set => set.Include(x => x.FilteredWords) - .Include(x => x.FilterWordsChannelIds)); - - WordFilteringServers.TryRemove(guildId); - ServerFilteredWords.TryRemove(guildId, out _); - - foreach (var c in gc.FilterWordsChannelIds) - { - WordFilteringChannels.TryRemove(c.ChannelId); - } - - gc.FilterWords = false; - gc.FilteredWords.Clear(); - gc.FilterWordsChannelIds.Clear(); - - uow.SaveChanges(); - } - } - - public ConcurrentHashSet FilteredWordsForServer(ulong guildId) - { - var words = new ConcurrentHashSet(); - if (WordFilteringServers.Contains(guildId)) - ServerFilteredWords.TryGetValue(guildId, out words); - return words; - } - - public FilterService(DiscordSocketClient client, DbService db) - { - _db = db; - - using(var uow = db.GetDbContext()) - { - var ids = client.GetGuildIds(); - var configs = uow.Set() - .AsQueryable() - .Include(x => x.FilteredWords) - .Include(x => x.FilterLinksChannelIds) - .Include(x => x.FilterWordsChannelIds) - .Include(x => x.FilterInvitesChannelIds) - .Where(gc => ids.Contains(gc.GuildId)) - .ToList(); - - InviteFilteringServers = new ConcurrentHashSet(configs.Where(gc => gc.FilterInvites).Select(gc => gc.GuildId)); - InviteFilteringChannels = new ConcurrentHashSet(configs.SelectMany(gc => gc.FilterInvitesChannelIds.Select(fci => fci.ChannelId))); - - LinkFilteringServers = new ConcurrentHashSet(configs.Where(gc => gc.FilterLinks).Select(gc => gc.GuildId)); - LinkFilteringChannels = new ConcurrentHashSet(configs.SelectMany(gc => gc.FilterLinksChannelIds.Select(fci => fci.ChannelId))); - - var dict = configs.ToDictionary(gc => gc.GuildId, gc => new ConcurrentHashSet(gc.FilteredWords.Select(fw => fw.Word))); - - ServerFilteredWords = new ConcurrentDictionary>(dict); - - var serverFiltering = configs.Where(gc => gc.FilterWords); - WordFilteringServers = new ConcurrentHashSet(serverFiltering.Select(gc => gc.GuildId)); - WordFilteringChannels = new ConcurrentHashSet(configs.SelectMany(gc => gc.FilterWordsChannelIds.Select(fwci => fwci.ChannelId))); + WordFilteringChannels.TryRemove(c.ChannelId); } - client.MessageUpdated += (oldData, newMsg, channel) => - { - var _ = Task.Run(() => - { - var guild = (channel as ITextChannel)?.Guild; - var usrMsg = newMsg as IUserMessage; + gc.FilterWords = false; + gc.FilteredWords.Clear(); + gc.FilterWordsChannelIds.Clear(); - if (guild is null || usrMsg is null) - return Task.CompletedTask; - - return RunBehavior(guild, usrMsg); - }); - return Task.CompletedTask; - }; - } - - public async Task RunBehavior(IGuild guild, IUserMessage msg) - { - if (!(msg.Author is IGuildUser gu) || gu.GuildPermissions.Administrator) - return false; - - var results = await Task.WhenAll( - FilterInvites(guild, msg), - FilterWords(guild, msg), - FilterLinks(guild, msg)); - - return results.Any(x => x); - } - - private async Task FilterWords(IGuild guild, IUserMessage usrMsg) - { - if (guild is null) - return false; - if (usrMsg is null) - return false; - - var filteredChannelWords = FilteredWordsForChannel(usrMsg.Channel.Id, guild.Id) ?? new ConcurrentHashSet(); - var filteredServerWords = FilteredWordsForServer(guild.Id) ?? new ConcurrentHashSet(); - var wordsInMessage = usrMsg.Content.ToLowerInvariant().Split(' '); - if (filteredChannelWords.Count != 0 || filteredServerWords.Count != 0) - { - foreach (var word in wordsInMessage) - { - if (filteredChannelWords.Contains(word) || - filteredServerWords.Contains(word)) - { - Log.Information("User {UserName} [{UserId}] used a filtered word in {ChannelId} channel", - usrMsg.Author.ToString(), - usrMsg.Author.Id, - usrMsg.Channel.Id); - - try - { - await usrMsg.DeleteAsync().ConfigureAwait(false); - } - catch (HttpException ex) - { - Log.Warning("I do not have permission to filter words in channel with id " + usrMsg.Channel.Id, ex); - } - return true; - } - } - } - return false; - } - - private async Task FilterInvites(IGuild guild, IUserMessage usrMsg) - { - if (guild is null) - return false; - if (usrMsg is null) - return false; - - if ((InviteFilteringChannels.Contains(usrMsg.Channel.Id) - || InviteFilteringServers.Contains(guild.Id)) - && usrMsg.Content.IsDiscordInvite()) - { - Log.Information("User {UserName} [{UserId}] sent a filtered invite to {ChannelId} channel", - usrMsg.Author.ToString(), - usrMsg.Author.Id, - usrMsg.Channel.Id); - - try - { - await usrMsg.DeleteAsync().ConfigureAwait(false); - return true; - } - catch (HttpException ex) - { - Log.Warning("I do not have permission to filter invites in channel with id " + usrMsg.Channel.Id, ex); - return true; - } - } - return false; - } - - private async Task FilterLinks(IGuild guild, IUserMessage usrMsg) - { - if (guild is null) - return false; - if (usrMsg is null) - return false; - - if ((LinkFilteringChannels.Contains(usrMsg.Channel.Id) - || LinkFilteringServers.Contains(guild.Id)) - && usrMsg.Content.TryGetUrlPath(out _)) - { - Log.Information("User {UserName} [{UserId}] sent a filtered link to {ChannelId} channel", - usrMsg.Author.ToString(), - usrMsg.Author.Id, - usrMsg.Channel.Id); - - try - { - await usrMsg.DeleteAsync().ConfigureAwait(false); - return true; - } - catch (HttpException ex) - { - Log.Warning("I do not have permission to filter links in channel with id " + usrMsg.Channel.Id, ex); - return true; - } - } - return false; + uow.SaveChanges(); } } -} + + public ConcurrentHashSet FilteredWordsForServer(ulong guildId) + { + var words = new ConcurrentHashSet(); + if (WordFilteringServers.Contains(guildId)) + ServerFilteredWords.TryGetValue(guildId, out words); + return words; + } + + public FilterService(DiscordSocketClient client, DbService db) + { + _db = db; + + using(var uow = db.GetDbContext()) + { + var ids = client.GetGuildIds(); + var configs = uow.Set() + .AsQueryable() + .Include(x => x.FilteredWords) + .Include(x => x.FilterLinksChannelIds) + .Include(x => x.FilterWordsChannelIds) + .Include(x => x.FilterInvitesChannelIds) + .Where(gc => ids.Contains(gc.GuildId)) + .ToList(); + + InviteFilteringServers = new ConcurrentHashSet(configs.Where(gc => gc.FilterInvites).Select(gc => gc.GuildId)); + InviteFilteringChannels = new ConcurrentHashSet(configs.SelectMany(gc => gc.FilterInvitesChannelIds.Select(fci => fci.ChannelId))); + + LinkFilteringServers = new ConcurrentHashSet(configs.Where(gc => gc.FilterLinks).Select(gc => gc.GuildId)); + LinkFilteringChannels = new ConcurrentHashSet(configs.SelectMany(gc => gc.FilterLinksChannelIds.Select(fci => fci.ChannelId))); + + var dict = configs.ToDictionary(gc => gc.GuildId, gc => new ConcurrentHashSet(gc.FilteredWords.Select(fw => fw.Word))); + + ServerFilteredWords = new ConcurrentDictionary>(dict); + + var serverFiltering = configs.Where(gc => gc.FilterWords); + WordFilteringServers = new ConcurrentHashSet(serverFiltering.Select(gc => gc.GuildId)); + WordFilteringChannels = new ConcurrentHashSet(configs.SelectMany(gc => gc.FilterWordsChannelIds.Select(fwci => fwci.ChannelId))); + } + + client.MessageUpdated += (oldData, newMsg, channel) => + { + var _ = Task.Run(() => + { + var guild = (channel as ITextChannel)?.Guild; + var usrMsg = newMsg as IUserMessage; + + if (guild is null || usrMsg is null) + return Task.CompletedTask; + + return RunBehavior(guild, usrMsg); + }); + return Task.CompletedTask; + }; + } + + public async Task RunBehavior(IGuild guild, IUserMessage msg) + { + if (!(msg.Author is IGuildUser gu) || gu.GuildPermissions.Administrator) + return false; + + var results = await Task.WhenAll( + FilterInvites(guild, msg), + FilterWords(guild, msg), + FilterLinks(guild, msg)); + + return results.Any(x => x); + } + + private async Task FilterWords(IGuild guild, IUserMessage usrMsg) + { + if (guild is null) + return false; + if (usrMsg is null) + return false; + + var filteredChannelWords = FilteredWordsForChannel(usrMsg.Channel.Id, guild.Id) ?? new ConcurrentHashSet(); + var filteredServerWords = FilteredWordsForServer(guild.Id) ?? new ConcurrentHashSet(); + var wordsInMessage = usrMsg.Content.ToLowerInvariant().Split(' '); + if (filteredChannelWords.Count != 0 || filteredServerWords.Count != 0) + { + foreach (var word in wordsInMessage) + { + if (filteredChannelWords.Contains(word) || + filteredServerWords.Contains(word)) + { + Log.Information("User {UserName} [{UserId}] used a filtered word in {ChannelId} channel", + usrMsg.Author.ToString(), + usrMsg.Author.Id, + usrMsg.Channel.Id); + + try + { + await usrMsg.DeleteAsync().ConfigureAwait(false); + } + catch (HttpException ex) + { + Log.Warning("I do not have permission to filter words in channel with id " + usrMsg.Channel.Id, ex); + } + return true; + } + } + } + return false; + } + + private async Task FilterInvites(IGuild guild, IUserMessage usrMsg) + { + if (guild is null) + return false; + if (usrMsg is null) + return false; + + if ((InviteFilteringChannels.Contains(usrMsg.Channel.Id) + || InviteFilteringServers.Contains(guild.Id)) + && usrMsg.Content.IsDiscordInvite()) + { + Log.Information("User {UserName} [{UserId}] sent a filtered invite to {ChannelId} channel", + usrMsg.Author.ToString(), + usrMsg.Author.Id, + usrMsg.Channel.Id); + + try + { + await usrMsg.DeleteAsync().ConfigureAwait(false); + return true; + } + catch (HttpException ex) + { + Log.Warning("I do not have permission to filter invites in channel with id " + usrMsg.Channel.Id, ex); + return true; + } + } + return false; + } + + private async Task FilterLinks(IGuild guild, IUserMessage usrMsg) + { + if (guild is null) + return false; + if (usrMsg is null) + return false; + + if ((LinkFilteringChannels.Contains(usrMsg.Channel.Id) + || LinkFilteringServers.Contains(guild.Id)) + && usrMsg.Content.TryGetUrlPath(out _)) + { + Log.Information("User {UserName} [{UserId}] sent a filtered link to {ChannelId} channel", + usrMsg.Author.ToString(), + usrMsg.Author.Id, + usrMsg.Channel.Id); + + try + { + await usrMsg.DeleteAsync().ConfigureAwait(false); + return true; + } + catch (HttpException ex) + { + Log.Warning("I do not have permission to filter links in channel with id " + usrMsg.Channel.Id, ex); + return true; + } + } + return false; + } +} \ No newline at end of file diff --git a/src/NadekoBot/Modules/Permissions/Services/GlobalPermissionService.cs b/src/NadekoBot/Modules/Permissions/Services/GlobalPermissionService.cs index 838e7635d..9ca8d4353 100644 --- a/src/NadekoBot/Modules/Permissions/Services/GlobalPermissionService.cs +++ b/src/NadekoBot/Modules/Permissions/Services/GlobalPermissionService.cs @@ -1,101 +1,98 @@ -using System.Collections.Generic; -using System.Threading.Tasks; +using System.Threading.Tasks; using Discord.Commands; -using Discord.WebSocket; using NadekoBot.Common.ModuleBehaviors; using NadekoBot.Services; -namespace NadekoBot.Modules.Permissions.Services +namespace NadekoBot.Modules.Permissions.Services; + +public class GlobalPermissionService : ILateBlocker, INService { - public class GlobalPermissionService : ILateBlocker, INService + private readonly BotConfigService _bss; + public int Priority { get; } = 0; + + public HashSet BlockedCommands => _bss.Data.Blocked.Commands; + public HashSet BlockedModules => _bss.Data.Blocked.Modules; + + public GlobalPermissionService(BotConfigService bss) { - private readonly BotConfigService _bss; - public int Priority { get; } = 0; - - public HashSet BlockedCommands => _bss.Data.Blocked.Commands; - public HashSet BlockedModules => _bss.Data.Blocked.Modules; - - public GlobalPermissionService(BotConfigService bss) - { - _bss = bss; - } - - - public Task TryBlockLate(ICommandContext ctx, string moduleName, CommandInfo command) - { - var settings = _bss.Data; - var commandName = command.Name.ToLowerInvariant(); - - if (commandName != "resetglobalperms" && - (settings.Blocked.Commands.Contains(commandName) || - settings.Blocked.Modules.Contains(moduleName.ToLowerInvariant()))) - { - return Task.FromResult(true); - } - - return Task.FromResult(false); - } - - /// - /// Toggles module blacklist - /// - /// Lowercase module name - /// Whether the module is added - public bool ToggleModule(string moduleName) - { - var added = false; - _bss.ModifyConfig(bs => - { - if (bs.Blocked.Modules.Add(moduleName)) - { - added = true; - } - else - { - bs.Blocked.Modules.Remove(moduleName); - added = false; - } - }); - - return added; - } - - /// - /// Toggles command blacklist - /// - /// Lowercase command name - /// Whether the command is added - public bool ToggleCommand(string commandName) - { - var added = false; - _bss.ModifyConfig(bs => - { - if (bs.Blocked.Commands.Add(commandName)) - { - added = true; - } - else - { - bs.Blocked.Commands.Remove(commandName); - added = false; - } - }); - - return added; - } - - /// - /// Resets all global permissions - /// - public Task Reset() - { - _bss.ModifyConfig(bs => - { - bs.Blocked.Commands.Clear(); - bs.Blocked.Modules.Clear(); - }); - - return Task.CompletedTask; - } + _bss = bss; } -} + + + public Task TryBlockLate(ICommandContext ctx, string moduleName, CommandInfo command) + { + var settings = _bss.Data; + var commandName = command.Name.ToLowerInvariant(); + + if (commandName != "resetglobalperms" && + (settings.Blocked.Commands.Contains(commandName) || + settings.Blocked.Modules.Contains(moduleName.ToLowerInvariant()))) + { + return Task.FromResult(true); + } + + return Task.FromResult(false); + } + + /// + /// Toggles module blacklist + /// + /// Lowercase module name + /// Whether the module is added + public bool ToggleModule(string moduleName) + { + var added = false; + _bss.ModifyConfig(bs => + { + if (bs.Blocked.Modules.Add(moduleName)) + { + added = true; + } + else + { + bs.Blocked.Modules.Remove(moduleName); + added = false; + } + }); + + return added; + } + + /// + /// Toggles command blacklist + /// + /// Lowercase command name + /// Whether the command is added + public bool ToggleCommand(string commandName) + { + var added = false; + _bss.ModifyConfig(bs => + { + if (bs.Blocked.Commands.Add(commandName)) + { + added = true; + } + else + { + bs.Blocked.Commands.Remove(commandName); + added = false; + } + }); + + return added; + } + + /// + /// Resets all global permissions + /// + public Task Reset() + { + _bss.ModifyConfig(bs => + { + bs.Blocked.Commands.Clear(); + bs.Blocked.Modules.Clear(); + }); + + return Task.CompletedTask; + } +} \ No newline at end of file diff --git a/src/NadekoBot/Modules/Permissions/Services/PermissionsService.cs b/src/NadekoBot/Modules/Permissions/Services/PermissionsService.cs index 7e4b495a4..f14a0ca25 100644 --- a/src/NadekoBot/Modules/Permissions/Services/PermissionsService.cs +++ b/src/NadekoBot/Modules/Permissions/Services/PermissionsService.cs @@ -1,6 +1,4 @@ -using System; -using System.Collections.Concurrent; -using System.Linq; +using System.Collections.Concurrent; using System.Threading.Tasks; using Discord; using Discord.Commands; @@ -13,180 +11,179 @@ using NadekoBot.Services; using NadekoBot.Services.Database.Models; using NadekoBot.Db; -namespace NadekoBot.Modules.Permissions.Services +namespace NadekoBot.Modules.Permissions.Services; + +public class PermissionService : ILateBlocker, INService { - public class PermissionService : ILateBlocker, INService - { - public int Priority { get; } = 0; + public int Priority { get; } = 0; - private readonly DbService _db; - private readonly CommandHandler _cmd; - private readonly IBotStrings _strings; - private readonly IEmbedBuilderService _eb; + private readonly DbService _db; + private readonly CommandHandler _cmd; + private readonly IBotStrings _strings; + private readonly IEmbedBuilderService _eb; - //guildid, root permission - public ConcurrentDictionary Cache { get; } = - new ConcurrentDictionary(); + //guildid, root permission + public ConcurrentDictionary Cache { get; } = + new ConcurrentDictionary(); - public PermissionService(DiscordSocketClient client, - DbService db, - CommandHandler cmd, - IBotStrings strings, - IEmbedBuilderService eb) + public PermissionService(DiscordSocketClient client, + DbService db, + CommandHandler cmd, + IBotStrings strings, + IEmbedBuilderService eb) + { + _db = db; + _cmd = cmd; + _strings = strings; + _eb = eb; + + using (var uow = _db.GetDbContext()) { - _db = db; - _cmd = cmd; - _strings = strings; - _eb = eb; - - using (var uow = _db.GetDbContext()) + foreach (var x in uow.GuildConfigs.Permissionsv2ForAll(client.Guilds.ToArray().Select(x => x.Id) + .ToList())) { - foreach (var x in uow.GuildConfigs.Permissionsv2ForAll(client.Guilds.ToArray().Select(x => x.Id) - .ToList())) + Cache.TryAdd(x.GuildId, new PermissionCache() { - Cache.TryAdd(x.GuildId, new PermissionCache() - { - Verbose = x.VerbosePermissions, - PermRole = x.PermissionRole, - Permissions = new PermissionsCollection(x.Permissions) - }); - } - } - } - - public PermissionCache GetCacheFor(ulong guildId) - { - if (!Cache.TryGetValue(guildId, out var pc)) - { - using (var uow = _db.GetDbContext()) - { - var config = uow.GuildConfigsForId(guildId, - set => set.Include(x => x.Permissions)); - UpdateCache(config); - } - Cache.TryGetValue(guildId, out pc); - if (pc is null) - throw new Exception("Cache is null."); - } - return pc; - } - - public async Task AddPermissions(ulong guildId, params Permissionv2[] perms) - { - using (var uow = _db.GetDbContext()) - { - var config = uow.GcWithPermissionsv2For(guildId); - //var orderedPerms = new PermissionsCollection(config.Permissions); - var max = config.Permissions.Max(x => x.Index); //have to set its index to be the highest - foreach (var perm in perms) - { - perm.Index = ++max; - config.Permissions.Add(perm); - } - await uow.SaveChangesAsync(); - UpdateCache(config); - } - } - - public void UpdateCache(GuildConfig config) - { - Cache.AddOrUpdate(config.GuildId, new PermissionCache() - { - Permissions = new PermissionsCollection(config.Permissions), - PermRole = config.PermissionRole, - Verbose = config.VerbosePermissions - }, (id, old) => - { - old.Permissions = new PermissionsCollection(config.Permissions); - old.PermRole = config.PermissionRole; - old.Verbose = config.VerbosePermissions; - return old; - }); - } - - public async Task TryBlockLate(ICommandContext ctx, string moduleName, CommandInfo command) - { - var guild = ctx.Guild; - var msg = ctx.Message; - var user = ctx.User; - var channel = ctx.Channel; - var commandName = command.Name.ToLowerInvariant(); - - await Task.Yield(); - if (guild is null) - { - return false; - } - else - { - var resetCommand = commandName == "resetperms"; - - PermissionCache pc = GetCacheFor(guild.Id); - if (!resetCommand && !pc.Permissions.CheckPermissions(msg, commandName, moduleName, out int index)) - { - if (pc.Verbose) - { - try - { - await channel.SendErrorAsync(_eb, - _strings.GetText(strs.perm_prevent(index + 1, - Format.Bold(pc.Permissions[index] - .GetCommand(_cmd.GetPrefix(guild), (SocketGuild)guild))), guild.Id)); - } - catch - { - } - } - - return true; - } - - - if (moduleName == nameof(Permissions)) - { - if (!(user is IGuildUser guildUser)) - return true; - - if (guildUser.GuildPermissions.Administrator) - return false; - - var permRole = pc.PermRole; - if (!ulong.TryParse(permRole, out var rid)) - rid = 0; - string returnMsg; - IRole role; - if (string.IsNullOrWhiteSpace(permRole) || (role = guild.GetRole(rid)) is null) - { - returnMsg = $"You need Admin permissions in order to use permission commands."; - if (pc.Verbose) - try { await channel.SendErrorAsync(_eb, returnMsg).ConfigureAwait(false); } catch { } - - return true; - } - else if (!guildUser.RoleIds.Contains(rid)) - { - returnMsg = $"You need the {Format.Bold(role.Name)} role in order to use permission commands."; - if (pc.Verbose) - try { await channel.SendErrorAsync(_eb, returnMsg).ConfigureAwait(false); } catch { } - - return true; - } - return false; - } - } - - return false; - } - - public async Task Reset(ulong guildId) - { - using (var uow = _db.GetDbContext()) - { - var config = uow.GcWithPermissionsv2For(guildId); - config.Permissions = Permissionv2.GetDefaultPermlist; - await uow.SaveChangesAsync(); - UpdateCache(config); + Verbose = x.VerbosePermissions, + PermRole = x.PermissionRole, + Permissions = new PermissionsCollection(x.Permissions) + }); } } } + + public PermissionCache GetCacheFor(ulong guildId) + { + if (!Cache.TryGetValue(guildId, out var pc)) + { + using (var uow = _db.GetDbContext()) + { + var config = uow.GuildConfigsForId(guildId, + set => set.Include(x => x.Permissions)); + UpdateCache(config); + } + Cache.TryGetValue(guildId, out pc); + if (pc is null) + throw new Exception("Cache is null."); + } + return pc; + } + + public async Task AddPermissions(ulong guildId, params Permissionv2[] perms) + { + using (var uow = _db.GetDbContext()) + { + var config = uow.GcWithPermissionsv2For(guildId); + //var orderedPerms = new PermissionsCollection(config.Permissions); + var max = config.Permissions.Max(x => x.Index); //have to set its index to be the highest + foreach (var perm in perms) + { + perm.Index = ++max; + config.Permissions.Add(perm); + } + await uow.SaveChangesAsync(); + UpdateCache(config); + } + } + + public void UpdateCache(GuildConfig config) + { + Cache.AddOrUpdate(config.GuildId, new PermissionCache() + { + Permissions = new PermissionsCollection(config.Permissions), + PermRole = config.PermissionRole, + Verbose = config.VerbosePermissions + }, (id, old) => + { + old.Permissions = new PermissionsCollection(config.Permissions); + old.PermRole = config.PermissionRole; + old.Verbose = config.VerbosePermissions; + return old; + }); + } + + public async Task TryBlockLate(ICommandContext ctx, string moduleName, CommandInfo command) + { + var guild = ctx.Guild; + var msg = ctx.Message; + var user = ctx.User; + var channel = ctx.Channel; + var commandName = command.Name.ToLowerInvariant(); + + await Task.Yield(); + if (guild is null) + { + return false; + } + else + { + var resetCommand = commandName == "resetperms"; + + PermissionCache pc = GetCacheFor(guild.Id); + if (!resetCommand && !pc.Permissions.CheckPermissions(msg, commandName, moduleName, out int index)) + { + if (pc.Verbose) + { + try + { + await channel.SendErrorAsync(_eb, + _strings.GetText(strs.perm_prevent(index + 1, + Format.Bold(pc.Permissions[index] + .GetCommand(_cmd.GetPrefix(guild), (SocketGuild)guild))), guild.Id)); + } + catch + { + } + } + + return true; + } + + + if (moduleName == nameof(Permissions)) + { + if (!(user is IGuildUser guildUser)) + return true; + + if (guildUser.GuildPermissions.Administrator) + return false; + + var permRole = pc.PermRole; + if (!ulong.TryParse(permRole, out var rid)) + rid = 0; + string returnMsg; + IRole role; + if (string.IsNullOrWhiteSpace(permRole) || (role = guild.GetRole(rid)) is null) + { + returnMsg = $"You need Admin permissions in order to use permission commands."; + if (pc.Verbose) + try { await channel.SendErrorAsync(_eb, returnMsg).ConfigureAwait(false); } catch { } + + return true; + } + else if (!guildUser.RoleIds.Contains(rid)) + { + returnMsg = $"You need the {Format.Bold(role.Name)} role in order to use permission commands."; + if (pc.Verbose) + try { await channel.SendErrorAsync(_eb, returnMsg).ConfigureAwait(false); } catch { } + + return true; + } + return false; + } + } + + return false; + } + + public async Task Reset(ulong guildId) + { + using (var uow = _db.GetDbContext()) + { + var config = uow.GcWithPermissionsv2For(guildId); + config.Permissions = Permissionv2.GetDefaultPermlist; + await uow.SaveChangesAsync(); + UpdateCache(config); + } + } } \ No newline at end of file diff --git a/src/NadekoBot/Modules/Searches/AnimeSearchCommands.cs b/src/NadekoBot/Modules/Searches/AnimeSearchCommands.cs index 066cb881b..a7d0ec932 100644 --- a/src/NadekoBot/Modules/Searches/AnimeSearchCommands.cs +++ b/src/NadekoBot/Modules/Searches/AnimeSearchCommands.cs @@ -3,195 +3,192 @@ using Discord; using Discord.Commands; using NadekoBot.Extensions; using NadekoBot.Modules.Searches.Services; -using System; -using System.Linq; using System.Threading.Tasks; using NadekoBot.Common.Attributes; using AngleSharp.Html.Dom; -namespace NadekoBot.Modules.Searches +namespace NadekoBot.Modules.Searches; + +public partial class Searches { - public partial class Searches + [Group] + public class AnimeSearchCommands : NadekoSubmodule { - [Group] - public class AnimeSearchCommands : NadekoSubmodule + // [NadekoCommand, Aliases] + // public async Task Novel([Leftover] string query) + // { + // if (string.IsNullOrWhiteSpace(query)) + // return; + // + // var novelData = await _service.GetNovelData(query).ConfigureAwait(false); + // + // if (novelData is null) + // { + // await ReplyErrorLocalizedAsync(strs.failed_finding_novel).ConfigureAwait(false); + // return; + // } + // + // var embed = _eb.Create() + // .WithOkColor() + // .WithDescription(novelData.Description.Replace("
", Environment.NewLine, StringComparison.InvariantCulture)) + // .WithTitle(novelData.Title) + // .WithUrl(novelData.Link) + // .WithImageUrl(novelData.ImageUrl) + // .AddField(GetText(strs.authors), string.Join("\n", novelData.Authors), true) + // .AddField(GetText(strs.status), novelData.Status, true) + // .AddField(GetText(strs.genres), string.Join(" ", novelData.Genres.Any() ? novelData.Genres : new[] { "none" }), true) + // .WithFooter($"{GetText(strs.score)} {novelData.Score}"); + // + // await ctx.Channel.EmbedAsync(embed).ConfigureAwait(false); + // } + + [NadekoCommand, Aliases] + [Priority(0)] + public async Task Mal([Leftover] string name) { - // [NadekoCommand, Aliases] - // public async Task Novel([Leftover] string query) - // { - // if (string.IsNullOrWhiteSpace(query)) - // return; - // - // var novelData = await _service.GetNovelData(query).ConfigureAwait(false); - // - // if (novelData is null) - // { - // await ReplyErrorLocalizedAsync(strs.failed_finding_novel).ConfigureAwait(false); - // return; - // } - // - // var embed = _eb.Create() - // .WithOkColor() - // .WithDescription(novelData.Description.Replace("
", Environment.NewLine, StringComparison.InvariantCulture)) - // .WithTitle(novelData.Title) - // .WithUrl(novelData.Link) - // .WithImageUrl(novelData.ImageUrl) - // .AddField(GetText(strs.authors), string.Join("\n", novelData.Authors), true) - // .AddField(GetText(strs.status), novelData.Status, true) - // .AddField(GetText(strs.genres), string.Join(" ", novelData.Genres.Any() ? novelData.Genres : new[] { "none" }), true) - // .WithFooter($"{GetText(strs.score)} {novelData.Score}"); - // - // await ctx.Channel.EmbedAsync(embed).ConfigureAwait(false); - // } + if (string.IsNullOrWhiteSpace(name)) + return; - [NadekoCommand, Aliases] - [Priority(0)] - public async Task Mal([Leftover] string name) + var fullQueryLink = "https://myanimelist.net/profile/" + name; + + var config = Configuration.Default.WithDefaultLoader(); + using (var document = await BrowsingContext.New(config).OpenAsync(fullQueryLink).ConfigureAwait(false)) { - if (string.IsNullOrWhiteSpace(name)) - return; + var imageElem = document.QuerySelector("body > div#myanimelist > div.wrapper > div#contentWrapper > div#content > div.content-container > div.container-left > div.user-profile > div.user-image > img"); + var imageUrl = ((IHtmlImageElement)imageElem)?.Source ?? "http://icecream.me/uploads/870b03f36b59cc16ebfe314ef2dde781.png"; - var fullQueryLink = "https://myanimelist.net/profile/" + name; + var stats = document.QuerySelectorAll("body > div#myanimelist > div.wrapper > div#contentWrapper > div#content > div.content-container > div.container-right > div#statistics > div.user-statistics-stats > div.stats > div.clearfix > ul.stats-status > li > span").Select(x => x.InnerHtml).ToList(); - var config = Configuration.Default.WithDefaultLoader(); - using (var document = await BrowsingContext.New(config).OpenAsync(fullQueryLink).ConfigureAwait(false)) - { - var imageElem = document.QuerySelector("body > div#myanimelist > div.wrapper > div#contentWrapper > div#content > div.content-container > div.container-left > div.user-profile > div.user-image > img"); - var imageUrl = ((IHtmlImageElement)imageElem)?.Source ?? "http://icecream.me/uploads/870b03f36b59cc16ebfe314ef2dde781.png"; + var favorites = document.QuerySelectorAll("div.user-favorites > div.di-tc"); - var stats = document.QuerySelectorAll("body > div#myanimelist > div.wrapper > div#contentWrapper > div#content > div.content-container > div.container-right > div#statistics > div.user-statistics-stats > div.stats > div.clearfix > ul.stats-status > li > span").Select(x => x.InnerHtml).ToList(); + var favAnime = GetText(strs.anime_no_fav); + if (favorites.Length > 0 && favorites[0].QuerySelector("p") is null) + favAnime = string.Join("\n", favorites[0].QuerySelectorAll("ul > li > div.di-tc.va-t > a") + .Shuffle() + .Take(3) + .Select(x => + { + var elem = (IHtmlAnchorElement)x; + return $"[{elem.InnerHtml}]({elem.Href})"; + })); - var favorites = document.QuerySelectorAll("div.user-favorites > div.di-tc"); + var info = document.QuerySelectorAll("ul.user-status:nth-child(3) > li.clearfix") + .Select(x => Tuple.Create(x.Children[0].InnerHtml, x.Children[1].InnerHtml)) + .ToList(); - var favAnime = GetText(strs.anime_no_fav); - if (favorites.Length > 0 && favorites[0].QuerySelector("p") is null) - favAnime = string.Join("\n", favorites[0].QuerySelectorAll("ul > li > div.di-tc.va-t > a") - .Shuffle() - .Take(3) - .Select(x => - { - var elem = (IHtmlAnchorElement)x; - return $"[{elem.InnerHtml}]({elem.Href})"; - })); + var daysAndMean = document.QuerySelectorAll("div.anime:nth-child(1) > div:nth-child(2) > div") + .Select(x => x.TextContent.Split(':').Select(y => y.Trim()).ToArray()) + .ToArray(); - var info = document.QuerySelectorAll("ul.user-status:nth-child(3) > li.clearfix") - .Select(x => Tuple.Create(x.Children[0].InnerHtml, x.Children[1].InnerHtml)) - .ToList(); + var embed = _eb.Create() + .WithOkColor() + .WithTitle(GetText(strs.mal_profile(name))) + .AddField("💚 " + GetText(strs.watching), stats[0], true) + .AddField("💙 " + GetText(strs.completed), stats[1], true); + if (info.Count < 3) + embed.AddField("💛 " + GetText(strs.on_hold), stats[2], true); + embed + .AddField("💔 " + GetText(strs.dropped), stats[3], true) + .AddField("⚪ " + GetText(strs.plan_to_watch), stats[4], true) + .AddField("🕐 " + daysAndMean[0][0], daysAndMean[0][1], true) + .AddField("📊 " + daysAndMean[1][0], daysAndMean[1][1], true) + .AddField(MalInfoToEmoji(info[0].Item1) + " " + info[0].Item1, info[0].Item2.TrimTo(20), true) + .AddField(MalInfoToEmoji(info[1].Item1) + " " + info[1].Item1, info[1].Item2.TrimTo(20), true); + if (info.Count > 2) + embed.AddField(MalInfoToEmoji(info[2].Item1) + " " + info[2].Item1, info[2].Item2.TrimTo(20), true); - var daysAndMean = document.QuerySelectorAll("div.anime:nth-child(1) > div:nth-child(2) > div") - .Select(x => x.TextContent.Split(':').Select(y => y.Trim()).ToArray()) - .ToArray(); - - var embed = _eb.Create() - .WithOkColor() - .WithTitle(GetText(strs.mal_profile(name))) - .AddField("💚 " + GetText(strs.watching), stats[0], true) - .AddField("💙 " + GetText(strs.completed), stats[1], true); - if (info.Count < 3) - embed.AddField("💛 " + GetText(strs.on_hold), stats[2], true); - embed - .AddField("💔 " + GetText(strs.dropped), stats[3], true) - .AddField("⚪ " + GetText(strs.plan_to_watch), stats[4], true) - .AddField("🕐 " + daysAndMean[0][0], daysAndMean[0][1], true) - .AddField("📊 " + daysAndMean[1][0], daysAndMean[1][1], true) - .AddField(MalInfoToEmoji(info[0].Item1) + " " + info[0].Item1, info[0].Item2.TrimTo(20), true) - .AddField(MalInfoToEmoji(info[1].Item1) + " " + info[1].Item1, info[1].Item2.TrimTo(20), true); - if (info.Count > 2) - embed.AddField(MalInfoToEmoji(info[2].Item1) + " " + info[2].Item1, info[2].Item2.TrimTo(20), true); - - embed - .WithDescription($@" + embed + .WithDescription($@" ** https://myanimelist.net/animelist/{ name } ** **{GetText(strs.top_3_fav_anime)}** {favAnime}" - ) - .WithUrl(fullQueryLink) - .WithImageUrl(imageUrl); - - await ctx.Channel.EmbedAsync(embed).ConfigureAwait(false); - } - } - - private static string MalInfoToEmoji(string info) - { - info = info.Trim().ToLowerInvariant(); - switch (info) - { - case "gender": - return "🚁"; - case "location": - return "🗺"; - case "last online": - return "👥"; - case "birthday": - return "📆"; - default: - return "❔"; - } - } - - [NadekoCommand, Aliases] - [RequireContext(ContextType.Guild)] - [Priority(1)] - public Task Mal(IGuildUser usr) => Mal(usr.Username); - - [NadekoCommand, Aliases] - public async Task Anime([Leftover] string query) - { - if (string.IsNullOrWhiteSpace(query)) - return; - - var animeData = await _service.GetAnimeData(query).ConfigureAwait(false); - - if (animeData is null) - { - await ReplyErrorLocalizedAsync(strs.failed_finding_anime).ConfigureAwait(false); - return; - } - - var embed = _eb.Create() - .WithOkColor() - .WithDescription(animeData.Synopsis.Replace("
", Environment.NewLine, StringComparison.InvariantCulture)) - .WithTitle(animeData.TitleEnglish) - .WithUrl(animeData.Link) - .WithImageUrl(animeData.ImageUrlLarge) - .AddField(GetText(strs.episodes), animeData.TotalEpisodes.ToString(), true) - .AddField(GetText(strs.status), animeData.AiringStatus.ToString(), true) - .AddField(GetText(strs.genres), string.Join(",\n", animeData.Genres.Any() ? animeData.Genres : new[] { "none" }), true) - .WithFooter($"{GetText(strs.score)} {animeData.AverageScore} / 100"); - await ctx.Channel.EmbedAsync(embed).ConfigureAwait(false); - } - - [NadekoCommand, Aliases] - [RequireContext(ContextType.Guild)] - public async Task Manga([Leftover] string query) - { - if (string.IsNullOrWhiteSpace(query)) - return; - - var mangaData = await _service.GetMangaData(query).ConfigureAwait(false); - - if (mangaData is null) - { - await ReplyErrorLocalizedAsync(strs.failed_finding_manga).ConfigureAwait(false); - return; - } - - var embed = _eb.Create() - .WithOkColor() - .WithDescription(mangaData.Synopsis.Replace("
", Environment.NewLine, StringComparison.InvariantCulture)) - .WithTitle(mangaData.TitleEnglish) - .WithUrl(mangaData.Link) - .WithImageUrl(mangaData.ImageUrlLge) - .AddField(GetText(strs.chapters), mangaData.TotalChapters.ToString(), true) - .AddField(GetText(strs.status), mangaData.PublishingStatus.ToString(), true) - .AddField(GetText(strs.genres), string.Join(",\n", mangaData.Genres.Any() ? mangaData.Genres : new[] { "none" }), true) - .WithFooter($"{GetText(strs.score)} {mangaData.AverageScore} / 100"); + ) + .WithUrl(fullQueryLink) + .WithImageUrl(imageUrl); await ctx.Channel.EmbedAsync(embed).ConfigureAwait(false); } } + + private static string MalInfoToEmoji(string info) + { + info = info.Trim().ToLowerInvariant(); + switch (info) + { + case "gender": + return "🚁"; + case "location": + return "🗺"; + case "last online": + return "👥"; + case "birthday": + return "📆"; + default: + return "❔"; + } + } + + [NadekoCommand, Aliases] + [RequireContext(ContextType.Guild)] + [Priority(1)] + public Task Mal(IGuildUser usr) => Mal(usr.Username); + + [NadekoCommand, Aliases] + public async Task Anime([Leftover] string query) + { + if (string.IsNullOrWhiteSpace(query)) + return; + + var animeData = await _service.GetAnimeData(query).ConfigureAwait(false); + + if (animeData is null) + { + await ReplyErrorLocalizedAsync(strs.failed_finding_anime).ConfigureAwait(false); + return; + } + + var embed = _eb.Create() + .WithOkColor() + .WithDescription(animeData.Synopsis.Replace("
", Environment.NewLine, StringComparison.InvariantCulture)) + .WithTitle(animeData.TitleEnglish) + .WithUrl(animeData.Link) + .WithImageUrl(animeData.ImageUrlLarge) + .AddField(GetText(strs.episodes), animeData.TotalEpisodes.ToString(), true) + .AddField(GetText(strs.status), animeData.AiringStatus.ToString(), true) + .AddField(GetText(strs.genres), string.Join(",\n", animeData.Genres.Any() ? animeData.Genres : new[] { "none" }), true) + .WithFooter($"{GetText(strs.score)} {animeData.AverageScore} / 100"); + await ctx.Channel.EmbedAsync(embed).ConfigureAwait(false); + } + + [NadekoCommand, Aliases] + [RequireContext(ContextType.Guild)] + public async Task Manga([Leftover] string query) + { + if (string.IsNullOrWhiteSpace(query)) + return; + + var mangaData = await _service.GetMangaData(query).ConfigureAwait(false); + + if (mangaData is null) + { + await ReplyErrorLocalizedAsync(strs.failed_finding_manga).ConfigureAwait(false); + return; + } + + var embed = _eb.Create() + .WithOkColor() + .WithDescription(mangaData.Synopsis.Replace("
", Environment.NewLine, StringComparison.InvariantCulture)) + .WithTitle(mangaData.TitleEnglish) + .WithUrl(mangaData.Link) + .WithImageUrl(mangaData.ImageUrlLge) + .AddField(GetText(strs.chapters), mangaData.TotalChapters.ToString(), true) + .AddField(GetText(strs.status), mangaData.PublishingStatus.ToString(), true) + .AddField(GetText(strs.genres), string.Join(",\n", mangaData.Genres.Any() ? mangaData.Genres : new[] { "none" }), true) + .WithFooter($"{GetText(strs.score)} {mangaData.AverageScore} / 100"); + + await ctx.Channel.EmbedAsync(embed).ConfigureAwait(false); + } } } \ No newline at end of file diff --git a/src/NadekoBot/Modules/Searches/Common/AnimeResult.cs b/src/NadekoBot/Modules/Searches/Common/AnimeResult.cs index e30b0cd1a..cc100edd0 100644 --- a/src/NadekoBot/Modules/Searches/Common/AnimeResult.cs +++ b/src/NadekoBot/Modules/Searches/Common/AnimeResult.cs @@ -1,26 +1,25 @@ using NadekoBot.Extensions; using Newtonsoft.Json; -namespace NadekoBot.Modules.Searches.Common -{ - public class AnimeResult - { - public int Id { get; set; } - public string AiringStatus => AiringStatusParsed.ToTitleCase(); - [JsonProperty("airing_status")] - public string AiringStatusParsed { get; set; } - [JsonProperty("title_english")] - public string TitleEnglish { get; set; } - [JsonProperty("total_episodes")] - public int TotalEpisodes { get; set; } - public string Description { get; set; } - [JsonProperty("image_url_lge")] - public string ImageUrlLarge { get; set; } - public string[] Genres { get; set; } - [JsonProperty("average_score")] - public string AverageScore { get; set; } +namespace NadekoBot.Modules.Searches.Common; - public string Link => "http://anilist.co/anime/" + Id; - public string Synopsis => Description?.Substring(0, Description.Length > 500 ? 500 : Description.Length) + "..."; - } +public class AnimeResult +{ + public int Id { get; set; } + public string AiringStatus => AiringStatusParsed.ToTitleCase(); + [JsonProperty("airing_status")] + public string AiringStatusParsed { get; set; } + [JsonProperty("title_english")] + public string TitleEnglish { get; set; } + [JsonProperty("total_episodes")] + public int TotalEpisodes { get; set; } + public string Description { get; set; } + [JsonProperty("image_url_lge")] + public string ImageUrlLarge { get; set; } + public string[] Genres { get; set; } + [JsonProperty("average_score")] + public string AverageScore { get; set; } + + public string Link => "http://anilist.co/anime/" + Id; + public string Synopsis => Description?.Substring(0, Description.Length > 500 ? 500 : Description.Length) + "..."; } \ No newline at end of file diff --git a/src/NadekoBot/Modules/Searches/Common/BibleVerses.cs b/src/NadekoBot/Modules/Searches/Common/BibleVerses.cs index 91164452e..780ab59ea 100644 --- a/src/NadekoBot/Modules/Searches/Common/BibleVerses.cs +++ b/src/NadekoBot/Modules/Searches/Common/BibleVerses.cs @@ -1,19 +1,18 @@ using Newtonsoft.Json; -namespace NadekoBot.Modules.Searches.Common -{ - public class BibleVerses - { - public string Error { get; set; } - public BibleVerse[] Verses { get; set; } - } +namespace NadekoBot.Modules.Searches.Common; - public class BibleVerse - { - [JsonProperty("book_name")] - public string BookName { get; set; } - public int Chapter { get; set; } - public int Verse { get; set; } - public string Text { get; set; } - } +public class BibleVerses +{ + public string Error { get; set; } + public BibleVerse[] Verses { get; set; } } + +public class BibleVerse +{ + [JsonProperty("book_name")] + public string BookName { get; set; } + public int Chapter { get; set; } + public int Verse { get; set; } + public string Text { get; set; } +} \ No newline at end of file diff --git a/src/NadekoBot/Modules/Searches/Common/CryptoData.cs b/src/NadekoBot/Modules/Searches/Common/CryptoData.cs index 070f7b507..eefc703b4 100644 --- a/src/NadekoBot/Modules/Searches/Common/CryptoData.cs +++ b/src/NadekoBot/Modules/Searches/Common/CryptoData.cs @@ -1,37 +1,35 @@ using Newtonsoft.Json; -using System.Collections.Generic; -namespace NadekoBot.Modules.Searches.Common +namespace NadekoBot.Modules.Searches.Common; + +public class CryptoResponse { - public class CryptoResponse - { - public List Data { get; set; } - } - - public class CryptoResponseData - { - public string Id { get; set; } - public string Name { get; set; } - public string Symbol { get; set; } - public string Slug { get; set; } - - [JsonProperty("cmc_rank")] - public int Rank { get; set; } - public CurrencyQuotes Quote { get; set; } - } - - public class CurrencyQuotes - { - public Quote Usd { get; set; } - } - - public class Quote - { - public double Price { get; set; } - public double Market_Cap { get; set; } - public string Percent_Change_1h { get; set; } - public string Percent_Change_24h { get; set; } - public string Percent_Change_7d { get; set; } - public double? Volume_24h { get; set; } - } + public List Data { get; set; } } + +public class CryptoResponseData +{ + public string Id { get; set; } + public string Name { get; set; } + public string Symbol { get; set; } + public string Slug { get; set; } + + [JsonProperty("cmc_rank")] + public int Rank { get; set; } + public CurrencyQuotes Quote { get; set; } +} + +public class CurrencyQuotes +{ + public Quote Usd { get; set; } +} + +public class Quote +{ + public double Price { get; set; } + public double Market_Cap { get; set; } + public string Percent_Change_1h { get; set; } + public string Percent_Change_24h { get; set; } + public string Percent_Change_7d { get; set; } + public double? Volume_24h { get; set; } +} \ No newline at end of file diff --git a/src/NadekoBot/Modules/Searches/Common/DefineModel.cs b/src/NadekoBot/Modules/Searches/Common/DefineModel.cs index 28969f2e8..6f44fdbd8 100644 --- a/src/NadekoBot/Modules/Searches/Common/DefineModel.cs +++ b/src/NadekoBot/Modules/Searches/Common/DefineModel.cs @@ -1,42 +1,40 @@ using Newtonsoft.Json; -using System.Collections.Generic; -namespace NadekoBot.Modules.Searches.Common +namespace NadekoBot.Modules.Searches.Common; + +public class Audio { - public class Audio - { - public string Url { get; set; } - } - - public class Example - { - public List