Global usings and file scoped namespaces

This commit is contained in:
Kwoth
2021-12-19 05:14:11 +01:00
parent bc31dae965
commit ee33313519
548 changed files with 47528 additions and 49115 deletions

View File

@@ -3,6 +3,7 @@
<PropertyGroup> <PropertyGroup>
<TargetFramework>net6.0</TargetFramework> <TargetFramework>net6.0</TargetFramework>
<LangVersion>10.0</LangVersion> <LangVersion>10.0</LangVersion>
<EnablePreviewFeatures>True</EnablePreviewFeatures>
<IsPackable>false</IsPackable> <IsPackable>false</IsPackable>
</PropertyGroup> </PropertyGroup>

View File

@@ -6,11 +6,8 @@ using NadekoBot.Common;
using NadekoBot.Services; using NadekoBot.Services;
using NadekoBot.Services.Database.Models; using NadekoBot.Services.Database.Models;
using NadekoBot.Extensions; using NadekoBot.Extensions;
using System;
using System.Collections.Generic;
using System.Collections.Immutable; using System.Collections.Immutable;
using System.Diagnostics; using System.Diagnostics;
using System.Linq;
using System.Net.Http; using System.Net.Http;
using System.Reflection; using System.Reflection;
using System.Threading.Tasks; using System.Threading.Tasks;
@@ -19,83 +16,81 @@ using NadekoBot.Common.ModuleBehaviors;
using NadekoBot.Common.Configs; using NadekoBot.Common.Configs;
using NadekoBot.Db; using NadekoBot.Db;
using NadekoBot.Modules.Administration.Services; 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<GuildConfig, Task> JoinedGuild = delegate { return Task.CompletedTask; };
public DiscordSocketClient Client { get; }
public ImmutableArray<GuildConfig> 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; if (shardId < 0)
private readonly CommandService _commandService; throw new ArgumentOutOfRangeException(nameof(shardId));
private readonly DbService _db;
private readonly IBotCredsProvider _credsProvider;
public event Func<GuildConfig, Task> JoinedGuild = delegate { return Task.CompletedTask; };
public DiscordSocketClient Client { get; }
public ImmutableArray<GuildConfig> AllGuildConfigs { get; private set; }
private IServiceProvider Services { get; set; } _credsProvider = new BotCredsProvider(totalShards);
_creds = _credsProvider.GetCreds();
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();
_db = new DbService(_creds); _db = new DbService(_creds);
if (shardId == 0) if (shardId == 0)
{ {
_db.Setup(); _db.Setup();
} }
Client = new DiscordSocketClient(new DiscordSocketConfig Client = new DiscordSocketClient(new DiscordSocketConfig
{ {
MessageCacheSize = 50, MessageCacheSize = 50,
LogLevel = LogSeverity.Warning, LogLevel = LogSeverity.Warning,
ConnectionTimeout = int.MaxValue, ConnectionTimeout = int.MaxValue,
TotalShards = _creds.TotalShards, TotalShards = _creds.TotalShards,
ShardId = shardId, ShardId = shardId,
AlwaysDownloadUsers = false, AlwaysDownloadUsers = false,
ExclusiveBulkDelete = true, ExclusiveBulkDelete = true,
}); });
_commandService = new CommandService(new CommandServiceConfig() _commandService = new CommandService(new CommandServiceConfig()
{ {
CaseSensitiveCommands = false, CaseSensitiveCommands = false,
DefaultRunMode = RunMode.Sync, DefaultRunMode = RunMode.Sync,
}); });
#if GLOBAL_NADEKO || DEBUG #if GLOBAL_NADEKO || DEBUG
Client.Log += Client_Log; Client.Log += Client_Log;
#endif #endif
} }
public List<ulong> GetCurrentGuildIds() public List<ulong> 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<IBotCredentials>(_ => _credsProvider.GetCreds()) // bot creds .AddTransient<IBotCredentials>(_ => _credsProvider.GetCreds()) // bot creds
.AddSingleton<IBotCredsProvider>(_credsProvider) .AddSingleton<IBotCredsProvider>(_credsProvider)
.AddSingleton(_db) // database .AddSingleton(_db) // database
@@ -118,248 +113,247 @@ namespace NadekoBot
#else #else
.AddSingleton<ILogCommandService, LogCommandService>() .AddSingleton<ILogCommandService, LogCommandService>()
#endif #endif
; ;
svcs.AddHttpClient(); svcs.AddHttpClient();
svcs.AddHttpClient("memelist").ConfigurePrimaryHttpMessageHandler(() => new HttpClientHandler svcs.AddHttpClient("memelist").ConfigurePrimaryHttpMessageHandler(() => new HttpClientHandler
{ {
AllowAutoRedirect = false AllowAutoRedirect = false
}); });
if (Environment.GetEnvironmentVariable("NADEKOBOT_IS_COORDINATED") != "1") if (Environment.GetEnvironmentVariable("NADEKOBOT_IS_COORDINATED") != "1")
{ {
svcs.AddSingleton<ICoordinator, SingleProcessCoordinator>(); svcs.AddSingleton<ICoordinator, SingleProcessCoordinator>();
} }
else else
{ {
svcs.AddSingleton<RemoteGrpcCoordinator>() svcs.AddSingleton<RemoteGrpcCoordinator>()
.AddSingleton<ICoordinator>(x => x.GetRequiredService<RemoteGrpcCoordinator>()) .AddSingleton<ICoordinator>(x => x.GetRequiredService<RemoteGrpcCoordinator>())
.AddSingleton<IReadyExecutor>(x => x.GetRequiredService<RemoteGrpcCoordinator>()); .AddSingleton<IReadyExecutor>(x => x.GetRequiredService<RemoteGrpcCoordinator>());
} }
svcs.AddSingleton<RedisLocalDataCache>() svcs.AddSingleton<RedisLocalDataCache>()
.AddSingleton<ILocalDataCache>(x => x.GetRequiredService<RedisLocalDataCache>()) .AddSingleton<ILocalDataCache>(x => x.GetRequiredService<RedisLocalDataCache>())
.AddSingleton<RedisImagesCache>() .AddSingleton<RedisImagesCache>()
.AddSingleton<IImageCache>(x => x.GetRequiredService<RedisImagesCache>()) .AddSingleton<IImageCache>(x => x.GetRequiredService<RedisImagesCache>())
.AddSingleton<IReadyExecutor>(x => x.GetRequiredService<RedisImagesCache>()) .AddSingleton<IReadyExecutor>(x => x.GetRequiredService<RedisImagesCache>())
.AddSingleton<IDataCache, RedisCache>(); .AddSingleton<IDataCache, RedisCache>();
svcs.Scan(scan => scan svcs.Scan(scan => scan
.FromAssemblyOf<IReadyExecutor>() .FromAssemblyOf<IReadyExecutor>()
.AddClasses(classes => classes .AddClasses(classes => classes
.AssignableToAny( .AssignableToAny(
// services // services
typeof(INService), typeof(INService),
// behaviours // behaviours
typeof(IEarlyBehavior), typeof(IEarlyBehavior),
typeof(ILateBlocker), typeof(ILateBlocker),
typeof(IInputTransformer), typeof(IInputTransformer),
typeof(ILateExecutor)) typeof(ILateExecutor))
#if GLOBAL_NADEKO #if GLOBAL_NADEKO
.WithoutAttribute<NoPublicBotAttribute>() .WithoutAttribute<NoPublicBotAttribute>()
#endif #endif
) )
.AsSelfWithInterfaces() .AsSelfWithInterfaces()
.WithSingletonLifetime() .WithSingletonLifetime()
); );
//initialize Services //initialize Services
Services = svcs.BuildServiceProvider(); Services = svcs.BuildServiceProvider();
var exec = Services.GetRequiredService<IBehaviourExecutor>(); var exec = Services.GetRequiredService<IBehaviourExecutor>();
exec.Initialize(); exec.Initialize();
if (Client.ShardId == 0) if (Client.ShardId == 0)
{ {
ApplyConfigMigrations(); ApplyConfigMigrations();
} }
_ = LoadTypeReaders(typeof(Bot).Assembly); _ = LoadTypeReaders(typeof(Bot).Assembly);
sw.Stop(); sw.Stop();
Log.Information($"All services loaded in {sw.Elapsed.TotalSeconds:F2}s"); Log.Information($"All services loaded in {sw.Elapsed.TotalSeconds:F2}s");
} }
private void ApplyConfigMigrations() private void ApplyConfigMigrations()
{
// execute all migrators
var migrators = Services.GetServices<IConfigMigrator>();
foreach (var migrator in migrators)
{ {
// execute all migrators migrator.EnsureMigrated();
var migrators = Services.GetServices<IConfigMigrator>();
foreach (var migrator in migrators)
{
migrator.EnsureMigrated();
}
}
private IEnumerable<object> LoadTypeReaders(Assembly assembly)
{
Type[] allTypes;
try
{
allTypes = assembly.GetTypes();
}
catch (ReflectionTypeLoadException ex)
{
Log.Warning(ex.LoaderExceptions[0], "Error getting types");
return Enumerable.Empty<object>();
}
var filteredTypes = allTypes
.Where(x => x.IsSubclassOf(typeof(TypeReader))
&& x.BaseType.GetGenericArguments().Length > 0
&& !x.IsAbstract);
var toReturn = new List<object>();
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<bool>();
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<CommandHandler>();
// 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<IReadyExecutor>();
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);
} }
} }
}
private IEnumerable<object> LoadTypeReaders(Assembly assembly)
{
Type[] allTypes;
try
{
allTypes = assembly.GetTypes();
}
catch (ReflectionTypeLoadException ex)
{
Log.Warning(ex.LoaderExceptions[0], "Error getting types");
return Enumerable.Empty<object>();
}
var filteredTypes = allTypes
.Where(x => x.IsSubclassOf(typeof(TypeReader))
&& x.BaseType.GetGenericArguments().Length > 0
&& !x.IsAbstract);
var toReturn = new List<object>();
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<bool>();
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<CommandHandler>();
// 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<IReadyExecutor>();
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);
}
}

View File

@@ -1,20 +1,17 @@
using System; using System.Runtime.CompilerServices;
using System.Runtime.CompilerServices;
using System.Threading.Tasks; using System.Threading.Tasks;
namespace NadekoBot.Common namespace NadekoBot.Common;
public class AsyncLazy<T> : Lazy<Task<T>>
{ {
public class AsyncLazy<T> : Lazy<Task<T>> public AsyncLazy(Func<T> valueFactory) :
{ base(() => Task.Run(valueFactory))
public AsyncLazy(Func<T> valueFactory) : { }
base(() => Task.Run(valueFactory))
{ }
public AsyncLazy(Func<Task<T>> taskFactory) : public AsyncLazy(Func<Task<T>> taskFactory) :
base(() => Task.Run(taskFactory)) base(() => Task.Run(taskFactory))
{ } { }
public TaskAwaiter<T> GetAwaiter() { return Value.GetAwaiter(); } public TaskAwaiter<T> GetAwaiter() { return Value.GetAwaiter(); }
} }
}

View File

@@ -1,18 +1,13 @@
using System; using System.Runtime.CompilerServices;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Runtime.CompilerServices;
using Discord.Commands; 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 AliasesAttribute([CallerMemberName] string memberName = "")
public sealed class AliasesAttribute : AliasAttribute : base(CommandNameLoadHelper.GetAliasesFor(memberName))
{ {
public AliasesAttribute([CallerMemberName] string memberName = "")
: base(CommandNameLoadHelper.GetAliasesFor(memberName))
{
}
} }
} }

View File

@@ -1,15 +1,14 @@
using Discord.Commands; using Discord.Commands;
namespace Discord namespace Discord;
{
public class BotPermAttribute : RequireBotPermissionAttribute
{
public BotPermAttribute(GuildPerm permission) : base((GuildPermission)permission)
{
}
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)
{
}
}

View File

@@ -1,36 +1,32 @@
using System; using System.IO;
using System.Collections.Generic;
using System.IO;
using System.Linq;
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<Dictionary<string, string[]>> LazyCommandAliases
= new Lazy<Dictionary<string, string[]>>(() => LoadCommandNames());
public static Dictionary<string, string[]> LoadCommandNames(string aliasesFilePath = "data/aliases.yml")
{ {
var text = File.ReadAllText(aliasesFilePath);
private static YamlDotNet.Serialization.IDeserializer _deserializer return _deserializer.Deserialize<Dictionary<string, string[]>>(text);
= new YamlDotNet.Serialization.Deserializer(); }
public static Lazy<Dictionary<string, string[]>> LazyCommandAliases
= new Lazy<Dictionary<string, string[]>>(() => LoadCommandNames());
public static Dictionary<string, string[]> LoadCommandNames(string aliasesFilePath = "data/aliases.yml")
{
var text = File.ReadAllText(aliasesFilePath);
return _deserializer.Deserialize<Dictionary<string, string[]>>(text);
}
public static string[] GetAliasesFor(string methodName) public static string[] GetAliasesFor(string methodName)
=> LazyCommandAliases.Value.TryGetValue(methodName.ToLowerInvariant(), out var aliases) && aliases.Length > 1 => LazyCommandAliases.Value.TryGetValue(methodName.ToLowerInvariant(), out var aliases) && aliases.Length > 1
? aliases.Skip(1).ToArray() ? aliases.Skip(1).ToArray()
: Array.Empty<string>(); : Array.Empty<string>();
public static string GetCommandNameFor(string methodName) public static string GetCommandNameFor(string methodName)
{ {
methodName = methodName.ToLowerInvariant(); methodName = methodName.ToLowerInvariant();
var toReturn = LazyCommandAliases.Value.TryGetValue(methodName, out var aliases) && aliases.Length > 0 var toReturn = LazyCommandAliases.Value.TryGetValue(methodName, out var aliases) && aliases.Length > 0
? aliases[0] ? aliases[0]
: methodName; : methodName;
return toReturn; return toReturn;
}
} }
} }

View File

@@ -1,16 +1,12 @@
using System; using Discord.Commands;
using System.Runtime.CompilerServices;
using Discord.Commands;
using NadekoBot.Services;
namespace NadekoBot.Common.Attributes namespace NadekoBot.Common.Attributes;
[AttributeUsage(AttributeTargets.Method)]
public sealed class DescriptionAttribute : SummaryAttribute
{ {
[AttributeUsage(AttributeTargets.Method)] // Localization.LoadCommand(memberName.ToLowerInvariant()).Desc
public sealed class DescriptionAttribute : SummaryAttribute public DescriptionAttribute(string text = "") : base(text)
{ {
// Localization.LoadCommand(memberName.ToLowerInvariant()).Desc
public DescriptionAttribute(string text = "") : base(text)
{
}
} }
} }

View File

@@ -1,9 +1,8 @@
namespace Discord.Commands namespace Discord.Commands;
public class LeftoverAttribute : RemainderAttribute
{ {
public class LeftoverAttribute : RemainderAttribute public LeftoverAttribute()
{ {
public LeftoverAttribute()
{
}
} }
} }

View File

@@ -1,19 +1,16 @@
using System; using System.Runtime.CompilerServices;
using System.Runtime.CompilerServices;
using Discord.Commands; 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 NadekoCommandAttribute([CallerMemberName] string memberName="")
public sealed class NadekoCommandAttribute : CommandAttribute : base(CommandNameLoadHelper.GetCommandNameFor(memberName))
{ {
public NadekoCommandAttribute([CallerMemberName] string memberName="") this.MethodName = memberName.ToLowerInvariant();
: base(CommandNameLoadHelper.GetCommandNameFor(memberName))
{
this.MethodName = memberName.ToLowerInvariant();
}
public string MethodName { get; }
} }
}
public string MethodName { get; }
}

View File

