mirror of
https://gitlab.com/Kwoth/nadekobot.git
synced 2025-09-11 09:48:26 -04:00
Restructured the project structure back to the way it was, there's no reasonable way to split the modules
This commit is contained in:
426
src/NadekoBot/_common/Services/CommandHandler.cs
Normal file
426
src/NadekoBot/_common/Services/CommandHandler.cs
Normal file
@@ -0,0 +1,426 @@
|
||||
#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 IBot _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,
|
||||
IBot 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()
|
||||
{
|
||||
// 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);
|
||||
}
|
||||
}
|
116
src/NadekoBot/_common/Services/Currency/CurrencyService.cs
Normal file
116
src/NadekoBot/_common/Services/Currency/CurrencyService.cs
Normal file
@@ -0,0 +1,116 @@
|
||||
#nullable disable
|
||||
using LinqToDB;
|
||||
using LinqToDB.EntityFrameworkCore;
|
||||
using NadekoBot.Db;
|
||||
using NadekoBot.Db.Models;
|
||||
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
|
||||
.GetTable<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);
|
||||
|
||||
public async Task<IReadOnlyList<DiscordUser>> GetTopRichest(ulong ignoreId, int page = 0, int perPage = 9)
|
||||
{
|
||||
await using var uow = _db.GetDbContext();
|
||||
return await uow.Set<DiscordUser>().GetTopRichest(ignoreId, page, perPage);
|
||||
}
|
||||
}
|
@@ -0,0 +1,45 @@
|
||||
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))
|
||||
{
|
||||
try
|
||||
{
|
||||
await to.SendConfirmAsync(ebs,
|
||||
string.IsNullOrWhiteSpace(note)
|
||||
? $"Received {formattedAmount} from {from} "
|
||||
: $"Received {formattedAmount} from {from}: {note}");
|
||||
}
|
||||
catch
|
||||
{
|
||||
//ignored
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
115
src/NadekoBot/_common/Services/Currency/DefaultWallet.cs
Normal file
115
src/NadekoBot/_common/Services/Currency/DefaultWallet.cs
Normal file
@@ -0,0 +1,115 @@
|
||||
using LinqToDB;
|
||||
using LinqToDB.EntityFrameworkCore;
|
||||
using NadekoBot.Db.Models;
|
||||
using Nadeko.Bot.Db.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
|
||||
.GetTable<DiscordUser>()
|
||||
.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
|
||||
.GetTable<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
|
||||
.GetTable<DiscordUser>()
|
||||
.Where(x => x.UserId == userId)
|
||||
.UpdateAsync(x => new()
|
||||
{
|
||||
CurrencyAmount = x.CurrencyAmount + amount
|
||||
});
|
||||
|
||||
if (changed == 0)
|
||||
{
|
||||
await ctx
|
||||
.GetTable<DiscordUser>()
|
||||
.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
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
110
src/NadekoBot/_common/Services/Currency/GamblingTxTracker.cs
Normal file
110
src/NadekoBot/_common/Services/Currency/GamblingTxTracker.cs
Normal file
@@ -0,0 +1,110 @@
|
||||
using LinqToDB;
|
||||
using LinqToDB.EntityFrameworkCore;
|
||||
using NadekoBot.Common.ModuleBehaviors;
|
||||
using NadekoBot.Services.Currency;
|
||||
using Nadeko.Bot.Db.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();
|
||||
}
|
||||
}
|
17
src/NadekoBot/_common/Services/IBehaviourExecutor.cs
Normal file
17
src/NadekoBot/_common/Services/IBehaviourExecutor.cs
Normal file
@@ -0,0 +1,17 @@
|
||||
#nullable disable
|
||||
namespace NadekoBot.Services;
|
||||
|
||||
public interface IBehaviorHandler
|
||||
{
|
||||
Task<bool> AddAsync(ICustomBehavior behavior);
|
||||
Task AddRangeAsync(IEnumerable<ICustomBehavior> behavior);
|
||||
Task<bool> RemoveAsync(ICustomBehavior behavior);
|
||||
Task RemoveRangeAsync(IEnumerable<ICustomBehavior> behs);
|
||||
|
||||
Task<bool> RunExecOnMessageAsync(SocketGuild guild, IUserMessage usrMsg);
|
||||
Task<string> RunInputTransformersAsync(SocketGuild guild, IUserMessage usrMsg);
|
||||
Task<bool> RunPreCommandAsync(ICommandContext context, CommandInfo cmd);
|
||||
ValueTask RunPostCommandAsync(ICommandContext ctx, string moduleName, CommandInfo cmd);
|
||||
Task RunOnNoCommandAsync(SocketGuild guild, IUserMessage usrMsg);
|
||||
void Initialize();
|
||||
}
|
12
src/NadekoBot/_common/Services/ICommandHandler.cs
Normal file
12
src/NadekoBot/_common/Services/ICommandHandler.cs
Normal file
@@ -0,0 +1,12 @@
|
||||
namespace NadekoBot.Services;
|
||||
|
||||
public interface ICommandHandler
|
||||
{
|
||||
string GetPrefix(IGuild ctxGuild);
|
||||
string GetPrefix(ulong? id = null);
|
||||
string SetDefaultPrefix(string toSet);
|
||||
string SetPrefix(IGuild ctxGuild, string toSet);
|
||||
ConcurrentDictionary<ulong, uint> UserMessagesSent { get; }
|
||||
|
||||
Task TryRunCommand(SocketGuild guild, ISocketMessageChannel channel, IUserMessage usrMsg);
|
||||
}
|
20
src/NadekoBot/_common/Services/ICoordinator.cs
Normal file
20
src/NadekoBot/_common/Services/ICoordinator.cs
Normal file
@@ -0,0 +1,20 @@
|
||||
#nullable disable
|
||||
namespace NadekoBot.Services;
|
||||
|
||||
public interface ICoordinator
|
||||
{
|
||||
bool RestartBot();
|
||||
void Die(bool graceful);
|
||||
bool RestartShard(int shardId);
|
||||
IList<ShardStatus> GetAllShardStatuses();
|
||||
int GetGuildCount();
|
||||
Task Reload();
|
||||
}
|
||||
|
||||
public class ShardStatus
|
||||
{
|
||||
public ConnectionState ConnectionState { get; set; }
|
||||
public DateTime LastUpdate { get; set; }
|
||||
public int ShardId { get; set; }
|
||||
public int GuildCount { get; set; }
|
||||
}
|
13
src/NadekoBot/_common/Services/ICustomBehavior.cs
Normal file
13
src/NadekoBot/_common/Services/ICustomBehavior.cs
Normal file
@@ -0,0 +1,13 @@
|
||||
using NadekoBot.Common.ModuleBehaviors;
|
||||
|
||||
namespace NadekoBot.Services;
|
||||
|
||||
public interface ICustomBehavior
|
||||
: IExecOnMessage,
|
||||
IInputTransformer,
|
||||
IExecPreCommand,
|
||||
IExecNoCommand,
|
||||
IExecPostCommand
|
||||
{
|
||||
|
||||
}
|
81
src/NadekoBot/_common/Services/IEmbedBuilderService.cs
Normal file
81
src/NadekoBot/_common/Services/IEmbedBuilderService.cs
Normal file
@@ -0,0 +1,81 @@
|
||||
#nullable disable
|
||||
using NadekoBot.Common.Configs;
|
||||
|
||||
namespace NadekoBot.Services;
|
||||
|
||||
public interface IEmbedBuilderService
|
||||
{
|
||||
IEmbedBuilder Create(ICommandContext ctx = null);
|
||||
IEmbedBuilder Create(EmbedBuilder eb);
|
||||
}
|
||||
|
||||
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 sealed class DiscordEmbedBuilderWrapper : IEmbedBuilder
|
||||
{
|
||||
private readonly BotConfig _botConfig;
|
||||
private EmbedBuilder embed;
|
||||
|
||||
public DiscordEmbedBuilderWrapper(in BotConfig botConfig, EmbedBuilder embed = null)
|
||||
{
|
||||
_botConfig = botConfig;
|
||||
this.embed = embed ?? new EmbedBuilder();
|
||||
}
|
||||
|
||||
public IEmbedBuilder WithDescription(string desc)
|
||||
=> Wrap(embed.WithDescription(desc));
|
||||
|
||||
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 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 WithUrl(string url)
|
||||
=> Wrap(embed.WithUrl(url));
|
||||
|
||||
public IEmbedBuilder WithImageUrl(string url)
|
||||
=> Wrap(embed.WithImageUrl(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 IEmbedBuilder WithDiscordColor(Color color)
|
||||
=> Wrap(embed.WithColor(color));
|
||||
|
||||
public Embed Build()
|
||||
=> embed.Build();
|
||||
|
||||
private IEmbedBuilder Wrap(EmbedBuilder eb)
|
||||
{
|
||||
embed = eb;
|
||||
return this;
|
||||
}
|
||||
}
|
19
src/NadekoBot/_common/Services/IGoogleApiService.cs
Normal file
19
src/NadekoBot/_common/Services/IGoogleApiService.cs
Normal file
@@ -0,0 +1,19 @@
|
||||
#nullable disable
|
||||
|
||||
namespace NadekoBot.Services;
|
||||
|
||||
public interface IGoogleApiService
|
||||
{
|
||||
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 id, int count = 1, string user = null);
|
||||
Task<IEnumerable<string>> GetPlaylistTracksAsync(string playlistId, int count = 50);
|
||||
Task<IReadOnlyDictionary<string, TimeSpan>> GetVideoDurationsAsync(IEnumerable<string> videoIds);
|
||||
Task<string> Translate(string sourceText, string sourceLanguage, string targetLanguage);
|
||||
|
||||
Task<string> ShortenUrl(string url);
|
||||
Task<string> ShortenUrl(Uri url);
|
||||
}
|
13
src/NadekoBot/_common/Services/ILocalDataCache.cs
Normal file
13
src/NadekoBot/_common/Services/ILocalDataCache.cs
Normal file
@@ -0,0 +1,13 @@
|
||||
#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();
|
||||
}
|
19
src/NadekoBot/_common/Services/ILocalization.cs
Normal file
19
src/NadekoBot/_common/Services/ILocalization.cs
Normal file
@@ -0,0 +1,19 @@
|
||||
#nullable disable
|
||||
using System.Globalization;
|
||||
|
||||
namespace NadekoBot.Services;
|
||||
|
||||
public interface ILocalization
|
||||
{
|
||||
CultureInfo DefaultCultureInfo { get; }
|
||||
IDictionary<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);
|
||||
}
|
9
src/NadekoBot/_common/Services/INService.cs
Normal file
9
src/NadekoBot/_common/Services/INService.cs
Normal file
@@ -0,0 +1,9 @@
|
||||
#nullable disable
|
||||
namespace NadekoBot.Services;
|
||||
|
||||
/// <summary>
|
||||
/// All services must implement this interface in order to be auto-discovered by the DI system
|
||||
/// </summary>
|
||||
public interface INService
|
||||
{
|
||||
}
|
15
src/NadekoBot/_common/Services/IRemindService.cs
Normal file
15
src/NadekoBot/_common/Services/IRemindService.cs
Normal file
@@ -0,0 +1,15 @@
|
||||
#nullable disable
|
||||
using Nadeko.Bot.Db.Models;
|
||||
|
||||
namespace NadekoBot.Modules.Utility.Services;
|
||||
|
||||
public interface IRemindService
|
||||
{
|
||||
Task AddReminderAsync(ulong userId,
|
||||
ulong targetId,
|
||||
ulong? guildId,
|
||||
bool isPrivate,
|
||||
DateTime time,
|
||||
string message,
|
||||
ReminderType reminderType);
|
||||
}
|
70
src/NadekoBot/_common/Services/IStatsService.cs
Normal file
70
src/NadekoBot/_common/Services/IStatsService.cs
Normal file
@@ -0,0 +1,70 @@
|
||||
#nullable disable
|
||||
namespace NadekoBot.Services;
|
||||
|
||||
public interface IStatsService
|
||||
{
|
||||
/// <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 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 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>
|
||||
/// 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 total amount of private memory currently in use by the bot, in Megabytes.
|
||||
/// </summary>
|
||||
double GetPrivateMemoryMegabytes();
|
||||
|
||||
GuildInfo GetGuildInfo(string name);
|
||||
GuildInfo GetGuildInfo(ulong id);
|
||||
}
|
||||
|
||||
public record struct GuildInfo
|
||||
{
|
||||
public required string Name { get; init; }
|
||||
public required string IconUrl { get; init; }
|
||||
public required string Owner { get; init; }
|
||||
public required ulong OwnerId { get; init; }
|
||||
public required ulong Id { get; init; }
|
||||
public required int TextChannels { get; init; }
|
||||
public required int VoiceChannels { get; init; }
|
||||
public required DateTime CreatedAt { get; init; }
|
||||
public required IReadOnlyList<string> Features { get; init; }
|
||||
public required IReadOnlyList<Emote> Emojis { get; init; }
|
||||
public required IReadOnlyList<IRole> Roles { get; init; }
|
||||
public int MemberCount { get; init; }
|
||||
}
|
6
src/NadekoBot/_common/Services/ITimezoneService.cs
Normal file
6
src/NadekoBot/_common/Services/ITimezoneService.cs
Normal file
@@ -0,0 +1,6 @@
|
||||
namespace NadekoBot.Common;
|
||||
|
||||
public interface ITimezoneService
|
||||
{
|
||||
TimeZoneInfo GetTimeZoneOrUtc(ulong? guildId);
|
||||
}
|
302
src/NadekoBot/_common/Services/Impl/BehaviorExecutor.cs
Normal file
302
src/NadekoBot/_common/Services/Impl/BehaviorExecutor.cs
Normal file
@@ -0,0 +1,302 @@
|
||||
#nullable disable
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using NadekoBot.Common.ModuleBehaviors;
|
||||
|
||||
namespace NadekoBot.Services;
|
||||
|
||||
// should be renamed to handler as it's not only executing
|
||||
public sealed class BehaviorHandler : IBehaviorHandler
|
||||
{
|
||||
private readonly IServiceProvider _services;
|
||||
|
||||
private IReadOnlyCollection<IExecNoCommand> noCommandExecs;
|
||||
private IReadOnlyCollection<IExecPreCommand> preCommandExecs;
|
||||
private IReadOnlyCollection<IExecOnMessage> onMessageExecs;
|
||||
private IReadOnlyCollection<IInputTransformer> inputTransformers;
|
||||
|
||||
private readonly SemaphoreSlim _customLock = new(1, 1);
|
||||
private readonly List<ICustomBehavior> _customExecs = new();
|
||||
|
||||
public BehaviorHandler(IServiceProvider services)
|
||||
{
|
||||
_services = services;
|
||||
}
|
||||
|
||||
public void Initialize()
|
||||
{
|
||||
noCommandExecs = _services.GetServices<IExecNoCommand>().ToArray();
|
||||
preCommandExecs = _services.GetServices<IExecPreCommand>().OrderByDescending(x => x.Priority).ToArray();
|
||||
onMessageExecs = _services.GetServices<IExecOnMessage>().OrderByDescending(x => x.Priority).ToArray();
|
||||
inputTransformers = _services.GetServices<IInputTransformer>().ToArray();
|
||||
}
|
||||
|
||||
#region Add/Remove
|
||||
|
||||
public async Task AddRangeAsync(IEnumerable<ICustomBehavior> execs)
|
||||
{
|
||||
await _customLock.WaitAsync();
|
||||
try
|
||||
{
|
||||
foreach (var exe in execs)
|
||||
{
|
||||
if (_customExecs.Contains(exe))
|
||||
continue;
|
||||
|
||||
_customExecs.Add(exe);
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
_customLock.Release();
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<bool> AddAsync(ICustomBehavior behavior)
|
||||
{
|
||||
await _customLock.WaitAsync();
|
||||
try
|
||||
{
|
||||
if (_customExecs.Contains(behavior))
|
||||
return false;
|
||||
|
||||
_customExecs.Add(behavior);
|
||||
return true;
|
||||
}
|
||||
finally
|
||||
{
|
||||
_customLock.Release();
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<bool> RemoveAsync(ICustomBehavior behavior)
|
||||
{
|
||||
await _customLock.WaitAsync();
|
||||
try
|
||||
{
|
||||
return _customExecs.Remove(behavior);
|
||||
}
|
||||
finally
|
||||
{
|
||||
_customLock.Release();
|
||||
}
|
||||
}
|
||||
|
||||
public async Task RemoveRangeAsync(IEnumerable<ICustomBehavior> behs)
|
||||
{
|
||||
await _customLock.WaitAsync();
|
||||
try
|
||||
{
|
||||
foreach(var beh in behs)
|
||||
_customExecs.Remove(beh);
|
||||
}
|
||||
finally
|
||||
{
|
||||
_customLock.Release();
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Running
|
||||
|
||||
public async Task<bool> RunExecOnMessageAsync(SocketGuild guild, IUserMessage usrMsg)
|
||||
{
|
||||
async Task<bool> Exec<T>(IReadOnlyCollection<T> execs)
|
||||
where T : IExecOnMessage
|
||||
{
|
||||
foreach (var exec in execs)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (await exec.ExecOnMessageAsync(guild, usrMsg))
|
||||
{
|
||||
Log.Information("{TypeName} blocked message g:{GuildId} u:{UserId} c:{ChannelId} msg:{Message}",
|
||||
GetExecName(exec),
|
||||
guild?.Id,
|
||||
usrMsg.Author.Id,
|
||||
usrMsg.Channel.Id,
|
||||
usrMsg.Content?.TrimTo(10));
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Error(ex,
|
||||
"An error occurred in {TypeName} late blocker: {ErrorMessage}",
|
||||
GetExecName(exec),
|
||||
ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
if (await Exec(onMessageExecs))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
await _customLock.WaitAsync();
|
||||
try
|
||||
{
|
||||
if (await Exec(_customExecs))
|
||||
return true;
|
||||
}
|
||||
finally
|
||||
{
|
||||
_customLock.Release();
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private string GetExecName(IBehavior exec)
|
||||
=> exec.Name;
|
||||
|
||||
public async Task<bool> RunPreCommandAsync(ICommandContext ctx, CommandInfo cmd)
|
||||
{
|
||||
async Task<bool> Exec<T>(IReadOnlyCollection<T> execs) where T: IExecPreCommand
|
||||
{
|
||||
foreach (var exec in execs)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (await exec.ExecPreCommandAsync(ctx, cmd.Module.GetTopLevelModule().Name, cmd))
|
||||
{
|
||||
Log.Information("{TypeName} Pre-Command blocked [{User}] Command: [{Command}]",
|
||||
GetExecName(exec),
|
||||
ctx.User,
|
||||
cmd.Aliases[0]);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Error(ex,
|
||||
"An error occurred in {TypeName} PreCommand: {ErrorMessage}",
|
||||
GetExecName(exec),
|
||||
ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
if (await Exec(preCommandExecs))
|
||||
return true;
|
||||
|
||||
await _customLock.WaitAsync();
|
||||
try
|
||||
{
|
||||
if (await Exec(_customExecs))
|
||||
return true;
|
||||
}
|
||||
finally
|
||||
{
|
||||
_customLock.Release();
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public async Task RunOnNoCommandAsync(SocketGuild guild, IUserMessage usrMsg)
|
||||
{
|
||||
async Task Exec<T>(IReadOnlyCollection<T> execs) where T : IExecNoCommand
|
||||
{
|
||||
foreach (var exec in execs)
|
||||
{
|
||||
try
|
||||
{
|
||||
await exec.ExecOnNoCommandAsync(guild, usrMsg);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Error(ex,
|
||||
"An error occurred in {TypeName} OnNoCommand: {ErrorMessage}",
|
||||
GetExecName(exec),
|
||||
ex.Message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await Exec(noCommandExecs);
|
||||
|
||||
await _customLock.WaitAsync();
|
||||
try
|
||||
{
|
||||
await Exec(_customExecs);
|
||||
}
|
||||
finally
|
||||
{
|
||||
_customLock.Release();
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<string> RunInputTransformersAsync(SocketGuild guild, IUserMessage usrMsg)
|
||||
{
|
||||
async Task<string> Exec<T>(IReadOnlyCollection<T> execs, string content)
|
||||
where T : IInputTransformer
|
||||
{
|
||||
foreach (var exec in execs)
|
||||
{
|
||||
try
|
||||
{
|
||||
var newContent = await exec.TransformInput(guild, usrMsg.Channel, usrMsg.Author, content);
|
||||
if (newContent is not null)
|
||||
{
|
||||
Log.Information("{ExecName} transformed content {OldContent} -> {NewContent}",
|
||||
GetExecName(exec),
|
||||
content,
|
||||
newContent);
|
||||
return newContent;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Warning(ex, "An error occured during InputTransform handling: {ErrorMessage}", ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
var newContent = await Exec(inputTransformers, usrMsg.Content);
|
||||
if (newContent is not null)
|
||||
return newContent;
|
||||
|
||||
await _customLock.WaitAsync();
|
||||
try
|
||||
{
|
||||
newContent = await Exec(_customExecs, usrMsg.Content);
|
||||
if (newContent is not null)
|
||||
return newContent;
|
||||
}
|
||||
finally
|
||||
{
|
||||
_customLock.Release();
|
||||
}
|
||||
|
||||
return usrMsg.Content;
|
||||
}
|
||||
|
||||
public async ValueTask RunPostCommandAsync(ICommandContext ctx, string moduleName, CommandInfo cmd)
|
||||
{
|
||||
foreach (var exec in _customExecs)
|
||||
{
|
||||
try
|
||||
{
|
||||
await exec.ExecPostCommandAsync(ctx, moduleName, cmd.Name);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Warning(ex,
|
||||
"An error occured during PostCommand handling in {ExecName}: {ErrorMessage}",
|
||||
GetExecName(exec),
|
||||
ex.Message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
143
src/NadekoBot/_common/Services/Impl/BlacklistService.cs
Normal file
143
src/NadekoBot/_common/Services/Impl/BlacklistService.cs
Normal file
@@ -0,0 +1,143 @@
|
||||
#nullable disable
|
||||
using LinqToDB;
|
||||
using LinqToDB.Data;
|
||||
using LinqToDB.EntityFrameworkCore;
|
||||
using NadekoBot.Common.ModuleBehaviors;
|
||||
using NadekoBot.Db;
|
||||
using NadekoBot.Db.Models;
|
||||
using Nadeko.Bot.Db.Models;
|
||||
|
||||
namespace NadekoBot.Modules.Permissions.Services;
|
||||
|
||||
public sealed class BlacklistService : IExecOnMessage
|
||||
{
|
||||
public int Priority
|
||||
=> int.MaxValue;
|
||||
|
||||
private readonly DbService _db;
|
||||
private readonly IPubSub _pubSub;
|
||||
private readonly IBotCredentials _creds;
|
||||
private IReadOnlyList<BlacklistEntry> blacklist;
|
||||
|
||||
private readonly TypedKey<BlacklistEntry[]> _blPubKey = new("blacklist.reload");
|
||||
|
||||
public BlacklistService(DbService db, IPubSub pubSub, IBotCredentials creds)
|
||||
{
|
||||
_db = db;
|
||||
_pubSub = pubSub;
|
||||
_creds = creds;
|
||||
|
||||
Reload(false);
|
||||
_pubSub.Sub(_blPubKey, OnReload);
|
||||
}
|
||||
|
||||
private ValueTask OnReload(BlacklistEntry[] newBlacklist)
|
||||
{
|
||||
blacklist = newBlacklist;
|
||||
return default;
|
||||
}
|
||||
|
||||
public Task<bool> ExecOnMessageAsync(IGuild guild, IUserMessage usrMsg)
|
||||
{
|
||||
foreach (var bl in blacklist)
|
||||
{
|
||||
if (guild is not null && bl.Type == BlacklistType.Server && bl.ItemId == guild.Id)
|
||||
{
|
||||
Log.Information("Blocked input from blacklisted guild: {GuildName} [{GuildId}]", guild.Name, guild.Id);
|
||||
|
||||
return Task.FromResult(true);
|
||||
}
|
||||
|
||||
if (bl.Type == BlacklistType.Channel && bl.ItemId == usrMsg.Channel.Id)
|
||||
{
|
||||
Log.Information("Blocked input from blacklisted channel: {ChannelName} [{ChannelId}]",
|
||||
usrMsg.Channel.Name,
|
||||
usrMsg.Channel.Id);
|
||||
|
||||
return Task.FromResult(true);
|
||||
}
|
||||
|
||||
if (bl.Type == BlacklistType.User && bl.ItemId == usrMsg.Author.Id)
|
||||
{
|
||||
Log.Information("Blocked input from blacklisted user: {UserName} [{UserId}]",
|
||||
usrMsg.Author.ToString(),
|
||||
usrMsg.Author.Id);
|
||||
|
||||
return Task.FromResult(true);
|
||||
}
|
||||
}
|
||||
|
||||
return Task.FromResult(false);
|
||||
}
|
||||
|
||||
public IReadOnlyList<BlacklistEntry> GetBlacklist()
|
||||
=> blacklist;
|
||||
|
||||
public void Reload(bool publish = true)
|
||||
{
|
||||
using var uow = _db.GetDbContext();
|
||||
var toPublish = uow.GetTable<BlacklistEntry>().ToArray();
|
||||
blacklist = toPublish;
|
||||
if (publish)
|
||||
_pubSub.Pub(_blPubKey, toPublish);
|
||||
}
|
||||
|
||||
public async Task Blacklist(BlacklistType type, ulong id)
|
||||
{
|
||||
if (_creds.OwnerIds.Contains(id))
|
||||
return;
|
||||
|
||||
await using var uow = _db.GetDbContext();
|
||||
|
||||
await uow
|
||||
.GetTable<BlacklistEntry>()
|
||||
.InsertAsync(() => new()
|
||||
{
|
||||
ItemId = id,
|
||||
Type = type,
|
||||
});
|
||||
|
||||
if (type == BlacklistType.User)
|
||||
{
|
||||
await uow.GetTable<DiscordUser>()
|
||||
.Where(x => x.UserId == id)
|
||||
.UpdateAsync(_ => new()
|
||||
{
|
||||
CurrencyAmount = 0
|
||||
});
|
||||
}
|
||||
|
||||
Reload();
|
||||
}
|
||||
|
||||
public async Task UnBlacklist(BlacklistType type, ulong id)
|
||||
{
|
||||
await using var uow = _db.GetDbContext();
|
||||
await uow.GetTable<BlacklistEntry>()
|
||||
.Where(bi => bi.ItemId == id && bi.Type == type)
|
||||
.DeleteAsync();
|
||||
|
||||
Reload();
|
||||
}
|
||||
|
||||
public async Task BlacklistUsers(IReadOnlyCollection<ulong> toBlacklist)
|
||||
{
|
||||
await using var uow = _db.GetDbContext();
|
||||
var bc = uow.GetTable<BlacklistEntry>();
|
||||
await bc.BulkCopyAsync(toBlacklist.Select(uid => new BlacklistEntry
|
||||
{
|
||||
ItemId = uid,
|
||||
Type = BlacklistType.User
|
||||
}));
|
||||
|
||||
var blList = toBlacklist.ToList();
|
||||
await uow.GetTable<DiscordUser>()
|
||||
.Where(x => blList.Contains(x.UserId))
|
||||
.UpdateAsync(_ => new()
|
||||
{
|
||||
CurrencyAmount = 0
|
||||
});
|
||||
|
||||
Reload();
|
||||
}
|
||||
}
|
172
src/NadekoBot/_common/Services/Impl/CommandsUtilityService.cs
Normal file
172
src/NadekoBot/_common/Services/Impl/CommandsUtilityService.cs
Normal file
@@ -0,0 +1,172 @@
|
||||
using CommandLine;
|
||||
using Nadeko.Common.Medusa;
|
||||
|
||||
namespace NadekoBot.Common;
|
||||
|
||||
public sealed class CommandsUtilityService : ICommandsUtilityService, INService
|
||||
{
|
||||
private readonly CommandHandler _ch;
|
||||
private readonly IBotStrings _strings;
|
||||
private readonly DiscordPermOverrideService _dpos;
|
||||
private readonly IEmbedBuilderService _eb;
|
||||
private readonly ILocalization _loc;
|
||||
private readonly IMedusaLoaderService _medusae;
|
||||
|
||||
public CommandsUtilityService(
|
||||
CommandHandler ch,
|
||||
IBotStrings strings,
|
||||
DiscordPermOverrideService dpos,
|
||||
IEmbedBuilderService eb,
|
||||
ILocalization loc,
|
||||
IMedusaLoaderService medusae)
|
||||
{
|
||||
_ch = ch;
|
||||
_strings = strings;
|
||||
_dpos = dpos;
|
||||
_eb = eb;
|
||||
_loc = loc;
|
||||
_medusae = medusae;
|
||||
}
|
||||
|
||||
public IEmbedBuilder GetCommandHelp(CommandInfo com, IGuild guild)
|
||||
{
|
||||
var prefix = _ch.GetPrefix(guild);
|
||||
|
||||
var str = $"**`{prefix + com.Aliases.First()}`**";
|
||||
var alias = com.Aliases.Skip(1).FirstOrDefault();
|
||||
if (alias is not null)
|
||||
str += $" **/ `{prefix + alias}`**";
|
||||
|
||||
var culture = _loc.GetCultureInfo(guild);
|
||||
|
||||
var em = _eb.Create()
|
||||
.AddField(str, $"{com.RealSummary(_strings, _medusae, culture, prefix)}", true);
|
||||
|
||||
_dpos.TryGetOverrides(guild?.Id ?? 0, com.Name, out var overrides);
|
||||
var reqs = GetCommandRequirements(com, (GuildPermission?)overrides);
|
||||
if (reqs.Any())
|
||||
em.AddField(GetText(strs.requires, guild), string.Join("\n", reqs));
|
||||
|
||||
Extensions.Extensions.WithOkColor(em.AddField(_strings.GetText(strs.usage),
|
||||
string.Join("\n", com.RealRemarksArr(_strings, _medusae, culture, prefix).Map(arg => Format.Code(arg))))
|
||||
.WithFooter(GetText(strs.module(com.Module.GetTopLevelModule().Name), guild)));
|
||||
|
||||
var opt = GetNadekoOptionType(com.Attributes);
|
||||
if (opt is not null)
|
||||
{
|
||||
var hs = GetCommandOptionHelp(opt);
|
||||
if (!string.IsNullOrWhiteSpace(hs))
|
||||
em.AddField(GetText(strs.options, guild), hs);
|
||||
}
|
||||
|
||||
return em;
|
||||
}
|
||||
|
||||
public static string GetCommandOptionHelp(Type opt)
|
||||
{
|
||||
var strs = GetCommandOptionHelpList(opt);
|
||||
|
||||
return string.Join("\n", strs);
|
||||
}
|
||||
|
||||
public static List<string> GetCommandOptionHelpList(Type opt)
|
||||
{
|
||||
var strs = opt.GetProperties()
|
||||
.Select(x => x.GetCustomAttributes(true).FirstOrDefault(a => a is OptionAttribute))
|
||||
.Where(x => x is not null)
|
||||
.Cast<OptionAttribute>()
|
||||
.Select(x =>
|
||||
{
|
||||
var toReturn = $"`--{x.LongName}`";
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(x.ShortName))
|
||||
toReturn += $" (`-{x.ShortName}`)";
|
||||
|
||||
toReturn += $" {x.HelpText} ";
|
||||
return toReturn;
|
||||
})
|
||||
.ToList();
|
||||
|
||||
return strs;
|
||||
}
|
||||
|
||||
public static Type? GetNadekoOptionType(IEnumerable<Attribute> attributes)
|
||||
=> attributes
|
||||
.Select(a => a.GetType())
|
||||
.Where(a => a.IsGenericType
|
||||
&& a.GetGenericTypeDefinition() == typeof(NadekoOptionsAttribute<>))
|
||||
.Select(a => a.GenericTypeArguments[0])
|
||||
.FirstOrDefault();
|
||||
|
||||
public static string[] GetCommandRequirements(CommandInfo cmd, GuildPerm? overrides = null)
|
||||
{
|
||||
var toReturn = new List<string>();
|
||||
|
||||
if (cmd.Preconditions.Any(x => x is OwnerOnlyAttribute))
|
||||
toReturn.Add("Bot Owner Only");
|
||||
|
||||
if (cmd.Preconditions.Any(x => x is NoPublicBotAttribute)
|
||||
|| cmd.Module
|
||||
.Preconditions
|
||||
.Any(x => x is NoPublicBotAttribute)
|
||||
|| cmd.Module.GetTopLevelModule()
|
||||
.Preconditions
|
||||
.Any(x => x is NoPublicBotAttribute))
|
||||
toReturn.Add("No Public Bot");
|
||||
|
||||
if (cmd.Preconditions
|
||||
.Any(x => x is OnlyPublicBotAttribute)
|
||||
|| cmd.Module
|
||||
.Preconditions
|
||||
.Any(x => x is OnlyPublicBotAttribute)
|
||||
|| cmd.Module.GetTopLevelModule()
|
||||
.Preconditions
|
||||
.Any(x => x is OnlyPublicBotAttribute))
|
||||
toReturn.Add("Only Public Bot");
|
||||
|
||||
var userPermString = cmd.Preconditions
|
||||
.Where(ca => ca is UserPermAttribute)
|
||||
.Cast<UserPermAttribute>()
|
||||
.Select(userPerm =>
|
||||
{
|
||||
if (userPerm.ChannelPermission is { } cPerm)
|
||||
return GetPreconditionString(cPerm);
|
||||
|
||||
if (userPerm.GuildPermission is { } gPerm)
|
||||
return GetPreconditionString(gPerm);
|
||||
|
||||
return string.Empty;
|
||||
})
|
||||
.Where(x => !string.IsNullOrWhiteSpace(x))
|
||||
.Join('\n');
|
||||
|
||||
if (overrides is null)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(userPermString))
|
||||
toReturn.Add(userPermString);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(userPermString))
|
||||
toReturn.Add(Format.Strikethrough(userPermString));
|
||||
|
||||
toReturn.Add(GetPreconditionString(overrides.Value));
|
||||
}
|
||||
|
||||
return toReturn.ToArray();
|
||||
}
|
||||
|
||||
public static string GetPreconditionString(ChannelPerm perm)
|
||||
=> (perm + " Channel Permission").Replace("Guild", "Server");
|
||||
|
||||
public static string GetPreconditionString(GuildPerm perm)
|
||||
=> (perm + " Server Permission").Replace("Guild", "Server");
|
||||
|
||||
public string GetText(LocStr str, IGuild? guild)
|
||||
=> _strings.GetText(str, guild?.Id);
|
||||
}
|
||||
|
||||
public interface ICommandsUtilityService
|
||||
{
|
||||
IEmbedBuilder GetCommandHelp(CommandInfo com, IGuild guild);
|
||||
}
|
@@ -0,0 +1,136 @@
|
||||
#nullable disable
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using NadekoBot.Common.ModuleBehaviors;
|
||||
using Nadeko.Bot.Db.Models;
|
||||
|
||||
namespace NadekoBot.Services;
|
||||
|
||||
public class DiscordPermOverrideService : INService, IExecPreCommand, IDiscordPermOverrideService
|
||||
{
|
||||
public int Priority { get; } = int.MaxValue;
|
||||
private readonly DbService _db;
|
||||
private readonly IServiceProvider _services;
|
||||
|
||||
private readonly ConcurrentDictionary<(ulong, string), DiscordPermOverride> _overrides;
|
||||
|
||||
public DiscordPermOverrideService(DbService db, IServiceProvider services)
|
||||
{
|
||||
_db = db;
|
||||
_services = services;
|
||||
using var uow = _db.GetDbContext();
|
||||
_overrides = uow.Set<DiscordPermOverride>()
|
||||
.AsNoTracking()
|
||||
.AsEnumerable()
|
||||
.ToDictionary(o => (o.GuildId ?? 0, o.Command), o => o)
|
||||
.ToConcurrent();
|
||||
}
|
||||
|
||||
public bool TryGetOverrides(ulong guildId, string commandName, out Nadeko.Bot.Db.GuildPerm? perm)
|
||||
{
|
||||
commandName = commandName.ToLowerInvariant();
|
||||
if (_overrides.TryGetValue((guildId, commandName), out var dpo))
|
||||
{
|
||||
perm = dpo.Perm;
|
||||
return true;
|
||||
}
|
||||
|
||||
perm = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
public Task<PreconditionResult> ExecuteOverrides(
|
||||
ICommandContext ctx,
|
||||
CommandInfo command,
|
||||
GuildPerm perms,
|
||||
IServiceProvider services)
|
||||
{
|
||||
var rupa = new RequireUserPermissionAttribute(perms);
|
||||
return rupa.CheckPermissionsAsync(ctx, command, services);
|
||||
}
|
||||
|
||||
public async Task AddOverride(ulong guildId, string commandName, GuildPerm perm)
|
||||
{
|
||||
commandName = commandName.ToLowerInvariant();
|
||||
await using var uow = _db.GetDbContext();
|
||||
var over = await uow.Set<DiscordPermOverride>()
|
||||
.AsQueryable()
|
||||
.FirstOrDefaultAsync(x => x.GuildId == guildId && commandName == x.Command);
|
||||
|
||||
if (over is null)
|
||||
{
|
||||
uow.Set<DiscordPermOverride>()
|
||||
.Add(over = new()
|
||||
{
|
||||
Command = commandName,
|
||||
Perm = (Nadeko.Bot.Db.GuildPerm)perm,
|
||||
GuildId = guildId
|
||||
});
|
||||
}
|
||||
else
|
||||
over.Perm = (Nadeko.Bot.Db.GuildPerm)perm;
|
||||
|
||||
_overrides[(guildId, commandName)] = over;
|
||||
|
||||
await uow.SaveChangesAsync();
|
||||
}
|
||||
|
||||
public async Task ClearAllOverrides(ulong guildId)
|
||||
{
|
||||
await using var uow = _db.GetDbContext();
|
||||
var overrides = await uow.Set<DiscordPermOverride>()
|
||||
.AsQueryable()
|
||||
.AsNoTracking()
|
||||
.Where(x => x.GuildId == guildId)
|
||||
.ToListAsync();
|
||||
|
||||
uow.RemoveRange(overrides);
|
||||
await uow.SaveChangesAsync();
|
||||
|
||||
foreach (var over in overrides)
|
||||
_overrides.TryRemove((guildId, over.Command), out _);
|
||||
}
|
||||
|
||||
public async Task RemoveOverride(ulong guildId, string commandName)
|
||||
{
|
||||
commandName = commandName.ToLowerInvariant();
|
||||
|
||||
await using var uow = _db.GetDbContext();
|
||||
var over = await uow.Set<DiscordPermOverride>()
|
||||
.AsQueryable()
|
||||
.AsNoTracking()
|
||||
.FirstOrDefaultAsync(x => x.GuildId == guildId && x.Command == commandName);
|
||||
|
||||
if (over is null)
|
||||
return;
|
||||
|
||||
uow.Remove(over);
|
||||
await uow.SaveChangesAsync();
|
||||
|
||||
_overrides.TryRemove((guildId, commandName), out _);
|
||||
}
|
||||
|
||||
public async Task<List<DiscordPermOverride>> GetAllOverrides(ulong guildId)
|
||||
{
|
||||
await using var uow = _db.GetDbContext();
|
||||
return await uow.Set<DiscordPermOverride>()
|
||||
.AsQueryable()
|
||||
.AsNoTracking()
|
||||
.Where(x => x.GuildId == guildId)
|
||||
.ToListAsync();
|
||||
}
|
||||
|
||||
public async Task<bool> ExecPreCommandAsync(ICommandContext context, string moduleName, CommandInfo command)
|
||||
{
|
||||
if (TryGetOverrides(context.Guild?.Id ?? 0, command.Name, out var perm) && perm is not null)
|
||||
{
|
||||
var result =
|
||||
await new RequireUserPermissionAttribute((GuildPermission)perm).CheckPermissionsAsync(context,
|
||||
command,
|
||||
_services);
|
||||
|
||||
return !result.IsSuccess;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
60
src/NadekoBot/_common/Services/Impl/FontProvider.cs
Normal file
60
src/NadekoBot/_common/Services/Impl/FontProvider.cs
Normal file
@@ -0,0 +1,60 @@
|
||||
#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");
|
||||
}
|
||||
}
|
17
src/NadekoBot/_common/Services/Impl/IImageCache.cs
Normal file
17
src/NadekoBot/_common/Services/Impl/IImageCache.cs
Normal file
@@ -0,0 +1,17 @@
|
||||
namespace NadekoBot.Services;
|
||||
|
||||
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);
|
||||
}
|
19
src/NadekoBot/_common/Services/Impl/ImagesConfig.cs
Normal file
19
src/NadekoBot/_common/Services/Impl/ImagesConfig.cs
Normal file
@@ -0,0 +1,19 @@
|
||||
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)
|
||||
{
|
||||
}
|
||||
}
|
11
src/NadekoBot/_common/Services/Impl/RedisImageExtensions.cs
Normal file
11
src/NadekoBot/_common/Services/Impl/RedisImageExtensions.cs
Normal file
@@ -0,0 +1,11 @@
|
||||
#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));
|
||||
}
|
@@ -0,0 +1,58 @@
|
||||
#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;
|
||||
}
|
@@ -0,0 +1,18 @@
|
||||
#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();
|
||||
}
|
212
src/NadekoBot/_common/Services/Impl/StatsService.cs
Normal file
212
src/NadekoBot/_common/Services/Impl/StatsService.cs
Normal file
@@ -0,0 +1,212 @@
|
||||
#nullable disable
|
||||
using Humanizer.Localisation;
|
||||
using NadekoBot.Common.ModuleBehaviors;
|
||||
using System.Diagnostics;
|
||||
|
||||
namespace NadekoBot.Services;
|
||||
|
||||
public sealed class StatsService : IStatsService, IReadyExecutor, INService
|
||||
{
|
||||
public const string BOT_VERSION = "5.0.0-alpha2";
|
||||
|
||||
public string Author
|
||||
=> "Kwoth#2452";
|
||||
|
||||
public double MessagesPerSecond
|
||||
=> MessageCounter / GetUptime().TotalSeconds;
|
||||
|
||||
public long TextChannels
|
||||
=> Interlocked.Read(ref textChannels);
|
||||
|
||||
public long VoiceChannels
|
||||
=> Interlocked.Read(ref voiceChannels);
|
||||
|
||||
public long MessageCounter
|
||||
=> Interlocked.Read(ref messageCounter);
|
||||
|
||||
public long CommandsRan
|
||||
=> Interlocked.Read(ref commandsRan);
|
||||
|
||||
private readonly Process _currentProcess = Process.GetCurrentProcess();
|
||||
private readonly DiscordSocketClient _client;
|
||||
private readonly IBotCredentials _creds;
|
||||
private readonly DateTime _started;
|
||||
|
||||
private long textChannels;
|
||||
private long voiceChannels;
|
||||
private long messageCounter;
|
||||
private long commandsRan;
|
||||
|
||||
private readonly IHttpClientFactory _httpFactory;
|
||||
|
||||
public StatsService(
|
||||
DiscordSocketClient client,
|
||||
CommandHandler cmdHandler,
|
||||
IBotCredentials creds,
|
||||
IHttpClientFactory factory)
|
||||
{
|
||||
_client = client;
|
||||
_creds = creds;
|
||||
_httpFactory = factory;
|
||||
|
||||
_started = DateTime.UtcNow;
|
||||
_client.MessageReceived += _ => Task.FromResult(Interlocked.Increment(ref messageCounter));
|
||||
cmdHandler.CommandExecuted += (_, _) => Task.FromResult(Interlocked.Increment(ref commandsRan));
|
||||
|
||||
_client.ChannelCreated += c =>
|
||||
{
|
||||
_ = Task.Run(() =>
|
||||
{
|
||||
if (c is ITextChannel)
|
||||
Interlocked.Increment(ref textChannels);
|
||||
else if (c is IVoiceChannel)
|
||||
Interlocked.Increment(ref voiceChannels);
|
||||
});
|
||||
|
||||
return Task.CompletedTask;
|
||||
};
|
||||
|
||||
_client.ChannelDestroyed += c =>
|
||||
{
|
||||
_ = Task.Run(() =>
|
||||
{
|
||||
if (c is ITextChannel)
|
||||
Interlocked.Decrement(ref textChannels);
|
||||
else if (c is IVoiceChannel)
|
||||
Interlocked.Decrement(ref voiceChannels);
|
||||
});
|
||||
|
||||
return Task.CompletedTask;
|
||||
};
|
||||
|
||||
_client.GuildAvailable += g =>
|
||||
{
|
||||
_ = 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;
|
||||
};
|
||||
|
||||
_client.JoinedGuild += g =>
|
||||
{
|
||||
_ = 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;
|
||||
};
|
||||
|
||||
_client.GuildUnavailable += g =>
|
||||
{
|
||||
_ = 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;
|
||||
};
|
||||
|
||||
_client.LeftGuild += g =>
|
||||
{
|
||||
_ = 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;
|
||||
};
|
||||
}
|
||||
|
||||
private void InitializeChannelCount()
|
||||
{
|
||||
var guilds = _client.Guilds;
|
||||
textChannels = guilds.Sum(static g => g.Channels.Count(static cx => cx is ITextChannel));
|
||||
voiceChannels = guilds.Sum(static g => g.Channels.Count(static cx => cx is IVoiceChannel));
|
||||
}
|
||||
|
||||
public async Task OnReadyAsync()
|
||||
{
|
||||
InitializeChannelCount();
|
||||
|
||||
using var timer = new PeriodicTimer(TimeSpan.FromHours(1));
|
||||
do
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(_creds.BotListToken))
|
||||
continue;
|
||||
|
||||
try
|
||||
{
|
||||
using var http = _httpFactory.CreateClient();
|
||||
using var content = new FormUrlEncodedContent(new Dictionary<string, string>
|
||||
{
|
||||
{ "shard_count", _creds.TotalShards.ToString() },
|
||||
{ "shard_id", _client.ShardId.ToString() },
|
||||
{ "server_count", _client.Guilds.Count().ToString() }
|
||||
});
|
||||
content.Headers.Clear();
|
||||
content.Headers.Add("Content-Type", "application/x-www-form-urlencoded");
|
||||
http.DefaultRequestHeaders.Add("Authorization", _creds.BotListToken);
|
||||
|
||||
using var res = await http.PostAsync(
|
||||
new Uri($"https://discordbots.org/api/bots/{_client.CurrentUser.Id}/stats"),
|
||||
content);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Error(ex, "Error in botlist post");
|
||||
}
|
||||
} while (await timer.WaitForNextTickAsync());
|
||||
}
|
||||
|
||||
public TimeSpan GetUptime()
|
||||
=> DateTime.UtcNow - _started;
|
||||
|
||||
public string GetUptimeString(string separator = ", ")
|
||||
{
|
||||
var time = GetUptime();
|
||||
return time.Humanize(3, maxUnit: TimeUnit.Day, minUnit: TimeUnit.Minute);
|
||||
}
|
||||
|
||||
public double GetPrivateMemoryMegabytes()
|
||||
{
|
||||
_currentProcess.Refresh();
|
||||
return _currentProcess.PrivateMemorySize64 / 1.Megabytes().Bytes;
|
||||
}
|
||||
|
||||
public GuildInfo GetGuildInfo(string name)
|
||||
=> throw new NotImplementedException();
|
||||
|
||||
public GuildInfo GetGuildInfo(ulong id)
|
||||
{
|
||||
var g = _client.GetGuild(id);
|
||||
|
||||
return new GuildInfo()
|
||||
{
|
||||
Id = g.Id,
|
||||
IconUrl = g.IconUrl,
|
||||
Name = g.Name,
|
||||
Owner = g.Owner.Username,
|
||||
OwnerId = g.OwnerId,
|
||||
CreatedAt = g.CreatedAt.UtcDateTime,
|
||||
VoiceChannels = g.VoiceChannels.Count,
|
||||
TextChannels = g.TextChannels.Count,
|
||||
Features = g.Features.Value.ToString().Split(","),
|
||||
Emojis = g.Emotes.ToArray(),
|
||||
Roles = g.Roles.OrderByDescending(x => x.Position).ToArray(),
|
||||
MemberCount = g.MemberCount,
|
||||
};
|
||||
}
|
||||
}
|
77
src/NadekoBot/_common/Services/Impl/YtdlOperation.cs
Normal file
77
src/NadekoBot/_common/Services/Impl/YtdlOperation.cs
Normal file
@@ -0,0 +1,77 @@
|
||||
#nullable disable
|
||||
using System.ComponentModel;
|
||||
using System.Diagnostics;
|
||||
using System.Text;
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
101
src/NadekoBot/_common/Services/strings/impl/BotStrings.cs
Normal file
101
src/NadekoBot/_common/Services/strings/impl/BotStrings.cs
Normal file
@@ -0,0 +1,101 @@
|
||||
#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();
|
||||
}
|
@@ -0,0 +1,73 @@
|
||||
#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);
|
||||
}
|
||||
}
|
@@ -0,0 +1,38 @@
|
||||
#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