Global usings and file scoped namespaces

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

View File

@@ -4,374 +4,369 @@ using Discord.Net;
using Discord.WebSocket;
using NadekoBot.Common.Collections;
using NadekoBot.Extensions;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using NadekoBot.Common.Configs;
using NadekoBot.Db;
using Serilog;
namespace NadekoBot.Services
namespace NadekoBot.Services;
public class CommandHandler : INService
{
public class CommandHandler : INService
public 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;
public event Func<IUserMessage, CommandInfo, Task> CommandExecuted = delegate { return Task.CompletedTask; };
public event Func<CommandInfo, ITextChannel, string, Task> CommandErrored = delegate { return Task.CompletedTask; };
public event Func<IUserMessage, Task> OnMessageNoTrigger = delegate { return Task.CompletedTask; };
//userid/msg count
public ConcurrentDictionary<ulong, uint> UserMessagesSent { get; } = new ConcurrentDictionary<ulong, uint>();
public ConcurrentHashSet<ulong> UsersOnShortCooldown { get; } = new ConcurrentHashSet<ulong>();
private readonly Timer _clearUsersOnShortCooldown;
public CommandHandler(
DiscordSocketClient client,
DbService db,
CommandService commandService,
BotConfigService bss,
Bot bot,
IBehaviourExecutor behaviourExecutor,
IServiceProvider services)
{
public const int GlobalCommandsCooldown = 750;
_client = client;
_commandService = commandService;
_bss = bss;
_bot = bot;
_behaviourExecutor = behaviourExecutor;
_db = db;
_services = services;
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;
public event Func<IUserMessage, CommandInfo, Task> CommandExecuted = delegate { return Task.CompletedTask; };
public event Func<CommandInfo, ITextChannel, string, Task> CommandErrored = delegate { return Task.CompletedTask; };
public event Func<IUserMessage, Task> OnMessageNoTrigger = delegate { return Task.CompletedTask; };
//userid/msg count
public ConcurrentDictionary<ulong, uint> UserMessagesSent { get; } = new ConcurrentDictionary<ulong, uint>();
public ConcurrentHashSet<ulong> UsersOnShortCooldown { get; } = new ConcurrentHashSet<ulong>();
private readonly Timer _clearUsersOnShortCooldown;
public CommandHandler(
DiscordSocketClient client,
DbService db,
CommandService commandService,
BotConfigService bss,
Bot bot,
IBehaviourExecutor behaviourExecutor,
IServiceProvider services)
_clearUsersOnShortCooldown = new Timer(_ =>
{
_client = client;
_commandService = commandService;
_bss = bss;
_bot = bot;
_behaviourExecutor = behaviourExecutor;
_db = db;
_services = services;
_clearUsersOnShortCooldown = new Timer(_ =>
{
UsersOnShortCooldown.Clear();
}, null, GlobalCommandsCooldown, GlobalCommandsCooldown);
UsersOnShortCooldown.Clear();
}, null, GlobalCommandsCooldown, GlobalCommandsCooldown);
_prefixes = bot.AllGuildConfigs
.Where(x => x.Prefix != null)
.ToDictionary(x => x.GuildId, x => x.Prefix)
.ToConcurrent();
_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(ulong? id = null)
{
if (id is null || !_prefixes.TryGetValue(id.Value, out var prefix))
return _bss.Data.Prefix;
return prefix;
}
public string SetDefaultPrefix(string prefix)
{
if (string.IsNullOrWhiteSpace(prefix))
throw new ArgumentNullException(nameof(prefix));
_bss.ModifyConfig(bs =>
{
bs.Prefix = prefix;
});
return prefix;
}
public string SetPrefix(IGuild guild, string prefix)
{
if (string.IsNullOrWhiteSpace(prefix))
throw new ArgumentNullException(nameof(prefix));
if (guild is null)
throw new ArgumentNullException(nameof(guild));
using (var uow = _db.GetDbContext())
{
var gc = uow.GuildConfigsForId(guild.Id, set => set);
gc.Prefix = prefix;
uow.SaveChanges();
}
public string GetPrefix(IGuild guild) => GetPrefix(guild?.Id);
_prefixes[guild.Id] = prefix;
public string GetPrefix(ulong? id = null)
return prefix;
}
public async Task ExecuteExternal(ulong? guildId, ulong channelId, string commandText)
{
if (guildId != null)
{
if (id is null || !_prefixes.TryGetValue(id.Value, out var prefix))
return _bss.Data.Prefix;
return prefix;
}
public string SetDefaultPrefix(string prefix)
{
if (string.IsNullOrWhiteSpace(prefix))
throw new ArgumentNullException(nameof(prefix));
_bss.ModifyConfig(bs =>
var guild = _client.GetGuild(guildId.Value);
if (!(guild?.GetChannel(channelId) is SocketTextChannel channel))
{
bs.Prefix = prefix;
});
return prefix;
}
public string SetPrefix(IGuild guild, string prefix)
{
if (string.IsNullOrWhiteSpace(prefix))
throw new ArgumentNullException(nameof(prefix));
if (guild is null)
throw new ArgumentNullException(nameof(guild));
using (var uow = _db.GetDbContext())
{
var gc = uow.GuildConfigsForId(guild.Id, set => set);
gc.Prefix = prefix;
uow.SaveChanges();
Log.Warning("Channel for external execution not found.");
return;
}
_prefixes[guild.Id] = prefix;
return prefix;
}
public async Task ExecuteExternal(ulong? guildId, ulong channelId, string commandText)
{
if (guildId != null)
try
{
var guild = _client.GetGuild(guildId.Value);
if (!(guild?.GetChannel(channelId) is SocketTextChannel channel))
{
Log.Warning("Channel for external execution not found.");
return;
}
try
{
IUserMessage msg = await channel.SendMessageAsync(commandText).ConfigureAwait(false);
msg = (IUserMessage)await channel.GetMessageAsync(msg.Id).ConfigureAwait(false);
await TryRunCommand(guild, channel, msg).ConfigureAwait(false);
//msg.DeleteAfter(5);
}
catch { }
IUserMessage msg = await channel.SendMessageAsync(commandText).ConfigureAwait(false);
msg = (IUserMessage)await channel.GetMessageAsync(msg.Id).ConfigureAwait(false);
await TryRunCommand(guild, channel, msg).ConfigureAwait(false);
//msg.DeleteAfter(5);
}
catch { }
}
}
public Task StartHandling()
public Task StartHandling()
{
_client.MessageReceived += (msg) => { var _ = Task.Run(() => MessageReceivedHandler(msg)); return Task.CompletedTask; };
return Task.CompletedTask;
}
private const float _oneThousandth = 1.0f / 1000;
private readonly DbService _db;
private Task LogSuccessfulExecution(IUserMessage usrMsg, ITextChannel channel, params int[] execPoints)
{
if (_bss.Data.ConsoleOutputType == ConsoleOutputType.Normal)
{
_client.MessageReceived += (msg) => { var _ = Task.Run(() => MessageReceivedHandler(msg)); return Task.CompletedTask; };
return Task.CompletedTask;
}
private const float _oneThousandth = 1.0f / 1000;
private readonly DbService _db;
private Task LogSuccessfulExecution(IUserMessage usrMsg, ITextChannel channel, params int[] execPoints)
{
if (_bss.Data.ConsoleOutputType == ConsoleOutputType.Normal)
{
Log.Information($"Command Executed after " + string.Join("/", execPoints.Select(x => (x * _oneThousandth).ToString("F3"))) + "s\n\t" +
"User: {0}\n\t" +
"Server: {1}\n\t" +
"Channel: {2}\n\t" +
"Message: {3}",
usrMsg.Author + " [" + usrMsg.Author.Id + "]", // {0}
(channel is null ? "PRIVATE" : channel.Guild.Name + " [" + channel.Guild.Id + "]"), // {1}
(channel is null ? "PRIVATE" : channel.Name + " [" + channel.Id + "]"), // {2}
usrMsg.Content // {3}
);
}
else
{
Log.Information("Succ | g:{0} | c: {1} | u: {2} | msg: {3}",
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)
{
if (_bss.Data.ConsoleOutputType == ConsoleOutputType.Normal)
{
Log.Warning($"Command Errored after " + string.Join("/", execPoints.Select(x => (x * _oneThousandth).ToString("F3"))) + "s\n\t" +
Log.Information($"Command Executed after " + string.Join("/", execPoints.Select(x => (x * _oneThousandth).ToString("F3"))) + "s\n\t" +
"User: {0}\n\t" +
"Server: {1}\n\t" +
"Channel: {2}\n\t" +
"Message: {3}\n\t" +
"Error: {4}",
usrMsg.Author + " [" + usrMsg.Author.Id + "]", // {0}
channel is null ? "PRIVATE" : channel.Guild.Name + " [" + channel.Guild.Id + "]", // {1}
channel is null ? "PRIVATE" : channel.Name + " [" + channel.Id + "]", // {2}
usrMsg.Content,// {3}
errorMessage
//exec.Result.ErrorReason // {4}
);
}
else
{
Log.Warning("Err | g:{0} | c: {1} | u: {2} | msg: {3}\n\tErr: {4}",
channel?.Guild.Id.ToString() ?? "-",
channel?.Id.ToString() ?? "-",
usrMsg.Author.Id,
usrMsg.Content.TrimTo(10),
errorMessage);
}
"Message: {3}",
usrMsg.Author + " [" + usrMsg.Author.Id + "]", // {0}
(channel is null ? "PRIVATE" : channel.Guild.Name + " [" + channel.Guild.Id + "]"), // {1}
(channel is null ? "PRIVATE" : channel.Name + " [" + channel.Id + "]"), // {2}
usrMsg.Content // {3}
);
}
private async Task MessageReceivedHandler(SocketMessage msg)
else
{
try
{
if (msg.Author.IsBot || !_bot.IsReady) //no bots, wait until bot connected and initialized
return;
if (!(msg is SocketUserMessage usrMsg))
return;
#if !GLOBAL_NADEKO
// track how many messagges each user is sending
UserMessagesSent.AddOrUpdate(usrMsg.Author.Id, 1, (key, old) => ++old);
#endif
var channel = msg.Channel as ISocketMessageChannel;
var guild = (msg.Channel as SocketTextChannel)?.Guild;
await TryRunCommand(guild, channel, usrMsg).ConfigureAwait(false);
}
catch (Exception ex)
{
Log.Warning(ex, "Error in CommandHandler");
if (ex.InnerException != null)
{
Log.Warning(ex.InnerException, "Inner Exception of the error in CommandHandler");
}
}
Log.Information("Succ | g:{0} | c: {1} | u: {2} | msg: {3}",
channel?.Guild.Id.ToString() ?? "-",
channel?.Id.ToString() ?? "-",
usrMsg.Author.Id,
usrMsg.Content.TrimTo(10));
}
return Task.CompletedTask;
}
public async Task TryRunCommand(SocketGuild guild, ISocketMessageChannel channel, IUserMessage usrMsg)
private void LogErroredExecution(string errorMessage, IUserMessage usrMsg, ITextChannel channel, params int[] execPoints)
{
if (_bss.Data.ConsoleOutputType == ConsoleOutputType.Normal)
{
var startTime = Environment.TickCount;
var blocked = await _behaviourExecutor.RunEarlyBehavioursAsync(guild, usrMsg);
if (blocked)
return;
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 CommandContext(_client, usrMsg), messageContent, isPrefixCommand ? 1 : prefix.Length, _services, MultiMatchHandling.Best).ConfigureAwait(false);
startTime = Environment.TickCount - startTime;
if (Success)
{
await LogSuccessfulExecution(usrMsg, channel as ITextChannel, blockTime, startTime).ConfigureAwait(false);
await CommandExecuted(usrMsg, Info).ConfigureAwait(false);
return;
}
else if (Error != null)
{
LogErroredExecution(Error, usrMsg, channel as ITextChannel, blockTime, startTime);
if (guild != null)
await CommandErrored(Info, channel as ITextChannel, Error).ConfigureAwait(false);
}
}
else
{
await OnMessageNoTrigger(usrMsg).ConfigureAwait(false);
}
await _behaviourExecutor.RunLateExecutorsAsync(guild, usrMsg);
Log.Warning($"Command Errored after " + string.Join("/", execPoints.Select(x => (x * _oneThousandth).ToString("F3"))) + "s\n\t" +
"User: {0}\n\t" +
"Server: {1}\n\t" +
"Channel: {2}\n\t" +
"Message: {3}\n\t" +
"Error: {4}",
usrMsg.Author + " [" + usrMsg.Author.Id + "]", // {0}
channel is null ? "PRIVATE" : channel.Guild.Name + " [" + channel.Guild.Id + "]", // {1}
channel is null ? "PRIVATE" : channel.Name + " [" + channel.Id + "]", // {2}
usrMsg.Content,// {3}
errorMessage
//exec.Result.ErrorReason // {4}
);
}
public Task<(bool Success, string Error, CommandInfo Info)> ExecuteCommandAsync(CommandContext context, string input, int argPos, IServiceProvider serviceProvider, MultiMatchHandling multiMatchHandling = MultiMatchHandling.Exception)
=> ExecuteCommand(context, input.Substring(argPos), serviceProvider, multiMatchHandling);
public async Task<(bool Success, string Error, CommandInfo Info)> ExecuteCommand(CommandContext context, string input, IServiceProvider services, MultiMatchHandling multiMatchHandling = MultiMatchHandling.Exception)
else
{
var searchResult = _commandService.Search(context, input);
if (!searchResult.IsSuccess)
return (false, null, null);
var commands = searchResult.Commands;
var preconditionResults = new Dictionary<CommandMatch, PreconditionResult>();
foreach (var match in commands)
{
preconditionResults[match] = await match.Command.CheckPreconditionsAsync(context, services).ConfigureAwait(false);
}
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);
return (false, bestCandidate.Value.ErrorReason, commands[0].Command);
}
var parseResultsDict = new Dictionary<CommandMatch, ParseResult>();
foreach (var pair in successfulPreconditions)
{
var parseResult = await pair.Key.ParseAsync(context, searchResult, pair.Value, services).ConfigureAwait(false);
if (parseResult.Error == CommandError.MultipleMatches)
{
IReadOnlyList<TypeReaderValue> argList, paramList;
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();
parseResult = ParseResult.FromSuccess(argList, paramList);
break;
}
}
parseResultsDict[pair.Key] = parseResult;
}
// Calculates the 'score' of a command given a parse result
float CalculateScore(CommandMatch match, ParseResult parseResult)
{
float argValuesScore = 0, paramValuesScore = 0;
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;
argValuesScore = argValuesSum / match.Command.Parameters.Count;
paramValuesScore = paramValuesSum / match.Command.Parameters.Count;
}
var totalArgsScore = (argValuesScore + paramValuesScore) / 2;
return match.Command.Priority + totalArgsScore * 0.99f;
}
//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));
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);
return (false, bestMatch.Value.ErrorReason, commands[0].Command);
}
var cmd = successfulParses[0].Key.Command;
// Bot will ignore commands which are ran more often than what specified by
// GlobalCommandsCooldown constant (miliseconds)
if (!UsersOnShortCooldown.Add(context.Message.Author.Id))
return (false, null, cmd);
//return SearchResult.FromError(CommandError.Exception, "You are on a global cooldown.");
var blocked = await _behaviourExecutor.RunLateBlockersAsync(context, cmd);
if (blocked)
return (false, null, cmd);
//If we get this far, at least one parse was successful. Execute the most likely overload.
var chosenOverload = successfulParses[0];
var execResult = (ExecuteResult)await chosenOverload.Key.ExecuteAsync(context, chosenOverload.Value, services).ConfigureAwait(false);
if (execResult.Exception != null && (!(execResult.Exception is HttpException he) || he.DiscordCode != 50013))
{
Log.Warning(execResult.Exception, "Command Error");
}
return (true, null, cmd);
Log.Warning("Err | g:{0} | c: {1} | u: {2} | msg: {3}\n\tErr: {4}",
channel?.Guild.Id.ToString() ?? "-",
channel?.Id.ToString() ?? "-",
usrMsg.Author.Id,
usrMsg.Content.TrimTo(10),
errorMessage);
}
}
private async Task MessageReceivedHandler(SocketMessage msg)
{
try
{
if (msg.Author.IsBot || !_bot.IsReady) //no bots, wait until bot connected and initialized
return;
if (!(msg is SocketUserMessage usrMsg))
return;
#if !GLOBAL_NADEKO
// track how many messagges each user is sending
UserMessagesSent.AddOrUpdate(usrMsg.Author.Id, 1, (key, old) => ++old);
#endif
var channel = msg.Channel as ISocketMessageChannel;
var guild = (msg.Channel as SocketTextChannel)?.Guild;
await TryRunCommand(guild, channel, usrMsg).ConfigureAwait(false);
}
catch (Exception ex)
{
Log.Warning(ex, "Error in CommandHandler");
if (ex.InnerException != null)
{
Log.Warning(ex.InnerException, "Inner Exception of the error in CommandHandler");
}
}
}
public async Task TryRunCommand(SocketGuild guild, ISocketMessageChannel channel, IUserMessage usrMsg)
{
var startTime = Environment.TickCount;
var blocked = await _behaviourExecutor.RunEarlyBehavioursAsync(guild, usrMsg);
if (blocked)
return;
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 CommandContext(_client, usrMsg), messageContent, isPrefixCommand ? 1 : prefix.Length, _services, MultiMatchHandling.Best).ConfigureAwait(false);
startTime = Environment.TickCount - startTime;
if (Success)
{
await LogSuccessfulExecution(usrMsg, channel as ITextChannel, blockTime, startTime).ConfigureAwait(false);
await CommandExecuted(usrMsg, Info).ConfigureAwait(false);
return;
}
else if (Error != null)
{
LogErroredExecution(Error, usrMsg, channel as ITextChannel, blockTime, startTime);
if (guild != null)
await CommandErrored(Info, channel as ITextChannel, Error).ConfigureAwait(false);
}
}
else
{
await OnMessageNoTrigger(usrMsg).ConfigureAwait(false);
}
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)
=> ExecuteCommand(context, input.Substring(argPos), serviceProvider, multiMatchHandling);
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, PreconditionResult>();
foreach (var match in commands)
{
preconditionResults[match] = await match.Command.CheckPreconditionsAsync(context, services).ConfigureAwait(false);
}
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);
return (false, bestCandidate.Value.ErrorReason, commands[0].Command);
}
var parseResultsDict = new Dictionary<CommandMatch, ParseResult>();
foreach (var pair in successfulPreconditions)
{
var parseResult = await pair.Key.ParseAsync(context, searchResult, pair.Value, services).ConfigureAwait(false);
if (parseResult.Error == CommandError.MultipleMatches)
{
IReadOnlyList<TypeReaderValue> argList, paramList;
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();
parseResult = ParseResult.FromSuccess(argList, paramList);
break;
}
}
parseResultsDict[pair.Key] = parseResult;
}
// Calculates the 'score' of a command given a parse result
float CalculateScore(CommandMatch match, ParseResult parseResult)
{
float argValuesScore = 0, paramValuesScore = 0;
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;
argValuesScore = argValuesSum / match.Command.Parameters.Count;
paramValuesScore = paramValuesSum / match.Command.Parameters.Count;
}
var totalArgsScore = (argValuesScore + paramValuesScore) / 2;
return match.Command.Priority + totalArgsScore * 0.99f;
}
//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));
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);
return (false, bestMatch.Value.ErrorReason, commands[0].Command);
}
var cmd = successfulParses[0].Key.Command;
// Bot will ignore commands which are ran more often than what specified by
// GlobalCommandsCooldown constant (miliseconds)
if (!UsersOnShortCooldown.Add(context.Message.Author.Id))
return (false, null, cmd);
//return SearchResult.FromError(CommandError.Exception, "You are on a global cooldown.");
var blocked = await _behaviourExecutor.RunLateBlockersAsync(context, cmd);
if (blocked)
return (false, null, cmd);
//If we get this far, at least one parse was successful. Execute the most likely overload.
var chosenOverload = successfulParses[0];
var execResult = (ExecuteResult)await chosenOverload.Key.ExecuteAsync(context, chosenOverload.Value, services).ConfigureAwait(false);
if (execResult.Exception != null && (!(execResult.Exception is HttpException he) || he.DiscordCode != 50013))
{
Log.Warning(execResult.Exception, "Command Error");
}
return (true, null, cmd);
}
}

View File