@@ -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)] public NadekoModuleAttribute(string moduleName) : base(moduleName)
sealed class NadekoModuleAttribute : GroupAttribute
{ {
public NadekoModuleAttribute(string moduleName) : base(moduleName)
{
}
} }
} }

View File

@@ -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 Type OptionType { get; set; }
public sealed class NadekoOptionsAttribute : Attribute
{
public Type OptionType { get; set; }
public NadekoOptionsAttribute(Type t) public NadekoOptionsAttribute(Type t)
{ {
this.OptionType = t; this.OptionType = t;
}
} }
} }

View File

@@ -1,20 +1,18 @@
using System; using System.Threading.Tasks;
using System.Threading.Tasks;
using Discord.Commands; using Discord.Commands;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using NadekoBot.Extensions; using NadekoBot.Extensions;
using NadekoBot.Services; using NadekoBot.Services;
namespace NadekoBot.Common.Attributes namespace NadekoBot.Common.Attributes;
{
[AttributeUsage(AttributeTargets.Method | AttributeTargets.Class)]
public sealed class OwnerOnlyAttribute : PreconditionAttribute
{
public override Task<PreconditionResult> CheckPermissionsAsync(ICommandContext context, CommandInfo executingCommand, IServiceProvider services)
{
var creds = services.GetRequiredService<IBotCredsProvider>().GetCreds();
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<PreconditionResult> CheckPermissionsAsync(ICommandContext context, CommandInfo executingCommand, IServiceProvider services)
{
var creds = services.GetRequiredService<IBotCredsProvider>().GetCreds();
return Task.FromResult((creds.IsOwner(context.User) || context.Client.CurrentUser.Id == context.User.Id ? PreconditionResult.FromSuccess() : PreconditionResult.FromError("Not owner")));
} }
} }

View File

@@ -1,38 +1,36 @@
using Discord.Commands; using Discord.Commands;
using NadekoBot.Services; using NadekoBot.Services;
using System;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
namespace NadekoBot.Common.Attributes namespace NadekoBot.Common.Attributes;
[AttributeUsage(AttributeTargets.Method)]
public sealed class RatelimitAttribute : PreconditionAttribute
{ {
[AttributeUsage(AttributeTargets.Method)] public int Seconds { get; }
public sealed class RatelimitAttribute : PreconditionAttribute
public RatelimitAttribute(int seconds)
{ {
public int Seconds { get; } if (seconds <= 0)
throw new ArgumentOutOfRangeException(nameof(seconds));
public RatelimitAttribute(int seconds) Seconds = seconds;
{
if (seconds <= 0)
throw new ArgumentOutOfRangeException(nameof(seconds));
Seconds = seconds;
}
public override Task<PreconditionResult> CheckPermissionsAsync(ICommandContext context, CommandInfo command, IServiceProvider services)
{
if (Seconds == 0)
return Task.FromResult(PreconditionResult.FromSuccess());
var cache = services.GetRequiredService<IDataCache>();
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));
}
} }
}
public override Task<PreconditionResult> CheckPermissionsAsync(ICommandContext context, CommandInfo command, IServiceProvider services)
{
if (Seconds == 0)
return Task.FromResult(PreconditionResult.FromSuccess());
var cache = services.GetRequiredService<IDataCache>();
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));
}
}

View File

@@ -1,21 +1,16 @@
using System; using Discord.Commands;
using System.Runtime.CompilerServices;
using Discord.Commands;
using NadekoBot.Services;
using Newtonsoft.Json;
namespace NadekoBot.Common.Attributes namespace NadekoBot.Common.Attributes;
[AttributeUsage(AttributeTargets.Method)]
public sealed class UsageAttribute : RemarksAttribute
{ {
[AttributeUsage(AttributeTargets.Method)] // public static string GetUsage(string memberName)
public sealed class UsageAttribute : RemarksAttribute // {
// 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)
{
}
} }
} }

View File

@@ -1,33 +1,31 @@
using System; using System.Threading.Tasks;
using System.Threading.Tasks;
using Discord.Commands; using Discord.Commands;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using NadekoBot.Modules.Administration.Services; using NadekoBot.Modules.Administration.Services;
namespace Discord namespace Discord;
[AttributeUsage(AttributeTargets.Method, AllowMultiple = false)]
public class UserPermAttribute : PreconditionAttribute
{ {
[AttributeUsage(AttributeTargets.Method, AllowMultiple = false)] public RequireUserPermissionAttribute UserPermissionAttribute { get; }
public class UserPermAttribute : PreconditionAttribute
public UserPermAttribute(GuildPerm permission)
{ {
public RequireUserPermissionAttribute UserPermissionAttribute { get; } UserPermissionAttribute = new RequireUserPermissionAttribute((GuildPermission)permission);
public UserPermAttribute(GuildPerm permission)
{
UserPermissionAttribute = new RequireUserPermissionAttribute((GuildPermission)permission);
}
public UserPermAttribute(ChannelPerm permission)
{
UserPermissionAttribute = new RequireUserPermissionAttribute((ChannelPermission)permission);
}
public override Task<PreconditionResult> CheckPermissionsAsync(ICommandContext context, CommandInfo command, IServiceProvider services)
{
var permService = services.GetRequiredService<DiscordPermOverrideService>();
if (permService.TryGetOverrides(context.Guild?.Id ?? 0, command.Name.ToUpperInvariant(), out var _))
return Task.FromResult(PreconditionResult.FromSuccess());
return UserPermissionAttribute.CheckPermissionsAsync(context, command, services);
}
} }
}
public UserPermAttribute(ChannelPerm permission)
{
UserPermissionAttribute = new RequireUserPermissionAttribute((ChannelPermission)permission);
}
public override Task<PreconditionResult> CheckPermissionsAsync(ICommandContext context, CommandInfo command, IServiceProvider services)
{
var permService = services.GetRequiredService<DiscordPermOverrideService>();
if (permService.TryGetOverrides(context.Guild?.Id ?? 0, command.Name.ToUpperInvariant(), out var _))
return Task.FromResult(PreconditionResult.FromSuccess());
return UserPermissionAttribute.CheckPermissionsAsync(context, command, services);
}
}

View File

@@ -1,20 +1,19 @@
using Newtonsoft.Json; using Newtonsoft.Json;
namespace NadekoBot.Common namespace NadekoBot.Common;
{
public class CmdStrings
{
public string[] Usages { get; }
public string Description { get; }
[JsonConstructor] public class CmdStrings
public CmdStrings( {
[JsonProperty("args")]string[] usages, public string[] Usages { get; }
[JsonProperty("desc")]string description public string Description { get; }
)
{ [JsonConstructor]
Usages = usages; public CmdStrings(
Description = description; [JsonProperty("args")]string[] usages,
} [JsonProperty("desc")]string description
)
{
Usages = usages;
Description = description;
} }
} }

File diff suppressed because it is too large Load Diff

View File

@@ -1,77 +1,74 @@
using System; using System.Collections;
using System.Collections;
using System.Collections.Generic;
namespace NadekoBot.Common.Collections namespace NadekoBot.Common.Collections;
public static class DisposableReadOnlyListExtensions
{ {
public static class DisposableReadOnlyListExtensions public static IDisposableReadOnlyList<T> AsDisposable<T>(this IReadOnlyList<T> arr) where T : IDisposable
{ => new DisposableReadOnlyList<T>(arr);
public static IDisposableReadOnlyList<T> AsDisposable<T>(this IReadOnlyList<T> arr) where T : IDisposable
=> new DisposableReadOnlyList<T>(arr);
public static IDisposableReadOnlyList<KeyValuePair<TKey, TValue>> AsDisposable<TKey, TValue>(this IReadOnlyList<KeyValuePair<TKey, TValue>> arr) where TValue : IDisposable public static IDisposableReadOnlyList<KeyValuePair<TKey, TValue>> AsDisposable<TKey, TValue>(this IReadOnlyList<KeyValuePair<TKey, TValue>> arr) where TValue : IDisposable
=> new DisposableReadOnlyList<TKey, TValue>(arr); => new DisposableReadOnlyList<TKey, TValue>(arr);
}
public interface IDisposableReadOnlyList<T> : IReadOnlyList<T>, IDisposable
{
}
public sealed class DisposableReadOnlyList<T> : IDisposableReadOnlyList<T>
where T : IDisposable
{
private readonly IReadOnlyList<T> _arr;
public int Count => _arr.Count;
public T this[int index] => _arr[index];
public DisposableReadOnlyList(IReadOnlyList<T> arr)
{
this._arr = arr;
} }
public interface IDisposableReadOnlyList<T> : IReadOnlyList<T>, IDisposable public IEnumerator<T> GetEnumerator()
=> _arr.GetEnumerator();
IEnumerator IEnumerable.GetEnumerator()
=> _arr.GetEnumerator();
public void Dispose()
{ {
} foreach (var item in _arr)
public sealed class DisposableReadOnlyList<T> : IDisposableReadOnlyList<T>
where T : IDisposable
{
private readonly IReadOnlyList<T> _arr;
public int Count => _arr.Count;
public T this[int index] => _arr[index];
public DisposableReadOnlyList(IReadOnlyList<T> arr)
{ {
this._arr = arr; item.Dispose();
}
public IEnumerator<T> GetEnumerator()
=> _arr.GetEnumerator();
IEnumerator IEnumerable.GetEnumerator()
=> _arr.GetEnumerator();
public void Dispose()
{
foreach (var item in _arr)
{
item.Dispose();
}
}
}
public sealed class DisposableReadOnlyList<T, U> : IDisposableReadOnlyList<KeyValuePair<T, U>>
where U : IDisposable
{
private readonly IReadOnlyList<KeyValuePair<T, U>> _arr;
public int Count => _arr.Count;
KeyValuePair<T, U> IReadOnlyList<KeyValuePair<T, U>>.this[int index] => _arr[index];
public DisposableReadOnlyList(IReadOnlyList<KeyValuePair<T, U>> arr)
{
this._arr = arr;
}
public IEnumerator<KeyValuePair<T, U>> GetEnumerator() =>
_arr.GetEnumerator();
IEnumerator IEnumerable.GetEnumerator() =>
_arr.GetEnumerator();
public void Dispose()
{
foreach (var item in _arr)
{
item.Value.Dispose();
}
} }
} }
} }
public sealed class DisposableReadOnlyList<T, U> : IDisposableReadOnlyList<KeyValuePair<T, U>>
where U : IDisposable
{
private readonly IReadOnlyList<KeyValuePair<T, U>> _arr;
public int Count => _arr.Count;
KeyValuePair<T, U> IReadOnlyList<KeyValuePair<T, U>>.this[int index] => _arr[index];
public DisposableReadOnlyList(IReadOnlyList<KeyValuePair<T, U>> arr)
{
this._arr = arr;
}
public IEnumerator<KeyValuePair<T, U>> GetEnumerator() =>
_arr.GetEnumerator();
IEnumerator IEnumerable.GetEnumerator() =>
_arr.GetEnumerator();
public void Dispose()
{
foreach (var item in _arr)
{
item.Value.Dispose();
}
}
}

View File

@@ -1,141 +1,138 @@
using System.Collections; using System.Collections;
using System.Collections.Generic;
using System.Linq;
using NadekoBot.Services.Database.Models; using NadekoBot.Services.Database.Models;
namespace NadekoBot.Common.Collections namespace NadekoBot.Common.Collections;
public class IndexedCollection<T> : IList<T> where T : class, IIndexed
{ {
public class IndexedCollection<T> : IList<T> where T : class, IIndexed public List<T> 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<T> Source { get; } Source = new List<T>();
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<T>();
}
public IndexedCollection(IEnumerable<T> source) public IndexedCollection(IEnumerable<T> 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(); if (Source[i].Index != i)
UpdateIndexes(); Source[i].Index = i;
} }
} }
}
public void UpdateIndexes() public static implicit operator List<T>(IndexedCollection<T> x) =>
x.Source;
public List<T> ToList() => Source.ToList();
public IEnumerator<T> 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) if (Source[i].Index != i)
Source[i].Index = i; Source[i].Index = i;
} }
} }
} }
return removed;
}
public static implicit operator List<T>(IndexedCollection<T> x) => public virtual void Insert(int index, T item)
x.Source; {
lock (_locker)
public List<T> ToList() => Source.ToList();
public IEnumerator<T> GetEnumerator() =>
Source.GetEnumerator();
IEnumerator IEnumerable.GetEnumerator() =>
Source.GetEnumerator();
public void Add(T item)
{ {
lock (_locker) Source.Insert(index, item);
for (int i = index; i < Source.Count; i++)
{ {
item.Index = Source.Count; Source[i].Index = i;
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;
}
} }
} }
} }
}
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;
}
}
}
}

View File

@@ -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 Cmd { get; set; } public string[] Usage { get; set; }
public string Desc { get; set; } }
public string[] Usage { get; set; }
}
}

View File

