Added and applied styles for private readonly fields, private fields to Extensions and Common folders.

- Some renamings and code cleanups
- Chained method calls, binary expressions and binary patterns will now break into newlines
- Type param constraints and base constructor calls will be on the new line
This commit is contained in:
Kwoth
2021-12-27 03:46:30 +01:00
parent 9ae030a5c5
commit 1b0392dfab
85 changed files with 1015 additions and 906 deletions

View File

@@ -1,8 +1,9 @@
namespace NadekoBot.Common.TypeReaders;
namespace NadekoBot.Common;
public enum AddRemove
{
Add = int.MinValue,
Remove = int.MinValue + 1,
Rem = int.MinValue + 1,
Rm = int.MinValue + 1,
}

View File

@@ -4,13 +4,15 @@ namespace NadekoBot.Common;
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) :
base(() => Task.Run(taskFactory))
{ }
public AsyncLazy(Func<Task<T>> taskFactory)
: base(() => Task.Run(taskFactory))
{
}
public TaskAwaiter<T> GetAwaiter()
=> Value.GetAwaiter();

View File

@@ -8,10 +8,7 @@ public class CmdStrings
public string Description { get; }
[JsonConstructor]
public CmdStrings(
[JsonProperty("args")]string[] usages,
[JsonProperty("desc")]string description
)
public CmdStrings([JsonProperty("args")] string[] usages, [JsonProperty("desc")] string description)
{
Usages = usages;
Description = description;

View File

@@ -3,14 +3,20 @@ using NadekoBot.Services.Database.Models;
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();
public int Count => Source.Count;
public bool IsReadOnly => false;
public int IndexOf(T item) => item.Index;
public int Count
=> Source.Count;
public bool IsReadOnly
=> false;
public int IndexOf([NotNull] T item)
=> item.Index;
public IndexedCollection()
=> Source = new();
@@ -36,16 +42,17 @@ public class IndexedCollection<T> : IList<T> where T : class, IIndexed
}
}
public static implicit operator List<T>(IndexedCollection<T> x) =>
x.Source;
public static implicit operator List<T>(IndexedCollection<T> x)
=> x.Source;
public List<T> ToList() => Source.ToList();
public List<T> ToList()
=> Source.ToList();
public IEnumerator<T> GetEnumerator() =>
Source.GetEnumerator();
public IEnumerator<T> GetEnumerator()
=> Source.GetEnumerator();
IEnumerator IEnumerable.GetEnumerator() =>
Source.GetEnumerator();
IEnumerator IEnumerable.GetEnumerator()
=> Source.GetEnumerator();
public void Add(T item)
{
@@ -82,19 +89,21 @@ public class IndexedCollection<T> : IList<T> where T : class, IIndexed
public virtual bool Remove(T item)
{
bool removed;
lock (_locker)
{
if (removed = Source.Remove(item))
if (Source.Remove(item))
{
for (var i = 0; i < Source.Count; i++)
{
if (Source[i].Index != i)
Source[i].Index = i;
}
return true;
}
}
return removed;
return false;
}
public virtual void Insert(int index, T item)

View File

@@ -18,14 +18,16 @@ next to the response. The color depends whether the command
is completed, errored or in progress (pending)
Color settings below are for the color of those lines.
To get color's hex, you can go here https://htmlcolorcodes.com/
and copy the hex code fo your selected color (marked as #)")]
and copy the hex code fo your selected color (marked as #)"
)]
public ColorConfig Color { get; set; }
[Comment("Default bot language. It has to be in the list of supported languages (.langli)")]
public CultureInfo DefaultLocale { get; set; }
[Comment(@"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; }
// [Comment(@"For what kind of updates will the bot check.
@@ -38,30 +40,35 @@ Allowed values: Simple, Normal, None")]
[Comment(@"Do you want any messages sent by users in Bot's DM to be forwarded to the owner(s)?")]
public bool ForwardMessages { get; set; }
[Comment(@"Do you want the message to be forwarded only to the first owner specified in the list of owners (in creds.yml),
or all owners? (this might cause the bot to lag if there's a lot of owners specified)")]
[Comment(
@"Do you want the message to be forwarded only to the first owner specified in the list of owners (in creds.yml),
or all owners? (this might cause the bot to lag if there's a lot of owners specified)"
)]
public bool ForwardToAllOwners { get; set; }
[Comment(@"When a user DMs the bot with a message which is not a command
they will receive this message. Leave empty for no response. The string which will be sent whenever someone DMs the bot.
Supports embeds. How it looks: https://puu.sh/B0BLV.png")]
Supports embeds. How it looks: https://puu.sh/B0BLV.png"
)]
[YamlMember(ScalarStyle = ScalarStyle.Literal)]
public string DmHelpText { get; set; }
[Comment(@"Only users who send a DM to the bot containing one of the specified words will get a DmHelpText response.
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; }
[Comment(@"This is the response for the .h command")]
[YamlMember(ScalarStyle = ScalarStyle.Literal)]
public string HelpText { get; set; }
[Comment(@"List of modules and commands completely blocked on the bot")]
public BlockedConfig Blocked { get; set; }
[Comment(@"Which string will be used to recognize the commands")]
public string Prefix { get; set; }
[Comment(@"Toggles whether your bot will group greet/bye messages into a single message every 5 seconds.
1st user who joins will get greeted immediately
If more users join within the next 5 seconds, they will be greeted in groups of 5.
@@ -70,12 +77,14 @@ Keep in mind this might break some of your embeds - for example if you have %use
it will become invalid, as it will resolve to a list of avatars of grouped users.
note: This setting is primarily used if you're afraid of raids, or you're running medium/large bots where some
servers might get hundreds of people join at once. This is used to prevent the bot from getting ratelimited,
and (slightly) reduce the greet spam in those servers.")]
and (slightly) reduce the greet spam in those servers."
)]
public bool GroupGreets { get; set; }
[Comment(@"Whether the bot will rotate through all specified statuses.
This setting can be changed via .rots command.
See RotatingStatuses submodule in Administration.")]
See RotatingStatuses submodule in Administration."
)]
public bool RotateStatuses { get; set; }
// [Comment(@"Whether the prefix will be a suffix, or prefix.
@@ -159,10 +168,10 @@ 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; }
@@ -173,7 +182,7 @@ public partial class ColorConfig
Pending = Rgba32.ParseHex("faa61a");
}
}
public enum ConsoleOutputType
{
Normal = 0,

View File

@@ -11,81 +11,93 @@ public sealed class Creds : IBotCredentials
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);
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"
};
Db = new() { Type = "sqlite", ConnectionString = "Data Source=data/NadekoBot.db" };
CoordinatorUrl = "http://localhost:3442";
RestartCommand = new()
{
};
RestartCommand = new();
}
[Comment(@"DO NOT CHANGE")]
public int Version { get; set; }
[Comment(@"Bot token. Do not share with anyone ever -> https://discordapp.com/developers/applications/")]
public string Token { get; set; }
[Comment(@"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; }
[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; }
[Comment(@"Login to https://console.cloud.google.com, create a new project, go to APIs & Services -> Library -> YouTube Data API and enable it.
[Comment(
@"Login to https://console.cloud.google.com, create a new project, go to APIs & Services -> Library -> YouTube Data API and enable it.
Then, go to APIs and Services -> Credentials and click Create credentials -> API key.
Used only for Youtube Data Api (at the moment).")]
Used only for Youtube Data Api (at the moment)."
)]
public string GoogleApiKey { get; set; }
[Comment(@"Settings for voting system for discordbots. Meant for use on global Nadeko.")]
public VotesSettings Votes { get; set; }
[Comment(@"Patreon auto reward system settings.
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; }
[Comment(@"Api key for sending stats to DiscordBotList.")]
public string BotListToken { get; set; }
[Comment(@"Official cleverbot api key.")]
public string CleverbotApiKey { get; set; }
[Comment(@"Redis connection string. Don't change if you don't know what you're doing.")]
public string RedisOptions { get; set; }
[Comment(@"Database options. Don't change if you don't know what you're doing. Leave null for default values")]
public DbOptions Db { get; set; }
[Comment(@"Address and port of the coordinator endpoint. Leave empty for default.
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; }
[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; }
[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; }
[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; }
[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; }
[Comment(@"Api key used for Osu related commands. Obtain this key at https://osu.ppy.sh/p/api")]
public string OsuApiKey { get; set; }
@@ -99,7 +111,8 @@ Linux default
args: ""NadekoBot.dll -- {0}""
Windows default
cmd: NadekoBot.exe
args: {0}")]
args: {0}"
)]
public RestartConfig RestartCommand { get; set; }
@@ -107,6 +120,7 @@ Windows default
{
[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; }
}
@@ -118,10 +132,16 @@ Windows default
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)")]
[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)
public PatreonSettings(
string accessToken,
string refreshToken,
string clientSecret,
string campaignId)
{
AccessToken = accessToken;
RefreshToken = refreshToken;
@@ -131,7 +151,6 @@ Windows default
public PatreonSettings()
{
}
}
@@ -139,28 +158,35 @@ Windows default
{
[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")]
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")]
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")]
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")]
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)
public VotesSettings(
string topggServiceUrl,
string topggKey,
string discordsServiceUrl,
string discordsKey)
{
TopggServiceUrl = topggServiceUrl;
TopggKey = topggKey;

View File

@@ -3,7 +3,7 @@
public class DownloadTracker : INService
{
private ConcurrentDictionary<ulong, DateTime> LastDownloads { get; } = new();
private readonly SemaphoreSlim downloadUsersSemaphore = new(1, 1);
private readonly SemaphoreSlim _downloadUsersSemaphore = new(1, 1);
/// <summary>
/// Ensures all users on the specified guild were downloaded within the last hour.
@@ -12,16 +12,16 @@ public class DownloadTracker : INService
/// <returns>Task representing download state</returns>
public async Task EnsureUsersDownloadedAsync(IGuild guild)
{
await downloadUsersSemaphore.WaitAsync();
await _downloadUsersSemaphore.WaitAsync();
try
{
var now = DateTime.UtcNow;
// download once per hour at most
var added = LastDownloads.AddOrUpdate(
guild.Id,
var added = LastDownloads.AddOrUpdate(guild.Id,
now,
(key, old) => now - old > TimeSpan.FromHours(1) ? now : old);
(_, old) => now - old > TimeSpan.FromHours(1) ? now : old
);
// means that this entry was just added - download the users
if (added == now)
@@ -29,7 +29,7 @@ public class DownloadTracker : INService
}
finally
{
downloadUsersSemaphore.Release();
_downloadUsersSemaphore.Release();
}
}
}

View File

@@ -1,7 +0,0 @@
namespace NadekoBot.Extensions;
public static class BotCredentialsExtensions
{
public static bool IsOwner(this IBotCredentials creds, IUser user)
=> creds.OwnerIds.Contains(user.Id);
}

View File

@@ -1,75 +0,0 @@
using System.Reflection;
using Microsoft.Extensions.DependencyInjection;
using NadekoBot.Modules.Music;
using NadekoBot.Modules.Music.Resolvers;
using NadekoBot.Modules.Music.Services;
using StackExchange.Redis;
namespace NadekoBot.Extensions;
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)
{
var baseType = typeof(ConfigServiceBase<>);
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));
}
}
return services;
}
public static IServiceCollection AddConfigMigrators(this IServiceCollection services)
=> services.AddSealedSubclassesOf(typeof(IConfigMigrator));
public static IServiceCollection AddMusic(this IServiceCollection services)
=> services
.AddSingleton<IMusicService, MusicService>()
.AddSingleton<ITrackResolveProvider, TrackResolveProvider>()
.AddSingleton<IYoutubeResolver, YtdlYoutubeResolver>()
.AddSingleton<ISoundcloudResolver, SoundcloudResolver>()
.AddSingleton<ILocalTrackResolver, LocalTrackResolver>()
.AddSingleton<IRadioResolver, RadioResolver>()
.AddSingleton<ITrackCacher, RedisTrackCacher>()
.AddSingleton<YtLoader>()
.AddSingleton<IPlaceholderProvider>(svc => svc.GetRequiredService<IMusicService>());
// consider using scrutor, because slightly different versions
// of this might be needed in several different places
public static IServiceCollection AddSealedSubclassesOf(this IServiceCollection services, Type baseType)
{
var subTypes = Assembly.GetCallingAssembly()
.ExportedTypes
.Where(type => type.IsSealed && baseType.IsAssignableFrom(type));
foreach (var subType in subTypes)
{
services.AddSingleton(baseType, subType);
}
return services;
}
public static IServiceCollection AddRedis(this IServiceCollection services, string redisOptions)
{
var conf = ConfigurationOptions.Parse(redisOptions);
services.AddSingleton(ConnectionMultiplexer.Connect(conf));
return services;
}
}

View File

@@ -6,7 +6,7 @@ public static class Helpers
{
if (!Console.IsInputRedirected)
Console.ReadKey();
Environment.Exit(exitCode);
}
}

View File

@@ -21,7 +21,7 @@ public interface IBotCredentials
string CoinmarketcapApiKey { get; }
string CoordinatorUrl { get; set; }
}
public class RestartConfig
{
public string Cmd { get; set; }

View File

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

View File

@@ -1,23 +1,13 @@
using System.Globalization;
using System.Text.Json;
using System.Text.Json.Serialization;
using SixLabors.ImageSharp.PixelFormats;
namespace NadekoBot.Common.JsonConverters;
public class Rgba32Converter : JsonConverter<Rgba32>
{
public override Rgba32 Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
=> 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)
=> new(reader.GetString());
=> new(reader.GetString() ?? "en-US");
public override void Write(Utf8JsonWriter writer, CultureInfo value, JsonSerializerOptions options)
=> writer.WriteStringValue(value.Name);

View File

@@ -0,0 +1,14 @@
using SixLabors.ImageSharp.PixelFormats;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace NadekoBot.Common.JsonConverters;
public class Rgba32Converter : JsonConverter<Rgba32>
{
public override Rgba32 Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
=> Rgba32.ParseHex(reader.GetString());
public override void Write(Utf8JsonWriter writer, Rgba32 value, JsonSerializerOptions options)
=> writer.WriteStringValue(value.ToHex());
}

View File

@@ -4,14 +4,17 @@ namespace NadekoBot.Common;
// needs proper invalid input check (character array input out of range)
// needs negative number support
// ReSharper disable once InconsistentNaming
#pragma warning disable IDE1006
public readonly struct kwum : IEquatable<kwum>
#pragma warning restore IDE1006
{
private readonly int _value;
private const string ValidCharacters = "23456789abcdefghijkmnpqrstuvwxyz";
private const string VALID_CHARACTERS = "23456789abcdefghijkmnpqrstuvwxyz";
public kwum(int num)
=> _value = num;
public kwum(in char c)
{
if (!IsValidChar(c))
@@ -21,11 +24,11 @@ public readonly struct kwum : IEquatable<kwum>
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static int InternalCharToValue(in char c)
=> ValidCharacters.IndexOf(c);
private static int InternalCharToValue(in char c)
=> VALID_CHARACTERS.IndexOf(c);
public kwum(in ReadOnlySpan<char> input)
{;
{
_value = 0;
for (var index = 0; index < input.Length; index++)
{
@@ -33,14 +36,14 @@ public readonly struct kwum : IEquatable<kwum>
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);
_value += VALID_CHARACTERS.IndexOf(c) * (int)Math.Pow(VALID_CHARACTERS.Length, input.Length - index - 1);
}
}
public static bool TryParse(in ReadOnlySpan<char> input, out kwum value)
{
value = default;
foreach(var c in input)
foreach (var c in input)
if (!IsValidChar(c))
return false;
@@ -59,25 +62,26 @@ public readonly struct kwum : IEquatable<kwum>
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(num);
public static bool IsValidChar(char c)
=> ValidCharacters.Contains(c);
=> VALID_CHARACTERS.Contains(c);
public override string ToString()
{
var count = ValidCharacters.Length;
var count = VALID_CHARACTERS.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[rem];
chars[--arrSize] = VALID_CHARACTERS[rem];
}
return new(chars);

View File

@@ -4,10 +4,14 @@ namespace NadekoBot.Common;
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 void NormalizeOptions()
{
}
}

View File

@@ -17,18 +17,21 @@ public class LoginErrorHandler
case HttpStatusCode.Unauthorized:
Log.Error("Your bot token is wrong.\n" +
"You can find the bot token under the Bot tab in the developer page.\n" +
"Fix your token in the credentials file and restart the bot");
"Fix your token in the credentials file and restart the bot"
);
break;
case HttpStatusCode.BadRequest:
Log.Error("Something has been incorrectly formatted in your credentials file.\n" +
"Use the JSON Guide as reference to fix it and restart the bot.");
"Use the JSON Guide as reference to fix it and restart the bot"
);
Log.Error("If you are on Linux, make sure Redis is installed and running");
break;
case HttpStatusCode.RequestTimeout:
Log.Error("The request timed out. Make sure you have no external program blocking the bot " +
"from connecting to the internet");
"from connecting to the internet"
);
break;
case HttpStatusCode.ServiceUnavailable:
@@ -38,7 +41,8 @@ public class LoginErrorHandler
case HttpStatusCode.TooManyRequests:
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;
default:
@@ -46,6 +50,6 @@ public class LoginErrorHandler
break;
}
Log.Fatal(ex.ToString());
Log.Fatal(ex, "Fatal error occurred while loading credentials");
}
}

