Applied codestyle to all .cs files

This commit is contained in:
Kwoth
2021-12-29 06:07:16 +01:00
parent 723447c7d4
commit 82000c97a4
543 changed files with 13221 additions and 14059 deletions

View File

@@ -1,8 +1,10 @@
#nullable disable
using System.Collections.Immutable;
using Discord.Interactions;
using NadekoBot.Common.Configs;
using NadekoBot.Db;
using Discord.Interactions;
using System.Collections.Immutable;
using ExecuteResult = Discord.Commands.ExecuteResult;
using PreconditionResult = Discord.Commands.PreconditionResult;
namespace NadekoBot.Services;
@@ -10,14 +12,7 @@ public class CommandHandler : INService
{
private const int GlobalCommandsCooldown = 750;
private readonly DiscordSocketClient _client;
private readonly CommandService _commandService;
private readonly BotConfigService _bss;
private readonly Bot _bot;
private readonly IBehaviourExecutor _behaviourExecutor;
private readonly IServiceProvider _services;
private readonly ConcurrentDictionary<ulong, string> _prefixes;
private const float _oneThousandth = 1.0f / 1000;
public event Func<IUserMessage, CommandInfo, Task> CommandExecuted = delegate { return Task.CompletedTask; };
public event Func<CommandInfo, ITextChannel, string, Task> CommandErrored = delegate { return Task.CompletedTask; };
@@ -27,7 +22,18 @@ public class CommandHandler : INService
public ConcurrentDictionary<ulong, uint> UserMessagesSent { get; } = new();
public ConcurrentHashSet<ulong> UsersOnShortCooldown { get; } = new();
private readonly DiscordSocketClient _client;
private readonly CommandService _commandService;
private readonly BotConfigService _bss;
private readonly Bot _bot;
private readonly IBehaviourExecutor _behaviourExecutor;
private readonly IServiceProvider _services;
private readonly ConcurrentDictionary<ulong, string> _prefixes;
private readonly Timer _clearUsersOnShortCooldown;
private readonly DbService _db;
private readonly InteractionService _interactions;
public CommandHandler(
DiscordSocketClient client,
@@ -49,17 +55,20 @@ public class CommandHandler : INService
_interactions = interactions;
_clearUsersOnShortCooldown = new(_ =>
{
UsersOnShortCooldown.Clear();
}, null, GlobalCommandsCooldown, GlobalCommandsCooldown);
_prefixes = bot.AllGuildConfigs
.Where(x => x.Prefix != null)
.ToDictionary(x => x.GuildId, x => x.Prefix)
.ToConcurrent();
{
UsersOnShortCooldown.Clear();
},
null,
GlobalCommandsCooldown,
GlobalCommandsCooldown);
_prefixes = bot.AllGuildConfigs.Where(x => x.Prefix != null)
.ToDictionary(x => x.GuildId, x => x.Prefix)
.ToConcurrent();
}
public string GetPrefix(IGuild guild) => GetPrefix(guild?.Id);
public string GetPrefix(IGuild guild)
=> GetPrefix(guild?.Id);
public string GetPrefix(ulong? id = null)
{
@@ -81,6 +90,7 @@ public class CommandHandler : INService
return prefix;
}
public string SetPrefix(IGuild guild, string prefix)
{
if (string.IsNullOrWhiteSpace(prefix))
@@ -135,64 +145,55 @@ public class CommandHandler : INService
await _interactions.ExecuteCommandAsync(ctx, _services);
}
private const float _oneThousandth = 1.0f / 1000;
private readonly DbService _db;
private readonly InteractionService _interactions;
private Task LogSuccessfulExecution(IUserMessage usrMsg, ITextChannel channel, params int[] execPoints)
{
if (_bss.Data.ConsoleOutputType == ConsoleOutputType.Normal)
{
Log.Information(@"Command Executed after {ExecTime}s
User: {User}
Server: {Server}
Channel: {Channel}
Message: {MessageContent}",
Message: {Message}",
string.Join("/", execPoints.Select(x => (x * _oneThousandth).ToString("F3"))),
usrMsg.Author + " [" + usrMsg.Author.Id + "]",
channel is null ? "PRIVATE" : channel.Guild.Name + " [" + channel.Guild.Id + "]",
channel is null ? "PRIVATE" : channel.Name + " [" + channel.Id + "]",
usrMsg.Content
);
}
usrMsg.Content);
else
{
Log.Information("Succ | g:{0} | c: {1} | u: {2} | msg: {3}",
Log.Information("Succ | g:{GuildId} | c: {ChannelId} | u: {UserId} | msg: {Message}",
channel?.Guild.Id.ToString() ?? "-",
channel?.Id.ToString() ?? "-",
usrMsg.Author.Id,
usrMsg.Content.TrimTo(10));
}
return Task.CompletedTask;
}
private void LogErroredExecution(string errorMessage, IUserMessage usrMsg, ITextChannel channel, params int[] execPoints)
private void LogErroredExecution(
string errorMessage,
IUserMessage usrMsg,
ITextChannel channel,
params int[] execPoints)
{
if (_bss.Data.ConsoleOutputType == ConsoleOutputType.Normal)
{
Log.Warning(@"Command Errored after {ExecTime}s
User: {User}
Server: {Server}
Server: {Guild}
Channel: {Channel}
Message: {MessageContent}
Message: {Message}
Error: {ErrorMessage}",
string.Join("/", execPoints.Select(x => (x * _oneThousandth).ToString("F3"))),
usrMsg.Author + " [" + usrMsg.Author.Id + "]",
channel is null ? "PRIVATE" : channel.Guild.Name + " [" + channel.Guild.Id + "]",
channel is null ? "PRIVATE" : channel.Name + " [" + channel.Id + "]",
channel is null ? "DM" : channel.Guild.Name + " [" + channel.Guild.Id + "]",
channel is null ? "DM" : channel.Name + " [" + channel.Id + "]",
usrMsg.Content,
errorMessage
);
}
errorMessage);
else
{
Log.Warning("Err | g:{0} | c: {1} | u: {2} | msg: {3}\n\tErr: {4}",
Log.Warning(@"Err | g:{GuildId} | c: {ChannelId} | u: {UserId} | msg: {Message}
Err: {ErrorMessage}",
channel?.Guild.Id.ToString() ?? "-",
channel?.Id.ToString() ?? "-",
usrMsg.Author.Id,
usrMsg.Content.TrimTo(10),
errorMessage);
}
}
private Task MessageReceivedHandler(SocketMessage msg)
@@ -205,29 +206,26 @@ public class CommandHandler : INService
return Task.CompletedTask;
Task.Run(async () =>
{
try
{
try
{
#if !GLOBAL_NADEKO
// track how many messagges each user is sending
UserMessagesSent.AddOrUpdate(usrMsg.Author.Id, 1, (key, old) => ++old);
// track how many messagges each user is sending
UserMessagesSent.AddOrUpdate(usrMsg.Author.Id, 1, (key, old) => ++old);
#endif
var channel = msg.Channel;
var guild = (msg.Channel as SocketTextChannel)?.Guild;
var channel = msg.Channel;
var guild = (msg.Channel as SocketTextChannel)?.Guild;
await TryRunCommand(guild, channel, usrMsg);
}
catch (Exception ex)
{
Log.Warning(ex, "Error in CommandHandler");
if (ex.InnerException != null)
{
Log.Warning(ex.InnerException, "Inner Exception of the error in CommandHandler");
}
}
await TryRunCommand(guild, channel, usrMsg);
}
);
catch (Exception ex)
{
Log.Warning(ex, "Error in CommandHandler");
if (ex.InnerException != null)
Log.Warning(ex.InnerException, "Inner Exception of the error in CommandHandler");
}
});
return Task.CompletedTask;
}
@@ -243,13 +241,17 @@ public class CommandHandler : INService
var blockTime = Environment.TickCount - startTime;
var messageContent = await _behaviourExecutor.RunInputTransformersAsync(guild, usrMsg);
var prefix = GetPrefix(guild?.Id);
var isPrefixCommand = messageContent.StartsWith(".prefix", StringComparison.InvariantCultureIgnoreCase);
// execute the command and measure the time it took
if (messageContent.StartsWith(prefix, StringComparison.InvariantCulture) || isPrefixCommand)
{
var (success, error, info) = await ExecuteCommandAsync(new(_client, usrMsg), messageContent, isPrefixCommand ? 1 : prefix.Length, _services, MultiMatchHandling.Best);
var (success, error, info) = await ExecuteCommandAsync(new(_client, usrMsg),
messageContent,
isPrefixCommand ? 1 : prefix.Length,
_services,
MultiMatchHandling.Best);
startTime = Environment.TickCount - startTime;
if (success)
@@ -258,7 +260,8 @@ public class CommandHandler : INService
await CommandExecuted(usrMsg, info);
return;
}
else if (error != null)
if (error != null)
{
LogErroredExecution(error, usrMsg, channel as ITextChannel, blockTime, startTime);
if (guild != null)
@@ -273,34 +276,38 @@ public class CommandHandler : INService
await _behaviourExecutor.RunLateExecutorsAsync(guild, usrMsg);
}
public Task<(bool Success, string Error, CommandInfo Info)> ExecuteCommandAsync(CommandContext context, string input, int argPos, IServiceProvider serviceProvider, MultiMatchHandling multiMatchHandling = MultiMatchHandling.Exception)
public Task<(bool Success, string Error, CommandInfo Info)> ExecuteCommandAsync(
CommandContext context,
string input,
int argPos,
IServiceProvider serviceProvider,
MultiMatchHandling multiMatchHandling = MultiMatchHandling.Exception)
=> ExecuteCommand(context, input[argPos..], serviceProvider, multiMatchHandling);
public async Task<(bool Success, string Error, CommandInfo Info)> ExecuteCommand(CommandContext context, string input, IServiceProvider services, MultiMatchHandling multiMatchHandling = MultiMatchHandling.Exception)
public async Task<(bool Success, string Error, CommandInfo Info)> ExecuteCommand(
CommandContext context,
string input,
IServiceProvider services,
MultiMatchHandling multiMatchHandling = MultiMatchHandling.Exception)
{
var searchResult = _commandService.Search(context, input);
if (!searchResult.IsSuccess)
return (false, null, null);
var commands = searchResult.Commands;
var preconditionResults = new Dictionary<CommandMatch, Discord.Commands.PreconditionResult>();
var preconditionResults = new Dictionary<CommandMatch, PreconditionResult>();
foreach (var match in commands)
{
preconditionResults[match] = await match.Command.CheckPreconditionsAsync(context, services);
}
var successfulPreconditions = preconditionResults
.Where(x => x.Value.IsSuccess)
.ToArray();
var successfulPreconditions = preconditionResults.Where(x => x.Value.IsSuccess).ToArray();
if (successfulPreconditions.Length == 0)
{
//All preconditions failed, return the one from the highest priority command
var bestCandidate = preconditionResults
.OrderByDescending(x => x.Key.Command.Priority)
.FirstOrDefault(x => !x.Value.IsSuccess);
var bestCandidate = preconditionResults.OrderByDescending(x => x.Key.Command.Priority)
.FirstOrDefault(x => !x.Value.IsSuccess);
return (false, bestCandidate.Value.ErrorReason, commands[0].Command);
}
@@ -315,8 +322,11 @@ public class CommandHandler : INService
switch (multiMatchHandling)
{
case MultiMatchHandling.Best:
argList = parseResult.ArgValues.Select(x => x.Values.OrderByDescending(y => y.Score).First()).ToImmutableArray();
paramList = parseResult.ParamValues.Select(x => x.Values.OrderByDescending(y => y.Score).First()).ToImmutableArray();
argList = parseResult.ArgValues.Select(x => x.Values.OrderByDescending(y => y.Score).First())
.ToImmutableArray();
paramList = parseResult.ParamValues
.Select(x => x.Values.OrderByDescending(y => y.Score).First())
.ToImmutableArray();
parseResult = ParseResult.FromSuccess(argList, paramList);
break;
}
@@ -324,6 +334,7 @@ public class CommandHandler : INService
parseResultsDict[pair.Key] = parseResult;
}
// Calculates the 'score' of a command given a parse result
float CalculateScore(CommandMatch match, ParseResult parseResult)
{
@@ -331,8 +342,12 @@ public class CommandHandler : INService
if (match.Command.Parameters.Count > 0)
{
var argValuesSum = parseResult.ArgValues?.Sum(x => x.Values.OrderByDescending(y => y.Score).FirstOrDefault().Score) ?? 0;
var paramValuesSum = parseResult.ParamValues?.Sum(x => x.Values.OrderByDescending(y => y.Score).FirstOrDefault().Score) ?? 0;
var argValuesSum =
parseResult.ArgValues?.Sum(x => x.Values.OrderByDescending(y => y.Score).FirstOrDefault().Score)
?? 0;
var paramValuesSum =
parseResult.ParamValues?.Sum(x => x.Values.OrderByDescending(y => y.Score).FirstOrDefault().Score)
?? 0;
argValuesScore = argValuesSum / match.Command.Parameters.Count;
paramValuesScore = paramValuesSum / match.Command.Parameters.Count;
@@ -343,19 +358,14 @@ public class CommandHandler : INService
}
//Order the parse results by their score so that we choose the most likely result to execute
var parseResults = parseResultsDict
.OrderByDescending(x => CalculateScore(x.Key, x.Value))
.ToList();
var parseResults = parseResultsDict.OrderByDescending(x => CalculateScore(x.Key, x.Value)).ToList();
var successfulParses = parseResults
.Where(x => x.Value.IsSuccess)
.ToArray();
var successfulParses = parseResults.Where(x => x.Value.IsSuccess).ToArray();
if (successfulParses.Length == 0)
{
//All parses failed, return the one from the highest priority command, using score as a tie breaker
var bestMatch = parseResults
.FirstOrDefault(x => !x.Value.IsSuccess);
var bestMatch = parseResults.FirstOrDefault(x => !x.Value.IsSuccess);
return (false, bestMatch.Value.ErrorReason, commands[0].Command);
}
@@ -373,13 +383,13 @@ public class CommandHandler : INService
//If we get this far, at least one parse was successful. Execute the most likely overload.
var chosenOverload = successfulParses[0];
var execResult = (Discord.Commands.ExecuteResult)await chosenOverload.Key.ExecuteAsync(context, chosenOverload.Value, services);
var execResult = (ExecuteResult)await chosenOverload.Key.ExecuteAsync(context, chosenOverload.Value, services);
if (execResult.Exception != null && (execResult.Exception is not HttpException he || he.DiscordCode != DiscordErrorCode.InsufficientPermissions))
{
if (execResult.Exception != null
&& (execResult.Exception is not HttpException he
|| he.DiscordCode != DiscordErrorCode.InsufficientPermissions))
Log.Warning(execResult.Exception, "Command Error");
}
return (true, null, cmd);
}
}
}