@@ -1,77 +1,73 @@
using System.Collections.Generic;
using System.Linq;
namespace NadekoBot.Services;
namespace NadekoBot.Services
public class GreetGrouper<T>
{
public class GreetGrouper<T>
private readonly Dictionary<ulong, HashSet<T>> group;
private readonly object locker = new object();
public GreetGrouper()
{
private readonly Dictionary<ulong, HashSet<T>> group;
private readonly object locker = new object();
group = new Dictionary<ulong, HashSet<T>>();
}
public GreetGrouper()
/// <summary>
/// Creates a group, if group already exists, adds the specified user
/// </summary>
/// <param name="guildId">Id of the server for which to create group for</param>
/// <param name="toAddIfExists">User to add if group already exists</param>
/// <returns></returns>
public bool CreateOrAdd(ulong guildId, T toAddIfExists)
{
lock (locker)
{
group = new Dictionary<ulong, HashSet<T>>();
}
/// <summary>
/// Creates a group, if group already exists, adds the specified user
/// </summary>
/// <param name="guildId">Id of the server for which to create group for</param>
/// <param name="toAddIfExists">User to add if group already exists</param>
/// <returns></returns>
public bool CreateOrAdd(ulong guildId, T toAddIfExists)
{
lock (locker)
if (group.TryGetValue(guildId, out var list))
{
if (group.TryGetValue(guildId, out var list))
list.Add(toAddIfExists);
return false;
}
group[guildId] = new HashSet<T>();
return true;
}
}
/// <summary>
/// Remove the specified amount of items from the group. If all items are removed, group will be removed.
/// </summary>
/// <param name="guildId">Id of the group</param>
/// <param name="count">Maximum number of items to retrieve</param>
/// <param name="items">Items retrieved</param>
/// <returns>Whether the group has no more items left and is deleted</returns>
public bool ClearGroup(ulong guildId, int count, out IEnumerable<T> items)
{
lock (locker)
{
if (group.TryGetValue(guildId, out var set))
{
// if we want more than there are, return everything
if (count >= set.Count)
{
list.Add(toAddIfExists);
return false;
items = set;
group.Remove(guildId);
return true;
}
group[guildId] = new HashSet<T>();
return true;
// if there are more in the group than what's needed
// take the requested number, remove them from the set
// and return them
var toReturn = set.TakeWhile(item => count-- != 0).ToList();
foreach (var item in toReturn)
set.Remove(item);
items = toReturn;
// returning falsemeans group is not yet deleted
// because there are items left
return false;
}
}
/// <summary>
/// Remove the specified amount of items from the group. If all items are removed, group will be removed.
/// </summary>
/// <param name="guildId">Id of the group</param>
/// <param name="count">Maximum number of items to retrieve</param>
/// <param name="items">Items retrieved</param>
/// <returns>Whether the group has no more items left and is deleted</returns>
public bool ClearGroup(ulong guildId, int count, out IEnumerable<T> items)
{
lock (locker)
{
if (group.TryGetValue(guildId, out var set))
{
// if we want more than there are, return everything
if (count >= set.Count)
{
items = set;
group.Remove(guildId);
return true;
}
// if there are more in the group than what's needed
// take the requested number, remove them from the set
// and return them
var toReturn = set.TakeWhile(item => count-- != 0).ToList();
foreach (var item in toReturn)
set.Remove(item);
items = toReturn;
// returning falsemeans group is not yet deleted
// because there are items left
return false;
}
items = Enumerable.Empty<T>();
return true;
}
items = Enumerable.Empty<T>();
return true;
}
}
}

View File

@@ -1,64 +1,61 @@
using Microsoft.Data.Sqlite;
using Microsoft.EntityFrameworkCore;
using NadekoBot.Services.Database;
using System;
using System.IO;
using System.Linq;
using LinqToDB.EntityFrameworkCore;
namespace NadekoBot.Services
namespace NadekoBot.Services;
public class DbService
{
public class DbService
private readonly DbContextOptions<NadekoContext> options;
private readonly DbContextOptions<NadekoContext> migrateOptions;
public DbService(IBotCredentials creds)
{
private readonly DbContextOptions<NadekoContext> options;
private readonly DbContextOptions<NadekoContext> migrateOptions;
public DbService(IBotCredentials creds)
{
LinqToDBForEFTools.Initialize();
LinqToDBForEFTools.Initialize();
var builder = new SqliteConnectionStringBuilder(creds.Db.ConnectionString);
builder.DataSource = Path.Combine(AppContext.BaseDirectory, builder.DataSource);
var builder = new SqliteConnectionStringBuilder(creds.Db.ConnectionString);
builder.DataSource = Path.Combine(AppContext.BaseDirectory, builder.DataSource);
var optionsBuilder = new DbContextOptionsBuilder<NadekoContext>();
optionsBuilder.UseSqlite(builder.ToString());
options = optionsBuilder.Options;
var optionsBuilder = new DbContextOptionsBuilder<NadekoContext>();
optionsBuilder.UseSqlite(builder.ToString());
options = optionsBuilder.Options;
optionsBuilder = new DbContextOptionsBuilder<NadekoContext>();
optionsBuilder.UseSqlite(builder.ToString());
migrateOptions = optionsBuilder.Options;
}
public void Setup()
{
using (var context = new NadekoContext(options))
{
if (context.Database.GetPendingMigrations().Any())
{
var mContext = new NadekoContext(migrateOptions);
mContext.Database.Migrate();
mContext.SaveChanges();
mContext.Dispose();
}
context.Database.ExecuteSqlRaw("PRAGMA journal_mode=WAL");
context.SaveChanges();
}
}
private NadekoContext GetDbContextInternal()
{
var context = new NadekoContext(options);
context.Database.SetCommandTimeout(60);
var conn = context.Database.GetDbConnection();
conn.Open();
using (var com = conn.CreateCommand())
{
com.CommandText = "PRAGMA journal_mode=WAL; PRAGMA synchronous=OFF";
com.ExecuteNonQuery();
}
return context;
}
public NadekoContext GetDbContext() => GetDbContextInternal();
optionsBuilder = new DbContextOptionsBuilder<NadekoContext>();
optionsBuilder.UseSqlite(builder.ToString());
migrateOptions = optionsBuilder.Options;
}
public void Setup()
{
using (var context = new NadekoContext(options))
{
if (context.Database.GetPendingMigrations().Any())
{
var mContext = new NadekoContext(migrateOptions);
mContext.Database.Migrate();
mContext.SaveChanges();
mContext.Dispose();
}
context.Database.ExecuteSqlRaw("PRAGMA journal_mode=WAL");
context.SaveChanges();
}
}
private NadekoContext GetDbContextInternal()
{
var context = new NadekoContext(options);
context.Database.SetCommandTimeout(60);
var conn = context.Database.GetDbConnection();
conn.Open();
using (var com = conn.CreateCommand())
{
com.CommandText = "PRAGMA journal_mode=WAL; PRAGMA synchronous=OFF";
com.ExecuteNonQuery();
}
return context;
}
public NadekoContext GetDbContext() => GetDbContextInternal();
}

File diff suppressed because it is too large Load Diff

View File

@@ -2,17 +2,15 @@
using Discord;
using Discord.Commands;
using Discord.WebSocket;
using Microsoft.Extensions.DependencyInjection;
namespace NadekoBot.Services
namespace NadekoBot.Services;
public interface IBehaviourExecutor
{
public interface IBehaviourExecutor
{
public Task<bool> RunEarlyBehavioursAsync(SocketGuild guild, IUserMessage usrMsg);
public Task<string> RunInputTransformersAsync(SocketGuild guild, IUserMessage usrMsg);
Task<bool> RunLateBlockersAsync(ICommandContext context, CommandInfo cmd);
Task RunLateExecutorsAsync(SocketGuild guild, IUserMessage usrMsg);
public Task<bool> RunEarlyBehavioursAsync(SocketGuild guild, IUserMessage usrMsg);
public Task<string> RunInputTransformersAsync(SocketGuild guild, IUserMessage usrMsg);
Task<bool> RunLateBlockersAsync(ICommandContext context, CommandInfo cmd);
Task RunLateExecutorsAsync(SocketGuild guild, IUserMessage usrMsg);
public void Initialize();
}
public void Initialize();
}

View File

@@ -1,24 +1,21 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using System.Threading.Tasks;
namespace NadekoBot.Services
namespace NadekoBot.Services;
public interface ICoordinator
{
public interface ICoordinator
{
bool RestartBot();
void Die(bool graceful);
bool RestartShard(int shardId);
IList<ShardStatus> GetAllShardStatuses();
int GetGuildCount();
Task Reload();
}
bool RestartBot();
void Die(bool graceful);
bool RestartShard(int shardId);
IList<ShardStatus> GetAllShardStatuses();
int GetGuildCount();
Task Reload();
}
public class ShardStatus
{
public Discord.ConnectionState ConnectionState { get; set; }
public DateTime LastUpdate { get; set; }
public int ShardId { get; set; }
public int GuildCount { get; set; }
}
public class ShardStatus
{
public Discord.ConnectionState ConnectionState { get; set; }
public DateTime LastUpdate { get; set; }
public int ShardId { get; set; }
public int GuildCount { get; set; }
}

View File

@@ -1,16 +1,14 @@
using Discord;
using System.Collections.Generic;
using System.Threading.Tasks;
namespace NadekoBot.Services
namespace NadekoBot.Services;
public interface ICurrencyService
{
public interface ICurrencyService
{
Task AddAsync(ulong userId, string reason, long amount, bool gamble = false);
Task AddAsync(IUser user, string reason, long amount, bool sendMessage = false, bool gamble = false);
Task AddBulkAsync(IEnumerable<ulong> userIds, IEnumerable<string> reasons, IEnumerable<long> amounts, bool gamble = false);
Task<bool> RemoveAsync(ulong userId, string reason, long amount, bool gamble = false);
Task<bool> RemoveAsync(IUser userId, string reason, long amount, bool sendMessage = false, bool gamble = false);
Task RemoveBulkAsync(IEnumerable<ulong> userIds, IEnumerable<string> reasons, IEnumerable<long> amounts, bool gamble = false);
}
}
Task AddAsync(ulong userId, string reason, long amount, bool gamble = false);
Task AddAsync(IUser user, string reason, long amount, bool sendMessage = false, bool gamble = false);
Task AddBulkAsync(IEnumerable<ulong> userIds, IEnumerable<string> reasons, IEnumerable<long> amounts, bool gamble = false);
Task<bool> RemoveAsync(ulong userId, string reason, long amount, bool gamble = false);
Task<bool> RemoveAsync(IUser userId, string reason, long amount, bool sendMessage = false, bool gamble = false);
Task RemoveBulkAsync(IEnumerable<ulong> userIds, IEnumerable<string> reasons, IEnumerable<long> amounts, bool gamble = false);
}

View File

@@ -1,31 +1,29 @@
using StackExchange.Redis;
using System;
using System.Threading.Tasks;
namespace NadekoBot.Services
namespace NadekoBot.Services;
public interface IDataCache
{
public interface IDataCache
{
ConnectionMultiplexer Redis { get; }
IImageCache LocalImages { get; }
ILocalDataCache LocalData { get; }
ConnectionMultiplexer Redis { get; }
IImageCache LocalImages { get; }
ILocalDataCache LocalData { get; }
Task<(bool Success, byte[] Data)> TryGetImageDataAsync(Uri key);
Task<(bool Success, string Data)> TryGetAnimeDataAsync(string key);
Task<(bool Success, string Data)> TryGetNovelDataAsync(string key);
Task SetImageDataAsync(Uri key, byte[] data);
Task SetAnimeDataAsync(string link, string data);
Task SetNovelDataAsync(string link, string data);
TimeSpan? AddTimelyClaim(ulong id, int period);
TimeSpan? TryAddRatelimit(ulong id, string name, int expireIn);
void RemoveAllTimelyClaims();
bool TryAddAffinityCooldown(ulong userId, out TimeSpan? time);
bool TryAddDivorceCooldown(ulong userId, out TimeSpan? time);
bool TryGetEconomy(out string data);
void SetEconomy(string data);
Task<(bool Success, byte[] Data)> TryGetImageDataAsync(Uri key);
Task<(bool Success, string Data)> TryGetAnimeDataAsync(string key);
Task<(bool Success, string Data)> TryGetNovelDataAsync(string key);
Task SetImageDataAsync(Uri key, byte[] data);
Task SetAnimeDataAsync(string link, string data);
Task SetNovelDataAsync(string link, string data);
TimeSpan? AddTimelyClaim(ulong id, int period);
TimeSpan? TryAddRatelimit(ulong id, string name, int expireIn);
void RemoveAllTimelyClaims();
bool TryAddAffinityCooldown(ulong userId, out TimeSpan? time);
bool TryAddDivorceCooldown(ulong userId, out TimeSpan? time);
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;
DateTime GetLastCurrencyDecay();
void SetLastCurrencyDecay();
}
}
Task<TOut> GetOrAddCachedDataAsync<TParam, TOut>(string key, Func<TParam, Task<TOut>> factory, TParam param, TimeSpan expiry) where TOut : class;
DateTime GetLastCurrencyDecay();
void SetLastCurrencyDecay();
}

View File

@@ -1,84 +1,82 @@
using System;
using Discord;
using Discord;
using Discord.Commands;
using NadekoBot.Common.Configs;
using NadekoBot.Extensions;
namespace NadekoBot.Services
namespace NadekoBot.Services;
public interface IEmbedBuilderService
{
public interface IEmbedBuilderService
{
IEmbedBuilder Create(ICommandContext ctx = null);
IEmbedBuilder Create(EmbedBuilder eb);
}
IEmbedBuilder Create(ICommandContext ctx = null);
IEmbedBuilder Create(EmbedBuilder eb);
}
public class EmbedBuilderService : IEmbedBuilderService, INService
{
private readonly BotConfigService _botConfigService;
public class EmbedBuilderService : IEmbedBuilderService, INService
{
private readonly BotConfigService _botConfigService;
public EmbedBuilderService(BotConfigService botConfigService)
{
_botConfigService = botConfigService;
}
public IEmbedBuilder Create(ICommandContext ctx = null)
=> new DiscordEmbedBuilderWrapper(_botConfigService.Data);
public IEmbedBuilder Create(EmbedBuilder embed)
=> new DiscordEmbedBuilderWrapper(_botConfigService.Data, embed);
public EmbedBuilderService(BotConfigService botConfigService)
{
_botConfigService = botConfigService;
}
public IEmbedBuilder Create(ICommandContext ctx = null)
=> new DiscordEmbedBuilderWrapper(_botConfigService.Data);
public IEmbedBuilder Create(EmbedBuilder embed)
=> new DiscordEmbedBuilderWrapper(_botConfigService.Data, embed);
}
public sealed class DiscordEmbedBuilderWrapper : IEmbedBuilder
public sealed class DiscordEmbedBuilderWrapper : IEmbedBuilder
{
private readonly BotConfig _botConfig;
private EmbedBuilder _embed;
public DiscordEmbedBuilderWrapper(in BotConfig botConfig, EmbedBuilder embed = null)
{
private readonly BotConfig _botConfig;
private EmbedBuilder _embed;
_botConfig = botConfig;
_embed = embed ?? new EmbedBuilder();
}
public DiscordEmbedBuilderWrapper(in BotConfig botConfig, EmbedBuilder embed = null)
{
_botConfig = botConfig;
_embed = embed ?? new EmbedBuilder();
}
public IEmbedBuilder WithDescription(string desc)
=> Wrap(_embed.WithDescription(desc));
public IEmbedBuilder WithDescription(string desc)
=> Wrap(_embed.WithDescription(desc));
public IEmbedBuilder WithTitle(string title)
=> Wrap(_embed.WithTitle(title));
public IEmbedBuilder WithTitle(string title)
=> Wrap(_embed.WithTitle(title));
public IEmbedBuilder AddField(string title, object value, bool isInline = false)
=> Wrap(_embed.AddField(title, value, isInline));
public IEmbedBuilder AddField(string title, object value, bool isInline = false)
=> Wrap(_embed.AddField(title, value, isInline));
public IEmbedBuilder WithFooter(string text, string iconUrl = null)
=> Wrap(_embed.WithFooter(text, iconUrl));
public IEmbedBuilder WithFooter(string text, string iconUrl = null)
=> Wrap(_embed.WithFooter(text, iconUrl));
public IEmbedBuilder WithAuthor(string name, string iconUrl = null, string url = null)
=> Wrap(_embed.WithAuthor(name, iconUrl, url));
public IEmbedBuilder WithAuthor(string name, string iconUrl = null, string url = null)
=> Wrap(_embed.WithAuthor(name, iconUrl, url));
public IEmbedBuilder WithUrl(string url)
=> Wrap(_embed.WithUrl(url));
public IEmbedBuilder WithUrl(string url)
=> Wrap(_embed.WithUrl(url));
public IEmbedBuilder WithImageUrl(string url)
=> Wrap(_embed.WithImageUrl(url));
public IEmbedBuilder WithImageUrl(string url)
=> Wrap(_embed.WithImageUrl(url));
public IEmbedBuilder WithThumbnailUrl(string url)
=> Wrap(_embed.WithThumbnailUrl(url));
public IEmbedBuilder WithThumbnailUrl(string url)
=> Wrap(_embed.WithThumbnailUrl(url));
public IEmbedBuilder WithColor(EmbedColor color)
=> color switch
{
EmbedColor.Ok => Wrap(_embed.WithColor(_botConfig.Color.Ok.ToDiscordColor())),
EmbedColor.Pending => Wrap(_embed.WithColor(_botConfig.Color.Pending.ToDiscordColor())),
EmbedColor.Error => Wrap(_embed.WithColor(_botConfig.Color.Error.ToDiscordColor())),
_ => throw new ArgumentOutOfRangeException(nameof(color), "Unsupported EmbedColor type")
};
public Embed Build()
=> _embed.Build();
private IEmbedBuilder Wrap(EmbedBuilder eb)
public IEmbedBuilder WithColor(EmbedColor color)
=> color switch
{
_embed = eb;
return this;
}
EmbedColor.Ok => Wrap(_embed.WithColor(_botConfig.Color.Ok.ToDiscordColor())),
EmbedColor.Pending => Wrap(_embed.WithColor(_botConfig.Color.Pending.ToDiscordColor())),
EmbedColor.Error => Wrap(_embed.WithColor(_botConfig.Color.Error.ToDiscordColor())),
_ => throw new ArgumentOutOfRangeException(nameof(color), "Unsupported EmbedColor type")
};
public Embed Build()
=> _embed.Build();
private IEmbedBuilder Wrap(EmbedBuilder eb)
{
_embed = eb;
return this;
}
}

View File

@@ -1,36 +1,33 @@
using Google.Apis.Customsearch.v1.Data;
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
namespace NadekoBot.Services
namespace NadekoBot.Services;
public interface IGoogleApiService : INService
{
public interface IGoogleApiService : INService
{
IReadOnlyDictionary<string, string> Languages { get; }
IReadOnlyDictionary<string, string> Languages { get; }
Task<IEnumerable<string>> GetVideoLinksByKeywordAsync(string keywords, int count = 1);
Task<IEnumerable<(string Name, string Id, string Url)>> GetVideoInfosByKeywordAsync(string keywords, int count = 1);
Task<IEnumerable<string>> GetPlaylistIdsByKeywordsAsync(string keywords, int count = 1);
Task<IEnumerable<string>> GetRelatedVideosAsync(string url, int count = 1);
Task<IEnumerable<string>> GetPlaylistTracksAsync(string playlistId, int count = 50);
Task<IReadOnlyDictionary<string, TimeSpan>> GetVideoDurationsAsync(IEnumerable<string> videoIds);
Task<ImageResult> GetImageAsync(string query);
Task<string> Translate(string sourceText, string sourceLanguage, string targetLanguage);
Task<IEnumerable<string>> GetVideoLinksByKeywordAsync(string keywords, int count = 1);
Task<IEnumerable<(string Name, string Id, string Url)>> GetVideoInfosByKeywordAsync(string keywords, int count = 1);
Task<IEnumerable<string>> GetPlaylistIdsByKeywordsAsync(string keywords, int count = 1);
Task<IEnumerable<string>> GetRelatedVideosAsync(string url, int count = 1);
Task<IEnumerable<string>> GetPlaylistTracksAsync(string playlistId, int count = 50);
Task<IReadOnlyDictionary<string, TimeSpan>> GetVideoDurationsAsync(IEnumerable<string> videoIds);
Task<ImageResult> GetImageAsync(string query);
Task<string> Translate(string sourceText, string sourceLanguage, string targetLanguage);
Task<string> ShortenUrl(string url);
Task<string> ShortenUrl(Uri url);
}
public struct ImageResult
{
public Result.ImageData Image { get; }
public string Link { get; }
public ImageResult(Result.ImageData image, string link)
{
this.Image = image;
this.Link = link;
}
}
Task<string> ShortenUrl(string url);
Task<string> ShortenUrl(Uri url);
}
public struct ImageResult
{
public Result.ImageData Image { get; }
public string Link { get; }
public ImageResult(Result.ImageData image, string link)
{
this.Image = image;
this.Link = link;
}
}