@@ -1,69 +1,68 @@
using System.Collections.Generic; using System.Globalization;
using System.Globalization;
using Cloneable; using Cloneable;
using NadekoBot.Common.Yml; using NadekoBot.Common.Yml;
using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.PixelFormats;
using YamlDotNet.Core; using YamlDotNet.Core;
using YamlDotNet.Serialization; using YamlDotNet.Serialization;
namespace NadekoBot.Common.Configs namespace NadekoBot.Common.Configs;
{
[Cloneable]
public sealed partial class BotConfig : ICloneable<BotConfig>
{
[Comment(@"DO NOT CHANGE")]
public int Version { get; set; } = 2;
[Comment(@"Most commands, when executed, have a small colored line [Cloneable]
public sealed partial class BotConfig : ICloneable<BotConfig>
{
[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 next to the response. The color depends whether the command
is completed, errored or in progress (pending) is completed, errored or in progress (pending)
Color settings below are for the color of those lines. Color settings below are for the color of those lines.
To get color's hex, you can go here https://htmlcolorcodes.com/ To get color's hex, you can go here https://htmlcolorcodes.com/
and copy the hex code fo your selected color (marked as #)")] 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)")] [Comment("Default bot language. It has to be in the list of supported languages (.langli)")]
public CultureInfo DefaultLocale { get; set; } 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")] 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. // [Comment(@"For what kind of updates will the bot check.
// Allowed values: Release, Commit, None")] // Allowed values: Release, Commit, None")]
// public UpdateCheckType CheckForUpdates { get; set; } // public UpdateCheckType CheckForUpdates { get; set; }
// [Comment(@"How often will the bot check for updates, in hours")] // [Comment(@"How often will the bot check for updates, in hours")]
// public int CheckUpdateInterval { get; set; } // 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)?")] [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; } 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)")] 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. 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")] Supports embeds. How it looks: https://puu.sh/B0BLV.png")]
[YamlMember(ScalarStyle = ScalarStyle.Literal)] [YamlMember(ScalarStyle = ScalarStyle.Literal)]
public string DmHelpText { get; set; } 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. Case insensitive.
Leave empty to reply with DmHelpText to every DM.")] Leave empty to reply with DmHelpText to every DM.")]
public List<string> DmHelpTextKeywords { get; set; } public List<string> DmHelpTextKeywords { get; set; }
[Comment(@"This is the response for the .h command")] [Comment(@"This is the response for the .h command")]
[YamlMember(ScalarStyle = ScalarStyle.Literal)] [YamlMember(ScalarStyle = ScalarStyle.Literal)]
public string HelpText { get; set; } public string HelpText { get; set; }
[Comment(@"List of modules and commands completely blocked on the bot")] [Comment(@"List of modules and commands completely blocked on the bot")]
public BlockedConfig Blocked { get; set; } public BlockedConfig Blocked { get; set; }
[Comment(@"Which string will be used to recognize the commands")] [Comment(@"Which string will be used to recognize the commands")]
public string Prefix { get; set; } 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 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. 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. 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 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, 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.")] 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. This setting can be changed via .rots command.
See RotatingStatuses submodule in Administration.")] 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. // [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 // 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")] // 'cash @Someone!' if your prefixIsSuffix: true")]
// public bool PrefixIsSuffix { get; set; } // public bool PrefixIsSuffix { get; set; }
// public string Prefixed(string text) => PrefixIsSuffix // public string Prefixed(string text) => PrefixIsSuffix
// ? text + Prefix // ? text + Prefix
// : Prefix + text; // : Prefix + text;
public string Prefixed(string text) public string Prefixed(string text)
=> Prefix + text; => Prefix + text;
public BotConfig() public BotConfig()
{ {
var color = new ColorConfig(); var color = new ColorConfig();
Color = color; Color = color;
DefaultLocale = new CultureInfo("en-US"); DefaultLocale = new CultureInfo("en-US");
ConsoleOutputType = ConsoleOutputType.Normal; ConsoleOutputType = ConsoleOutputType.Normal;
ForwardMessages = false; ForwardMessages = false;
ForwardToAllOwners = false; ForwardToAllOwners = false;
DmHelpText = @"{""description"": ""Type `%prefix%h` for help.""}"; DmHelpText = @"{""description"": ""Type `%prefix%h` for help.""}";
HelpText = @"{ HelpText = @"{
""title"": ""To invite me to your server, use this link"", ""title"": ""To invite me to your server, use this link"",
""description"": ""https://discordapp.com/oauth2/authorize?client_id={0}&scope=bot&permissions=66186303"", ""description"": ""https://discordapp.com/oauth2/authorize?client_id={0}&scope=bot&permissions=66186303"",
""color"": 53380, ""color"": 53380,
@@ -126,59 +125,58 @@ See RotatingStatuses submodule in Administration.")]
} }
] ]
}"; }";
var blocked = new BlockedConfig(); var blocked = new BlockedConfig();
Blocked = blocked; Blocked = blocked;
Prefix = "."; Prefix = ".";
RotateStatuses = false; RotateStatuses = false;
GroupGreets = false; GroupGreets = false;
DmHelpTextKeywords = new List<string>() DmHelpTextKeywords = new List<string>()
{
"help",
"commands",
"cmds",
"module",
"can you do"
};
}
}
[Cloneable]
public sealed partial class BlockedConfig
{
public HashSet<string> Commands { get; set; }
public HashSet<string> Modules { get; set; }
public BlockedConfig()
{ {
Modules = new HashSet<string>(); "help",
Commands = new HashSet<string>(); "commands",
} "cmds",
"module",
"can you do"
};
} }
}
[Cloneable] [Cloneable]
public partial class ColorConfig public sealed partial class BlockedConfig
{
public HashSet<string> Commands { get; set; }
public HashSet<string> Modules { get; set; }
public BlockedConfig()
{ {
[Comment(@"Color used for embed responses when command successfully executes")] Modules = new HashSet<string>();
public Rgba32 Ok { get; set; } Commands = new HashSet<string>();
[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");
}
} }
}
[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 public enum ConsoleOutputType
{ {
Normal = 0, Normal = 0,
Simple = 1, Simple = 1,
None = 2, None = 2,
}
} }

View File

@@ -1,18 +1,17 @@
namespace NadekoBot.Common.Configs namespace NadekoBot.Common.Configs;
/// <summary>
/// Base interface for available config serializers
/// </summary>
public interface IConfigSeria
{ {
/// <summary> /// <summary>
/// Base interface for available config serializers /// Serialize the object to string
/// </summary> /// </summary>
public interface IConfigSeria public string Serialize<T>(T obj);
{
/// <summary>
/// Serialize the object to string
/// </summary>
public string Serialize<T>(T obj);
/// <summary> /// <summary>
/// Deserialize string data into an object of the specified type /// Deserialize string data into an object of the specified type
/// </summary> /// </summary>
public T Deserialize<T>(string data); public T Deserialize<T>(string data);
}
} }

View File

@@ -1,97 +1,95 @@
using System.Collections.Generic; using NadekoBot.Common.Yml;
using NadekoBot.Common.Yml;
using YamlDotNet.Serialization;
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<ulong>();
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; Type = "sqlite",
Token = string.Empty; ConnectionString = "Data Source=data/NadekoBot.db"
OwnerIds = new List<ulong>(); };
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"
};
CoordinatorUrl = "http://localhost:3442"; CoordinatorUrl = "http://localhost:3442";
RestartCommand = new() RestartCommand = new()
{ {
}; };
} }
[Comment(@"DO NOT CHANGE")] [Comment(@"DO NOT CHANGE")]
public int Version { get; set; } public int Version { get; set; }
[Comment(@"Bot token. Do not share with anyone ever -> https://discordapp.com/developers/applications/")] [Comment(@"Bot token. Do not share with anyone ever -> https://discordapp.com/developers/applications/")]
public string Token { get; set; } 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**")] **DO NOT ADD PEOPLE YOU DON'T TRUST**")]
public ICollection<ulong> OwnerIds { get; set; } public ICollection<ulong> 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.")] 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. Then, go to APIs and Services -> Credentials and click Create credentials -> API key.
Used only for Youtube Data Api (at the moment).")] 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.")] [Comment(@"Settings for voting system for discordbots. Meant for use on global Nadeko.")]
public VotesSettings Votes { get; set; } 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")] 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.")] [Comment(@"Api key for sending stats to DiscordBotList.")]
public string BotListToken { get; set; } public string BotListToken { get; set; }
[Comment(@"Official cleverbot api key.")] [Comment(@"Official cleverbot api key.")]
public string CleverbotApiKey { get; set; } public string CleverbotApiKey { get; set; }
[Comment(@"Redis connection string. Don't change if you don't know what you're doing.")] [Comment(@"Redis connection string. Don't change if you don't know what you're doing.")]
public string RedisOptions { get; set; } 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")] [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; } 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.")] 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)")] [Comment(@"Api key obtained on https://rapidapi.com (go to MyApps -> Add New App -> Enter Name -> Application key)")]
public string RapidApiKey { get; set; } 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.")] 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")] 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.")] 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")] [Comment(@"Api key used for Osu related commands. Obtain this key at https://osu.ppy.sh/p/api")]
public string OsuApiKey { get; set; } 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) Only used if bot is executed directly (NOT through the coordinator)
placeholders: placeholders:
{0} -> shard id {0} -> shard id
@@ -102,118 +100,117 @@ Linux default
Windows default Windows default
cmd: NadekoBot.exe cmd: NadekoBot.exe
args: {0}")] 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")] AccessToken = accessToken;
public string Type { get; set; } RefreshToken = refreshToken;
[Comment(@"Connection string. Will default to ""Data Source=data/NadekoBot.db""")] ClientSecret = clientSecret;
public string ConnectionString { get; set; } CampaignId = campaignId;
}
// 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()
{
}
} }
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; }
}
}
}

View File

@@ -1,43 +1,41 @@
using NadekoBot.Services; using NadekoBot.Services;
using System;
using System.Collections.Concurrent; using System.Collections.Concurrent;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Discord; using Discord;
namespace NadekoBot.Common namespace NadekoBot.Common;
public class DownloadTracker : INService
{ {
public class DownloadTracker : INService private ConcurrentDictionary<ulong, DateTime> LastDownloads { get; } = new ConcurrentDictionary<ulong, DateTime>();
private SemaphoreSlim downloadUsersSemaphore = new SemaphoreSlim(1, 1);
/// <summary>
/// Ensures all users on the specified guild were downloaded within the last hour.
/// </summary>
/// <param name="guild">Guild to check and potentially download users from</param>
/// <returns>Task representing download state</returns>
public async Task EnsureUsersDownloadedAsync(IGuild guild)
{ {
private ConcurrentDictionary<ulong, DateTime> LastDownloads { get; } = new ConcurrentDictionary<ulong, DateTime>(); await downloadUsersSemaphore.WaitAsync();
private SemaphoreSlim downloadUsersSemaphore = new SemaphoreSlim(1, 1); try
/// <summary>
/// Ensures all users on the specified guild were downloaded within the last hour.
/// </summary>
/// <param name="guild">Guild to check and potentially download users from</param>
/// <returns>Task representing download state</returns>
public async Task EnsureUsersDownloadedAsync(IGuild guild)
{ {
await downloadUsersSemaphore.WaitAsync(); var now = DateTime.UtcNow;
try
{
var now = DateTime.UtcNow;
// download once per hour at most // download once per hour at most
var added = LastDownloads.AddOrUpdate( var added = LastDownloads.AddOrUpdate(
guild.Id, guild.Id,
now, now,
(key, old) => (now - old) > TimeSpan.FromHours(1) ? now : old); (key, old) => (now - old) > TimeSpan.FromHours(1) ? now : old);
// means that this entry was just added - download the users // means that this entry was just added - download the users
if (added == now) if (added == now)
await guild.DownloadUsersAsync(); await guild.DownloadUsersAsync();
} }
finally finally
{ {
downloadUsersSemaphore.Release(); downloadUsersSemaphore.Release();
}
} }
} }
} }

View File

@@ -1,11 +1,9 @@
using Discord; 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);
}
} }

View File

@@ -1,81 +1,77 @@
using System; using System.Reflection;
using System.Linq;
using System.Reflection;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using NadekoBot.Common; using NadekoBot.Common;
using NadekoBot.Modules.Music; using NadekoBot.Modules.Music;
using NadekoBot.Services; using NadekoBot.Services;
using NadekoBot.Modules.Administration.Services;
using NadekoBot.Modules.Music.Resolvers; using NadekoBot.Modules.Music.Resolvers;
using NadekoBot.Modules.Music.Services; using NadekoBot.Modules.Music.Services;
using StackExchange.Redis; 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<IStringsSource, LocalFileStringsSource>()
.AddSingleton<IBotStringsProvider, LocalBotStringsProvider>()
.AddSingleton<IBotStrings, BotStrings>()
: services.AddSingleton<IStringsSource, LocalFileStringsSource>()
.AddSingleton<IBotStringsProvider, RedisBotStringsProvider>()
.AddSingleton<IBotStrings, BotStrings>();
public static IServiceCollection AddConfigServices(this IServiceCollection services)
{ {
public static IServiceCollection AddBotStringsServices(this IServiceCollection services, int totalShards) var baseType = typeof(ConfigServiceBase<>);
=> totalShards <= 1
? services
.AddSingleton<IStringsSource, LocalFileStringsSource>()
.AddSingleton<IBotStringsProvider, LocalBotStringsProvider>()
.AddSingleton<IBotStrings, BotStrings>()
: services.AddSingleton<IStringsSource, LocalFileStringsSource>()
.AddSingleton<IBotStringsProvider, RedisBotStringsProvider>()
.AddSingleton<IBotStrings, BotStrings>();
public static IServiceCollection AddConfigServices(this IServiceCollection services) foreach (var type in Assembly.GetCallingAssembly().ExportedTypes.Where(x => x.IsSealed))
{ {
var baseType = typeof(ConfigServiceBase<>); if (type.BaseType?.IsGenericType == true && type.BaseType.GetGenericTypeDefinition() == baseType)
foreach (var type in Assembly.GetCallingAssembly().ExportedTypes.Where(x => x.IsSealed))
{ {
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) return services;
=> services.AddSealedSubclassesOf(typeof(IConfigMigrator)); }
public static IServiceCollection AddMusic(this IServiceCollection services) public static IServiceCollection AddConfigMigrators(this IServiceCollection services)
=> services => services.AddSealedSubclassesOf(typeof(IConfigMigrator));
.AddSingleton<IMusicService, MusicService>()
.AddSingleton<ITrackResolveProvider, TrackResolveProvider>() public static IServiceCollection AddMusic(this IServiceCollection services)
.AddSingleton<IYoutubeResolver, YtdlYoutubeResolver>() => services
.AddSingleton<ISoundcloudResolver, SoundcloudResolver>() .AddSingleton<IMusicService, MusicService>()
.AddSingleton<ILocalTrackResolver, LocalTrackResolver>() .AddSingleton<ITrackResolveProvider, TrackResolveProvider>()
.AddSingleton<IRadioResolver, RadioResolver>() .AddSingleton<IYoutubeResolver, YtdlYoutubeResolver>()
.AddSingleton<ITrackCacher, RedisTrackCacher>() .AddSingleton<ISoundcloudResolver, SoundcloudResolver>()
.AddSingleton<YtLoader>() .AddSingleton<ILocalTrackResolver, LocalTrackResolver>()
.AddSingleton<IPlaceholderProvider>(svc => svc.GetRequiredService<IMusicService>()); .AddSingleton<IRadioResolver, RadioResolver>()
.AddSingleton<ITrackCacher, RedisTrackCacher>()
.AddSingleton<YtLoader>()
.AddSingleton<IPlaceholderProvider>(svc => svc.GetRequiredService<IMusicService>());
// consider using scrutor, because slightly different versions // consider using scrutor, because slightly different versions
// of this might be needed in several different places // of this might be needed in several different places
public static IServiceCollection AddSealedSubclassesOf(this IServiceCollection services, Type baseType) 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() services.AddSingleton(baseType, subType);
.ExportedTypes
.Where(type => type.IsSealed && baseType.IsAssignableFrom(type));
foreach (var subType in subTypes)
{
services.AddSingleton(baseType, subType);
}
return services;
} }
public static IServiceCollection AddRedis(this IServiceCollection services, string redisOptions) return services;
{ }
var conf = ConfigurationOptions.Parse(redisOptions);
services.AddSingleton(ConnectionMultiplexer.Connect(conf)); public static IServiceCollection AddRedis(this IServiceCollection services, string redisOptions)
return services; {
} var conf = ConfigurationOptions.Parse(redisOptions);
services.AddSingleton(ConnectionMultiplexer.Connect(conf));
return services;
} }
} }

View File