View File

@@ -1,8 +1,8 @@
#nullable disable
using LinqToDB.EntityFrameworkCore;
using Microsoft.Data.Sqlite;
using Microsoft.EntityFrameworkCore;
using NadekoBot.Services.Database;
using LinqToDB.EntityFrameworkCore;
namespace NadekoBot.Services;
@@ -14,7 +14,7 @@ public class DbService
public DbService(IBotCredentials creds)
{
LinqToDBForEFTools.Initialize();
var builder = new SqliteConnectionStringBuilder(creds.Db.ConnectionString);
builder.DataSource = Path.Combine(AppContext.BaseDirectory, builder.DataSource);
@@ -37,6 +37,7 @@ public class DbService
mContext.SaveChanges();
mContext.Dispose();
}
context.Database.ExecuteSqlRaw("PRAGMA journal_mode=WAL");
context.SaveChanges();
}
@@ -53,5 +54,6 @@ public class DbService
return context;
}
public NadekoContext GetDbContext() => GetDbContextInternal();
}
public NadekoContext GetDbContext()
=> GetDbContextInternal();
}

View File

@@ -9,4 +9,4 @@ public interface IBehaviourExecutor
Task RunLateExecutorsAsync(SocketGuild guild, IUserMessage usrMsg);
public void Initialize();
}
}

View File

@@ -10,11 +10,11 @@ public interface ICoordinator
int GetGuildCount();
Task Reload();
}
public class ShardStatus
{
public Discord.ConnectionState ConnectionState { get; set; }
public ConnectionState ConnectionState { get; set; }
public DateTime LastUpdate { get; set; }
public int ShardId { get; set; }
public int GuildCount { get; set; }
}
}

View File

@@ -3,10 +3,41 @@ namespace NadekoBot.Services;
public interface ICurrencyService
{
Task AddAsync(ulong userId, string reason, long amount, bool gamble = false);
Task AddAsync(IUser user, string reason, long amount, bool sendMessage = false, bool gamble = false);
Task AddBulkAsync(IEnumerable<ulong> userIds, IEnumerable<string> reasons, IEnumerable<long> amounts, bool gamble = false);
Task<bool> RemoveAsync(ulong userId, string reason, long amount, bool gamble = false);
Task<bool> RemoveAsync(IUser userId, string reason, long amount, bool sendMessage = false, bool gamble = false);
Task RemoveBulkAsync(IEnumerable<ulong> userIds, IEnumerable<string> reasons, IEnumerable<long> amounts, bool gamble = false);
}
Task AddAsync(
ulong userId,
string reason,
long amount,
bool gamble = false);
Task AddAsync(
IUser user,
string reason,
long amount,
bool sendMessage = false,
bool gamble = false);
Task AddBulkAsync(
IEnumerable<ulong> userIds,
IEnumerable<string> reasons,
IEnumerable<long> amounts,
bool gamble = false);
Task<bool> RemoveAsync(
ulong userId,
string reason,
long amount,
bool gamble = false);
Task<bool> RemoveAsync(
IUser userId,
string reason,
long amount,
bool sendMessage = false,
bool gamble = false);
Task RemoveBulkAsync(
IEnumerable<ulong> userIds,
IEnumerable<string> reasons,
IEnumerable<long> amounts,
bool gamble = false);
}

View File

@@ -23,7 +23,13 @@ public interface IDataCache
bool TryGetEconomy(out string data);
void SetEconomy(string data);
Task<TOut> GetOrAddCachedDataAsync<TParam, TOut>(string key, Func<TParam, Task<TOut>> factory, TParam param, TimeSpan expiry) where TOut : class;
Task<TOut> GetOrAddCachedDataAsync<TParam, TOut>(
string key,
Func<TParam, Task<TOut>> factory,
TParam param,
TimeSpan expiry)
where TOut : class;
DateTime GetLastCurrencyDecay();
void SetLastCurrencyDecay();
}
}

View File

@@ -16,13 +16,13 @@ public class EmbedBuilderService : IEmbedBuilderService, INService
public EmbedBuilderService(BotConfigService botConfigService)
=> _botConfigService = botConfigService;
public IEmbedBuilder Create(ICommandContext ctx = null)
public IEmbedBuilder Create(ICommandContext ctx = null)
=> new DiscordEmbedBuilderWrapper(_botConfigService.Data);
public IEmbedBuilder Create(EmbedBuilder embed)
public IEmbedBuilder Create(EmbedBuilder embed)
=> new DiscordEmbedBuilderWrapper(_botConfigService.Data, embed);
}
public sealed class DiscordEmbedBuilderWrapper : IEmbedBuilder
{
private readonly BotConfig _botConfig;
@@ -57,7 +57,7 @@ public sealed class DiscordEmbedBuilderWrapper : IEmbedBuilder
public IEmbedBuilder WithThumbnailUrl(string url)
=> Wrap(_embed.WithThumbnailUrl(url));
public IEmbedBuilder WithColor(EmbedColor color)
=> color switch
{
@@ -75,4 +75,4 @@ public sealed class DiscordEmbedBuilderWrapper : IEmbedBuilder
_embed = eb;
return this;
}
}
}

View File

@@ -27,7 +27,7 @@ public struct ImageResult
public ImageResult(Result.ImageData image, string link)
{
this.Image = image;
this.Link = link;
Image = image;
Link = link;
}
}
}

View File

@@ -26,4 +26,4 @@ public interface IImageCache
byte[] GetCard(string key);
Task Reload();
}
}

View File

@@ -10,4 +10,4 @@ public interface ILocalDataCache
IReadOnlyDictionary<string, SearchPokemonAbility> PokemonAbilities { get; }
IReadOnlyDictionary<int, string> PokemonMap { get; }
TriviaQuestion[] TriviaQuestions { get; }
}
}

View File

@@ -16,4 +16,4 @@ public interface ILocalization
void SetDefaultCulture(CultureInfo ci);
void SetGuildCulture(IGuild guild, CultureInfo ci);
void SetGuildCulture(ulong guildId, CultureInfo ci);
}
}

View File

@@ -2,8 +2,8 @@
namespace NadekoBot.Services;
/// <summary>
/// All services must implement this interface in order to be auto-discovered by the DI system
/// All services must implement this interface in order to be auto-discovered by the DI system
/// </summary>
public interface INService
{
}
}

View File

@@ -4,53 +4,53 @@ namespace NadekoBot.Services;
public interface IStatsService
{
/// <summary>
/// The author of the bot.
/// The author of the bot.
/// </summary>
string Author { get; }
/// <summary>
/// The total amount of commands ran since startup.
/// The total amount of commands ran since startup.
/// </summary>
long CommandsRan { get; }
/// <summary>
/// The Discord framework used by the bot.
/// The Discord framework used by the bot.
/// </summary>
string Library { get; }
/// <summary>
/// The amount of messages seen by the bot since startup.
/// The amount of messages seen by the bot since startup.
/// </summary>
long MessageCounter { get; }
/// <summary>
/// The rate of messages the bot sees every second.
/// The rate of messages the bot sees every second.
/// </summary>
double MessagesPerSecond { get; }
/// <summary>
/// The total amount of text channels the bot can see.
/// The total amount of text channels the bot can see.
/// </summary>
long TextChannels { get; }
/// <summary>
/// The total amount of voice channels the bot can see.
/// The total amount of voice channels the bot can see.
/// </summary>
long VoiceChannels { get; }
/// <summary>
/// Gets for how long the bot has been up since startup.
/// Gets for how long the bot has been up since startup.
/// </summary>
TimeSpan GetUptime();
/// <summary>
/// Gets a formatted string of how long the bot has been up since startup.
/// Gets a formatted string of how long the bot has been up since startup.
/// </summary>
/// <param name="separator">The formatting separator.</param>
string GetUptimeString(string separator = ", ");
/// <summary>
/// Gets total amount of private memory currently in use by the bot, in Megabytes.
/// Gets total amount of private memory currently in use by the bot, in Megabytes.
/// </summary>
double GetPrivateMemory();
}
}

View File

@@ -1,6 +1,6 @@
#nullable disable
using NadekoBot.Common.ModuleBehaviors;
using Microsoft.Extensions.DependencyInjection;
using NadekoBot.Common.ModuleBehaviors;
namespace NadekoBot.Services;
@@ -19,20 +19,15 @@ public sealed class BehaviorExecutor : IBehaviourExecutor, INService
{
_lateExecutors = _services.GetServices<ILateExecutor>();
_lateBlockers = _services.GetServices<ILateBlocker>();
_earlyBehaviors = _services.GetServices<IEarlyBehavior>()
.OrderByDescending(x => x.Priority);
_earlyBehaviors = _services.GetServices<IEarlyBehavior>().OrderByDescending(x => x.Priority);
_transformers = _services.GetServices<IInputTransformer>();
}
public async Task<bool> RunEarlyBehavioursAsync(SocketGuild guild, IUserMessage usrMsg)
{
foreach (var beh in _earlyBehaviors)
{
if (await beh.RunBehavior(guild, usrMsg))
{
return true;
}
}
return false;
}
@@ -43,7 +38,7 @@ public sealed class BehaviorExecutor : IBehaviourExecutor, INService
foreach (var exec in _transformers)
{
string newContent;
if ((newContent = await exec.TransformInput(guild, usrMsg.Channel, usrMsg.Author, messageContent))
if ((newContent = await exec.TransformInput(guild, usrMsg.Channel, usrMsg.Author, messageContent))
!= messageContent.ToLowerInvariant())
{
messageContent = newContent;
@@ -57,16 +52,14 @@ public sealed class BehaviorExecutor : IBehaviourExecutor, INService
public async Task<bool> RunLateBlockersAsync(ICommandContext ctx, CommandInfo cmd)
{
foreach (var exec in _lateBlockers)
{
if (await exec.TryBlockLate(ctx, cmd.Module.GetTopLevelModule().Name, cmd))
{
Log.Information("Late blocking User [{0}] Command: [{1}] in [{2}]",
Log.Information("Late blocking User [{0}] Command: [{1}] in [{2}]",
ctx.User,
cmd.Aliases[0],
exec.GetType().Name);
return true;
}
}
return false;
}
@@ -74,17 +67,13 @@ public sealed class BehaviorExecutor : IBehaviourExecutor, INService
public async Task RunLateExecutorsAsync(SocketGuild guild, IUserMessage usrMsg)
{
foreach (var exec in _lateExecutors)
{
try
{
await exec.LateExecute(guild, usrMsg);
}
catch (Exception ex)
{
Log.Error(ex, "Error in {TypeName} late executor: {ErrorMessage}",
exec.GetType().Name,
ex.Message);
Log.Error(ex, "Error in {TypeName} late executor: {ErrorMessage}", exec.GetType().Name, ex.Message);
}
}
}
}
}

View File

@@ -12,63 +12,81 @@ public interface IBotCredsProvider
public IBotCredentials GetCreds();
public void ModifyCredsFile(Action<Creds> func);
}
public sealed class BotCredsProvider : IBotCredsProvider
{
private readonly int? _totalShards;
private const string _credsFileName = "creds.yml";
private const string _credsExampleFileName = "creds_example.yml";
private string CredsPath => Path.Combine(Directory.GetCurrentDirectory(), _credsFileName);
private string CredsExamplePath => Path.Combine(Directory.GetCurrentDirectory(), _credsExampleFileName);
private string OldCredsJsonPath => Path.Combine(Directory.GetCurrentDirectory(), "credentials.json");
private string OldCredsJsonBackupPath => Path.Combine(Directory.GetCurrentDirectory(), "credentials.json.bak");
private string CredsPath
=> Path.Combine(Directory.GetCurrentDirectory(), _credsFileName);
private string CredsExamplePath
=> Path.Combine(Directory.GetCurrentDirectory(), _credsExampleFileName);
private string OldCredsJsonPath
=> Path.Combine(Directory.GetCurrentDirectory(), "credentials.json");
private string OldCredsJsonBackupPath
=> Path.Combine(Directory.GetCurrentDirectory(), "credentials.json.bak");
private readonly int? _totalShards;
private readonly Creds _creds = new();
private readonly IConfigurationRoot _config;
private readonly object reloadLock = new();
public BotCredsProvider(int? totalShards = null)
{
_totalShards = totalShards;
if (!File.Exists(CredsExamplePath)) File.WriteAllText(CredsExamplePath, Yaml.Serializer.Serialize(_creds));
MigrateCredentials();
if (!File.Exists(CredsPath))
Log.Warning($"{CredsPath} is missing. "
+ "Attempting to load creds from environment variables prefixed with 'NadekoBot_'. "
+ $"Example is in {CredsExamplePath}");
_config = new ConfigurationBuilder().AddYamlFile(CredsPath, false, true)
.AddEnvironmentVariables("NadekoBot_")
.Build();
ChangeToken.OnChange(() => _config.GetReloadToken(), Reload);
Reload();
}
public void Reload()
{
lock (reloadLock)
{
_creds.OwnerIds.Clear();
_config.Bind(_creds);
if (string.IsNullOrWhiteSpace(_creds.Token))
{
Log.Error("Token is missing from creds.yml or Environment variables.\n" +
"Add it and restart the program.");
Log.Error("Token is missing from creds.yml or Environment variables.\n"
+ "Add it and restart the program.");
Helpers.ReadErrorAndExit(5);
return;
}
if (string.IsNullOrWhiteSpace(_creds.RestartCommand?.Cmd)
|| string.IsNullOrWhiteSpace(_creds.RestartCommand?.Args))
{
if (Environment.OSVersion.Platform == PlatformID.Unix)
{
_creds.RestartCommand = new()
{
Args = "dotnet",
Cmd = "NadekoBot.dll -- {0}",
};
}
_creds.RestartCommand = new() { Args = "dotnet", Cmd = "NadekoBot.dll -- {0}" };
else
{
_creds.RestartCommand = new()
{
Args = "NadekoBot.exe",
Cmd = "{0}",
};
}
_creds.RestartCommand = new() { Args = "NadekoBot.exe", Cmd = "{0}" };
}
if (string.IsNullOrWhiteSpace(_creds.RedisOptions))
_creds.RedisOptions = "127.0.0.1,syncTimeout=3000";
if (string.IsNullOrWhiteSpace(_creds.CoinmarketcapApiKey))
_creds.CoinmarketcapApiKey = "e79ec505-0913-439d-ae07-069e296a6079";
@@ -76,35 +94,6 @@ public sealed class BotCredsProvider : IBotCredsProvider
}
}
public BotCredsProvider(int? totalShards = null)
{
_totalShards = totalShards;
if (!File.Exists(CredsExamplePath))
{
File.WriteAllText(CredsExamplePath, Yaml.Serializer.Serialize(_creds));
}
MigrateCredentials();
if (!File.Exists(CredsPath))
{
Log.Warning($"{CredsPath} is missing. " +
$"Attempting to load creds from environment variables prefixed with 'NadekoBot_'. " +
$"Example is in {CredsExamplePath}");
}
_config = new ConfigurationBuilder()
.AddYamlFile(CredsPath, false, true)
.AddEnvironmentVariables("NadekoBot_")
.Build();
ChangeToken.OnChange(
() => _config.GetReloadToken(),
Reload);
Reload();
}
public void ModifyCredsFile(Action<Creds> func)
{
var ymlData = File.ReadAllText(_credsFileName);
@@ -114,13 +103,13 @@ public sealed class BotCredsProvider : IBotCredsProvider
ymlData = Yaml.Serializer.Serialize(creds);
File.WriteAllText(_credsFileName, ymlData);
Reload();
}
/// <summary>
/// Checks if there's a V2 credentials file present, loads it if it exists,
/// converts it to new model, and saves it to YAML. Also backs up old credentials to credentials.json.bak
/// Checks if there's a V2 credentials file present, loads it if it exists,
/// converts it to new model, and saves it to YAML. Also backs up old credentials to credentials.json.bak
/// </summary>
private void MigrateCredentials()
{
@@ -140,25 +129,20 @@ public sealed class BotCredsProvider : IBotCredsProvider
OsuApiKey = oldCreds.OsuApiKey,
CleverbotApiKey = oldCreds.CleverbotApiKey,
TotalShards = oldCreds.TotalShards <= 1 ? 1 : oldCreds.TotalShards,
Patreon = new(oldCreds.PatreonAccessToken,
null,
null,
oldCreds.PatreonCampaignId),
Votes = new(oldCreds.VotesUrl,
oldCreds.VotesToken,
string.Empty,
string.Empty),
Patreon = new(oldCreds.PatreonAccessToken, null, null, oldCreds.PatreonCampaignId),
Votes = new(oldCreds.VotesUrl, oldCreds.VotesToken, string.Empty, string.Empty),
BotListToken = oldCreds.BotListToken,
RedisOptions = oldCreds.RedisOptions,
LocationIqApiKey = oldCreds.LocationIqApiKey,
TimezoneDbApiKey = oldCreds.TimezoneDbApiKey,
CoinmarketcapApiKey = oldCreds.CoinmarketcapApiKey,
CoinmarketcapApiKey = oldCreds.CoinmarketcapApiKey
};
File.Move(OldCredsJsonPath, OldCredsJsonBackupPath, true);
File.WriteAllText(CredsPath, Yaml.Serializer.Serialize(creds));
Log.Warning("Data from credentials.json has been moved to creds.yml\nPlease inspect your creds.yml for correctness");
Log.Warning(
"Data from credentials.json has been moved to creds.yml\nPlease inspect your creds.yml for correctness");
}
if (File.Exists(_credsFileName))
@@ -170,8 +154,8 @@ public sealed class BotCredsProvider : IBotCredsProvider
File.WriteAllText(_credsFileName, Yaml.Serializer.Serialize(creds));
}
}
}
public IBotCredentials GetCreds() => _creds;
}
public IBotCredentials GetCreds()
=> _creds;
}