View File

@@ -1,6 +0,0 @@
namespace NadekoBot.Common.ModuleBehaviors;
public interface IInputTransformer
{
Task<string> TransformInput(IGuild guild, IMessageChannel channel, IUser user, string input);
}

View File

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

View File

@@ -3,6 +3,6 @@
public interface ILateBlocker
{
public int Priority { get; }
Task<bool> TryBlockLate(ICommandContext context, string moduleName, CommandInfo command);
}

View File

@@ -1,60 +1,78 @@
using System.Globalization;
// ReSharper disable InconsistentNaming
namespace NadekoBot.Modules;
[UsedImplicitly(ImplicitUseTargetFlags.Default
| ImplicitUseTargetFlags.WithInheritors
| ImplicitUseTargetFlags.WithMembers)]
[UsedImplicitly(ImplicitUseTargetFlags.Default |
ImplicitUseTargetFlags.WithInheritors |
ImplicitUseTargetFlags.WithMembers
)]
public abstract class NadekoModule : ModuleBase
{
protected CultureInfo _cultureInfo { get; set; }
protected CultureInfo Culture { 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);
public string Prefix
=> CmdHandler.GetPrefix(ctx.Guild);
protected ICommandContext ctx => Context;
protected NadekoModule()
{
}
protected ICommandContext ctx
=> Context;
protected override void BeforeExecute(CommandInfo cmd)
=> _cultureInfo = Localization.GetCultureInfo(ctx.Guild?.Id);
=> Culture = Localization.GetCultureInfo(ctx.Guild?.Id);
protected string GetText(in LocStr data) =>
Strings.GetText(data, _cultureInfo);
protected string GetText(in LocStr data)
=> Strings.GetText(data, Culture);
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> 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> 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)
public Task<IUserMessage> ErrorLocalizedAsync(LocStr str)
=> SendErrorAsync(GetText(str));
public Task<IUserMessage> PendingLocalizedAsync(LocStr str)
public Task<IUserMessage> PendingLocalizedAsync(LocStr str)
=> SendPendingAsync(GetText(str));
public Task<IUserMessage> ConfirmLocalizedAsync(LocStr str)
public Task<IUserMessage> ConfirmLocalizedAsync(LocStr str)
=> SendConfirmAsync(GetText(str));
public Task<IUserMessage> ReplyErrorLocalizedAsync(LocStr str)
public Task<IUserMessage> ReplyErrorLocalizedAsync(LocStr str)
=> SendErrorAsync($"{Format.Bold(ctx.User.ToString())} {GetText(str)}");
public Task<IUserMessage> ReplyPendingLocalizedAsync(LocStr str)
public Task<IUserMessage> ReplyPendingLocalizedAsync(LocStr str)
=> SendPendingAsync($"{Format.Bold(ctx.User.ToString())} {GetText(str)}");
public Task<IUserMessage> ReplyConfirmLocalizedAsync(LocStr str)
@@ -62,17 +80,19 @@ public abstract class NadekoModule : ModuleBase
public async Task<bool> PromptUserConfirmAsync(IEmbedBuilder embed)
{
embed
.WithPendingColor()
embed.WithPendingColor()
.WithFooter("yes/no");
var msg = await ctx.Channel.EmbedAsync(embed).ConfigureAwait(false);
var msg = await ctx.Channel.EmbedAsync(embed)
.ConfigureAwait(false);
try
{
var input = await GetUserInputAsync(ctx.User.Id, ctx.Channel.Id).ConfigureAwait(false);
var input = await GetUserInputAsync(ctx.User.Id, ctx.Channel.Id)
.ConfigureAwait(false);
input = input?.ToUpperInvariant();
if (input != "YES" && input != "Y")
if (input != "YES" &&
input != "Y")
{
return false;
}
@@ -94,7 +114,9 @@ public abstract class NadekoModule : ModuleBase
{
dsc.MessageReceived += MessageReceived;
if (await Task.WhenAny(userInputTask.Task, Task.Delay(10000)).ConfigureAwait(false) != userInputTask.Task)
if (await Task.WhenAny(userInputTask.Task, Task.Delay(10000))
.ConfigureAwait(false) !=
userInputTask.Task)
{
return null;
}
@@ -109,21 +131,23 @@ public abstract class NadekoModule : ModuleBase
Task MessageReceived(SocketMessage arg)
{
var _ = Task.Run(() =>
{
if (arg is not SocketUserMessage userMsg ||
userMsg.Channel is not ITextChannel chan ||
userMsg.Author.Id != userId ||
userMsg.Channel.Id != channelId)
{
if (arg is not SocketUserMessage userMsg ||
userMsg.Channel is not ITextChannel ||
userMsg.Author.Id != userId ||
userMsg.Channel.Id != channelId)
{
return Task.CompletedTask;
}
if (userInputTask.TrySetResult(arg.Content))
{
userMsg.DeleteAfter(1);
}
return Task.CompletedTask;
}
if (userInputTask.TrySetResult(arg.Content))
{
userMsg.DeleteAfter(1);
}
return Task.CompletedTask;
});
);
return Task.CompletedTask;
}
}
@@ -132,20 +156,12 @@ public abstract class NadekoModule : ModuleBase
public abstract class NadekoModule<TService> : NadekoModule
{
public TService _service { get; set; }
protected NadekoModule() : base()
{
}
}
public abstract class NadekoSubmodule : NadekoModule
{
protected NadekoSubmodule() : base() { }
}
public abstract class NadekoSubmodule<TService> : NadekoModule<TService>
{
protected NadekoSubmodule() : base()
{
}
}

View File

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

View File

@@ -6,7 +6,8 @@ public class NadekoRandom : Random
{
private readonly RandomNumberGenerator _rng;
public NadekoRandom() : base()
public NadekoRandom()
: base()
=> _rng = RandomNumberGenerator.Create();
public override int Next()

View File

@@ -1,12 +1,15 @@
namespace NadekoBot.Common;
using System.Diagnostics.CodeAnalysis;
namespace NadekoBot.Common;
[AttributeUsage(AttributeTargets.Method | AttributeTargets.Class)]
[SuppressMessage("Style", "IDE0022:Use expression body for methods")]
public sealed class NoPublicBotAttribute : PreconditionAttribute
{
public override Task<PreconditionResult> CheckPermissionsAsync(ICommandContext context, CommandInfo command, IServiceProvider services)
{
#if GLOBAL_NADEKO
return Task.FromResult(PreconditionResult.FromError("Not available on the public bot. To learn how to selfhost a private bot, click [here](https://nadekobot.readthedocs.io/en/latest/)."));
return Task.FromResult(PreconditionResult.FromError("Not available on the public bot. To learn how to selfhost a private bot, click [here](https://nadekobot.readthedocs.io/en/latest/)."));
#else
return Task.FromResult(PreconditionResult.FromSuccess());
#endif

View File

@@ -4,15 +4,18 @@ namespace NadekoBot.Common;
public static class OptionsParser
{
public static T ParseFrom<T>(string[] args) where T : INadekoCommandOptions, new()
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 (T, bool) ParseFrom<T>(T options, string[] args)
where T : INadekoCommandOptions
{
using var p = new Parser(x =>
{
x.HelpWriter = null;
});
{
x.HelpWriter = null;
}
);
var res = p.ParseArguments<T>(args);
options = res.MapResult(x => x, x => options);
options.NormalizeOptions();

View File

@@ -4,37 +4,54 @@ namespace NadekoBot.Common;
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

@@ -2,21 +2,24 @@
public static class PlatformHelper
{
private const int ProcessorCountRefreshIntervalMs = 30000;
private const int PROCESSOR_COUNT_REFRESH_INTERVAL_MS = 30000;
private static volatile int _processorCount;
private static volatile int _lastProcessorCountRefreshTicks;
private static volatile int processorCount;
private static volatile int lastProcessorCountRefreshTicks;
public static int ProcessorCount {
get {
public static int ProcessorCount
{
get
{
var now = Environment.TickCount;
if (_processorCount == 0 || now - _lastProcessorCountRefreshTicks >= ProcessorCountRefreshIntervalMs)
if (processorCount == 0 ||
now - lastProcessorCountRefreshTicks >= PROCESSOR_COUNT_REFRESH_INTERVAL_MS)
{
_processorCount = Environment.ProcessorCount;
_lastProcessorCountRefreshTicks = now;
processorCount = Environment.ProcessorCount;
lastProcessorCountRefreshTicks = now;
}
return _processorCount;
return processorCount;
}
}
}

View File

@@ -12,19 +12,21 @@ public class SearchPokemon
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 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}";
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; }

View File

@@ -3,22 +3,20 @@
public class EventPubSub : IPubSub
{
private readonly Dictionary<string, Dictionary<Delegate, List<Func<object, ValueTask>>>> _actions = new();
private readonly object locker = new();
private readonly object _locker = new();
public Task Sub<TData>(in TypedKey<TData> key, Func<TData, ValueTask> action)
{
Func<object, ValueTask> localAction = obj => action((TData) obj);
lock(locker)
Func<object, ValueTask> localAction = obj => action((TData)obj);
lock (_locker)
{
Dictionary<Delegate, List<Func<object, ValueTask>>> keyActions;
if (!_actions.TryGetValue(key.Key, out keyActions))
if (!_actions.TryGetValue(key.Key, out var keyActions))
{
keyActions = new();
_actions[key.Key] = keyActions;
}
List<Func<object, ValueTask>> sameActions;
if (!keyActions.TryGetValue(action, out sameActions))
if (!keyActions.TryGetValue(action, out var sameActions))
{
sameActions = new();
keyActions[action] = sameActions;
@@ -29,19 +27,17 @@ public class EventPubSub : IPubSub
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))
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.WhenAll(actions.SelectMany(kvp => kvp.Value).Select(action => action(data).AsTask()));
}
return Task.CompletedTask;
@@ -50,12 +46,11 @@ public class EventPubSub : IPubSub
public Task Unsub<TData>(in TypedKey<TData> key, Func<TData, ValueTask> action)
{
lock (locker)
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
@@ -63,13 +58,13 @@ public class EventPubSub : IPubSub
{
// remove last subscription
sameActions.RemoveAt(sameActions.Count - 1);
// if the last subscription was the only subscription
// we can safely remove this action's dictionary entry
if (sameActions.Count == 0)
{
actions.Remove(action);
// if our dictionary has no more elements after
// removing the entry
// it's safe to remove it from the key's subscriptions

View File

@@ -5,23 +5,19 @@ namespace NadekoBot.Common;
public class JsonSeria : ISeria
{
private readonly JsonSerializerOptions serializerOptions = new()
private readonly JsonSerializerOptions _serializerOptions = new()
{
Converters =
{
new Rgba32Converter(),
new CultureInfoConverter(),
}
Converters = { new Rgba32Converter(), new CultureInfoConverter(), }
};
public byte[] Serialize<T>(T data)
=> JsonSerializer.SerializeToUtf8Bytes(data, serializerOptions);
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

@@ -18,13 +18,15 @@ public sealed class RedisPubSub : IPubSub
public Task Pub<TData>(in TypedKey<TData> key, TData data)
{
var serialized = _serializer.Serialize(data);
return _multi.GetSubscriber().PublishAsync($"{_creds.RedisKey()}:{key.Key}", serialized, CommandFlags.FireAndForget);
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) =>
async void OnSubscribeHandler(RedisChannel _, RedisValue data)
{
try
{
@@ -33,8 +35,10 @@ public sealed class RedisPubSub : IPubSub
}
catch (Exception ex)
{
Log.Error($"Error handling the event {eventName}: {ex.Message}");
Log.Error("Error handling the event {EventName}: {ErrorMessage}", eventName, ex.Message);
}
});
}
return _multi.GetSubscriber().SubscribeAsync($"{_creds.RedisKey()}:{eventName}", OnSubscribeHandler);
}
}

View File

@@ -9,18 +9,22 @@ public readonly struct TypedKey<TData>
public static implicit operator TypedKey<TData>(in string input)
=> new(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 int GetHashCode()
=> Key?.GetHashCode() ?? 0;
public override string ToString() => Key;
public override string ToString()
=> Key;
}

View File

@@ -10,28 +10,31 @@ public class YamlSeria : IConfigSeria
private readonly ISerializer _serializer;
private readonly IDeserializer _deserializer;
private static readonly Regex CodePointRegex
= new(@"(\\U(?<code>[a-zA-Z0-9]{8})|\\u(?<code>[a-zA-Z0-9]{4})|\\x(?<code>[a-zA-Z0-9]{2}))",
RegexOptions.Compiled);
private static readonly Regex _codePointRegex =
new(@"(\\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;
});
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)
public T Deserialize<T>(string data)
=> _deserializer.Deserialize<T>(data);
}

View File

@@ -5,21 +5,29 @@ namespace NadekoBot.Common;
public class ReplacementBuilder
{
private static readonly Regex rngRegex = new("%rng(?:(?<from>(?:-)?\\d+)-(?<to>(?:-)?\\d+))?%", RegexOptions.Compiled);
private static readonly Regex _rngRegex = new("%rng(?:(?<from>(?:-)?\\d+)-(?<to>(?:-)?\\d+))?%",
RegexOptions.Compiled
);
private readonly ConcurrentDictionary<string, Func<string>> _reps = new();
private readonly ConcurrentDictionary<Regex, Func<Match, string>> _regex = new();
public ReplacementBuilder()
=> WithRngRegex();
public ReplacementBuilder WithDefault(IUser usr, IMessageChannel ch, SocketGuild g, DiscordSocketClient client)
=> this.WithUser(usr)
.WithChannel(ch)
.WithServer(client, g)
.WithClient(client);
public ReplacementBuilder WithDefault(
IUser usr,
IMessageChannel ch,
SocketGuild g,
DiscordSocketClient client)
=> 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 WithDefault(ICommandContext ctx)
=> WithDefault(ctx.User,
ctx.Channel,
ctx.Guild as SocketGuild,
(DiscordSocketClient)ctx.Client
);
public ReplacementBuilder WithMention(DiscordSocketClient client)
{
@@ -30,12 +38,14 @@ public class ReplacementBuilder
public ReplacementBuilder WithClient(DiscordSocketClient client)
{
WithMention(client);
_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.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());
@@ -51,19 +61,20 @@ public class ReplacementBuilder
_reps.TryAdd("%server.members%", () => g is { } sg ? sg.MemberCount.ToString() : "?");
_reps.TryAdd("%server.boosters%", () => g.PremiumSubscriptionCount.ToString());
_reps.TryAdd("%server.boost_level%", () => ((int)g.PremiumTier).ToString());
_reps.TryAdd("%server.time%", () =>
{
var to = TimeZoneInfo.Local;
if (g != null)
_reps.TryAdd("%server.time%",
() =>
{
if (GuildTimezoneService.AllServices.TryGetValue(client.CurrentUser.Id, out var tz))
to = tz.GetTimeZoneOrDefault(g.Id) ?? TimeZoneInfo.Local;
}
var 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 TimeZoneInfo.ConvertTime(DateTime.UtcNow, TimeZoneInfo.Utc, to).ToString("HH:mm ") +
to.StandardName.GetInitials();
}
);
return this;
}
@@ -80,7 +91,7 @@ public class ReplacementBuilder
public ReplacementBuilder WithUser(IUser user)
{
WithManyUsers(new[] {user});
WithManyUsers(new[] { user });
return this;
}
@@ -92,10 +103,18 @@ public class ReplacementBuilder
_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") ?? "-")));
_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;
}
@@ -110,21 +129,24 @@ public class ReplacementBuilder
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;
_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 == 0 &&
to == 0)
return rng.Next(0, 11).ToString();
if (from >= to)
return string.Empty;
if (from >= to)
return string.Empty;
return rng.Next(from, to + 1).ToString();
});
return rng.Next(from, to + 1).ToString();
}
);
return this;
}