@@ -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: // Summary:
// Defines the available permissions for a channel. // Allows creation of instant invites.
[Flags] CreateInstantInvite = 1,
public enum ChannelPerm : ulong //
{ // Summary:
// // Allows kicking members.
// Summary: //
// Allows creation of instant invites. // Remarks:
CreateInstantInvite = 1, // This permission requires the owner account to use two-factor authentication when
// // used on a guild that has server-wide 2FA enabled.
// Summary: KickMembers = 2,
// Allows management and editing of channels. //
ManageChannel = 16, // Summary:
// // Allows banning members.
// Summary: //
// Allows for the addition of reactions to messages. // Remarks:
AddReactions = 64, // This permission requires the owner account to use two-factor authentication when
PrioritySpeaker = 256, // used on a guild that has server-wide 2FA enabled.
// BanMembers = 4,
// Summary: //
// Allows for reading of messages. This flag is obsolete, use Discord.ChannelPermission.ViewChannel // Summary:
// instead. // Allows all permissions and bypasses channel permission overwrites.
ReadMessages = 1024, //
// // Remarks:
// Summary: // This permission requires the owner account to use two-factor authentication when
// Allows guild members to view a channel, which includes reading messages in text // used on a guild that has server-wide 2FA enabled.
// channels. Administrator = 8,
ViewChannel = 1024, //
// // Summary:
// Summary: // Allows management and editing of channels.
// Allows for sending messages in a channel. //
SendMessages = 2048, // Remarks:
// // This permission requires the owner account to use two-factor authentication when
// Summary: // used on a guild that has server-wide 2FA enabled.
// Allows for sending of text-to-speech messages. ManageChannels = 16,
SendTTSMessages = 4096, //
// // Summary:
// Summary: // Allows management and editing of the guild.
// Allows for deletion of other users messages. //
ManageMessages = 8192, // Remarks:
// // This permission requires the owner account to use two-factor authentication when
// Summary: // used on a guild that has server-wide 2FA enabled.
// Allows links sent by users with this permission will be auto-embedded. ManageGuild = 32,
EmbedLinks = 16384, //
// // Summary:
// Summary: // Allows for the addition of reactions to messages.
// Allows for uploading images and files. AddReactions = 64,
AttachFiles = 32768, //
// // Summary:
// Summary: // Allows for viewing of audit logs.
// Allows for reading of message history. ViewAuditLog = 128,
ReadMessageHistory = 65536, PrioritySpeaker = 256,
// ReadMessages = 1024,
// Summary: ViewChannel = 1024,
// Allows for using the @everyone tag to notify all users in a channel, and the SendMessages = 2048,
// @here tag to notify all online users in a channel. //
MentionEveryone = 131072, // Summary:
// // Allows for sending of text-to-speech messages.
// Summary: SendTTSMessages = 4096,
// Allows the usage of custom emojis from other servers. //
UseExternalEmojis = 262144, // Summary:
// // Allows for deletion of other users messages.
// Summary: //
// Allows for joining of a voice channel. // Remarks:
Connect = 1048576, // This permission requires the owner account to use two-factor authentication when
// // used on a guild that has server-wide 2FA enabled.
// Summary: ManageMessages = 8192,
// Allows for speaking in a voice channel. //
Speak = 2097152, // Summary:
// // Allows links sent by users with this permission will be auto-embedded.
// Summary: EmbedLinks = 16384,
// Allows for muting members in a voice channel. //
MuteMembers = 4194304, // Summary:
// // Allows for uploading images and files.
// Summary: AttachFiles = 32768,
// Allows for deafening of members in a voice channel. //
DeafenMembers = 8388608, // Summary:
// // Allows for reading of message history.
// Summary: ReadMessageHistory = 65536,
// Allows for moving of members between voice channels. //
MoveMembers = 16777216, // Summary:
// // Allows for using the @everyone tag to notify all users in a channel, and the
// Summary: // @here tag to notify all online users in a channel.
// Allows for using voice-activity-detection in a voice channel. MentionEveryone = 131072,
UseVAD = 33554432, //
// // Summary:
// Summary: // Allows the usage of custom emojis from other servers.
// Allows management and editing of roles. UseExternalEmojis = 262144,
ManageRoles = 268435456, //
// // Summary:
// Summary: // Allows for joining of a voice channel.
// Allows management and editing of webhooks. Connect = 1048576,
ManageWebhooks = 536870912 //
} // 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
}

View File

@@ -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);
}
} }
} }

View File

@@ -1,36 +1,31 @@
using System.Collections.Generic; using NadekoBot.Common;
using Discord;
using System.Collections.Immutable;
using System.Linq;
using NadekoBot.Common;
namespace NadekoBot namespace NadekoBot;
public interface IBotCredentials
{ {
public interface IBotCredentials string Token { get; }
{ string GoogleApiKey { get; }
string Token { get; } ICollection<ulong> OwnerIds { get; }
string GoogleApiKey { get; } string RapidApiKey { get; }
ICollection<ulong> OwnerIds { get; }
string RapidApiKey { get; }
Creds.DbOptions Db { get; } Creds.DbOptions Db { get; }
string OsuApiKey { get; } string OsuApiKey { get; }
int TotalShards { get; } int TotalShards { get; }
Creds.PatreonSettings Patreon { get; } Creds.PatreonSettings Patreon { get; }
string CleverbotApiKey { get; } string CleverbotApiKey { get; }
RestartConfig RestartCommand { get; } RestartConfig RestartCommand { get; }
Creds.VotesSettings Votes { get; } Creds.VotesSettings Votes { get; }
string BotListToken { get; } string BotListToken { get; }
string RedisOptions { get; } string RedisOptions { get; }
string LocationIqApiKey { get; } string LocationIqApiKey { get; }
string TimezoneDbApiKey { get; } string TimezoneDbApiKey { get; }
string CoinmarketcapApiKey { get; } string CoinmarketcapApiKey { get; }
string CoordinatorUrl { get; set; } string CoordinatorUrl { get; set; }
}
public class RestartConfig
{
public string Cmd { get; set; }
public string Args { get; set; }
}
} }
public class RestartConfig
{
public string Cmd { get; set; }
public string Args { get; set; }
}

View File

@@ -1,7 +1,6 @@
namespace NadekoBot.Common namespace NadekoBot.Common;
public interface ICloneable<T> where T : new()
{ {
public interface ICloneable<T> where T : new() public T Clone();
{
public T Clone();
}
} }

View File

@@ -1,25 +1,24 @@
using Discord; using Discord;
namespace NadekoBot 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);
}
public enum EmbedColor public interface IEmbedBuilder
{ {
Ok, IEmbedBuilder WithDescription(string desc);
Pending, IEmbedBuilder WithTitle(string title);
Error, 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,
} }

View File

@@ -1,7 +1,6 @@
namespace NadekoBot.Common namespace NadekoBot.Common;
public interface INadekoCommandOptions
{ {
public interface INadekoCommandOptions void NormalizeOptions();
{ }
void NormalizeOptions();
}
}

View File

@@ -1,10 +1,6 @@
using System; namespace NadekoBot.Common;
using System.Collections.Generic;
namespace NadekoBot.Common public interface IPlaceholderProvider
{ {
public interface IPlaceholderProvider public IEnumerable<(string Name, Func<string> Func)> GetPlaceholders();
{
public IEnumerable<(string Name, Func<string> Func)> GetPlaceholders();
}
} }

View File

@@ -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 Uri Bg { get; set; }
public int Version { get; set; } = 3; 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; }
//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 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; }
}
}

View File

@@ -1,34 +1,32 @@
using System; using System.Globalization;
using System.Globalization;
using System.Text.Json; using System.Text.Json;
using System.Text.Json.Serialization; using System.Text.Json.Serialization;
using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.PixelFormats;
namespace NadekoBot.Common.JsonConverters namespace NadekoBot.Common.JsonConverters;
public class Rgba32Converter : JsonConverter<Rgba32>
{ {
public class Rgba32Converter : JsonConverter<Rgba32> 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());
{
return Rgba32.ParseHex(reader.GetString());
}
public override void Write(Utf8JsonWriter writer, Rgba32 value, JsonSerializerOptions options)
{
writer.WriteStringValue(value.ToHex());
}
} }
public class CultureInfoConverter : JsonConverter<CultureInfo>
{
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) public override void Write(Utf8JsonWriter writer, Rgba32 value, JsonSerializerOptions options)
{ {
writer.WriteStringValue(value.Name); writer.WriteStringValue(value.ToHex());
} }
}
public class CultureInfoConverter : JsonConverter<CultureInfo>
{
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);
} }
} }

View File

@@ -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<kwum>
{ {
// needs proper invalid input check (character array input out of range) private readonly int _value;
// needs negative number support private const string ValidCharacters = "23456789abcdefghijkmnpqrstuvwxyz";
public readonly struct kwum : IEquatable<kwum>
public kwum(int num)
=> _value = num;
public kwum(in char c)
{ {
private readonly int _value; if (!IsValidChar(c))
private const string ValidCharacters = "23456789abcdefghijkmnpqrstuvwxyz"; throw new ArgumentException("Character needs to be a valid kwum character.", nameof(c));
public kwum(int num) _value = InternalCharToValue(c);
=> _value = num; }
public kwum(in char c) [MethodImpl(MethodImplOptions.AggressiveInlining)]
private static int InternalCharToValue(in char c)
=> ValidCharacters.IndexOf(c);
public kwum(in ReadOnlySpan<char> input)
{;
_value = 0;
for (var index = 0; index < input.Length; index++)
{ {
var c = input[index];
if (!IsValidChar(c)) 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); _value += ValidCharacters.IndexOf(c) * (int)Math.Pow(ValidCharacters.Length, input.Length - index - 1);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static int InternalCharToValue(in char c)
=> ValidCharacters.IndexOf(c);
public kwum(in ReadOnlySpan<char> 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<char> 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<char> 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();
} }
} }
public static bool TryParse(in ReadOnlySpan<char> 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<char> 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();
}
} }

View File

@@ -1,14 +1,13 @@
using CommandLine; using CommandLine;
namespace NadekoBot.Common namespace NadekoBot.Common;
{
public class LbOpts : INadekoCommandOptions public class LbOpts : INadekoCommandOptions
{ {
[Option('c', "clean", Default = false, HelpText = "Only show users who are on the server.")] [Option('c', "clean", Default = false, HelpText = "Only show users who are on the server.")]
public bool Clean { get; set; } public bool Clean { get; set; }
public void NormalizeOptions() public void NormalizeOptions()
{ {
}
} }
} }

View File

@@ -1,57 +1,54 @@
using System; using System.Net;
using System.Net;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using Discord.Net; 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)] Log.Fatal(ex, "A fatal error has occurred while attempting to connect to Discord");
public static void Handle(Exception ex) }
{
Log.Fatal(ex, "A fatal error has occurred while attempting to connect to Discord");
}
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void Handle(HttpException ex) public static void Handle(HttpException ex)
{
switch (ex.HttpCode)
{ {
switch (ex.HttpCode) case HttpStatusCode.Unauthorized:
{ Log.Error("Your bot token is wrong.\n" +
case HttpStatusCode.Unauthorized: "You can find the bot token under the Bot tab in the developer page.\n" +
Log.Error("Your bot token is wrong.\n" + "Fix your token in the credentials file and restart the bot");
"You can find the bot token under the Bot tab in the developer page.\n" + break;
"Fix your token in the credentials file and restart the bot");
break;
case HttpStatusCode.BadRequest: case HttpStatusCode.BadRequest:
Log.Error("Something has been incorrectly formatted in your credentials file.\n" + 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."); "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"); Log.Error("If you are on Linux, make sure Redis is installed and running");
break; break;
case HttpStatusCode.RequestTimeout: case HttpStatusCode.RequestTimeout:
Log.Error("The request timed out. Make sure you have no external program blocking the bot " + Log.Error("The request timed out. Make sure you have no external program blocking the bot " +
"from connecting to the internet"); "from connecting to the internet");
break; break;
case HttpStatusCode.ServiceUnavailable: case HttpStatusCode.ServiceUnavailable:
case HttpStatusCode.InternalServerError: case HttpStatusCode.InternalServerError:
Log.Error("Discord is having internal issues. Please, try again later"); Log.Error("Discord is having internal issues. Please, try again later");
break; break;
case HttpStatusCode.TooManyRequests: case HttpStatusCode.TooManyRequests:
Log.Error("Your bot has been ratelimited by Discord. Please, try again later.\n" + Log.Error("Your bot has been ratelimited by Discord. Please, try again later.\n" +
"Global ratelimits usually last for an hour"); "Global ratelimits usually last for an hour");
break; break;
default: default:
Log.Warning("An error occurred while attempting to connect to Discord"); Log.Warning("An error occurred while attempting to connect to Discord");
break; break;
}
Log.Fatal(ex.ToString());
} }
Log.Fatal(ex.ToString());
} }
} }

View File

@@ -1,14 +1,13 @@
using System.Threading.Tasks; using System.Threading.Tasks;
using Discord; using Discord;
namespace NadekoBot.Common.ModuleBehaviors namespace NadekoBot.Common.ModuleBehaviors;
/// <summary>
/// Implemented by modules which block execution before anything is executed
/// </summary>
public interface IEarlyBehavior
{ {
/// <summary> int Priority { get; }
/// Implemented by modules which block execution before anything is executed Task<bool> RunBehavior(IGuild guild, IUserMessage msg);
/// </summary> }
public interface IEarlyBehavior
{
int Priority { get; }
Task<bool> RunBehavior(IGuild guild, IUserMessage msg);
}
}

View File

@@ -1,10 +1,9 @@
using System.Threading.Tasks; using System.Threading.Tasks;
using Discord; using Discord;
namespace NadekoBot.Common.ModuleBehaviors namespace NadekoBot.Common.ModuleBehaviors;
public interface IInputTransformer
{ {
public interface IInputTransformer Task<string> TransformInput(IGuild guild, IMessageChannel channel, IUser user, string input);
{ }
Task<string> TransformInput(IGuild guild, IMessageChannel channel, IUser user, string input);
}
}

View File

@@ -1,13 +1,11 @@
using System.Threading.Tasks; using System.Threading.Tasks;
using Discord.Commands; 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<bool> TryBlockLate(ICommandContext context, string moduleName, CommandInfo command); Task<bool> TryBlockLate(ICommandContext context, string moduleName, CommandInfo command);
} }
}

View File

@@ -1,13 +1,12 @@
using System.Threading.Tasks; using System.Threading.Tasks;
using Discord; using Discord;
namespace NadekoBot.Common.ModuleBehaviors namespace NadekoBot.Common.ModuleBehaviors;
/// <summary>
/// Last thing to be executed, won't stop further executions
/// </summary>
public interface ILateExecutor
{ {
/// <summary> Task LateExecute(IGuild guild, IUserMessage msg);
/// Last thing to be executed, won't stop further executions }
/// </summary>
public interface ILateExecutor
{
Task LateExecute(IGuild guild, IUserMessage msg);
}
}

View File

@@ -2,33 +2,32 @@
using Discord.WebSocket; using Discord.WebSocket;
using System.Threading.Tasks; 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; } Blocked = false,
public string NewInput { get; set; } NewInput = null,
};
public static ModuleBehaviorResult None() => new ModuleBehaviorResult public static ModuleBehaviorResult FromBlocked(bool blocked) => new ModuleBehaviorResult
{
Blocked = false,
NewInput = null,
};
public static ModuleBehaviorResult FromBlocked(bool blocked) => new ModuleBehaviorResult
{
Blocked = blocked,
NewInput = null,
};
}
public interface IModuleBehavior
{ {
/// <summary> Blocked = blocked,
/// Negative priority means it will try to apply as early as possible NewInput = null,
/// Positive priority menas it will try to apply as late as possible };
/// </summary>
int Priority { get; }
Task<ModuleBehaviorResult> ApplyBehavior(DiscordSocketClient client, IGuild guild, IUserMessage msg);
}
} }
public interface IModuleBehavior
{
/// <summary>
/// Negative priority means it will try to apply as early as possible
/// Positive priority menas it will try to apply as late as possible
/// </summary>
int Priority { get; }
Task<ModuleBehaviorResult> ApplyBehavior(DiscordSocketClient client, IGuild guild, IUserMessage msg);
}

View File

@@ -1,16 +1,15 @@
using System.Threading.Tasks; using System.Threading.Tasks;
namespace NadekoBot.Common.ModuleBehaviors namespace NadekoBot.Common.ModuleBehaviors;
/// <summary>
/// All services which need to execute something after
/// the bot is ready should implement this interface
/// </summary>
public interface IReadyExecutor
{ {
/// <summary> /// <summary>
/// All services which need to execute something after /// Executed when bot is ready
/// the bot is ready should implement this interface
/// </summary> /// </summary>
public interface IReadyExecutor public Task OnReadyAsync();
{
/// <summary>
/// Executed when bot is ready
/// </summary>
public Task OnReadyAsync();
}
} }

View File