View File

@@ -1,8 +1,8 @@
#nullable disable
using NadekoBot.Services.Database;
using NadekoBot.Services.Database.Models;
using NadekoBot.Db;
using NadekoBot.Modules.Gambling.Services;
using NadekoBot.Services.Database;
using NadekoBot.Services.Database.Models;
namespace NadekoBot.Services;
@@ -13,7 +13,11 @@ public class CurrencyService : ICurrencyService, INService
private readonly IEmbedBuilderService _eb;
private readonly IUser _bot;
public CurrencyService(DbService db, DiscordSocketClient c, GamblingConfigService gss, IEmbedBuilderService eb)
public CurrencyService(
DbService db,
DiscordSocketClient c,
GamblingConfigService gss,
IEmbedBuilderService eb)
{
_db = db;
_gss = gss;
@@ -21,16 +25,18 @@ public class CurrencyService : ICurrencyService, INService
_bot = c.CurrentUser;
}
private CurrencyTransaction GetCurrencyTransaction(ulong userId, string reason, long amount) =>
new()
{
Amount = amount,
UserId = userId,
Reason = reason ?? "-",
};
private CurrencyTransaction GetCurrencyTransaction(ulong userId, string reason, long amount)
=> new() { Amount = amount, UserId = userId, Reason = reason ?? "-" };
private bool InternalChange(ulong userId, string userName, string discrim, string avatar,
string reason, long amount, bool gamble, NadekoContext uow)
private bool InternalChange(
ulong userId,
string userName,
string discrim,
string avatar,
string reason,
long amount,
bool gamble,
NadekoContext uow)
{
var result = uow.TryUpdateCurrencyState(userId, userName, discrim, avatar, amount);
if (result)
@@ -45,47 +51,64 @@ public class CurrencyService : ICurrencyService, INService
uow.TryUpdateCurrencyState(_bot.Id, _bot.Username, _bot.Discriminator, _bot.AvatarId, -amount, true);
}
}
return result;
}
private async Task InternalAddAsync(ulong userId, string userName, string discrim, string avatar, string reason, long amount, bool gamble)
private async Task InternalAddAsync(
ulong userId,
string userName,
string discrim,
string avatar,
string reason,
long amount,
bool gamble)
{
if (amount < 0)
{
throw new ArgumentException("You can't add negative amounts. Use RemoveAsync method for that.", nameof(amount));
}
throw new ArgumentException("You can't add negative amounts. Use RemoveAsync method for that.",
nameof(amount));
await using var uow = _db.GetDbContext();
InternalChange(userId, userName, discrim, avatar, reason, amount, gamble, uow);
await uow.SaveChangesAsync();
}
public Task AddAsync(ulong userId, string reason, long amount, bool gamble = false)
public Task AddAsync(
ulong userId,
string reason,
long amount,
bool gamble = false)
=> InternalAddAsync(userId, null, null, null, reason, amount, gamble);
public async Task AddAsync(IUser user, string reason, long amount, bool sendMessage = false, bool gamble = false)
public async Task AddAsync(
IUser user,
string reason,
long amount,
bool sendMessage = false,
bool gamble = false)
{
await InternalAddAsync(user.Id, user.Username, user.Discriminator, user.AvatarId, reason, amount, gamble);
if (sendMessage)
{
try
{
var sign = _gss.Data.Currency.Sign;
await user
.EmbedAsync(_eb.Create()
.WithOkColor()
.WithTitle($"Received Currency")
.AddField("Amount", amount + sign)
.AddField("Reason", reason));
await user.EmbedAsync(_eb.Create()
.WithOkColor()
.WithTitle("Received Currency")
.AddField("Amount", amount + sign)
.AddField("Reason", reason));
}
catch
{
// ignored
}
}
}
public async Task AddBulkAsync(IEnumerable<ulong> userIds, IEnumerable<string> reasons, IEnumerable<long> amounts, bool gamble = false)
public async Task AddBulkAsync(
IEnumerable<ulong> userIds,
IEnumerable<string> reasons,
IEnumerable<long> amounts,
bool gamble = false)
{
var idArray = userIds as ulong[] ?? userIds.ToArray();
var reasonArray = reasons as string[] ?? reasons.ToArray();
@@ -97,15 +120,17 @@ public class CurrencyService : ICurrencyService, INService
var userIdHashSet = new HashSet<ulong>(idArray.Length);
await using var uow = _db.GetDbContext();
for (var i = 0; i < idArray.Length; i++)
{
// i have to prevent same user changing more than once as it will cause db error
if (userIdHashSet.Add(idArray[i]))
InternalChange(idArray[i], null, null, null, reasonArray[i], amountArray[i], gamble, uow);
}
await uow.SaveChangesAsync();
}
public async Task RemoveBulkAsync(IEnumerable<ulong> userIds, IEnumerable<string> reasons, IEnumerable<long> amounts, bool gamble = false)
public async Task RemoveBulkAsync(
IEnumerable<ulong> userIds,
IEnumerable<string> reasons,
IEnumerable<long> amounts,
bool gamble = false)
{
var idArray = userIds as ulong[] ?? userIds.ToArray();
var reasonArray = reasons as string[] ?? reasons.ToArray();
@@ -117,20 +142,24 @@ public class CurrencyService : ICurrencyService, INService
var userIdHashSet = new HashSet<ulong>(idArray.Length);
await using var uow = _db.GetDbContext();
for (var i = 0; i < idArray.Length; i++)
{
// i have to prevent same user changing more than once as it will cause db error
if (userIdHashSet.Add(idArray[i]))
InternalChange(idArray[i], null, null, null, reasonArray[i], -amountArray[i], gamble, uow);
}
await uow.SaveChangesAsync();
}
private async Task<bool> InternalRemoveAsync(ulong userId, string userName, string userDiscrim, string avatar, string reason, long amount, bool gamble = false)
private async Task<bool> InternalRemoveAsync(
ulong userId,
string userName,
string userDiscrim,
string avatar,
string reason,
long amount,
bool gamble = false)
{
if (amount < 0)
{
throw new ArgumentException("You can't remove negative amounts. Use AddAsync method for that.", nameof(amount));
}
throw new ArgumentException("You can't remove negative amounts. Use AddAsync method for that.",
nameof(amount));
bool result;
await using var uow = _db.GetDbContext();
@@ -139,9 +168,18 @@ public class CurrencyService : ICurrencyService, INService
return result;
}
public Task<bool> RemoveAsync(ulong userId, string reason, long amount, bool gamble = false)
public Task<bool> RemoveAsync(
ulong userId,
string reason,
long amount,
bool gamble = false)
=> InternalRemoveAsync(userId, null, null, null, reason, amount, gamble);
public Task<bool> RemoveAsync(IUser user, string reason, long amount, bool sendMessage = false, bool gamble = false)
public Task<bool> RemoveAsync(
IUser user,
string reason,
long amount,
bool sendMessage = false,
bool gamble = false)
=> InternalRemoveAsync(user.Id, user.Username, user.Discriminator, user.AvatarId, reason, amount, gamble);
}
}

View File

@@ -5,6 +5,19 @@ namespace NadekoBot.Services;
public class FontProvider : INService
{
public FontFamily DottyFont { get; }
public FontFamily UniSans { get; }
public FontFamily NotoSans { get; }
//public FontFamily Emojis { get; }
/// <summary>
/// Font used for .rip command
/// </summary>
public Font RipFont { get; }
public List<FontFamily> FallBackFonts { get; }
private readonly FontCollection _fonts;
public FontProvider()
@@ -20,44 +33,23 @@ public class FontProvider : INService
// try loading some emoji and jap fonts on windows as fallback fonts
if (Environment.OSVersion.Platform == PlatformID.Win32NT)
{
try
{
var fontsfolder = System.Environment.GetFolderPath(System.Environment.SpecialFolder.Fonts);
var fontsfolder = Environment.GetFolderPath(Environment.SpecialFolder.Fonts);
FallBackFonts.Add(_fonts.Install(Path.Combine(fontsfolder, "seguiemj.ttf")));
FallBackFonts.AddRange(_fonts.InstallCollection(Path.Combine(fontsfolder, "msgothic.ttc")));
FallBackFonts.AddRange(_fonts.InstallCollection(Path.Combine(fontsfolder, "segoe.ttc")));
}
catch { }
}
// any fonts present in data/fonts should be added as fallback fonts
// this will allow support for special characters when drawing text
foreach (var font in Directory.GetFiles(@"data/fonts"))
{
if (font.EndsWith(".ttf"))
{
FallBackFonts.Add(_fonts.Install(font));
}
else if (font.EndsWith(".ttc"))
{
FallBackFonts.AddRange(_fonts.InstallCollection(font));
}
}
else if (font.EndsWith(".ttc")) FallBackFonts.AddRange(_fonts.InstallCollection(font));
RipFont = NotoSans.CreateFont(20, FontStyle.Bold);
DottyFont = FallBackFonts.First(x => x.Name == "dotty");
}
public FontFamily DottyFont { get; }
public FontFamily UniSans { get; }
public FontFamily NotoSans { get; }
//public FontFamily Emojis { get; }
/// <summary>
/// Font used for .rip command
/// </summary>
public Font RipFont { get; }
public List<FontFamily> FallBackFonts { get; }
}
}

View File