View File

@@ -18,10 +18,10 @@ public class Replacer
if (string.IsNullOrWhiteSpace(input))
return input;
foreach (var (Key, Text) in _replacements)
foreach (var (key, text) in _replacements)
{
if (input.Contains(Key))
input = input.Replace(Key, Text(), StringComparison.InvariantCulture);
if (input.Contains(key))
input = input.Replace(key, text(), StringComparison.InvariantCulture);
}
foreach (var item in _regex)
@@ -58,8 +58,7 @@ public class Replacer
{
newEmbedData.Author = new()
{
Name = Replace(embedData.Author.Name),
IconUrl = Replace(embedData.Author.IconUrl)
Name = Replace(embedData.Author.Name), IconUrl = Replace(embedData.Author.IconUrl)
};
}
@@ -70,9 +69,7 @@ public class Replacer
{
var newF = new SmartTextEmbedField
{
Name = Replace(f.Name),
Value = Replace(f.Value),
Inline = f.Inline
Name = Replace(f.Name), Value = Replace(f.Value), Inline = f.Inline
};
fields.Add(newF);
}
@@ -84,8 +81,7 @@ public class Replacer
{
newEmbedData.Footer = new()
{
Text = Replace(embedData.Footer.Text),
IconUrl = Replace(embedData.Footer.IconUrl)
Text = Replace(embedData.Footer.Text), IconUrl = Replace(embedData.Footer.IconUrl)
};
}