View File

@@ -1,33 +1,31 @@
using NadekoBot.Common;
using System.Collections.Generic;
using System.Threading.Tasks;
namespace NadekoBot.Services
namespace NadekoBot.Services;
public interface IImageCache
{
public interface IImageCache
{
ImageUrls ImageUrls { get; }
ImageUrls ImageUrls { get; }
IReadOnlyList<byte[]> Heads { get; }
IReadOnlyList<byte[]> Tails { get; }
IReadOnlyList<byte[]> Heads { get; }
IReadOnlyList<byte[]> Tails { get; }
IReadOnlyList<byte[]> Dice { get; }
IReadOnlyList<byte[]> Dice { get; }
IReadOnlyList<byte[]> SlotEmojis { get; }
IReadOnlyList<byte[]> Currency { get; }
IReadOnlyList<byte[]> SlotEmojis { get; }
IReadOnlyList<byte[]> Currency { get; }
byte[] SlotBackground { get; }
byte[] SlotBackground { get; }
byte[] RategirlMatrix { get; }
byte[] RategirlDot { get; }
byte[] RategirlMatrix { get; }
byte[] RategirlDot { get; }
byte[] XpBackground { get; }
byte[] XpBackground { get; }
byte[] Rip { get; }
byte[] RipOverlay { get; }
byte[] Rip { get; }
byte[] RipOverlay { get; }
byte[] GetCard(string key);
byte[] GetCard(string key);
Task Reload();
}
}
Task Reload();
}

View File

@@ -1,14 +1,12 @@
using NadekoBot.Common.Pokemon;
using NadekoBot.Modules.Games.Common.Trivia;
using System.Collections.Generic;
namespace NadekoBot.Services
namespace NadekoBot.Services;
public interface ILocalDataCache
{
public interface ILocalDataCache
{
IReadOnlyDictionary<string, SearchPokemon> Pokemons { get; }
IReadOnlyDictionary<string, SearchPokemonAbility> PokemonAbilities { get; }
IReadOnlyDictionary<int, string> PokemonMap { get; }
TriviaQuestion[] TriviaQuestions { get; }
}
}
IReadOnlyDictionary<string, SearchPokemon> Pokemons { get; }
IReadOnlyDictionary<string, SearchPokemonAbility> PokemonAbilities { get; }
IReadOnlyDictionary<int, string> PokemonMap { get; }
TriviaQuestion[] TriviaQuestions { get; }
}

View File

@@ -2,20 +2,19 @@
using System.Globalization;
using Discord;
namespace NadekoBot.Services
{
public interface ILocalization
{
CultureInfo DefaultCultureInfo { get; }
ConcurrentDictionary<ulong, CultureInfo> GuildCultureInfos { get; }
namespace NadekoBot.Services;
CultureInfo GetCultureInfo(IGuild guild);
CultureInfo GetCultureInfo(ulong? guildId);
void RemoveGuildCulture(IGuild guild);
void RemoveGuildCulture(ulong guildId);
void ResetDefaultCulture();
void SetDefaultCulture(CultureInfo ci);
void SetGuildCulture(IGuild guild, CultureInfo ci);
void SetGuildCulture(ulong guildId, CultureInfo ci);
}
public interface ILocalization
{
CultureInfo DefaultCultureInfo { get; }
ConcurrentDictionary<ulong, CultureInfo> GuildCultureInfos { get; }
CultureInfo GetCultureInfo(IGuild guild);
CultureInfo GetCultureInfo(ulong? guildId);
void RemoveGuildCulture(IGuild guild);
void RemoveGuildCulture(ulong guildId);
void ResetDefaultCulture();
void SetDefaultCulture(CultureInfo ci);
void SetGuildCulture(IGuild guild, CultureInfo ci);
void SetGuildCulture(ulong guildId, CultureInfo ci);
}

View File

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

View File

@@ -1,58 +1,55 @@
using System;
namespace NadekoBot.Services;
namespace NadekoBot.Services
public interface IStatsService
{
public interface IStatsService
{
/// <summary>
/// The author of the bot.
/// </summary>
string Author { get; }
/// <summary>
/// The author of the bot.
/// </summary>
string Author { get; }
/// <summary>
/// The total amount of commands ran since startup.
/// </summary>
long CommandsRan { get; }
/// <summary>
/// The total amount of commands ran since startup.
/// </summary>
long CommandsRan { get; }
/// <summary>
/// The Discord framework used by the bot.
/// </summary>
string Library { get; }
/// <summary>
/// The Discord framework used by the bot.
/// </summary>
string Library { get; }
/// <summary>
/// The amount of messages seen by the bot since startup.
/// </summary>
long MessageCounter { get; }
/// <summary>
/// The amount of messages seen by the bot since startup.
/// </summary>
long MessageCounter { get; }
/// <summary>
/// The rate of messages the bot sees every second.
/// </summary>
double MessagesPerSecond { get; }
/// <summary>
/// The rate of messages the bot sees every second.
/// </summary>
double MessagesPerSecond { get; }
/// <summary>
/// The total amount of text channels the bot can see.
/// </summary>
long TextChannels { get; }
/// <summary>
/// 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.
/// </summary>
long VoiceChannels { get; }
/// <summary>
/// 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.
/// </summary>
TimeSpan GetUptime();
/// <summary>
/// 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.
/// </summary>
/// <param name="separator">The formatting separator.</param>
string GetUptimeString(string separator = ", ");
/// <summary>
/// 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.
/// </summary>
double GetPrivateMemory();
}
}
/// <summary>
/// Gets total amount of private memory currently in use by the bot, in Megabytes.
/// </summary>
double GetPrivateMemory();
}

View File

@@ -1,100 +1,95 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using System.Threading.Tasks;
using Discord;
using Discord.Commands;
using Discord.WebSocket;
using NadekoBot.Common.ModuleBehaviors;
using NadekoBot.Extensions;
using Serilog;
using Microsoft.Extensions.DependencyInjection;
namespace NadekoBot.Services
namespace NadekoBot.Services;
public sealed class BehaviorExecutor : IBehaviourExecutor, INService
{
public sealed class BehaviorExecutor : IBehaviourExecutor, INService
private readonly IServiceProvider _services;
private IEnumerable<ILateExecutor> _lateExecutors;
private IEnumerable<ILateBlocker> _lateBlockers;
private IEnumerable<IEarlyBehavior> _earlyBehaviors;
private IEnumerable<IInputTransformer> _transformers;
public BehaviorExecutor(IServiceProvider services)
{
private readonly IServiceProvider _services;
private IEnumerable<ILateExecutor> _lateExecutors;
private IEnumerable<ILateBlocker> _lateBlockers;
private IEnumerable<IEarlyBehavior> _earlyBehaviors;
private IEnumerable<IInputTransformer> _transformers;
_services = services;
}
public BehaviorExecutor(IServiceProvider services)
{
_services = services;
}
public void Initialize()
{
_lateExecutors = _services.GetServices<ILateExecutor>();
_lateBlockers = _services.GetServices<ILateBlocker>();
_earlyBehaviors = _services.GetServices<IEarlyBehavior>()
.OrderByDescending(x => x.Priority);
_transformers = _services.GetServices<IInputTransformer>();
}
public void Initialize()
public async Task<bool> RunEarlyBehavioursAsync(SocketGuild guild, IUserMessage usrMsg)
{
foreach (var beh in _earlyBehaviors)
{
_lateExecutors = _services.GetServices<ILateExecutor>();
_lateBlockers = _services.GetServices<ILateBlocker>();
_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))
{
if (await beh.RunBehavior(guild, usrMsg))
{
return true;
}
return true;
}
return false;
}
public async Task<string> RunInputTransformersAsync(SocketGuild guild, IUserMessage usrMsg)
return false;
}
public async Task<string> RunInputTransformersAsync(SocketGuild guild, IUserMessage usrMsg)
{
var messageContent = usrMsg.Content;
foreach (var exec in _transformers)
{
var messageContent = usrMsg.Content;
foreach (var exec in _transformers)
string newContent;
if ((newContent = await exec.TransformInput(guild, usrMsg.Channel, usrMsg.Author, messageContent))
!= messageContent.ToLowerInvariant())
{
string newContent;
if ((newContent = await exec.TransformInput(guild, usrMsg.Channel, usrMsg.Author, messageContent))
!= messageContent.ToLowerInvariant())
{
messageContent = newContent;
break;
}
messageContent = newContent;
break;
}
return messageContent;
}
public async Task<bool> RunLateBlockersAsync(ICommandContext ctx, CommandInfo cmd)
return messageContent;
}
public async Task<bool> RunLateBlockersAsync(ICommandContext ctx, CommandInfo cmd)
{
foreach (var exec in _lateBlockers)
{
foreach (var exec in _lateBlockers)
if (await exec.TryBlockLate(ctx, cmd.Module.GetTopLevelModule().Name, cmd))
{
if (await exec.TryBlockLate(ctx, cmd.Module.GetTopLevelModule().Name, cmd))
{
Log.Information("Late blocking User [{0}] Command: [{1}] in [{2}]",
ctx.User,
cmd.Aliases[0],
exec.GetType().Name);
return true;
}
Log.Information("Late blocking User [{0}] Command: [{1}] in [{2}]",
ctx.User,
cmd.Aliases[0],
exec.GetType().Name);
return true;
}
return false;
}
public async Task RunLateExecutorsAsync(SocketGuild guild, IUserMessage usrMsg)
return false;
}
public async Task RunLateExecutorsAsync(SocketGuild guild, IUserMessage usrMsg)
{
foreach (var exec in _lateExecutors)
{
foreach (var exec in _lateExecutors)
try
{
try
{
await exec.LateExecute(guild, usrMsg).ConfigureAwait(false);
}
catch (Exception ex)
{
Log.Error(ex, "Error in {TypeName} late executor: {ErrorMessage}",
exec.GetType().Name,
ex.Message);
}
await exec.LateExecute(guild, usrMsg).ConfigureAwait(false);
}
catch (Exception ex)
{
Log.Error(ex, "Error in {TypeName} late executor: {ErrorMessage}",
exec.GetType().Name,
ex.Message);
}
}
}

View File