@@ -6,152 +6,151 @@ using NadekoBot.Extensions;
using System.Globalization; using System.Globalization;
using System.Threading.Tasks; 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<IUserMessage> SendErrorAsync(string error)
=> ctx.Channel.SendErrorAsync(_eb, error);
public Task<IUserMessage> SendErrorAsync(string title, string error, string url = null, string footer = null)
=> ctx.Channel.SendErrorAsync(_eb, title, error, url, footer);
public Task<IUserMessage> SendConfirmAsync(string text)
=> ctx.Channel.SendConfirmAsync(_eb, text);
public Task<IUserMessage> SendConfirmAsync(string title, string text, string url = null, string footer = null)
=> ctx.Channel.SendConfirmAsync(_eb, title, text, url, footer);
public Task<IUserMessage> SendPendingAsync(string text)
=> ctx.Channel.SendPendingAsync(_eb, text);
public Task<IUserMessage> ErrorLocalizedAsync(LocStr str)
=> SendErrorAsync(GetText(str));
protected NadekoModule() public Task<IUserMessage> PendingLocalizedAsync(LocStr str)
=> SendPendingAsync(GetText(str));
public Task<IUserMessage> ConfirmLocalizedAsync(LocStr str)
=> SendConfirmAsync(GetText(str));
public Task<IUserMessage> ReplyErrorLocalizedAsync(LocStr str)
=> SendErrorAsync($"{Format.Bold(ctx.User.ToString())} {GetText(str)}");
public Task<IUserMessage> ReplyPendingLocalizedAsync(LocStr str)
=> SendPendingAsync($"{Format.Bold(ctx.User.ToString())} {GetText(str)}");
public Task<IUserMessage> ReplyConfirmLocalizedAsync(LocStr str)
=> SendConfirmAsync($"{Format.Bold(ctx.User.ToString())} {GetText(str)}");
public async Task<bool> 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<string> GetUserInputAsync(ulong userId, ulong channelId)
{
var userInputTask = new TaskCompletionSource<string>();
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); var _ = Task.Run(() =>
}
protected string GetText(in LocStr data) =>
Strings.GetText(data, _cultureInfo);
public Task<IUserMessage> SendErrorAsync(string error)
=> ctx.Channel.SendErrorAsync(_eb, error);
public Task<IUserMessage> SendErrorAsync(string title, string error, string url = null, string footer = null)
=> ctx.Channel.SendErrorAsync(_eb, title, error, url, footer);
public Task<IUserMessage> SendConfirmAsync(string text)
=> ctx.Channel.SendConfirmAsync(_eb, text);
public Task<IUserMessage> SendConfirmAsync(string title, string text, string url = null, string footer = null)
=> ctx.Channel.SendConfirmAsync(_eb, title, text, url, footer);
public Task<IUserMessage> SendPendingAsync(string text)
=> ctx.Channel.SendPendingAsync(_eb, text);
public Task<IUserMessage> ErrorLocalizedAsync(LocStr str)
=> SendErrorAsync(GetText(str));
public Task<IUserMessage> PendingLocalizedAsync(LocStr str)
=> SendPendingAsync(GetText(str));
public Task<IUserMessage> ConfirmLocalizedAsync(LocStr str)
=> SendConfirmAsync(GetText(str));
public Task<IUserMessage> ReplyErrorLocalizedAsync(LocStr str)
=> SendErrorAsync($"{Format.Bold(ctx.User.ToString())} {GetText(str)}");
public Task<IUserMessage> ReplyPendingLocalizedAsync(LocStr str)
=> SendPendingAsync($"{Format.Bold(ctx.User.ToString())} {GetText(str)}");
public Task<IUserMessage> ReplyConfirmLocalizedAsync(LocStr str)
=> SendConfirmAsync($"{Format.Bold(ctx.User.ToString())} {GetText(str)}");
public async Task<bool> 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); if (!(arg is SocketUserMessage userMsg) ||
input = input?.ToUpperInvariant(); !(userMsg.Channel is ITextChannel chan) ||
userMsg.Author.Id != userId ||
if (input != "YES" && input != "Y") userMsg.Channel.Id != channelId)
{ {
return false;
}
return true;
}
finally
{
var _ = Task.Run(() => msg.DeleteAsync());
}
}
// TypeConverter typeConverter = TypeDescriptor.GetConverter(propType); ?
public async Task<string> GetUserInputAsync(ulong userId, ulong channelId)
{
var userInputTask = new TaskCompletionSource<string>();
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; return Task.CompletedTask;
}); }
if (userInputTask.TrySetResult(arg.Content))
{
userMsg.DeleteAfter(1);
}
return Task.CompletedTask; return Task.CompletedTask;
} });
return Task.CompletedTask;
} }
} }
}
public abstract class NadekoModule<TService> : NadekoModule public abstract class NadekoModule<TService> : NadekoModule
{
public TService _service { get; set; }
protected NadekoModule() : base()
{ {
public TService _service { get; set; }
protected NadekoModule() : base()
{
}
} }
}
public abstract class NadekoSubmodule : NadekoModule public abstract class NadekoSubmodule : NadekoModule
{ {
protected NadekoSubmodule() : base() { } protected NadekoSubmodule() : base() { }
} }
public abstract class NadekoSubmodule<TService> : NadekoModule<TService> public abstract class NadekoSubmodule<TService> : NadekoModule<TService>
{
protected NadekoSubmodule() : base()
{ {
protected NadekoSubmodule() : base()
{
}
} }
} }

View File

@@ -1,7 +1,6 @@
namespace NadekoBot.Modules namespace NadekoBot.Modules;
public static class NadekoModuleExtensions
{ {
public static class NadekoModuleExtensions
{
} }
}

View File

@@ -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; _rng = RandomNumberGenerator.Create();
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);
}
} }
}
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);
}
}

View File

@@ -1,19 +1,17 @@
using System; using System.Threading.Tasks;
using System.Threading.Tasks;
using Discord.Commands; 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 override Task<PreconditionResult> CheckPermissionsAsync(ICommandContext context, CommandInfo command, IServiceProvider services)
public sealed class NoPublicBotAttribute : PreconditionAttribute
{ {
public override Task<PreconditionResult> CheckPermissionsAsync(ICommandContext context, CommandInfo command, IServiceProvider services)
{
#if GLOBAL_NADEKO #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/).")); 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 #else
return Task.FromResult(PreconditionResult.FromSuccess()); return Task.FromResult(PreconditionResult.FromSuccess());
#endif #endif
}
} }
} }

View File

@@ -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 class SlotData
public Uri[] Currency { get; set; } {
public Uri[] Dice { get; set; } public Uri[] Emojis { get; set; }
public RategirlData Rategirl { get; set; } public Uri[] Numbers { get; set; }
public XpData Xp { get; set; } public Uri Bg { get; set; }
}
//new public class CoinData
public RipData Rip { get; set; } {
public SlotData Slots { get; set; } public Uri[] Heads { get; set; }
public Uri[] Tails { get; set; }
}
public class RipData public class RategirlData
{ {
public Uri Bg { get; set; } public Uri Matrix { get; set; }
public Uri Overlay { get; set; } public Uri Dot { get; set; }
} }
public class SlotData public class XpData
{ {
public Uri[] Emojis { get; set; } public Uri Bg { 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; }
}
} }
} }

View File

@@ -1,24 +1,23 @@
using CommandLine; using CommandLine;
namespace NadekoBot.Common namespace NadekoBot.Common;
{
public static class OptionsParser
{
public static T ParseFrom<T>(string[] args) where T : INadekoCommandOptions, new()
=> ParseFrom(new T(), args).Item1;
public static (T, bool) ParseFrom<T>(T options, string[] args) where T : INadekoCommandOptions public static class OptionsParser
{
public static T ParseFrom<T>(string[] args) where T : INadekoCommandOptions, new()
=> ParseFrom(new T(), args).Item1;
public static (T, bool) ParseFrom<T>(T options, string[] args) where T : INadekoCommandOptions
{
using (var p = new Parser(x =>
{
x.HelpWriter = null;
}))
{ {
using (var p = new Parser(x => var res = p.ParseArguments<T>(args);
{ options = res.MapResult(x => x, x => options);
x.HelpWriter = null; options.NormalizeOptions();
})) return (options, res.Tag == ParserResultType.Parsed);
{
var res = p.ParseArguments<T>(args);
options = res.MapResult(x => x, x => options);
options.NormalizeOptions();
return (options, res.Tag == ParserResultType.Parsed);
}
} }
} }
} }

View File

@@ -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 Title { get; set; } public string Version { get; set; }
public string Artist { get; set; }
public string Version { get; set; }
}
} }

View File

@@ -1,41 +1,40 @@
using Newtonsoft.Json; 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; }
}
} }

View File

@@ -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 _processorCount;
private static volatile int _lastProcessorCountRefreshTicks; private static volatile int _lastProcessorCountRefreshTicks;
public static int ProcessorCount { public static int ProcessorCount {
get { get {
var now = Environment.TickCount; var now = Environment.TickCount;
if (_processorCount == 0 || (now - _lastProcessorCountRefreshTicks) >= ProcessorCountRefreshIntervalMs) if (_processorCount == 0 || (now - _lastProcessorCountRefreshTicks) >= ProcessorCountRefreshIntervalMs)
{ {
_processorCount = Environment.ProcessorCount; _processorCount = Environment.ProcessorCount;
_lastProcessorCountRefreshTicks = now; _lastProcessorCountRefreshTicks = now;
}
return _processorCount;
} }
return _processorCount;
} }
} }
} }

View File

@@ -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; }
}
}

View File

@@ -1,40 +1,38 @@
using Newtonsoft.Json; 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 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<string, string> 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 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<string, string> 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; }
}

View File

@@ -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 Desc { get; set; } public string Name { get; set; }
public string ShortDesc { get; set; } public float Rating { get; set; }
public string Name { get; set; } }
public float Rating { get; set; }
}
}

View File

@@ -1,95 +1,90 @@
using System; using System.Threading.Tasks;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace NadekoBot.Common namespace NadekoBot.Common;
public class EventPubSub : IPubSub
{ {
public class EventPubSub : IPubSub private readonly Dictionary<string, Dictionary<Delegate, List<Func<object, ValueTask>>>> _actions
= new Dictionary<string, Dictionary<Delegate, List<Func<object, ValueTask>>>>();
private readonly object locker = new object();
public Task Sub<TData>(in TypedKey<TData> key, Func<TData, ValueTask> action)
{ {
private readonly Dictionary<string, Dictionary<Delegate, List<Func<object, ValueTask>>>> _actions Func<object, ValueTask> localAction = obj => action((TData) obj);
= new Dictionary<string, Dictionary<Delegate, List<Func<object, ValueTask>>>>(); lock(locker)
private readonly object locker = new object();
public Task Sub<TData>(in TypedKey<TData> key, Func<TData, ValueTask> action)
{ {
Func<object, ValueTask> localAction = obj => action((TData) obj); Dictionary<Delegate, List<Func<object, ValueTask>>> keyActions;
lock(locker) if (!_actions.TryGetValue(key.Key, out keyActions))
{ {
Dictionary<Delegate, List<Func<object, ValueTask>>> keyActions; keyActions = new Dictionary<Delegate, List<Func<object, ValueTask>>>();
if (!_actions.TryGetValue(key.Key, out keyActions)) _actions[key.Key] = keyActions;
{
keyActions = new Dictionary<Delegate, List<Func<object, ValueTask>>>();
_actions[key.Key] = keyActions;
}
List<Func<object, ValueTask>> sameActions;
if (!keyActions.TryGetValue(action, out sameActions))
{
sameActions = new List<Func<object, ValueTask>>();
keyActions[action] = sameActions;
}
sameActions.Add(localAction);
return Task.CompletedTask;
} }
}
public Task Pub<TData>(in TypedKey<TData> 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<Func<object, ValueTask>> sameActions;
if (!keyActions.TryGetValue(action, out sameActions))
{
sameActions = new List<Func<object, ValueTask>>();
keyActions[action] = sameActions;
} }
}
public Task Unsub<TData>(in TypedKey<TData> key, Func<TData, ValueTask> action) sameActions.Add(localAction);
return Task.CompletedTask;
}
}
public Task Pub<TData>(in TypedKey<TData> key, TData data)
{
lock (locker)
{ {
lock (locker) if(_actions.TryGetValue(key.Key, out var actions))
{ {
// get subscriptions for this action // if this class ever gets used, this needs to be properly implemented
if (_actions.TryGetValue(key.Key, out var actions)) // 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<TData>(in TypedKey<TData> key, Func<TData, ValueTask> 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(); // remove last subscription
// get subscriptions which have the same action hash code sameActions.RemoveAt(sameActions.Count - 1);
// 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);
// if the last subscription was the only subscription // if the last subscription was the only subscription
// we can safely remove this action's dictionary entry // we can safely remove this action's dictionary entry
if (sameActions.Count == 0) if (sameActions.Count == 0)
{ {
actions.Remove(action); actions.Remove(action);
// if our dictionary has no more elements after // if our dictionary has no more elements after
// removing the entry // removing the entry
// it's safe to remove it from the key's subscriptions // it's safe to remove it from the key's subscriptions
if (actions.Count == 0) if (actions.Count == 0)
{ {
_actions.Remove(key.Key); _actions.Remove(key.Key);
}
} }
} }
} }
return Task.CompletedTask;
} }
return Task.CompletedTask;
} }
} }
} }

View File

@@ -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<TData>(in TypedKey<TData> key, TData data);
{ public Task Sub<TData>(in TypedKey<TData> key, Func<TData, ValueTask> action);
public Task Pub<TData>(in TypedKey<TData> key, TData data);
public Task Sub<TData>(in TypedKey<TData> key, Func<TData, ValueTask> action);
}
} }

View File

@@ -1,8 +1,7 @@
namespace NadekoBot.Common namespace NadekoBot.Common;
public interface ISeria
{ {
public interface ISeria byte[] Serialize<T>(T data);
{ T Deserialize<T>(byte[] data);
byte[] Serialize<T>(T data);
T Deserialize<T>(byte[] data);
}
} }

View File

@@ -1,28 +1,27 @@
using System.Text.Json; using System.Text.Json;
using NadekoBot.Common.JsonConverters; using NadekoBot.Common.JsonConverters;
namespace NadekoBot.Common namespace NadekoBot.Common;
{
public class JsonSeria : ISeria
{
private JsonSerializerOptions serializerOptions = new JsonSerializerOptions()
{
Converters =
{
new Rgba32Converter(),
new CultureInfoConverter(),
}
};
public byte[] Serialize<T>(T data)
=> JsonSerializer.SerializeToUtf8Bytes(data, serializerOptions);
public T Deserialize<T>(byte[] data) public class JsonSeria : ISeria
{
private JsonSerializerOptions serializerOptions = new JsonSerializerOptions()
{
Converters =
{ {
if (data is null) new Rgba32Converter(),
return default; new CultureInfoConverter(),
}
};
public byte[] Serialize<T>(T data)
=> JsonSerializer.SerializeToUtf8Bytes(data, serializerOptions);
public T Deserialize<T>(byte[] data)
{
if (data is null)
return default;
return JsonSerializer.Deserialize<T>(data, serializerOptions); return JsonSerializer.Deserialize<T>(data, serializerOptions);
}
} }
} }

View File