@@ -7,6 +7,7 @@ using Google.Apis.YouTube.v3;
using Newtonsoft.Json.Linq;
using System.Net;
using System.Text.RegularExpressions;
using System.Xml;
namespace NadekoBot.Services;
@@ -14,26 +15,162 @@ public class GoogleApiService : IGoogleApiService, INService
{
private const string SearchEngineId = "018084019232060951019:hs5piey28-e";
private static readonly Regex
plRegex = new("(?:youtu\\.be\\/|list=)(?<id>[\\da-zA-Z\\-_]*)", RegexOptions.Compiled);
public IReadOnlyDictionary<string, string> Languages { get; } = new Dictionary<string, string>
{
{ "afrikaans", "af" },
{ "albanian", "sq" },
{ "arabic", "ar" },
{ "armenian", "hy" },
{ "azerbaijani", "az" },
{ "basque", "eu" },
{ "belarusian", "be" },
{ "bengali", "bn" },
{ "bulgarian", "bg" },
{ "catalan", "ca" },
{ "chinese-traditional", "zh-TW" },
{ "chinese-simplified", "zh-CN" },
{ "chinese", "zh-CN" },
{ "croatian", "hr" },
{ "czech", "cs" },
{ "danish", "da" },
{ "dutch", "nl" },
{ "english", "en" },
{ "esperanto", "eo" },
{ "estonian", "et" },
{ "filipino", "tl" },
{ "finnish", "fi" },
{ "french", "fr" },
{ "galician", "gl" },
{ "german", "de" },
{ "georgian", "ka" },
{ "greek", "el" },
{ "haitian Creole", "ht" },
{ "hebrew", "iw" },
{ "hindi", "hi" },
{ "hungarian", "hu" },
{ "icelandic", "is" },
{ "indonesian", "id" },
{ "irish", "ga" },
{ "italian", "it" },
{ "japanese", "ja" },
{ "korean", "ko" },
{ "lao", "lo" },
{ "latin", "la" },
{ "latvian", "lv" },
{ "lithuanian", "lt" },
{ "macedonian", "mk" },
{ "malay", "ms" },
{ "maltese", "mt" },
{ "norwegian", "no" },
{ "persian", "fa" },
{ "polish", "pl" },
{ "portuguese", "pt" },
{ "romanian", "ro" },
{ "russian", "ru" },
{ "serbian", "sr" },
{ "slovak", "sk" },
{ "slovenian", "sl" },
{ "spanish", "es" },
{ "swahili", "sw" },
{ "swedish", "sv" },
{ "tamil", "ta" },
{ "telugu", "te" },
{ "thai", "th" },
{ "turkish", "tr" },
{ "ukrainian", "uk" },
{ "urdu", "ur" },
{ "vietnamese", "vi" },
{ "welsh", "cy" },
{ "yiddish", "yi" },
{ "af", "af" },
{ "sq", "sq" },
{ "ar", "ar" },
{ "hy", "hy" },
{ "az", "az" },
{ "eu", "eu" },
{ "be", "be" },
{ "bn", "bn" },
{ "bg", "bg" },
{ "ca", "ca" },
{ "zh-tw", "zh-TW" },
{ "zh-cn", "zh-CN" },
{ "hr", "hr" },
{ "cs", "cs" },
{ "da", "da" },
{ "nl", "nl" },
{ "en", "en" },
{ "eo", "eo" },
{ "et", "et" },
{ "tl", "tl" },
{ "fi", "fi" },
{ "fr", "fr" },
{ "gl", "gl" },
{ "de", "de" },
{ "ka", "ka" },
{ "el", "el" },
{ "ht", "ht" },
{ "iw", "iw" },
{ "hi", "hi" },
{ "hu", "hu" },
{ "is", "is" },
{ "id", "id" },
{ "ga", "ga" },
{ "it", "it" },
{ "ja", "ja" },
{ "ko", "ko" },
{ "lo", "lo" },
{ "la", "la" },
{ "lv", "lv" },
{ "lt", "lt" },
{ "mk", "mk" },
{ "ms", "ms" },
{ "mt", "mt" },
{ "no", "no" },
{ "fa", "fa" },
{ "pl", "pl" },
{ "pt", "pt" },
{ "ro", "ro" },
{ "ru", "ru" },
{ "sr", "sr" },
{ "sk", "sk" },
{ "sl", "sl" },
{ "es", "es" },
{ "sw", "sw" },
{ "sv", "sv" },
{ "ta", "ta" },
{ "te", "te" },
{ "th", "th" },
{ "tr", "tr" },
{ "uk", "uk" },
{ "ur", "ur" },
{ "vi", "vi" },
{ "cy", "cy" },
{ "yi", "yi" }
};
private readonly YouTubeService yt;
private readonly UrlshortenerService sh;
private readonly CustomsearchService cs;
//private readonly Regex YtVideoIdRegex = new Regex(@"(?:youtube\.com\/\S*(?:(?:\/e(?:mbed))?\/|watch\?(?:\S*?&?v\=))|youtu\.be\/)(?<id>[a-zA-Z0-9_-]{6,11})", RegexOptions.Compiled);
private readonly IBotCredentials _creds;
private readonly IHttpClientFactory _httpFactory;
public GoogleApiService(IBotCredentials creds, IHttpClientFactory factory)
{
_creds = creds;
_httpFactory = factory;
var bcs = new BaseClientService.Initializer
{
ApplicationName = "Nadeko Bot",
ApiKey = _creds.GoogleApiKey,
};
var bcs = new BaseClientService.Initializer { ApplicationName = "Nadeko Bot", ApiKey = _creds.GoogleApiKey };
yt = new(bcs);
sh = new(bcs);
cs = new(bcs);
}
private static readonly Regex plRegex = new("(?:youtu\\.be\\/|list=)(?<id>[\\da-zA-Z\\-_]*)", RegexOptions.Compiled);
public async Task<IEnumerable<string>> GetPlaylistIdsByKeywordsAsync(string keywords, int count = 1)
{
await Task.Yield();
@@ -44,10 +181,7 @@ public class GoogleApiService : IGoogleApiService, INService
throw new ArgumentOutOfRangeException(nameof(count));
var match = plRegex.Match(keywords);
if (match.Length > 1)
{
return new[] { match.Groups["id"].Value.ToString() };
}
if (match.Length > 1) return new[] { match.Groups["id"].Value };
var query = yt.Search.List("snippet");
query.MaxResults = count;
query.Type = "playlist";
@@ -56,10 +190,6 @@ public class GoogleApiService : IGoogleApiService, INService
return (await query.ExecuteAsync()).Items.Select(i => i.Id.PlaylistId);
}
//private readonly Regex YtVideoIdRegex = new Regex(@"(?:youtube\.com\/\S*(?:(?:\/e(?:mbed))?\/|watch\?(?:\S*?&?v\=))|youtu\.be\/)(?<id>[a-zA-Z0-9_-]{6,11})", RegexOptions.Compiled);
private readonly IBotCredentials _creds;
private readonly IHttpClientFactory _httpFactory;
// todo future add quota users
public async Task<IEnumerable<string>> GetRelatedVideosAsync(string id, int count = 1)
{
@@ -93,7 +223,9 @@ public class GoogleApiService : IGoogleApiService, INService
return (await query.ExecuteAsync()).Items.Select(i => "http://www.youtube.com/watch?v=" + i.Id.VideoId);
}
public async Task<IEnumerable<(string Name, string Id, string Url)>> GetVideoInfosByKeywordAsync(string keywords, int count = 1)
public async Task<IEnumerable<(string Name, string Id, string Url)>> GetVideoInfosByKeywordAsync(
string keywords,
int count = 1)
{
await Task.Yield();
if (string.IsNullOrWhiteSpace(keywords))
@@ -106,10 +238,12 @@ public class GoogleApiService : IGoogleApiService, INService
query.MaxResults = count;
query.Q = keywords;
query.Type = "video";
return (await query.ExecuteAsync()).Items.Select(i => (i.Snippet.Title.TrimTo(50), i.Id.VideoId, "http://www.youtube.com/watch?v=" + i.Id.VideoId));
return (await query.ExecuteAsync()).Items.Select(i
=> (i.Snippet.Title.TrimTo(50), i.Id.VideoId, "http://www.youtube.com/watch?v=" + i.Id.VideoId));
}
public Task<string> ShortenUrl(Uri url) => ShortenUrl(url.ToString());
public Task<string> ShortenUrl(Uri url)
=> ShortenUrl(url.ToString());
public async Task<string> ShortenUrl(string url)
{
@@ -163,8 +297,7 @@ public class GoogleApiService : IGoogleApiService, INService
toReturn.AddRange(data.Items.Select(i => i.ContentDetails.VideoId));
nextPageToken = data.NextPageToken;
}
while (count > 0 && !string.IsNullOrWhiteSpace(nextPageToken));
} while (count > 0 && !string.IsNullOrWhiteSpace(nextPageToken));
return toReturn;
}
@@ -189,12 +322,8 @@ public class GoogleApiService : IGoogleApiService, INService
q.Id = string.Join(",", videoIdsList.Take(toGet));
videoIdsList = videoIdsList.Skip(toGet).ToList();
var items = (await q.ExecuteAsync()).Items;
foreach (var i in items)
{
toReturn.Add(i.Id, System.Xml.XmlConvert.ToTimeSpan(i.ContentDetails.Duration));
}
}
while (remaining > 0);
foreach (var i in items) toReturn.Add(i.Id, XmlConvert.ToTimeSpan(i.ContentDetails.Duration));
} while (remaining > 0);
return toReturn;
}
@@ -218,156 +347,24 @@ public class GoogleApiService : IGoogleApiService, INService
return new(search.Items[0].Image, search.Items[0].Link);
}
public IReadOnlyDictionary<string, string> Languages { get; } = new Dictionary<string, string>() {
{ "afrikaans", "af"},
{ "albanian", "sq"},
{ "arabic", "ar"},
{ "armenian", "hy"},
{ "azerbaijani", "az"},
{ "basque", "eu"},
{ "belarusian", "be"},
{ "bengali", "bn"},
{ "bulgarian", "bg"},
{ "catalan", "ca"},
{ "chinese-traditional", "zh-TW"},
{ "chinese-simplified", "zh-CN"},
{ "chinese", "zh-CN"},
{ "croatian", "hr"},
{ "czech", "cs"},
{ "danish", "da"},
{ "dutch", "nl"},
{ "english", "en"},
{ "esperanto", "eo"},
{ "estonian", "et"},
{ "filipino", "tl"},
{ "finnish", "fi"},
{ "french", "fr"},
{ "galician", "gl"},
{ "german", "de"},
{ "georgian", "ka"},
{ "greek", "el"},
{ "haitian Creole", "ht"},
{ "hebrew", "iw"},
{ "hindi", "hi"},
{ "hungarian", "hu"},
{ "icelandic", "is"},
{ "indonesian", "id"},
{ "irish", "ga"},
{ "italian", "it"},
{ "japanese", "ja"},
{ "korean", "ko"},
{ "lao", "lo"},
{ "latin", "la"},
{ "latvian", "lv"},
{ "lithuanian", "lt"},
{ "macedonian", "mk"},
{ "malay", "ms"},
{ "maltese", "mt"},
{ "norwegian", "no"},
{ "persian", "fa"},
{ "polish", "pl"},
{ "portuguese", "pt"},
{ "romanian", "ro"},
{ "russian", "ru"},
{ "serbian", "sr"},
{ "slovak", "sk"},
{ "slovenian", "sl"},
{ "spanish", "es"},
{ "swahili", "sw"},
{ "swedish", "sv"},
{ "tamil", "ta"},
{ "telugu", "te"},
{ "thai", "th"},
{ "turkish", "tr"},
{ "ukrainian", "uk"},
{ "urdu", "ur"},
{ "vietnamese", "vi"},
{ "welsh", "cy"},
{ "yiddish", "yi"},
{ "af", "af"},
{ "sq", "sq"},
{ "ar", "ar"},
{ "hy", "hy"},
{ "az", "az"},
{ "eu", "eu"},
{ "be", "be"},
{ "bn", "bn"},
{ "bg", "bg"},
{ "ca", "ca"},
{ "zh-tw", "zh-TW"},
{ "zh-cn", "zh-CN"},
{ "hr", "hr"},
{ "cs", "cs"},
{ "da", "da"},
{ "nl", "nl"},
{ "en", "en"},
{ "eo", "eo"},
{ "et", "et"},
{ "tl", "tl"},
{ "fi", "fi"},
{ "fr", "fr"},
{ "gl", "gl"},
{ "de", "de"},
{ "ka", "ka"},
{ "el", "el"},
{ "ht", "ht"},
{ "iw", "iw"},
{ "hi", "hi"},
{ "hu", "hu"},
{ "is", "is"},
{ "id", "id"},
{ "ga", "ga"},
{ "it", "it"},
{ "ja", "ja"},
{ "ko", "ko"},
{ "lo", "lo"},
{ "la", "la"},
{ "lv", "lv"},
{ "lt", "lt"},
{ "mk", "mk"},
{ "ms", "ms"},
{ "mt", "mt"},
{ "no", "no"},
{ "fa", "fa"},
{ "pl", "pl"},
{ "pt", "pt"},
{ "ro", "ro"},
{ "ru", "ru"},
{ "sr", "sr"},
{ "sk", "sk"},
{ "sl", "sl"},
{ "es", "es"},
{ "sw", "sw"},
{ "sv", "sv"},
{ "ta", "ta"},
{ "te", "te"},
{ "th", "th"},
{ "tr", "tr"},
{ "uk", "uk"},
{ "ur", "ur"},
{ "vi", "vi"},
{ "cy", "cy"},
{ "yi", "yi"},
};
public async Task<string> Translate(string sourceText, string sourceLanguage, string targetLanguage)
{
await Task.Yield();
string text;
if (!Languages.ContainsKey(sourceLanguage) ||
!Languages.ContainsKey(targetLanguage))
if (!Languages.ContainsKey(sourceLanguage) || !Languages.ContainsKey(targetLanguage))
throw new ArgumentException(nameof(sourceLanguage) + "/" + nameof(targetLanguage));
var url = new Uri(string.Format("https://translate.googleapis.com/translate_a/single?client=gtx&sl={0}&tl={1}&dt=t&q={2}",
var url = new Uri(string.Format(
"https://translate.googleapis.com/translate_a/single?client=gtx&sl={0}&tl={1}&dt=t&q={2}",
ConvertToLanguageCode(sourceLanguage),
ConvertToLanguageCode(targetLanguage),
WebUtility.UrlEncode(sourceText)));
using (var http = _httpFactory.CreateClient())
{
http.DefaultRequestHeaders.Add("user-agent", "Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2228.0 Safari/537.36");
http.DefaultRequestHeaders.Add("user-agent",
"Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2228.0 Safari/537.36");
text = await http.GetStringAsync(url);
}
@@ -379,4 +376,4 @@ public class GoogleApiService : IGoogleApiService, INService
Languages.TryGetValue(language, out var mode);
return mode;
}
}
}

View File