View File

@@ -24,9 +24,7 @@ public struct ShmartNumber : IEquatable<ShmartNumber>
=> Value.ToString();
public override bool Equals(object obj)
=> obj is ShmartNumber sn
? Equals(sn)
: false;
=> obj is ShmartNumber sn && Equals(sn);
public bool Equals(ShmartNumber other)
=> other.Value == Value;

View File

@@ -15,14 +15,13 @@ public sealed record SmartEmbedText : SmartText
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 is { Length: > 0 };
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 is { Length: > 0 };
public static SmartEmbedText FromEmbed(IEmbed eb, string plainText = null)
{
@@ -34,42 +33,23 @@ public sealed record SmartEmbedText : SmartText
Url = eb.Url,
Thumbnail = eb.Thumbnail?.Url,
Image = eb.Image?.Url,
Author = eb.Author is { } ea
? new()
{
Name = ea.Name,
Url = ea.Url,
IconUrl = ea.IconUrl
}
: null,
Footer = eb.Footer is { } ef
? new()
{
Text = ef.Text,
IconUrl = ef.IconUrl
}
: null
Author = eb.Author is { } ea ? new() { Name = ea.Name, Url = ea.Url, IconUrl = ea.IconUrl } : null,
Footer = eb.Footer is { } ef ? new() { Text = ef.Text, IconUrl = ef.IconUrl } : null
};
if (eb.Fields.Length > 0)
set.Fields = eb
.Fields
.Select(field => new SmartTextEmbedField()
{
Inline = field.Inline,
Name = field.Name,
Value = field.Value,
})
set.Fields = eb.Fields.Select(field
=> new SmartTextEmbedField() { Inline = field.Inline, Name = field.Name, Value = field.Value, }
)
.ToArray();
set.Color = eb.Color?.RawValue ?? 0;
return set;
}
public EmbedBuilder GetEmbed()
{
var embed = new EmbedBuilder()
.WithColor(Color);
var embed = new EmbedBuilder().WithColor(Color);
if (!string.IsNullOrWhiteSpace(Title))
embed.WithTitle(Title);
@@ -77,26 +57,31 @@ public sealed record SmartEmbedText : SmartText
if (!string.IsNullOrWhiteSpace(Description))
embed.WithDescription(Description);
if (Url != null && Uri.IsWellFormedUriString(Url, UriKind.Absolute))
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.WithIconUrl(Footer.IconUrl);
});
{
efb.WithText(Footer.Text);
if (Uri.IsWellFormedUriString(Footer.IconUrl, UriKind.Absolute))
efb.WithIconUrl(Footer.IconUrl);
}
);
}
if (Thumbnail != null && Uri.IsWellFormedUriString(Thumbnail, UriKind.Absolute))
if (Thumbnail != null &&
Uri.IsWellFormedUriString(Thumbnail, UriKind.Absolute))
embed.WithThumbnailUrl(Thumbnail);
if (Image != null && Uri.IsWellFormedUriString(Image, UriKind.Absolute))
if (Image != null &&
Uri.IsWellFormedUriString(Image, UriKind.Absolute))
embed.WithImageUrl(Image);
if (Author != null && !string.IsNullOrWhiteSpace(Author.Name))
if (Author != null &&
!string.IsNullOrWhiteSpace(Author.Name))
{
if (!Uri.IsWellFormedUriString(Author.IconUrl, UriKind.Absolute))
Author.IconUrl = null;
@@ -110,7 +95,8 @@ public sealed record SmartEmbedText : SmartText
{
foreach (var f in Fields)
{
if (!string.IsNullOrWhiteSpace(f.Name) && !string.IsNullOrWhiteSpace(f.Value))
if (!string.IsNullOrWhiteSpace(f.Name) &&
!string.IsNullOrWhiteSpace(f.Value))
embed.AddField(f.Name, f.Value, f.Inline);
}
}

View File

@@ -4,8 +4,11 @@ namespace NadekoBot;
public abstract record SmartText
{
public bool IsEmbed => this is SmartEmbedText;
public bool IsPlainText => this is SmartPlainText;
public bool IsEmbed
=> this is SmartEmbedText;
public bool IsPlainText
=> this is SmartPlainText;
public static SmartText operator +(SmartText text, string input)
=> text switch
@@ -14,7 +17,7 @@ public abstract record SmartText
SmartPlainText spt => new SmartPlainText(spt.Text + input),
_ => throw new ArgumentOutOfRangeException(nameof(text))
};
public static SmartText operator +(string input, SmartText text)
=> text switch
{
@@ -22,10 +25,11 @@ public abstract record SmartText
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("{"))
if (string.IsNullOrWhiteSpace(input) ||
!input.TrimStart().StartsWith("{"))
{
return new SmartPlainText(input);
}
@@ -34,6 +38,9 @@ public abstract record SmartText
{
var smartEmbedText = JsonConvert.DeserializeObject<SmartEmbedText>(input);
if (smartEmbedText is null)
throw new();
smartEmbedText.NormalizeFields();
if (!smartEmbedText.IsValid)

View File

@@ -5,8 +5,7 @@ namespace NadekoBot;
public class SmartTextEmbedAuthor
{
public string Name { get; set; }
public string IconUrl { get; set; }
[JsonProperty("icon_url")]
private string Icon_Url { set => IconUrl = value; }
public string IconUrl { get; set; }
public string Url { get; set; }
}

View File

@@ -2,10 +2,11 @@
namespace NadekoBot;
// todo test smarttextembedfooter and smarttextembedauthor
public class SmartTextEmbedFooter
{
public string Text { get; set; }
public string IconUrl { get; set; }
[JsonProperty("icon_url")]
private string Icon_Url { set => IconUrl = value; }
public string IconUrl { get; set; }
}

View File

@@ -17,36 +17,37 @@ public sealed class ReactionEventWrapper : IDisposable
_client.ReactionsCleared += Discord_ReactionsCleared;
}
private Task Discord_ReactionsCleared(
Cacheable<IUserMessage, ulong> msg,
Cacheable<IMessageChannel, ulong> channel)
private Task Discord_ReactionsCleared(Cacheable<IUserMessage, ulong> msg, Cacheable<IMessageChannel, ulong> channel)
{
Task.Run(() =>
{
try
{
if (msg.Id == Message.Id)
OnReactionsCleared?.Invoke();
try
{
if (msg.Id == Message.Id)
OnReactionsCleared?.Invoke();
}
catch { }
}
catch { }
});
);
return Task.CompletedTask;
}
private Task Discord_ReactionRemoved(
Cacheable<IUserMessage, ulong> msg,
Cacheable<IMessageChannel, ulong> cacheable, SocketReaction reaction)
Cacheable<IMessageChannel, ulong> cacheable,
SocketReaction reaction)
{
Task.Run(() =>
{
try
{
if (msg.Id == Message.Id)
OnReactionRemoved?.Invoke(reaction);
try
{
if (msg.Id == Message.Id)
OnReactionRemoved?.Invoke(reaction);
}
catch { }
}
catch { }
});
);
return Task.CompletedTask;
}
@@ -57,16 +58,17 @@ public sealed class ReactionEventWrapper : IDisposable
SocketReaction reaction)
{
Task.Run(() =>
{
try
{
if (msg.Id == Message.Id)
OnReactionAdded?.Invoke(reaction);
try
{
if (msg.Id == Message.Id)
OnReactionAdded?.Invoke(reaction);
}
catch
{
}
}
catch
{
}
});
);
return Task.CompletedTask;
}