@@ -1,182 +1,178 @@
using System;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Configuration;
using System.IO;
using System.Linq;
using Microsoft.Extensions.Primitives;
using NadekoBot.Common;
using NadekoBot.Common.Yml;
using Newtonsoft.Json;
using Serilog;
namespace NadekoBot.Services
namespace NadekoBot.Services;
public interface IBotCredsProvider
{
public interface IBotCredsProvider
{
public void Reload();
public IBotCredentials GetCreds();
public void ModifyCredsFile(Action<Creds> func);
}
public void Reload();
public IBotCredentials GetCreds();
public void ModifyCredsFile(Action<Creds> func);
}
public sealed class BotCredsProvider : IBotCredsProvider
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 Creds _creds = new Creds();
private IConfigurationRoot _config;
private readonly object reloadLock = new object();
public void Reload()
{
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 Creds _creds = new Creds();
private IConfigurationRoot _config;
private readonly object reloadLock = new object();
public void Reload()
lock (reloadLock)
{
lock (reloadLock)
_creds.OwnerIds.Clear();
_config.Bind(_creds);
if (string.IsNullOrWhiteSpace(_creds.Token))
{
_creds.OwnerIds.Clear();
_config.Bind(_creds);
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.Token))
if (string.IsNullOrWhiteSpace(_creds.RestartCommand?.Cmd)
|| string.IsNullOrWhiteSpace(_creds.RestartCommand?.Args))
{
if (Environment.OSVersion.Platform == PlatformID.Unix)
{
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 RestartConfig()
{
_creds.RestartCommand = new RestartConfig()
{
Args = "dotnet",
Cmd = "NadekoBot.dll -- {0}",
};
}
else
Args = "dotnet",
Cmd = "NadekoBot.dll -- {0}",
};
}
else
{
_creds.RestartCommand = new RestartConfig()
{
_creds.RestartCommand = new RestartConfig()
{
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";
_creds.TotalShards = _totalShards ?? _creds.TotalShards;
}
}
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);
var creds = Yaml.Deserializer.Deserialize<Creds>(ymlData);
func(creds);
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
/// </summary>
private void MigrateCredentials()
{
if (File.Exists(OldCredsJsonPath))
{
Log.Information("Migrating old creds...");
var jsonCredentialsFileText = File.ReadAllText(OldCredsJsonPath);
var oldCreds = JsonConvert.DeserializeObject<Creds.Old>(jsonCredentialsFileText);
var creds = new Creds
{
Version = 1,
Token = oldCreds.Token,
OwnerIds = oldCreds.OwnerIds.Distinct().ToHashSet(),
GoogleApiKey = oldCreds.GoogleApiKey,
RapidApiKey = oldCreds.MashapeKey,
OsuApiKey = oldCreds.OsuApiKey,
CleverbotApiKey = oldCreds.CleverbotApiKey,
TotalShards = oldCreds.TotalShards <= 1 ? 1 : oldCreds.TotalShards,
Patreon = new Creds.PatreonSettings(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,
};
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");
}
if (File.Exists(_credsFileName))
{
var creds = Yaml.Deserializer.Deserialize<Creds>(File.ReadAllText(_credsFileName));
if (creds.Version <= 1)
{
creds.Version = 2;
File.WriteAllText(_credsFileName, Yaml.Serializer.Serialize(creds));
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";
_creds.TotalShards = _totalShards ?? _creds.TotalShards;
}
public IBotCredentials GetCreds() => _creds;
}
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);
var creds = Yaml.Deserializer.Deserialize<Creds>(ymlData);
func(creds);
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
/// </summary>
private void MigrateCredentials()
{
if (File.Exists(OldCredsJsonPath))
{
Log.Information("Migrating old creds...");
var jsonCredentialsFileText = File.ReadAllText(OldCredsJsonPath);
var oldCreds = JsonConvert.DeserializeObject<Creds.Old>(jsonCredentialsFileText);
var creds = new Creds
{
Version = 1,
Token = oldCreds.Token,
OwnerIds = oldCreds.OwnerIds.Distinct().ToHashSet(),
GoogleApiKey = oldCreds.GoogleApiKey,
RapidApiKey = oldCreds.MashapeKey,
OsuApiKey = oldCreds.OsuApiKey,
CleverbotApiKey = oldCreds.CleverbotApiKey,
TotalShards = oldCreds.TotalShards <= 1 ? 1 : oldCreds.TotalShards,
Patreon = new Creds.PatreonSettings(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,
};
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");
}
if (File.Exists(_credsFileName))
{
var creds = Yaml.Deserializer.Deserialize<Creds>(File.ReadAllText(_credsFileName));
if (creds.Version <= 1)
{
creds.Version = 2;
File.WriteAllText(_credsFileName, Yaml.Serializer.Serialize(creds));
}
}
}
public IBotCredentials GetCreds() => _creds;
}

View File

@@ -3,166 +3,162 @@ using Discord.WebSocket;
using NadekoBot.Services.Database;
using NadekoBot.Services.Database.Models;
using NadekoBot.Extensions;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using NadekoBot.Db;
using NadekoBot.Modules.Gambling.Services;
namespace NadekoBot.Services
namespace NadekoBot.Services;
public class CurrencyService : ICurrencyService, INService
{
public class CurrencyService : ICurrencyService, INService
private readonly DbService _db;
private readonly GamblingConfigService _gss;
private readonly IEmbedBuilderService _eb;
private readonly IUser _bot;
public CurrencyService(DbService db, DiscordSocketClient c, GamblingConfigService gss, IEmbedBuilderService eb)
{
private readonly DbService _db;
private readonly GamblingConfigService _gss;
private readonly IEmbedBuilderService _eb;
private readonly IUser _bot;
_db = db;
_gss = gss;
_eb = eb;
_bot = c.CurrentUser;
}
public CurrencyService(DbService db, DiscordSocketClient c, GamblingConfigService gss, IEmbedBuilderService eb)
private CurrencyTransaction GetCurrencyTransaction(ulong userId, string reason, long amount) =>
new CurrencyTransaction
{
_db = db;
_gss = gss;
_eb = eb;
_bot = c.CurrentUser;
}
Amount = amount,
UserId = userId,
Reason = reason ?? "-",
};
private CurrencyTransaction GetCurrencyTransaction(ulong userId, string reason, long amount) =>
new CurrencyTransaction
{
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)
{
var result = uow.TryUpdateCurrencyState(userId, userName, discrim, avatar, amount);
if (result)
{
var t = GetCurrencyTransaction(userId, reason, amount);
uow.CurrencyTransactions.Add(t);
var t = GetCurrencyTransaction(userId, reason, amount);
uow.CurrencyTransactions.Add(t);
if (gamble)
{
var t2 = GetCurrencyTransaction(_bot.Id, reason, -amount);
uow.CurrencyTransactions.Add(t2);
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)
{
if (amount < 0)
if (gamble)
{
throw new ArgumentException("You can't add negative amounts. Use RemoveAsync method for that.", nameof(amount));
}
using (var uow = _db.GetDbContext())
{
InternalChange(userId, userName, discrim, avatar, reason, amount, gamble, uow);
await uow.SaveChangesAsync();
var t2 = GetCurrencyTransaction(_bot.Id, reason, -amount);
uow.CurrencyTransactions.Add(t2);
uow.TryUpdateCurrencyState(_bot.Id, _bot.Username, _bot.Discriminator, _bot.AvatarId, -amount, true);
}
}
return result;
}
public Task AddAsync(ulong userId, string reason, long amount, bool gamble = false)
private async Task InternalAddAsync(ulong userId, string userName, string discrim, string avatar, string reason, long amount, bool gamble)
{
if (amount < 0)
{
return InternalAddAsync(userId, null, null, null, reason, amount, gamble);
throw new ArgumentException("You can't add negative amounts. Use RemoveAsync method for that.", nameof(amount));
}
public async Task AddAsync(IUser user, string reason, long amount, bool sendMessage = false, bool gamble = false)
using (var uow = _db.GetDbContext())
{
await InternalAddAsync(user.Id, user.Username, user.Discriminator, user.AvatarId, reason, amount, gamble);
if (sendMessage)
{
try
{
var sign = _gss.Data.Currency.Sign;
await (await user.GetOrCreateDMChannelAsync())
.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)
{
ulong[] idArray = userIds as ulong[] ?? userIds.ToArray();
string[] reasonArray = reasons as string[] ?? reasons.ToArray();
long[] amountArray = amounts as long[] ?? amounts.ToArray();
if (idArray.Length != reasonArray.Length || reasonArray.Length != amountArray.Length)
throw new ArgumentException("Cannot perform bulk operation. Arrays are not of equal length.");
var userIdHashSet = new HashSet<ulong>(idArray.Length);
using (var uow = _db.GetDbContext())
{
for (int 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)
{
var idArray = userIds as ulong[] ?? userIds.ToArray();
var reasonArray = reasons as string[] ?? reasons.ToArray();
var amountArray = amounts as long[] ?? amounts.ToArray();
if (idArray.Length != reasonArray.Length || reasonArray.Length != amountArray.Length)
throw new ArgumentException("Cannot perform bulk operation. Arrays are not of equal length.");
var userIdHashSet = new HashSet<ulong>(idArray.Length);
using (var uow = _db.GetDbContext())
{
for (int 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)
{
if (amount < 0)
{
throw new ArgumentException("You can't remove negative amounts. Use AddAsync method for that.", nameof(amount));
}
bool result;
using (var uow = _db.GetDbContext())
{
result = InternalChange(userId, userName, userDiscrim, avatar, reason, -amount, gamble, uow);
await uow.SaveChangesAsync();
}
return result;
}
public Task<bool> RemoveAsync(ulong userId, string reason, long amount, bool gamble = false)
{
return InternalRemoveAsync(userId, null, null, null, reason, amount, gamble);
}
public Task<bool> RemoveAsync(IUser user, string reason, long amount, bool sendMessage = false, bool gamble = false)
{
return InternalRemoveAsync(user.Id, user.Username, user.Discriminator, user.AvatarId, reason, amount, gamble);
InternalChange(userId, userName, discrim, avatar, reason, amount, gamble, uow);
await uow.SaveChangesAsync();
}
}
}
public Task AddAsync(ulong userId, string reason, long amount, bool gamble = false)
{
return InternalAddAsync(userId, null, null, null, reason, amount, gamble);
}
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 (await user.GetOrCreateDMChannelAsync())
.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)
{
ulong[] idArray = userIds as ulong[] ?? userIds.ToArray();
string[] reasonArray = reasons as string[] ?? reasons.ToArray();
long[] amountArray = amounts as long[] ?? amounts.ToArray();
if (idArray.Length != reasonArray.Length || reasonArray.Length != amountArray.Length)
throw new ArgumentException("Cannot perform bulk operation. Arrays are not of equal length.");
var userIdHashSet = new HashSet<ulong>(idArray.Length);
using (var uow = _db.GetDbContext())
{
for (int 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)
{
var idArray = userIds as ulong[] ?? userIds.ToArray();
var reasonArray = reasons as string[] ?? reasons.ToArray();
var amountArray = amounts as long[] ?? amounts.ToArray();
if (idArray.Length != reasonArray.Length || reasonArray.Length != amountArray.Length)
throw new ArgumentException("Cannot perform bulk operation. Arrays are not of equal length.");
var userIdHashSet = new HashSet<ulong>(idArray.Length);
using (var uow = _db.GetDbContext())
{
for (int 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)
{
if (amount < 0)
{
throw new ArgumentException("You can't remove negative amounts. Use AddAsync method for that.", nameof(amount));
}
bool result;
using (var uow = _db.GetDbContext())
{
result = InternalChange(userId, userName, userDiscrim, avatar, reason, -amount, gamble, uow);
await uow.SaveChangesAsync();
}
return result;
}
public Task<bool> RemoveAsync(ulong userId, string reason, long amount, bool gamble = false)
{
return InternalRemoveAsync(userId, null, null, null, reason, amount, gamble);
}
public Task<bool> RemoveAsync(IUser user, string reason, long amount, bool sendMessage = false, bool gamble = false)
{
return InternalRemoveAsync(user.Id, user.Username, user.Discriminator, user.AvatarId, reason, amount, gamble);
}
}

View File

@@ -1,67 +1,63 @@
using SixLabors.Fonts;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
namespace NadekoBot.Services
namespace NadekoBot.Services;
public class FontProvider : INService
{
public class FontProvider : INService
private readonly FontCollection _fonts;
public FontProvider()
{
private readonly FontCollection _fonts;
_fonts = new FontCollection();
public FontProvider()
NotoSans = _fonts.Install("data/fonts/NotoSans-Bold.ttf");
UniSans = _fonts.Install("data/fonts/Uni Sans.ttf");
FallBackFonts = new List<FontFamily>();
//FallBackFonts.Add(_fonts.Install("data/fonts/OpenSansEmoji.ttf"));
// try loading some emoji and jap fonts on windows as fallback fonts
if (Environment.OSVersion.Platform == PlatformID.Win32NT)
{
_fonts = new FontCollection();
NotoSans = _fonts.Install("data/fonts/NotoSans-Bold.ttf");
UniSans = _fonts.Install("data/fonts/Uni Sans.ttf");
FallBackFonts = new List<FontFamily>();
//FallBackFonts.Add(_fonts.Install("data/fonts/OpenSansEmoji.ttf"));
// try loading some emoji and jap fonts on windows as fallback fonts
if (Environment.OSVersion.Platform == PlatformID.Win32NT)
try
{
try
{
string fontsfolder = System.Environment.GetFolderPath(System.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 { }
string fontsfolder = System.Environment.GetFolderPath(System.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")));
}
// 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));
}
}
RipFont = NotoSans.CreateFont(20, FontStyle.Bold);
DottyFont = FallBackFonts.First(x => x.Name == "dotty");
catch { }
}
public FontFamily DottyFont { get; }
// 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));
}
}
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; }
RipFont = NotoSans.CreateFont(20, FontStyle.Bold);
DottyFont = FallBackFonts.First(x => x.Name == "dotty");
}
}
public FontFamily DottyFont { get; }
public FontFamily UniSans { get; }
public FontFamily NotoSans { get; }
//public FontFamily Emojis { get; }
/// <summary>
/// Font used for .rip command
/// </summary>
public Font RipFont { get; }
public List<FontFamily> FallBackFonts { get; }
}

View File

@@ -7,385 +7,380 @@ using Google.Apis.YouTube.v3;
using NadekoBot.Common;
using NadekoBot.Extensions;
using Newtonsoft.Json.Linq;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using Serilog;
namespace NadekoBot.Services
namespace NadekoBot.Services;
public class GoogleApiService : IGoogleApiService, INService
{
public class GoogleApiService : IGoogleApiService, INService
{
private const string SearchEngineId = "018084019232060951019:hs5piey28-e";
private const string SearchEngineId = "018084019232060951019:hs5piey28-e";
private YouTubeService yt;
private UrlshortenerService sh;
private CustomsearchService cs;
private YouTubeService yt;
private UrlshortenerService sh;
private CustomsearchService cs;
public GoogleApiService(IBotCredentials creds, IHttpClientFactory factory)
{
_creds = creds;
_httpFactory = factory;
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 YouTubeService(bcs);
sh = new UrlshortenerService(bcs);
cs = new CustomsearchService(bcs);
}
private static readonly Regex plRegex = new Regex("(?:youtu\\.be\\/|list=)(?<id>[\\da-zA-Z\\-_]*)", RegexOptions.Compiled);
public async Task<IEnumerable<string>> GetPlaylistIdsByKeywordsAsync(string keywords, int count = 1)
yt = new YouTubeService(bcs);
sh = new UrlshortenerService(bcs);
cs = new CustomsearchService(bcs);
}
private static readonly Regex plRegex = new Regex("(?:youtu\\.be\\/|list=)(?<id>[\\da-zA-Z\\-_]*)", RegexOptions.Compiled);
public async Task<IEnumerable<string>> GetPlaylistIdsByKeywordsAsync(string keywords, int count = 1)
{
await Task.Yield();
if (string.IsNullOrWhiteSpace(keywords))
throw new ArgumentNullException(nameof(keywords));
if (count <= 0)
throw new ArgumentOutOfRangeException(nameof(count));
var match = plRegex.Match(keywords);
if (match.Length > 1)
{
await Task.Yield();
if (string.IsNullOrWhiteSpace(keywords))
throw new ArgumentNullException(nameof(keywords));
if (count <= 0)
throw new ArgumentOutOfRangeException(nameof(count));
var match = plRegex.Match(keywords);
if (match.Length > 1)
{
return new[] { match.Groups["id"].Value.ToString() };
}
var query = yt.Search.List("snippet");
query.MaxResults = count;
query.Type = "playlist";
query.Q = keywords;
return (await query.ExecuteAsync().ConfigureAwait(false)).Items.Select(i => i.Id.PlaylistId);
return new[] { match.Groups["id"].Value.ToString() };
}
var query = yt.Search.List("snippet");
query.MaxResults = count;
query.Type = "playlist";
query.Q = keywords;
//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;
return (await query.ExecuteAsync().ConfigureAwait(false)).Items.Select(i => i.Id.PlaylistId);
}
// todo future add quota users
public async Task<IEnumerable<string>> GetRelatedVideosAsync(string id, int count = 1)
//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)
{
await Task.Yield();
if (string.IsNullOrWhiteSpace(id))
throw new ArgumentNullException(nameof(id));
if (count <= 0)
throw new ArgumentOutOfRangeException(nameof(count));
var query = yt.Search.List("snippet");
query.MaxResults = count;
query.RelatedToVideoId = id;
query.Type = "video";
return (await query.ExecuteAsync().ConfigureAwait(false)).Items.Select(i => "http://www.youtube.com/watch?v=" + i.Id.VideoId);
}
public async Task<IEnumerable<string>> GetVideoLinksByKeywordAsync(string keywords, int count = 1)
{
await Task.Yield();
if (string.IsNullOrWhiteSpace(keywords))
throw new ArgumentNullException(nameof(keywords));
if (count <= 0)
throw new ArgumentOutOfRangeException(nameof(count));
var query = yt.Search.List("snippet");
query.MaxResults = count;
query.Q = keywords;
query.Type = "video";
query.SafeSearch = SearchResource.ListRequest.SafeSearchEnum.Strict;
return (await query.ExecuteAsync().ConfigureAwait(false)).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)
{
await Task.Yield();
if (string.IsNullOrWhiteSpace(keywords))
throw new ArgumentNullException(nameof(keywords));
if (count <= 0)
throw new ArgumentOutOfRangeException(nameof(count));
var query = yt.Search.List("snippet");
query.MaxResults = count;
query.Q = keywords;
query.Type = "video";
return (await query.ExecuteAsync().ConfigureAwait(false)).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 async Task<string> ShortenUrl(string url)
{
await Task.Yield();
if (string.IsNullOrWhiteSpace(url))
throw new ArgumentNullException(nameof(url));
if (string.IsNullOrWhiteSpace(_creds.GoogleApiKey))
return url;
try
{
await Task.Yield();
if (string.IsNullOrWhiteSpace(id))
throw new ArgumentNullException(nameof(id));
if (count <= 0)
throw new ArgumentOutOfRangeException(nameof(count));
var query = yt.Search.List("snippet");
query.MaxResults = count;
query.RelatedToVideoId = id;
query.Type = "video";
return (await query.ExecuteAsync().ConfigureAwait(false)).Items.Select(i => "http://www.youtube.com/watch?v=" + i.Id.VideoId);
var response = await sh.Url.Insert(new Url { LongUrl = url }).ExecuteAsync().ConfigureAwait(false);
return response.Id;
}
public async Task<IEnumerable<string>> GetVideoLinksByKeywordAsync(string keywords, int count = 1)
catch (GoogleApiException ex) when (ex.HttpStatusCode == HttpStatusCode.Forbidden)
{
await Task.Yield();
if (string.IsNullOrWhiteSpace(keywords))
throw new ArgumentNullException(nameof(keywords));
if (count <= 0)
throw new ArgumentOutOfRangeException(nameof(count));
var query = yt.Search.List("snippet");
query.MaxResults = count;
query.Q = keywords;
query.Type = "video";
query.SafeSearch = SearchResource.ListRequest.SafeSearchEnum.Strict;
return (await query.ExecuteAsync().ConfigureAwait(false)).Items.Select(i => "http://www.youtube.com/watch?v=" + i.Id.VideoId);
return url;
}
public async Task<IEnumerable<(string Name, string Id, string Url)>> GetVideoInfosByKeywordAsync(string keywords, int count = 1)
catch (Exception ex)
{
await Task.Yield();
if (string.IsNullOrWhiteSpace(keywords))
throw new ArgumentNullException(nameof(keywords));
if (count <= 0)
throw new ArgumentOutOfRangeException(nameof(count));
var query = yt.Search.List("snippet");
query.MaxResults = count;
query.Q = keywords;
query.Type = "video";
return (await query.ExecuteAsync().ConfigureAwait(false)).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 async Task<string> ShortenUrl(string url)
{
await Task.Yield();
if (string.IsNullOrWhiteSpace(url))
throw new ArgumentNullException(nameof(url));
if (string.IsNullOrWhiteSpace(_creds.GoogleApiKey))
return url;
try
{
var response = await sh.Url.Insert(new Url { LongUrl = url }).ExecuteAsync().ConfigureAwait(false);
return response.Id;
}
catch (GoogleApiException ex) when (ex.HttpStatusCode == HttpStatusCode.Forbidden)
{
return url;
}
catch (Exception ex)
{
Log.Warning(ex, "Error shortening URL");
return url;
}
}
public async Task<IEnumerable<string>> GetPlaylistTracksAsync(string playlistId, int count = 50)
{
await Task.Yield();
if (string.IsNullOrWhiteSpace(playlistId))
throw new ArgumentNullException(nameof(playlistId));
if (count <= 0)
throw new ArgumentOutOfRangeException(nameof(count));
string nextPageToken = null;
List<string> toReturn = new List<string>(count);
do
{
var toGet = count > 50 ? 50 : count;
count -= toGet;
var query = yt.PlaylistItems.List("contentDetails");
query.MaxResults = toGet;
query.PlaylistId = playlistId;
query.PageToken = nextPageToken;
var data = await query.ExecuteAsync().ConfigureAwait(false);
toReturn.AddRange(data.Items.Select(i => i.ContentDetails.VideoId));
nextPageToken = data.NextPageToken;
}
while (count > 0 && !string.IsNullOrWhiteSpace(nextPageToken));
return toReturn;
}
public async Task<IReadOnlyDictionary<string, TimeSpan>> GetVideoDurationsAsync(IEnumerable<string> videoIds)
{
await Task.Yield();
var videoIdsList = videoIds as List<string> ?? videoIds.ToList();
Dictionary<string, TimeSpan> toReturn = new Dictionary<string, TimeSpan>();
if (!videoIdsList.Any())
return toReturn;
var remaining = videoIdsList.Count;
do
{
var toGet = remaining > 50 ? 50 : remaining;
remaining -= toGet;
var q = yt.Videos.List("contentDetails");
q.Id = string.Join(",", videoIdsList.Take(toGet));
videoIdsList = videoIdsList.Skip(toGet).ToList();
var items = (await q.ExecuteAsync().ConfigureAwait(false)).Items;
foreach (var i in items)
{
toReturn.Add(i.Id, System.Xml.XmlConvert.ToTimeSpan(i.ContentDetails.Duration));
}
}
while (remaining > 0);
return toReturn;
}
public async Task<ImageResult> GetImageAsync(string query)
{
await Task.Yield();
if (string.IsNullOrWhiteSpace(query))
throw new ArgumentNullException(nameof(query));
var req = cs.Cse.List();
req.Q = query;
req.Cx = SearchEngineId;
req.Num = 1;
req.Fields = "items(image(contextLink,thumbnailLink),link)";
req.SearchType = CseResource.ListRequest.SearchTypeEnum.Image;
req.Start = new NadekoRandom().Next(0, 20);
var search = await req.ExecuteAsync().ConfigureAwait(false);
return new ImageResult(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))
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}",
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");
text = await http.GetStringAsync(url).ConfigureAwait(false);
}
return (string.Concat(JArray.Parse(text)[0].Select(x => x[0])));
}
private string ConvertToLanguageCode(string language)
{
Languages.TryGetValue(language, out var mode);
return mode;
Log.Warning(ex, "Error shortening URL");
return url;
}
}
public async Task<IEnumerable<string>> GetPlaylistTracksAsync(string playlistId, int count = 50)
{
await Task.Yield();
if (string.IsNullOrWhiteSpace(playlistId))
throw new ArgumentNullException(nameof(playlistId));
if (count <= 0)
throw new ArgumentOutOfRangeException(nameof(count));
string nextPageToken = null;
List<string> toReturn = new List<string>(count);
do
{
var toGet = count > 50 ? 50 : count;
count -= toGet;
var query = yt.PlaylistItems.List("contentDetails");
query.MaxResults = toGet;
query.PlaylistId = playlistId;
query.PageToken = nextPageToken;
var data = await query.ExecuteAsync().ConfigureAwait(false);
toReturn.AddRange(data.Items.Select(i => i.ContentDetails.VideoId));
nextPageToken = data.NextPageToken;
}
while (count > 0 && !string.IsNullOrWhiteSpace(nextPageToken));
return toReturn;
}
public async Task<IReadOnlyDictionary<string, TimeSpan>> GetVideoDurationsAsync(IEnumerable<string> videoIds)
{
await Task.Yield();
var videoIdsList = videoIds as List<string> ?? videoIds.ToList();
Dictionary<string, TimeSpan> toReturn = new Dictionary<string, TimeSpan>();
if (!videoIdsList.Any())
return toReturn;
var remaining = videoIdsList.Count;
do
{
var toGet = remaining > 50 ? 50 : remaining;
remaining -= toGet;
var q = yt.Videos.List("contentDetails");
q.Id = string.Join(",", videoIdsList.Take(toGet));
videoIdsList = videoIdsList.Skip(toGet).ToList();
var items = (await q.ExecuteAsync().ConfigureAwait(false)).Items;
foreach (var i in items)
{
toReturn.Add(i.Id, System.Xml.XmlConvert.ToTimeSpan(i.ContentDetails.Duration));
}
}
while (remaining > 0);
return toReturn;
}
public async Task<ImageResult> GetImageAsync(string query)
{
await Task.Yield();
if (string.IsNullOrWhiteSpace(query))
throw new ArgumentNullException(nameof(query));
var req = cs.Cse.List();
req.Q = query;
req.Cx = SearchEngineId;
req.Num = 1;
req.Fields = "items(image(contextLink,thumbnailLink),link)";
req.SearchType = CseResource.ListRequest.SearchTypeEnum.Image;
req.Start = new NadekoRandom().Next(0, 20);
var search = await req.ExecuteAsync().ConfigureAwait(false);
return new ImageResult(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))
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}",
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");
text = await http.GetStringAsync(url).ConfigureAwait(false);
}
return (string.Concat(JArray.Parse(text)[0].Select(x => x[0])));
}
private string ConvertToLanguageCode(string language)
{
Languages.TryGetValue(language, out var mode);
return mode;
}
}

View File

@@ -1,123 +1,119 @@
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using Discord;
using NadekoBot.Common;
using Newtonsoft.Json;
using System.IO;
using NadekoBot.Db;
using NadekoBot.Modules.Administration;
namespace NadekoBot.Services
namespace NadekoBot.Services;
// todo future use guild locale more in the code (from guild settings) (for dates, currency, etc?)
public class Localization : ILocalization, INService
{
// todo future use guild locale more in the code (from guild settings) (for dates, currency, etc?)
public class Localization : ILocalization, INService
private readonly BotConfigService _bss;
private readonly DbService _db;
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 Localization(BotConfigService bss, Bot bot, DbService db)
{
private readonly BotConfigService _bss;
private readonly DbService _db;
_bss = bss;
_db = db;
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 Localization(BotConfigService bss, Bot bot, DbService db)
{
_bss = bss;
_db = db;
var cultureInfoNames = bot.AllGuildConfigs
.ToDictionary(x => x.GuildId, x => x.Locale);
var cultureInfoNames = bot.AllGuildConfigs
.ToDictionary(x => x.GuildId, x => x.Locale);
GuildCultureInfos = new ConcurrentDictionary<ulong, CultureInfo>(cultureInfoNames.ToDictionary(x => x.Key, x =>
{
CultureInfo cultureInfo = null;
try
{
if (x.Value is null)
return null;
cultureInfo = new CultureInfo(x.Value);
}
catch { }
return cultureInfo;
}).Where(x => x.Value != null));
GuildCultureInfos = new ConcurrentDictionary<ulong, CultureInfo>(cultureInfoNames.ToDictionary(x => x.Key, x =>
{
CultureInfo cultureInfo = null;
try
{
if (x.Value is null)
return null;
cultureInfo = new CultureInfo(x.Value);
}
catch { }
return cultureInfo;
}).Where(x => x.Value != null));
}
public void SetGuildCulture(IGuild guild, CultureInfo ci) =>
SetGuildCulture(guild.Id, ci);
public void SetGuildCulture(ulong guildId, CultureInfo ci)
{
if (ci.Name == _bss.Data.DefaultLocale.Name)
{
RemoveGuildCulture(guildId);
return;
}
public void SetGuildCulture(IGuild guild, CultureInfo ci) =>
SetGuildCulture(guild.Id, ci);
public void SetGuildCulture(ulong guildId, CultureInfo ci)
using (var uow = _db.GetDbContext())
{
if (ci.Name == _bss.Data.DefaultLocale.Name)
{
RemoveGuildCulture(guildId);
return;
}
var gc = uow.GuildConfigsForId(guildId, set => set);
gc.Locale = ci.Name;
uow.SaveChanges();
}
GuildCultureInfos.AddOrUpdate(guildId, ci, (id, old) => ci);
}
public void RemoveGuildCulture(IGuild guild) =>
RemoveGuildCulture(guild.Id);
public void RemoveGuildCulture(ulong guildId)
{
if (GuildCultureInfos.TryRemove(guildId, out var _))
{
using (var uow = _db.GetDbContext())
{
var gc = uow.GuildConfigsForId(guildId, set => set);
gc.Locale = ci.Name;
gc.Locale = null;
uow.SaveChanges();
}
GuildCultureInfos.AddOrUpdate(guildId, ci, (id, old) => ci);
}
public void RemoveGuildCulture(IGuild guild) =>
RemoveGuildCulture(guild.Id);
public void RemoveGuildCulture(ulong guildId)
{
if (GuildCultureInfos.TryRemove(guildId, out var _))
{
using (var uow = _db.GetDbContext())
{
var gc = uow.GuildConfigsForId(guildId, set => set);
gc.Locale = null;
uow.SaveChanges();
}
}
}
public void SetDefaultCulture(CultureInfo ci)
{
_bss.ModifyConfig(bs =>
{
bs.DefaultLocale = ci;
});
}
public void ResetDefaultCulture() =>
SetDefaultCulture(CultureInfo.CurrentCulture);
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;
}
public static CommandData LoadCommand(string key)
{
_commandData.TryGetValue(key, out var toReturn);
if (toReturn is null)
return new CommandData
{
Cmd = key,
Desc = key,
Usage = new[] { key },
};
return toReturn;
}
}
}
public void SetDefaultCulture(CultureInfo ci)
{
_bss.ModifyConfig(bs =>
{
bs.DefaultLocale = ci;
});
}
public void ResetDefaultCulture() =>
SetDefaultCulture(CultureInfo.CurrentCulture);
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;
}
public static CommandData LoadCommand(string key)
{
_commandData.TryGetValue(key, out var toReturn);
if (toReturn is null)
return new CommandData
{
Cmd = key,
Desc = key,
Usage = new[] { key },
};
return toReturn;
}
}

View File

@@ -1,214 +1,210 @@
using NadekoBot.Extensions;
using Newtonsoft.Json;
using StackExchange.Redis;
using System;
using System.Linq;
using System.Net;
using System.Threading.Tasks;
using Discord.WebSocket;
namespace NadekoBot.Services
namespace NadekoBot.Services;
public class RedisCache : IDataCache
{
public class RedisCache : IDataCache
public ConnectionMultiplexer Redis { get; }
public IImageCache LocalImages { get; }
public ILocalDataCache LocalData { get; }
private readonly string _redisKey;
private readonly EndPoint _redisEndpoint;
public RedisCache(ConnectionMultiplexer redis, IBotCredentials creds,
IImageCache imageCache, ILocalDataCache dataCache)
{
public ConnectionMultiplexer Redis { get; }
Redis = redis;
_redisEndpoint = Redis.GetEndPoints().First();
LocalImages = imageCache;
LocalData = dataCache;
_redisKey = creds.RedisKey();
}
public IImageCache LocalImages { get; }
public ILocalDataCache LocalData { get; }
// things here so far don't need the bot id
// because it's a good thing if different bots
// which are hosted on the same PC
// can re-use the same image/anime data
public async Task<(bool Success, byte[] Data)> TryGetImageDataAsync(Uri key)
{
var _db = Redis.GetDatabase();
byte[] x = await _db.StringGetAsync("image_" + key).ConfigureAwait(false);
return (x != null, x);
}
private readonly string _redisKey;
private readonly EndPoint _redisEndpoint;
public Task SetImageDataAsync(Uri key, byte[] data)
{
var _db = Redis.GetDatabase();
return _db.StringSetAsync("image_" + key, data);
}
public RedisCache(ConnectionMultiplexer redis, IBotCredentials creds,
IImageCache imageCache, ILocalDataCache dataCache)
{
Redis = redis;
_redisEndpoint = Redis.GetEndPoints().First();
LocalImages = imageCache;
LocalData = dataCache;
_redisKey = creds.RedisKey();
}
// things here so far don't need the bot id
// because it's a good thing if different bots
// which are hosted on the same PC
// can re-use the same image/anime data
public async Task<(bool Success, byte[] Data)> TryGetImageDataAsync(Uri key)
public async Task<(bool Success, string Data)> TryGetAnimeDataAsync(string key)
{
var _db = Redis.GetDatabase();
string x = await _db.StringGetAsync("anime_" + key).ConfigureAwait(false);
return (x != null, x);
}
public Task SetAnimeDataAsync(string key, string data)
{
var _db = Redis.GetDatabase();
return _db.StringSetAsync("anime_" + key, data, expiry: TimeSpan.FromHours(3));
}
public async Task<(bool Success, string Data)> TryGetNovelDataAsync(string key)
{
var _db = Redis.GetDatabase();
string x = await _db.StringGetAsync("novel_" + key).ConfigureAwait(false);
return (x != null, x);
}
public Task SetNovelDataAsync(string key, string data)
{
var _db = Redis.GetDatabase();
return _db.StringSetAsync("novel_" + key, data, expiry: TimeSpan.FromHours(3));
}
private readonly object timelyLock = new object();
public TimeSpan? AddTimelyClaim(ulong id, int period)
{
if (period == 0)
return null;
lock (timelyLock)
{
var time = TimeSpan.FromHours(period);
var _db = Redis.GetDatabase();
byte[] x = await _db.StringGetAsync("image_" + key).ConfigureAwait(false);
return (x != null, x);
}
public Task SetImageDataAsync(Uri key, byte[] data)
{
var _db = Redis.GetDatabase();
return _db.StringSetAsync("image_" + key, data);
}
public async Task<(bool Success, string Data)> TryGetAnimeDataAsync(string key)
{
var _db = Redis.GetDatabase();
string x = await _db.StringGetAsync("anime_" + key).ConfigureAwait(false);
return (x != null, x);
}
public Task SetAnimeDataAsync(string key, string data)
{
var _db = Redis.GetDatabase();
return _db.StringSetAsync("anime_" + key, data, expiry: TimeSpan.FromHours(3));
}
public async Task<(bool Success, string Data)> TryGetNovelDataAsync(string key)
{
var _db = Redis.GetDatabase();
string x = await _db.StringGetAsync("novel_" + key).ConfigureAwait(false);
return (x != null, x);
}
public Task SetNovelDataAsync(string key, string data)
{
var _db = Redis.GetDatabase();
return _db.StringSetAsync("novel_" + key, data, expiry: TimeSpan.FromHours(3));
}
private readonly object timelyLock = new object();
public TimeSpan? AddTimelyClaim(ulong id, int period)
{
if (period == 0)
if ((bool?)_db.StringGet($"{_redisKey}_timelyclaim_{id}") is null)
{
_db.StringSet($"{_redisKey}_timelyclaim_{id}", true, time);
return null;
lock (timelyLock)
{
var time = TimeSpan.FromHours(period);
var _db = Redis.GetDatabase();
if ((bool?)_db.StringGet($"{_redisKey}_timelyclaim_{id}") is null)
{
_db.StringSet($"{_redisKey}_timelyclaim_{id}", true, time);
return null;
}
return _db.KeyTimeToLive($"{_redisKey}_timelyclaim_{id}");
}
return _db.KeyTimeToLive($"{_redisKey}_timelyclaim_{id}");
}
}
public void RemoveAllTimelyClaims()
public void RemoveAllTimelyClaims()
{
var server = Redis.GetServer(_redisEndpoint);
var _db = Redis.GetDatabase();
foreach (var k in server.Keys(pattern: $"{_redisKey}_timelyclaim_*"))
{
var server = Redis.GetServer(_redisEndpoint);
var _db = Redis.GetDatabase();
foreach (var k in server.Keys(pattern: $"{_redisKey}_timelyclaim_*"))
{
_db.KeyDelete(k, CommandFlags.FireAndForget);
}
_db.KeyDelete(k, CommandFlags.FireAndForget);
}
}
public bool TryAddAffinityCooldown(ulong userId, out TimeSpan? time)
public bool TryAddAffinityCooldown(ulong userId, out TimeSpan? time)
{
var _db = Redis.GetDatabase();
time = _db.KeyTimeToLive($"{_redisKey}_affinity_{userId}");
if (time is null)
{
var _db = Redis.GetDatabase();
time = _db.KeyTimeToLive($"{_redisKey}_affinity_{userId}");
if (time is null)
{
time = TimeSpan.FromMinutes(30);
_db.StringSet($"{_redisKey}_affinity_{userId}", true, time);
return true;
}
return false;
time = TimeSpan.FromMinutes(30);
_db.StringSet($"{_redisKey}_affinity_{userId}", true, time);
return true;
}
return false;
}
public bool TryAddDivorceCooldown(ulong userId, out TimeSpan? time)
public bool TryAddDivorceCooldown(ulong userId, out TimeSpan? time)
{
var _db = Redis.GetDatabase();
time = _db.KeyTimeToLive($"{_redisKey}_divorce_{userId}");
if (time is null)
{
var _db = Redis.GetDatabase();
time = _db.KeyTimeToLive($"{_redisKey}_divorce_{userId}");
if (time is null)
{
time = TimeSpan.FromHours(6);
_db.StringSet($"{_redisKey}_divorce_{userId}", true, time);
return true;
}
return false;
time = TimeSpan.FromHours(6);
_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));
}
public Task SetStreamDataAsync(string url, string data)
{
var _db = Redis.GetDatabase();
return _db.StringSetAsync($"{_redisKey}_stream_{url}", data, expiry: TimeSpan.FromHours(6));
}
public bool TryGetStreamData(string url, out string dataStr)
{
var _db = Redis.GetDatabase();
dataStr = _db.StringGet($"{_redisKey}_stream_{url}");
public bool TryGetStreamData(string url, out string dataStr)
{
var _db = Redis.GetDatabase();
dataStr = _db.StringGet($"{_redisKey}_stream_{url}");
return !string.IsNullOrWhiteSpace(dataStr);
}
return !string.IsNullOrWhiteSpace(dataStr);
}
public TimeSpan? TryAddRatelimit(ulong id, string name, int expireIn)
{
var _db = Redis.GetDatabase();
if (_db.StringSet($"{_redisKey}_ratelimit_{id}_{name}",
public TimeSpan? TryAddRatelimit(ulong id, string name, int expireIn)
{
var _db = Redis.GetDatabase();
if (_db.StringSet($"{_redisKey}_ratelimit_{id}_{name}",
0, // i don't use the value
TimeSpan.FromSeconds(expireIn),
When.NotExists))
{
return null;
}
return _db.KeyTimeToLive($"{_redisKey}_ratelimit_{id}_{name}");
}
public bool TryGetEconomy(out string data)
{
var _db = Redis.GetDatabase();
if ((data = _db.StringGet($"{_redisKey}_economy")) != null)
{
return true;
}
return false;
return null;
}
public void SetEconomy(string data)
{
var _db = Redis.GetDatabase();
_db.StringSet($"{_redisKey}_economy",
data,
expiry: TimeSpan.FromMinutes(3));
}
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();
RedisValue data = await _db.StringGetAsync(key).ConfigureAwait(false);
if (!data.HasValue)
{
var obj = await factory(param).ConfigureAwait(false);
if (obj is null)
return default(TOut);
await _db.StringSetAsync(key, JsonConvert.SerializeObject(obj),
expiry: expiry).ConfigureAwait(false);
return obj;
}
return (TOut)JsonConvert.DeserializeObject(data, typeof(TOut));
}
public DateTime GetLastCurrencyDecay()
{
var db = Redis.GetDatabase();
var str = (string)db.StringGet($"{_redisKey}_last_currency_decay");
if(string.IsNullOrEmpty(str))
return DateTime.MinValue;
return JsonConvert.DeserializeObject<DateTime>(str);
}
public void SetLastCurrencyDecay()
{
var db = Redis.GetDatabase();
db.StringSet($"{_redisKey}_last_currency_decay", JsonConvert.SerializeObject(DateTime.UtcNow));
}
return _db.KeyTimeToLive($"{_redisKey}_ratelimit_{id}_{name}");
}
}
public bool TryGetEconomy(out string data)
{
var _db = Redis.GetDatabase();
if ((data = _db.StringGet($"{_redisKey}_economy")) != null)
{
return true;
}
return false;
}
public void SetEconomy(string data)
{
var _db = Redis.GetDatabase();
_db.StringSet($"{_redisKey}_economy",
data,
expiry: TimeSpan.FromMinutes(3));
}
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();
RedisValue data = await _db.StringGetAsync(key).ConfigureAwait(false);
if (!data.HasValue)
{
var obj = await factory(param).ConfigureAwait(false);
if (obj is null)
return default(TOut);
await _db.StringSetAsync(key, JsonConvert.SerializeObject(obj),
expiry: expiry).ConfigureAwait(false);
return obj;
}
return (TOut)JsonConvert.DeserializeObject(data, typeof(TOut));
}
public DateTime GetLastCurrencyDecay()
{
var db = Redis.GetDatabase();
var str = (string)db.StringGet($"{_redisKey}_last_currency_decay");
if(string.IsNullOrEmpty(str))
return DateTime.MinValue;
return JsonConvert.DeserializeObject<DateTime>(str);
}
public void SetLastCurrencyDecay()
{
var db = Redis.GetDatabase();
db.StringSet($"{_redisKey}_last_currency_decay", JsonConvert.SerializeObject(DateTime.UtcNow));
}
}

