Full system rewrite incoming
This commit is contained in:
parent
0fecee1265
commit
4cde58b3e2
756 changed files with 26 additions and 66237 deletions
|
@ -1,78 +0,0 @@
|
||||||
#nullable disable
|
|
||||||
namespace Ellie;
|
|
||||||
|
|
||||||
public interface IBotCredentials
|
|
||||||
{
|
|
||||||
string Token { get; }
|
|
||||||
string GoogleApiKey { get; }
|
|
||||||
ICollection<ulong> OwnerIds { get; set; }
|
|
||||||
bool UsePrivilegedIntents { get; }
|
|
||||||
string RapidApiKey { get; }
|
|
||||||
|
|
||||||
Creds.DbOptions Db { get; }
|
|
||||||
string OsuApiKey { get; }
|
|
||||||
int TotalShards { get; }
|
|
||||||
Creds.PatreonSettings Patreon { get; }
|
|
||||||
string CleverbotApiKey { get; }
|
|
||||||
string Gpt3ApiKey { get; }
|
|
||||||
RestartConfig RestartCommand { get; }
|
|
||||||
Creds.VotesSettings Votes { get; }
|
|
||||||
string BotListToken { get; }
|
|
||||||
string RedisOptions { get; }
|
|
||||||
string LocationIqApiKey { get; }
|
|
||||||
string TimezoneDbApiKey { get; }
|
|
||||||
string CoinmarketcapApiKey { get; }
|
|
||||||
string TrovoClientId { get; }
|
|
||||||
string CoordinatorUrl { get; set; }
|
|
||||||
string TwitchClientId { get; set; }
|
|
||||||
string TwitchClientSecret { get; set; }
|
|
||||||
GoogleApiConfig Google { get; set; }
|
|
||||||
BotCacheImplemenation BotCache { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
public interface IVotesSettings
|
|
||||||
{
|
|
||||||
string TopggServiceUrl { get; set; }
|
|
||||||
string TopggKey { get; set; }
|
|
||||||
string DiscordsServiceUrl { get; set; }
|
|
||||||
string DiscordsKey { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
public interface IPatreonSettings
|
|
||||||
{
|
|
||||||
public string ClientId { get; set; }
|
|
||||||
public string AccessToken { get; set; }
|
|
||||||
public string RefreshToken { get; set; }
|
|
||||||
public string ClientSecret { get; set; }
|
|
||||||
public string CampaignId { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
public interface IRestartConfig
|
|
||||||
{
|
|
||||||
string Cmd { get; set; }
|
|
||||||
string Args { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
public class RestartConfig : IRestartConfig
|
|
||||||
{
|
|
||||||
public string Cmd { get; set; }
|
|
||||||
public string Args { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
public enum BotCacheImplemenation
|
|
||||||
{
|
|
||||||
Memory,
|
|
||||||
Redis
|
|
||||||
}
|
|
||||||
|
|
||||||
public interface IDbOptions
|
|
||||||
{
|
|
||||||
string Type { get; set; }
|
|
||||||
string ConnectionString { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
public interface IGoogleApiConfig
|
|
||||||
{
|
|
||||||
string SearchId { get; init; }
|
|
||||||
string ImageSearchId { get; init; }
|
|
||||||
}
|
|
|
@ -1,8 +0,0 @@
|
||||||
namespace Ellie;
|
|
||||||
|
|
||||||
public interface IBotCredsProvider
|
|
||||||
{
|
|
||||||
public void Reload();
|
|
||||||
public IBotCredentials GetCreds();
|
|
||||||
public void ModifyCredsFile(Action<IBotCredentials> func);
|
|
||||||
}
|
|
|
@ -1,13 +0,0 @@
|
||||||
#nullable disable
|
|
||||||
using YamlDotNet.Serialization;
|
|
||||||
|
|
||||||
namespace Ellie.Services;
|
|
||||||
|
|
||||||
public sealed class CommandStrings
|
|
||||||
{
|
|
||||||
[YamlMember(Alias = "desc")]
|
|
||||||
public string Desc { get; set; }
|
|
||||||
|
|
||||||
[YamlMember(Alias = "args")]
|
|
||||||
public string[] Args { get; set; }
|
|
||||||
}
|
|
|
@ -1,16 +0,0 @@
|
||||||
#nullable disable
|
|
||||||
using System.Globalization;
|
|
||||||
|
|
||||||
namespace Ellie.Common;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Defines methods to retrieve and reload bot strings
|
|
||||||
/// </summary>
|
|
||||||
public interface IBotStrings
|
|
||||||
{
|
|
||||||
string GetText(string key, ulong? guildId = null, params object[] data);
|
|
||||||
string GetText(string key, CultureInfo locale, params object[] data);
|
|
||||||
void Reload();
|
|
||||||
CommandStrings GetCommandStrings(string commandName, ulong? guildId = null);
|
|
||||||
CommandStrings GetCommandStrings(string commandName, CultureInfo cultureInfo);
|
|
||||||
}
|
|
|
@ -1,17 +0,0 @@
|
||||||
#nullable disable
|
|
||||||
using System.Globalization;
|
|
||||||
|
|
||||||
namespace Ellie.Common;
|
|
||||||
|
|
||||||
public static class BotStringsExtensions
|
|
||||||
{
|
|
||||||
// this one is for pipe fun, see PipeExtensions.cs
|
|
||||||
public static string GetText(this IBotStrings strings, in LocStr str, in ulong guildId)
|
|
||||||
=> strings.GetText(str.Key, guildId, str.Params);
|
|
||||||
|
|
||||||
public static string GetText(this IBotStrings strings, in LocStr str, ulong? guildId = null)
|
|
||||||
=> strings.GetText(str.Key, guildId, str.Params);
|
|
||||||
|
|
||||||
public static string GetText(this IBotStrings strings, in LocStr str, CultureInfo culture)
|
|
||||||
=> strings.GetText(str.Key, culture, str.Params);
|
|
||||||
}
|
|
|
@ -1,28 +0,0 @@
|
||||||
#nullable disable
|
|
||||||
namespace Ellie.Services;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Implemented by classes which provide localized strings in their own ways
|
|
||||||
/// </summary>
|
|
||||||
public interface IBotStringsProvider
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Gets localized string
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="localeName">Language name</param>
|
|
||||||
/// <param name="key">String key</param>
|
|
||||||
/// <returns>Localized string</returns>
|
|
||||||
string GetText(string localeName, string key);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Reloads string cache
|
|
||||||
/// </summary>
|
|
||||||
void Reload();
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets command arg examples and description
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="localeName">Language name</param>
|
|
||||||
/// <param name="commandName">Command name</param>
|
|
||||||
CommandStrings GetCommandStrings(string localeName, string commandName);
|
|
||||||
}
|
|
|
@ -1,17 +0,0 @@
|
||||||
#nullable disable
|
|
||||||
|
|
||||||
namespace Ellie.Services;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Basic interface used for classes implementing strings loading mechanism
|
|
||||||
/// </summary>
|
|
||||||
public interface IStringsSource
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Gets all response strings
|
|
||||||
/// </summary>
|
|
||||||
/// <returns>Dictionary(localename, Dictionary(key, response))</returns>
|
|
||||||
Dictionary<string, Dictionary<string, string>> GetResponseStrings();
|
|
||||||
|
|
||||||
Dictionary<string, Dictionary<string, CommandStrings>> GetCommandStrings();
|
|
||||||
}
|
|
|
@ -1,13 +0,0 @@
|
||||||
namespace Ellie;
|
|
||||||
|
|
||||||
public readonly struct LocStr
|
|
||||||
{
|
|
||||||
public readonly string Key;
|
|
||||||
public readonly object[] Params;
|
|
||||||
|
|
||||||
public LocStr(string key, params object[] data)
|
|
||||||
{
|
|
||||||
Key = key;
|
|
||||||
Params = data;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,12 +0,0 @@
|
||||||
using System.Runtime.CompilerServices;
|
|
||||||
|
|
||||||
namespace Ellie.Common.Attributes;
|
|
||||||
|
|
||||||
[AttributeUsage(AttributeTargets.Method)]
|
|
||||||
public sealed class AliasesAttribute : AliasAttribute
|
|
||||||
{
|
|
||||||
public AliasesAttribute([CallerMemberName] string memberName = "")
|
|
||||||
: base(CommandNameLoadHelper.GetAliasesFor(memberName))
|
|
||||||
{
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,18 +0,0 @@
|
||||||
using System.Runtime.CompilerServices;
|
|
||||||
|
|
||||||
namespace Ellie.Common.Attributes;
|
|
||||||
|
|
||||||
[AttributeUsage(AttributeTargets.Method)]
|
|
||||||
public sealed class CmdAttribute : CommandAttribute
|
|
||||||
{
|
|
||||||
public string MethodName { get; }
|
|
||||||
|
|
||||||
public CmdAttribute([CallerMemberName] string memberName = "")
|
|
||||||
: base(CommandNameLoadHelper.GetCommandNameFor(memberName))
|
|
||||||
{
|
|
||||||
MethodName = memberName.ToLowerInvariant();
|
|
||||||
Aliases = CommandNameLoadHelper.GetAliasesFor(memberName);
|
|
||||||
Remarks = memberName.ToLowerInvariant();
|
|
||||||
Summary = memberName.ToLowerInvariant();
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,11 +0,0 @@
|
||||||
#nullable disable
|
|
||||||
namespace Ellie.Common;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Classed marked with this attribute will not be added to the service provider
|
|
||||||
/// </summary>
|
|
||||||
[AttributeUsage(AttributeTargets.Class)]
|
|
||||||
public class DIIgnoreAttribute : Attribute
|
|
||||||
{
|
|
||||||
|
|
||||||
}
|
|
|
@ -1,7 +0,0 @@
|
||||||
namespace Ellie.Common.Attributes;
|
|
||||||
|
|
||||||
[AttributeUsage(AttributeTargets.Method)]
|
|
||||||
public sealed class EllieOptionsAttribute<TOption> : Attribute
|
|
||||||
where TOption: IEllieCommandOptions
|
|
||||||
{
|
|
||||||
}
|
|
|
@ -1,21 +0,0 @@
|
||||||
#nullable disable
|
|
||||||
using System.Diagnostics.CodeAnalysis;
|
|
||||||
|
|
||||||
namespace Ellie.Common;
|
|
||||||
|
|
||||||
[AttributeUsage(AttributeTargets.Method | AttributeTargets.Class)]
|
|
||||||
[SuppressMessage("Style", "IDE0022:Use expression body for methods")]
|
|
||||||
public sealed class NoPublicBotAttribute : PreconditionAttribute
|
|
||||||
{
|
|
||||||
public override Task<PreconditionResult> CheckPermissionsAsync(
|
|
||||||
ICommandContext context,
|
|
||||||
CommandInfo command,
|
|
||||||
IServiceProvider services)
|
|
||||||
{
|
|
||||||
#if GLOBAL_ELLIE
|
|
||||||
return Task.FromResult(PreconditionResult.FromError("Not available on the public bot. To learn how to selfhost a private bot, click [here](https://docs.elliebot.net)."));
|
|
||||||
#else
|
|
||||||
return Task.FromResult(PreconditionResult.FromSuccess());
|
|
||||||
#endif
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,21 +0,0 @@
|
||||||
#nullable disable
|
|
||||||
using System.Diagnostics.CodeAnalysis;
|
|
||||||
|
|
||||||
namespace Ellie.Common;
|
|
||||||
|
|
||||||
[AttributeUsage(AttributeTargets.Method | AttributeTargets.Class)]
|
|
||||||
[SuppressMessage("Style", "IDE0022:Use expression body for methods")]
|
|
||||||
public sealed class OnlyPublicBotAttribute : PreconditionAttribute
|
|
||||||
{
|
|
||||||
public override Task<PreconditionResult> CheckPermissionsAsync(
|
|
||||||
ICommandContext context,
|
|
||||||
CommandInfo command,
|
|
||||||
IServiceProvider services)
|
|
||||||
{
|
|
||||||
#if GLOBAL_ELLIE || DEBUG
|
|
||||||
return Task.FromResult(PreconditionResult.FromSuccess());
|
|
||||||
#else
|
|
||||||
return Task.FromResult(PreconditionResult.FromError("Only available on the public bot."));
|
|
||||||
#endif
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,19 +0,0 @@
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
|
||||||
|
|
||||||
namespace Ellie.Common.Attributes;
|
|
||||||
|
|
||||||
[AttributeUsage(AttributeTargets.Method | AttributeTargets.Class)]
|
|
||||||
public sealed class OwnerOnlyAttribute : PreconditionAttribute
|
|
||||||
{
|
|
||||||
public override Task<PreconditionResult> CheckPermissionsAsync(
|
|
||||||
ICommandContext context,
|
|
||||||
CommandInfo command,
|
|
||||||
IServiceProvider services)
|
|
||||||
{
|
|
||||||
var creds = services.GetRequiredService<IBotCredsProvider>().GetCreds();
|
|
||||||
|
|
||||||
return Task.FromResult(creds.IsOwner(context.User) || context.Client.CurrentUser.Id == context.User.Id
|
|
||||||
? PreconditionResult.FromSuccess()
|
|
||||||
: PreconditionResult.FromError("Not owner"));
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,38 +0,0 @@
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
|
||||||
|
|
||||||
namespace Ellie.Common.Attributes;
|
|
||||||
|
|
||||||
[AttributeUsage(AttributeTargets.Method)]
|
|
||||||
public sealed class RatelimitAttribute : PreconditionAttribute
|
|
||||||
{
|
|
||||||
public int Seconds { get; }
|
|
||||||
|
|
||||||
public RatelimitAttribute(int seconds)
|
|
||||||
{
|
|
||||||
if (seconds <= 0)
|
|
||||||
throw new ArgumentOutOfRangeException(nameof(seconds));
|
|
||||||
|
|
||||||
Seconds = seconds;
|
|
||||||
}
|
|
||||||
|
|
||||||
public override async Task<PreconditionResult> CheckPermissionsAsync(
|
|
||||||
ICommandContext context,
|
|
||||||
CommandInfo command,
|
|
||||||
IServiceProvider services)
|
|
||||||
{
|
|
||||||
if (Seconds == 0)
|
|
||||||
return PreconditionResult.FromSuccess();
|
|
||||||
|
|
||||||
var cache = services.GetRequiredService<IBotCache>();
|
|
||||||
var rem = await cache.GetRatelimitAsync(
|
|
||||||
new($"precondition:{context.User.Id}:{command.Name}"),
|
|
||||||
Seconds.Seconds());
|
|
||||||
|
|
||||||
if (rem is null)
|
|
||||||
return PreconditionResult.FromSuccess();
|
|
||||||
|
|
||||||
var msgContent = $"You can use this command again in {rem.Value.TotalSeconds:F1}s.";
|
|
||||||
|
|
||||||
return PreconditionResult.FromError(msgContent);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,29 +0,0 @@
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
|
||||||
|
|
||||||
namespace Discord;
|
|
||||||
|
|
||||||
[AttributeUsage(AttributeTargets.Method, AllowMultiple = true)]
|
|
||||||
public class UserPermAttribute : RequireUserPermissionAttribute
|
|
||||||
{
|
|
||||||
public UserPermAttribute(GuildPerm permission)
|
|
||||||
: base(permission)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
public UserPermAttribute(ChannelPerm permission)
|
|
||||||
: base(permission)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
public override Task<PreconditionResult> CheckPermissionsAsync(
|
|
||||||
ICommandContext context,
|
|
||||||
CommandInfo command,
|
|
||||||
IServiceProvider services)
|
|
||||||
{
|
|
||||||
var permService = services.GetRequiredService<IDiscordPermOverrideService>();
|
|
||||||
if (permService.TryGetOverrides(context.Guild?.Id ?? 0, command.Name.ToUpperInvariant(), out _))
|
|
||||||
return Task.FromResult(PreconditionResult.FromSuccess());
|
|
||||||
|
|
||||||
return base.CheckPermissionsAsync(context, command, services);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,30 +0,0 @@
|
||||||
#nullable disable
|
|
||||||
namespace Ellie.Common.TypeReaders;
|
|
||||||
|
|
||||||
public sealed class CommandTypeReader : EllieTypeReader<CommandInfo>
|
|
||||||
{
|
|
||||||
private readonly CommandService _cmds;
|
|
||||||
private readonly ICommandHandler _handler;
|
|
||||||
|
|
||||||
public CommandTypeReader(ICommandHandler handler, CommandService cmds)
|
|
||||||
{
|
|
||||||
_handler = handler;
|
|
||||||
_cmds = cmds;
|
|
||||||
}
|
|
||||||
|
|
||||||
public override ValueTask<TypeReaderResult<CommandInfo>> ReadAsync(ICommandContext ctx, string input)
|
|
||||||
{
|
|
||||||
input = input.ToUpperInvariant();
|
|
||||||
var prefix = _handler.GetPrefix(ctx.Guild);
|
|
||||||
if (!input.StartsWith(prefix.ToUpperInvariant(), StringComparison.InvariantCulture))
|
|
||||||
return new(TypeReaderResult.FromError<CommandInfo>(CommandError.ParseFailed, "No such command found."));
|
|
||||||
|
|
||||||
input = input[prefix.Length..];
|
|
||||||
|
|
||||||
var cmd = _cmds.Commands.FirstOrDefault(c => c.Aliases.Select(a => a.ToUpperInvariant()).Contains(input));
|
|
||||||
if (cmd is null)
|
|
||||||
return new(TypeReaderResult.FromError<CommandInfo>(CommandError.ParseFailed, "No such command found."));
|
|
||||||
|
|
||||||
return new(TypeReaderResult.FromSuccess(cmd));
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,25 +0,0 @@
|
||||||
#nullable disable
|
|
||||||
namespace Ellie.Common;
|
|
||||||
|
|
||||||
public abstract class CleanupModuleBase : EllieModule
|
|
||||||
{
|
|
||||||
protected async Task ConfirmActionInternalAsync(string name, Func<Task> action)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var embed = _eb.Create()
|
|
||||||
.WithTitle(GetText(strs.sql_confirm_exec))
|
|
||||||
.WithDescription(name);
|
|
||||||
|
|
||||||
if (!await PromptUserConfirmAsync(embed))
|
|
||||||
return;
|
|
||||||
|
|
||||||
await action();
|
|
||||||
await ctx.OkAsync();
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
await SendErrorAsync(ex.ToString());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,10 +0,0 @@
|
||||||
#nullable disable
|
|
||||||
using System.Runtime.InteropServices;
|
|
||||||
|
|
||||||
namespace Ellie.Modules.Permissions;
|
|
||||||
|
|
||||||
[StructLayout(LayoutKind.Sequential, Size = 1)]
|
|
||||||
public readonly struct CleverBotResponseStr
|
|
||||||
{
|
|
||||||
public const string CLEVERBOT_RESPONSE = "cleverbot:response";
|
|
||||||
}
|
|
|
@ -1,31 +0,0 @@
|
||||||
using YamlDotNet.Serialization;
|
|
||||||
|
|
||||||
namespace Ellie.Common.Attributes;
|
|
||||||
|
|
||||||
public static class CommandNameLoadHelper
|
|
||||||
{
|
|
||||||
private static readonly IDeserializer _deserializer = new Deserializer();
|
|
||||||
|
|
||||||
private static readonly Lazy<Dictionary<string, string[]>> _lazyCommandAliases
|
|
||||||
= new(() => LoadAliases());
|
|
||||||
|
|
||||||
public static Dictionary<string, string[]> LoadAliases(string aliasesFilePath = "data/aliases.yml")
|
|
||||||
{
|
|
||||||
var text = File.ReadAllText(aliasesFilePath);
|
|
||||||
return _deserializer.Deserialize<Dictionary<string, string[]>>(text);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static string[] GetAliasesFor(string methodName)
|
|
||||||
=> _lazyCommandAliases.Value.TryGetValue(methodName.ToLowerInvariant(), out var aliases) && aliases.Length > 1
|
|
||||||
? aliases.Skip(1).ToArray()
|
|
||||||
: Array.Empty<string>();
|
|
||||||
|
|
||||||
public static string GetCommandNameFor(string methodName)
|
|
||||||
{
|
|
||||||
methodName = methodName.ToLowerInvariant();
|
|
||||||
var toReturn = _lazyCommandAliases.Value.TryGetValue(methodName, out var aliases) && aliases.Length > 0
|
|
||||||
? aliases[0]
|
|
||||||
: methodName;
|
|
||||||
return toReturn;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,203 +0,0 @@
|
||||||
#nullable disable
|
|
||||||
|
|
||||||
using Cloneable;
|
|
||||||
using Ellie.Common.Yml;
|
|
||||||
using SixLabors.ImageSharp.PixelFormats;
|
|
||||||
using System.Globalization;
|
|
||||||
using YamlDotNet.Core;
|
|
||||||
using YamlDotNet.Serialization;
|
|
||||||
|
|
||||||
namespace Ellie.Common.Configs;
|
|
||||||
|
|
||||||
[Cloneable]
|
|
||||||
public sealed partial class BotConfig : ICloneable<BotConfig>
|
|
||||||
{
|
|
||||||
[Comment("""DO NOT CHANGE""")]
|
|
||||||
public int Version { get; set; } = 5;
|
|
||||||
|
|
||||||
[Comment("""
|
|
||||||
Most commands, when executed, have a small colored line
|
|
||||||
next to the response. The color depends whether the command
|
|
||||||
is completed, errored or in progress (pending)
|
|
||||||
Color settings below are for the color of those lines.
|
|
||||||
To get color's hex, you can go here https://htmlcolorcodes.com/
|
|
||||||
and copy the hex code fo your selected color (marked as #)
|
|
||||||
""")]
|
|
||||||
public ColorConfig Color { get; set; }
|
|
||||||
|
|
||||||
[Comment("Default bot language. It has to be in the list of supported languages (.langli)")]
|
|
||||||
public CultureInfo DefaultLocale { get; set; }
|
|
||||||
|
|
||||||
[Comment("""
|
|
||||||
Style in which executed commands will show up in the console.
|
|
||||||
Allowed values: Simple, Normal, None
|
|
||||||
""")]
|
|
||||||
public ConsoleOutputType ConsoleOutputType { get; set; }
|
|
||||||
|
|
||||||
[Comment("""Whether the bot will check for new releases every hour""")]
|
|
||||||
public bool CheckForUpdates { get; set; } = true;
|
|
||||||
|
|
||||||
[Comment("""Do you want any messages sent by users in Bot's DM to be forwarded to the owner(s)?""")]
|
|
||||||
public bool ForwardMessages { get; set; }
|
|
||||||
|
|
||||||
[Comment("""
|
|
||||||
Do you want the message to be forwarded only to the first owner specified in the list of owners (in creds.yml),
|
|
||||||
or all owners? (this might cause the bot to lag if there's a lot of owners specified)
|
|
||||||
""")]
|
|
||||||
public bool ForwardToAllOwners { get; set; }
|
|
||||||
|
|
||||||
[Comment("""
|
|
||||||
Any messages sent by users in Bot's DM to be forwarded to the specified channel.
|
|
||||||
This option will only work when ForwardToAllOwners is set to false
|
|
||||||
""")]
|
|
||||||
public ulong? ForwardToChannel { get; set; }
|
|
||||||
|
|
||||||
[Comment("""
|
|
||||||
When a user DMs the bot with a message which is not a command
|
|
||||||
they will receive this message. Leave empty for no response. The string which will be sent whenever someone DMs the bot.
|
|
||||||
Supports embeds. How it looks: https://puu.sh/B0BLV.png
|
|
||||||
""")]
|
|
||||||
[YamlMember(ScalarStyle = ScalarStyle.Literal)]
|
|
||||||
public string DmHelpText { get; set; }
|
|
||||||
|
|
||||||
[Comment("""
|
|
||||||
Only users who send a DM to the bot containing one of the specified words will get a DmHelpText response.
|
|
||||||
Case insensitive.
|
|
||||||
Leave empty to reply with DmHelpText to every DM.
|
|
||||||
""")]
|
|
||||||
public List<string> DmHelpTextKeywords { get; set; }
|
|
||||||
|
|
||||||
[Comment("""This is the response for the .h command""")]
|
|
||||||
[YamlMember(ScalarStyle = ScalarStyle.Literal)]
|
|
||||||
public string HelpText { get; set; }
|
|
||||||
|
|
||||||
[Comment("""List of modules and commands completely blocked on the bot""")]
|
|
||||||
public BlockedConfig Blocked { get; set; }
|
|
||||||
|
|
||||||
[Comment("""Which string will be used to recognize the commands""")]
|
|
||||||
public string Prefix { get; set; }
|
|
||||||
|
|
||||||
[Comment("""
|
|
||||||
Toggles whether your bot will group greet/bye messages into a single message every 5 seconds.
|
|
||||||
1st user who joins will get greeted immediately
|
|
||||||
If more users join within the next 5 seconds, they will be greeted in groups of 5.
|
|
||||||
This will cause %user.mention% and other placeholders to be replaced with multiple users.
|
|
||||||
Keep in mind this might break some of your embeds - for example if you have %user.avatar% in the thumbnail,
|
|
||||||
it will become invalid, as it will resolve to a list of avatars of grouped users.
|
|
||||||
note: This setting is primarily used if you're afraid of raids, or you're running medium/large bots where some
|
|
||||||
servers might get hundreds of people join at once. This is used to prevent the bot from getting ratelimited,
|
|
||||||
and (slightly) reduce the greet spam in those servers.
|
|
||||||
""")]
|
|
||||||
public bool GroupGreets { get; set; }
|
|
||||||
|
|
||||||
[Comment("""
|
|
||||||
Whether the bot will rotate through all specified statuses.
|
|
||||||
This setting can be changed via .ropl command.
|
|
||||||
See RotatingStatuses submodule in Administration.
|
|
||||||
""")]
|
|
||||||
public bool RotateStatuses { get; set; }
|
|
||||||
|
|
||||||
public BotConfig()
|
|
||||||
{
|
|
||||||
var color = new ColorConfig();
|
|
||||||
Color = color;
|
|
||||||
DefaultLocale = new("en-US");
|
|
||||||
ConsoleOutputType = ConsoleOutputType.Normal;
|
|
||||||
ForwardMessages = false;
|
|
||||||
ForwardToAllOwners = false;
|
|
||||||
DmHelpText = """{"description": "Type `%prefix%h` for help."}""";
|
|
||||||
HelpText = """
|
|
||||||
{
|
|
||||||
"title": "To invite me to your server, use this link",
|
|
||||||
"description": "https://discordapp.com/oauth2/authorize?client_id={0}&scope=bot&permissions=66186303",
|
|
||||||
"color": 53380,
|
|
||||||
"thumbnail": "https://i.imgur.com/nKYyqMK.png",
|
|
||||||
"fields": [
|
|
||||||
{
|
|
||||||
"name": "Useful help commands",
|
|
||||||
"value": "`%bot.prefix%modules` Lists all bot modules.
|
|
||||||
`%prefix%h CommandName` Shows some help about a specific command.
|
|
||||||
`%prefix%commands ModuleName` Lists all commands in a module.",
|
|
||||||
"inline": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "List of all Commands",
|
|
||||||
"value": "https://commands.elliebot.net",
|
|
||||||
"inline": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Ellie Support Server",
|
|
||||||
"value": "https://discord.elliebot.net/ ",
|
|
||||||
"inline": true
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
""";
|
|
||||||
var blocked = new BlockedConfig();
|
|
||||||
Blocked = blocked;
|
|
||||||
Prefix = ".";
|
|
||||||
RotateStatuses = false;
|
|
||||||
GroupGreets = false;
|
|
||||||
DmHelpTextKeywords = new()
|
|
||||||
{
|
|
||||||
"help",
|
|
||||||
"commands",
|
|
||||||
"cmds",
|
|
||||||
"module",
|
|
||||||
"can you do"
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// [Comment(@"Whether the prefix will be a suffix, or prefix.
|
|
||||||
// For example, if your prefix is ! you will run a command called 'cash' by typing either
|
|
||||||
// '!cash @Someone' if your prefixIsSuffix: false or
|
|
||||||
// 'cash @Someone!' if your prefixIsSuffix: true")]
|
|
||||||
// public bool PrefixIsSuffix { get; set; }
|
|
||||||
|
|
||||||
// public string Prefixed(string text) => PrefixIsSuffix
|
|
||||||
// ? text + Prefix
|
|
||||||
// : Prefix + text;
|
|
||||||
|
|
||||||
public string Prefixed(string text)
|
|
||||||
=> Prefix + text;
|
|
||||||
}
|
|
||||||
|
|
||||||
[Cloneable]
|
|
||||||
public sealed partial class BlockedConfig
|
|
||||||
{
|
|
||||||
public HashSet<string> Commands { get; set; }
|
|
||||||
public HashSet<string> Modules { get; set; }
|
|
||||||
|
|
||||||
public BlockedConfig()
|
|
||||||
{
|
|
||||||
Modules = new();
|
|
||||||
Commands = new();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
[Cloneable]
|
|
||||||
public partial class ColorConfig
|
|
||||||
{
|
|
||||||
[Comment("""Color used for embed responses when command successfully executes""")]
|
|
||||||
public Rgba32 Ok { get; set; }
|
|
||||||
|
|
||||||
[Comment("""Color used for embed responses when command has an error""")]
|
|
||||||
public Rgba32 Error { get; set; }
|
|
||||||
|
|
||||||
[Comment("""Color used for embed responses while command is doing work or is in progress""")]
|
|
||||||
public Rgba32 Pending { get; set; }
|
|
||||||
|
|
||||||
public ColorConfig()
|
|
||||||
{
|
|
||||||
Ok = Rgba32.ParseHex("00e584");
|
|
||||||
Error = Rgba32.ParseHex("ee281f");
|
|
||||||
Pending = Rgba32.ParseHex("faa61a");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public enum ConsoleOutputType
|
|
||||||
{
|
|
||||||
Normal = 0,
|
|
||||||
Simple = 1,
|
|
||||||
None = 2
|
|
||||||
}
|
|
|
@ -1,18 +0,0 @@
|
||||||
namespace Ellie.Common.Configs;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Base interface for available config serializers
|
|
||||||
/// </summary>
|
|
||||||
public interface IConfigSeria
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Serialize the object to string
|
|
||||||
/// </summary>
|
|
||||||
public string Serialize<T>(T obj)
|
|
||||||
where T : notnull;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Deserialize string data into an object of the specified type
|
|
||||||
/// </summary>
|
|
||||||
public T Deserialize<T>(string data);
|
|
||||||
}
|
|
|
@ -1,272 +0,0 @@
|
||||||
#nullable disable
|
|
||||||
using Ellie.Common.Yml;
|
|
||||||
|
|
||||||
namespace Ellie.Common;
|
|
||||||
|
|
||||||
public sealed class Creds : IBotCredentials
|
|
||||||
{
|
|
||||||
[Comment("""DO NOT CHANGE""")]
|
|
||||||
public int Version { get; set; }
|
|
||||||
|
|
||||||
[Comment("""Bot token. Do not share with anyone ever -> https://discordapp.com/developers/applications/""")]
|
|
||||||
public string Token { get; set; }
|
|
||||||
|
|
||||||
[Comment("""
|
|
||||||
List of Ids of the users who have bot owner permissions
|
|
||||||
**DO NOT ADD PEOPLE YOU DON'T TRUST**
|
|
||||||
""")]
|
|
||||||
public ICollection<ulong> OwnerIds { get; set; }
|
|
||||||
|
|
||||||
[Comment("Keep this on 'true' unless you're sure your bot shouldn't use privileged intents or you're waiting to be accepted")]
|
|
||||||
public bool UsePrivilegedIntents { get; set; }
|
|
||||||
|
|
||||||
[Comment("""
|
|
||||||
The number of shards that the bot will be running on.
|
|
||||||
Leave at 1 if you don't know what you're doing.
|
|
||||||
|
|
||||||
note: If you are planning to have more than one shard, then you must change botCache to 'redis'.
|
|
||||||
Also, in that case you should be using Ellie.Coordinator to start the bot, and it will correctly override this value.
|
|
||||||
""")]
|
|
||||||
public int TotalShards { get; set; }
|
|
||||||
|
|
||||||
[Comment(
|
|
||||||
"""
|
|
||||||
Login to https://console.cloud.google.com, create a new project, go to APIs & Services -> Library -> YouTube Data API and enable it.
|
|
||||||
Then, go to APIs and Services -> Credentials and click Create credentials -> API key.
|
|
||||||
Used only for Youtube Data Api (at the moment).
|
|
||||||
""")]
|
|
||||||
public string GoogleApiKey { get; set; }
|
|
||||||
|
|
||||||
[Comment(
|
|
||||||
"""
|
|
||||||
Create a new custom search here https://programmablesearchengine.google.com/cse/create/new
|
|
||||||
Enable SafeSearch
|
|
||||||
Remove all Sites to Search
|
|
||||||
Enable Search the entire web
|
|
||||||
Copy the 'Search Engine ID' to the SearchId field
|
|
||||||
|
|
||||||
Do all steps again but enable image search for the ImageSearchId
|
|
||||||
""")]
|
|
||||||
public GoogleApiConfig Google { get; set; }
|
|
||||||
|
|
||||||
[Comment("""Settings for voting system for discordbots. Meant for use on global Ellie.""")]
|
|
||||||
public VotesSettings Votes { get; set; }
|
|
||||||
|
|
||||||
[Comment("""
|
|
||||||
Patreon auto reward system settings.
|
|
||||||
go to https://www.patreon.com/portal -> my clients -> create client
|
|
||||||
""")]
|
|
||||||
public PatreonSettings Patreon { get; set; }
|
|
||||||
|
|
||||||
[Comment("""Api key for sending stats to DiscordBotList.""")]
|
|
||||||
public string BotListToken { get; set; }
|
|
||||||
|
|
||||||
[Comment("""Official cleverbot api key.""")]
|
|
||||||
public string CleverbotApiKey { get; set; }
|
|
||||||
|
|
||||||
[Comment(@"Official GPT-3 api key.")]
|
|
||||||
public string Gpt3ApiKey { get; set; }
|
|
||||||
|
|
||||||
[Comment("""
|
|
||||||
Which cache implementation should bot use.
|
|
||||||
'memory' - Cache will be in memory of the bot's process itself. Only use this on bots with a single shard. When the bot is restarted the cache is reset.
|
|
||||||
'redis' - Uses redis (which needs to be separately downloaded and installed). The cache will persist through bot restarts. You can configure connection string in creds.yml
|
|
||||||
""")]
|
|
||||||
public BotCacheImplemenation BotCache { get; set; }
|
|
||||||
|
|
||||||
[Comment("""
|
|
||||||
Redis connection string. Don't change if you don't know what you're doing.
|
|
||||||
Only used if botCache is set to 'redis'
|
|
||||||
""")]
|
|
||||||
public string RedisOptions { get; set; }
|
|
||||||
|
|
||||||
[Comment("""Database options. Don't change if you don't know what you're doing. Leave null for default values""")]
|
|
||||||
public DbOptions Db { get; set; }
|
|
||||||
|
|
||||||
[Comment("""
|
|
||||||
Address and port of the coordinator endpoint. Leave empty for default.
|
|
||||||
Change only if you've changed the coordinator address or port.
|
|
||||||
""")]
|
|
||||||
public string CoordinatorUrl { get; set; }
|
|
||||||
|
|
||||||
[Comment(
|
|
||||||
"""Api key obtained on https://rapidapi.com (go to MyApps -> Add New App -> Enter Name -> Application key)""")]
|
|
||||||
public string RapidApiKey { get; set; }
|
|
||||||
|
|
||||||
[Comment("""
|
|
||||||
https://locationiq.com api key (register and you will receive the token in the email).
|
|
||||||
Used only for .time command.
|
|
||||||
""")]
|
|
||||||
public string LocationIqApiKey { get; set; }
|
|
||||||
|
|
||||||
[Comment("""
|
|
||||||
https://timezonedb.com api key (register and you will receive the token in the email).
|
|
||||||
Used only for .time command
|
|
||||||
""")]
|
|
||||||
public string TimezoneDbApiKey { get; set; }
|
|
||||||
|
|
||||||
[Comment("""
|
|
||||||
https://pro.coinmarketcap.com/account/ api key. There is a free plan for personal use.
|
|
||||||
Used for cryptocurrency related commands.
|
|
||||||
""")]
|
|
||||||
public string CoinmarketcapApiKey { get; set; }
|
|
||||||
|
|
||||||
// [Comment(@"https://polygon.io/dashboard/api-keys api key. Free plan allows for 5 queries per minute.
|
|
||||||
// Used for stocks related commands.")]
|
|
||||||
// public string PolygonIoApiKey { get; set; }
|
|
||||||
|
|
||||||
[Comment("""Api key used for Osu related commands. Obtain this key at https://osu.ppy.sh/p/api""")]
|
|
||||||
public string OsuApiKey { get; set; }
|
|
||||||
|
|
||||||
[Comment("""
|
|
||||||
Optional Trovo client id.
|
|
||||||
You should use this if Trovo stream notifications stopped working or you're getting ratelimit errors.
|
|
||||||
""")]
|
|
||||||
public string TrovoClientId { get; set; }
|
|
||||||
|
|
||||||
[Comment("""Obtain by creating an application at https://dev.twitch.tv/console/apps""")]
|
|
||||||
public string TwitchClientId { get; set; }
|
|
||||||
|
|
||||||
[Comment("""Obtain by creating an application at https://dev.twitch.tv/console/apps""")]
|
|
||||||
public string TwitchClientSecret { get; set; }
|
|
||||||
|
|
||||||
[Comment("""
|
|
||||||
Command and args which will be used to restart the bot.
|
|
||||||
Only used if bot is executed directly (NOT through the coordinator)
|
|
||||||
placeholders:
|
|
||||||
{0} -> shard id
|
|
||||||
{1} -> total shards
|
|
||||||
Linux default
|
|
||||||
cmd: dotnet
|
|
||||||
args: "Ellie.dll -- {0}"
|
|
||||||
Windows default
|
|
||||||
cmd: Ellie.exe
|
|
||||||
args: "{0}"
|
|
||||||
""")]
|
|
||||||
public RestartConfig RestartCommand { get; set; }
|
|
||||||
|
|
||||||
public Creds()
|
|
||||||
{
|
|
||||||
Version = 7;
|
|
||||||
Token = string.Empty;
|
|
||||||
UsePrivilegedIntents = true;
|
|
||||||
OwnerIds = new List<ulong>();
|
|
||||||
TotalShards = 1;
|
|
||||||
GoogleApiKey = string.Empty;
|
|
||||||
Votes = new VotesSettings(string.Empty, string.Empty, string.Empty, string.Empty);
|
|
||||||
Patreon = new PatreonSettings(string.Empty, string.Empty, string.Empty, string.Empty);
|
|
||||||
BotListToken = string.Empty;
|
|
||||||
CleverbotApiKey = string.Empty;
|
|
||||||
Gpt3ApiKey = string.Empty;
|
|
||||||
BotCache = BotCacheImplemenation.Memory;
|
|
||||||
RedisOptions = "localhost:6379,syncTimeout=30000,responseTimeout=30000,allowAdmin=true,password=";
|
|
||||||
Db = new DbOptions()
|
|
||||||
{
|
|
||||||
Type = "sqlite",
|
|
||||||
ConnectionString = "Data Source=data/Ellie.db"
|
|
||||||
};
|
|
||||||
|
|
||||||
CoordinatorUrl = "http://localhost:3442";
|
|
||||||
|
|
||||||
RestartCommand = new RestartConfig();
|
|
||||||
Google = new GoogleApiConfig();
|
|
||||||
}
|
|
||||||
|
|
||||||
public class DbOptions
|
|
||||||
: IDbOptions
|
|
||||||
{
|
|
||||||
[Comment("""
|
|
||||||
Database type. "sqlite", "mysql" and "postgresql" are supported.
|
|
||||||
Default is "sqlite"
|
|
||||||
""")]
|
|
||||||
public string Type { get; set; }
|
|
||||||
|
|
||||||
[Comment("""
|
|
||||||
Database connection string.
|
|
||||||
You MUST change this if you're not using "sqlite" type.
|
|
||||||
Default is "Data Source=data/Ellie.db"
|
|
||||||
Example for mysql: "Server=localhost;Port=3306;Uid=root;Pwd=my_super_secret_mysql_password;Database=ellie"
|
|
||||||
Example for postgresql: "Server=localhost;Port=5432;User Id=postgres;Password=my_super_secret_postgres_password;Database=ellie;"
|
|
||||||
""")]
|
|
||||||
public string ConnectionString { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
public sealed record PatreonSettings : IPatreonSettings
|
|
||||||
{
|
|
||||||
public string ClientId { get; set; }
|
|
||||||
public string AccessToken { get; set; }
|
|
||||||
public string RefreshToken { get; set; }
|
|
||||||
public string ClientSecret { get; set; }
|
|
||||||
|
|
||||||
[Comment(
|
|
||||||
"""Campaign ID of your patreon page. Go to your patreon page (make sure you're logged in) and type "prompt('Campaign ID', window.patreon.bootstrap.creator.data.id);" in the console. (ctrl + shift + i)""")]
|
|
||||||
public string CampaignId { get; set; }
|
|
||||||
|
|
||||||
public PatreonSettings(
|
|
||||||
string accessToken,
|
|
||||||
string refreshToken,
|
|
||||||
string clientSecret,
|
|
||||||
string campaignId)
|
|
||||||
{
|
|
||||||
AccessToken = accessToken;
|
|
||||||
RefreshToken = refreshToken;
|
|
||||||
ClientSecret = clientSecret;
|
|
||||||
CampaignId = campaignId;
|
|
||||||
}
|
|
||||||
|
|
||||||
public PatreonSettings()
|
|
||||||
{
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public sealed record VotesSettings : IVotesSettings
|
|
||||||
{
|
|
||||||
[Comment("""
|
|
||||||
top.gg votes service url
|
|
||||||
This is the url of your instance of the Ellie.Votes api
|
|
||||||
Example: https://votes.my.cool.bot.com
|
|
||||||
""")]
|
|
||||||
public string TopggServiceUrl { get; set; }
|
|
||||||
|
|
||||||
[Comment("""
|
|
||||||
Authorization header value sent to the TopGG service url with each request
|
|
||||||
This should be equivalent to the TopggKey in your Ellie.Votes api appsettings.json file
|
|
||||||
""")]
|
|
||||||
public string TopggKey { get; set; }
|
|
||||||
|
|
||||||
[Comment("""
|
|
||||||
discords.com votes service url
|
|
||||||
This is the url of your instance of the Ellie.Votes api
|
|
||||||
Example: https://votes.my.cool.bot.com
|
|
||||||
""")]
|
|
||||||
public string DiscordsServiceUrl { get; set; }
|
|
||||||
|
|
||||||
[Comment("""
|
|
||||||
Authorization header value sent to the Discords service url with each request
|
|
||||||
This should be equivalent to the DiscordsKey in your Ellie.Votes api appsettings.json file
|
|
||||||
""")]
|
|
||||||
public string DiscordsKey { get; set; }
|
|
||||||
|
|
||||||
public VotesSettings()
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
public VotesSettings(
|
|
||||||
string topggServiceUrl,
|
|
||||||
string topggKey,
|
|
||||||
string discordsServiceUrl,
|
|
||||||
string discordsKey)
|
|
||||||
{
|
|
||||||
TopggServiceUrl = topggServiceUrl;
|
|
||||||
TopggKey = topggKey;
|
|
||||||
DiscordsServiceUrl = discordsServiceUrl;
|
|
||||||
DiscordsKey = discordsKey;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public class GoogleApiConfig : IGoogleApiConfig
|
|
||||||
{
|
|
||||||
public string SearchId { get; init; }
|
|
||||||
public string ImageSearchId { get; init; }
|
|
||||||
}
|
|
|
@ -1,5 +0,0 @@
|
||||||
namespace Ellie.Services.Currency;
|
|
||||||
|
|
||||||
public enum CurrencyType{
|
|
||||||
Default
|
|
||||||
}
|
|
|
@ -1,10 +0,0 @@
|
||||||
namespace Ellie.Modules.Gambling.Bank;
|
|
||||||
|
|
||||||
public interface IBankService
|
|
||||||
{
|
|
||||||
Task<bool> DepositAsync(ulong userId, long amount);
|
|
||||||
Task<bool> WithdrawAsync(ulong userId, long amount);
|
|
||||||
Task<long> GetBalanceAsync(ulong userId);
|
|
||||||
Task<bool> AwardAsync(ulong userId, long amount);
|
|
||||||
Task<bool> TakeAsync(ulong userId, long amount);
|
|
||||||
}
|
|
|
@ -1,40 +0,0 @@
|
||||||
using Ellie.Services.Currency;
|
|
||||||
|
|
||||||
namespace Ellie.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 Ellie.Services.Currency;
|
|
||||||
|
|
||||||
namespace Ellie.Services;
|
|
||||||
|
|
||||||
public interface ITxTracker
|
|
||||||
{
|
|
||||||
Task TrackAdd(long amount, TxData? txData);
|
|
||||||
Task TrackRemove(long amount, TxData? txData);
|
|
||||||
}
|
|
|
@ -1,40 +0,0 @@
|
||||||
namespace Ellie.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 Ellie.Services.Currency;
|
|
||||||
|
|
||||||
public record class TxData(
|
|
||||||
string Type,
|
|
||||||
string Extra,
|
|
||||||
string? Note = "",
|
|
||||||
ulong? OtherId = null);
|
|
|
@ -1,18 +0,0 @@
|
||||||
#nullable disable
|
|
||||||
using LinqToDB.Common;
|
|
||||||
using LinqToDB.EntityFrameworkCore;
|
|
||||||
using Microsoft.EntityFrameworkCore;
|
|
||||||
using Ellie.Services.Database;
|
|
||||||
|
|
||||||
namespace Ellie.Services;
|
|
||||||
|
|
||||||
public abstract class DbService
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Call this to apply all migrations
|
|
||||||
/// </summary>
|
|
||||||
public abstract Task SetupAsync();
|
|
||||||
|
|
||||||
public abstract DbContext CreateRawDbContext(string dbType, string connString);
|
|
||||||
public abstract DbContext GetDbContext();
|
|
||||||
}
|
|
|
@ -1,141 +0,0 @@
|
||||||
using MessageType = Discord.MessageType;
|
|
||||||
|
|
||||||
namespace Ellie.Modules.Administration;
|
|
||||||
|
|
||||||
public sealed class DoAsUserMessage : IUserMessage
|
|
||||||
{
|
|
||||||
private readonly string _message;
|
|
||||||
private IUserMessage _msg;
|
|
||||||
private readonly IUser _user;
|
|
||||||
|
|
||||||
public DoAsUserMessage(SocketUserMessage msg, IUser user, string message)
|
|
||||||
{
|
|
||||||
_msg = msg;
|
|
||||||
_user = user;
|
|
||||||
_message = message;
|
|
||||||
}
|
|
||||||
|
|
||||||
public ulong Id => _msg.Id;
|
|
||||||
|
|
||||||
public DateTimeOffset CreatedAt => _msg.CreatedAt;
|
|
||||||
|
|
||||||
public Task DeleteAsync(RequestOptions? options = null)
|
|
||||||
{
|
|
||||||
return _msg.DeleteAsync(options);
|
|
||||||
}
|
|
||||||
|
|
||||||
public Task AddReactionAsync(IEmote emote, RequestOptions? options = null)
|
|
||||||
{
|
|
||||||
return _msg.AddReactionAsync(emote, options);
|
|
||||||
}
|
|
||||||
|
|
||||||
public Task RemoveReactionAsync(IEmote emote, IUser user, RequestOptions? options = null)
|
|
||||||
{
|
|
||||||
return _msg.RemoveReactionAsync(emote, user, options);
|
|
||||||
}
|
|
||||||
|
|
||||||
public Task RemoveReactionAsync(IEmote emote, ulong userId, RequestOptions? options = null)
|
|
||||||
{
|
|
||||||
return _msg.RemoveReactionAsync(emote, userId, options);
|
|
||||||
}
|
|
||||||
|
|
||||||
public Task RemoveAllReactionsAsync(RequestOptions? options = null)
|
|
||||||
{
|
|
||||||
return _msg.RemoveAllReactionsAsync(options);
|
|
||||||
}
|
|
||||||
|
|
||||||
public Task RemoveAllReactionsForEmoteAsync(IEmote emote, RequestOptions? options = null)
|
|
||||||
{
|
|
||||||
return _msg.RemoveAllReactionsForEmoteAsync(emote, options);
|
|
||||||
}
|
|
||||||
|
|
||||||
public IAsyncEnumerable<IReadOnlyCollection<IUser>> GetReactionUsersAsync(IEmote emoji, int limit,
|
|
||||||
RequestOptions? options = null)
|
|
||||||
{
|
|
||||||
return _msg.GetReactionUsersAsync(emoji, limit, options);
|
|
||||||
}
|
|
||||||
|
|
||||||
public MessageType Type => _msg.Type;
|
|
||||||
|
|
||||||
public MessageSource Source => _msg.Source;
|
|
||||||
|
|
||||||
public bool IsTTS => _msg.IsTTS;
|
|
||||||
|
|
||||||
public bool IsPinned => _msg.IsPinned;
|
|
||||||
|
|
||||||
public bool IsSuppressed => _msg.IsSuppressed;
|
|
||||||
|
|
||||||
public bool MentionedEveryone => _msg.MentionedEveryone;
|
|
||||||
|
|
||||||
public string Content => _message;
|
|
||||||
|
|
||||||
public string CleanContent => _msg.CleanContent;
|
|
||||||
|
|
||||||
public DateTimeOffset Timestamp => _msg.Timestamp;
|
|
||||||
|
|
||||||
public DateTimeOffset? EditedTimestamp => _msg.EditedTimestamp;
|
|
||||||
|
|
||||||
public IMessageChannel Channel => _msg.Channel;
|
|
||||||
|
|
||||||
public IUser Author => _user;
|
|
||||||
|
|
||||||
public IThreadChannel Thread => _msg.Thread;
|
|
||||||
|
|
||||||
public IReadOnlyCollection<IAttachment> Attachments => _msg.Attachments;
|
|
||||||
|
|
||||||
public IReadOnlyCollection<IEmbed> Embeds => _msg.Embeds;
|
|
||||||
|
|
||||||
public IReadOnlyCollection<ITag> Tags => _msg.Tags;
|
|
||||||
|
|
||||||
public IReadOnlyCollection<ulong> MentionedChannelIds => _msg.MentionedChannelIds;
|
|
||||||
|
|
||||||
public IReadOnlyCollection<ulong> MentionedRoleIds => _msg.MentionedRoleIds;
|
|
||||||
|
|
||||||
public IReadOnlyCollection<ulong> MentionedUserIds => _msg.MentionedUserIds;
|
|
||||||
|
|
||||||
public MessageActivity Activity => _msg.Activity;
|
|
||||||
|
|
||||||
public MessageApplication Application => _msg.Application;
|
|
||||||
|
|
||||||
public MessageReference Reference => _msg.Reference;
|
|
||||||
|
|
||||||
public IReadOnlyDictionary<IEmote, ReactionMetadata> Reactions => _msg.Reactions;
|
|
||||||
|
|
||||||
public IReadOnlyCollection<IMessageComponent> Components => _msg.Components;
|
|
||||||
|
|
||||||
public IReadOnlyCollection<IStickerItem> Stickers => _msg.Stickers;
|
|
||||||
|
|
||||||
public MessageFlags? Flags => _msg.Flags;
|
|
||||||
|
|
||||||
public IMessageInteraction Interaction => _msg.Interaction;
|
|
||||||
public MessageRoleSubscriptionData RoleSubscriptionData => _msg.RoleSubscriptionData;
|
|
||||||
|
|
||||||
public Task ModifyAsync(Action<MessageProperties> func, RequestOptions? options = null)
|
|
||||||
{
|
|
||||||
return _msg.ModifyAsync(func, options);
|
|
||||||
}
|
|
||||||
|
|
||||||
public Task PinAsync(RequestOptions? options = null)
|
|
||||||
{
|
|
||||||
return _msg.PinAsync(options);
|
|
||||||
}
|
|
||||||
|
|
||||||
public Task UnpinAsync(RequestOptions? options = null)
|
|
||||||
{
|
|
||||||
return _msg.UnpinAsync(options);
|
|
||||||
}
|
|
||||||
|
|
||||||
public Task CrosspostAsync(RequestOptions? options = null)
|
|
||||||
{
|
|
||||||
return _msg.CrosspostAsync(options);
|
|
||||||
}
|
|
||||||
|
|
||||||
public string Resolve(TagHandling userHandling = TagHandling.Name, TagHandling channelHandling = TagHandling.Name,
|
|
||||||
TagHandling roleHandling = TagHandling.Name,
|
|
||||||
TagHandling everyoneHandling = TagHandling.Ignore, TagHandling emojiHandling = TagHandling.Name)
|
|
||||||
{
|
|
||||||
return _msg.Resolve(userHandling, channelHandling, roleHandling, everyoneHandling, emojiHandling);
|
|
||||||
}
|
|
||||||
|
|
||||||
public IUserMessage ReferencedMessage => _msg.ReferencedMessage;
|
|
||||||
}
|
|
|
@ -34,7 +34,8 @@
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<AdditionalFiles Include="..\Ellie\data\strings\responses\responses.en-US.json" />
|
<AdditionalFiles Include="..\Ellie\data\strings\responses\responses.en-US.json">
|
||||||
|
<Link>responses.en-US.json</Link>
|
||||||
|
</AdditionalFiles>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|
|
@ -1,143 +0,0 @@
|
||||||
#nullable disable
|
|
||||||
using System.Globalization;
|
|
||||||
|
|
||||||
// ReSharper disable InconsistentNaming
|
|
||||||
|
|
||||||
namespace Ellie.Common;
|
|
||||||
|
|
||||||
[UsedImplicitly(ImplicitUseTargetFlags.Default
|
|
||||||
| ImplicitUseTargetFlags.WithInheritors
|
|
||||||
| ImplicitUseTargetFlags.WithMembers)]
|
|
||||||
public abstract class EllieModule : ModuleBase
|
|
||||||
{
|
|
||||||
protected CultureInfo Culture { get; set; }
|
|
||||||
|
|
||||||
// Injected by Discord.net
|
|
||||||
public IBotStrings Strings { get; set; }
|
|
||||||
public ICommandHandler _cmdHandler { get; set; }
|
|
||||||
public ILocalization _localization { get; set; }
|
|
||||||
public IEmbedBuilderService _eb { get; set; }
|
|
||||||
public IEllieInteractionService _inter { get; set; }
|
|
||||||
|
|
||||||
protected string prefix
|
|
||||||
=> _cmdHandler.GetPrefix(ctx.Guild);
|
|
||||||
|
|
||||||
protected ICommandContext ctx
|
|
||||||
=> Context;
|
|
||||||
|
|
||||||
protected override void BeforeExecute(CommandInfo command)
|
|
||||||
=> Culture = _localization.GetCultureInfo(ctx.Guild?.Id);
|
|
||||||
|
|
||||||
protected string GetText(in LocStr data)
|
|
||||||
=> Strings.GetText(data, Culture);
|
|
||||||
|
|
||||||
public Task<IUserMessage> SendErrorAsync(
|
|
||||||
string title,
|
|
||||||
string error,
|
|
||||||
string url = null,
|
|
||||||
string footer = null,
|
|
||||||
EllieInteraction inter = null)
|
|
||||||
=> ctx.Channel.SendErrorAsync(_eb, title, error, url, footer);
|
|
||||||
|
|
||||||
public Task<IUserMessage> SendConfirmAsync(
|
|
||||||
string title,
|
|
||||||
string text,
|
|
||||||
string url = null,
|
|
||||||
string footer = null)
|
|
||||||
=> ctx.Channel.SendConfirmAsync(_eb, title, text, url, footer);
|
|
||||||
|
|
||||||
//
|
|
||||||
public Task<IUserMessage> SendErrorAsync(string text, EllieInteraction inter = null)
|
|
||||||
=> ctx.Channel.SendAsync(_eb, text, MsgType.Error, inter);
|
|
||||||
|
|
||||||
public Task<IUserMessage> SendConfirmAsync(string text, EllieInteraction inter = null)
|
|
||||||
=> ctx.Channel.SendAsync(_eb, text, MsgType.Ok, inter);
|
|
||||||
|
|
||||||
public Task<IUserMessage> SendPendingAsync(string text, EllieInteraction inter = null)
|
|
||||||
=> ctx.Channel.SendAsync(_eb, text, MsgType.Pending, inter);
|
|
||||||
|
|
||||||
|
|
||||||
// localized normal
|
|
||||||
public Task<IUserMessage> ErrorLocalizedAsync(LocStr str, EllieInteraction inter = null)
|
|
||||||
=> SendErrorAsync(GetText(str), inter);
|
|
||||||
|
|
||||||
public Task<IUserMessage> PendingLocalizedAsync(LocStr str, EllieInteraction inter = null)
|
|
||||||
=> SendPendingAsync(GetText(str), inter);
|
|
||||||
|
|
||||||
public Task<IUserMessage> ConfirmLocalizedAsync(LocStr str, EllieInteraction inter = null)
|
|
||||||
=> SendConfirmAsync(GetText(str), inter);
|
|
||||||
|
|
||||||
// localized replies
|
|
||||||
public Task<IUserMessage> ReplyErrorLocalizedAsync(LocStr str, EllieInteraction inter = null)
|
|
||||||
=> SendErrorAsync($"{Format.Bold(ctx.User.ToString())} {GetText(str)}", inter);
|
|
||||||
|
|
||||||
public Task<IUserMessage> ReplyPendingLocalizedAsync(LocStr str, EllieInteraction inter = null)
|
|
||||||
=> SendPendingAsync($"{Format.Bold(ctx.User.ToString())} {GetText(str)}", inter);
|
|
||||||
|
|
||||||
public Task<IUserMessage> ReplyConfirmLocalizedAsync(LocStr str, EllieInteraction inter = null)
|
|
||||||
=> SendConfirmAsync($"{Format.Bold(ctx.User.ToString())} {GetText(str)}", inter);
|
|
||||||
|
|
||||||
public async Task<bool> PromptUserConfirmAsync(IEmbedBuilder embed)
|
|
||||||
{
|
|
||||||
embed.WithPendingColor().WithFooter("yes/no");
|
|
||||||
|
|
||||||
var msg = await ctx.Channel.EmbedAsync(embed);
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var input = await GetUserInputAsync(ctx.User.Id, ctx.Channel.Id);
|
|
||||||
input = input?.ToUpperInvariant();
|
|
||||||
|
|
||||||
if (input != "YES" && input != "Y")
|
|
||||||
return false;
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
_ = Task.Run(() => msg.DeleteAsync());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TypeConverter typeConverter = TypeDescriptor.GetConverter(propType); ?
|
|
||||||
public async Task<string> GetUserInputAsync(ulong userId, ulong channelId)
|
|
||||||
{
|
|
||||||
var userInputTask = new TaskCompletionSource<string>();
|
|
||||||
var dsc = (DiscordSocketClient)ctx.Client;
|
|
||||||
try
|
|
||||||
{
|
|
||||||
dsc.MessageReceived += MessageReceived;
|
|
||||||
|
|
||||||
if (await Task.WhenAny(userInputTask.Task, Task.Delay(10000)) != userInputTask.Task)
|
|
||||||
return null;
|
|
||||||
|
|
||||||
return await userInputTask.Task;
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
dsc.MessageReceived -= MessageReceived;
|
|
||||||
}
|
|
||||||
|
|
||||||
Task MessageReceived(SocketMessage arg)
|
|
||||||
{
|
|
||||||
_ = Task.Run(() =>
|
|
||||||
{
|
|
||||||
if (arg is not SocketUserMessage userMsg
|
|
||||||
|| userMsg.Channel is not ITextChannel
|
|
||||||
|| userMsg.Author.Id != userId
|
|
||||||
|| userMsg.Channel.Id != channelId)
|
|
||||||
return Task.CompletedTask;
|
|
||||||
|
|
||||||
if (userInputTask.TrySetResult(arg.Content))
|
|
||||||
userMsg.DeleteAfter(1);
|
|
||||||
|
|
||||||
return Task.CompletedTask;
|
|
||||||
});
|
|
||||||
return Task.CompletedTask;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public abstract class EllieModule<TService> : EllieModule
|
|
||||||
{
|
|
||||||
public TService _service { get; set; }
|
|
||||||
}
|
|
|
@ -1,15 +0,0 @@
|
||||||
#nullable disable
|
|
||||||
|
|
||||||
namespace Ellie.Common.TypeReaders;
|
|
||||||
|
|
||||||
[MeansImplicitUse(ImplicitUseTargetFlags.Default | ImplicitUseTargetFlags.WithInheritors)]
|
|
||||||
public abstract class EllieTypeReader<T> : TypeReader
|
|
||||||
{
|
|
||||||
public abstract ValueTask<TypeReaderResult<T>> ReadAsync(ICommandContext ctx, string input);
|
|
||||||
|
|
||||||
public override async Task<Discord.Commands.TypeReaderResult> ReadAsync(
|
|
||||||
ICommandContext ctx,
|
|
||||||
string input,
|
|
||||||
IServiceProvider services)
|
|
||||||
=> await ReadAsync(ctx, input);
|
|
||||||
}
|
|
|
@ -1,13 +0,0 @@
|
||||||
using Microsoft.EntityFrameworkCore;
|
|
||||||
using Ellie.Db;
|
|
||||||
using Ellie.Db.Models;
|
|
||||||
// todo fix these namespaces. It should only be Ellie.Bot.Db
|
|
||||||
using Ellie.Services.Database;
|
|
||||||
|
|
||||||
namespace Ellie.Extensions;
|
|
||||||
|
|
||||||
public static class DbExtensions
|
|
||||||
{
|
|
||||||
public static DiscordUser GetOrCreateUser(this DbContext ctx, IUser original, Func<IQueryable<DiscordUser>, IQueryable<DiscordUser>>? includes = null)
|
|
||||||
=> ctx.GetOrCreateUser(original.Id, original.Username, original.Discriminator, original.AvatarId, includes);
|
|
||||||
}
|
|
|
@ -1,97 +0,0 @@
|
||||||
using SixLabors.ImageSharp;
|
|
||||||
using SixLabors.ImageSharp.Drawing;
|
|
||||||
using SixLabors.ImageSharp.Drawing.Processing;
|
|
||||||
using SixLabors.ImageSharp.Formats;
|
|
||||||
using SixLabors.ImageSharp.Formats.Png;
|
|
||||||
using SixLabors.ImageSharp.PixelFormats;
|
|
||||||
using SixLabors.ImageSharp.Processing;
|
|
||||||
using Color = Discord.Color;
|
|
||||||
|
|
||||||
namespace Ellie.Extensions;
|
|
||||||
|
|
||||||
public static class ImagesharpExtensions
|
|
||||||
{
|
|
||||||
// https://github.com/SixLabors/Samples/blob/master/ImageSharp/AvatarWithRoundedCorner/Program.cs
|
|
||||||
public static IImageProcessingContext ApplyRoundedCorners(this IImageProcessingContext ctx, float cornerRadius)
|
|
||||||
{
|
|
||||||
var size = ctx.GetCurrentSize();
|
|
||||||
var corners = BuildCorners(size.Width, size.Height, cornerRadius);
|
|
||||||
|
|
||||||
ctx.SetGraphicsOptions(new GraphicsOptions
|
|
||||||
{
|
|
||||||
Antialias = true,
|
|
||||||
// enforces that any part of this shape that has color is punched out of the background
|
|
||||||
AlphaCompositionMode = PixelAlphaCompositionMode.DestOut
|
|
||||||
});
|
|
||||||
|
|
||||||
foreach (var c in corners)
|
|
||||||
ctx = ctx.Fill(SixLabors.ImageSharp.Color.Red, c);
|
|
||||||
|
|
||||||
return ctx;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static IPathCollection BuildCorners(int imageWidth, int imageHeight, float cornerRadius)
|
|
||||||
{
|
|
||||||
// first create a square
|
|
||||||
var rect = new RectangularPolygon(-0.5f, -0.5f, cornerRadius, cornerRadius);
|
|
||||||
|
|
||||||
// then cut out of the square a circle so we are left with a corner
|
|
||||||
var cornerTopLeft = rect.Clip(new EllipsePolygon(cornerRadius - 0.5f, cornerRadius - 0.5f, cornerRadius));
|
|
||||||
|
|
||||||
// corner is now a corner shape positions top left
|
|
||||||
//lets make 3 more positioned correctly, we can do that by translating the original around the center of the image
|
|
||||||
|
|
||||||
var rightPos = imageWidth - cornerTopLeft.Bounds.Width + 1;
|
|
||||||
var bottomPos = imageHeight - cornerTopLeft.Bounds.Height + 1;
|
|
||||||
|
|
||||||
// move it across the width of the image - the width of the shape
|
|
||||||
var cornerTopRight = cornerTopLeft.RotateDegree(90).Translate(rightPos, 0);
|
|
||||||
var cornerBottomLeft = cornerTopLeft.RotateDegree(-90).Translate(0, bottomPos);
|
|
||||||
var cornerBottomRight = cornerTopLeft.RotateDegree(180).Translate(rightPos, bottomPos);
|
|
||||||
|
|
||||||
return new PathCollection(cornerTopLeft, cornerBottomLeft, cornerTopRight, cornerBottomRight);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static Color ToDiscordColor(this Rgba32 color)
|
|
||||||
=> new(color.R, color.G, color.B);
|
|
||||||
|
|
||||||
public static MemoryStream ToStream(this Image<Rgba32> img, IImageFormat? format = null)
|
|
||||||
{
|
|
||||||
var imageStream = new MemoryStream();
|
|
||||||
if (format?.Name == "GIF")
|
|
||||||
img.SaveAsGif(imageStream);
|
|
||||||
else
|
|
||||||
{
|
|
||||||
img.SaveAsPng(imageStream,
|
|
||||||
new()
|
|
||||||
{
|
|
||||||
ColorType = PngColorType.RgbWithAlpha,
|
|
||||||
CompressionLevel = PngCompressionLevel.DefaultCompression
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
imageStream.Position = 0;
|
|
||||||
return imageStream;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static async Task<MemoryStream> ToStreamAsync(this Image<Rgba32> img, IImageFormat? format = null)
|
|
||||||
{
|
|
||||||
var imageStream = new MemoryStream();
|
|
||||||
if (format?.Name == "GIF")
|
|
||||||
{
|
|
||||||
await img.SaveAsGifAsync(imageStream);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
await img.SaveAsPngAsync(imageStream,
|
|
||||||
new PngEncoder()
|
|
||||||
{
|
|
||||||
ColorType = PngColorType.RgbWithAlpha,
|
|
||||||
CompressionLevel = PngCompressionLevel.DefaultCompression
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
imageStream.Position = 0;
|
|
||||||
return imageStream;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,31 +0,0 @@
|
||||||
// // global using System.Collections.Concurrent;
|
|
||||||
global using NonBlocking;
|
|
||||||
//
|
|
||||||
// // packages
|
|
||||||
global using Humanizer;
|
|
||||||
//
|
|
||||||
// // ellie
|
|
||||||
global using Ellie;
|
|
||||||
global using Ellie.Services;
|
|
||||||
global using Ellise.Common; // new project
|
|
||||||
global using Ellie.Common; // old + ellie specific things
|
|
||||||
global using Ellie.Common.Attributes;
|
|
||||||
global using Ellie.Extensions;
|
|
||||||
|
|
||||||
// discord
|
|
||||||
global using Discord;
|
|
||||||
global using Discord.Commands;
|
|
||||||
global using Discord.Net;
|
|
||||||
global using Discord.WebSocket;
|
|
||||||
|
|
||||||
// aliases
|
|
||||||
global using GuildPerm = Discord.GuildPermission;
|
|
||||||
global using ChannelPerm = Discord.ChannelPermission;
|
|
||||||
global using BotPermAttribute = Discord.Commands.RequireBotPermissionAttribute;
|
|
||||||
global using LeftoverAttribute = Discord.Commands.RemainderAttribute;
|
|
||||||
|
|
||||||
// non-essential
|
|
||||||
global using JetBrains.Annotations;
|
|
||||||
|
|
||||||
|
|
||||||
global using Serilog;
|
|
|
@ -1,12 +0,0 @@
|
||||||
#nullable disable
|
|
||||||
using Ellie.Services.Database.Models;
|
|
||||||
|
|
||||||
namespace Ellie;
|
|
||||||
|
|
||||||
public interface IBot
|
|
||||||
{
|
|
||||||
IReadOnlyList<ulong> GetCurrentGuildIds();
|
|
||||||
event Func<GuildConfig, Task> JoinedGuild;
|
|
||||||
IReadOnlyCollection<GuildConfig> AllGuildConfigs { get; }
|
|
||||||
bool IsReady { get; }
|
|
||||||
}
|
|
|
@ -1,8 +0,0 @@
|
||||||
#nullable disable
|
|
||||||
namespace Ellie.Common;
|
|
||||||
|
|
||||||
public interface ICloneable<T>
|
|
||||||
where T : new()
|
|
||||||
{
|
|
||||||
public T Clone();
|
|
||||||
}
|
|
|
@ -1,29 +0,0 @@
|
||||||
using System.Globalization;
|
|
||||||
using System.Numerics;
|
|
||||||
|
|
||||||
namespace Ellie.Bot.Common;
|
|
||||||
|
|
||||||
public interface ICurrencyProvider
|
|
||||||
{
|
|
||||||
string GetCurrencySign();
|
|
||||||
}
|
|
||||||
|
|
||||||
public static class CurrencyHelper
|
|
||||||
{
|
|
||||||
public static string N<T>(T cur, IFormatProvider format)
|
|
||||||
where T : INumber<T>
|
|
||||||
=> cur.ToString("C0", format);
|
|
||||||
|
|
||||||
public static string N<T>(T cur, CultureInfo culture, string currencySign)
|
|
||||||
where T : INumber<T>
|
|
||||||
=> N(cur, GetCurrencyFormat(culture, currencySign));
|
|
||||||
|
|
||||||
private static IFormatProvider GetCurrencyFormat(CultureInfo culture, string currencySign)
|
|
||||||
{
|
|
||||||
var flowersCurrencyCulture = (CultureInfo)culture.Clone();
|
|
||||||
flowersCurrencyCulture.NumberFormat.CurrencySymbol = currencySign;
|
|
||||||
flowersCurrencyCulture.NumberFormat.CurrencyNegativePattern = 5;
|
|
||||||
|
|
||||||
return flowersCurrencyCulture;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,7 +0,0 @@
|
||||||
#nullable disable
|
|
||||||
namespace Ellise.Common;
|
|
||||||
|
|
||||||
public interface IDiscordPermOverrideService
|
|
||||||
{
|
|
||||||
bool TryGetOverrides(ulong guildId, string commandName, out Ellie.Bot.Db.GuildPerm? perm);
|
|
||||||
}
|
|
|
@ -1,7 +0,0 @@
|
||||||
#nullable disable
|
|
||||||
namespace Ellie.Common;
|
|
||||||
|
|
||||||
public interface IEllieCommandOptions
|
|
||||||
{
|
|
||||||
void NormalizeOptions();
|
|
||||||
}
|
|
|
@ -1,35 +0,0 @@
|
||||||
using Ellie.Services.Database.Models;
|
|
||||||
|
|
||||||
namespace Ellie.Common;
|
|
||||||
|
|
||||||
public interface ILogCommandService
|
|
||||||
{
|
|
||||||
void AddDeleteIgnore(ulong xId);
|
|
||||||
Task LogServer(ulong guildId, ulong channelId, bool actionValue);
|
|
||||||
bool LogIgnore(ulong guildId, ulong itemId, IgnoredItemType itemType);
|
|
||||||
LogSetting? GetGuildLogSettings(ulong guildId);
|
|
||||||
bool Log(ulong guildId, ulong? channelId, LogType type);
|
|
||||||
}
|
|
||||||
|
|
||||||
public enum LogType
|
|
||||||
{
|
|
||||||
Other,
|
|
||||||
MessageUpdated,
|
|
||||||
MessageDeleted,
|
|
||||||
UserJoined,
|
|
||||||
UserLeft,
|
|
||||||
UserBanned,
|
|
||||||
UserUnbanned,
|
|
||||||
UserUpdated,
|
|
||||||
ChannelCreated,
|
|
||||||
ChannelDestroyed,
|
|
||||||
ChannelUpdated,
|
|
||||||
UserPresence,
|
|
||||||
VoicePresence,
|
|
||||||
VoicePresenceTts,
|
|
||||||
UserMuted,
|
|
||||||
UserWarned,
|
|
||||||
|
|
||||||
ThreadDeleted,
|
|
||||||
ThreadCreated
|
|
||||||
}
|
|
|
@ -1,13 +0,0 @@
|
||||||
using OneOf;
|
|
||||||
using OneOf.Types;
|
|
||||||
|
|
||||||
namespace Ellie.Bot.Common;
|
|
||||||
|
|
||||||
public interface IPermissionChecker
|
|
||||||
{
|
|
||||||
Task<OneOf<Success, Error<LocStr>>> CheckAsync(IGuild guild,
|
|
||||||
IMessageChannel channel,
|
|
||||||
IUser author,
|
|
||||||
string module,
|
|
||||||
string? cmd);
|
|
||||||
}
|
|
|
@ -1,7 +0,0 @@
|
||||||
#nullable disable
|
|
||||||
namespace Ellie.Common;
|
|
||||||
|
|
||||||
public interface IPlaceholderProvider
|
|
||||||
{
|
|
||||||
public IEnumerable<(string Name, Func<string> Func)> GetPlaceholders();
|
|
||||||
}
|
|
|
@ -1,82 +0,0 @@
|
||||||
namespace Ellie;
|
|
||||||
|
|
||||||
public sealed class EllieInteraction
|
|
||||||
{
|
|
||||||
private readonly ulong _authorId;
|
|
||||||
private readonly ButtonBuilder _button;
|
|
||||||
private readonly Func<SocketMessageComponent, Task> _onClick;
|
|
||||||
private readonly bool _onlyAuthor;
|
|
||||||
public DiscordSocketClient Client { get; }
|
|
||||||
|
|
||||||
private readonly TaskCompletionSource<bool> _interactionCompletedSource;
|
|
||||||
|
|
||||||
private IUserMessage message = null!;
|
|
||||||
|
|
||||||
public EllieInteraction(DiscordSocketClient client,
|
|
||||||
ulong authorId,
|
|
||||||
ButtonBuilder button,
|
|
||||||
Func<SocketMessageComponent, Task> onClick,
|
|
||||||
bool onlyAuthor)
|
|
||||||
{
|
|
||||||
_authorId = authorId;
|
|
||||||
_button = button;
|
|
||||||
_onClick = onClick;
|
|
||||||
_onlyAuthor = onlyAuthor;
|
|
||||||
_interactionCompletedSource = new(TaskCreationOptions.RunContinuationsAsynchronously);
|
|
||||||
|
|
||||||
Client = client;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task RunAsync(IUserMessage msg)
|
|
||||||
{
|
|
||||||
message = msg;
|
|
||||||
|
|
||||||
Client.InteractionCreated += OnInteraction;
|
|
||||||
await Task.WhenAny(Task.Delay(15_000), _interactionCompletedSource.Task);
|
|
||||||
Client.InteractionCreated -= OnInteraction;
|
|
||||||
|
|
||||||
await msg.ModifyAsync(m => m.Components = new ComponentBuilder().Build());
|
|
||||||
}
|
|
||||||
|
|
||||||
private Task OnInteraction(SocketInteraction arg)
|
|
||||||
{
|
|
||||||
if (arg is not SocketMessageComponent smc)
|
|
||||||
return Task.CompletedTask;
|
|
||||||
|
|
||||||
if (smc.Message.Id != message.Id)
|
|
||||||
return Task.CompletedTask;
|
|
||||||
|
|
||||||
if (_onlyAuthor && smc.User.Id != _authorId)
|
|
||||||
return Task.CompletedTask;
|
|
||||||
|
|
||||||
if (smc.Data.CustomId != _button.CustomId)
|
|
||||||
return Task.CompletedTask;
|
|
||||||
|
|
||||||
_ = Task.Run(async () =>
|
|
||||||
{
|
|
||||||
await ExecuteOnActionAsync(smc);
|
|
||||||
|
|
||||||
// this should only be a thing on single-response buttons
|
|
||||||
_interactionCompletedSource.TrySetResult(true);
|
|
||||||
|
|
||||||
if (!smc.HasResponded)
|
|
||||||
{
|
|
||||||
await smc.DeferAsync();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return Task.CompletedTask;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public MessageComponent CreateComponent()
|
|
||||||
{
|
|
||||||
var comp = new ComponentBuilder()
|
|
||||||
.WithButton(_button);
|
|
||||||
|
|
||||||
return comp.Build();
|
|
||||||
}
|
|
||||||
|
|
||||||
public Task ExecuteOnActionAsync(SocketMessageComponent smc)
|
|
||||||
=> _onClick(smc);
|
|
||||||
}
|
|
|
@ -1,8 +0,0 @@
|
||||||
namespace Ellie;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Represents essential interacation data
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="Emote">Emote which will show on a button</param>
|
|
||||||
/// <param name="CustomId">Custom interaction id</param>
|
|
||||||
public record EllieInteractionData(IEmote Emote, string CustomId, string? Text = null);
|
|
|
@ -1,20 +0,0 @@
|
||||||
namespace Ellie;
|
|
||||||
|
|
||||||
public class EllieInteractionService : IEllieInteractionService, IEService
|
|
||||||
{
|
|
||||||
private readonly DiscordSocketClient _client;
|
|
||||||
|
|
||||||
public EllieInteractionService(DiscordSocketClient client)
|
|
||||||
{
|
|
||||||
_client = client;
|
|
||||||
}
|
|
||||||
|
|
||||||
public EllieInteraction Create<T>(
|
|
||||||
ulong userId,
|
|
||||||
SimpleInteraction<T> inter)
|
|
||||||
=> new EllieInteraction(_client,
|
|
||||||
userId,
|
|
||||||
inter.Button,
|
|
||||||
inter.TriggerAsync,
|
|
||||||
onlyAuthor: true);
|
|
||||||
}
|
|
|
@ -1,8 +0,0 @@
|
||||||
namespace Ellie;
|
|
||||||
|
|
||||||
public interface IEllieInteractionService
|
|
||||||
{
|
|
||||||
public EllieInteraction Create<T>(
|
|
||||||
ulong userId,
|
|
||||||
SimpleInteraction<T> inter);
|
|
||||||
}
|
|
|
@ -1,20 +0,0 @@
|
||||||
namespace Ellie;
|
|
||||||
|
|
||||||
public class SimpleInteraction<T>
|
|
||||||
{
|
|
||||||
public ButtonBuilder Button { get; }
|
|
||||||
private readonly Func<SocketMessageComponent, T, Task> _onClick;
|
|
||||||
private readonly T? _state;
|
|
||||||
|
|
||||||
public SimpleInteraction(ButtonBuilder button, Func<SocketMessageComponent, T?, Task> onClick, T? state = default)
|
|
||||||
{
|
|
||||||
Button = button;
|
|
||||||
_onClick = onClick;
|
|
||||||
_state = state;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task TriggerAsync(SocketMessageComponent smc)
|
|
||||||
{
|
|
||||||
await _onClick(smc, _state!);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,24 +0,0 @@
|
||||||
using System.Globalization;
|
|
||||||
|
|
||||||
namespace Ellie.Marmalade;
|
|
||||||
|
|
||||||
public interface IMarmaladeLoaderSevice
|
|
||||||
{
|
|
||||||
Task<MarmaladeLoadResult> LoadMarmaladeAsync(string marmaladeName);
|
|
||||||
Task<MarmaladeUnloadResult> UnloadMarmaladeAsync(string marmaladeName);
|
|
||||||
string GetCommandDescription(string marmaladeName, string commandName, CultureInfo culture);
|
|
||||||
string[] GetCommandExampleArgs(string marmaladeName, string commandName, CultureInfo culture);
|
|
||||||
Task ReloadStrings();
|
|
||||||
IReadOnlyCollection<string> GetAllMarmalades();
|
|
||||||
IReadOnlyCollection<MarmaladeStats> GetLoadedMarmalades(CultureInfo? cultureInfo = null);
|
|
||||||
}
|
|
||||||
|
|
||||||
public sealed record MarmaladeStats(string Name,
|
|
||||||
string? Description,
|
|
||||||
IReadOnlyCollection<CanaryStats> Canaries);
|
|
||||||
|
|
||||||
public sealed record CanaryStats(string Name,
|
|
||||||
string? Prefix,
|
|
||||||
IReadOnlyCollection<CanaryCommandStats> Commands);
|
|
||||||
|
|
||||||
public sealed record CanaryCommandStats(string Name);
|
|
|
@ -1,10 +0,0 @@
|
||||||
namespace Ellie.Marmalade;
|
|
||||||
|
|
||||||
public enum MarmaladeLoadResult
|
|
||||||
{
|
|
||||||
Success,
|
|
||||||
NotFound,
|
|
||||||
AlreadyLoaded,
|
|
||||||
Empty,
|
|
||||||
UnknownError,
|
|
||||||
}
|
|
|
@ -1,9 +0,0 @@
|
||||||
namespace Ellie.Marmalade;
|
|
||||||
|
|
||||||
public enum MarmaladeUnloadResult
|
|
||||||
{
|
|
||||||
Success,
|
|
||||||
NotLoaded,
|
|
||||||
PossiblyUnable,
|
|
||||||
NotFound,
|
|
||||||
}
|
|
|
@ -1,8 +0,0 @@
|
||||||
namespace Ellie.Common;
|
|
||||||
|
|
||||||
public enum MsgType
|
|
||||||
{
|
|
||||||
Ok,
|
|
||||||
Pending,
|
|
||||||
Error
|
|
||||||
}
|
|
|
@ -1,6 +0,0 @@
|
||||||
namespace Ellie.Common.ModuleBehaviors;
|
|
||||||
|
|
||||||
public interface IBehavior
|
|
||||||
{
|
|
||||||
public virtual string Name => this.GetType().Name;
|
|
||||||
}
|
|
|
@ -1,19 +0,0 @@
|
||||||
namespace Ellie.Common.ModuleBehaviors;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Executed if no command was found for this message
|
|
||||||
/// </summary>
|
|
||||||
public interface IExecNoCommand : IBehavior
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Executed at the end of the lifecycle if no command was found
|
|
||||||
/// <see cref="IExecOnMessage"/> →
|
|
||||||
/// <see cref="IInputTransformer"/> →
|
|
||||||
/// <see cref="IExecPreCommand"/> →
|
|
||||||
/// [<see cref="IExecPostCommand"/> | *<see cref="IExecNoCommand"/>*]
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="guild"></param>
|
|
||||||
/// <param name="msg"></param>
|
|
||||||
/// <returns>A task representing completion</returns>
|
|
||||||
Task ExecOnNoCommandAsync(IGuild guild, IUserMessage msg);
|
|
||||||
}
|
|
|
@ -1,21 +0,0 @@
|
||||||
namespace Ellie.Common.ModuleBehaviors;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Implemented by modules to handle non-bot messages received
|
|
||||||
/// </summary>
|
|
||||||
public interface IExecOnMessage : IBehavior
|
|
||||||
{
|
|
||||||
int Priority { get; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Ran after a non-bot message was received
|
|
||||||
/// *<see cref="IExecOnMessage"/>* →
|
|
||||||
/// <see cref="IInputTransformer"/> →
|
|
||||||
/// <see cref="IExecPreCommand"/> →
|
|
||||||
/// [<see cref="IExecPostCommand"/> | <see cref="IExecNoCommand"/>]
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="guild">Guild where the message was sent</param>
|
|
||||||
/// <param name="msg">The message that was received</param>
|
|
||||||
/// <returns>Whether further processing of this message should be blocked</returns>
|
|
||||||
Task<bool> ExecOnMessageAsync(IGuild guild, IUserMessage msg);
|
|
||||||
}
|
|
|
@ -1,22 +0,0 @@
|
||||||
namespace Ellie.Common.ModuleBehaviors;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// This interface's method is executed after the command successfully finished execution.
|
|
||||||
/// ***There is no support for this method in Ellie services.***
|
|
||||||
/// It is only meant to be used in medusa system
|
|
||||||
/// </summary>
|
|
||||||
public interface IExecPostCommand : IBehavior
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Executed after a command was successfully executed
|
|
||||||
/// <see cref="IExecOnMessage"/> →
|
|
||||||
/// <see cref="IInputTransformer"/> →
|
|
||||||
/// <see cref="IExecPreCommand"/> →
|
|
||||||
/// [*<see cref="IExecPostCommand"/>* | <see cref="IExecNoCommand"/>]
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="ctx">Command context</param>
|
|
||||||
/// <param name="moduleName">Module name</param>
|
|
||||||
/// <param name="commandName">Command name</param>
|
|
||||||
/// <returns>A task representing completion</returns>
|
|
||||||
ValueTask ExecPostCommandAsync(ICommandContext ctx, string moduleName, string commandName);
|
|
||||||
}
|
|
|
@ -1,25 +0,0 @@
|
||||||
namespace Ellie.Common.ModuleBehaviors;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// This interface's method is executed after a command was found but before it was executed.
|
|
||||||
/// Able to block further processing of a command
|
|
||||||
/// </summary>
|
|
||||||
public interface IExecPreCommand : IBehavior
|
|
||||||
{
|
|
||||||
public int Priority { get; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// <para>
|
|
||||||
/// Ran after a command was found but before execution.
|
|
||||||
/// </para>
|
|
||||||
/// <see cref="IExecOnMessage"/> →
|
|
||||||
/// <see cref="IInputTransformer"/> →
|
|
||||||
/// *<see cref="IExecPreCommand"/>* →
|
|
||||||
/// [<see cref="IExecPostCommand"/> | <see cref="IExecNoCommand"/>]
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="context">Command context</param>
|
|
||||||
/// <param name="moduleName">Name of the module</param>
|
|
||||||
/// <param name="command">Command info</param>
|
|
||||||
/// <returns>Whether further processing of the command is blocked</returns>
|
|
||||||
Task<bool> ExecPreCommandAsync(ICommandContext context, string moduleName, CommandInfo command);
|
|
||||||
}
|
|
|
@ -1,25 +0,0 @@
|
||||||
namespace Ellie.Common.ModuleBehaviors;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Implemented by services which may transform input before a command is searched for
|
|
||||||
/// </summary>
|
|
||||||
public interface IInputTransformer : IBehavior
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Ran after a non-bot message was received
|
|
||||||
/// <see cref="IExecOnMessage"/> ->
|
|
||||||
/// *<see cref="IInputTransformer"/>* ->
|
|
||||||
/// <see cref="IExecPreCommand"/> ->
|
|
||||||
/// [<see cref="IExecPostCommand"/> OR <see cref="IExecNoCommand"/>]
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="guild">Guild</param>
|
|
||||||
/// <param name="channel">Channel in which the message was sent</param>
|
|
||||||
/// <param name="user">User who sent the message</param>
|
|
||||||
/// <param name="input">Content of the message</param>
|
|
||||||
/// <returns>New input, if any, otherwise null</returns>
|
|
||||||
Task<string?> TransformInput(
|
|
||||||
IGuild guild,
|
|
||||||
IMessageChannel channel,
|
|
||||||
IUser user,
|
|
||||||
string input);
|
|
||||||
}
|
|
|
@ -1,13 +0,0 @@
|
||||||
namespace Ellie.Common.ModuleBehaviors;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// All services which need to execute something after
|
|
||||||
/// the bot is ready should implement this interface
|
|
||||||
/// </summary>
|
|
||||||
public interface IReadyExecutor : IBehavior
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Executed when bot is ready
|
|
||||||
/// </summary>
|
|
||||||
public Task OnReadyAsync();
|
|
||||||
}
|
|
|
@ -1,7 +0,0 @@
|
||||||
namespace Ellie.Modules.Patronage;
|
|
||||||
|
|
||||||
public readonly struct FeatureLimitKey
|
|
||||||
{
|
|
||||||
public string PrettyName { get; init; }
|
|
||||||
public string Key { get; init; }
|
|
||||||
}
|
|
|
@ -1,8 +0,0 @@
|
||||||
namespace Ellie.Modules.Patronage;
|
|
||||||
|
|
||||||
public readonly struct FeatureQuotaStats
|
|
||||||
{
|
|
||||||
public (uint Cur, uint Max) Hourly { get; init; }
|
|
||||||
public (uint Cur, uint Max) Daily { get; init; }
|
|
||||||
public (uint Cur, uint Max) Monthly { get; init; }
|
|
||||||
}
|
|
|
@ -1,11 +0,0 @@
|
||||||
namespace Ellie.Modules.Patronage;
|
|
||||||
|
|
||||||
public interface ISubscriberData
|
|
||||||
{
|
|
||||||
public string UniquePlatformUserId { get; }
|
|
||||||
public ulong UserId { get; }
|
|
||||||
public int Cents { get; }
|
|
||||||
|
|
||||||
public DateTime? LastCharge { get; }
|
|
||||||
public SubscriptionChargeStatus ChargeStatus { get; }
|
|
||||||
}
|
|
|
@ -1,56 +0,0 @@
|
||||||
using Ellie.Db.Models;
|
|
||||||
using OneOf;
|
|
||||||
|
|
||||||
namespace Ellie.Modules.Patronage;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Manages patrons and provides access to their data
|
|
||||||
/// </summary>
|
|
||||||
public interface IPatronageService
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Called when the payment is made.
|
|
||||||
/// Either as a single payment for that patron,
|
|
||||||
/// or as a recurring monthly donation.
|
|
||||||
/// </summary>
|
|
||||||
public event Func<Patron, Task> OnNewPatronPayment;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Called when the patron changes the pledge amount
|
|
||||||
/// (Patron old, Patron new) => Task
|
|
||||||
/// </summary>
|
|
||||||
public event Func<Patron, Patron, Task> OnPatronUpdated;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Called when the patron refunds the purchase or it's marked as fraud
|
|
||||||
/// </summary>
|
|
||||||
public event Func<Patron, Task> OnPatronRefunded;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets a Patron with the specified userId
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="userId">UserId for which to get the patron data for.</param>
|
|
||||||
/// <returns>A patron with the specifeid userId</returns>
|
|
||||||
public Task<Patron> GetPatronAsync(ulong userId);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the quota statistic for the user/patron specified by the userId
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="userId">UserId of the user for which to get the quota statistic for</param>
|
|
||||||
/// <returns>Quota stats for the specified user</returns>
|
|
||||||
Task<UserQuotaStats> GetUserQuotaStatistic(ulong userId);
|
|
||||||
|
|
||||||
|
|
||||||
Task<FeatureLimit> TryGetFeatureLimitAsync(FeatureLimitKey key, ulong userId, int? defaultValue);
|
|
||||||
|
|
||||||
ValueTask<OneOf<(uint Hourly, uint Daily, uint Monthly), QuotaLimit>> TryIncrementQuotaCounterAsync(
|
|
||||||
ulong userId,
|
|
||||||
bool isSelf,
|
|
||||||
FeatureType featureType,
|
|
||||||
string featureName,
|
|
||||||
uint? maybeHourly,
|
|
||||||
uint? maybeDaily,
|
|
||||||
uint? maybeMonthly);
|
|
||||||
|
|
||||||
PatronConfigData GetConfig();
|
|
||||||
}
|
|
|
@ -1,16 +0,0 @@
|
||||||
#nullable disable
|
|
||||||
namespace Ellie.Modules.Patronage;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Services implementing this interface are handling pledges/subscriptions/payments coming
|
|
||||||
/// from a payment platform.
|
|
||||||
/// </summary>
|
|
||||||
public interface ISubscriptionHandler
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Get Current patrons in batches.
|
|
||||||
/// This will only return patrons who have their discord account connected
|
|
||||||
/// </summary>
|
|
||||||
/// <returns>Batched patrons</returns>
|
|
||||||
public IAsyncEnumerable<IReadOnlyCollection<ISubscriberData>> GetPatronsAsync();
|
|
||||||
}
|
|
|
@ -1,38 +0,0 @@
|
||||||
namespace Ellie.Modules.Patronage;
|
|
||||||
|
|
||||||
public readonly struct Patron
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Unique id assigned to this patron by the payment platform
|
|
||||||
/// </summary>
|
|
||||||
public string UniquePlatformUserId { get; init; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Discord UserId to which this <see cref="UniquePlatformUserId"/> is connected to
|
|
||||||
/// </summary>
|
|
||||||
public ulong UserId { get; init; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Amount the Patron is currently pledging or paid
|
|
||||||
/// </summary>
|
|
||||||
public int Amount { get; init; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Current Tier of the patron
|
|
||||||
/// (do not question it in consumer classes, as the calculation should be always internal and may change)
|
|
||||||
/// </summary>
|
|
||||||
public PatronTier Tier { get; init; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// When was the last time this <see cref="Amount"/> was paid
|
|
||||||
/// </summary>
|
|
||||||
public DateTime PaidAt { get; init; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// After which date does the user's Patronage benefit end
|
|
||||||
/// </summary>
|
|
||||||
public DateTime ValidThru { get; init; }
|
|
||||||
|
|
||||||
public bool IsActive
|
|
||||||
=> !ValidThru.IsBeforeToday();
|
|
||||||
}
|
|
|
@ -1,37 +0,0 @@
|
||||||
using Ellie.Common.Yml;
|
|
||||||
using Cloneable;
|
|
||||||
|
|
||||||
namespace Ellie.Modules.Patronage;
|
|
||||||
|
|
||||||
[Cloneable]
|
|
||||||
public partial class PatronConfigData : ICloneable<PatronConfigData>
|
|
||||||
{
|
|
||||||
[Comment("DO NOT CHANGE")]
|
|
||||||
public int Version { get; set; } = 2;
|
|
||||||
|
|
||||||
[Comment("Whether the patronage feature is enabled")]
|
|
||||||
public bool IsEnabled { get; set; }
|
|
||||||
|
|
||||||
[Comment("List of patron only features and relevant quota data")]
|
|
||||||
public FeatureQuotas Quotas { get; set; }
|
|
||||||
|
|
||||||
public PatronConfigData()
|
|
||||||
{
|
|
||||||
Quotas = new();
|
|
||||||
}
|
|
||||||
|
|
||||||
public class FeatureQuotas
|
|
||||||
{
|
|
||||||
[Comment("Dictionary of feature names with their respective limits. Set to null for unlimited")]
|
|
||||||
public Dictionary<string, Dictionary<PatronTier, int?>> Features { get; set; } = new();
|
|
||||||
|
|
||||||
[Comment("Dictionary of commands with their respective quota data")]
|
|
||||||
public Dictionary<string, Dictionary<PatronTier, Dictionary<QuotaPer, uint>?>> Commands { get; set; } = new();
|
|
||||||
|
|
||||||
[Comment("Dictionary of groups with their respective quota data")]
|
|
||||||
public Dictionary<string, Dictionary<PatronTier, Dictionary<QuotaPer, uint>?>> Groups { get; set; } = new();
|
|
||||||
|
|
||||||
[Comment("Dictionary of modules with their respective quota data")]
|
|
||||||
public Dictionary<string, Dictionary<PatronTier, Dictionary<QuotaPer, uint>?>> Modules { get; set; } = new();
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,33 +0,0 @@
|
||||||
namespace Ellie.Modules.Patronage;
|
|
||||||
|
|
||||||
public static class PatronExtensions
|
|
||||||
{
|
|
||||||
public static string ToFullName(this PatronTier tier)
|
|
||||||
=> tier switch
|
|
||||||
{
|
|
||||||
_ => $"Patron Tier {tier}",
|
|
||||||
};
|
|
||||||
|
|
||||||
public static string ToFullName(this QuotaPer per)
|
|
||||||
=> per.Humanize(LetterCasing.LowerCase);
|
|
||||||
|
|
||||||
public static DateTime DayOfNextMonth(this DateTime date, int day)
|
|
||||||
{
|
|
||||||
var nextMonth = date.AddMonths(1);
|
|
||||||
var dt = DateTime.SpecifyKind(new(nextMonth.Year, nextMonth.Month, day), DateTimeKind.Utc);
|
|
||||||
return dt;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static DateTime FirstOfNextMonth(this DateTime date)
|
|
||||||
=> date.DayOfNextMonth(1);
|
|
||||||
|
|
||||||
public static DateTime SecondOfNextMonth(this DateTime date)
|
|
||||||
=> date.DayOfNextMonth(2);
|
|
||||||
|
|
||||||
public static string ToShortAndRelativeTimestampTag(this DateTime date)
|
|
||||||
{
|
|
||||||
var fullResetStr = TimestampTag.FromDateTime(date, TimestampTagStyles.ShortDateTime);
|
|
||||||
var relativeResetStr = TimestampTag.FromDateTime(date, TimestampTagStyles.Relative);
|
|
||||||
return $"{fullResetStr}\n{relativeResetStr}";
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,14 +0,0 @@
|
||||||
// ReSharper disable InconsistentNaming
|
|
||||||
namespace Ellie.Modules.Patronage;
|
|
||||||
|
|
||||||
public enum PatronTier
|
|
||||||
{
|
|
||||||
None,
|
|
||||||
I,
|
|
||||||
V,
|
|
||||||
X,
|
|
||||||
XX,
|
|
||||||
L,
|
|
||||||
C,
|
|
||||||
ComingSoon
|
|
||||||
}
|
|
|
@ -1,66 +0,0 @@
|
||||||
using Ellie.Db.Models;
|
|
||||||
|
|
||||||
namespace Ellie.Modules.Patronage;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Represents information about why the user has triggered a quota limit
|
|
||||||
/// </summary>
|
|
||||||
public readonly struct QuotaLimit
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Amount of usages reached, which is the limit
|
|
||||||
/// </summary>
|
|
||||||
public uint Quota { get; init; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Which period is this quota limit for (hourly, daily, monthly, etc...)
|
|
||||||
/// </summary>
|
|
||||||
public QuotaPer QuotaPeriod { get; init; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// When does this quota limit reset
|
|
||||||
/// </summary>
|
|
||||||
public DateTime ResetsAt { get; init; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Type of the feature this quota limit is for
|
|
||||||
/// </summary>
|
|
||||||
public FeatureType FeatureType { get; init; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Name of the feature this quota limit is for
|
|
||||||
/// </summary>
|
|
||||||
public string Feature { get; init; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Whether it is the user's own quota (true), or server owners (false)
|
|
||||||
/// </summary>
|
|
||||||
public bool IsOwnQuota { get; init; }
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Respresent information about the feature limit
|
|
||||||
/// </summary>
|
|
||||||
public readonly struct FeatureLimit
|
|
||||||
{
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Whether this limit comes from the patronage system
|
|
||||||
/// </summary>
|
|
||||||
public bool IsPatronLimit { get; init; } = false;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Maximum limit allowed
|
|
||||||
/// </summary>
|
|
||||||
public int? Quota { get; init; } = null;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Name of the limit
|
|
||||||
/// </summary>
|
|
||||||
public string Name { get; init; } = string.Empty;
|
|
||||||
|
|
||||||
public FeatureLimit()
|
|
||||||
{
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,8 +0,0 @@
|
||||||
namespace Ellie.Modules.Patronage;
|
|
||||||
|
|
||||||
public enum QuotaPer
|
|
||||||
{
|
|
||||||
PerHour,
|
|
||||||
PerDay,
|
|
||||||
PerMonth,
|
|
||||||
}
|
|
|
@ -1,10 +0,0 @@
|
||||||
#nullable disable
|
|
||||||
namespace Ellie.Modules.Patronage;
|
|
||||||
|
|
||||||
public enum SubscriptionChargeStatus
|
|
||||||
{
|
|
||||||
Paid,
|
|
||||||
Refunded,
|
|
||||||
Unpaid,
|
|
||||||
Other,
|
|
||||||
}
|
|
|
@ -1,25 +0,0 @@
|
||||||
namespace Ellie.Modules.Patronage;
|
|
||||||
|
|
||||||
public readonly struct UserQuotaStats
|
|
||||||
{
|
|
||||||
private static readonly IReadOnlyDictionary<string, FeatureQuotaStats> _emptyDictionary
|
|
||||||
= new Dictionary<string, FeatureQuotaStats>();
|
|
||||||
public PatronTier Tier { get; init; }
|
|
||||||
= PatronTier.None;
|
|
||||||
|
|
||||||
public IReadOnlyDictionary<string, FeatureQuotaStats> Features { get; init; }
|
|
||||||
= _emptyDictionary;
|
|
||||||
|
|
||||||
public IReadOnlyDictionary<string, FeatureQuotaStats> Commands { get; init; }
|
|
||||||
= _emptyDictionary;
|
|
||||||
|
|
||||||
public IReadOnlyDictionary<string, FeatureQuotaStats> Groups { get; init; }
|
|
||||||
= _emptyDictionary;
|
|
||||||
|
|
||||||
public IReadOnlyDictionary<string, FeatureQuotaStats> Modules { get; init; }
|
|
||||||
= _emptyDictionary;
|
|
||||||
|
|
||||||
public UserQuotaStats()
|
|
||||||
{
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,164 +0,0 @@
|
||||||
#nullable disable
|
|
||||||
using System.Text.RegularExpressions;
|
|
||||||
|
|
||||||
namespace Ellie.Common;
|
|
||||||
|
|
||||||
public class ReplacementBuilder
|
|
||||||
{
|
|
||||||
private static readonly Regex _rngRegex = new("%rng(?:(?<from>(?:-)?\\d+)-(?<to>(?:-)?\\d+))?%",
|
|
||||||
RegexOptions.Compiled);
|
|
||||||
|
|
||||||
private readonly ConcurrentDictionary<Regex, Func<Match, string>> _regex = new();
|
|
||||||
|
|
||||||
private readonly ConcurrentDictionary<string, Func<string>> _reps = new();
|
|
||||||
|
|
||||||
public ReplacementBuilder()
|
|
||||||
=> WithRngRegex();
|
|
||||||
|
|
||||||
public ReplacementBuilder WithDefault(
|
|
||||||
IUser usr,
|
|
||||||
IMessageChannel ch,
|
|
||||||
SocketGuild g,
|
|
||||||
DiscordSocketClient client)
|
|
||||||
=> WithUser(usr).WithChannel(ch).WithServer(client, g).WithClient(client);
|
|
||||||
|
|
||||||
public ReplacementBuilder WithDefault(ICommandContext ctx)
|
|
||||||
=> WithDefault(ctx.User, ctx.Channel, ctx.Guild as SocketGuild, (DiscordSocketClient)ctx.Client);
|
|
||||||
|
|
||||||
public ReplacementBuilder WithMention(DiscordSocketClient client)
|
|
||||||
{
|
|
||||||
_reps.TryAdd("%bot.mention%", () => client.CurrentUser.Mention);
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public ReplacementBuilder WithClient(DiscordSocketClient client)
|
|
||||||
{
|
|
||||||
WithMention(client);
|
|
||||||
|
|
||||||
_reps.TryAdd("%bot.status%", () => client.Status.ToString());
|
|
||||||
_reps.TryAdd("%bot.latency%", () => client.Latency.ToString());
|
|
||||||
_reps.TryAdd("%bot.name%", () => client.CurrentUser.Username);
|
|
||||||
_reps.TryAdd("%bot.fullname%", () => client.CurrentUser.ToString());
|
|
||||||
_reps.TryAdd("%bot.time%",
|
|
||||||
() => DateTime.Now.ToString("HH:mm " + TimeZoneInfo.Local.StandardName.GetInitials()));
|
|
||||||
_reps.TryAdd("%bot.discrim%", () => client.CurrentUser.Discriminator);
|
|
||||||
_reps.TryAdd("%bot.id%", () => client.CurrentUser.Id.ToString());
|
|
||||||
_reps.TryAdd("%bot.avatar%", () => client.CurrentUser.RealAvatarUrl().ToString());
|
|
||||||
|
|
||||||
WithStats(client);
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public ReplacementBuilder WithServer(DiscordSocketClient client, SocketGuild g)
|
|
||||||
{
|
|
||||||
_reps.TryAdd("%server%", () => g is null ? "DM" : g.Name);
|
|
||||||
_reps.TryAdd("%server.id%", () => g is null ? "DM" : g.Id.ToString());
|
|
||||||
_reps.TryAdd("%server.name%", () => g is null ? "DM" : g.Name);
|
|
||||||
_reps.TryAdd("%server.icon%", () => g is null ? null : g.IconUrl);
|
|
||||||
_reps.TryAdd("%server.members%", () => g is { } sg ? sg.MemberCount.ToString() : "?");
|
|
||||||
_reps.TryAdd("%server.boosters%", () => g.PremiumSubscriptionCount.ToString());
|
|
||||||
_reps.TryAdd("%server.boost_level%", () => ((int)g.PremiumTier).ToString());
|
|
||||||
// todo fix
|
|
||||||
// _reps.TryAdd("%server.time%",
|
|
||||||
// () =>
|
|
||||||
// {
|
|
||||||
// var to = TimeZoneInfo.Local;
|
|
||||||
// if (g is not null)
|
|
||||||
// {
|
|
||||||
// if (GuildTimezoneService.AllServices.TryGetValue(client.CurrentUser.Id, out var tz))
|
|
||||||
// to = tz.GetTimeZoneOrDefault(g.Id) ?? TimeZoneInfo.Local;
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// return TimeZoneInfo.ConvertTime(DateTime.UtcNow, TimeZoneInfo.Utc, to).ToString("HH:mm ")
|
|
||||||
// + to.StandardName.GetInitials();
|
|
||||||
// });
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public ReplacementBuilder WithChannel(IMessageChannel ch)
|
|
||||||
{
|
|
||||||
_reps.TryAdd("%channel%", () => ch.Name);
|
|
||||||
_reps.TryAdd("%channel.mention%", () => (ch as ITextChannel)?.Mention ?? "#" + ch.Name);
|
|
||||||
_reps.TryAdd("%channel.name%", () => ch.Name);
|
|
||||||
_reps.TryAdd("%channel.id%", () => ch.Id.ToString());
|
|
||||||
_reps.TryAdd("%channel.created%", () => ch.CreatedAt.ToString("HH:mm dd.MM.yyyy"));
|
|
||||||
_reps.TryAdd("%channel.nsfw%", () => (ch as ITextChannel)?.IsNsfw.ToString() ?? "-");
|
|
||||||
_reps.TryAdd("%channel.topic%", () => (ch as ITextChannel)?.Topic ?? "-");
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public ReplacementBuilder WithUser(IUser user)
|
|
||||||
{
|
|
||||||
WithManyUsers(new[] { user });
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public ReplacementBuilder WithManyUsers(IEnumerable<IUser> users)
|
|
||||||
{
|
|
||||||
_reps.TryAdd("%user%", () => string.Join(" ", users.Select(user => user.Mention)));
|
|
||||||
_reps.TryAdd("%user.mention%", () => string.Join(" ", users.Select(user => user.Mention)));
|
|
||||||
_reps.TryAdd("%user.fullname%", () => string.Join(" ", users.Select(user => user.ToString())));
|
|
||||||
_reps.TryAdd("%user.name%", () => string.Join(" ", users.Select(user => user.Username)));
|
|
||||||
_reps.TryAdd("%user.discrim%", () => string.Join(" ", users.Select(user => user.Discriminator)));
|
|
||||||
_reps.TryAdd("%user.avatar%", () => string.Join(" ", users.Select(user => user.RealAvatarUrl().ToString())));
|
|
||||||
_reps.TryAdd("%user.id%", () => string.Join(" ", users.Select(user => user.Id.ToString())));
|
|
||||||
_reps.TryAdd("%user.created_time%",
|
|
||||||
() => string.Join(" ", users.Select(user => user.CreatedAt.ToString("HH:mm"))));
|
|
||||||
_reps.TryAdd("%user.created_date%",
|
|
||||||
() => string.Join(" ", users.Select(user => user.CreatedAt.ToString("dd.MM.yyyy"))));
|
|
||||||
_reps.TryAdd("%user.joined_time%",
|
|
||||||
() => string.Join(" ", users.Select(user => (user as IGuildUser)?.JoinedAt?.ToString("HH:mm") ?? "-")));
|
|
||||||
_reps.TryAdd("%user.joined_date%",
|
|
||||||
() => string.Join(" ",
|
|
||||||
users.Select(user => (user as IGuildUser)?.JoinedAt?.ToString("dd.MM.yyyy") ?? "-")));
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
private ReplacementBuilder WithStats(DiscordSocketClient c)
|
|
||||||
{
|
|
||||||
_reps.TryAdd("%shard.servercount%", () => c.Guilds.Count.ToString());
|
|
||||||
_reps.TryAdd("%shard.usercount%", () => c.Guilds.Sum(g => g.MemberCount).ToString());
|
|
||||||
_reps.TryAdd("%shard.id%", () => c.ShardId.ToString());
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public ReplacementBuilder WithRngRegex()
|
|
||||||
{
|
|
||||||
var rng = new EllieRandom();
|
|
||||||
_regex.TryAdd(_rngRegex,
|
|
||||||
match =>
|
|
||||||
{
|
|
||||||
if (!int.TryParse(match.Groups["from"].ToString(), out var from))
|
|
||||||
from = 0;
|
|
||||||
if (!int.TryParse(match.Groups["to"].ToString(), out var to))
|
|
||||||
to = 0;
|
|
||||||
|
|
||||||
if (from == 0 && to == 0)
|
|
||||||
return rng.Next(0, 11).ToString();
|
|
||||||
|
|
||||||
if (from >= to)
|
|
||||||
return string.Empty;
|
|
||||||
|
|
||||||
return rng.Next(from, to + 1).ToString();
|
|
||||||
});
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public ReplacementBuilder WithOverride(string key, Func<string> output)
|
|
||||||
{
|
|
||||||
_reps.AddOrUpdate(key, output, delegate { return output; });
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Replacer Build()
|
|
||||||
=> new(_reps.Select(x => (x.Key, x.Value)).ToArray(), _regex.Select(x => (x.Key, x.Value)).ToArray());
|
|
||||||
|
|
||||||
public ReplacementBuilder WithProviders(IEnumerable<IPlaceholderProvider> phProviders)
|
|
||||||
{
|
|
||||||
foreach (var provider in phProviders)
|
|
||||||
foreach (var ovr in provider.GetPlaceholders())
|
|
||||||
_reps.TryAdd(ovr.Name, ovr.Func);
|
|
||||||
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,93 +0,0 @@
|
||||||
#nullable disable
|
|
||||||
using System.Text.RegularExpressions;
|
|
||||||
|
|
||||||
namespace Ellie.Common;
|
|
||||||
|
|
||||||
public class Replacer
|
|
||||||
{
|
|
||||||
private readonly IEnumerable<(Regex Regex, Func<Match, string> Replacement)> _regex;
|
|
||||||
private readonly IEnumerable<(string Key, Func<string> Text)> _replacements;
|
|
||||||
|
|
||||||
public Replacer(IEnumerable<(string, Func<string>)> replacements, IEnumerable<(Regex, Func<Match, string>)> regex)
|
|
||||||
{
|
|
||||||
_replacements = replacements;
|
|
||||||
_regex = regex;
|
|
||||||
}
|
|
||||||
|
|
||||||
public string Replace(string input)
|
|
||||||
{
|
|
||||||
if (string.IsNullOrWhiteSpace(input))
|
|
||||||
return input;
|
|
||||||
|
|
||||||
foreach (var (key, text) in _replacements)
|
|
||||||
{
|
|
||||||
if (input.Contains(key))
|
|
||||||
input = input.Replace(key, text(), StringComparison.InvariantCulture);
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach (var item in _regex)
|
|
||||||
input = item.Regex.Replace(input, m => item.Replacement(m));
|
|
||||||
|
|
||||||
return input;
|
|
||||||
}
|
|
||||||
|
|
||||||
public SmartText Replace(SmartText data)
|
|
||||||
=> data switch
|
|
||||||
{
|
|
||||||
SmartEmbedText embedData => Replace(embedData) with
|
|
||||||
{
|
|
||||||
PlainText = Replace(embedData.PlainText),
|
|
||||||
Color = embedData.Color
|
|
||||||
},
|
|
||||||
SmartPlainText plain => Replace(plain),
|
|
||||||
SmartEmbedTextArray arr => Replace(arr),
|
|
||||||
_ => throw new ArgumentOutOfRangeException(nameof(data), "Unsupported argument type")
|
|
||||||
};
|
|
||||||
|
|
||||||
private SmartEmbedTextArray Replace(SmartEmbedTextArray embedArr)
|
|
||||||
=> new()
|
|
||||||
{
|
|
||||||
Embeds = embedArr.Embeds.Map(e => Replace(e) with
|
|
||||||
{
|
|
||||||
Color = e.Color
|
|
||||||
}),
|
|
||||||
Content = Replace(embedArr.Content)
|
|
||||||
};
|
|
||||||
|
|
||||||
private SmartPlainText Replace(SmartPlainText plain)
|
|
||||||
=> Replace(plain.Text);
|
|
||||||
|
|
||||||
private T Replace<T>(T embedData) where T : SmartEmbedTextBase, new()
|
|
||||||
{
|
|
||||||
var newEmbedData = new T
|
|
||||||
{
|
|
||||||
Description = Replace(embedData.Description),
|
|
||||||
Title = Replace(embedData.Title),
|
|
||||||
Thumbnail = Replace(embedData.Thumbnail),
|
|
||||||
Image = Replace(embedData.Image),
|
|
||||||
Url = Replace(embedData.Url),
|
|
||||||
Author = embedData.Author is null
|
|
||||||
? null
|
|
||||||
: new()
|
|
||||||
{
|
|
||||||
Name = Replace(embedData.Author.Name),
|
|
||||||
IconUrl = Replace(embedData.Author.IconUrl)
|
|
||||||
},
|
|
||||||
Fields = embedData.Fields?.Map(f => new SmartTextEmbedField
|
|
||||||
{
|
|
||||||
Name = Replace(f.Name),
|
|
||||||
Value = Replace(f.Value),
|
|
||||||
Inline = f.Inline
|
|
||||||
}),
|
|
||||||
Footer = embedData.Footer is null
|
|
||||||
? null
|
|
||||||
: new()
|
|
||||||
{
|
|
||||||
Text = Replace(embedData.Footer.Text),
|
|
||||||
IconUrl = Replace(embedData.Footer.IconUrl)
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return newEmbedData;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,427 +0,0 @@
|
||||||
#nullable disable
|
|
||||||
using Ellie.Common.Configs;
|
|
||||||
using Ellie.Common.ModuleBehaviors;
|
|
||||||
using Ellie.Db;
|
|
||||||
using ExecuteResult = Discord.Commands.ExecuteResult;
|
|
||||||
using PreconditionResult = Discord.Commands.PreconditionResult;
|
|
||||||
|
|
||||||
namespace Ellie.Services;
|
|
||||||
|
|
||||||
public class CommandHandler : IEService, 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()
|
|
||||||
{
|
|
||||||
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_ELLIE
|
|
||||||
// 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,109 +0,0 @@
|
||||||
#nullable disable
|
|
||||||
using LinqToDB;
|
|
||||||
using LinqToDB.EntityFrameworkCore;
|
|
||||||
using Ellie.Db.Models;
|
|
||||||
using Ellie.Services.Currency;
|
|
||||||
|
|
||||||
namespace Ellie.Services;
|
|
||||||
|
|
||||||
public sealed class CurrencyService : ICurrencyService, IEService
|
|
||||||
{
|
|
||||||
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);
|
|
||||||
}
|
|
|
@ -1,39 +0,0 @@
|
||||||
using Ellie.Services.Currency;
|
|
||||||
|
|
||||||
namespace Ellie.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,115 +0,0 @@
|
||||||
using LinqToDB;
|
|
||||||
using LinqToDB.EntityFrameworkCore;
|
|
||||||
using Ellie.Db.Models;
|
|
||||||
using Ellie.Services.Database.Models;
|
|
||||||
|
|
||||||
namespace Ellie.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
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,110 +0,0 @@
|
||||||
using LinqToDB;
|
|
||||||
using LinqToDB.EntityFrameworkCore;
|
|
||||||
using Ellie.Common.ModuleBehaviors;
|
|
||||||
using Ellie.Services.Currency;
|
|
||||||
using Ellie.Services.Database.Models;
|
|
||||||
|
|
||||||
namespace Ellie.Services;
|
|
||||||
|
|
||||||
public sealed class GamblingTxTracker : ITxTracker, IEService, 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,17 +0,0 @@
|
||||||
#nullable disable
|
|
||||||
namespace Ellie.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();
|
|
||||||
}
|
|
|
@ -1,12 +0,0 @@
|
||||||
namespace Ellie.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);
|
|
||||||
}
|
|
|
@ -1,20 +0,0 @@
|
||||||
#nullable disable
|
|
||||||
namespace Ellie.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; }
|
|
||||||
}
|
|
|
@ -1,13 +0,0 @@
|
||||||
using Ellie.Common.ModuleBehaviors;
|
|
||||||
|
|
||||||
namespace Ellie.Services;
|
|
||||||
|
|
||||||
public interface ICustomBehavior
|
|
||||||
: IExecOnMessage,
|
|
||||||
IInputTransformer,
|
|
||||||
IExecPreCommand,
|
|
||||||
IExecNoCommand,
|
|
||||||
IExecPostCommand
|
|
||||||
{
|
|
||||||
|
|
||||||
}
|
|
|
@ -1,9 +0,0 @@
|
||||||
#nullable disable
|
|
||||||
namespace Ellie.Services;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// All services must implement this interface in order to be auto-discovered by the DI system
|
|
||||||
/// </summary>
|
|
||||||
public interface IEService
|
|
||||||
{
|
|
||||||
}
|
|
|
@ -1,81 +0,0 @@
|
||||||
#nullable disable
|
|
||||||
using Ellie.Common.Configs;
|
|
||||||
|
|
||||||
namespace Ellie.Services;
|
|
||||||
|
|
||||||
public interface IEmbedBuilderService
|
|
||||||
{
|
|
||||||
IEmbedBuilder Create(ICommandContext ctx = null);
|
|
||||||
IEmbedBuilder Create(EmbedBuilder eb);
|
|
||||||
}
|
|
||||||
|
|
||||||
public class EmbedBuilderService : IEmbedBuilderService, IEService
|
|
||||||
{
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,19 +0,0 @@
|
||||||
#nullable disable
|
|
||||||
|
|
||||||
namespace Ellie.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);
|
|
||||||
}
|
|
|
@ -1,13 +0,0 @@
|
||||||
#nullable disable
|
|
||||||
using Ellie.Common.Pokemon;
|
|
||||||
using Ellie.Modules.Games.Common.Trivia;
|
|
||||||
|
|
||||||
namespace Ellie.Services;
|
|
||||||
|
|
||||||
public interface ILocalDataCache
|
|
||||||
{
|
|
||||||
Task<IReadOnlyDictionary<string, SearchPokemon>> GetPokemonsAsync();
|
|
||||||
Task<IReadOnlyDictionary<string, SearchPokemonAbility>> GetPokemonAbilitiesAsync();
|
|
||||||
Task<TriviaQuestionModel[]> GetTriviaQuestionsAsync();
|
|
||||||
Task<IReadOnlyDictionary<int, string>> GetPokemonMapAsync();
|
|
||||||
}
|
|
|
@ -1,19 +0,0 @@
|
||||||
#nullable disable
|
|
||||||
using System.Globalization;
|
|
||||||
|
|
||||||
namespace Ellie.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);
|
|
||||||
}
|
|
|
@ -1,15 +0,0 @@
|
||||||
#nullable disable
|
|
||||||
using Ellie.Services.Database.Models;
|
|
||||||
|
|
||||||
namespace Ellie.Modules.Utility.Services;
|
|
||||||
|
|
||||||
public interface IRemindService
|
|
||||||
{
|
|
||||||
Task AddReminderAsync(ulong userId,
|
|
||||||
ulong targetId,
|
|
||||||
ulong? guildId,
|
|
||||||
bool isPrivate,
|
|
||||||
DateTime time,
|
|
||||||
string message,
|
|
||||||
ReminderType reminderType);
|
|
||||||
}
|
|
|
@ -1,51 +0,0 @@
|
||||||
#nullable disable
|
|
||||||
namespace Ellie.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();
|
|
||||||
}
|
|
|
@ -1,6 +0,0 @@
|
||||||
namespace Ellie.Common;
|
|
||||||
|
|
||||||
public interface ITimezoneService
|
|
||||||
{
|
|
||||||
TimeZoneInfo GetTimeZoneOrUtc(ulong? guildId);
|
|
||||||
}
|
|
|
@ -1,302 +0,0 @@
|
||||||
#nullable disable
|
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
|
||||||
using Ellie.Common.ModuleBehaviors;
|
|
||||||
|
|
||||||
namespace Ellie.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
|
|
||||||
}
|
|
|
@ -1,136 +0,0 @@
|
||||||
#nullable disable
|
|
||||||
using LinqToDB;
|
|
||||||
using LinqToDB.EntityFrameworkCore;
|
|
||||||
using Ellie.Common.ModuleBehaviors;
|
|
||||||
using Ellie.Db;
|
|
||||||
using Ellie.Db.Models;
|
|
||||||
using Ellie.Services.Database.Models;
|
|
||||||
|
|
||||||
namespace Ellie.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,
|
|
||||||
});
|
|
||||||
|
|
||||||
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 void BlacklistUsers(IReadOnlyCollection<ulong> toBlacklist)
|
|
||||||
{
|
|
||||||
using (var uow = _db.GetDbContext())
|
|
||||||
{
|
|
||||||
var bc = uow.Set<BlacklistEntry>();
|
|
||||||
bc.AddRange(toBlacklist.Select(x => new BlacklistEntry
|
|
||||||
{
|
|
||||||
ItemId = x,
|
|
||||||
Type = BlacklistType.User
|
|
||||||
}));
|
|
||||||
|
|
||||||
// todo check if blacklist works and removes currency
|
|
||||||
uow.GetTable<DiscordUser>()
|
|
||||||
.UpdateAsync(x => toBlacklist.Contains(x.UserId),
|
|
||||||
_ => new()
|
|
||||||
{
|
|
||||||
CurrencyAmount = 0
|
|
||||||
});
|
|
||||||
|
|
||||||
uow.SaveChanges();
|
|
||||||
}
|
|
||||||
|
|
||||||
Reload();
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,172 +0,0 @@
|
||||||
using CommandLine;
|
|
||||||
|
|
||||||
namespace Ellie.Common;
|
|
||||||
|
|
||||||
public sealed class CommandsUtilityService : ICommandsUtilityService, IEService
|
|
||||||
{
|
|
||||||
private readonly CommandHandler _ch;
|
|
||||||
private readonly IBotStrings _strings;
|
|
||||||
private readonly DiscordPermOverrideService _dpos;
|
|
||||||
private readonly IEmbedBuilderService _eb;
|
|
||||||
private readonly ILocalization _loc;
|
|
||||||
private readonly Ellie.Marmalade.IMarmaladeLoaderSevice _marmalades;
|
|
||||||
|
|
||||||
public CommandsUtilityService(
|
|
||||||
CommandHandler ch,
|
|
||||||
IBotStrings strings,
|
|
||||||
DiscordPermOverrideService dpos,
|
|
||||||
IEmbedBuilderService eb,
|
|
||||||
ILocalization loc,
|
|
||||||
Ellie.Marmalade.IMarmaladeLoaderSevice marmalades)
|
|
||||||
{
|
|
||||||
_ch = ch;
|
|
||||||
_strings = strings;
|
|
||||||
_dpos = dpos;
|
|
||||||
_eb = eb;
|
|
||||||
_loc = loc;
|
|
||||||
_marmalades = marmalades;
|
|
||||||
}
|
|
||||||
|
|
||||||
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, _marmalades, 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));
|
|
||||||
|
|
||||||
em.AddField(_strings.GetText(strs.usage),
|
|
||||||
string.Join("\n", com.RealRemarksArr(_strings, _marmalades, culture, prefix).Map(arg => Format.Code(arg))))
|
|
||||||
.WithFooter(GetText(strs.module(com.Module.GetTopLevelModule().Name), guild))
|
|
||||||
.WithOkColor();
|
|
||||||
|
|
||||||
var opt = GetEllieOptionType(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? GetEllieOptionType(IEnumerable<Attribute> attributes)
|
|
||||||
=> attributes
|
|
||||||
.Select(a => a.GetType())
|
|
||||||
.Where(a => a.IsGenericType
|
|
||||||
&& a.GetGenericTypeDefinition() == typeof(EllieOptionsAttribute<>))
|
|
||||||
.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);
|
|
||||||
}
|
|
|
@ -1,136 +0,0 @@
|
||||||
#nullable disable
|
|
||||||
using Microsoft.EntityFrameworkCore;
|
|
||||||
using Ellie.Common.ModuleBehaviors;
|
|
||||||
using Ellie.Services.Database.Models;
|
|
||||||
|
|
||||||
namespace Ellie.Services;
|
|
||||||
|
|
||||||
public class DiscordPermOverrideService : IEService, 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 Ellie.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 = (Ellie.Bot.Db.GuildPerm)perm,
|
|
||||||
GuildId = guildId
|
|
||||||
});
|
|
||||||
}
|
|
||||||
else
|
|
||||||
over.Perm = (Ellie.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;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,60 +0,0 @@
|
||||||
#nullable disable
|
|
||||||
using SixLabors.Fonts;
|
|
||||||
|
|
||||||
namespace Ellie.Services;
|
|
||||||
|
|
||||||
public class FontProvider : IEService
|
|
||||||
{
|
|
||||||
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,17 +0,0 @@
|
||||||
namespace Ellie.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);
|
|
||||||
}
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue