mirror of
https://gitlab.com/Kwoth/nadekobot.git
synced 2025-09-11 09:48:26 -04:00
Gambling moved to a separate project. Project builds
This commit is contained in:
@@ -1,427 +0,0 @@
|
||||
#nullable disable
|
||||
using NadekoBot.Common.Configs;
|
||||
using NadekoBot.Common.ModuleBehaviors;
|
||||
using NadekoBot.Db;
|
||||
using ExecuteResult = Discord.Commands.ExecuteResult;
|
||||
using PreconditionResult = Discord.Commands.PreconditionResult;
|
||||
|
||||
namespace NadekoBot.Services;
|
||||
|
||||
public class CommandHandler : INService, IReadyExecutor, ICommandHandler
|
||||
{
|
||||
private const int GLOBAL_COMMANDS_COOLDOWN = 750;
|
||||
|
||||
private const float ONE_THOUSANDTH = 1.0f / 1000;
|
||||
|
||||
public event Func<IUserMessage, CommandInfo, Task> CommandExecuted = delegate { return Task.CompletedTask; };
|
||||
public event Func<CommandInfo, ITextChannel, string, Task> CommandErrored = delegate { return Task.CompletedTask; };
|
||||
|
||||
//userid/msg count
|
||||
public ConcurrentDictionary<ulong, uint> UserMessagesSent { get; } = new();
|
||||
|
||||
public ConcurrentHashSet<ulong> UsersOnShortCooldown { get; } = new();
|
||||
|
||||
private readonly DiscordSocketClient _client;
|
||||
private readonly CommandService _commandService;
|
||||
private readonly BotConfigService _bss;
|
||||
private readonly Bot _bot;
|
||||
private readonly IBehaviorHandler _behaviorHandler;
|
||||
private readonly IServiceProvider _services;
|
||||
|
||||
private readonly ConcurrentDictionary<ulong, string> _prefixes;
|
||||
|
||||
private readonly DbService _db;
|
||||
// private readonly InteractionService _interactions;
|
||||
|
||||
public CommandHandler(
|
||||
DiscordSocketClient client,
|
||||
DbService db,
|
||||
CommandService commandService,
|
||||
BotConfigService bss,
|
||||
Bot bot,
|
||||
IBehaviorHandler behaviorHandler,
|
||||
// InteractionService interactions,
|
||||
IServiceProvider services)
|
||||
{
|
||||
_client = client;
|
||||
_commandService = commandService;
|
||||
_bss = bss;
|
||||
_bot = bot;
|
||||
_behaviorHandler = behaviorHandler;
|
||||
_db = db;
|
||||
_services = services;
|
||||
// _interactions = interactions;
|
||||
|
||||
_prefixes = bot.AllGuildConfigs.Where(x => x.Prefix is not null)
|
||||
.ToDictionary(x => x.GuildId, x => x.Prefix)
|
||||
.ToConcurrent();
|
||||
|
||||
}
|
||||
|
||||
public async Task OnReadyAsync()
|
||||
{
|
||||
Log.Information("Command handler runnning on ready");
|
||||
// clear users on short cooldown every GLOBAL_COMMANDS_COOLDOWN miliseconds
|
||||
using var timer = new PeriodicTimer(TimeSpan.FromMilliseconds(GLOBAL_COMMANDS_COOLDOWN));
|
||||
while (await timer.WaitForNextTickAsync())
|
||||
UsersOnShortCooldown.Clear();
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
_prefixes[guild.Id] = prefix;
|
||||
|
||||
return prefix;
|
||||
}
|
||||
|
||||
public async Task ExecuteExternal(ulong? guildId, ulong channelId, string commandText)
|
||||
{
|
||||
if (guildId is not null)
|
||||
{
|
||||
var guild = _client.GetGuild(guildId.Value);
|
||||
if (guild?.GetChannel(channelId) is not SocketTextChannel channel)
|
||||
{
|
||||
Log.Warning("Channel for external execution not found");
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
IUserMessage msg = await channel.SendMessageAsync(commandText);
|
||||
msg = (IUserMessage)await channel.GetMessageAsync(msg.Id);
|
||||
await TryRunCommand(guild, channel, msg);
|
||||
//msg.DeleteAfter(5);
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
}
|
||||
|
||||
public Task StartHandling()
|
||||
{
|
||||
_client.MessageReceived += MessageReceivedHandler;
|
||||
// _client.SlashCommandExecuted += SlashCommandExecuted;
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
// private async Task SlashCommandExecuted(SocketSlashCommand arg)
|
||||
// {
|
||||
// var ctx = new SocketInteractionContext<SocketSlashCommand>(_client, arg);
|
||||
// await _interactions.ExecuteCommandAsync(ctx, _services);
|
||||
// }
|
||||
|
||||
private Task LogSuccessfulExecution(IUserMessage usrMsg, ITextChannel channel, params int[] execPoints)
|
||||
{
|
||||
if (_bss.Data.ConsoleOutputType == ConsoleOutputType.Normal)
|
||||
{
|
||||
Log.Information("""
|
||||
Command Executed after {ExecTime}s
|
||||
User: {User}
|
||||
Server: {Server}
|
||||
Channel: {Channel}
|
||||
Message: {Message}
|
||||
""",
|
||||
string.Join("/", execPoints.Select(x => (x * ONE_THOUSANDTH).ToString("F3"))),
|
||||
usrMsg.Author + " [" + usrMsg.Author.Id + "]",
|
||||
channel is null ? "PRIVATE" : channel.Guild.Name + " [" + channel.Guild.Id + "]",
|
||||
channel is null ? "PRIVATE" : channel.Name + " [" + channel.Id + "]",
|
||||
usrMsg.Content);
|
||||
}
|
||||
else
|
||||
{
|
||||
Log.Information("Succ | g:{GuildId} | c: {ChannelId} | u: {UserId} | msg: {Message}",
|
||||
channel?.Guild.Id.ToString() ?? "-",
|
||||
channel?.Id.ToString() ?? "-",
|
||||
usrMsg.Author.Id,
|
||||
usrMsg.Content.TrimTo(10));
|
||||
}
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private void LogErroredExecution(
|
||||
string errorMessage,
|
||||
IUserMessage usrMsg,
|
||||
ITextChannel channel,
|
||||
params int[] execPoints)
|
||||
{
|
||||
if (_bss.Data.ConsoleOutputType == ConsoleOutputType.Normal)
|
||||
{
|
||||
Log.Warning("""
|
||||
Command Errored after {ExecTime}s
|
||||
User: {User}
|
||||
Server: {Guild}
|
||||
Channel: {Channel}
|
||||
Message: {Message}
|
||||
Error: {ErrorMessage}
|
||||
""",
|
||||
string.Join("/", execPoints.Select(x => (x * ONE_THOUSANDTH).ToString("F3"))),
|
||||
usrMsg.Author + " [" + usrMsg.Author.Id + "]",
|
||||
channel is null ? "DM" : channel.Guild.Name + " [" + channel.Guild.Id + "]",
|
||||
channel is null ? "DM" : channel.Name + " [" + channel.Id + "]",
|
||||
usrMsg.Content,
|
||||
errorMessage);
|
||||
}
|
||||
else
|
||||
{
|
||||
Log.Warning("""
|
||||
Err | g:{GuildId} | c: {ChannelId} | u: {UserId} | msg: {Message}
|
||||
Err: {ErrorMessage}
|
||||
""",
|
||||
channel?.Guild.Id.ToString() ?? "-",
|
||||
channel?.Id.ToString() ?? "-",
|
||||
usrMsg.Author.Id,
|
||||
usrMsg.Content.TrimTo(10),
|
||||
errorMessage);
|
||||
}
|
||||
}
|
||||
|
||||
private Task MessageReceivedHandler(SocketMessage msg)
|
||||
{
|
||||
//no bots, wait until bot connected and initialized
|
||||
if (msg.Author.IsBot || !_bot.IsReady)
|
||||
return Task.CompletedTask;
|
||||
|
||||
if (msg is not SocketUserMessage usrMsg)
|
||||
return Task.CompletedTask;
|
||||
|
||||
Task.Run(async () =>
|
||||
{
|
||||
try
|
||||
{
|
||||
#if !GLOBAL_NADEKO
|
||||
// track how many messages each user is sending
|
||||
UserMessagesSent.AddOrUpdate(usrMsg.Author.Id, 1, (_, old) => ++old);
|
||||
#endif
|
||||
|
||||
var channel = msg.Channel;
|
||||
var guild = (msg.Channel as SocketTextChannel)?.Guild;
|
||||
|
||||
await TryRunCommand(guild, channel, usrMsg);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Warning(ex, "Error in CommandHandler");
|
||||
if (ex.InnerException is not null)
|
||||
Log.Warning(ex.InnerException, "Inner Exception of the error in CommandHandler");
|
||||
}
|
||||
});
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public async Task TryRunCommand(SocketGuild guild, ISocketMessageChannel channel, IUserMessage usrMsg)
|
||||
{
|
||||
var startTime = Environment.TickCount;
|
||||
|
||||
var blocked = await _behaviorHandler.RunExecOnMessageAsync(guild, usrMsg);
|
||||
if (blocked)
|
||||
return;
|
||||
|
||||
var blockTime = Environment.TickCount - startTime;
|
||||
|
||||
var messageContent = await _behaviorHandler.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 (isPrefixCommand || messageContent.StartsWith(prefix, StringComparison.InvariantCulture))
|
||||
{
|
||||
var context = new CommandContext(_client, usrMsg);
|
||||
var (success, error, info) = await ExecuteCommandAsync(context,
|
||||
messageContent,
|
||||
isPrefixCommand ? 1 : prefix.Length,
|
||||
_services,
|
||||
MultiMatchHandling.Best);
|
||||
|
||||
startTime = Environment.TickCount - startTime;
|
||||
|
||||
// if a command is found
|
||||
if (info is not null)
|
||||
{
|
||||
// if it successfully executed
|
||||
if (success)
|
||||
{
|
||||
await LogSuccessfulExecution(usrMsg, channel as ITextChannel, blockTime, startTime);
|
||||
await CommandExecuted(usrMsg, info);
|
||||
await _behaviorHandler.RunPostCommandAsync(context, info.Module.GetTopLevelModule().Name, info);
|
||||
return;
|
||||
}
|
||||
|
||||
// if it errored
|
||||
if (error is not null)
|
||||
{
|
||||
error = HumanizeError(error);
|
||||
LogErroredExecution(error, usrMsg, channel as ITextChannel, blockTime, startTime);
|
||||
|
||||
if (guild is not null)
|
||||
await CommandErrored(info, channel as ITextChannel, error);
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await _behaviorHandler.RunOnNoCommandAsync(guild, usrMsg);
|
||||
}
|
||||
|
||||
private string HumanizeError(string error)
|
||||
{
|
||||
if (error.Contains("parse int", StringComparison.OrdinalIgnoreCase)
|
||||
|| error.Contains("parse float"))
|
||||
return "Invalid number specified. Make sure you're specifying parameters in the correct order.";
|
||||
|
||||
return error;
|
||||
}
|
||||
|
||||
public Task<(bool Success, string Error, CommandInfo Info)> ExecuteCommandAsync(
|
||||
ICommandContext context,
|
||||
string input,
|
||||
int argPos,
|
||||
IServiceProvider serviceProvider,
|
||||
MultiMatchHandling multiMatchHandling = MultiMatchHandling.Exception)
|
||||
=> ExecuteCommand(context, input[argPos..], serviceProvider, multiMatchHandling);
|
||||
|
||||
|
||||
public async Task<(bool Success, string Error, CommandInfo Info)> ExecuteCommand(
|
||||
ICommandContext 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);
|
||||
|
||||
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);
|
||||
|
||||
if (parseResult.Error == CommandError.MultipleMatches)
|
||||
{
|
||||
IReadOnlyList<TypeReaderValue> argList, paramList;
|
||||
switch (multiMatchHandling)
|
||||
{
|
||||
case MultiMatchHandling.Best:
|
||||
argList = parseResult.ArgValues
|
||||
.Map(x => x.Values.MaxBy(y => y.Score));
|
||||
paramList = parseResult.ParamValues
|
||||
.Map(x => x.Values.MaxBy(y => y.Score));
|
||||
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)).ToList();
|
||||
|
||||
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 _behaviorHandler.RunPreCommandAsync(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);
|
||||
|
||||
if (execResult.Exception is not null
|
||||
&& (execResult.Exception is not HttpException he
|
||||
|| he.DiscordCode != DiscordErrorCode.InsufficientPermissions))
|
||||
Log.Warning(execResult.Exception, "Command Error");
|
||||
|
||||
return (true, null, cmd);
|
||||
}
|
||||
}
|
@@ -1,106 +0,0 @@
|
||||
#nullable disable
|
||||
using LinqToDB;
|
||||
using NadekoBot.Services.Currency;
|
||||
|
||||
namespace NadekoBot.Services;
|
||||
|
||||
public sealed class CurrencyService : ICurrencyService, INService
|
||||
{
|
||||
private readonly DbService _db;
|
||||
private readonly ITxTracker _txTracker;
|
||||
|
||||
public CurrencyService(DbService db, ITxTracker txTracker)
|
||||
{
|
||||
_db = db;
|
||||
_txTracker = txTracker;
|
||||
}
|
||||
|
||||
public Task<IWallet> GetWalletAsync(ulong userId, CurrencyType type = CurrencyType.Default)
|
||||
{
|
||||
if (type == CurrencyType.Default)
|
||||
return Task.FromResult<IWallet>(new DefaultWallet(userId, _db));
|
||||
|
||||
throw new ArgumentOutOfRangeException(nameof(type));
|
||||
}
|
||||
|
||||
public async Task AddBulkAsync(
|
||||
IReadOnlyCollection<ulong> userIds,
|
||||
long amount,
|
||||
TxData txData,
|
||||
CurrencyType type = CurrencyType.Default)
|
||||
{
|
||||
if (type == CurrencyType.Default)
|
||||
{
|
||||
foreach (var userId in userIds)
|
||||
{
|
||||
var wallet = await GetWalletAsync(userId);
|
||||
await wallet.Add(amount, txData);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
throw new ArgumentOutOfRangeException(nameof(type));
|
||||
}
|
||||
|
||||
public async Task RemoveBulkAsync(
|
||||
IReadOnlyCollection<ulong> userIds,
|
||||
long amount,
|
||||
TxData txData,
|
||||
CurrencyType type = CurrencyType.Default)
|
||||
{
|
||||
if (type == CurrencyType.Default)
|
||||
{
|
||||
await using var ctx = _db.GetDbContext();
|
||||
await ctx.DiscordUser
|
||||
.Where(x => userIds.Contains(x.UserId))
|
||||
.UpdateAsync(du => new()
|
||||
{
|
||||
CurrencyAmount = du.CurrencyAmount >= amount
|
||||
? du.CurrencyAmount - amount
|
||||
: 0
|
||||
});
|
||||
await ctx.SaveChangesAsync();
|
||||
return;
|
||||
}
|
||||
|
||||
throw new ArgumentOutOfRangeException(nameof(type));
|
||||
}
|
||||
|
||||
public async Task AddAsync(
|
||||
ulong userId,
|
||||
long amount,
|
||||
TxData txData)
|
||||
{
|
||||
var wallet = await GetWalletAsync(userId);
|
||||
await wallet.Add(amount, txData);
|
||||
await _txTracker.TrackAdd(amount, txData);
|
||||
}
|
||||
|
||||
public async Task AddAsync(
|
||||
IUser user,
|
||||
long amount,
|
||||
TxData txData)
|
||||
=> await AddAsync(user.Id, amount, txData);
|
||||
|
||||
public async Task<bool> RemoveAsync(
|
||||
ulong userId,
|
||||
long amount,
|
||||
TxData txData)
|
||||
{
|
||||
if (amount == 0)
|
||||
return true;
|
||||
|
||||
var wallet = await GetWalletAsync(userId);
|
||||
var result = await wallet.Take(amount, txData);
|
||||
if(result)
|
||||
await _txTracker.TrackRemove(amount, txData);
|
||||
return result;
|
||||
}
|
||||
|
||||
public async Task<bool> RemoveAsync(
|
||||
IUser user,
|
||||
long amount,
|
||||
TxData txData)
|
||||
=> await RemoveAsync(user.Id, amount, txData);
|
||||
}
|
@@ -1,39 +0,0 @@
|
||||
using NadekoBot.Services.Currency;
|
||||
|
||||
namespace NadekoBot.Services;
|
||||
|
||||
public static class CurrencyServiceExtensions
|
||||
{
|
||||
public static async Task<long> GetBalanceAsync(this ICurrencyService cs, ulong userId)
|
||||
{
|
||||
var wallet = await cs.GetWalletAsync(userId);
|
||||
return await wallet.GetBalance();
|
||||
}
|
||||
|
||||
// FUTURE should be a transaction
|
||||
public static async Task<bool> TransferAsync(
|
||||
this ICurrencyService cs,
|
||||
IEmbedBuilderService ebs,
|
||||
IUser from,
|
||||
IUser to,
|
||||
long amount,
|
||||
string? note,
|
||||
string formattedAmount)
|
||||
{
|
||||
var fromWallet = await cs.GetWalletAsync(from.Id);
|
||||
var toWallet = await cs.GetWalletAsync(to.Id);
|
||||
|
||||
var extra = new TxData("gift", from.ToString()!, note, from.Id);
|
||||
|
||||
if (await fromWallet.Transfer(amount, toWallet, extra))
|
||||
{
|
||||
await to.SendConfirmAsync(ebs,
|
||||
string.IsNullOrWhiteSpace(note)
|
||||
? $"Received {formattedAmount} from {from} "
|
||||
: $"Received {formattedAmount} from {from}: {note}");
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
@@ -1,6 +0,0 @@
|
||||
namespace NadekoBot.Services.Currency;
|
||||
|
||||
public enum CurrencyType
|
||||
{
|
||||
Default
|
||||
}
|
@@ -1,113 +0,0 @@
|
||||
using LinqToDB;
|
||||
using LinqToDB.EntityFrameworkCore;
|
||||
using NadekoBot.Services.Database;
|
||||
using NadekoBot.Services.Database.Models;
|
||||
|
||||
namespace NadekoBot.Services.Currency;
|
||||
|
||||
public class DefaultWallet : IWallet
|
||||
{
|
||||
private readonly DbService _db;
|
||||
public ulong UserId { get; }
|
||||
|
||||
public DefaultWallet(ulong userId, DbService db)
|
||||
{
|
||||
UserId = userId;
|
||||
_db = db;
|
||||
}
|
||||
|
||||
public async Task<long> GetBalance()
|
||||
{
|
||||
await using var ctx = _db.GetDbContext();
|
||||
var userId = UserId;
|
||||
return await ctx.DiscordUser
|
||||
.ToLinqToDBTable()
|
||||
.Where(x => x.UserId == userId)
|
||||
.Select(x => x.CurrencyAmount)
|
||||
.FirstOrDefaultAsync();
|
||||
}
|
||||
|
||||
public async Task<bool> Take(long amount, TxData? txData)
|
||||
{
|
||||
if (amount < 0)
|
||||
throw new ArgumentOutOfRangeException(nameof(amount), "Amount to take must be non negative.");
|
||||
|
||||
await using var ctx = _db.GetDbContext();
|
||||
|
||||
var userId = UserId;
|
||||
var changed = await ctx.DiscordUser
|
||||
.Where(x => x.UserId == userId && x.CurrencyAmount >= amount)
|
||||
.UpdateAsync(x => new()
|
||||
{
|
||||
CurrencyAmount = x.CurrencyAmount - amount
|
||||
});
|
||||
|
||||
if (changed == 0)
|
||||
return false;
|
||||
|
||||
if (txData is not null)
|
||||
{
|
||||
await ctx
|
||||
.GetTable<CurrencyTransaction>()
|
||||
.InsertAsync(() => new()
|
||||
{
|
||||
Amount = -amount,
|
||||
Note = txData.Note,
|
||||
UserId = userId,
|
||||
Type = txData.Type,
|
||||
Extra = txData.Extra,
|
||||
OtherId = txData.OtherId,
|
||||
DateAdded = DateTime.UtcNow
|
||||
});
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public async Task Add(long amount, TxData? txData)
|
||||
{
|
||||
if (amount <= 0)
|
||||
throw new ArgumentOutOfRangeException(nameof(amount), "Amount must be greater than 0.");
|
||||
|
||||
await using var ctx = _db.GetDbContext();
|
||||
var userId = UserId;
|
||||
|
||||
await using (var tran = await ctx.Database.BeginTransactionAsync())
|
||||
{
|
||||
var changed = await ctx.DiscordUser
|
||||
.Where(x => x.UserId == userId)
|
||||
.UpdateAsync(x => new()
|
||||
{
|
||||
CurrencyAmount = x.CurrencyAmount + amount
|
||||
});
|
||||
|
||||
if (changed == 0)
|
||||
{
|
||||
await ctx.DiscordUser
|
||||
.ToLinqToDBTable()
|
||||
.Value(x => x.UserId, userId)
|
||||
.Value(x => x.Username, "Unknown")
|
||||
.Value(x => x.Discriminator, "????")
|
||||
.Value(x => x.CurrencyAmount, amount)
|
||||
.InsertAsync();
|
||||
}
|
||||
|
||||
await tran.CommitAsync();
|
||||
}
|
||||
|
||||
if (txData is not null)
|
||||
{
|
||||
await ctx.GetTable<CurrencyTransaction>()
|
||||
.InsertAsync(() => new()
|
||||
{
|
||||
Amount = amount,
|
||||
UserId = userId,
|
||||
Note = txData.Note,
|
||||
Type = txData.Type,
|
||||
Extra = txData.Extra,
|
||||
OtherId = txData.OtherId,
|
||||
DateAdded = DateTime.UtcNow
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,110 +0,0 @@
|
||||
using LinqToDB;
|
||||
using LinqToDB.EntityFrameworkCore;
|
||||
using NadekoBot.Common.ModuleBehaviors;
|
||||
using NadekoBot.Services.Currency;
|
||||
using NadekoBot.Services.Database.Models;
|
||||
|
||||
namespace NadekoBot.Services;
|
||||
|
||||
public sealed class GamblingTxTracker : ITxTracker, INService, IReadyExecutor
|
||||
{
|
||||
private static readonly IReadOnlySet<string> _gamblingTypes = new HashSet<string>(new[]
|
||||
{
|
||||
"lula",
|
||||
"betroll",
|
||||
"betflip",
|
||||
"blackjack",
|
||||
"betdraw",
|
||||
"slot",
|
||||
});
|
||||
|
||||
private ConcurrentDictionary<string, (decimal Bet, decimal PaidOut)> _stats = new();
|
||||
|
||||
private readonly DbService _db;
|
||||
|
||||
public GamblingTxTracker(DbService db)
|
||||
{
|
||||
_db = db;
|
||||
}
|
||||
|
||||
public async Task OnReadyAsync()
|
||||
{
|
||||
using var timer = new PeriodicTimer(TimeSpan.FromHours(1));
|
||||
while (await timer.WaitForNextTickAsync())
|
||||
{
|
||||
await using var ctx = _db.GetDbContext();
|
||||
await using var trans = await ctx.Database.BeginTransactionAsync();
|
||||
|
||||
try
|
||||
{
|
||||
var keys = _stats.Keys;
|
||||
foreach (var key in keys)
|
||||
{
|
||||
if (_stats.TryRemove(key, out var stat))
|
||||
{
|
||||
await ctx.GetTable<GamblingStats>()
|
||||
.InsertOrUpdateAsync(() => new()
|
||||
{
|
||||
Feature = key,
|
||||
Bet = stat.Bet,
|
||||
PaidOut = stat.PaidOut,
|
||||
DateAdded = DateTime.UtcNow
|
||||
}, old => new()
|
||||
{
|
||||
Bet = old.Bet + stat.Bet,
|
||||
PaidOut = old.PaidOut + stat.PaidOut,
|
||||
}, () => new()
|
||||
{
|
||||
Feature = key
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Error(ex, "An error occurred in gambling tx tracker");
|
||||
}
|
||||
finally
|
||||
{
|
||||
await trans.CommitAsync();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public Task TrackAdd(long amount, TxData? txData)
|
||||
{
|
||||
if (txData is null)
|
||||
return Task.CompletedTask;
|
||||
|
||||
if (_gamblingTypes.Contains(txData.Type))
|
||||
{
|
||||
_stats.AddOrUpdate(txData.Type,
|
||||
_ => (0, amount),
|
||||
(_, old) => (old.Bet, old.PaidOut + amount));
|
||||
}
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task TrackRemove(long amount, TxData? txData)
|
||||
{
|
||||
if (txData is null)
|
||||
return Task.CompletedTask;
|
||||
|
||||
if (_gamblingTypes.Contains(txData.Type))
|
||||
{
|
||||
_stats.AddOrUpdate(txData.Type,
|
||||
_ => (amount, 0),
|
||||
(_, old) => (old.Bet + amount, old.PaidOut));
|
||||
}
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public async Task<IReadOnlyCollection<GamblingStats>> GetAllAsync()
|
||||
{
|
||||
await using var ctx = _db.GetDbContext();
|
||||
return await ctx.Set<GamblingStats>()
|
||||
.ToListAsyncEF();
|
||||
}
|
||||
}
|
@@ -1,40 +0,0 @@
|
||||
using NadekoBot.Services.Currency;
|
||||
|
||||
namespace NadekoBot.Services;
|
||||
|
||||
public interface ICurrencyService
|
||||
{
|
||||
Task<IWallet> GetWalletAsync(ulong userId, CurrencyType type = CurrencyType.Default);
|
||||
|
||||
Task AddBulkAsync(
|
||||
IReadOnlyCollection<ulong> userIds,
|
||||
long amount,
|
||||
TxData? txData,
|
||||
CurrencyType type = CurrencyType.Default);
|
||||
|
||||
Task RemoveBulkAsync(
|
||||
IReadOnlyCollection<ulong> userIds,
|
||||
long amount,
|
||||
TxData? txData,
|
||||
CurrencyType type = CurrencyType.Default);
|
||||
|
||||
Task AddAsync(
|
||||
ulong userId,
|
||||
long amount,
|
||||
TxData? txData);
|
||||
|
||||
Task AddAsync(
|
||||
IUser user,
|
||||
long amount,
|
||||
TxData? txData);
|
||||
|
||||
Task<bool> RemoveAsync(
|
||||
ulong userId,
|
||||
long amount,
|
||||
TxData? txData);
|
||||
|
||||
Task<bool> RemoveAsync(
|
||||
IUser user,
|
||||
long amount,
|
||||
TxData? txData);
|
||||
}
|
@@ -1,9 +0,0 @@
|
||||
using NadekoBot.Services.Currency;
|
||||
|
||||
namespace NadekoBot.Services;
|
||||
|
||||
public interface ITxTracker
|
||||
{
|
||||
Task TrackAdd(long amount, TxData? txData);
|
||||
Task TrackRemove(long amount, TxData? txData);
|
||||
}
|
@@ -1,40 +0,0 @@
|
||||
namespace NadekoBot.Services.Currency;
|
||||
|
||||
public interface IWallet
|
||||
{
|
||||
public ulong UserId { get; }
|
||||
|
||||
public Task<long> GetBalance();
|
||||
public Task<bool> Take(long amount, TxData? txData);
|
||||
public Task Add(long amount, TxData? txData);
|
||||
|
||||
public async Task<bool> Transfer(
|
||||
long amount,
|
||||
IWallet to,
|
||||
TxData? txData)
|
||||
{
|
||||
if (amount <= 0)
|
||||
throw new ArgumentOutOfRangeException(nameof(amount), "Amount must be greater than 0.");
|
||||
|
||||
if (txData is not null)
|
||||
txData = txData with
|
||||
{
|
||||
OtherId = to.UserId
|
||||
};
|
||||
|
||||
var succ = await Take(amount, txData);
|
||||
|
||||
if (!succ)
|
||||
return false;
|
||||
|
||||
if (txData is not null)
|
||||
txData = txData with
|
||||
{
|
||||
OtherId = UserId
|
||||
};
|
||||
|
||||
await to.Add(amount, txData);
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
@@ -1,7 +0,0 @@
|
||||
namespace NadekoBot.Services.Currency;
|
||||
|
||||
public record class TxData(
|
||||
string Type,
|
||||
string Extra,
|
||||
string? Note = "",
|
||||
ulong? OtherId = null);
|
@@ -1,13 +0,0 @@
|
||||
#nullable disable
|
||||
using NadekoBot.Common.Pokemon;
|
||||
using NadekoBot.Modules.Games.Common.Trivia;
|
||||
|
||||
namespace NadekoBot.Services;
|
||||
|
||||
public interface ILocalDataCache
|
||||
{
|
||||
Task<IReadOnlyDictionary<string, SearchPokemon>> GetPokemonsAsync();
|
||||
Task<IReadOnlyDictionary<string, SearchPokemonAbility>> GetPokemonAbilitiesAsync();
|
||||
Task<TriviaQuestionModel[]> GetTriviaQuestionsAsync();
|
||||
Task<IReadOnlyDictionary<int, string>> GetPokemonMapAsync();
|
||||
}
|
@@ -60,9 +60,16 @@ public sealed class BotCredsProvider : IBotCredsProvider
|
||||
CredsExamplePath);
|
||||
}
|
||||
|
||||
_config = new ConfigurationBuilder().AddYamlFile(CredsPath, false, true)
|
||||
.AddEnvironmentVariables("NadekoBot_")
|
||||
.Build();
|
||||
try
|
||||
{
|
||||
_config = new ConfigurationBuilder().AddYamlFile(CredsPath, false, true)
|
||||
.AddEnvironmentVariables("NadekoBot_")
|
||||
.Build();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine(ex.ToString());
|
||||
}
|
||||
|
||||
_changeToken = ChangeToken.OnChange(() => _config.GetReloadToken(), Reload);
|
||||
Reload();
|
||||
|
@@ -1,60 +0,0 @@
|
||||
#nullable disable
|
||||
using SixLabors.Fonts;
|
||||
|
||||
namespace NadekoBot.Services;
|
||||
|
||||
public class FontProvider : INService
|
||||
{
|
||||
public FontFamily DottyFont { get; }
|
||||
|
||||
public FontFamily UniSans { get; }
|
||||
|
||||
public FontFamily NotoSans { get; }
|
||||
//public FontFamily Emojis { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Font used for .rip command
|
||||
/// </summary>
|
||||
public Font RipFont { get; }
|
||||
|
||||
public List<FontFamily> FallBackFonts { get; }
|
||||
private readonly FontCollection _fonts;
|
||||
|
||||
public FontProvider()
|
||||
{
|
||||
_fonts = new();
|
||||
|
||||
NotoSans = _fonts.Add("data/fonts/NotoSans-Bold.ttf");
|
||||
UniSans = _fonts.Add("data/fonts/Uni Sans.ttf");
|
||||
|
||||
FallBackFonts = new();
|
||||
|
||||
//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
|
||||
{
|
||||
var fontsfolder = Environment.GetFolderPath(Environment.SpecialFolder.Fonts);
|
||||
FallBackFonts.Add(_fonts.Add(Path.Combine(fontsfolder, "seguiemj.ttf")));
|
||||
FallBackFonts.AddRange(_fonts.AddCollection(Path.Combine(fontsfolder, "msgothic.ttc")));
|
||||
FallBackFonts.AddRange(_fonts.AddCollection(Path.Combine(fontsfolder, "segoe.ttc")));
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
|
||||
// any fonts present in data/fonts should be added as fallback fonts
|
||||
// this will allow support for special characters when drawing text
|
||||
foreach (var font in Directory.GetFiles(@"data/fonts"))
|
||||
{
|
||||
if (font.EndsWith(".ttf"))
|
||||
FallBackFonts.Add(_fonts.Add(font));
|
||||
else if (font.EndsWith(".ttc"))
|
||||
FallBackFonts.AddRange(_fonts.AddCollection(font));
|
||||
}
|
||||
|
||||
RipFont = NotoSans.CreateFont(20, FontStyle.Bold);
|
||||
DottyFont = FallBackFonts.First(x => x.Name == "dotty");
|
||||
}
|
||||
}
|
@@ -1,38 +1,4 @@
|
||||
using NadekoBot.Common.Configs;
|
||||
|
||||
namespace NadekoBot.Services;
|
||||
|
||||
public sealed class ImagesConfig : ConfigServiceBase<ImageUrls>
|
||||
{
|
||||
private const string PATH = "data/images.yml";
|
||||
|
||||
private static readonly TypedKey<ImageUrls> _changeKey =
|
||||
new("config.images.updated");
|
||||
|
||||
public override string Name
|
||||
=> "images";
|
||||
|
||||
public ImagesConfig(IConfigSeria serializer, IPubSub pubSub)
|
||||
: base(PATH, serializer, pubSub, _changeKey)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
public interface IImageCache
|
||||
{
|
||||
Task<byte[]?> GetHeadsImageAsync();
|
||||
Task<byte[]?> GetTailsImageAsync();
|
||||
Task<byte[]?> GetCurrencyImageAsync();
|
||||
Task<byte[]?> GetXpBackgroundImageAsync();
|
||||
Task<byte[]?> GetRategirlBgAsync();
|
||||
Task<byte[]?> GetRategirlDotAsync();
|
||||
Task<byte[]?> GetDiceAsync(int num);
|
||||
Task<byte[]?> GetSlotEmojiAsync(int number);
|
||||
Task<byte[]?> GetSlotBgAsync();
|
||||
Task<byte[]?> GetRipBgAsync();
|
||||
Task<byte[]?> GetRipOverlayAsync();
|
||||
Task<byte[]?> GetImageDataAsync(Uri url);
|
||||
}
|
||||
namespace NadekoBot.Services;
|
||||
|
||||
public sealed class ImageCache : IImageCache, INService
|
||||
{
|
||||
@@ -114,4 +80,4 @@ public sealed class ImageCache : IImageCache, INService
|
||||
|
||||
public Task<byte[]?> GetRipOverlayAsync()
|
||||
=> GetImageDataAsync(_ic.Data.Rip.Overlay);
|
||||
}
|
||||
}
|
27
src/NadekoBot/Services/Impl/PubSub/JsonSeria.cs
Normal file
27
src/NadekoBot/Services/Impl/PubSub/JsonSeria.cs
Normal file
@@ -0,0 +1,27 @@
|
||||
using NadekoBot.Common.JsonConverters;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace NadekoBot.Common;
|
||||
|
||||
public class JsonSeria : ISeria
|
||||
{
|
||||
private readonly JsonSerializerOptions _serializerOptions = new()
|
||||
{
|
||||
Converters =
|
||||
{
|
||||
new Rgba32Converter(),
|
||||
new CultureInfoConverter()
|
||||
}
|
||||
};
|
||||
|
||||
public byte[] Serialize<T>(T data)
|
||||
=> JsonSerializer.SerializeToUtf8Bytes(data, _serializerOptions);
|
||||
|
||||
public T? Deserialize<T>(byte[]? data)
|
||||
{
|
||||
if (data is null)
|
||||
return default;
|
||||
|
||||
return JsonSerializer.Deserialize<T>(data, _serializerOptions);
|
||||
}
|
||||
}
|
52
src/NadekoBot/Services/Impl/PubSub/RedisPubSub.cs
Normal file
52
src/NadekoBot/Services/Impl/PubSub/RedisPubSub.cs
Normal file
@@ -0,0 +1,52 @@
|
||||
using StackExchange.Redis;
|
||||
|
||||
namespace NadekoBot.Common;
|
||||
|
||||
public sealed class RedisPubSub : IPubSub
|
||||
{
|
||||
private readonly IBotCredentials _creds;
|
||||
private readonly ConnectionMultiplexer _multi;
|
||||
private readonly ISeria _serializer;
|
||||
|
||||
public RedisPubSub(ConnectionMultiplexer multi, ISeria serializer, IBotCredentials creds)
|
||||
{
|
||||
_multi = multi;
|
||||
_serializer = serializer;
|
||||
_creds = creds;
|
||||
}
|
||||
|
||||
public Task Pub<TData>(in TypedKey<TData> key, TData data)
|
||||
where TData : notnull
|
||||
{
|
||||
var serialized = _serializer.Serialize(data);
|
||||
return _multi.GetSubscriber()
|
||||
.PublishAsync($"{_creds.RedisKey()}:{key.Key}", serialized, CommandFlags.FireAndForget);
|
||||
}
|
||||
|
||||
public Task Sub<TData>(in TypedKey<TData> key, Func<TData, ValueTask> action)
|
||||
where TData : notnull
|
||||
{
|
||||
var eventName = key.Key;
|
||||
|
||||
async void OnSubscribeHandler(RedisChannel _, RedisValue data)
|
||||
{
|
||||
try
|
||||
{
|
||||
var dataObj = _serializer.Deserialize<TData>(data);
|
||||
if (dataObj is not null)
|
||||
await action(dataObj);
|
||||
else
|
||||
{
|
||||
Log.Warning("Publishing event {EventName} with a null value. This is not allowed",
|
||||
eventName);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Error("Error handling the event {EventName}: {ErrorMessage}", eventName, ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
return _multi.GetSubscriber().SubscribeAsync($"{_creds.RedisKey()}:{eventName}", OnSubscribeHandler);
|
||||
}
|
||||
}
|
39
src/NadekoBot/Services/Impl/PubSub/YamlSeria.cs
Normal file
39
src/NadekoBot/Services/Impl/PubSub/YamlSeria.cs
Normal file
@@ -0,0 +1,39 @@
|
||||
using NadekoBot.Common.Configs;
|
||||
using NadekoBot.Common.Yml;
|
||||
using System.Text.RegularExpressions;
|
||||
using YamlDotNet.Serialization;
|
||||
|
||||
namespace NadekoBot.Common;
|
||||
|
||||
public class YamlSeria : IConfigSeria
|
||||
{
|
||||
private static readonly Regex _codePointRegex =
|
||||
new(@"(\\U(?<code>[a-zA-Z0-9]{8})|\\u(?<code>[a-zA-Z0-9]{4})|\\x(?<code>[a-zA-Z0-9]{2}))",
|
||||
RegexOptions.Compiled);
|
||||
|
||||
private readonly IDeserializer _deserializer;
|
||||
private readonly ISerializer _serializer;
|
||||
|
||||
public YamlSeria()
|
||||
{
|
||||
_serializer = Yaml.Serializer;
|
||||
_deserializer = Yaml.Deserializer;
|
||||
}
|
||||
|
||||
public string Serialize<T>(T obj)
|
||||
where T : notnull
|
||||
{
|
||||
var escapedOutput = _serializer.Serialize(obj);
|
||||
var output = _codePointRegex.Replace(escapedOutput,
|
||||
me =>
|
||||
{
|
||||
var str = me.Groups["code"].Value;
|
||||
var newString = YamlHelper.UnescapeUnicodeCodePoint(str);
|
||||
return newString;
|
||||
});
|
||||
return output;
|
||||
}
|
||||
|
||||
public T Deserialize<T>(string data)
|
||||
=> _deserializer.Deserialize<T>(data);
|
||||
}
|
119
src/NadekoBot/Services/Impl/RedisBotCache.cs
Normal file
119
src/NadekoBot/Services/Impl/RedisBotCache.cs
Normal file
@@ -0,0 +1,119 @@
|
||||
using OneOf;
|
||||
using OneOf.Types;
|
||||
using StackExchange.Redis;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace NadekoBot.Common;
|
||||
|
||||
public sealed class RedisBotCache : IBotCache
|
||||
{
|
||||
private static readonly Type[] _supportedTypes = new []
|
||||
{
|
||||
typeof(bool), typeof(int), typeof(uint), typeof(long),
|
||||
typeof(ulong), typeof(float), typeof(double),
|
||||
typeof(string), typeof(byte[]), typeof(ReadOnlyMemory<byte>), typeof(Memory<byte>),
|
||||
typeof(RedisValue),
|
||||
};
|
||||
|
||||
private static readonly JsonSerializerOptions _opts = new()
|
||||
{
|
||||
PropertyNameCaseInsensitive = true,
|
||||
NumberHandling = JsonNumberHandling.AllowReadingFromString,
|
||||
AllowTrailingCommas = true,
|
||||
IgnoreReadOnlyProperties = false,
|
||||
};
|
||||
private readonly ConnectionMultiplexer _conn;
|
||||
|
||||
public RedisBotCache(ConnectionMultiplexer conn)
|
||||
{
|
||||
_conn = conn;
|
||||
}
|
||||
|
||||
public async ValueTask<bool> AddAsync<T>(TypedKey<T> key, T value, TimeSpan? expiry = null, bool overwrite = true)
|
||||
{
|
||||
// if a null value is passed, remove the key
|
||||
if (value is null)
|
||||
{
|
||||
await RemoveAsync(key);
|
||||
return false;
|
||||
}
|
||||
|
||||
var db = _conn.GetDatabase();
|
||||
RedisValue val = IsSupportedType(typeof(T))
|
||||
? RedisValue.Unbox(value)
|
||||
: JsonSerializer.Serialize(value, _opts);
|
||||
|
||||
var success = await db.StringSetAsync(key.Key,
|
||||
val,
|
||||
expiry: expiry,
|
||||
when: overwrite ? When.Always : When.NotExists);
|
||||
|
||||
return success;
|
||||
}
|
||||
|
||||
public bool IsSupportedType(Type type)
|
||||
{
|
||||
if (type.IsGenericType)
|
||||
{
|
||||
var typeDef = type.GetGenericTypeDefinition();
|
||||
if (typeDef == typeof(Nullable<>))
|
||||
return IsSupportedType(type.GenericTypeArguments[0]);
|
||||
}
|
||||
|
||||
foreach (var t in _supportedTypes)
|
||||
{
|
||||
if (type == t)
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public async ValueTask<OneOf<T, None>> GetAsync<T>(TypedKey<T> key)
|
||||
{
|
||||
var db = _conn.GetDatabase();
|
||||
var val = await db.StringGetAsync(key.Key);
|
||||
if (val == default)
|
||||
return new None();
|
||||
|
||||
if (IsSupportedType(typeof(T)))
|
||||
return (T)((IConvertible)val).ToType(typeof(T), null);
|
||||
|
||||
return JsonSerializer.Deserialize<T>(val.ToString(), _opts)!;
|
||||
}
|
||||
|
||||
public async ValueTask<bool> RemoveAsync<T>(TypedKey<T> key)
|
||||
{
|
||||
var db = _conn.GetDatabase();
|
||||
|
||||
return await db.KeyDeleteAsync(key.Key);
|
||||
}
|
||||
|
||||
public async ValueTask<T?> GetOrAddAsync<T>(TypedKey<T> key, Func<Task<T?>> createFactory, TimeSpan? expiry = null)
|
||||
{
|
||||
var result = await GetAsync(key);
|
||||
|
||||
return await result.Match<Task<T?>>(
|
||||
v => Task.FromResult<T?>(v),
|
||||
async _ =>
|
||||
{
|
||||
var factoryValue = await createFactory();
|
||||
|
||||
if (factoryValue is null)
|
||||
return default;
|
||||
|
||||
await AddAsync(key, factoryValue, expiry);
|
||||
|
||||
// get again to make sure it's the cached value
|
||||
// and not the late factory value, in case there's a race condition
|
||||
|
||||
var newResult = await GetAsync(key);
|
||||
|
||||
// it's fine to do this, it should blow up if something went wrong.
|
||||
return newResult.Match<T?>(
|
||||
v => v,
|
||||
_ => default);
|
||||
});
|
||||
}
|
||||
}
|
@@ -1,11 +0,0 @@
|
||||
#nullable disable
|
||||
namespace NadekoBot.Services;
|
||||
|
||||
public static class RedisImageExtensions
|
||||
{
|
||||
private const string OLD_CDN_URL = "nadeko-pictures.nyc3.digitaloceanspaces.com";
|
||||
private const string NEW_CDN_URL = "cdn.nadeko.bot";
|
||||
|
||||
public static Uri ToNewCdn(this Uri uri)
|
||||
=> new(uri.ToString().Replace(OLD_CDN_URL, NEW_CDN_URL));
|
||||
}
|
@@ -1,58 +0,0 @@
|
||||
#nullable disable
|
||||
using System.Diagnostics;
|
||||
|
||||
namespace NadekoBot.Services;
|
||||
|
||||
public class SingleProcessCoordinator : ICoordinator
|
||||
{
|
||||
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))
|
||||
{
|
||||
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 () =>
|
||||
{
|
||||
await Task.Delay(2000);
|
||||
Die();
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
public void Die(bool graceful = false)
|
||||
=> Environment.Exit(5);
|
||||
|
||||
public bool RestartShard(int shardId)
|
||||
=> RestartBot();
|
||||
|
||||
public IList<ShardStatus> GetAllShardStatuses()
|
||||
=> new[]
|
||||
{
|
||||
new ShardStatus
|
||||
{
|
||||
ConnectionState = _client.ConnectionState,
|
||||
GuildCount = _client.Guilds.Count,
|
||||
LastUpdate = DateTime.UtcNow,
|
||||
ShardId = _client.ShardId
|
||||
}
|
||||
};
|
||||
|
||||
public int GetGuildCount()
|
||||
=> _client.Guilds.Count;
|
||||
|
||||
public Task Reload()
|
||||
=> Task.CompletedTask;
|
||||
}
|
@@ -1,18 +0,0 @@
|
||||
#nullable disable
|
||||
using System.Collections;
|
||||
|
||||
namespace NadekoBot.Services;
|
||||
|
||||
public class StartingGuildsService : IEnumerable<ulong>, INService
|
||||
{
|
||||
private readonly IReadOnlyList<ulong> _guilds;
|
||||
|
||||
public StartingGuildsService(DiscordSocketClient client)
|
||||
=> _guilds = client.Guilds.Select(x => x.Id).ToList();
|
||||
|
||||
public IEnumerator<ulong> GetEnumerator()
|
||||
=> _guilds.GetEnumerator();
|
||||
|
||||
IEnumerator IEnumerable.GetEnumerator()
|
||||
=> _guilds.GetEnumerator();
|
||||
}
|
@@ -7,14 +7,11 @@ namespace NadekoBot.Services;
|
||||
|
||||
public sealed class StatsService : IStatsService, IReadyExecutor, INService
|
||||
{
|
||||
public const string BOT_VERSION = "4.3.13";
|
||||
public const string BOT_VERSION = "5.0.0-alpha1";
|
||||
|
||||
public string Author
|
||||
=> "Kwoth#2452";
|
||||
|
||||
public string Library
|
||||
=> "Discord.Net";
|
||||
|
||||
public double MessagesPerSecond
|
||||
=> MessageCounter / GetUptime().TotalSeconds;
|
||||
|
||||
|
@@ -1,78 +0,0 @@
|
||||
#nullable disable
|
||||
using System.ComponentModel;
|
||||
using System.Diagnostics;
|
||||
using System.Text;
|
||||
using Nadeko.Common;
|
||||
|
||||
namespace NadekoBot.Services;
|
||||
|
||||
public class YtdlOperation
|
||||
{
|
||||
private readonly string _baseArgString;
|
||||
private readonly bool _isYtDlp;
|
||||
|
||||
public YtdlOperation(string baseArgString, bool isYtDlp = false)
|
||||
{
|
||||
_baseArgString = baseArgString;
|
||||
_isYtDlp = isYtDlp;
|
||||
}
|
||||
|
||||
private Process CreateProcess(string[] args)
|
||||
{
|
||||
var newArgs = args.Map(arg => (object)arg.Replace("\"", ""));
|
||||
return new()
|
||||
{
|
||||
StartInfo = new()
|
||||
{
|
||||
FileName = _isYtDlp ? "yt-dlp" : "youtube-dl",
|
||||
Arguments = string.Format(_baseArgString, newArgs),
|
||||
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 {FileName} {Arguments}", process.StartInfo.FileName, process.StartInfo.Arguments);
|
||||
process.Start();
|
||||
|
||||
var str = await process.StandardOutput.ReadToEndAsync();
|
||||
var err = await process.StandardError.ReadToEndAsync();
|
||||
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 {FileName} {Arguments}", process.StartInfo.FileName, process.StartInfo.Arguments);
|
||||
process.Start();
|
||||
|
||||
string line;
|
||||
while ((line = await process.StandardOutput.ReadLineAsync()) is not null)
|
||||
yield return line;
|
||||
}
|
||||
}
|
@@ -1,101 +0,0 @@
|
||||
#nullable disable
|
||||
using System.Globalization;
|
||||
|
||||
namespace NadekoBot.Services;
|
||||
|
||||
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("en-US");
|
||||
|
||||
private readonly ILocalization _localization;
|
||||
private readonly IBotStringsProvider _stringsProvider;
|
||||
|
||||
public BotStrings(ILocalization loc, IBotStringsProvider stringsProvider)
|
||||
{
|
||||
_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);
|
||||
|
||||
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.";
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
Log.Warning("'{CommandName}' doesn't exist in 'en-US' command strings. Please report this",
|
||||
commandName);
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
public void Reload()
|
||||
=> _stringsProvider.Reload();
|
||||
}
|
@@ -1,73 +0,0 @@
|
||||
#nullable disable
|
||||
using Newtonsoft.Json;
|
||||
using YamlDotNet.Serialization;
|
||||
|
||||
namespace NadekoBot.Services;
|
||||
|
||||
/// <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")
|
||||
{
|
||||
_responsesPath = responsesPath;
|
||||
_commandsPath = commandsPath;
|
||||
}
|
||||
|
||||
public Dictionary<string, Dictionary<string, string>> GetResponseStrings()
|
||||
{
|
||||
var outputDict = new Dictionary<string, Dictionary<string, string>>();
|
||||
foreach (var file in Directory.GetFiles(_responsesPath))
|
||||
{
|
||||
try
|
||||
{
|
||||
var langDict = JsonConvert.DeserializeObject<Dictionary<string, string>>(File.ReadAllText(file));
|
||||
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();
|
||||
|
||||
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)
|
||||
{
|
||||
fileName = Path.GetFileName(fileName);
|
||||
var dotIndex = fileName.IndexOf('.') + 1;
|
||||
var secondDotIndex = fileName.LastIndexOf('.');
|
||||
return fileName.Substring(dotIndex, secondDotIndex - dotIndex);
|
||||
}
|
||||
}
|
@@ -1,38 +0,0 @@
|
||||
#nullable disable
|
||||
namespace NadekoBot.Services;
|
||||
|
||||
public class MemoryBotStringsProvider : IBotStringsProvider
|
||||
{
|
||||
private readonly IStringsSource _source;
|
||||
private IReadOnlyDictionary<string, Dictionary<string, string>> responseStrings;
|
||||
private IReadOnlyDictionary<string, Dictionary<string, CommandStrings>> commandStrings;
|
||||
|
||||
public MemoryBotStringsProvider(IStringsSource source)
|
||||
{
|
||||
_source = source;
|
||||
Reload();
|
||||
}
|
||||
|
||||
public string GetText(string localeName, string key)
|
||||
{
|
||||
if (responseStrings.TryGetValue(localeName, out var langStrings) && langStrings.TryGetValue(key, out var text))
|
||||
return text;
|
||||
|
||||
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))
|
||||
return strings;
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user