View File

@@ -1,13 +1,10 @@
using System;
namespace NadekoBot.Services;
namespace NadekoBot.Services
public static class RedisImageExtensions
{
public static class RedisImageExtensions
{
private const string OldCdnUrl = "nadeko-pictures.nyc3.digitaloceanspaces.com";
private const string NewCdnUrl = "cdn.nadeko.bot";
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));
}
public static Uri ToNewCdn(this Uri uri)
=> new(uri.ToString().Replace(OldCdnUrl, NewCdnUrl));
}

View File

@@ -2,299 +2,294 @@
using NadekoBot.Extensions;
using Newtonsoft.Json;
using StackExchange.Redis;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net.Http;
using System.Threading.Tasks;
using NadekoBot.Common.ModuleBehaviors;
using NadekoBot.Common.Yml;
using Serilog;
namespace NadekoBot.Services
namespace NadekoBot.Services;
public sealed class RedisImagesCache : IImageCache, IReadyExecutor
{
public sealed class RedisImagesCache : IImageCache, IReadyExecutor
{
private readonly ConnectionMultiplexer _con;
private readonly IBotCredentials _creds;
private readonly HttpClient _http;
private readonly string _imagesPath;
private readonly ConnectionMultiplexer _con;
private readonly IBotCredentials _creds;
private readonly HttpClient _http;
private readonly string _imagesPath;
private IDatabase _db => _con.GetDatabase();
private IDatabase _db => _con.GetDatabase();
private const string _basePath = "data/";
private const string _cardsPath = "data/images/cards";
private const string _basePath = "data/";
private const string _cardsPath = "data/images/cards";
public ImageUrls ImageUrls { get; private set; }
public ImageUrls ImageUrls { get; private set; }
public enum ImageKeys
public enum ImageKeys
{
CoinHeads,
CoinTails,
Dice,
SlotBg,
SlotEmojis,
Currency,
RategirlMatrix,
RategirlDot,
RipOverlay,
RipBg,
XpBg
}
public IReadOnlyList<byte[]> Heads
=> GetByteArrayData(ImageKeys.CoinHeads);
public IReadOnlyList<byte[]> Tails
=> GetByteArrayData(ImageKeys.CoinTails);
public IReadOnlyList<byte[]> Dice
=> GetByteArrayData(ImageKeys.Dice);
public IReadOnlyList<byte[]> SlotEmojis
=> GetByteArrayData(ImageKeys.SlotEmojis);
public IReadOnlyList<byte[]> Currency
=> GetByteArrayData(ImageKeys.Currency);
public byte[] SlotBackground
=> GetByteData(ImageKeys.SlotBg);
public byte[] RategirlMatrix
=> GetByteData(ImageKeys.RategirlMatrix);
public byte[] RategirlDot
=> GetByteData(ImageKeys.RategirlDot);
public byte[] XpBackground
=> GetByteData(ImageKeys.XpBg);
public byte[] Rip
=> GetByteData(ImageKeys.RipBg);
public byte[] RipOverlay
=> GetByteData(ImageKeys.RipOverlay);
public byte[] GetCard(string key)
{
// since cards are always local for now, don't cache them
return File.ReadAllBytes(Path.Join(_cardsPath, key + ".jpg"));
}
public async Task OnReadyAsync()
{
if (await AllKeysExist())
return;
await Reload();
}
public RedisImagesCache(ConnectionMultiplexer con, IBotCredentials creds)
{
_con = con;
_creds = creds;
_http = new HttpClient();
_imagesPath = Path.Combine(_basePath, "images.yml");
Migrate();
ImageUrls = Yaml.Deserializer.Deserialize<ImageUrls>(File.ReadAllText(_imagesPath));
}
private void Migrate()
{
// migrate to yml
if (File.Exists(Path.Combine(_basePath, "images.json")))
{
CoinHeads,
CoinTails,
Dice,
SlotBg,
SlotEmojis,
Currency,
RategirlMatrix,
RategirlDot,
RipOverlay,
RipBg,
XpBg
}
public IReadOnlyList<byte[]> Heads
=> GetByteArrayData(ImageKeys.CoinHeads);
public IReadOnlyList<byte[]> Tails
=> GetByteArrayData(ImageKeys.CoinTails);
public IReadOnlyList<byte[]> Dice
=> GetByteArrayData(ImageKeys.Dice);
public IReadOnlyList<byte[]> SlotEmojis
=> GetByteArrayData(ImageKeys.SlotEmojis);
public IReadOnlyList<byte[]> Currency
=> GetByteArrayData(ImageKeys.Currency);
public byte[] SlotBackground
=> GetByteData(ImageKeys.SlotBg);
public byte[] RategirlMatrix
=> GetByteData(ImageKeys.RategirlMatrix);
public byte[] RategirlDot
=> GetByteData(ImageKeys.RategirlDot);
public byte[] XpBackground
=> GetByteData(ImageKeys.XpBg);
public byte[] Rip
=> GetByteData(ImageKeys.RipBg);
public byte[] RipOverlay
=> GetByteData(ImageKeys.RipOverlay);
public byte[] GetCard(string key)
{
// since cards are always local for now, don't cache them
return File.ReadAllBytes(Path.Join(_cardsPath, key + ".jpg"));
}
public async Task OnReadyAsync()
{
if (await AllKeysExist())
return;
await Reload();
}
public RedisImagesCache(ConnectionMultiplexer con, IBotCredentials creds)
{
_con = con;
_creds = creds;
_http = new HttpClient();
_imagesPath = Path.Combine(_basePath, "images.yml");
Migrate();
ImageUrls = Yaml.Deserializer.Deserialize<ImageUrls>(File.ReadAllText(_imagesPath));
}
private void Migrate()
{
// migrate to yml
if (File.Exists(Path.Combine(_basePath, "images.json")))
{
var oldFilePath = Path.Combine(_basePath, "images.json");
var backupFilePath = Path.Combine(_basePath, "images.json.backup");
var oldFilePath = Path.Combine(_basePath, "images.json");
var backupFilePath = Path.Combine(_basePath, "images.json.backup");
var oldData = JsonConvert.DeserializeObject<OldImageUrls>(
File.ReadAllText(oldFilePath));
var oldData = JsonConvert.DeserializeObject<OldImageUrls>(
File.ReadAllText(oldFilePath));
if (oldData is not null)
if (oldData is not null)
{
var newData = new ImageUrls()
{
var newData = new ImageUrls()
Coins = new ImageUrls.CoinData()
{
Coins = new ImageUrls.CoinData()
{
Heads = oldData.Coins.Heads.Length == 1 &&
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,
},
Dice = oldData.Dice.Map(x => x.ToNewCdn()),
Currency = oldData.Currency.Map(x => x.ToNewCdn()),
Rategirl = new ImageUrls.RategirlData()
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,
},
Dice = oldData.Dice.Map(x => x.ToNewCdn()),
Currency = oldData.Currency.Map(x => x.ToNewCdn()),
Rategirl = new ImageUrls.RategirlData()
{
Dot = oldData.Rategirl.Dot.ToNewCdn(),
Matrix = oldData.Rategirl.Matrix.ToNewCdn()
},
Rip = new ImageUrls.RipData()
{
Bg = oldData.Rip.Bg.ToNewCdn(),
Overlay = oldData.Rip.Overlay.ToNewCdn(),
},
Slots = new ImageUrls.SlotData()
{
Bg = new Uri("https://cdn.nadeko.bot/slots/slots_bg.png"),
Emojis = new[]
{
Dot = oldData.Rategirl.Dot.ToNewCdn(),
Matrix = oldData.Rategirl.Matrix.ToNewCdn()
},
Rip = new ImageUrls.RipData()
{
Bg = oldData.Rip.Bg.ToNewCdn(),
Overlay = oldData.Rip.Overlay.ToNewCdn(),
},
Slots = new ImageUrls.SlotData()
{
Bg = new Uri("https://cdn.nadeko.bot/slots/slots_bg.png"),
Emojis = new[]
{
"https://cdn.nadeko.bot/slots/0.png",
"https://cdn.nadeko.bot/slots/1.png",
"https://cdn.nadeko.bot/slots/2.png",
"https://cdn.nadeko.bot/slots/3.png",
"https://cdn.nadeko.bot/slots/4.png",
"https://cdn.nadeko.bot/slots/5.png"
}.Map(x => new Uri(x))
},
Xp = new ImageUrls.XpData()
{
Bg = oldData.Xp.Bg.ToNewCdn(),
},
Version = 2,
};
"https://cdn.nadeko.bot/slots/0.png",
"https://cdn.nadeko.bot/slots/1.png",
"https://cdn.nadeko.bot/slots/2.png",
"https://cdn.nadeko.bot/slots/3.png",
"https://cdn.nadeko.bot/slots/4.png",
"https://cdn.nadeko.bot/slots/5.png"
}.Map(x => new Uri(x))
},
Xp = new ImageUrls.XpData()
{
Bg = oldData.Xp.Bg.ToNewCdn(),
},
Version = 2,
};
File.Move(oldFilePath, backupFilePath, true);
File.WriteAllText(_imagesPath, Yaml.Serializer.Serialize(newData));
}
File.Move(oldFilePath, backupFilePath, true);
File.WriteAllText(_imagesPath, Yaml.Serializer.Serialize(newData));
}
}
// removed numbers from slots
var localImageUrls = Yaml.Deserializer.Deserialize<ImageUrls>(File.ReadAllText(_imagesPath));
if (localImageUrls.Version == 2)
// removed numbers from slots
var localImageUrls = Yaml.Deserializer.Deserialize<ImageUrls>(File.ReadAllText(_imagesPath));
if (localImageUrls.Version == 2)
{
localImageUrls.Version = 3;
File.WriteAllText(_imagesPath, Yaml.Serializer.Serialize(localImageUrls));
}
}
public async Task Reload()
{
ImageUrls = Yaml.Deserializer.Deserialize<ImageUrls>(await File.ReadAllTextAsync(_imagesPath));
foreach (var key in GetAllKeys())
{
switch (key)
{
localImageUrls.Version = 3;
File.WriteAllText(_imagesPath, Yaml.Serializer.Serialize(localImageUrls));
case ImageKeys.CoinHeads:
await Load(key, ImageUrls.Coins.Heads);
break;
case ImageKeys.CoinTails:
await Load(key, ImageUrls.Coins.Tails);
break;
case ImageKeys.Dice:
await Load(key, ImageUrls.Dice);
break;
case ImageKeys.SlotBg:
await Load(key, ImageUrls.Slots.Bg);
break;
case ImageKeys.SlotEmojis:
await Load(key, ImageUrls.Slots.Emojis);
break;
case ImageKeys.Currency:
await Load(key, ImageUrls.Currency);
break;
case ImageKeys.RategirlMatrix:
await Load(key, ImageUrls.Rategirl.Matrix);
break;
case ImageKeys.RategirlDot:
await Load(key, ImageUrls.Rategirl.Dot);
break;
case ImageKeys.RipOverlay:
await Load(key, ImageUrls.Rip.Overlay);
break;
case ImageKeys.RipBg:
await Load(key, ImageUrls.Rip.Bg);
break;
case ImageKeys.XpBg:
await Load(key, ImageUrls.Xp.Bg);
break;
default:
throw new ArgumentOutOfRangeException();
}
}
}
public async Task Reload()
{
ImageUrls = Yaml.Deserializer.Deserialize<ImageUrls>(await File.ReadAllTextAsync(_imagesPath));
foreach (var key in GetAllKeys())
{
switch (key)
{
case ImageKeys.CoinHeads:
await Load(key, ImageUrls.Coins.Heads);
break;
case ImageKeys.CoinTails:
await Load(key, ImageUrls.Coins.Tails);
break;
case ImageKeys.Dice:
await Load(key, ImageUrls.Dice);
break;
case ImageKeys.SlotBg:
await Load(key, ImageUrls.Slots.Bg);
break;
case ImageKeys.SlotEmojis:
await Load(key, ImageUrls.Slots.Emojis);
break;
case ImageKeys.Currency:
await Load(key, ImageUrls.Currency);
break;
case ImageKeys.RategirlMatrix:
await Load(key, ImageUrls.Rategirl.Matrix);
break;
case ImageKeys.RategirlDot:
await Load(key, ImageUrls.Rategirl.Dot);
break;
case ImageKeys.RipOverlay:
await Load(key, ImageUrls.Rip.Overlay);
break;
case ImageKeys.RipBg:
await Load(key, ImageUrls.Rip.Bg);
break;
case ImageKeys.XpBg:
await Load(key, ImageUrls.Xp.Bg);
break;
default:
throw new ArgumentOutOfRangeException();
}
}
}
private async Task Load(ImageKeys key, Uri uri)
{
var data = await GetImageData(uri);
if (data is null)
return;
private async Task Load(ImageKeys key, Uri uri)
{
var data = await GetImageData(uri);
if (data is null)
return;
await _db.StringSetAsync(GetRedisKey(key), data);
}
await _db.StringSetAsync(GetRedisKey(key), data);
}
private async Task Load(ImageKeys key, Uri[] uris)
{
await _db.KeyDeleteAsync(GetRedisKey(key));
var imageData = await Task.WhenAll(uris.Select(GetImageData));
var vals = imageData
.Where(x => x is not null)
.Select(x => (RedisValue)x)
.ToArray();
private async Task Load(ImageKeys key, Uri[] uris)
{
await _db.KeyDeleteAsync(GetRedisKey(key));
var imageData = await Task.WhenAll(uris.Select(GetImageData));
var vals = imageData
.Where(x => x is not null)
.Select(x => (RedisValue)x)
.ToArray();
await _db.ListRightPushAsync(GetRedisKey(key), vals);
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.",
vals.Length, uris.Length, key);
}
}
private async Task<byte[]> GetImageData(Uri uri)
if (uris.Length != vals.Length)
{
if (uri.IsFile)
{
try
{
var bytes = await File.ReadAllBytesAsync(uri.LocalPath);
return bytes;
}
catch (Exception ex)
{
Log.Warning(ex, "Failed reading image bytes from uri: {Uri}", uri.ToString());
return null;
}
}
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);
}
}
private async Task<byte[]> GetImageData(Uri uri)
{
if (uri.IsFile)
{
try
{
return await _http.GetByteArrayAsync(uri);
var bytes = await File.ReadAllBytesAsync(uri.LocalPath);
return bytes;
}
catch (Exception ex)
{
Log.Warning(ex, "Image url you provided is not a valid image: {Uri}", uri.ToString());
Log.Warning(ex, "Failed reading image bytes from uri: {Uri}", uri.ToString());
return null;
}
}
private async Task<bool> AllKeysExist()
try
{
var tasks = await Task.WhenAll(GetAllKeys()
.Select(x => _db.KeyExistsAsync(GetRedisKey(x))));
return tasks.All(exist => exist);
return await _http.GetByteArrayAsync(uri);
}
catch (Exception ex)
{
Log.Warning(ex, "Image url you provided is not a valid image: {Uri}", uri.ToString());
return null;
}
private IEnumerable<ImageKeys> GetAllKeys() =>
Enum.GetValues<ImageKeys>();
private byte[][] GetByteArrayData(ImageKeys key)
=> _db.ListRange(GetRedisKey(key)).Map(x => (byte[])x);
private byte[] GetByteData(ImageKeys key)
=> _db.StringGet(GetRedisKey(key));
private RedisKey GetRedisKey(ImageKeys key)
=> _creds.RedisKey() + "_image_" + key;
}
private async Task<bool> AllKeysExist()
{
var tasks = await Task.WhenAll(GetAllKeys()
.Select(x => _db.KeyExistsAsync(GetRedisKey(x))));
return tasks.All(exist => exist);
}
private IEnumerable<ImageKeys> GetAllKeys() =>
Enum.GetValues<ImageKeys>();
private byte[][] GetByteArrayData(ImageKeys key)
=> _db.ListRange(GetRedisKey(key)).Map(x => (byte[])x);
private byte[] GetByteData(ImageKeys key)
=> _db.StringGet(GetRedisKey(key));
private RedisKey GetRedisKey(ImageKeys key)
=> _creds.RedisKey() + "_image_" + key;
}