@@ -1,45 +1,50 @@
#nullable disable
using System.Globalization;
using Newtonsoft.Json;
using NadekoBot.Db;
using Newtonsoft.Json;
using System.Globalization;
namespace NadekoBot.Services;
public class Localization : ILocalization, INService
{
private readonly BotConfigService _bss;
private readonly DbService _db;
private static readonly Dictionary<string, CommandData> _commandData =
JsonConvert.DeserializeObject<Dictionary<string, CommandData>>(
File.ReadAllText("./data/strings/commands/commands.en-US.json"));
public ConcurrentDictionary<ulong, CultureInfo> GuildCultureInfos { get; }
public CultureInfo DefaultCultureInfo => _bss.Data.DefaultLocale;
private static readonly Dictionary<string, CommandData> _commandData = JsonConvert.DeserializeObject<Dictionary<string, CommandData>>(
File.ReadAllText("./data/strings/commands/commands.en-US.json"));
public CultureInfo DefaultCultureInfo
=> _bss.Data.DefaultLocale;
private readonly BotConfigService _bss;
private readonly DbService _db;
public Localization(BotConfigService bss, Bot bot, DbService db)
{
_bss = bss;
_db = db;
var cultureInfoNames = bot.AllGuildConfigs
.ToDictionary(x => x.GuildId, x => x.Locale);
GuildCultureInfos = new(cultureInfoNames.ToDictionary(x => x.Key, x =>
{
CultureInfo cultureInfo = null;
try
{
if (x.Value is null)
return null;
cultureInfo = new(x.Value);
}
catch { }
return cultureInfo;
}).Where(x => x.Value != null));
var cultureInfoNames = bot.AllGuildConfigs.ToDictionary(x => x.GuildId, x => x.Locale);
GuildCultureInfos = new(cultureInfoNames.ToDictionary(x => x.Key,
x =>
{
CultureInfo cultureInfo = null;
try
{
if (x.Value is null)
return null;
cultureInfo = new(x.Value);
}
catch { }
return cultureInfo;
})
.Where(x => x.Value != null));
}
public void SetGuildCulture(IGuild guild, CultureInfo ci) =>
SetGuildCulture(guild.Id, ci);
public void SetGuildCulture(IGuild guild, CultureInfo ci)
=> SetGuildCulture(guild.Id, ci);
public void SetGuildCulture(ulong guildId, CultureInfo ci)
{
@@ -59,12 +64,11 @@ public class Localization : ILocalization, INService
GuildCultureInfos.AddOrUpdate(guildId, ci, (id, old) => ci);
}
public void RemoveGuildCulture(IGuild guild) =>
RemoveGuildCulture(guild.Id);
public void RemoveGuildCulture(IGuild guild)
=> RemoveGuildCulture(guild.Id);
public void RemoveGuildCulture(ulong guildId)
{
if (GuildCultureInfos.TryRemove(guildId, out var _))
{
using var uow = _db.GetDbContext();
@@ -80,17 +84,17 @@ public class Localization : ILocalization, INService
bs.DefaultLocale = ci;
});
public void ResetDefaultCulture() =>
SetDefaultCulture(CultureInfo.CurrentCulture);
public void ResetDefaultCulture()
=> SetDefaultCulture(CultureInfo.CurrentCulture);
public CultureInfo GetCultureInfo(IGuild guild) =>
GetCultureInfo(guild?.Id);
public CultureInfo GetCultureInfo(IGuild guild)
=> GetCultureInfo(guild?.Id);
public CultureInfo GetCultureInfo(ulong? guildId)
{
if (guildId is null || !GuildCultureInfos.TryGetValue(guildId.Value, out var info) || info is null)
return _bss.Data.DefaultLocale;
return info;
}
@@ -99,13 +103,8 @@ public class Localization : ILocalization, INService
_commandData.TryGetValue(key, out var toReturn);
if (toReturn is null)
return new()
{
Cmd = key,
Desc = key,
Usage = new[] { key },
};
return new() { Cmd = key, Desc = key, Usage = new[] { key } };
return toReturn;
}
}
}

View File

@@ -15,8 +15,13 @@ public class RedisCache : IDataCache
private readonly string _redisKey;
private readonly EndPoint _redisEndpoint;
public RedisCache(ConnectionMultiplexer redis, IBotCredentials creds,
IImageCache imageCache, ILocalDataCache dataCache)
private readonly object timelyLock = new();
public RedisCache(
ConnectionMultiplexer redis,
IBotCredentials creds,
IImageCache imageCache,
ILocalDataCache dataCache)
{
Redis = redis;
_redisEndpoint = Redis.GetEndPoints().First();
@@ -52,7 +57,7 @@ public class RedisCache : IDataCache
public Task SetAnimeDataAsync(string key, string data)
{
var _db = Redis.GetDatabase();
return _db.StringSetAsync("anime_" + key, data, expiry: TimeSpan.FromHours(3));
return _db.StringSetAsync("anime_" + key, data, TimeSpan.FromHours(3));
}
public async Task<(bool Success, string Data)> TryGetNovelDataAsync(string key)
@@ -65,10 +70,9 @@ public class RedisCache : IDataCache
public Task SetNovelDataAsync(string key, string data)
{
var _db = Redis.GetDatabase();
return _db.StringSetAsync("novel_" + key, data, expiry: TimeSpan.FromHours(3));
return _db.StringSetAsync("novel_" + key, data, TimeSpan.FromHours(3));
}
private readonly object timelyLock = new();
public TimeSpan? AddTimelyClaim(ulong id, int period)
{
if (period == 0)
@@ -82,6 +86,7 @@ public class RedisCache : IDataCache
_db.StringSet($"{_redisKey}_timelyclaim_{id}", true, time);
return null;
}
return _db.KeyTimeToLive($"{_redisKey}_timelyclaim_{id}");
}
}
@@ -91,9 +96,7 @@ public class RedisCache : IDataCache
var server = Redis.GetServer(_redisEndpoint);
var _db = Redis.GetDatabase();
foreach (var k in server.Keys(pattern: $"{_redisKey}_timelyclaim_*"))
{
_db.KeyDelete(k, CommandFlags.FireAndForget);
}
}
public bool TryAddAffinityCooldown(ulong userId, out TimeSpan? time)
@@ -106,6 +109,7 @@ public class RedisCache : IDataCache
_db.StringSet($"{_redisKey}_affinity_{userId}", true, time);
return true;
}
return false;
}
@@ -119,13 +123,14 @@ public class RedisCache : IDataCache
_db.StringSet($"{_redisKey}_divorce_{userId}", true, time);
return true;
}
return false;
}
public Task SetStreamDataAsync(string url, string data)
{
var _db = Redis.GetDatabase();
return _db.StringSetAsync($"{_redisKey}_stream_{url}", data, expiry: TimeSpan.FromHours(6));
return _db.StringSetAsync($"{_redisKey}_stream_{url}", data, TimeSpan.FromHours(6));
}
public bool TryGetStreamData(string url, out string dataStr)
@@ -143,9 +148,7 @@ public class RedisCache : IDataCache
0, // i don't use the value
TimeSpan.FromSeconds(expireIn),
When.NotExists))
{
return null;
}
return _db.KeyTimeToLive($"{_redisKey}_ratelimit_{id}_{name}");
}
@@ -153,10 +156,7 @@ public class RedisCache : IDataCache
public bool TryGetEconomy(out string data)
{
var _db = Redis.GetDatabase();
if ((data = _db.StringGet($"{_redisKey}_economy")) != null)
{
return true;
}
if ((data = _db.StringGet($"{_redisKey}_economy")) != null) return true;
return false;
}
@@ -164,12 +164,15 @@ public class RedisCache : IDataCache
public void SetEconomy(string data)
{
var _db = Redis.GetDatabase();
_db.StringSet($"{_redisKey}_economy",
data,
expiry: TimeSpan.FromMinutes(3));
_db.StringSet($"{_redisKey}_economy", data, TimeSpan.FromMinutes(3));
}
public async Task<TOut> GetOrAddCachedDataAsync<TParam, TOut>(string key, Func<TParam, Task<TOut>> factory, TParam param, TimeSpan expiry) where TOut : class
public async Task<TOut> GetOrAddCachedDataAsync<TParam, TOut>(
string key,
Func<TParam, Task<TOut>> factory,
TParam param,
TimeSpan expiry)
where TOut : class
{
var _db = Redis.GetDatabase();
@@ -179,13 +182,13 @@ public class RedisCache : IDataCache
var obj = await factory(param);
if (obj is null)
return default(TOut);
return default;
await _db.StringSetAsync(key, JsonConvert.SerializeObject(obj),
expiry: expiry);
await _db.StringSetAsync(key, JsonConvert.SerializeObject(obj), expiry);
return obj;
}
return (TOut)JsonConvert.DeserializeObject(data, typeof(TOut));
}
@@ -194,7 +197,7 @@ public class RedisCache : IDataCache
var db = Redis.GetDatabase();
var str = (string)db.StringGet($"{_redisKey}_last_currency_decay");
if(string.IsNullOrEmpty(str))
if (string.IsNullOrEmpty(str))
return DateTime.MinValue;
return JsonConvert.DeserializeObject<DateTime>(str);
@@ -206,4 +209,4 @@ public class RedisCache : IDataCache
db.StringSet($"{_redisKey}_last_currency_decay", JsonConvert.SerializeObject(DateTime.UtcNow));
}
}
}

View File

@@ -5,7 +5,7 @@ public static class RedisImageExtensions
{
private const string OldCdnUrl = "nadeko-pictures.nyc3.digitaloceanspaces.com";
private const string NewCdnUrl = "cdn.nadeko.bot";
public static Uri ToNewCdn(this Uri uri)
=> new(uri.ToString().Replace(OldCdnUrl, NewCdnUrl));
}
}

View File