@@ -1,46 +1,42 @@
using System; using System.Threading.Tasks;
using System.Threading.Tasks;
using NadekoBot.Services;
using NadekoBot.Extensions; using NadekoBot.Extensions;
using Serilog;
using StackExchange.Redis; 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; _multi = multi;
private readonly ISeria _serializer; _serializer = serializer;
private readonly IBotCredentials _creds; _creds = creds;
}
public RedisPubSub(ConnectionMultiplexer multi, ISeria serializer, IBotCredentials creds) public Task Pub<TData>(in TypedKey<TData> key, TData data)
{ {
_multi = multi; var serialized = _serializer.Serialize(data);
_serializer = serializer; return _multi.GetSubscriber().PublishAsync($"{_creds.RedisKey()}:{key.Key}", serialized, CommandFlags.FireAndForget);
_creds = creds; }
}
public Task Pub<TData>(in TypedKey<TData> key, TData data) public Task Sub<TData>(in TypedKey<TData> key, Func<TData, ValueTask> action)
{
var eventName = key.Key;
return _multi.GetSubscriber().SubscribeAsync($"{_creds.RedisKey()}:{eventName}", async (ch, data) =>
{ {
var serialized = _serializer.Serialize(data); try
return _multi.GetSubscriber().PublishAsync($"{_creds.RedisKey()}:{key.Key}", serialized, CommandFlags.FireAndForget);
}
public Task Sub<TData>(in TypedKey<TData> key, Func<TData, ValueTask> action)
{
var eventName = key.Key;
return _multi.GetSubscriber().SubscribeAsync($"{_creds.RedisKey()}:{eventName}", async (ch, data) =>
{ {
try var dataObj = _serializer.Deserialize<TData>(data);
{ await action(dataObj);
var dataObj = _serializer.Deserialize<TData>(data); }
await action(dataObj); catch (Exception ex)
} {
catch (Exception ex) Log.Error($"Error handling the event {eventName}: {ex.Message}");
{ }
Log.Error($"Error handling the event {eventName}: {ex.Message}"); });
}
});
}
} }
} }

View File

@@ -1,29 +1,28 @@
namespace NadekoBot.Common namespace NadekoBot.Common;
public readonly struct TypedKey<TData>
{ {
public readonly struct TypedKey<TData> public readonly string Key;
public TypedKey(in string key)
{ {
public readonly string Key; Key = key;
public TypedKey(in string key)
{
Key = key;
}
public static implicit operator TypedKey<TData>(in string input)
=> new TypedKey<TData>(input);
public static implicit operator string(in TypedKey<TData> input)
=> input.Key;
public static bool operator ==(in TypedKey<TData> left, in TypedKey<TData> right)
=> left.Key == right.Key;
public static bool operator !=(in TypedKey<TData> left, in TypedKey<TData> right)
=> !(left == right);
public override bool Equals(object obj)
=> obj is TypedKey<TData> o && o == this;
public override int GetHashCode() => Key?.GetHashCode() ?? 0;
public override string ToString() => Key;
} }
public static implicit operator TypedKey<TData>(in string input)
=> new TypedKey<TData>(input);
public static implicit operator string(in TypedKey<TData> input)
=> input.Key;
public static bool operator ==(in TypedKey<TData> left, in TypedKey<TData> right)
=> left.Key == right.Key;
public static bool operator !=(in TypedKey<TData> left, in TypedKey<TData> right)
=> !(left == right);
public override bool Equals(object obj)
=> obj is TypedKey<TData> o && o == this;
public override int GetHashCode() => Key?.GetHashCode() ?? 0;
public override string ToString() => Key;
} }

View File

@@ -3,36 +3,35 @@ using NadekoBot.Common.Yml;
using NadekoBot.Common.Configs; using NadekoBot.Common.Configs;
using YamlDotNet.Serialization; 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(?<code>[a-zA-Z0-9]{8})|\\u(?<code>[a-zA-Z0-9]{4})|\\x(?<code>[a-zA-Z0-9]{2}))",
RegexOptions.Compiled);
public YamlSeria()
{ {
private readonly ISerializer _serializer; _serializer = Yaml.Serializer;
private readonly IDeserializer _deserializer; _deserializer = Yaml.Deserializer;
private static readonly Regex CodePointRegex
= new Regex(@"(\\U(?<code>[a-zA-Z0-9]{8})|\\u(?<code>[a-zA-Z0-9]{4})|\\x(?<code>[a-zA-Z0-9]{2}))",
RegexOptions.Compiled);
public YamlSeria()
{
_serializer = Yaml.Serializer;
_deserializer = Yaml.Deserializer;
}
public string Serialize<T>(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<T>(string data)
=> _deserializer.Deserialize<T>(data);
} }
public string Serialize<T>(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<T>(string data)
=> _deserializer.Deserialize<T>(data);
} }

View File

@@ -3,235 +3,229 @@ using Discord.Commands;
using Discord.WebSocket; using Discord.WebSocket;
using NadekoBot.Extensions; using NadekoBot.Extensions;
using NadekoBot.Modules.Administration.Services; using NadekoBot.Modules.Administration.Services;
using NadekoBot.Modules.Music.Services;
using System;
using System.Collections.Concurrent; using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions; 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(?:(?<from>(?:-)?\\d+)-(?<to>(?:-)?\\d+))?%", RegexOptions.Compiled);
private ConcurrentDictionary<string, Func<string>> _reps = new ConcurrentDictionary<string, Func<string>>();
private ConcurrentDictionary<Regex, Func<Match, string>> _regex = new ConcurrentDictionary<Regex, Func<Match, string>>();
public ReplacementBuilder()
{ {
private static readonly Regex rngRegex = new Regex("%rng(?:(?<from>(?:-)?\\d+)-(?<to>(?:-)?\\d+))?%", RegexOptions.Compiled); WithRngRegex();
private ConcurrentDictionary<string, Func<string>> _reps = new ConcurrentDictionary<string, Func<string>>(); }
private ConcurrentDictionary<Regex, Func<Match, string>> _regex = new ConcurrentDictionary<Regex, Func<Match, string>>();
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(); TimeZoneInfo to = TimeZoneInfo.Local;
} if (g != null)
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 (GuildTimezoneService.AllServices.TryGetValue(client.CurrentUser.Id, out var tz))
if (g != null) to = tz.GetTimeZoneOrDefault(g.Id) ?? TimeZoneInfo.Local;
{
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<IUser> 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<string> 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<IPlaceholderProvider> phProviders)
{
foreach (var provider in phProviders)
{
foreach (var ovr in provider.GetPlaceholders())
{
_reps.TryAdd(ovr.Name, ovr.Func);
}
} }
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<IUser> 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<string> 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<IPlaceholderProvider> phProviders)
{
foreach (var provider in phProviders)
{
foreach (var ovr in provider.GetPlaceholders())
{
_reps.TryAdd(ovr.Name, ovr.Func);
}
}
return this;
}
}

View File

@@ -1,91 +1,88 @@
using System; using System.Text.RegularExpressions;
using System.Collections.Generic;
using System.Text.RegularExpressions;
namespace NadekoBot.Common.Replacements namespace NadekoBot.Common.Replacements;
public class Replacer
{ {
public class Replacer private readonly IEnumerable<(string Key, Func<string> Text)> _replacements;
private readonly IEnumerable<(Regex Regex, Func<Match, string> Replacement)> _regex;
public Replacer(IEnumerable<(string, Func<string>)> replacements, IEnumerable<(Regex, Func<Match, string>)> regex)
{ {
private readonly IEnumerable<(string Key, Func<string> Text)> _replacements; _replacements = replacements;
private readonly IEnumerable<(Regex Regex, Func<Match, string> Replacement)> _regex; _regex = regex;
public Replacer(IEnumerable<(string, Func<string>)> replacements, IEnumerable<(Regex, Func<Match, string>)> 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<SmartTextEmbedField>();
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;
}
} }
}
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<SmartTextEmbedField>();
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;
}
}

View File

@@ -1,16 +1,14 @@
using Newtonsoft.Json; using Newtonsoft.Json;
using Newtonsoft.Json.Serialization; 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;
var contract = base.CreateObjectContract(objectType); return contract;
contract.ItemRequired = Required.DisallowNull;
return contract;
}
} }
} }

View File

@@ -1,63 +1,60 @@
using System; namespace NadekoBot.Common;
namespace NadekoBot.Common public struct ShmartNumber : IEquatable<ShmartNumber>
{ {
public struct ShmartNumber : IEquatable<ShmartNumber> public long Value { get; }
public string Input { get; }
public ShmartNumber(long val, string input = null)
{ {
public long Value { get; } Value = val;
public string Input { get; } Input = input;
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);
}
} }
}
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);
}
}

View File

@@ -1,137 +1,133 @@
using System; using Discord;
using System.Linq;
using Discord;
using NadekoBot.Extensions; 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; } var set = new SmartEmbedText();
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();
set.PlainText = plainText; set.PlainText = plainText;
set.Title = eb.Title; set.Title = eb.Title;
set.Description = eb.Description; set.Description = eb.Description;
set.Url = eb.Url; set.Url = eb.Url;
set.Thumbnail = eb.Thumbnail?.Url; set.Thumbnail = eb.Thumbnail?.Url;
set.Image = eb.Image?.Url; set.Image = eb.Image?.Url;
set.Author = eb.Author is EmbedAuthor ea set.Author = eb.Author is EmbedAuthor ea
? new() ? new()
{ {
Name = ea.Name, Name = ea.Name,
Url = ea.Url, Url = ea.Url,
IconUrl = ea.IconUrl IconUrl = ea.IconUrl
} }
: null; : null;
set.Footer = eb.Footer is EmbedFooter ef set.Footer = eb.Footer is EmbedFooter ef
? new() ? new()
{ {
Text = ef.Text, Text = ef.Text,
IconUrl = ef.IconUrl IconUrl = ef.IconUrl
} }
: null; : null;
if (eb.Fields.Length > 0) if (eb.Fields.Length > 0)
set.Fields = eb set.Fields = eb
.Fields .Fields
.Select(field => new SmartTextEmbedField() .Select(field => new SmartTextEmbedField()
{ {
Inline = field.Inline, Inline = field.Inline,
Name = field.Name, Name = field.Name,
Value = field.Value, Value = field.Value,
}) })
.ToArray(); .ToArray();
set.Color = eb.Color?.RawValue ?? 0; set.Color = eb.Color?.RawValue ?? 0;
return set; 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() embed.WithFooter(efb =>
.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 => efb.WithText(Footer.Text);
{ if (Uri.IsWellFormedUriString(Footer.IconUrl, UriKind.Absolute))
efb.WithText(Footer.Text); efb.WithIconUrl(Footer.IconUrl);
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;
} }
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) if (!string.IsNullOrWhiteSpace(f.Name) && !string.IsNullOrWhiteSpace(f.Value))
{ embed.AddField(f.Name, f.Value, f.Inline);
f.Name = f.Name.TrimTo(256); }
f.Value = f.Value.TrimTo(1024); }
}
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);
} }
} }
} }

View File

@@ -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) public static implicit operator SmartPlainText(string input)
{ => new SmartPlainText(input);
Text = text;
}
public static implicit operator SmartPlainText(string input) public static implicit operator string(SmartPlainText input)
=> new SmartPlainText(input); => input.Text;
public static implicit operator string(SmartPlainText input) public override string ToString()
=> input.Text; {
return Text;
public override string ToString()
{
return Text;
}
} }
} }

View File

@@ -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) public static SmartText operator +(SmartText text, string input)
=> text switch => 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)
{ {
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<SmartEmbedText>(input);
smartEmbedText.NormalizeFields();
if (!smartEmbedText.IsValid)
{ {
return new SmartPlainText(input); return new SmartPlainText(input);
} }
try return smartEmbedText;
{ }
var smartEmbedText = JsonConvert.DeserializeObject<SmartEmbedText>(input); catch
{
smartEmbedText.NormalizeFields(); return new SmartPlainText(input);
if (!smartEmbedText.IsValid)
{
return new SmartPlainText(input);
}
return smartEmbedText;
}
catch
{
return new SmartPlainText(input);
}
} }
} }
} }

View File

@@ -1,13 +1,12 @@
using Newtonsoft.Json; using Newtonsoft.Json;
namespace NadekoBot namespace NadekoBot;
public class SmartTextEmbedAuthor
{ {
public class SmartTextEmbedAuthor public string Name { get; set; }
{ public string IconUrl { get; set; }
public string Name { get; set; } [JsonProperty("icon_url")]
public string IconUrl { get; set; } private string Icon_Url { set => IconUrl = value; }
[JsonProperty("icon_url")] public string Url { get; set; }
private string Icon_Url { set => IconUrl = value; }
public string Url { get; set; }
}
} }

View File

@@ -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 string Name { get; set; } public bool Inline { get; set; }
public string Value { get; set; }
public bool Inline { get; set; }
}
} }

View File

@@ -1,12 +1,11 @@
using Newtonsoft.Json; using Newtonsoft.Json;
namespace NadekoBot namespace NadekoBot;
public class SmartTextEmbedFooter
{ {
public class SmartTextEmbedFooter public string Text { get; set; }
{ public string IconUrl { get; set; }
public string Text { get; set; } [JsonProperty("icon_url")]
public string IconUrl { get; set; } private string Icon_Url { set => IconUrl = value; }
[JsonProperty("icon_url")]
private string Icon_Url { set => IconUrl = value; }
}
} }

View File

@@ -1,91 +1,89 @@
using Discord; using Discord;
using Discord.WebSocket; using Discord.WebSocket;
using System;
using System.Threading.Tasks; 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<SocketReaction> OnReactionAdded = delegate { };
public event Action<SocketReaction> OnReactionRemoved = delegate { };
public event Action OnReactionsCleared = delegate { };
public ReactionEventWrapper(DiscordSocketClient client, IUserMessage msg)
{ {
public IUserMessage Message { get; } Message = msg ?? throw new ArgumentNullException(nameof(msg));
public event Action<SocketReaction> OnReactionAdded = delegate { }; _client = client;
public event Action<SocketReaction> OnReactionRemoved = delegate { };
public event Action OnReactionsCleared = delegate { };
public ReactionEventWrapper(DiscordSocketClient client, IUserMessage msg) _client.ReactionAdded += Discord_ReactionAdded;
{ _client.ReactionRemoved += Discord_ReactionRemoved;
Message = msg ?? throw new ArgumentNullException(nameof(msg)); _client.ReactionsCleared += Discord_ReactionsCleared;
_client = client;
_client.ReactionAdded += Discord_ReactionAdded;
_client.ReactionRemoved += Discord_ReactionRemoved;
_client.ReactionsCleared += Discord_ReactionsCleared;
}
private Task Discord_ReactionsCleared(Cacheable<IUserMessage, ulong> msg, ISocketMessageChannel channel)
{
Task.Run(() =>
{
try
{
if (msg.Id == Message.Id)
OnReactionsCleared?.Invoke();
}
catch { }
});
return Task.CompletedTask;
}
private Task Discord_ReactionRemoved(Cacheable<IUserMessage, ulong> 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<IUserMessage, ulong> 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();
}
} }
}
private Task Discord_ReactionsCleared(Cacheable<IUserMessage, ulong> msg, ISocketMessageChannel channel)
{
Task.Run(() =>
{
try
{
if (msg.Id == Message.Id)
OnReactionsCleared?.Invoke();
}
catch { }
});
return Task.CompletedTask;
}
private Task Discord_ReactionRemoved(Cacheable<IUserMessage, ulong> 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<IUserMessage, ulong> 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();
}
}

View File

@@ -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,
Add = int.MinValue, Rm = int.MinValue + 1,
Rem = int.MinValue + 1, }
Rm = int.MinValue + 1,
}
}

View File