View File

@@ -3,100 +3,94 @@ using NadekoBot.Extensions;
using NadekoBot.Modules.Games.Common.Trivia;
using Newtonsoft.Json;
using StackExchange.Redis;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using Discord;
using Discord.WebSocket;
using Serilog;
namespace NadekoBot.Services
namespace NadekoBot.Services;
public class RedisLocalDataCache : ILocalDataCache
{
public class RedisLocalDataCache : ILocalDataCache
private readonly ConnectionMultiplexer _con;
private readonly IBotCredentials _creds;
private IDatabase _db => _con.GetDatabase();
private const string pokemonAbilitiesFile = "data/pokemon/pokemon_abilities.json";
private const string pokemonListFile = "data/pokemon/pokemon_list.json";
private const string pokemonMapPath = "data/pokemon/name-id_map.json";
private const string questionsFile = "data/trivia_questions.json";
public IReadOnlyDictionary<string, SearchPokemon> Pokemons
{
private readonly ConnectionMultiplexer _con;
private readonly IBotCredentials _creds;
get => Get<Dictionary<string, SearchPokemon>>("pokemon_list");
private set => Set("pokemon_list", value);
}
private IDatabase _db => _con.GetDatabase();
public IReadOnlyDictionary<string, SearchPokemonAbility> PokemonAbilities
{
get => Get<Dictionary<string, SearchPokemonAbility>>("pokemon_abilities");
private set => Set("pokemon_abilities", value);
}
private const string pokemonAbilitiesFile = "data/pokemon/pokemon_abilities.json";
private const string pokemonListFile = "data/pokemon/pokemon_list.json";
private const string pokemonMapPath = "data/pokemon/name-id_map.json";
private const string questionsFile = "data/trivia_questions.json";
public TriviaQuestion[] TriviaQuestions
{
get => Get<TriviaQuestion[]>("trivia_questions");
private set => Set("trivia_questions", value);
}
public IReadOnlyDictionary<string, SearchPokemon> Pokemons
public IReadOnlyDictionary<int, string> PokemonMap
{
get => Get<Dictionary<int, string>>("pokemon_map");
private set => Set("pokemon_map", value);
}
public RedisLocalDataCache(ConnectionMultiplexer con, IBotCredentials creds, DiscordSocketClient client)
{
_con = con;
_creds = creds;
var shardId = client.ShardId;
if (shardId == 0)
{
get => Get<Dictionary<string, SearchPokemon>>("pokemon_list");
private set => Set("pokemon_list", value);
}
public IReadOnlyDictionary<string, SearchPokemonAbility> PokemonAbilities
{
get => Get<Dictionary<string, SearchPokemonAbility>>("pokemon_abilities");
private set => Set("pokemon_abilities", value);
}
public TriviaQuestion[] TriviaQuestions
{
get => Get<TriviaQuestion[]>("trivia_questions");
private set => Set("trivia_questions", value);
}
public IReadOnlyDictionary<int, string> PokemonMap
{
get => Get<Dictionary<int, string>>("pokemon_map");
private set => Set("pokemon_map", value);
}
public RedisLocalDataCache(ConnectionMultiplexer con, IBotCredentials creds, DiscordSocketClient client)
{
_con = con;
_creds = creds;
var shardId = client.ShardId;
if (shardId == 0)
if (!File.Exists(pokemonListFile))
{
if (!File.Exists(pokemonListFile))
{
Log.Warning($"{pokemonListFile} is missing. Pokemon abilities not loaded");
}
else
{
Pokemons = JsonConvert.DeserializeObject<Dictionary<string, SearchPokemon>>(File.ReadAllText(pokemonListFile));
}
Log.Warning($"{pokemonListFile} is missing. Pokemon abilities not loaded");
}
else
{
Pokemons = JsonConvert.DeserializeObject<Dictionary<string, SearchPokemon>>(File.ReadAllText(pokemonListFile));
}
if (!File.Exists(pokemonAbilitiesFile))
{
Log.Warning($"{pokemonAbilitiesFile} is missing. Pokemon abilities not loaded.");
}
else
{
PokemonAbilities = JsonConvert.DeserializeObject<Dictionary<string, SearchPokemonAbility>>(File.ReadAllText(pokemonAbilitiesFile));
}
if (!File.Exists(pokemonAbilitiesFile))
{
Log.Warning($"{pokemonAbilitiesFile} is missing. Pokemon abilities not loaded.");
}
else
{
PokemonAbilities = JsonConvert.DeserializeObject<Dictionary<string, SearchPokemonAbility>>(File.ReadAllText(pokemonAbilitiesFile));
}
try
{
TriviaQuestions = JsonConvert.DeserializeObject<TriviaQuestion[]>(File.ReadAllText(questionsFile));
PokemonMap = JsonConvert.DeserializeObject<PokemonNameId[]>(File.ReadAllText(pokemonMapPath))
.ToDictionary(x => x.Id, x => x.Name);
}
catch (Exception ex)
{
Log.Error(ex, "Error loading local data");
throw;
}
try
{
TriviaQuestions = JsonConvert.DeserializeObject<TriviaQuestion[]>(File.ReadAllText(questionsFile));
PokemonMap = JsonConvert.DeserializeObject<PokemonNameId[]>(File.ReadAllText(pokemonMapPath))
.ToDictionary(x => x.Id, x => x.Name);
}
catch (Exception ex)
{
Log.Error(ex, "Error loading local data");
throw;
}
}
private T Get<T>(string key) where T : class
{
return JsonConvert.DeserializeObject<T>(_db.StringGet($"{_creds.RedisKey()}_localdata_{key}"));
}
private void Set(string key, object obj)
{
_db.StringSet($"{_creds.RedisKey()}_localdata_{key}", JsonConvert.SerializeObject(obj));
}
}
}
private T Get<T>(string key) where T : class
{
return JsonConvert.DeserializeObject<T>(_db.StringGet($"{_creds.RedisKey()}_localdata_{key}"));
}
private void Set(string key, object obj)
{
_db.StringSet($"{_creds.RedisKey()}_localdata_{key}", JsonConvert.SerializeObject(obj));
}
}

View File

@@ -1,146 +1,140 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using System.Threading.Tasks;
using Discord;
using Discord.WebSocket;
using Grpc.Core;
using NadekoBot.Common.ModuleBehaviors;
using NadekoBot.Coordinator;
using NadekoBot.Services;
using NadekoBot.Extensions;
using Serilog;
namespace NadekoBot.Services
namespace NadekoBot.Services;
public class RemoteGrpcCoordinator : ICoordinator, IReadyExecutor
{
public class RemoteGrpcCoordinator : ICoordinator, IReadyExecutor
private readonly Coordinator.Coordinator.CoordinatorClient _coordClient;
private readonly DiscordSocketClient _client;
public RemoteGrpcCoordinator(IBotCredentials creds, DiscordSocketClient client)
{
private readonly Coordinator.Coordinator.CoordinatorClient _coordClient;
private readonly DiscordSocketClient _client;
public RemoteGrpcCoordinator(IBotCredentials creds, DiscordSocketClient client)
{
var coordUrl = string.IsNullOrWhiteSpace(creds.CoordinatorUrl)
? "http://localhost:3442"
: creds.CoordinatorUrl;
var coordUrl = string.IsNullOrWhiteSpace(creds.CoordinatorUrl)
? "http://localhost:3442"
: creds.CoordinatorUrl;
var channel = Grpc.Net.Client.GrpcChannel.ForAddress(coordUrl);
_coordClient = new(channel);
_client = client;
}
var channel = Grpc.Net.Client.GrpcChannel.ForAddress(coordUrl);
_coordClient = new(channel);
_client = client;
}
public bool RestartBot()
public bool RestartBot()
{
_coordClient.RestartAllShards(new RestartAllRequest
{
_coordClient.RestartAllShards(new RestartAllRequest
{
});
});
return true;
}
return true;
}
public void Die(bool graceful)
public void Die(bool graceful)
{
_coordClient.Die(new DieRequest()
{
_coordClient.Die(new DieRequest()
Graceful = graceful
});
}
public bool RestartShard(int shardId)
{
_coordClient.RestartShard(new RestartShardRequest
{
ShardId = shardId,
});
return true;
}
public IList<ShardStatus> GetAllShardStatuses()
{
var res = _coordClient.GetAllStatuses(new GetAllStatusesRequest());
return res.Statuses
.ToArray()
.Map(s => new ShardStatus()
{
Graceful = graceful
ConnectionState = FromCoordConnState(s.State),
GuildCount = s.GuildCount,
ShardId = s.ShardId,
LastUpdate = s.LastUpdate.ToDateTime(),
});
}
}
public bool RestartShard(int shardId)
public int GetGuildCount()
{
var res = _coordClient.GetAllStatuses(new GetAllStatusesRequest());
return res.Statuses.Sum(x => x.GuildCount);
}
public async Task Reload()
{
await _coordClient.ReloadAsync(new());
}
public Task OnReadyAsync()
{
Task.Run(async () =>
{
_coordClient.RestartShard(new RestartShardRequest
var gracefulImminent = false;
while (true)
{
ShardId = shardId,
});
return true;
}
public IList<ShardStatus> GetAllShardStatuses()
{
var res = _coordClient.GetAllStatuses(new GetAllStatusesRequest());
return res.Statuses
.ToArray()
.Map(s => new ShardStatus()
try
{
ConnectionState = FromCoordConnState(s.State),
GuildCount = s.GuildCount,
ShardId = s.ShardId,
LastUpdate = s.LastUpdate.ToDateTime(),
});
}
public int GetGuildCount()
{
var res = _coordClient.GetAllStatuses(new GetAllStatusesRequest());
return res.Statuses.Sum(x => x.GuildCount);
}
public async Task Reload()
{
await _coordClient.ReloadAsync(new());
}
public Task OnReadyAsync()
{
Task.Run(async () =>
{
var gracefulImminent = false;
while (true)
var reply = await _coordClient.HeartbeatAsync(new HeartbeatRequest
{
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)
{
try
if (!gracefulImminent)
{
var reply = await _coordClient.HeartbeatAsync(new HeartbeatRequest
{
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}",
ex.Message);
break;
}
await Task.Delay(22500).ConfigureAwait(false);
}
catch (Exception ex)
{
Log.Error(ex, "Unexpected heartbeat exception: {Message}", ex.Message);
Log.Warning(ex, "Hearbeat failed and graceful shutdown was not expected: {Message}",
ex.Message);
break;
}
await Task.Delay(7500).ConfigureAwait(false);
await Task.Delay(22500).ConfigureAwait(false);
}
catch (Exception ex)
{
Log.Error(ex, "Unexpected heartbeat exception: {Message}", ex.Message);
break;
}
Environment.Exit(5);
});
await Task.Delay(7500).ConfigureAwait(false);
}
return Task.CompletedTask;
}
Environment.Exit(5);
});
private ConnState ToCoordConnState(ConnectionState state)
=> state switch
{
ConnectionState.Connecting => ConnState.Connecting,
ConnectionState.Connected => ConnState.Connected,
_ => ConnState.Disconnected
};
private ConnectionState FromCoordConnState(ConnState state)
=> state switch
{
ConnState.Connecting => ConnectionState.Connecting,
ConnState.Connected => ConnectionState.Connected,
_ => ConnectionState.Disconnected
};
return Task.CompletedTask;
}
private ConnState ToCoordConnState(ConnectionState state)
=> state switch
{
ConnectionState.Connecting => ConnState.Connecting,
ConnectionState.Connected => ConnState.Connected,
_ => ConnState.Disconnected
};
private ConnectionState FromCoordConnState(ConnState state)
=> state switch
{
ConnState.Connecting => ConnectionState.Connecting,
ConnState.Connected => ConnectionState.Connected,
_ => ConnectionState.Disconnected
};
}

View File

@@ -1,73 +1,68 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Diagnostics;
using System.Threading.Tasks;
using Discord.WebSocket;
using NadekoBot.Services;
using Serilog;
namespace NadekoBot.Services
namespace NadekoBot.Services;
public class SingleProcessCoordinator : ICoordinator
{
public class SingleProcessCoordinator : ICoordinator
private readonly IBotCredentials _creds;
private readonly DiscordSocketClient _client;
public SingleProcessCoordinator(IBotCredentials creds, DiscordSocketClient client)
{
private readonly IBotCredentials _creds;
private readonly DiscordSocketClient _client;
public SingleProcessCoordinator(IBotCredentials creds, DiscordSocketClient client)
_creds = creds;
_client = client;
}
public bool RestartBot()
{
if (string.IsNullOrWhiteSpace(_creds.RestartCommand?.Cmd)
|| string.IsNullOrWhiteSpace(_creds.RestartCommand?.Args))
{
_creds = creds;
_client = client;
Log.Error("You must set RestartCommand.Cmd and RestartCommand.Args in creds.yml");
return false;
}
public bool RestartBot()
{
if (string.IsNullOrWhiteSpace(_creds.RestartCommand?.Cmd)
|| string.IsNullOrWhiteSpace(_creds.RestartCommand?.Args))
{
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 () =>
Process.Start(_creds.RestartCommand.Cmd, _creds.RestartCommand.Args);
_ = Task.Run(async () =>
{
await Task.Delay(2000);
Die();
});
return true;
}
public void Die(bool graceful = false)
{
Environment.Exit(5);
}
public bool RestartShard(int shardId)
{
return RestartBot();
}
public IList<ShardStatus> GetAllShardStatuses()
{
return new[]
{
new ShardStatus()
{
await Task.Delay(2000);
Die();
});
return true;
}
ConnectionState = _client.ConnectionState,
GuildCount = _client.Guilds.Count,
LastUpdate = DateTime.UtcNow,
ShardId = _client.ShardId
}
};
}
public void Die(bool graceful = false)
{
Environment.Exit(5);
}
public int GetGuildCount()
{
return _client.Guilds.Count;
}
public bool RestartShard(int shardId)
{
return RestartBot();
}
public IList<ShardStatus> GetAllShardStatuses()
{
return new[]
{
new ShardStatus()
{
ConnectionState = _client.ConnectionState,
GuildCount = _client.Guilds.Count,
LastUpdate = DateTime.UtcNow,
ShardId = _client.ShardId
}
};
}
public int GetGuildCount()
{
return _client.Guilds.Count;
}
public Task Reload()
{
return Task.CompletedTask;
}
public Task Reload()
{
return Task.CompletedTask;
}
}