View File

@@ -36,10 +36,7 @@ public sealed class CommandOrCrTypeReader : NadekoTypeReader<CommandOrCrInfo>
private readonly CustomReactionsService _crs;
private readonly CommandHandler _commandHandler;
public CommandOrCrTypeReader(
CommandService cmds,
CustomReactionsService crs,
CommandHandler commandHandler)
public CommandOrCrTypeReader(CommandService cmds, CustomReactionsService crs, CommandHandler commandHandler)
{
_cmds = cmds;
_crs = crs;
@@ -58,8 +55,12 @@ public sealed class CommandOrCrTypeReader : NadekoTypeReader<CommandOrCrInfo>
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.FromSuccess(new CommandOrCrInfo(((CommandInfo)cmd.Values.First().Value).Name,
CommandOrCrInfo.Type.Normal
)
);
}
return TypeReaderResult.FromError(CommandError.ParseFailed, "No such command or cr found.");
}
}
@@ -74,7 +75,9 @@ public class CommandOrCrInfo
public string Name { get; set; }
public Type CmdType { get; set; }
public bool IsCustom => CmdType == Type.Custom;
public bool IsCustom
=> CmdType == Type.Custom;
public CommandOrCrInfo(string input, Type type)
{

View File

@@ -12,8 +12,11 @@ public sealed class GuildDateTimeTypeReader : NadekoTypeReader<GuildDateTime>
public override Task<TypeReaderResult> ReadAsync(ICommandContext context, string input)
{
var gdt = Parse(context.Guild.Id, input);
if(gdt is null)
return Task.FromResult(TypeReaderResult.FromError(CommandError.ParseFailed, "Input string is in an incorrect format."));
if (gdt is null)
return Task.FromResult(TypeReaderResult.FromError(CommandError.ParseFailed,
"Input string is in an incorrect format."
)
);
return Task.FromResult(TypeReaderResult.FromSuccess(gdt));
}

View File

@@ -10,7 +10,9 @@ public sealed class ModuleTypeReader : NadekoTypeReader<ModuleInfo>
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;
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."));
@@ -28,14 +30,14 @@ public sealed class ModuleOrCrTypeReader : NadekoTypeReader<ModuleOrCrInfo>
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")
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,
}));
return Task.FromResult(TypeReaderResult.FromSuccess(new ModuleOrCrInfo { Name = input, }));
}
}

