mirror of
https://gitlab.com/Kwoth/nadekobot.git
synced 2025-09-11 09:48:26 -04:00
Applied codestyle to all .cs files
This commit is contained in:
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
@@ -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();
|
||||
}
|
@@ -9,4 +9,4 @@ public interface IBehaviourExecutor
|
||||
Task RunLateExecutorsAsync(SocketGuild guild, IUserMessage usrMsg);
|
||||
|
||||
public void Initialize();
|
||||
}
|
||||
}
|
@@ -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; }
|
||||
}
|
||||
}
|
@@ -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);
|
||||
}
|
@@ -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();
|
||||
}
|
||||
}
|
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
@@ -27,7 +27,7 @@ public struct ImageResult
|
||||
|
||||
public ImageResult(Result.ImageData image, string link)
|
||||
{
|
||||
this.Image = image;
|
||||
this.Link = link;
|
||||
Image = image;
|
||||
Link = link;
|
||||
}
|
||||
}
|
||||
}
|
@@ -26,4 +26,4 @@ public interface IImageCache
|
||||
byte[] GetCard(string key);
|
||||
|
||||
Task Reload();
|
||||
}
|
||||
}
|
@@ -10,4 +10,4 @@ public interface ILocalDataCache
|
||||
IReadOnlyDictionary<string, SearchPokemonAbility> PokemonAbilities { get; }
|
||||
IReadOnlyDictionary<int, string> PokemonMap { get; }
|
||||
TriviaQuestion[] TriviaQuestions { get; }
|
||||
}
|
||||
}
|
@@ -16,4 +16,4 @@ public interface ILocalization
|
||||
void SetDefaultCulture(CultureInfo ci);
|
||||
void SetGuildCulture(IGuild guild, CultureInfo ci);
|
||||
void SetGuildCulture(ulong guildId, CultureInfo ci);
|
||||
}
|
||||
}
|
@@ -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
|
||||
{
|
||||
}
|
||||
}
|
@@ -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();
|
||||
}
|
||||
}
|
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -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;
|
||||
}
|
@@ -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);
|
||||
}
|
||||
}
|
@@ -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; }
|
||||
}
|
||||
}
|
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
@@ -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));
|
||||
}
|
||||
}
|
||||
}
|
@@ -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));
|
||||
}
|
||||
}
|
@@ -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;
|
||||
}
|
||||
}
|
@@ -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));
|
||||
}
|
||||
}
|
@@ -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
|
||||
};
|
||||
}
|
||||
}
|
@@ -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;
|
||||
}
|
||||
}
|
@@ -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; }
|
||||
}
|
||||
}
|
@@ -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();
|
||||
}
|
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
@@ -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");
|
||||
}
|
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
@@ -4,4 +4,4 @@ namespace NadekoBot.Services;
|
||||
public interface IConfigMigrator
|
||||
{
|
||||
public void EnsureMigrated();
|
||||
}
|
||||
}
|
@@ -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);
|
||||
}
|
||||
}
|
@@ -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);
|
@@ -5,4 +5,4 @@ public static class StandardConversions
|
||||
{
|
||||
public static double CelsiusToFahrenheit(double cel)
|
||||
=> (cel * 1.8f) + 32;
|
||||
}
|
||||
}
|
@@ -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);
|
||||
}
|
||||
}
|
@@ -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);
|
||||
}
|
||||
}
|
@@ -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();
|
||||
}
|
||||
}
|
@@ -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; }
|
||||
}
|
||||
}
|
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user