View File

@@ -1,78 +1,75 @@
using Newtonsoft.Json;
using System;
using System.Linq;
using System.Net.Http;
using System.Threading.Tasks;
namespace NadekoBot.Services
namespace NadekoBot.Services;
public class SoundCloudApiService : INService
{
public class SoundCloudApiService : INService
private readonly IHttpClientFactory _httpFactory;
public SoundCloudApiService(IHttpClientFactory factory)
{
private readonly IHttpClientFactory _httpFactory;
public SoundCloudApiService(IHttpClientFactory factory)
{
_httpFactory = factory;
}
public async Task<SoundCloudVideo> ResolveVideoAsync(string url)
{
if (string.IsNullOrWhiteSpace(url))
throw new ArgumentNullException(nameof(url));
string response = "";
using (var http = _httpFactory.CreateClient())
{
response = await http.GetStringAsync($"https://scapi.nadeko.bot/resolve?url={url}").ConfigureAwait(false);
}
var responseObj = JsonConvert.DeserializeObject<SoundCloudVideo>(response);
if (responseObj?.Kind != "track")
throw new InvalidOperationException("Url is either not a track, or it doesn't exist.");
return responseObj;
}
public async Task<SoundCloudVideo> GetVideoByQueryAsync(string query)
{
if (string.IsNullOrWhiteSpace(query))
throw new ArgumentNullException(nameof(query));
var response = "";
using (var http = _httpFactory.CreateClient())
{
response = await http.GetStringAsync(new Uri($"https://scapi.nadeko.bot/tracks?q={Uri.EscapeDataString(query)}")).ConfigureAwait(false);
}
var responseObj = JsonConvert.DeserializeObject<SoundCloudVideo[]>(response)
.FirstOrDefault(s => s.Streamable is true);
if (responseObj?.Kind != "track")
throw new InvalidOperationException("Query yielded no results.");
return responseObj;
}
_httpFactory = factory;
}
public class SoundCloudVideo
public async Task<SoundCloudVideo> ResolveVideoAsync(string url)
{
public string Kind { get; set; } = "";
public long Id { get; set; } = 0;
public SoundCloudUser User { get; set; } = new SoundCloudUser();
public string Title { get; set; } = "";
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; } = "";
[JsonProperty("artwork_url")]
public string ArtworkUrl { get; set; } = "";
if (string.IsNullOrWhiteSpace(url))
throw new ArgumentNullException(nameof(url));
string response = "";
using (var http = _httpFactory.CreateClient())
{
response = await http.GetStringAsync($"https://scapi.nadeko.bot/resolve?url={url}").ConfigureAwait(false);
}
var responseObj = JsonConvert.DeserializeObject<SoundCloudVideo>(response);
if (responseObj?.Kind != "track")
throw new InvalidOperationException("Url is either not a track, or it doesn't exist.");
return responseObj;
}
public class SoundCloudUser
public async Task<SoundCloudVideo> GetVideoByQueryAsync(string query)
{
[JsonProperty("username")]
public string Name { get; set; }
if (string.IsNullOrWhiteSpace(query))
throw new ArgumentNullException(nameof(query));
var response = "";
using (var http = _httpFactory.CreateClient())
{
response = await http.GetStringAsync(new Uri($"https://scapi.nadeko.bot/tracks?q={Uri.EscapeDataString(query)}")).ConfigureAwait(false);
}
var responseObj = JsonConvert.DeserializeObject<SoundCloudVideo[]>(response)
.FirstOrDefault(s => s.Streamable is true);
if (responseObj?.Kind != "track")
throw new InvalidOperationException("Query yielded no results.");
return responseObj;
}
}
public class SoundCloudVideo
{
public string Kind { get; set; } = "";
public long Id { get; set; } = 0;
public SoundCloudUser User { get; set; } = new SoundCloudUser();
public string Title { get; set; } = "";
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; } = "";
[JsonProperty("artwork_url")]
public string ArtworkUrl { get; set; } = "";
}
public class SoundCloudUser
{
[JsonProperty("username")]
public string Name { get; set; }
}

View File

@@ -1,24 +1,21 @@
using Discord.WebSocket;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Collections;
namespace NadekoBot.Services
namespace NadekoBot.Services;
public class StartingGuildsService : IEnumerable<ulong>, INService
{
public class StartingGuildsService : IEnumerable<ulong>, INService
private readonly ImmutableList<ulong> _guilds;
public StartingGuildsService(DiscordSocketClient client)
{
private readonly ImmutableList<ulong> _guilds;
public StartingGuildsService(DiscordSocketClient client)
{
this._guilds = client.Guilds.Select(x => x.Id).ToImmutableList();
}
public IEnumerator<ulong> GetEnumerator() =>
_guilds.GetEnumerator();
IEnumerator IEnumerable.GetEnumerator() =>
_guilds.GetEnumerator();
this._guilds = client.Guilds.Select(x => x.Id).ToImmutableList();
}
}
public IEnumerator<ulong> GetEnumerator() =>
_guilds.GetEnumerator();
IEnumerator IEnumerable.GetEnumerator() =>
_guilds.GetEnumerator();
}

View File

@@ -2,186 +2,181 @@
using Discord.WebSocket;
using NadekoBot.Common.ModuleBehaviors;
using NadekoBot.Extensions;
using Serilog;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
namespace NadekoBot.Services
namespace NadekoBot.Services;
public class StatsService : IStatsService, IReadyExecutor, INService, IDisposable
{
public class StatsService : IStatsService, IReadyExecutor, INService, IDisposable
private readonly Process _currentProcess = Process.GetCurrentProcess();
private readonly DiscordSocketClient _client;
private readonly IBotCredentials _creds;
private readonly DateTime _started;
public const string BotVersion = "3.0.11";
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)
{
private readonly Process _currentProcess = Process.GetCurrentProcess();
private readonly DiscordSocketClient _client;
private readonly IBotCredentials _creds;
private readonly DateTime _started;
_client = client;
_creds = creds;
_httpFactory = factory;
public const string BotVersion = "3.0.11";
public string Author => "Kwoth#2452";
public string Library => "Discord.Net";
public double MessagesPerSecond => MessageCounter / GetUptime().TotalSeconds;
_started = DateTime.UtcNow;
_client.MessageReceived += _ => Task.FromResult(Interlocked.Increment(ref _messageCounter));
cmdHandler.CommandExecuted += (_, e) => Task.FromResult(Interlocked.Increment(ref _commandsRan));
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)
_client.ChannelCreated += (c) =>
{
_client = client;
_creds = creds;
_httpFactory = factory;
_started = DateTime.UtcNow;
_client.MessageReceived += _ => Task.FromResult(Interlocked.Increment(ref _messageCounter));
cmdHandler.CommandExecuted += (_, e) => Task.FromResult(Interlocked.Increment(ref _commandsRan));
_client.ChannelCreated += (c) =>
var _ = Task.Run(() =>
{
var _ = Task.Run(() =>
{
if (c is ITextChannel)
Interlocked.Increment(ref _textChannels);
else if (c is IVoiceChannel)
Interlocked.Increment(ref _voiceChannels);
});
if (c is ITextChannel)
Interlocked.Increment(ref _textChannels);
else if (c is IVoiceChannel)
Interlocked.Increment(ref _voiceChannels);
});
return Task.CompletedTask;
};
return Task.CompletedTask;
};
_client.ChannelDestroyed += (c) =>
_client.ChannelDestroyed += (c) =>
{
var _ = Task.Run(() =>
{
var _ = Task.Run(() =>
{
if (c is ITextChannel)
Interlocked.Decrement(ref _textChannels);
else if (c is IVoiceChannel)
Interlocked.Decrement(ref _voiceChannels);
});
if (c is ITextChannel)
Interlocked.Decrement(ref _textChannels);
else if (c is IVoiceChannel)
Interlocked.Decrement(ref _voiceChannels);
});
return Task.CompletedTask;
};
return Task.CompletedTask;
};
_client.GuildAvailable += (g) =>
_client.GuildAvailable += (g) =>
{
var _ = Task.Run(() =>
{
var _ = Task.Run(() =>
{
var tc = g.Channels.Count(cx => cx is ITextChannel);
var vc = g.Channels.Count - tc;
Interlocked.Add(ref _textChannels, tc);
Interlocked.Add(ref _voiceChannels, vc);
});
return Task.CompletedTask;
};
var tc = g.Channels.Count(cx => cx is ITextChannel);
var vc = g.Channels.Count - tc;
Interlocked.Add(ref _textChannels, tc);
Interlocked.Add(ref _voiceChannels, vc);
});
return Task.CompletedTask;
};
_client.JoinedGuild += (g) =>
_client.JoinedGuild += (g) =>
{
var _ = Task.Run(() =>
{
var _ = Task.Run(() =>
{
var tc = g.Channels.Count(cx => cx is ITextChannel);
var vc = g.Channels.Count - tc;
Interlocked.Add(ref _textChannels, tc);
Interlocked.Add(ref _voiceChannels, vc);
});
return Task.CompletedTask;
};
var tc = g.Channels.Count(cx => cx is ITextChannel);
var vc = g.Channels.Count - tc;
Interlocked.Add(ref _textChannels, tc);
Interlocked.Add(ref _voiceChannels, vc);
});
return Task.CompletedTask;
};
_client.GuildUnavailable += (g) =>
_client.GuildUnavailable += (g) =>
{
var _ = Task.Run(() =>
{
var _ = Task.Run(() =>
{
var tc = g.Channels.Count(cx => cx is ITextChannel);
var vc = g.Channels.Count - tc;
Interlocked.Add(ref _textChannels, -tc);
Interlocked.Add(ref _voiceChannels, -vc);
});
var tc = g.Channels.Count(cx => cx is ITextChannel);
var vc = g.Channels.Count - tc;
Interlocked.Add(ref _textChannels, -tc);
Interlocked.Add(ref _voiceChannels, -vc);
});
return Task.CompletedTask;
};
return Task.CompletedTask;
};
_client.LeftGuild += (g) =>
_client.LeftGuild += (g) =>
{
var _ = Task.Run(() =>
{
var _ = Task.Run(() =>
{
var tc = g.Channels.Count(cx => cx is ITextChannel);
var vc = g.Channels.Count - tc;
Interlocked.Add(ref _textChannels, -tc);
Interlocked.Add(ref _voiceChannels, -vc);
});
var tc = g.Channels.Count(cx => cx is ITextChannel);
var vc = g.Channels.Count - tc;
Interlocked.Add(ref _textChannels, -tc);
Interlocked.Add(ref _voiceChannels, -vc);
});
return Task.CompletedTask;
};
return Task.CompletedTask;
};
_botlistTimer = new Timer(async (state) =>
_botlistTimer = new Timer(async (state) =>
{
if (string.IsNullOrWhiteSpace(_creds.BotListToken))
return;
try
{
if (string.IsNullOrWhiteSpace(_creds.BotListToken))
return;
try
using (var http = _httpFactory.CreateClient())
{
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() }
}))
{
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).ConfigureAwait(false)) { }
}
using (await http.PostAsync(new Uri($"https://discordbots.org/api/bots/{client.CurrentUser.Id}/stats"), content).ConfigureAwait(false)) { }
}
}
catch (Exception ex)
{
Log.Error(ex, "Error ");
// ignored
}
}, null, TimeSpan.FromMinutes(5), TimeSpan.FromHours(1));
}
public TimeSpan GetUptime() =>
DateTime.UtcNow - _started;
public string GetUptimeString(string separator = ", ")
{
var time = GetUptime();
return $"{time.Days} days{separator}{time.Hours} hours{separator}{time.Minutes} minutes";
}
public Task OnReadyAsync()
{
var guilds = _client.Guilds;
_textChannels = guilds.Sum(g => g.Channels.Count(cx => cx is ITextChannel));
_voiceChannels = guilds.Sum(g => g.Channels.Count(cx => cx is IVoiceChannel));
return Task.CompletedTask;
}
public double GetPrivateMemory()
{
_currentProcess.Refresh();
return _currentProcess.PrivateMemorySize64 / (double)1.MiB();
}
public void Dispose()
{
_currentProcess.Dispose();
GC.SuppressFinalize(this);
}
}
catch (Exception ex)
{
Log.Error(ex, "Error ");
// ignored
}
}, null, TimeSpan.FromMinutes(5), TimeSpan.FromHours(1));
}
}
public TimeSpan GetUptime() =>
DateTime.UtcNow - _started;
public string GetUptimeString(string separator = ", ")
{
var time = GetUptime();
return $"{time.Days} days{separator}{time.Hours} hours{separator}{time.Minutes} minutes";
}
public Task OnReadyAsync()
{
var guilds = _client.Guilds;
_textChannels = guilds.Sum(g => g.Channels.Count(cx => cx is ITextChannel));
_voiceChannels = guilds.Sum(g => g.Channels.Count(cx => cx is IVoiceChannel));
return Task.CompletedTask;
}
public double GetPrivateMemory()
{
_currentProcess.Refresh();
return _currentProcess.PrivateMemorySize64 / (double)1.MiB();
}
public void Dispose()
{
_currentProcess.Dispose();
GC.SuppressFinalize(this);
}
}

View File

@@ -1,81 +1,77 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics;
using System.Text;
using System.Threading.Tasks;
using NadekoBot.Extensions;
using Serilog;
namespace NadekoBot.Services
namespace NadekoBot.Services;
public class YtdlOperation
{
public class YtdlOperation
private readonly string _baseArgString;
public YtdlOperation(string baseArgString)
{
private readonly string _baseArgString;
_baseArgString = baseArgString;
}
public YtdlOperation(string baseArgString)
private Process CreateProcess(string[] args)
{
args = args.Map(arg => arg.Replace("\"", ""));
return new Process()
{
_baseArgString = baseArgString;
}
private Process CreateProcess(string[] args)
{
args = args.Map(arg => arg.Replace("\"", ""));
return new Process()
StartInfo = new ProcessStartInfo()
{
StartInfo = new ProcessStartInfo()
{
FileName = "youtube-dl",
Arguments = string.Format(_baseArgString, args),
UseShellExecute = false,
RedirectStandardError = true,
RedirectStandardOutput = true,
StandardOutputEncoding = Encoding.UTF8,
StandardErrorEncoding = Encoding.UTF8,
CreateNoWindow = true,
},
};
}
FileName = "youtube-dl",
Arguments = string.Format(_baseArgString, args),
UseShellExecute = false,
RedirectStandardError = true,
RedirectStandardOutput = true,
StandardOutputEncoding = Encoding.UTF8,
StandardErrorEncoding = Encoding.UTF8,
CreateNoWindow = true,
},
};
}
public async Task<string> GetDataAsync(params string[] args)
{
try
{
using var process = CreateProcess(args);
Log.Debug($"Executing {process.StartInfo.FileName} {process.StartInfo.Arguments}");
process.Start();
var str = await process.StandardOutput.ReadToEndAsync().ConfigureAwait(false);
var err = await process.StandardError.ReadToEndAsync().ConfigureAwait(false);
if (!string.IsNullOrEmpty(err))
Log.Warning("YTDL warning: {YtdlWarning}", err);
return str;
}
catch (Win32Exception)
{
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);
return default;
}
}
public async IAsyncEnumerable<string> EnumerateDataAsync(params string[] args)
public async Task<string> GetDataAsync(params string[] args)
{
try
{
using var process = CreateProcess(args);
Log.Debug($"Executing {process.StartInfo.FileName} {process.StartInfo.Arguments}");
process.Start();
string line;
while((line = await process.StandardOutput.ReadLineAsync()) != null)
yield return line;
var str = await process.StandardOutput.ReadToEndAsync().ConfigureAwait(false);
var err = await process.StandardError.ReadToEndAsync().ConfigureAwait(false);
if (!string.IsNullOrEmpty(err))
Log.Warning("YTDL warning: {YtdlWarning}", err);
return str;
}
catch (Win32Exception)
{
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);
return default;
}
}
}
public async IAsyncEnumerable<string> EnumerateDataAsync(params string[] args)
{
using var process = CreateProcess(args);
Log.Debug($"Executing {process.StartInfo.FileName} {process.StartInfo.Arguments}");
process.Start();
string line;
while((line = await process.StandardOutput.ReadLineAsync()) != null)
yield return line;
}
}

View File

@@ -1,38 +1,35 @@
using System;
using System.Text;
using Serilog;
using System.Text;
using Serilog.Events;
using Serilog.Sinks.SystemConsole.Themes;
namespace NadekoBot.Services
{
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;
}
namespace NadekoBot.Services;
private static ConsoleTheme GetTheme()
{
if(Environment.OSVersion.Platform == PlatformID.Unix)
return AnsiConsoleTheme.Code;
#if DEBUG
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;
}
private static ConsoleTheme GetTheme()
{
if(Environment.OSVersion.Platform == PlatformID.Unix)
return AnsiConsoleTheme.Code;
#if DEBUG
return AnsiConsoleTheme.Code;
#else
return ConsoleTheme.None;
#endif
}
}
}
}

View File

@@ -1,42 +1,39 @@
using System;
using Discord;
using NadekoBot.Common;
using NadekoBot.Common;
using NadekoBot.Common.Configs;
using SixLabors.ImageSharp.PixelFormats;
namespace NadekoBot.Services
{
/// <summary>
/// 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 TypedKey<BotConfig> changeKey = new TypedKey<BotConfig>("config.bot.updated");
public BotConfigService(IConfigSeria serializer, IPubSub pubSub)
: base(FilePath, serializer, pubSub, changeKey)
{
AddParsedProp("color.ok", bs => bs.Color.Ok, Rgba32.TryParseHex, ConfigPrinters.Color);
AddParsedProp("color.error", bs => bs.Color.Error, Rgba32.TryParseHex, ConfigPrinters.Color);
AddParsedProp("color.pending", bs => bs.Color.Pending, Rgba32.TryParseHex, ConfigPrinters.Color);
AddParsedProp("help.text", bs => bs.HelpText, ConfigParsers.String, ConfigPrinters.ToString);
AddParsedProp("help.dmtext", bs => bs.DmHelpText, ConfigParsers.String, ConfigPrinters.ToString);
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();
}
namespace NadekoBot.Services;
private void Migrate()
/// <summary>
/// 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 TypedKey<BotConfig> changeKey = new TypedKey<BotConfig>("config.bot.updated");
public BotConfigService(IConfigSeria serializer, IPubSub pubSub)
: base(FilePath, serializer, pubSub, changeKey)
{
AddParsedProp("color.ok", bs => bs.Color.Ok, Rgba32.TryParseHex, ConfigPrinters.Color);
AddParsedProp("color.error", bs => bs.Color.Error, Rgba32.TryParseHex, ConfigPrinters.Color);
AddParsedProp("color.pending", bs => bs.Color.Pending, Rgba32.TryParseHex, ConfigPrinters.Color);
AddParsedProp("help.text", bs => bs.HelpText, ConfigParsers.String, ConfigPrinters.ToString);
AddParsedProp("help.dmtext", bs => bs.DmHelpText, ConfigParsers.String, ConfigPrinters.ToString);
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)
{
if (_data.Version < 2)
{
ModifyConfig(c => c.Version = 2);
}
ModifyConfig(c => c.Version = 2);
}
}
}

View File

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

View File