View File

@@ -1,5 +1,6 @@
namespace NadekoBot.Common.TypeReaders;
[MeansImplicitUse(ImplicitUseTargetFlags.Default | ImplicitUseTargetFlags.WithInheritors )]
public abstract class NadekoTypeReader<T> : TypeReader
{
public abstract Task<TypeReaderResult> ReadAsync(ICommandContext ctx, string input);

View File

@@ -63,12 +63,10 @@ public sealed class ShmartNumberTypeReader : NadekoTypeReader<ShmartNumber>
case "MAX":
args.Result = Max(ctx);
break;
default:
break;
}
}
private static readonly Regex percentRegex = new(@"^((?<num>100|\d{1,2})%)$", RegexOptions.Compiled);
private static readonly Regex _percentRegex = new(@"^((?<num>100|\d{1,2})%)$", RegexOptions.Compiled);
private long Cur(ICommandContext ctx)
{
@@ -80,15 +78,13 @@ public sealed class ShmartNumberTypeReader : NadekoTypeReader<ShmartNumber>
{
var settings = _gambling.Data;
var max = settings.MaxBet;
return max == 0
? Cur(ctx)
: max;
return max == 0 ? Cur(ctx) : max;
}
private bool TryHandlePercentage(ICommandContext ctx, string input, out long num)
{
num = 0;
var m = percentRegex.Match(input);
var m = _percentRegex.Match(input);
if (m.Captures.Count != 0)
{
if (!long.TryParse(m.Groups["num"].ToString(), out var percent))
@@ -97,6 +93,7 @@ public sealed class ShmartNumberTypeReader : NadekoTypeReader<ShmartNumber>
num = (long)(Cur(ctx) * (percent / 100.0f));
return true;
}
return false;
}
}