@@ -1,26 +1,13 @@
#nullable disable
using Newtonsoft.Json;
using StackExchange.Redis;
using NadekoBot.Common.ModuleBehaviors;
using NadekoBot.Common.Yml;
using Newtonsoft.Json;
using StackExchange.Redis;
namespace NadekoBot.Services;
public sealed class RedisImagesCache : IImageCache, IReadyExecutor
{
private readonly ConnectionMultiplexer _con;
private readonly IBotCredentials _creds;
private readonly HttpClient _http;
private readonly string _imagesPath;
private IDatabase Db
=> _con.GetDatabase();
private const string BASE_PATH = "data/";
private const string CARDS_PATH = $"{BASE_PATH}images/cards";
public ImageUrls ImageUrls { get; private set; }
public enum ImageKeys
{
CoinHeads,
@@ -36,6 +23,14 @@ public sealed class RedisImagesCache : IImageCache, IReadyExecutor
XpBg
}
private const string BASE_PATH = "data/";
private const string CARDS_PATH = $"{BASE_PATH}images/cards";
private IDatabase Db
=> _con.GetDatabase();
public ImageUrls ImageUrls { get; private set; }
public IReadOnlyList<byte[]> Heads
=> GetByteArrayData(ImageKeys.CoinHeads);
@@ -69,17 +64,10 @@ public sealed class RedisImagesCache : IImageCache, IReadyExecutor
public byte[] RipOverlay
=> GetByteData(ImageKeys.RipOverlay);
public byte[] GetCard(string key)
// since cards are always local for now, don't cache them
=> File.ReadAllBytes(Path.Join(CARDS_PATH, key + ".jpg"));
public async Task OnReadyAsync()
{
if (await AllKeysExist())
return;
await Reload();
}
private readonly ConnectionMultiplexer _con;
private readonly IBotCredentials _creds;
private readonly HttpClient _http;
private readonly string _imagesPath;
public RedisImagesCache(ConnectionMultiplexer con, IBotCredentials creds)
{
@@ -93,6 +81,18 @@ public sealed class RedisImagesCache : IImageCache, IReadyExecutor
ImageUrls = Yaml.Deserializer.Deserialize<ImageUrls>(File.ReadAllText(_imagesPath));
}
public byte[] GetCard(string key)
// since cards are always local for now, don't cache them
=> File.ReadAllBytes(Path.Join(CARDS_PATH, key + ".jpg"));
public async Task OnReadyAsync()
{
if (await AllKeysExist())
return;
await Reload();
}
private void Migrate()
{
// migrate to yml
@@ -105,21 +105,22 @@ public sealed class RedisImagesCache : IImageCache, IReadyExecutor
if (oldData is not null)
{
var newData = new ImageUrls()
var newData = new ImageUrls
{
Coins =
new()
{
Heads = oldData.Coins.Heads.Length == 1 &&
oldData.Coins.Heads[0].ToString() ==
"https://nadeko-pictures.nyc3.digitaloceanspaces.com/other/coins/heads.png"
? new[] { new Uri("https://cdn.nadeko.bot/coins/heads3.png") }
: oldData.Coins.Heads,
Tails = oldData.Coins.Tails.Length == 1 &&
oldData.Coins.Tails[0].ToString() ==
"https://nadeko-pictures.nyc3.digitaloceanspaces.com/other/coins/tails.png"
Heads =
oldData.Coins.Heads.Length == 1
&& oldData.Coins.Heads[0].ToString()
== "https://nadeko-pictures.nyc3.digitaloceanspaces.com/other/coins/heads.png"
? new[] { new Uri("https://cdn.nadeko.bot/coins/heads3.png") }
: oldData.Coins.Heads,
Tails = oldData.Coins.Tails.Length == 1
&& oldData.Coins.Tails[0].ToString()
== "https://nadeko-pictures.nyc3.digitaloceanspaces.com/other/coins/tails.png"
? new[] { new Uri("https://cdn.nadeko.bot/coins/tails3.png") }
: oldData.Coins.Tails,
: oldData.Coins.Tails
},
Dice = oldData.Dice.Map(x => x.ToNewCdn()),
Currency = oldData.Currency.Map(x => x.ToNewCdn()),
@@ -128,7 +129,7 @@ public sealed class RedisImagesCache : IImageCache, IReadyExecutor
{
Dot = oldData.Rategirl.Dot.ToNewCdn(), Matrix = oldData.Rategirl.Matrix.ToNewCdn()
},
Rip = new() { Bg = oldData.Rip.Bg.ToNewCdn(), Overlay = oldData.Rip.Overlay.ToNewCdn(), },
Rip = new() { Bg = oldData.Rip.Bg.ToNewCdn(), Overlay = oldData.Rip.Overlay.ToNewCdn() },
Slots = new()
{
Bg = new("https://cdn.nadeko.bot/slots/slots_bg.png"),
@@ -139,8 +140,8 @@ public sealed class RedisImagesCache : IImageCache, IReadyExecutor
"https://cdn.nadeko.bot/slots/4.png", "https://cdn.nadeko.bot/slots/5.png"
}.Map(x => new Uri(x))
},
Xp = new() { Bg = oldData.Xp.Bg.ToNewCdn(), },
Version = 2,
Xp = new() { Bg = oldData.Xp.Bg.ToNewCdn() },
Version = 2
};
File.Move(oldFilePath, backupFilePath, true);
@@ -161,7 +162,6 @@ public sealed class RedisImagesCache : IImageCache, IReadyExecutor
{
ImageUrls = Yaml.Deserializer.Deserialize<ImageUrls>(await File.ReadAllTextAsync(_imagesPath));
foreach (var key in GetAllKeys())
{
switch (key)
{
case ImageKeys.CoinHeads:
@@ -200,7 +200,6 @@ public sealed class RedisImagesCache : IImageCache, IReadyExecutor
default:
throw new ArgumentOutOfRangeException();
}
}
}
private async Task Load(ImageKeys key, Uri uri)
@@ -221,20 +220,17 @@ public sealed class RedisImagesCache : IImageCache, IReadyExecutor
await Db.ListRightPushAsync(GetRedisKey(key), vals);
if (uris.Length != vals.Length)
{
Log.Information("{Loaded}/{Max} URIs for the key '{ImageKey}' have been loaded.\n" +
"Some of the supplied URIs are either unavailable or invalid",
Log.Information(
"{Loaded}/{Max} URIs for the key '{ImageKey}' have been loaded.\n"
+ "Some of the supplied URIs are either unavailable or invalid",
vals.Length,
uris.Length,
key
);
}
key);
}
private async Task<byte[]> GetImageData(Uri uri)
{
if (uri.IsFile)
{
try
{
var bytes = await File.ReadAllBytesAsync(uri.LocalPath);
@@ -245,7 +241,6 @@ public sealed class RedisImagesCache : IImageCache, IReadyExecutor
Log.Warning(ex, "Failed reading image bytes from uri: {Uri}", uri.ToString());
return null;
}
}
try
{
@@ -260,9 +255,7 @@ public sealed class RedisImagesCache : IImageCache, IReadyExecutor
private async Task<bool> AllKeysExist()
{
var tasks = await GetAllKeys()
.Select(x => Db.KeyExistsAsync(GetRedisKey(x)))
.WhenAll();
var tasks = await GetAllKeys().Select(x => Db.KeyExistsAsync(GetRedisKey(x))).WhenAll();
return tasks.All(exist => exist);
}
@@ -278,4 +271,4 @@ public sealed class RedisImagesCache : IImageCache, IReadyExecutor
private RedisKey GetRedisKey(ImageKeys key)
=> _creds.RedisKey() + "_image_" + key;
}
}

View File

@@ -8,9 +8,6 @@ namespace NadekoBot.Services;
public class RedisLocalDataCache : ILocalDataCache
{
private readonly ConnectionMultiplexer _con;
private readonly IBotCredentials _creds;
private const string POKEMON_ABILITIES_FILE = "data/pokemon/pokemon_abilities.json";
private const string POKEMON_LIST_FILE = "data/pokemon/pokemon_list.json";
private const string POKEMON_MAP_PATH = "data/pokemon/name-id_map.json";
@@ -40,6 +37,9 @@ public class RedisLocalDataCache : ILocalDataCache
private init => Set("pokemon_map", value);
}
private readonly ConnectionMultiplexer _con;
private readonly IBotCredentials _creds;
public RedisLocalDataCache(ConnectionMultiplexer con, IBotCredentials creds, DiscordSocketClient client)
{
_con = con;
@@ -49,32 +49,25 @@ public class RedisLocalDataCache : ILocalDataCache
if (shardId == 0)
{
if (!File.Exists(POKEMON_LIST_FILE))
{
Log.Warning($"{POKEMON_LIST_FILE} is missing. Pokemon abilities not loaded");
}
else
{
Pokemons =
JsonConvert.DeserializeObject<Dictionary<string, SearchPokemon>>(File.ReadAllText(POKEMON_LIST_FILE));
}
JsonConvert.DeserializeObject<Dictionary<string, SearchPokemon>>(
File.ReadAllText(POKEMON_LIST_FILE));
if (!File.Exists(POKEMON_ABILITIES_FILE))
{
Log.Warning($"{POKEMON_ABILITIES_FILE} is missing. Pokemon abilities not loaded.");
}
else
{
PokemonAbilities =
JsonConvert.DeserializeObject<Dictionary<string, SearchPokemonAbility>>(
File.ReadAllText(POKEMON_ABILITIES_FILE)
);
}
File.ReadAllText(POKEMON_ABILITIES_FILE));
try
{
TriviaQuestions = JsonConvert.DeserializeObject<TriviaQuestion[]>(File.ReadAllText(QUESTIONS_FILE));
PokemonMap = JsonConvert.DeserializeObject<PokemonNameId[]>(File.ReadAllText(POKEMON_MAP_PATH))
?.ToDictionary(x => x.Id, x => x.Name) ?? new();
?.ToDictionary(x => x.Id, x => x.Name)
?? new();
}
catch (Exception ex)
{
@@ -90,4 +83,4 @@ public class RedisLocalDataCache : ILocalDataCache
private void Set(string key, object obj)
=> _con.GetDatabase().StringSet($"{_creds.RedisKey()}_localdata_{key}", JsonConvert.SerializeObject(obj));
}
}

View File

@@ -1,5 +1,6 @@
#nullable disable
using Grpc.Core;
using Grpc.Net.Client;
using NadekoBot.Common.ModuleBehaviors;
using NadekoBot.Coordinator;
@@ -12,37 +13,26 @@ public class RemoteGrpcCoordinator : ICoordinator, IReadyExecutor
public RemoteGrpcCoordinator(IBotCredentials creds, DiscordSocketClient client)
{
var coordUrl = string.IsNullOrWhiteSpace(creds.CoordinatorUrl)
? "http://localhost:3442"
: creds.CoordinatorUrl;
var channel = Grpc.Net.Client.GrpcChannel.ForAddress(coordUrl);
var coordUrl = string.IsNullOrWhiteSpace(creds.CoordinatorUrl) ? "http://localhost:3442" : creds.CoordinatorUrl;
var channel = GrpcChannel.ForAddress(coordUrl);
_coordClient = new(channel);
_client = client;
}
public bool RestartBot()
{
_coordClient.RestartAllShards(new()
{
});
_coordClient.RestartAllShards(new());
return true;
}
public void Die(bool graceful)
=> _coordClient.Die(new()
{
Graceful = graceful
});
=> _coordClient.Die(new() { Graceful = graceful });
public bool RestartShard(int shardId)
{
_coordClient.RestartShard(new()
{
ShardId = shardId,
});
_coordClient.RestartShard(new() { ShardId = shardId });
return true;
}
@@ -51,15 +41,14 @@ public class RemoteGrpcCoordinator : ICoordinator, IReadyExecutor
{
var res = _coordClient.GetAllStatuses(new());
return res.Statuses
.ToArray()
.Map(s => new ShardStatus()
{
ConnectionState = FromCoordConnState(s.State),
GuildCount = s.GuildCount,
ShardId = s.ShardId,
LastUpdate = s.LastUpdate.ToDateTime(),
});
return res.Statuses.ToArray()
.Map(s => new ShardStatus
{
ConnectionState = FromCoordConnState(s.State),
GuildCount = s.GuildCount,
ShardId = s.ShardId,
LastUpdate = s.LastUpdate.ToDateTime()
});
}
public int GetGuildCount()
@@ -82,18 +71,21 @@ public class RemoteGrpcCoordinator : ICoordinator, IReadyExecutor
try
{
var reply = await _coordClient.HeartbeatAsync(new()
{
State = ToCoordConnState(_client.ConnectionState),
GuildCount = _client.ConnectionState == ConnectionState.Connected ? _client.Guilds.Count : 0,
ShardId = _client.ShardId,
}, deadline: DateTime.UtcNow + TimeSpan.FromSeconds(10));
{
State = ToCoordConnState(_client.ConnectionState),
GuildCount =
_client.ConnectionState == ConnectionState.Connected ? _client.Guilds.Count : 0,
ShardId = _client.ShardId
},
deadline: DateTime.UtcNow + TimeSpan.FromSeconds(10));
gracefulImminent = reply.GracefulImminent;
}
catch (RpcException ex)
{
if (!gracefulImminent)
{
Log.Warning(ex, "Hearbeat failed and graceful shutdown was not expected: {Message}",
Log.Warning(ex,
"Hearbeat failed and graceful shutdown was not expected: {Message}",
ex.Message);
break;
}
@@ -130,4 +122,4 @@ public class RemoteGrpcCoordinator : ICoordinator, IReadyExecutor
ConnState.Connected => ConnectionState.Connected,
_ => ConnectionState.Disconnected
};
}
}

View File

@@ -13,6 +13,7 @@ public class SingleProcessCoordinator : ICoordinator
_creds = creds;
_client = client;
}
public bool RestartBot()
{
if (string.IsNullOrWhiteSpace(_creds.RestartCommand?.Cmd)
@@ -21,7 +22,7 @@ public class SingleProcessCoordinator : ICoordinator
Log.Error("You must set RestartCommand.Cmd and RestartCommand.Args in creds.yml");
return false;
}
Process.Start(_creds.RestartCommand.Cmd, _creds.RestartCommand.Args);
_ = Task.Run(async () =>
{
@@ -40,7 +41,7 @@ public class SingleProcessCoordinator : ICoordinator
public IList<ShardStatus> GetAllShardStatuses()
=> new[]
{
new ShardStatus()
new ShardStatus
{
ConnectionState = _client.ConnectionState,
GuildCount = _client.Guilds.Count,
@@ -54,4 +55,4 @@ public class SingleProcessCoordinator : ICoordinator
public Task Reload()
=> Task.CompletedTask;
}
}

View File

@@ -21,7 +21,7 @@ public class SoundCloudApiService : INService
{
response = await http.GetStringAsync($"https://scapi.nadeko.bot/resolve?url={url}");
}
var responseObj = JsonConvert.DeserializeObject<SoundCloudVideo>(response);
if (responseObj?.Kind != "track")
throw new InvalidOperationException("Url is either not a track, or it doesn't exist.");
@@ -37,12 +37,13 @@ public class SoundCloudApiService : INService
var response = string.Empty;
using (var http = _httpFactory.CreateClient())
{
response = await http.GetStringAsync(new Uri($"https://scapi.nadeko.bot/tracks?q={Uri.EscapeDataString(query)}"));
response = await http.GetStringAsync(
new Uri($"https://scapi.nadeko.bot/tracks?q={Uri.EscapeDataString(query)}"));
}
var responseObj = JsonConvert.DeserializeObject<SoundCloudVideo[]>(response)
.FirstOrDefault(s => s.Streamable is true);
.FirstOrDefault(s => s.Streamable is true);
if (responseObj?.Kind != "track")
throw new InvalidOperationException("Query yielded no results.");
@@ -56,17 +57,22 @@ public class SoundCloudVideo
public long Id { get; set; } = 0;
public SoundCloudUser User { get; set; } = new();
public string Title { get; set; } = string.Empty;
public string FullName => User.Name + " - " + Title;
public string FullName
=> User.Name + " - " + Title;
public bool? Streamable { get; set; } = false;
public int Duration { get; set; }
[JsonProperty("permalink_url")]
public string TrackLink { get; set; } = string.Empty;
[JsonProperty("artwork_url")]
public string ArtworkUrl { get; set; } = string.Empty;
}
public class SoundCloudUser
{
[JsonProperty("username")]
public string Name { get; set; }
}
}

View File

@@ -1,6 +1,6 @@
#nullable disable
using System.Collections.Immutable;
using System.Collections;
using System.Collections.Immutable;
namespace NadekoBot.Services;
@@ -9,11 +9,11 @@ public class StartingGuildsService : IEnumerable<ulong>, INService
private readonly ImmutableList<ulong> _guilds;
public StartingGuildsService(DiscordSocketClient client)
=> this._guilds = client.Guilds.Select(x => x.Id).ToImmutableList();
=> _guilds = client.Guilds.Select(x => x.Id).ToImmutableList();
public IEnumerator<ulong> GetEnumerator() =>
_guilds.GetEnumerator();
public IEnumerator<ulong> GetEnumerator()
=> _guilds.GetEnumerator();
IEnumerator IEnumerable.GetEnumerator() =>
_guilds.GetEnumerator();
}
IEnumerator IEnumerable.GetEnumerator()
=> _guilds.GetEnumerator();
}

View File