@@ -1,90 +1,87 @@
using System; using System.Threading.Tasks;
using System.Linq;
using System.Threading.Tasks;
using Discord.Commands; using Discord.Commands;
using NadekoBot.Services; using NadekoBot.Services;
using NadekoBot.Modules.CustomReactions.Services; using NadekoBot.Modules.CustomReactions.Services;
namespace NadekoBot.Common.TypeReaders namespace NadekoBot.Common.TypeReaders;
public sealed class CommandTypeReader : NadekoTypeReader<CommandInfo>
{ {
public sealed class CommandTypeReader : NadekoTypeReader<CommandInfo> private readonly CommandHandler _handler;
private readonly CommandService _cmds;
public CommandTypeReader(CommandHandler handler, CommandService cmds)
{ {
private readonly CommandHandler _handler; _handler = handler;
private readonly CommandService _cmds; _cmds = cmds;
public CommandTypeReader(CommandHandler handler, CommandService cmds)
{
_handler = handler;
_cmds = cmds;
}
public override Task<TypeReaderResult> 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));
}
} }
public sealed class CommandOrCrTypeReader : NadekoTypeReader<CommandOrCrInfo> public override Task<TypeReaderResult> ReadAsync(ICommandContext context, string input)
{ {
private readonly CommandService _cmds; input = input.ToUpperInvariant();
private readonly CustomReactionsService _crs; var prefix = _handler.GetPrefix(context.Guild);
private readonly CommandHandler _commandHandler; if (!input.StartsWith(prefix.ToUpperInvariant(), StringComparison.InvariantCulture))
return Task.FromResult(TypeReaderResult.FromError(CommandError.ParseFailed, "No such command found."));
public CommandOrCrTypeReader( input = input.Substring(prefix.Length);
CommandService cmds,
CustomReactionsService crs,
CommandHandler commandHandler)
{
_cmds = cmds;
_crs = crs;
_commandHandler = commandHandler;
}
public override async Task<TypeReaderResult> ReadAsync(ICommandContext context, string input) var cmd = _cmds.Commands.FirstOrDefault(c => c.Aliases.Select(a => a.ToUpperInvariant()).Contains(input));
{ if (cmd is null)
input = input.ToUpperInvariant(); return Task.FromResult(TypeReaderResult.FromError(CommandError.ParseFailed, "No such command found."));
if (_crs.ReactionExists(context.Guild?.Id, input)) return Task.FromResult(TypeReaderResult.FromSuccess(cmd));
{
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;
}
} }
} }
public sealed class CommandOrCrTypeReader : NadekoTypeReader<CommandOrCrInfo>
{
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<TypeReaderResult> 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;
}
}

View File

@@ -2,16 +2,15 @@
using Discord; using Discord;
using Discord.Commands; using Discord.Commands;
namespace NadekoBot.Common.TypeReaders namespace NadekoBot.Common.TypeReaders;
{
public sealed class EmoteTypeReader : NadekoTypeReader<Emote>
{
public override Task<TypeReaderResult> 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)); public sealed class EmoteTypeReader : NadekoTypeReader<Emote>
} {
public override Task<TypeReaderResult> 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));
} }
} }

View File

@@ -1,53 +1,51 @@
using System; using System.Threading.Tasks;
using System.Threading.Tasks;
using Discord.Commands; using Discord.Commands;
using NadekoBot.Modules.Administration.Services; using NadekoBot.Modules.Administration.Services;
namespace NadekoBot.Common.TypeReaders namespace NadekoBot.Common.TypeReaders;
public sealed class GuildDateTimeTypeReader : NadekoTypeReader<GuildDateTime>
{ {
public sealed class GuildDateTimeTypeReader : NadekoTypeReader<GuildDateTime> private readonly GuildTimezoneService _gts;
public GuildDateTimeTypeReader(GuildTimezoneService gts)
{ {
private readonly GuildTimezoneService _gts; _gts = gts;
}
public GuildDateTimeTypeReader(GuildTimezoneService gts)
{
_gts = gts;
}
public override Task<TypeReaderResult> ReadAsync(ICommandContext context, string input) public override Task<TypeReaderResult> ReadAsync(ICommandContext context, string input)
{ {
var gdt = Parse(context.Guild.Id, input); var gdt = Parse(context.Guild.Id, input);
if(gdt is null) if(gdt is null)
return Task.FromResult(TypeReaderResult.FromError(CommandError.ParseFailed, "Input string is in an incorrect format.")); return Task.FromResult(TypeReaderResult.FromError(CommandError.ParseFailed, "Input string is in an incorrect format."));
return Task.FromResult(TypeReaderResult.FromSuccess(gdt)); 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);
}
} }
public class GuildDateTime private GuildDateTime Parse(ulong guildId, string input)
{ {
public TimeZoneInfo Timezone { get; } if (!DateTime.TryParse(input, out var dt))
public DateTime CurrentGuildTime { get; } return null;
public DateTime InputTime { get; }
public DateTime InputTimeUtc { get; }
public GuildDateTime(TimeZoneInfo guildTimezone, DateTime inputTime) var tz = _gts.GetTimeZoneOrUtc(guildId);
{
var now = DateTime.UtcNow; return new(tz, dt);
Timezone = guildTimezone;
CurrentGuildTime = TimeZoneInfo.ConvertTime(now, TimeZoneInfo.Utc, Timezone);
InputTime = inputTime;
InputTimeUtc = TimeZoneInfo.ConvertTime(inputTime, Timezone, TimeZoneInfo.Utc);
}
} }
} }
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);
}
}

View File

@@ -1,31 +1,29 @@
using System.Linq; using System.Threading.Tasks;
using System.Threading.Tasks;
using Discord.Commands; using Discord.Commands;
using Discord.WebSocket; using Discord.WebSocket;
using Discord; using Discord;
namespace NadekoBot.Common.TypeReaders namespace NadekoBot.Common.TypeReaders;
public sealed class GuildTypeReader : NadekoTypeReader<IGuild>
{ {
public sealed class GuildTypeReader : NadekoTypeReader<IGuild> private readonly DiscordSocketClient _client;
public GuildTypeReader(DiscordSocketClient client)
{ {
private readonly DiscordSocketClient _client; _client = client;
public GuildTypeReader(DiscordSocketClient client)
{
_client = client;
}
public override Task<TypeReaderResult> 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"));
}
} }
}
public override Task<TypeReaderResult> 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"));
}
}

View File

@@ -1,24 +1,23 @@
using System.Threading.Tasks; using System.Threading.Tasks;
using Discord.Commands; using Discord.Commands;
namespace NadekoBot.Common.TypeReaders namespace NadekoBot.Common.TypeReaders;
public sealed class KwumTypeReader : NadekoTypeReader<kwum>
{ {
public sealed class KwumTypeReader : NadekoTypeReader<kwum> public override Task<TypeReaderResult> ReadAsync(ICommandContext context, string input)
{ {
public override Task<TypeReaderResult> 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<SmartText> public sealed class SmartTextTypeReader : NadekoTypeReader<SmartText>
{
public override Task<TypeReaderResult> ReadAsync(ICommandContext ctx, string input)
{ {
public override Task<TypeReaderResult> ReadAsync(ICommandContext ctx, string input) return Task.FromResult(TypeReaderResult.FromSuccess(SmartText.CreateFrom(input)));
{
return Task.FromResult(TypeReaderResult.FromSuccess(SmartText.CreateFrom(input)));
}
} }
} }

View File

@@ -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); this.Value = value;
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();
} }
}
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();
}

View File

@@ -1,65 +1,62 @@
using System; using System.Text.RegularExpressions;
using System.Collections.Generic;
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(
@"^(?:(?<months>\d)mo)?(?:(?<weeks>\d{1,2})w)?(?:(?<days>\d{1,2})d)?(?:(?<hours>\d{1,4})h)?(?:(?<minutes>\d{1,5})m)?(?:(?<seconds>\d{1,6})s)?$",
RegexOptions.Compiled | RegexOptions.Multiline);
private StoopidTime() { }
public static StoopidTime FromInput(string input)
{ {
public string Input { get; set; } var m = _regex.Match(input);
public TimeSpan Time { get; set; }
private static readonly Regex _regex = new Regex( if (m.Length == 0)
@"^(?:(?<months>\d)mo)?(?:(?<weeks>\d{1,2})w)?(?:(?<days>\d{1,2})d)?(?:(?<hours>\d{1,4})h)?(?:(?<minutes>\d{1,5})m)?(?:(?<seconds>\d{1,6})s)?$",
RegexOptions.Compiled | RegexOptions.Multiline);
private StoopidTime() { }
public static StoopidTime FromInput(string input)
{ {
var m = _regex.Match(input); throw new ArgumentException("Invalid string input format.");
if (m.Length == 0)
{
throw new ArgumentException("Invalid string input format.");
}
string output = "";
var namesAndValues = new Dictionary<string, int>();
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,
};
} }
string output = "";
var namesAndValues = new Dictionary<string, int>();
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,
};
} }
} }

View File

@@ -1,55 +1,53 @@
using System.Linq; using System.Threading.Tasks;
using System.Threading.Tasks;
using Discord.Commands; using Discord.Commands;
using NadekoBot.Extensions; using NadekoBot.Extensions;
namespace NadekoBot.Common.TypeReaders namespace NadekoBot.Common.TypeReaders;
public sealed class ModuleTypeReader : NadekoTypeReader<ModuleInfo>
{ {
public sealed class ModuleTypeReader : NadekoTypeReader<ModuleInfo> private readonly CommandService _cmds;
public ModuleTypeReader(CommandService cmds)
{ {
private readonly CommandService _cmds; _cmds = cmds;
public ModuleTypeReader(CommandService cmds)
{
_cmds = cmds;
}
public override Task<TypeReaderResult> 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));
}
} }
public sealed class ModuleOrCrTypeReader : NadekoTypeReader<ModuleOrCrInfo> public override Task<TypeReaderResult> 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) return Task.FromResult(TypeReaderResult.FromSuccess(module));
{
_cmds = cmds;
}
public override Task<TypeReaderResult> 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; }
} }
} }
public sealed class ModuleOrCrTypeReader : NadekoTypeReader<ModuleOrCrInfo>
{
private readonly CommandService _cmds;
public ModuleOrCrTypeReader(CommandService cmds)
{
_cmds = cmds;
}
public override Task<TypeReaderResult> 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; }
}

View File

@@ -1,14 +1,12 @@
using System; using System.Threading.Tasks;
using System.Threading.Tasks;
using Discord.Commands; using Discord.Commands;
namespace NadekoBot.Common.TypeReaders namespace NadekoBot.Common.TypeReaders;
{
public abstract class NadekoTypeReader<T> : TypeReader
{
public abstract Task<TypeReaderResult> ReadAsync(ICommandContext ctx, string input);
public override Task<TypeReaderResult> ReadAsync(ICommandContext ctx, string input, IServiceProvider services) public abstract class NadekoTypeReader<T> : TypeReader
=> ReadAsync(ctx, input); {
} public abstract Task<TypeReaderResult> ReadAsync(ICommandContext ctx, string input);
}
public override Task<TypeReaderResult> ReadAsync(ICommandContext ctx, string input, IServiceProvider services)
=> ReadAsync(ctx, input);
}

View File

@@ -2,39 +2,38 @@
using Discord.Commands; using Discord.Commands;
using NadekoBot.Common.TypeReaders.Models; using NadekoBot.Common.TypeReaders.Models;
namespace NadekoBot.Common.TypeReaders namespace NadekoBot.Common.TypeReaders;
/// <summary>
/// Used instead of bool for more flexible keywords for true/false only in the permission module
/// </summary>
public sealed class PermissionActionTypeReader : NadekoTypeReader<PermissionAction>
{ {
/// <summary> public override Task<TypeReaderResult> ReadAsync(ICommandContext context, string input)
/// Used instead of bool for more flexible keywords for true/false only in the permission module
/// </summary>
public sealed class PermissionActionTypeReader : NadekoTypeReader<PermissionAction>
{ {
public override Task<TypeReaderResult> ReadAsync(ICommandContext context, string input) input = input.ToUpperInvariant();
switch (input)
{ {
input = input.ToUpperInvariant(); case "1":
switch (input) case "T":
{ case "TRUE":
case "1": case "ENABLE":
case "T": case "ENABLED":
case "TRUE": case "ALLOW":
case "ENABLE": case "PERMIT":
case "ENABLED": case "UNBAN":
case "ALLOW": return Task.FromResult(TypeReaderResult.FromSuccess(PermissionAction.Enable));
case "PERMIT": case "0":
case "UNBAN": case "F":
return Task.FromResult(TypeReaderResult.FromSuccess(PermissionAction.Enable)); case "FALSE":
case "0": case "DENY":
case "F": case "DISABLE":
case "FALSE": case "DISABLED":
case "DENY": case "DISALLOW":
case "DISABLE": case "BAN":
case "DISABLED": return Task.FromResult(TypeReaderResult.FromSuccess(PermissionAction.Disable));
case "DISALLOW": default:
case "BAN": return Task.FromResult(TypeReaderResult.FromError(CommandError.ParseFailed, "Did not receive a valid boolean value"));
return Task.FromResult(TypeReaderResult.FromSuccess(PermissionAction.Disable));
default:
return Task.FromResult(TypeReaderResult.FromError(CommandError.ParseFailed, "Did not receive a valid boolean value"));
}
} }
} }
} }

View File

@@ -1,25 +1,23 @@
using System; using System.Threading.Tasks;
using System.Threading.Tasks;
using Discord.Commands; using Discord.Commands;
using SixLabors.ImageSharp; using SixLabors.ImageSharp;
namespace NadekoBot.Common.TypeReaders namespace NadekoBot.Common.TypeReaders;
{
public sealed class Rgba32TypeReader : NadekoTypeReader<Color>
{
public override async Task<TypeReaderResult> ReadAsync(ICommandContext context, string input)
{
await Task.Yield();
input = input.Replace("#", "", StringComparison.InvariantCulture); public sealed class Rgba32TypeReader : NadekoTypeReader<Color>
try {
{ public override async Task<TypeReaderResult> ReadAsync(ICommandContext context, string input)
return TypeReaderResult.FromSuccess(Color.ParseHex(input)); {
} await Task.Yield();
catch
{ input = input.Replace("#", "", StringComparison.InvariantCulture);
return TypeReaderResult.FromError(CommandError.ParseFailed, "Parameter is not a valid color hex."); try
} {
return TypeReaderResult.FromSuccess(Color.ParseHex(input));
}
catch
{
return TypeReaderResult.FromError(CommandError.ParseFailed, "Parameter is not a valid color hex.");
} }
} }
} }

View File

@@ -1,107 +1,105 @@
using Discord.Commands; using Discord.Commands;
using System;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using System.Threading.Tasks; using System.Threading.Tasks;
using NadekoBot.Db; using NadekoBot.Db;
using NadekoBot.Modules.Gambling.Services; using NadekoBot.Modules.Gambling.Services;
using NadekoBot.Services; using NadekoBot.Services;
namespace NadekoBot.Common.TypeReaders namespace NadekoBot.Common.TypeReaders;
public sealed class ShmartNumberTypeReader : NadekoTypeReader<ShmartNumber>
{ {
public sealed class ShmartNumberTypeReader : NadekoTypeReader<ShmartNumber> private readonly DbService _db;
private readonly GamblingConfigService _gambling;
public ShmartNumberTypeReader(DbService db, GamblingConfigService gambling)
{ {
private readonly DbService _db; _db = db;
private readonly GamblingConfigService _gambling; _gambling = gambling;
}
public ShmartNumberTypeReader(DbService db, GamblingConfigService gambling) public override async Task<TypeReaderResult> 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; var expr = new NCalc.Expression(i, NCalc.EvaluateOptions.IgnoreCase);
_gambling = gambling; 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)
public override async Task<TypeReaderResult> ReadAsync(ICommandContext context, string input)
{ {
await Task.Yield(); return TypeReaderResult.FromError(CommandError.ParseFailed, $"Invalid input: {input}");
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(@"^((?<num>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;
} }
} }
}
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(@"^((?<num>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;
}
}

View File