View File

@@ -12,9 +12,7 @@ public class CommentGatheringTypeInspector : TypeInspectorSkeleton
=> this.innerTypeDescriptor = innerTypeDescriptor ?? throw new ArgumentNullException("innerTypeDescriptor");
public override IEnumerable<IPropertyDescriptor> GetProperties(Type type, object container)
=> innerTypeDescriptor
.GetProperties(type, container)
.Select(d => new CommentsPropertyDescriptor(d));
=> innerTypeDescriptor.GetProperties(type, container).Select(d => new CommentsPropertyDescriptor(d));
private sealed class CommentsPropertyDescriptor : IPropertyDescriptor
{
@@ -31,14 +29,16 @@ public class CommentGatheringTypeInspector : TypeInspectorSkeleton
public Type Type
=> baseDescriptor.Type;
public Type TypeOverride {
public Type TypeOverride
{
get => baseDescriptor.TypeOverride;
set => baseDescriptor.TypeOverride = value;
}
public int Order { get; set; }
public ScalarStyle ScalarStyle {
public ScalarStyle ScalarStyle
{
get => baseDescriptor.ScalarStyle;
set => baseDescriptor.ScalarStyle = value;
}
@@ -49,7 +49,8 @@ public class CommentGatheringTypeInspector : TypeInspectorSkeleton
public void Write(object target, object value)
=> baseDescriptor.Write(target, value);
public T GetCustomAttribute<T>() where T : Attribute
public T GetCustomAttribute<T>()
where T : Attribute
=> baseDescriptor.GetCustomAttribute<T>();
public IObjectDescriptor Read(object target)

View File

@@ -14,8 +14,8 @@ public class CommentsObjectGraphVisitor : ChainedObjectGraphVisitor
public override bool EnterMapping(IPropertyDescriptor key, IObjectDescriptor value, IEmitter context)
{
var commentsDescriptor = value as CommentsObjectDescriptor;
if (commentsDescriptor != null && !string.IsNullOrWhiteSpace(commentsDescriptor.Comment))
if (value is CommentsObjectDescriptor commentsDescriptor &&
!string.IsNullOrWhiteSpace(commentsDescriptor.Comment))
{
context.Emit(new Comment(commentsDescriptor.Comment.Replace("\n", "\n# "), false));
}

View File

@@ -7,11 +7,12 @@ namespace NadekoBot.Common.Yml;
public class MultilineScalarFlowStyleEmitter : ChainedEventEmitter
{
public MultilineScalarFlowStyleEmitter(IEventEmitter nextEmitter)
: base(nextEmitter) { }
: base(nextEmitter)
{
}
public override void Emit(ScalarEventInfo eventInfo, IEmitter emitter)
{
if (typeof(string).IsAssignableFrom(eventInfo.Source.Type))
{
var value = eventInfo.Source.Value as string;
@@ -19,10 +20,7 @@ public class MultilineScalarFlowStyleEmitter : ChainedEventEmitter
{
var isMultiLine = value.IndexOfAny(new char[] { '\r', '\n', '\x85', '\x2028', '\x2029' }) >= 0;
if (isMultiLine)
eventInfo = new(eventInfo.Source)
{
Style = ScalarStyle.Literal,
};
eventInfo = new(eventInfo.Source) { Style = ScalarStyle.Literal, };
}
}

View File

@@ -21,11 +21,11 @@ public class Rgba32Converter : IYamlTypeConverter
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));
var val = (uint)((color.B << 0) | (color.G << 8) | (color.R << 16));
emitter.Emit(new Scalar(val.ToString("X6").ToLower()));
}
}
public class CultureInfoConverter : IYamlTypeConverter
{
public bool Accepts(Type type)

View File

@@ -4,22 +4,23 @@ 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 ISerializer Serializer
=> new SerializerBuilder().WithTypeInspector(inner => new CommentGatheringTypeInspector(inner))
.WithEmissionPhaseObjectGraphVisitor(args => new CommentsObjectGraphVisitor(args.InnerVisitor))
.WithEventEmitter(args => new MultilineScalarFlowStyleEmitter(args))
.WithNamingConvention(YamlDotNet.Serialization.NamingConventions.CamelCaseNamingConvention.Instance)
.WithIndentedSequences()
.WithTypeConverter(new Rgba32Converter())
.WithTypeConverter(new CultureInfoConverter())
.WithTypeConverter(new UriConverter())
.Build();
public static IDeserializer Deserializer => new DeserializerBuilder()
.WithNamingConvention(YamlDotNet.Serialization.NamingConventions.CamelCaseNamingConvention.Instance)
.WithTypeConverter(new Rgba32Converter())
.WithTypeConverter(new CultureInfoConverter())
.WithTypeConverter(new UriConverter())
.IgnoreUnmatchedProperties()
.Build();
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

@@ -15,27 +15,28 @@ public class YamlHelper
// Scan the character value.
foreach(var c in point)
foreach (var c in point)
{
if (!IsHex(c))
{
return point;
}
character = (character << 4) + AsHex(c);
}
// Check the value and write the character.
if (character is >= 0xD800 and <= 0xDFFF or > 0x10FFFF)
if (character is (>= 0xD800 and <= 0xDFFF) or > 0x10FFFF)
{
return point;
}
return char.ConvertFromUtf32(character);
}
public static bool IsHex(char c)
=> c is >= '0' and <= '9' or >= 'A' and <= 'F' or >= 'a' and <= 'f';
=> c is (>= '0' and <= '9') or (>= 'A' and <= 'F') or (>= 'a' and <= 'f');
public static int AsHex(char c)
{
@@ -43,10 +44,12 @@ public class YamlHelper
{
return c - '0';
}
if (c <= 'F')
{
return c - 'A' + 10;
}
return c - 'a' + 10;
}
}