@@ -6,30 +6,47 @@ namespace NadekoBot.Services;
public class StatsService : IStatsService, IReadyExecutor, INService, IDisposable
{
public const string BotVersion = "4.0.0";
public string Author
=> "Kwoth#2452";
public string Library
=> "Discord.Net";
public double MessagesPerSecond
=> MessageCounter / GetUptime().TotalSeconds;
public long TextChannels
=> Interlocked.Read(ref _textChannels);
public long VoiceChannels
=> Interlocked.Read(ref _voiceChannels);
public long MessageCounter
=> Interlocked.Read(ref _messageCounter);
public long CommandsRan
=> Interlocked.Read(ref _commandsRan);
private readonly Process _currentProcess = Process.GetCurrentProcess();
private readonly DiscordSocketClient _client;
private readonly IBotCredentials _creds;
private readonly DateTime _started;
public const string BotVersion = "4.0.0";
public string Author => "Kwoth#2452";
public string Library => "Discord.Net";
public double MessagesPerSecond => MessageCounter / GetUptime().TotalSeconds;
private long _textChannels;
public long TextChannels => Interlocked.Read(ref _textChannels);
private long _voiceChannels;
public long VoiceChannels => Interlocked.Read(ref _voiceChannels);
private long _messageCounter;
public long MessageCounter => Interlocked.Read(ref _messageCounter);
private long _commandsRan;
public long CommandsRan => Interlocked.Read(ref _commandsRan);
private readonly Timer _botlistTimer;
private readonly IHttpClientFactory _httpFactory;
public StatsService(DiscordSocketClient client, CommandHandler cmdHandler,
IBotCredentials creds, IHttpClientFactory factory)
public StatsService(
DiscordSocketClient client,
CommandHandler cmdHandler,
IBotCredentials creds,
IHttpClientFactory factory)
{
_client = client;
_creds = creds;
@@ -116,34 +133,39 @@ public class StatsService : IStatsService, IReadyExecutor, INService, IDisposabl
};
_botlistTimer = new(async state =>
{
if (string.IsNullOrWhiteSpace(_creds.BotListToken))
return;
try
{
using var http = _httpFactory.CreateClient();
using var content = new FormUrlEncodedContent(
new Dictionary<string, string> {
{ "shard_count", _creds.TotalShards.ToString()},
if (string.IsNullOrWhiteSpace(_creds.BotListToken))
return;
try
{
using var http = _httpFactory.CreateClient();
using var content = new FormUrlEncodedContent(new Dictionary<string, string>
{
{ "shard_count", _creds.TotalShards.ToString() },
{ "shard_id", client.ShardId.ToString() },
{ "server_count", client.Guilds.Count().ToString() }
});
content.Headers.Clear();
content.Headers.Add("Content-Type", "application/x-www-form-urlencoded");
http.DefaultRequestHeaders.Add("Authorization", _creds.BotListToken);
content.Headers.Clear();
content.Headers.Add("Content-Type", "application/x-www-form-urlencoded");
http.DefaultRequestHeaders.Add("Authorization", _creds.BotListToken);
using (await http.PostAsync(new Uri($"https://discordbots.org/api/bots/{client.CurrentUser.Id}/stats"), content)) { }
}
catch (Exception ex)
{
Log.Error(ex, "Error ");
// ignored
}
}, null, TimeSpan.FromMinutes(5), TimeSpan.FromHours(1));
using (await http.PostAsync(
new Uri($"https://discordbots.org/api/bots/{client.CurrentUser.Id}/stats"),
content)) { }
}
catch (Exception ex)
{
Log.Error(ex, "Error ");
// ignored
}
},
null,
TimeSpan.FromMinutes(5),
TimeSpan.FromHours(1));
}
public TimeSpan GetUptime() =>
DateTime.UtcNow - _started;
public TimeSpan GetUptime()
=> DateTime.UtcNow - _started;
public string GetUptimeString(string separator = ", ")
{
@@ -170,4 +192,4 @@ public class StatsService : IStatsService, IReadyExecutor, INService, IDisposabl
_currentProcess.Dispose();
GC.SuppressFinalize(this);
}
}
}

View File

@@ -26,11 +26,11 @@ public class YtdlOperation
RedirectStandardOutput = true,
StandardOutputEncoding = Encoding.UTF8,
StandardErrorEncoding = Encoding.UTF8,
CreateNoWindow = true,
},
CreateNoWindow = true
}
};
}
public async Task<string> GetDataAsync(params string[] args)
{
try
@@ -49,13 +49,12 @@ public class YtdlOperation
}
catch (Win32Exception)
{
Log.Error("youtube-dl is likely not installed. " +
"Please install it before running the command again");
Log.Error("youtube-dl is likely not installed. " + "Please install it before running the command again");
return default;
}
catch (Exception ex)
{
Log.Error(ex , "Exception running youtube-dl: {ErrorMessage}", ex.Message);
Log.Error(ex, "Exception running youtube-dl: {ErrorMessage}", ex.Message);
return default;
}
}
@@ -66,9 +65,9 @@ public class YtdlOperation
Log.Debug($"Executing {process.StartInfo.FileName} {process.StartInfo.Arguments}");
process.Start();
string line;
while((line = await process.StandardOutput.ReadLineAsync()) != null)
while ((line = await process.StandardOutput.ReadLineAsync()) != null)
yield return line;
}
}
}

View File

@@ -1,7 +1,7 @@
#nullable disable
using System.Text;
using Serilog.Events;
using Serilog.Sinks.SystemConsole.Themes;
using System.Text;
namespace NadekoBot.Services;
@@ -9,23 +9,23 @@ public static class LogSetup
{
public static void SetupLogger(object source)
{
Log.Logger = new LoggerConfiguration()
.MinimumLevel.Override("Microsoft", LogEventLevel.Information)
.MinimumLevel.Override("System", LogEventLevel.Information)
.MinimumLevel.Override("Microsoft.AspNetCore", LogEventLevel.Warning)
.Enrich.FromLogContext()
.WriteTo.Console(LogEventLevel.Information,
theme: GetTheme(),
outputTemplate: "[{Timestamp:HH:mm:ss} {Level:u3}] | #{LogSource} | {Message:lj}{NewLine}{Exception}")
.Enrich.WithProperty("LogSource", source)
.CreateLogger();
System.Console.OutputEncoding = Encoding.UTF8;
Log.Logger = new LoggerConfiguration().MinimumLevel.Override("Microsoft", LogEventLevel.Information)
.MinimumLevel.Override("System", LogEventLevel.Information)
.MinimumLevel.Override("Microsoft.AspNetCore", LogEventLevel.Warning)
.Enrich.FromLogContext()
.WriteTo.Console(LogEventLevel.Information,
theme: GetTheme(),
outputTemplate:
"[{Timestamp:HH:mm:ss} {Level:u3}] | #{LogSource} | {Message:lj}{NewLine}{Exception}")
.Enrich.WithProperty("LogSource", source)
.CreateLogger();
Console.OutputEncoding = Encoding.UTF8;
}
private static ConsoleTheme GetTheme()
{
if(Environment.OSVersion.Platform == PlatformID.Unix)
if (Environment.OSVersion.Platform == PlatformID.Unix)
return AnsiConsoleTheme.Code;
#if DEBUG
return AnsiConsoleTheme.Code;
@@ -33,4 +33,4 @@ public static class LogSetup
return ConsoleTheme.None;
#endif
}
}
}

View File

@@ -5,15 +5,14 @@ using SixLabors.ImageSharp.PixelFormats;
namespace NadekoBot.Services;
/// <summary>
/// Settings service for bot-wide configuration.
/// Settings service for bot-wide configuration.
/// </summary>
public sealed class BotConfigService : ConfigServiceBase<BotConfig>
{
public override string Name { get; } = "bot";
private const string FilePath = "data/bot.yml";
private static readonly TypedKey<BotConfig> changeKey = new("config.bot.updated");
public override string Name { get; } = "bot";
public BotConfigService(IConfigSeria serializer, IPubSub pubSub)
: base(FilePath, serializer, pubSub, changeKey)
{
@@ -25,15 +24,12 @@ public sealed class BotConfigService : ConfigServiceBase<BotConfig>
AddParsedProp("console.type", bs => bs.ConsoleOutputType, Enum.TryParse, ConfigPrinters.ToString);
AddParsedProp("locale", bs => bs.DefaultLocale, ConfigParsers.Culture, ConfigPrinters.Culture);
AddParsedProp("prefix", bs => bs.Prefix, ConfigParsers.String, ConfigPrinters.ToString);
Migrate();
}
private void Migrate()
{
if (data.Version < 2)
{
ModifyConfig(c => c.Version = 2);
}
if (data.Version < 2) ModifyConfig(c => c.Version = 2);
}
}
}

View File

@@ -1,16 +1,16 @@
#nullable disable
using System.Globalization;
using SixLabors.ImageSharp.PixelFormats;
using System.Globalization;
namespace NadekoBot.Services;
/// <summary>
/// Custom setting value parsers for types which don't have them by default
/// Custom setting value parsers for types which don't have them by default
/// </summary>
public static class ConfigParsers
{
/// <summary>
/// Default string parser. Passes input to output and returns true.
/// Default string parser. Passes input to output and returns true.
/// </summary>
public static bool String(string input, out string output)
{
@@ -42,5 +42,5 @@ public static class ConfigPrinters
=> culture.Name;
public static string Color(Rgba32 color)
=> ((uint) ((color.B << 0) | (color.G << 8) | (color.R << 16))).ToString("X6");
}
=> ((uint)((color.B << 0) | (color.G << 8) | (color.R << 16))).ToString("X6");
}

View File

@@ -1,37 +1,45 @@
using NadekoBot.Common.Configs;
using NadekoBot.Common.Yml;
using System.Linq.Expressions;
using System.Reflection;
using NadekoBot.Common.Yml;
using NadekoBot.Common.Configs;
namespace NadekoBot.Services;
/// <summary>
/// Base service for all settings services
/// Base service for all settings services
/// </summary>
/// <typeparam name="TSettings">Type of the settings</typeparam>
public abstract class ConfigServiceBase<TSettings> : IConfigService
public abstract class ConfigServiceBase<TSettings> : IConfigService
where TSettings : ICloneable<TSettings>, new()
{
// todo future config arrays are not copied - they're not protected from mutations
public TSettings Data
=> data.Clone();
public abstract string Name { get; }
protected readonly string _filePath;
protected readonly IConfigSeria _serializer;
protected readonly IPubSub _pubSub;
private readonly TypedKey<TSettings> _changeKey;
protected TSettings data;
// todo future config arrays are not copied - they're not protected from mutations
public TSettings Data => data.Clone();
public abstract string Name { get; }
private readonly Dictionary<string, Func<TSettings, string, bool>> _propSetters = new();
private readonly Dictionary<string, Func<object>> _propSelectors = new();
private readonly Dictionary<string, Func<object, string>> _propPrinters = new();
private readonly Dictionary<string, string> _propComments = new();
/// <summary>
/// Initialized an instance of <see cref="ConfigServiceBase{TSettings}"/>
/// Initialized an instance of <see cref="ConfigServiceBase{TSettings}" />
/// </summary>
/// <param name="filePath">Path to the file where the settings are serialized/deserialized to and from</param>
/// <param name="serializer">Serializer which will be used</param>
/// <param name="pubSub">Pubsub implementation for signaling when settings are updated</param>
/// <param name="changeKey">Key used to signal changed event</param>
protected ConfigServiceBase(string filePath, IConfigSeria serializer, IPubSub pubSub,
protected ConfigServiceBase(
string filePath,
IConfigSeria serializer,
IPubSub pubSub,
TypedKey<TSettings> changeKey)
{
_filePath = filePath;
@@ -55,7 +63,7 @@ public abstract class ConfigServiceBase<TSettings> : IConfigService
}
/// <summary>
/// Loads data from disk. If file doesn't exist, it will be created with default values
/// Loads data from disk. If file doesn't exist, it will be created with default values
/// </summary>
protected void Load()
{
@@ -70,7 +78,7 @@ public abstract class ConfigServiceBase<TSettings> : IConfigService
}
/// <summary>
/// Loads new data and publishes the new state
/// Loads new data and publishes the new state
/// </summary>
public void Reload()
{
@@ -79,26 +87,20 @@ public abstract class ConfigServiceBase<TSettings> : IConfigService
}
/// <summary>
/// Doesn't do anything by default. This method will be executed after
/// <see cref="data"/> is reloaded from <see cref="_filePath"/> or new data is recieved
/// from the publish event
/// Doesn't do anything by default. This method will be executed after
/// <see cref="data" /> is reloaded from <see cref="_filePath" /> or new data is recieved
/// from the publish event
/// </summary>
protected virtual void OnStateUpdate()
{
}
private void Save()
{
var strData = _serializer.Serialize(data);
File.WriteAllText(_filePath, strData);
}
private readonly Dictionary<string, Func<TSettings, string, bool>> _propSetters = new();
private readonly Dictionary<string, Func<object>> _propSelectors = new();
private readonly Dictionary<string, Func<object, string>> _propPrinters = new();
private readonly Dictionary<string, string> _propComments = new();
protected void AddParsedProp<TProp>(
string key,
Expression<Func<TSettings, TProp>> selector,
@@ -108,19 +110,21 @@ public abstract class ConfigServiceBase<TSettings> : IConfigService
{
checker ??= _ => true;
key = key.ToLowerInvariant();
_propPrinters[key] = obj => printer((TProp)obj);
_propPrinters[key] = obj => printer((TProp)obj);
_propSelectors[key] = () => selector.Compile()(data);
_propSetters[key] = Magic(selector, parser, checker);
_propComments[key] = ((MemberExpression)selector.Body).Member.GetCustomAttribute<CommentAttribute>()?.Comment;
}
private Func<TSettings, string, bool> Magic<TProp>(Expression<Func<TSettings, TProp>> selector,
SettingParser<TProp> parser, Func<TProp, bool> checker)
private Func<TSettings, string, bool> Magic<TProp>(
Expression<Func<TSettings, TProp>> selector,
SettingParser<TProp> parser,
Func<TProp, bool> checker)
=> (target, input) =>
{
if (!parser(input, out var value))
return false;
if (!checker(value))
return false;
@@ -129,22 +133,22 @@ public abstract class ConfigServiceBase<TSettings> : IConfigService
var prop = (PropertyInfo)expr.Member;
var expressions = new List<MemberExpression>();
while (true)
{
expr = expr.Expression as MemberExpression;
if (expr is null)
break;
expressions.Add(expr);
}
foreach (var memberExpression in expressions.AsEnumerable().Reverse())
{
var localProp = (PropertyInfo) memberExpression.Member;
var localProp = (PropertyInfo)memberExpression.Member;
targetObject = localProp.GetValue(targetObject);
}
prop!.SetValue(targetObject, value, null);
return true;
};
@@ -155,8 +159,7 @@ public abstract class ConfigServiceBase<TSettings> : IConfigService
public string GetSetting(string prop)
{
prop = prop.ToLowerInvariant();
if (!_propSelectors.TryGetValue(prop, out var selector) ||
!_propPrinters.TryGetValue(prop, out var printer))
if (!_propSelectors.TryGetValue(prop, out var selector) || !_propPrinters.TryGetValue(prop, out var printer))
return default;
return printer(selector());
@@ -171,8 +174,7 @@ public abstract class ConfigServiceBase<TSettings> : IConfigService
}
private bool SetProperty(TSettings target, string key, string value)
=> _propSetters.TryGetValue(key.ToLowerInvariant(), out var magic)
&& magic(target, value);
=> _propSetters.TryGetValue(key.ToLowerInvariant(), out var magic) && magic(target, value);
public bool SetSetting(string prop, string newValue)
{
@@ -181,8 +183,8 @@ public abstract class ConfigServiceBase<TSettings> : IConfigService
{
success = SetProperty(bs, prop, newValue);
});
if(success)
if (success)
PublishChange();
return success;
@@ -196,4 +198,4 @@ public abstract class ConfigServiceBase<TSettings> : IConfigService
Save();
PublishChange();
}
}
}