@@ -1,7 +1,4 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.IO;
using System.Linq.Expressions;
using System.Reflection;
using System.Threading.Tasks;
@@ -9,199 +6,198 @@ using NadekoBot.Common.Yml;
using NadekoBot.Common;
using NadekoBot.Common.Configs;
namespace NadekoBot.Services
namespace NadekoBot.Services;
/// <summary>
/// Base service for all settings services
/// </summary>
/// <typeparam name="TSettings">Type of the settings</typeparam>
public abstract class ConfigServiceBase<TSettings> : IConfigService
where TSettings : ICloneable<TSettings>, new()
{
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; }
/// <summary>
/// Base service for all settings services
/// Initialized an instance of <see cref="ConfigServiceBase{TSettings}"/>
/// </summary>
/// <typeparam name="TSettings">Type of the settings</typeparam>
public abstract class ConfigServiceBase<TSettings> : IConfigService
where TSettings : ICloneable<TSettings>, new()
/// <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,
TypedKey<TSettings> changeKey)
{
protected readonly string _filePath;
protected readonly IConfigSeria _serializer;
protected readonly IPubSub _pubSub;
private readonly TypedKey<TSettings> _changeKey;
_filePath = filePath;
_serializer = serializer;
_pubSub = pubSub;
_changeKey = changeKey;
protected TSettings _data;
Load();
_pubSub.Sub(_changeKey, OnChangePublished);
}
private void PublishChange()
{
_pubSub.Pub(_changeKey, _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 ValueTask OnChangePublished(TSettings newData)
{
_data = newData;
OnStateUpdate();
return default;
}
/// <summary>
/// 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,
TypedKey<TSettings> changeKey)
/// <summary>
/// Loads data from disk. If file doesn't exist, it will be created with default values
/// </summary>
protected void Load()
{
// if file is deleted, regenerate it with default values
if (!File.Exists(_filePath))
{
_filePath = filePath;
_serializer = serializer;
_pubSub = pubSub;
_changeKey = changeKey;
Load();
_pubSub.Sub(_changeKey, OnChangePublished);
}
private void PublishChange()
{
_pubSub.Pub(_changeKey, _data);
}
private ValueTask OnChangePublished(TSettings newData)
{
_data = newData;
OnStateUpdate();
return default;
}
/// <summary>
/// Loads data from disk. If file doesn't exist, it will be created with default values
/// </summary>
protected void Load()
{
// if file is deleted, regenerate it with default values
if (!File.Exists(_filePath))
{
_data = new TSettings();
Save();
}
_data = _serializer.Deserialize<TSettings>(File.ReadAllText(_filePath));
}
/// <summary>
/// Loads new data and publishes the new state
/// </summary>
public void Reload()
{
Load();
_pubSub.Pub(_changeKey, _data);
}
/// <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
/// </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 Dictionary<string, Func<TSettings, string, bool>>();
private readonly Dictionary<string, Func<object>> _propSelectors = new Dictionary<string, Func<object>>();
private readonly Dictionary<string, Func<object, string>> _propPrinters = new Dictionary<string, Func<object, string>>();
private readonly Dictionary<string, string> _propComments = new Dictionary<string, string>();
protected void AddParsedProp<TProp>(
string key,
Expression<Func<TSettings, TProp>> selector,
SettingParser<TProp> parser,
Func<TProp, string> printer,
Func<TProp, bool> checker = null)
{
checker ??= _ => true;
key = key.ToLowerInvariant();
_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)
=> (target, input) =>
{
if (!parser(input, out var value))
return false;
if (!checker(value))
return false;
object targetObject = target;
var expr = (MemberExpression)selector.Body;
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;
targetObject = localProp.GetValue(targetObject);
}
prop!.SetValue(targetObject, value, null);
return true;
};
public IReadOnlyList<string> GetSettableProps()
=> _propSetters.Keys.ToList();
public string GetSetting(string prop)
{
prop = prop.ToLowerInvariant();
if (!_propSelectors.TryGetValue(prop, out var selector) ||
!_propPrinters.TryGetValue(prop, out var printer))
return default;
return printer(selector());
}
public string GetComment(string prop)
{
if (_propComments.TryGetValue(prop, out var comment))
return comment;
return null;
}
private bool SetProperty(TSettings target, string key, string value)
=> _propSetters.TryGetValue(key.ToLowerInvariant(), out var magic)
&& magic(target, value);
public bool SetSetting(string prop, string newValue)
{
var success = true;
ModifyConfig(bs =>
{
success = SetProperty(bs, prop, newValue);
});
if(success)
PublishChange();
return success;
}
public void ModifyConfig(Action<TSettings> action)
{
var copy = Data;
action(copy);
_data = copy;
_data = new TSettings();
Save();
PublishChange();
}
_data = _serializer.Deserialize<TSettings>(File.ReadAllText(_filePath));
}
/// <summary>
/// Loads new data and publishes the new state
/// </summary>
public void Reload()
{
Load();
_pubSub.Pub(_changeKey, _data);
}
/// <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
/// </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 Dictionary<string, Func<TSettings, string, bool>>();
private readonly Dictionary<string, Func<object>> _propSelectors = new Dictionary<string, Func<object>>();
private readonly Dictionary<string, Func<object, string>> _propPrinters = new Dictionary<string, Func<object, string>>();
private readonly Dictionary<string, string> _propComments = new Dictionary<string, string>();
protected void AddParsedProp<TProp>(
string key,
Expression<Func<TSettings, TProp>> selector,
SettingParser<TProp> parser,
Func<TProp, string> printer,
Func<TProp, bool> checker = null)
{
checker ??= _ => true;
key = key.ToLowerInvariant();
_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)
=> (target, input) =>
{
if (!parser(input, out var value))
return false;
if (!checker(value))
return false;
object targetObject = target;
var expr = (MemberExpression)selector.Body;
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;
targetObject = localProp.GetValue(targetObject);
}
prop!.SetValue(targetObject, value, null);
return true;
};
public IReadOnlyList<string> GetSettableProps()
=> _propSetters.Keys.ToList();
public string GetSetting(string prop)
{
prop = prop.ToLowerInvariant();
if (!_propSelectors.TryGetValue(prop, out var selector) ||
!_propPrinters.TryGetValue(prop, out var printer))
return default;
return printer(selector());
}
public string GetComment(string prop)
{
if (_propComments.TryGetValue(prop, out var comment))
return comment;
return null;
}
private bool SetProperty(TSettings target, string key, string value)
=> _propSetters.TryGetValue(key.ToLowerInvariant(), out var magic)
&& magic(target, value);
public bool SetSetting(string prop, string newValue)
{
var success = true;
ModifyConfig(bs =>
{
success = SetProperty(bs, prop, newValue);
});
if(success)
PublishChange();
return success;
}
public void ModifyConfig(Action<TSettings> action)
{
var copy = Data;
action(copy);
_data = copy;
Save();
PublishChange();
}
}

View File

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

View File

@@ -1,45 +1,42 @@
using System.Collections.Generic;
namespace NadekoBot.Services;
namespace NadekoBot.Services
/// <summary>
/// Interface that all services which deal with configs should implement
/// </summary>
public interface IConfigService
{
/// <summary>
/// Interface that all services which deal with configs should implement
/// Name of the config
/// </summary>
public interface IConfigService
{
/// <summary>
/// Name of the config
/// </summary>
public string Name { get; }
/// <summary>
/// Loads new data and publishes the new state
/// </summary>
void Reload();
/// <summary>
/// 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
/// </summary>
/// <param name="prop">Prop name</param>
/// <returns>Value of the prop</returns>
string GetSetting(string prop);
public string Name { get; }
/// <summary>
/// Loads new data and publishes the new state
/// </summary>
void Reload();
/// <summary>
/// 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
/// </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
/// </summary>
/// <param name="prop">Prop name</param>
/// <returns>Value of the prop</returns>
string GetComment(string prop);
/// <summary>
/// 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
/// </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);
}
/// <summary>
/// Sets the value of the specified property
/// </summary>
/// <param name="prop">Property to set</param>
/// <param name="newValue">Value to set the property to</param>
/// <returns>Success</returns>
bool SetSetting(string prop, string newValue);
}

View File

@@ -1,8 +1,7 @@
namespace NadekoBot.Services
{
/// <summary>
/// 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);
}
namespace NadekoBot.Services;
/// <summary>
/// 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);

View File

@@ -1,10 +1,9 @@
namespace NadekoBot.Services
namespace NadekoBot.Services;
public static class StandardConversions
{
public static class StandardConversions
public static double CelsiusToFahrenheit(double cel)
{
public static double CelsiusToFahrenheit(double cel)
{
return cel * 1.8f + 32;
}
return cel * 1.8f + 32;
}
}
}

View File

@@ -1,16 +1,15 @@
using System.Globalization;
namespace NadekoBot.Services
namespace NadekoBot.Services;
/// <summary>
/// Defines methods to retrieve and reload bot strings
/// </summary>
public interface IBotStrings
{
/// <summary>
/// Defines methods to retrieve and reload bot strings
/// </summary>
public interface IBotStrings
{
string GetText(string key, ulong? guildId = null, params object[] data);
string GetText(string key, CultureInfo locale, params object[] data);
void Reload();
CommandStrings GetCommandStrings(string commandName, ulong? guildId = null);
CommandStrings GetCommandStrings(string commandName, CultureInfo cultureInfo);
}
}
string GetText(string key, ulong? guildId = null, params object[] data);
string GetText(string key, CultureInfo locale, params object[] data);
void Reload();
CommandStrings GetCommandStrings(string commandName, ulong? guildId = null);
CommandStrings GetCommandStrings(string commandName, CultureInfo cultureInfo);
}

View File

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

View File

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

View File

@@ -1,107 +1,104 @@
using System;
using System.Globalization;
using Serilog;
using System.Globalization;
using YamlDotNet.Serialization;
namespace NadekoBot.Services
namespace NadekoBot.Services;
public class BotStrings : IBotStrings
{
public class BotStrings : IBotStrings
/// <summary>
/// Used as failsafe in case response key doesn't exist in the selected or default language.
/// </summary>
private readonly CultureInfo _usCultureInfo = new CultureInfo("en-US");
private readonly ILocalization _localization;
private readonly IBotStringsProvider _stringsProvider;
public BotStrings(ILocalization loc, IBotStringsProvider stringsProvider)
{
/// <summary>
/// Used as failsafe in case response key doesn't exist in the selected or default language.
/// </summary>
private readonly CultureInfo _usCultureInfo = new CultureInfo("en-US");
private readonly ILocalization _localization;
private readonly IBotStringsProvider _stringsProvider;
_localization = loc;
_stringsProvider = stringsProvider;
}
public BotStrings(ILocalization loc, IBotStringsProvider stringsProvider)
private string GetString(string key, CultureInfo cultureInfo)
=> _stringsProvider.GetText(cultureInfo.Name, key);
public string GetText(string key, ulong? guildId = null, params object[] data)
=> GetText(key, _localization.GetCultureInfo(guildId), data);
public string GetText(string key, CultureInfo cultureInfo)
{
var text = GetString(key, cultureInfo);
if (string.IsNullOrWhiteSpace(text))
{
_localization = loc;
_stringsProvider = stringsProvider;
}
private string GetString(string key, CultureInfo cultureInfo)
=> _stringsProvider.GetText(cultureInfo.Name, key);
public string GetText(string key, ulong? guildId = null, params object[] data)
=> GetText(key, _localization.GetCultureInfo(guildId), data);
public string GetText(string key, CultureInfo cultureInfo)
{
var text = GetString(key, cultureInfo);
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))
{
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.";
}
}
return text;
}
public string GetText(string key, CultureInfo cultureInfo, params object[] data)
{
try
{
return string.Format(GetText(key, cultureInfo), data);
}
catch (FormatException)
{
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." +
$" Key '{key}' is missing from resources. You may ignore this message.";
}
}
return text;
}
public CommandStrings GetCommandStrings(string commandName, ulong? guildId = null)
=> GetCommandStrings(commandName, _localization.GetCultureInfo(guildId));
public CommandStrings GetCommandStrings(string commandName, CultureInfo cultureInfo)
public string GetText(string key, CultureInfo cultureInfo, params object[] data)
{
try
{
var cmdStrings = _stringsProvider.GetCommandStrings(cultureInfo.Name, commandName);
if (cmdStrings is null)
return string.Format(GetText(key, cultureInfo), data);
}
catch (FormatException)
{
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.";
}
}
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);
if (cmdStrings is null)
{
if (cultureInfo.Name == _usCultureInfo.Name)
{
if (cultureInfo.Name == _usCultureInfo.Name)
{
Log.Warning("'{CommandName}' doesn't exist in 'en-US' command strings. Please report this",
commandName);
Log.Warning("'{CommandName}' doesn't exist in 'en-US' command strings. Please report this",
commandName);
return new CommandStrings()
{
Args = new[] {""},
Desc = "?"
};
}
return new CommandStrings()
{
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);
}
return cmdStrings;
return GetCommandStrings(commandName, _usCultureInfo);
}
public void Reload()
{
_stringsProvider.Reload();
}
return cmdStrings;
}
public class CommandStrings
public void Reload()
{
[YamlMember(Alias = "desc")]
public string Desc { get; set; }
[YamlMember(Alias = "args")]
public string[] Args { get; set; }
_stringsProvider.Reload();
}
}
public class CommandStrings
{
[YamlMember(Alias = "desc")]
public string Desc { get; set; }
[YamlMember(Alias = "args")]
public string[] Args { get; set; }
}

View File

@@ -1,45 +1,42 @@
using System.Collections.Generic;
namespace NadekoBot.Services;
namespace NadekoBot.Services
public class LocalBotStringsProvider : IBotStringsProvider
{
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)
{
private readonly IStringsSource _source;
private IReadOnlyDictionary<string, Dictionary<string, string>> responseStrings;
private IReadOnlyDictionary<string, Dictionary<string, CommandStrings>> commandStrings;
_source = source;
Reload();
}
public LocalBotStringsProvider(IStringsSource source)
public string GetText(string localeName, string key)
{
if (responseStrings.TryGetValue(localeName, out var langStrings)
&& langStrings.TryGetValue(key, out var text))
{
_source = source;
Reload();
}
public string GetText(string localeName, string key)
{
if (responseStrings.TryGetValue(localeName, out var langStrings)
&& langStrings.TryGetValue(key, out var text))
{
return text;
}
return null;
return text;
}
public void Reload()
return null;
}
public void Reload()
{
responseStrings = _source.GetResponseStrings();
commandStrings = _source.GetCommandStrings();
}
public CommandStrings GetCommandStrings(string localeName, string commandName)
{
if (commandStrings.TryGetValue(localeName, out var langStrings)
&& langStrings.TryGetValue(commandName, out var strings))
{
responseStrings = _source.GetResponseStrings();
commandStrings = _source.GetCommandStrings();
return strings;
}
public CommandStrings GetCommandStrings(string localeName, string commandName)
{
if (commandStrings.TryGetValue(localeName, out var langStrings)
&& langStrings.TryGetValue(commandName, out var strings))
{
return strings;
}
return null;
}
return null;
}
}

View File

@@ -1,77 +1,73 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.IO;
using Newtonsoft.Json;
using Serilog;
using YamlDotNet.Serialization;
namespace NadekoBot.Services
namespace NadekoBot.Services;
/// <summary>
/// Loads strings from the local default filepath <see cref="_responsesPath"/>
/// </summary>
public class LocalFileStringsSource : IStringsSource
{
/// <summary>
/// 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",
string commandsPath = "data/strings/commands")
{
private readonly string _responsesPath = "data/strings/responses";
private readonly string _commandsPath = "data/strings/commands";
public LocalFileStringsSource(string responsesPath = "data/strings/responses",
string commandsPath = "data/strings/commands")
{
_responsesPath = responsesPath;
_commandsPath = commandsPath;
}
_responsesPath = responsesPath;
_commandsPath = commandsPath;
}
public Dictionary<string, Dictionary<string, string>> GetResponseStrings()
public Dictionary<string, Dictionary<string, string>> GetResponseStrings()
{
var outputDict = new Dictionary<string, Dictionary<string, string>>();
foreach (var file in Directory.GetFiles(_responsesPath))
{
var outputDict = new Dictionary<string, Dictionary<string, string>>();
foreach (var file in Directory.GetFiles(_responsesPath))
try
{
try
{
var langDict = JsonConvert.DeserializeObject<Dictionary<string, string>>(File.ReadAllText(file));
var localeName = GetLocaleName(file);
outputDict[localeName] = langDict;
}
catch (Exception ex)
{
Log.Error(ex, "Error loading {FileName} response strings: {ErrorMessage}", file, ex.Message);
}
var langDict = JsonConvert.DeserializeObject<Dictionary<string, string>>(File.ReadAllText(file));
var localeName = GetLocaleName(file);
outputDict[localeName] = langDict;
}
catch (Exception ex)
{
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();
return outputDict;
}
public Dictionary<string, Dictionary<string, CommandStrings>> GetCommandStrings()
{
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);
var langDict = deserializer.Deserialize<Dictionary<string, CommandStrings>>(text);
var localeName = GetLocaleName(file);
outputDict[localeName] = langDict;
}
catch (Exception ex)
{
Log.Error(ex, "Error loading {FileName} command strings: {ErrorMessage}", file, ex.Message);
}
}
return outputDict;
}
private static string GetLocaleName(string fileName)
var outputDict = new Dictionary<string, Dictionary<string, CommandStrings>>();
foreach (var file in Directory.GetFiles(_commandsPath))
{
fileName = Path.GetFileName(fileName);
var dotIndex = fileName.IndexOf('.') + 1;
var secondDotIndex = fileName.LastIndexOf('.');
return fileName.Substring(dotIndex, secondDotIndex - dotIndex);
try
{
var text = File.ReadAllText(file);
var langDict = deserializer.Deserialize<Dictionary<string, CommandStrings>>(text);
var localeName = GetLocaleName(file);
outputDict[localeName] = langDict;
}
catch (Exception ex)
{
Log.Error(ex, "Error loading {FileName} command strings: {ErrorMessage}", file, ex.Message);
}
}
return outputDict;
}
private static string GetLocaleName(string fileName)
{
fileName = Path.GetFileName(fileName);
var dotIndex = fileName.IndexOf('.') + 1;
var secondDotIndex = fileName.LastIndexOf('.');
return fileName.Substring(dotIndex, secondDotIndex - dotIndex);
}
}

View File

@@ -1,80 +1,77 @@
using System;
using System.Linq;
using System.Web;
using System.Web;
using Discord.WebSocket;
using NadekoBot.Extensions;
using StackExchange.Redis;
namespace NadekoBot.Services
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"/>
/// </summary>
public class RedisBotStringsProvider : IBotStringsProvider
{
/// <summary>
/// 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
private readonly ConnectionMultiplexer _redis;
private readonly IStringsSource _source;
private readonly IBotCredentials _creds;
public RedisBotStringsProvider(ConnectionMultiplexer redis, DiscordSocketClient discordClient,
IStringsSource source, IBotCredentials creds)
{
private readonly ConnectionMultiplexer _redis;
private readonly IStringsSource _source;
private readonly IBotCredentials _creds;
_redis = redis;
_source = source;
_creds = creds;
public RedisBotStringsProvider(ConnectionMultiplexer redis, DiscordSocketClient discordClient,
IStringsSource source, IBotCredentials creds)
{
_redis = redis;
_source = source;
_creds = creds;
if(discordClient.ShardId == 0)
Reload();
}
if(discordClient.ShardId == 0)
Reload();
}
public string GetText(string localeName, string key)
{
var value = _redis.GetDatabase().HashGet($"{_creds.RedisKey()}:responses:{localeName}", key);
return value;
}
public string GetText(string localeName, string key)
{
var value = _redis.GetDatabase().HashGet($"{_creds.RedisKey()}:responses:{localeName}", key);
return value;
}
public CommandStrings GetCommandStrings(string localeName, string commandName)
{
string argsStr = _redis.GetDatabase().HashGet($"{_creds.RedisKey()}:commands:{localeName}", $"{commandName}::args");
if (argsStr == default)
return null;
public CommandStrings GetCommandStrings(string localeName, string commandName)
{
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");
if (descStr == default)
return null;
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 CommandStrings()
{
Args = args,
Desc = descStr
};
}
public void Reload()
var args = Array.ConvertAll(argsStr.Split('&'), HttpUtility.UrlDecode);
return new CommandStrings()
{
var redisDb = _redis.GetDatabase();
foreach (var (localeName, localeStrings) in _source.GetResponseStrings())
{
var hashFields = localeStrings
.Select(x => new HashEntry(x.Key, x.Value))
.ToArray();
Args = args,
Desc = descStr
};
}
redisDb.HashSet($"{_creds.RedisKey()}:responses:{localeName}", hashFields);
}
public void Reload()
{
var redisDb = _redis.GetDatabase();
foreach (var (localeName, localeStrings) in _source.GetResponseStrings())
{
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();
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();
redisDb.HashSet($"{_creds.RedisKey()}:commands:{localeName}", hashFields);
}
redisDb.HashSet($"{_creds.RedisKey()}:commands:{localeName}", hashFields);
}
}
}