@@ -1,25 +1,23 @@
using Discord.Commands; using Discord.Commands;
using NadekoBot.Common.TypeReaders.Models; using NadekoBot.Common.TypeReaders.Models;
using System;
using System.Threading.Tasks; using System.Threading.Tasks;
namespace NadekoBot.Common.TypeReaders namespace NadekoBot.Common.TypeReaders;
public sealed class StoopidTimeTypeReader : NadekoTypeReader<StoopidTime>
{ {
public sealed class StoopidTimeTypeReader : NadekoTypeReader<StoopidTime> public override Task<TypeReaderResult> ReadAsync(ICommandContext context, string input)
{ {
public override Task<TypeReaderResult> ReadAsync(ICommandContext context, string input) if (string.IsNullOrWhiteSpace(input))
return Task.FromResult(TypeReaderResult.FromError(CommandError.Unsuccessful, "Input is empty."));
try
{ {
if (string.IsNullOrWhiteSpace(input)) var time = StoopidTime.FromInput(input);
return Task.FromResult(TypeReaderResult.FromError(CommandError.Unsuccessful, "Input is empty.")); return Task.FromResult(TypeReaderResult.FromSuccess(time));
try }
{ catch (Exception ex)
var time = StoopidTime.FromInput(input); {
return Task.FromResult(TypeReaderResult.FromSuccess(time)); return Task.FromResult(TypeReaderResult.FromError(CommandError.Exception, ex.Message));
}
catch (Exception ex)
{
return Task.FromResult(TypeReaderResult.FromError(CommandError.Exception, ex.Message));
}
} }
} }
} }

View File

@@ -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) public CommentAttribute(string comment)
{ {
Comment = comment; Comment = comment;
}
} }
} }

View File

@@ -1,73 +1,69 @@
using System; using YamlDotNet.Core;
using System.Collections.Generic;
using System.Linq;
using YamlDotNet.Core;
using YamlDotNet.Serialization; using YamlDotNet.Serialization;
using YamlDotNet.Serialization.TypeInspectors; 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<IPropertyDescriptor> 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<IPropertyDescriptor> GetProperties(Type type, object container) public string Name { get; set; }
{
return innerTypeDescriptor public Type Type { get { return baseDescriptor.Type; } }
.GetProperties(type, container)
.Select(d => new CommentsPropertyDescriptor(d)); 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) public T GetCustomAttribute<T>() where T : Attribute
{ {
this.baseDescriptor = baseDescriptor; return baseDescriptor.GetCustomAttribute<T>();
Name = baseDescriptor.Name; }
}
public string Name { get; set; } public IObjectDescriptor Read(object target)
{
public Type Type { get { return baseDescriptor.Type; } } var comment = baseDescriptor.GetCustomAttribute<CommentAttribute>();
return comment != null
public Type TypeOverride { ? new CommentsObjectDescriptor(baseDescriptor.Read(target), comment.Comment)
get { return baseDescriptor.TypeOverride; } : baseDescriptor.Read(target);
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<T>() where T : Attribute
{
return baseDescriptor.GetCustomAttribute<T>();
}
public IObjectDescriptor Read(object target)
{
var comment = baseDescriptor.GetCustomAttribute<CommentAttribute>();
return comment != null
? new CommentsObjectDescriptor(baseDescriptor.Read(target), comment.Comment)
: baseDescriptor.Read(target);
}
} }
} }
} }

View File

@@ -1,24 +1,22 @@
using System; using YamlDotNet.Core;
using YamlDotNet.Core;
using YamlDotNet.Serialization; 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; this.innerDescriptor = innerDescriptor;
this.Comment = comment;
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; } }
} }
}
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; } }
}

View File

@@ -3,24 +3,23 @@ using YamlDotNet.Core.Events;
using YamlDotNet.Serialization; using YamlDotNet.Serialization;
using YamlDotNet.Serialization.ObjectGraphVisitors; using YamlDotNet.Serialization.ObjectGraphVisitors;
namespace NadekoBot.Common.Yml namespace NadekoBot.Common.Yml;
public class CommentsObjectGraphVisitor : ChainedObjectGraphVisitor
{ {
public class CommentsObjectGraphVisitor : ChainedObjectGraphVisitor public CommentsObjectGraphVisitor(IObjectGraphVisitor<IEmitter> nextVisitor)
: base(nextVisitor)
{ {
public CommentsObjectGraphVisitor(IObjectGraphVisitor<IEmitter> 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);
}
}

View File

@@ -2,31 +2,30 @@
using YamlDotNet.Serialization; using YamlDotNet.Serialization;
using YamlDotNet.Serialization.EventEmitters; 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))
{ {
string value = eventInfo.Source.Value as string;
if (typeof(string).IsAssignableFrom(eventInfo.Source.Type)) if (!string.IsNullOrEmpty(value))
{ {
string value = eventInfo.Source.Value as string; bool isMultiLine = value.IndexOfAny(new char[] { '\r', '\n', '\x85', '\x2028', '\x2029' }) >= 0;
if (!string.IsNullOrEmpty(value)) if (isMultiLine)
{ eventInfo = new ScalarEventInfo(eventInfo.Source)
bool isMultiLine = value.IndexOfAny(new char[] { '\r', '\n', '\x85', '\x2028', '\x2029' }) >= 0; {
if (isMultiLine) Style = ScalarStyle.Literal,
eventInfo = new ScalarEventInfo(eventInfo.Source) };
{
Style = ScalarStyle.Literal,
};
}
} }
nextEmitter.Emit(eventInfo, emitter);
} }
nextEmitter.Emit(eventInfo, emitter);
} }
} }

View File

@@ -1,52 +1,50 @@
using System; using System.Globalization;
using System.Globalization;
using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.PixelFormats;
using YamlDotNet.Core; using YamlDotNet.Core;
using YamlDotNet.Core.Events; using YamlDotNet.Core.Events;
using YamlDotNet.Serialization; 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);
{
return type == typeof(Rgba32);
}
public object ReadYaml(IParser parser, Type type)
{
var scalar = parser.Consume<Scalar>();
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()));
}
} }
public class CultureInfoConverter : IYamlTypeConverter public object ReadYaml(IParser parser, Type type)
{ {
public bool Accepts(Type type) var scalar = parser.Consume<Scalar>();
{ var result = Rgba32.ParseHex(scalar.Value);
return type == typeof(CultureInfo); return result;
} }
public object ReadYaml(IParser parser, Type type) public void WriteYaml(IEmitter emitter, object value, Type type)
{ {
var scalar = parser.Consume<Scalar>(); var color = (Rgba32)value;
var result = new CultureInfo(scalar.Value); var val = (uint) (color.B << 0 | color.G << 8 | color.R << 16);
return result; 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) public object ReadYaml(IParser parser, Type type)
{ {
var ci = (CultureInfo)value; var scalar = parser.Consume<Scalar>();
emitter.Emit(new Scalar(ci.Name)); 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));
} }
} }

View File

@@ -1,28 +1,26 @@
using System; using YamlDotNet.Core;
using YamlDotNet.Core;
using YamlDotNet.Core.Events; using YamlDotNet.Core.Events;
using YamlDotNet.Serialization; 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) public object ReadYaml(IParser parser, Type type)
{ {
var scalar = parser.Consume<Scalar>(); var scalar = parser.Consume<Scalar>();
var result = new Uri(scalar.Value); var result = new Uri(scalar.Value);
return result; return result;
} }
public void WriteYaml(IEmitter emitter, object value, Type type) public void WriteYaml(IEmitter emitter, object value, Type type)
{ {
var uri = (Uri)value; var uri = (Uri)value;
emitter.Emit(new Scalar(uri.ToString())); emitter.Emit(new Scalar(uri.ToString()));
}
} }
} }

View File

@@ -1,26 +1,25 @@
using YamlDotNet.Serialization; using YamlDotNet.Serialization;
namespace NadekoBot.Common.Yml 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();
public static IDeserializer Deserializer => new DeserializerBuilder() public class Yaml
.WithNamingConvention(YamlDotNet.Serialization.NamingConventions.CamelCaseNamingConvention.Instance) {
.WithTypeConverter(new Rgba32Converter()) public static ISerializer Serializer => new SerializerBuilder()
.WithTypeConverter(new CultureInfoConverter()) .WithTypeInspector(inner => new CommentGatheringTypeInspector(inner))
.WithTypeConverter(new UriConverter()) .WithEmissionPhaseObjectGraphVisitor(args => new CommentsObjectGraphVisitor(args.InnerVisitor))
.IgnoreUnmatchedProperties() .WithEventEmitter(args => new MultilineScalarFlowStyleEmitter(args))
.Build(); .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();
} }

View File

@@ -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
/// <summary>
/// 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
/// </summary>
/// <param name="point">Unicode code point</param>
/// <returns>Actual character</returns>
public static string UnescapeUnicodeCodePoint(string point)
{ {
// https://github.com/aaubry/YamlDotNet/blob/0f4cc205e8b2dd8ef6589d96de32bf608a687c6f/YamlDotNet/Core/Scanner.cs#L1687 var character = 0;
/// <summary>
/// This is modified code from yamldotnet's repo which handles parsing unicode code points // Scan the character value.
/// it is needed as yamldotnet doesn't support unescaped unicode characters
/// </summary> foreach(var c in point)
/// <param name="point">Unicode code point</param>
/// <returns>Actual character</returns>
public static string UnescapeUnicodeCodePoint(string point)
{ {
var character = 0; if (!IsHex(c))
// 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)
{ {
return point; return point;
} }
character = (character << 4) + AsHex(c);
}
return char.ConvertFromUtf32(character); // Check the value and write the character.
}
if (character >= 0xD800 && character <= 0xDFFF || character > 0x10FFFF)
public static bool IsHex(char c)
{ {
return return point;
(c >= '0' && c <= '9') ||
(c >= 'A' && c <= 'F') ||
(c >= 'a' && c <= 'f');
} }
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';
{
return c - '0';
}
if (c <= 'F')
{
return c - 'A' + 10;
}
return c - 'a' + 10;
} }
if (c <= 'F')
{
return c - 'A' + 10;
}
return c - 'a' + 10;
} }
} }

View File

@@ -1,48 +1,45 @@
using System.Linq; using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore;
using System.Collections.Generic;
using NadekoBot.Db.Models; using NadekoBot.Db.Models;
namespace NadekoBot.Db namespace NadekoBot.Db;
public static class ClubExtensions
{ {
public static class ClubExtensions private static IQueryable<ClubInfo> Include(this DbSet<ClubInfo> clubs)
{ => clubs.Include(x => x.Owner)
private static IQueryable<ClubInfo> Include(this DbSet<ClubInfo> clubs) .Include(x => x.Applicants)
=> clubs.Include(x => x.Owner) .ThenInclude(x => x.User)
.Include(x => x.Applicants) .Include(x => x.Bans)
.ThenInclude(x => x.User) .ThenInclude(x => x.User)
.Include(x => x.Bans) .Include(x => x.Users)
.ThenInclude(x => x.User) .AsQueryable();
.Include(x => x.Users) public static ClubInfo GetByOwner(this DbSet<ClubInfo> clubs, ulong userId)
.AsQueryable(); => Include(clubs).FirstOrDefault(c => c.Owner.UserId == userId);
public static ClubInfo GetByOwner(this DbSet<ClubInfo> clubs, ulong userId)
=> Include(clubs).FirstOrDefault(c => c.Owner.UserId == userId);
public static ClubInfo GetByOwnerOrAdmin(this DbSet<ClubInfo> clubs, ulong userId) public static ClubInfo GetByOwnerOrAdmin(this DbSet<ClubInfo> clubs, ulong userId)
=> Include(clubs).FirstOrDefault(c => c.Owner.UserId == userId => Include(clubs).FirstOrDefault(c => c.Owner.UserId == userId
|| c.Users.Any(u => u.UserId == userId && u.IsClubAdmin)); || c.Users.Any(u => u.UserId == userId && u.IsClubAdmin));
public static ClubInfo GetByMember(this DbSet<ClubInfo> clubs, ulong userId) public static ClubInfo GetByMember(this DbSet<ClubInfo> clubs, ulong userId)
=> Include(clubs).FirstOrDefault(c => c.Users.Any(u => u.UserId == userId)); => Include(clubs).FirstOrDefault(c => c.Users.Any(u => u.UserId == userId));
public static ClubInfo GetByName(this DbSet<ClubInfo> clubs, string name, int discrim) public static ClubInfo GetByName(this DbSet<ClubInfo> clubs, string name, int discrim)
=> Include(clubs).FirstOrDefault(c => c.Name.ToUpper() == name.ToUpper() && c.Discrim == discrim); => Include(clubs).FirstOrDefault(c => c.Name.ToUpper() == name.ToUpper() && c.Discrim == discrim);
public static int GetNextDiscrim(this DbSet<ClubInfo> clubs, string name) public static int GetNextDiscrim(this DbSet<ClubInfo> clubs, string name)
=> Include(clubs) => Include(clubs)
.Where(x => x.Name.ToUpper() == name.ToUpper()) .Where(x => x.Name.ToUpper() == name.ToUpper())
.Select(x => x.Discrim) .Select(x => x.Discrim)
.DefaultIfEmpty() .DefaultIfEmpty()
.Max() + 1; .Max() + 1;
public static List<ClubInfo> GetClubLeaderboardPage(this DbSet<ClubInfo> clubs, int page) public static List<ClubInfo> GetClubLeaderboardPage(this DbSet<ClubInfo> clubs, int page)
{ {
return clubs return clubs
.AsNoTracking() .AsNoTracking()
.OrderByDescending(x => x.Xp) .OrderByDescending(x => x.Xp)
.Skip(page * 9) .Skip(page * 9)
.Take(9) .Take(9)
.ToList(); .ToList();
}
} }
} }

View File

@@ -1,21 +1,18 @@
using System.Collections.Generic; using Microsoft.EntityFrameworkCore;
using System.Linq;
using Microsoft.EntityFrameworkCore;
using NadekoBot.Services.Database.Models; using NadekoBot.Services.Database.Models;
namespace NadekoBot.Db namespace NadekoBot.Db;
public static class CurrencyTransactionExtensions
{ {
public static class CurrencyTransactionExtensions public static List<CurrencyTransaction> GetPageFor(this DbSet<CurrencyTransaction> set, ulong userId, int page)
{ {
public static List<CurrencyTransaction> GetPageFor(this DbSet<CurrencyTransaction> set, ulong userId, int page) return set.AsQueryable()
{ .AsNoTracking()
return set.AsQueryable() .Where(x => x.UserId == userId)
.AsNoTracking() .OrderByDescending(x => x.DateAdded)
.Where(x => x.UserId == userId) .Skip(15 * page)
.OrderByDescending(x => x.DateAdded) .Take(15)
.Skip(15 * page) .ToList();
.Take(15)
.ToList();
}
} }
} }

View File

@@ -1,30 +1,27 @@
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using System.Collections.Generic;
using System.Linq;
using LinqToDB; using LinqToDB;
using NadekoBot.Services.Database.Models; 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<CustomReaction> crs, ulong guildId)
{ {
public static int ClearFromGuild(this DbSet<CustomReaction> crs, ulong guildId) return crs.Delete(x => x.GuildId == guildId);
{
return crs.Delete(x => x.GuildId == guildId);
}
public static IEnumerable<CustomReaction> ForId(this DbSet<CustomReaction> crs, ulong id)
{
return crs
.AsNoTracking()
.AsQueryable()
.Where(x => x.GuildId == id)
.ToArray();
}
public static CustomReaction GetByGuildIdAndInput(this DbSet<CustomReaction> crs, ulong? guildId, string input)
{
return crs.FirstOrDefault(x => x.GuildId == guildId && x.Trigger.ToUpper() == input);
}
} }
}
public static IEnumerable<CustomReaction> ForId(this DbSet<CustomReaction> crs, ulong id)
{
return crs
.AsNoTracking()
.AsQueryable()
.Where(x => x.GuildId == id)
.ToArray();
}
public static CustomReaction GetByGuildIdAndInput(this DbSet<CustomReaction> crs, ulong? guildId, string input)
{
return crs.FirstOrDefault(x => x.GuildId == guildId && x.Trigger.ToUpper() == input);
}
}

Some files were not shown because too many files have changed in this diff Show More