View File

@@ -4,4 +4,4 @@ namespace NadekoBot.Services;
public interface IConfigMigrator
{
public void EnsureMigrated();
}
}

View File

@@ -2,42 +2,45 @@
namespace NadekoBot.Services;
/// <summary>
/// Interface that all services which deal with configs should implement
/// Interface that all services which deal with configs should implement
/// </summary>
public interface IConfigService
{
/// <summary>
/// Name of the config
/// Name of the config
/// </summary>
public string Name { get; }
/// <summary>
/// Loads new data and publishes the new state
/// Loads new data and publishes the new state
/// </summary>
void Reload();
/// <summary>
/// Gets the list of props you can set
/// Gets the list of props you can set
/// </summary>
/// <returns>List of props</returns>
IReadOnlyList<string> GetSettableProps();
/// <summary>
/// Gets the value of the specified property
/// Gets the value of the specified property
/// </summary>
/// <param name="prop">Prop name</param>
/// <returns>Value of the prop</returns>
string GetSetting(string prop);
/// <summary>
/// Gets the value of the specified property
/// Gets the value of the specified property
/// </summary>
/// <param name="prop">Prop name</param>
/// <returns>Value of the prop</returns>
string GetComment(string prop);
/// <summary>
/// Sets the value of the specified property
/// Sets the value of the specified property
/// </summary>
/// <param name="prop">Property to set</param>
/// <param name="newValue">Value to set the property to</param>
/// <returns>Success</returns>
bool SetSetting(string prop, string newValue);
}
}

View File

@@ -2,7 +2,7 @@
namespace NadekoBot.Services;
/// <summary>
/// Delegate which describes a parser which can convert string input into given data type
/// Delegate which describes a parser which can convert string input into given data type
/// </summary>
/// <typeparam name="TData">Data type to convert string to</typeparam>
public delegate bool SettingParser<TData>(string input, out TData output);
public delegate bool SettingParser<TData>(string input, out TData output);

View File

@@ -5,4 +5,4 @@ public static class StandardConversions
{
public static double CelsiusToFahrenheit(double cel)
=> (cel * 1.8f) + 32;
}
}

View File

@@ -4,7 +4,7 @@ using System.Globalization;
namespace NadekoBot.Services;
/// <summary>
/// Defines methods to retrieve and reload bot strings
/// Defines methods to retrieve and reload bot strings
/// </summary>
public interface IBotStrings
{
@@ -13,4 +13,4 @@ public interface IBotStrings
void Reload();
CommandStrings GetCommandStrings(string commandName, ulong? guildId = null);
CommandStrings GetCommandStrings(string commandName, CultureInfo cultureInfo);
}
}

View File

@@ -2,27 +2,27 @@
namespace NadekoBot.Services;
/// <summary>
/// Implemented by classes which provide localized strings in their own ways
/// Implemented by classes which provide localized strings in their own ways
/// </summary>
public interface IBotStringsProvider
{
/// <summary>
/// Gets localized string
/// Gets localized string
/// </summary>
/// <param name="localeName">Language name</param>
/// <param name="key">String key</param>
/// <returns>Localized string</returns>
string GetText(string localeName, string key);
/// <summary>
/// Reloads string cache
/// Reloads string cache
/// </summary>
void Reload();
/// <summary>
/// Gets command arg examples and description
/// Gets command arg examples and description
/// </summary>
/// <param name="localeName">Language name</param>
/// <param name="commandName">Command name</param>
CommandStrings GetCommandStrings(string localeName, string commandName);
}
}

View File

@@ -2,15 +2,15 @@
namespace NadekoBot.Services;
/// <summary>
/// Basic interface used for classes implementing strings loading mechanism
/// Basic interface used for classes implementing strings loading mechanism
/// </summary>
public interface IStringsSource
{
/// <summary>
/// Gets all response strings
/// Gets all response strings
/// </summary>
/// <returns>Dictionary(localename, Dictionary(key, response))</returns>
Dictionary<string, Dictionary<string, string>> GetResponseStrings();
Dictionary<string, Dictionary<string, CommandStrings>> GetCommandStrings();
}
}

View File

@@ -7,9 +7,10 @@ namespace NadekoBot.Services;
public class BotStrings : IBotStrings
{
/// <summary>
/// Used as failsafe in case response key doesn't exist in the selected or default language.
/// Used as failsafe in case response key doesn't exist in the selected or default language.
/// </summary>
private readonly CultureInfo _usCultureInfo = new("en-US");
private readonly ILocalization _localization;
private readonly IBotStringsProvider _stringsProvider;
@@ -31,15 +32,16 @@ public class BotStrings : IBotStrings
if (string.IsNullOrWhiteSpace(text))
{
Log.Warning("'{Key}' key is missing from '{LanguageName}' response strings. You may ignore this message", key, cultureInfo.Name);
Log.Warning("'{Key}' key is missing from '{LanguageName}' response strings. You may ignore this message",
key,
cultureInfo.Name);
text = GetString(key, _usCultureInfo) ?? $"Error: dkey {key} not found!";
if (string.IsNullOrWhiteSpace(text))
{
return
$"I can't tell you if the command is executed, because there was an error printing out the response." +
$" Key '{key}' is missing from resources. You may ignore this message.";
}
"I can't tell you if the command is executed, because there was an error printing out the response."
+ $" Key '{key}' is missing from resources. You may ignore this message.";
}
return text;
}
@@ -51,39 +53,38 @@ public class BotStrings : IBotStrings
}
catch (FormatException)
{
Log.Warning(" Key '{Key}' is not properly formatted in '{LanguageName}' response strings. Please report this", key, cultureInfo.Name);
Log.Warning(
" Key '{Key}' is not properly formatted in '{LanguageName}' response strings. Please report this",
key,
cultureInfo.Name);
if (cultureInfo.Name != _usCultureInfo.Name)
return GetText(key, _usCultureInfo, data);
return
$"I can't tell you if the command is executed, because there was an error printing out the response.\n" +
$"Key '{key}' is not properly formatted. Please report this.";
"I can't tell you if the command is executed, because there was an error printing out the response.\n"
+ $"Key '{key}' is not properly formatted. Please report this.";
}
}
public CommandStrings GetCommandStrings(string commandName, ulong? guildId = null)
=> GetCommandStrings(commandName, _localization.GetCultureInfo(guildId));
public CommandStrings GetCommandStrings(string commandName, CultureInfo cultureInfo)
{
var cmdStrings = _stringsProvider.GetCommandStrings(cultureInfo.Name, commandName);
var cmdStrings = _stringsProvider.GetCommandStrings(cultureInfo.Name, commandName);
if (cmdStrings is null)
{
if (cultureInfo.Name == _usCultureInfo.Name)
{
Log.Warning("'{CommandName}' doesn't exist in 'en-US' command strings. Please report this",
commandName);
return new()
{
Args = new[] {""},
Desc = "?"
};
return new() { Args = new[] { "" }, Desc = "?" };
}
// Log.Warning(@"'{CommandName}' command strings don't exist in '{LanguageName}' culture.
// This message is safe to ignore, however you can ask in Nadeko support server how you can contribute command translations",
// commandName, cultureInfo.Name);
return GetCommandStrings(commandName, _usCultureInfo);
}
@@ -98,6 +99,7 @@ public class CommandStrings
{
[YamlMember(Alias = "desc")]
public string Desc { get; set; }
[YamlMember(Alias = "args")]
public string[] Args { get; set; }
}
}

View File

@@ -6,20 +6,17 @@ public class LocalBotStringsProvider : IBotStringsProvider
private readonly IStringsSource _source;
private IReadOnlyDictionary<string, Dictionary<string, string>> responseStrings;
private IReadOnlyDictionary<string, Dictionary<string, CommandStrings>> commandStrings;
public LocalBotStringsProvider(IStringsSource source)
{
_source = source;
Reload();
}
public string GetText(string localeName, string key)
{
if (responseStrings.TryGetValue(localeName, out var langStrings)
&& langStrings.TryGetValue(key, out var text))
{
if (responseStrings.TryGetValue(localeName, out var langStrings) && langStrings.TryGetValue(key, out var text))
return text;
}
return null;
}
@@ -34,10 +31,8 @@ public class LocalBotStringsProvider : IBotStringsProvider
{
if (commandStrings.TryGetValue(localeName, out var langStrings)
&& langStrings.TryGetValue(commandName, out var strings))
{
return strings;
}
return null;
}
}
}

View File

@@ -5,25 +5,25 @@ using YamlDotNet.Serialization;
namespace NadekoBot.Services;
/// <summary>
/// Loads strings from the local default filepath <see cref="_responsesPath"/>
/// Loads strings from the local default filepath <see cref="_responsesPath" />
/// </summary>
public class LocalFileStringsSource : IStringsSource
{
private readonly string _responsesPath = "data/strings/responses";
private readonly string _commandsPath = "data/strings/commands";
public LocalFileStringsSource(string responsesPath = "data/strings/responses",
public LocalFileStringsSource(
string responsesPath = "data/strings/responses",
string commandsPath = "data/strings/commands")
{
_responsesPath = responsesPath;
_commandsPath = commandsPath;
}
public Dictionary<string, Dictionary<string, string>> GetResponseStrings()
{
var outputDict = new Dictionary<string, Dictionary<string, string>>();
foreach (var file in Directory.GetFiles(_responsesPath))
{
try
{
var langDict = JsonConvert.DeserializeObject<Dictionary<string, string>>(File.ReadAllText(file));
@@ -34,19 +34,16 @@ public class LocalFileStringsSource : IStringsSource
{
Log.Error(ex, "Error loading {FileName} response strings: {ErrorMessage}", file, ex.Message);
}
}
return outputDict;
}
public Dictionary<string, Dictionary<string, CommandStrings>> GetCommandStrings()
{
var deserializer = new DeserializerBuilder()
.Build();
var deserializer = new DeserializerBuilder().Build();
var outputDict = new Dictionary<string, Dictionary<string, CommandStrings>>();
foreach (var file in Directory.GetFiles(_commandsPath))
{
try
{
var text = File.ReadAllText(file);
@@ -58,11 +55,10 @@ public class LocalFileStringsSource : IStringsSource
{
Log.Error(ex, "Error loading {FileName} command strings: {ErrorMessage}", file, ex.Message);
}
}
return outputDict;
}
private static string GetLocaleName(string fileName)
{
fileName = Path.GetFileName(fileName);
@@ -70,4 +66,4 @@ public class LocalFileStringsSource : IStringsSource
var secondDotIndex = fileName.LastIndexOf('.');
return fileName.Substring(dotIndex, secondDotIndex - dotIndex);
}
}
}

View File

@@ -1,12 +1,12 @@
#nullable disable
using System.Web;
using StackExchange.Redis;
using System.Web;
namespace NadekoBot.Services;
/// <summary>
/// Uses <see cref="IStringsSource"/> to load strings into redis hash (only on Shard 0)
/// and retrieves them from redis via <see cref="GetText"/>
/// Uses <see cref="IStringsSource" /> to load strings into redis hash (only on Shard 0)
/// and retrieves them from redis via <see cref="GetText" />
/// </summary>
public class RedisBotStringsProvider : IBotStringsProvider
{
@@ -14,14 +14,17 @@ public class RedisBotStringsProvider : IBotStringsProvider
private readonly IStringsSource _source;
private readonly IBotCredentials _creds;
public RedisBotStringsProvider(ConnectionMultiplexer redis, DiscordSocketClient discordClient,
IStringsSource source, IBotCredentials creds)
public RedisBotStringsProvider(
ConnectionMultiplexer redis,
DiscordSocketClient discordClient,
IStringsSource source,
IBotCredentials creds)
{
_redis = redis;
_source = source;
_creds = creds;
if(discordClient.ShardId == 0)
if (discordClient.ShardId == 0)
Reload();
}
@@ -33,20 +36,18 @@ public class RedisBotStringsProvider : IBotStringsProvider
public CommandStrings GetCommandStrings(string localeName, string commandName)
{
string argsStr = _redis.GetDatabase().HashGet($"{_creds.RedisKey()}:commands:{localeName}", $"{commandName}::args");
string argsStr = _redis.GetDatabase()
.HashGet($"{_creds.RedisKey()}:commands:{localeName}", $"{commandName}::args");
if (argsStr == default)
return null;
var descStr = _redis.GetDatabase().HashGet($"{_creds.RedisKey()}:commands:{localeName}", $"{commandName}::desc");
var descStr = _redis.GetDatabase()
.HashGet($"{_creds.RedisKey()}:commands:{localeName}", $"{commandName}::desc");
if (descStr == default)
return null;
var args = Array.ConvertAll(argsStr.Split('&'), HttpUtility.UrlDecode);
return new()
{
Args = args,
Desc = descStr
};
return new() { Args = args, Desc = descStr };
}
public void Reload()
@@ -54,23 +55,20 @@ public class RedisBotStringsProvider : IBotStringsProvider
var redisDb = _redis.GetDatabase();
foreach (var (localeName, localeStrings) in _source.GetResponseStrings())
{
var hashFields = localeStrings
.Select(x => new HashEntry(x.Key, x.Value))
.ToArray();
var hashFields = localeStrings.Select(x => new HashEntry(x.Key, x.Value)).ToArray();
redisDb.HashSet($"{_creds.RedisKey()}:responses:{localeName}", hashFields);
}
foreach (var (localeName, localeStrings) in _source.GetCommandStrings())
{
var hashFields = localeStrings
.Select(x => new HashEntry($"{x.Key}::args",
string.Join('&', Array.ConvertAll(x.Value.Args, HttpUtility.UrlEncode))))
.Concat(localeStrings
.Select(x => new HashEntry($"{x.Key}::desc", x.Value.Desc)))
.ToArray();
.Select(x => new HashEntry($"{x.Key}::args",
string.Join('&', Array.ConvertAll(x.Value.Args, HttpUtility.UrlEncode))))
.Concat(localeStrings.Select(x => new HashEntry($"{x.Key}::desc", x.Value.Desc)))
.ToArray();
redisDb.HashSet($"{_creds.RedisKey()}:commands:{localeName}", hashFields);
}
}
}
}