Removed a bunch of things out of EllieBot
This commit is contained in:
parent
9adb5a9906
commit
957790a85b
150 changed files with 435 additions and 5984 deletions
404
src/EllieBot/Bot.cs
Normal file
404
src/EllieBot/Bot.cs
Normal file
|
@ -0,0 +1,404 @@
|
|||
#nullable disable
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using EllieBot.Common.Configs;
|
||||
using EllieBot.Common.ModuleBehaviors;
|
||||
using EllieBot.Db;
|
||||
using EllieBot.Modules.Utility;
|
||||
using EllieBot.Services.Database.Models;
|
||||
using System.Collections.Immutable;
|
||||
using System.Diagnostics;
|
||||
using System.Net;
|
||||
using System.Reflection;
|
||||
using RunMode = Discord.Commands.RunMode;
|
||||
|
||||
namespace EllieBot;
|
||||
|
||||
public sealed class Bot
|
||||
{
|
||||
public event Func<GuildConfig, Task> JoinedGuild = delegate { return Task.CompletedTask; };
|
||||
|
||||
public DiscordSocketClient Client { get; }
|
||||
public ImmutableArray<GuildConfig> AllGuildConfigs { get; private set; }
|
||||
|
||||
private IServiceProvider Services { get; set; }
|
||||
|
||||
public string Mention { get; private set; }
|
||||
public bool IsReady { get; private set; }
|
||||
public int ShardId { get; set; }
|
||||
|
||||
private readonly IBotCredentials _creds;
|
||||
private readonly CommandService _commandService;
|
||||
private readonly DbService _db;
|
||||
|
||||
private readonly IBotCredsProvider _credsProvider;
|
||||
// private readonly InteractionService _interactionService;
|
||||
|
||||
public Bot(int shardId, int? totalShards, string credPath = null)
|
||||
{
|
||||
if (shardId < 0)
|
||||
throw new ArgumentOutOfRangeException(nameof(shardId));
|
||||
|
||||
ShardId = shardId;
|
||||
_credsProvider = new BotCredsProvider(totalShards, credPath);
|
||||
_creds = _credsProvider.GetCreds();
|
||||
|
||||
_db = new(_credsProvider);
|
||||
|
||||
var messageCacheSize =
|
||||
#if GLOBAL_ELLIE
|
||||
0;
|
||||
#else
|
||||
50;
|
||||
#endif
|
||||
|
||||
if(!_creds.UsePrivilegedIntents)
|
||||
Log.Warning("You are not using privileged intents. Some features will not work properly");
|
||||
|
||||
Client = new(new()
|
||||
{
|
||||
MessageCacheSize = messageCacheSize,
|
||||
LogLevel = LogSeverity.Warning,
|
||||
ConnectionTimeout = int.MaxValue,
|
||||
TotalShards = _creds.TotalShards,
|
||||
ShardId = shardId,
|
||||
AlwaysDownloadUsers = false,
|
||||
AlwaysResolveStickers = false,
|
||||
AlwaysDownloadDefaultStickers = false,
|
||||
GatewayIntents = _creds.UsePrivilegedIntents
|
||||
? GatewayIntents.All
|
||||
: GatewayIntents.AllUnprivileged,
|
||||
LogGatewayIntentWarnings = false,
|
||||
FormatUsersInBidirectionalUnicode = false,
|
||||
DefaultRetryMode = RetryMode.Retry502
|
||||
});
|
||||
|
||||
_commandService = new(new()
|
||||
{
|
||||
CaseSensitiveCommands = false,
|
||||
DefaultRunMode = RunMode.Sync,
|
||||
});
|
||||
|
||||
// _interactionService = new(Client.Rest);
|
||||
|
||||
Client.Log += Client_Log;
|
||||
}
|
||||
|
||||
|
||||
public List<ulong> GetCurrentGuildIds()
|
||||
=> Client.Guilds.Select(x => x.Id).ToList();
|
||||
|
||||
private void AddServices()
|
||||
{
|
||||
var startingGuildIdList = GetCurrentGuildIds();
|
||||
var sw = Stopwatch.StartNew();
|
||||
var bot = Client.CurrentUser;
|
||||
|
||||
using (var uow = _db.GetDbContext())
|
||||
{
|
||||
uow.EnsureUserCreated(bot.Id, bot.Username, bot.Discriminator, bot.AvatarId);
|
||||
AllGuildConfigs = uow.GuildConfigs.GetAllGuildConfigs(startingGuildIdList).ToImmutableArray();
|
||||
}
|
||||
|
||||
var svcs = new ServiceCollection().AddTransient(_ => _credsProvider.GetCreds()) // bot creds
|
||||
.AddSingleton(_credsProvider)
|
||||
.AddSingleton(_db) // database
|
||||
.AddSingleton(Client) // discord socket client
|
||||
.AddSingleton(_commandService)
|
||||
// .AddSingleton(_interactionService)
|
||||
.AddSingleton(this)
|
||||
.AddSingleton<ISeria, JsonSeria>()
|
||||
.AddSingleton<IConfigSeria, YamlSeria>()
|
||||
.AddConfigServices()
|
||||
.AddConfigMigrators()
|
||||
.AddMemoryCache()
|
||||
// music
|
||||
.AddMusic()
|
||||
// cache
|
||||
.AddCache(_creds);
|
||||
|
||||
|
||||
svcs.AddHttpClient();
|
||||
svcs.AddHttpClient("memelist")
|
||||
.ConfigurePrimaryHttpMessageHandler(() => new HttpClientHandler
|
||||
{
|
||||
AllowAutoRedirect = false
|
||||
});
|
||||
|
||||
svcs.AddHttpClient("google:search")
|
||||
.ConfigurePrimaryHttpMessageHandler(() => new HttpClientHandler()
|
||||
{
|
||||
AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate
|
||||
});
|
||||
|
||||
if (Environment.GetEnvironmentVariable("ELLIE_IS_COORDINATED") != "1")
|
||||
svcs.AddSingleton<ICoordinator, SingleProcessCoordinator>();
|
||||
else
|
||||
{
|
||||
svcs.AddSingleton<RemoteGrpcCoordinator>()
|
||||
.AddSingleton<ICoordinator>(x => x.GetRequiredService<RemoteGrpcCoordinator>())
|
||||
.AddSingleton<IReadyExecutor>(x => x.GetRequiredService<RemoteGrpcCoordinator>());
|
||||
}
|
||||
|
||||
svcs.Scan(scan => scan.FromAssemblyOf<IReadyExecutor>()
|
||||
.AddClasses(classes => classes.AssignableToAny(
|
||||
// services
|
||||
typeof(IEService),
|
||||
|
||||
// behaviours
|
||||
typeof(IExecOnMessage),
|
||||
typeof(IInputTransformer),
|
||||
typeof(IExecPreCommand),
|
||||
typeof(IExecPostCommand),
|
||||
typeof(IExecNoCommand))
|
||||
.WithoutAttribute<DontAddToIocContainerAttribute>()
|
||||
#if GLOBAL_ELLIE
|
||||
.WithoutAttribute<NoPublicBotAttribute>()
|
||||
#endif
|
||||
)
|
||||
.AsSelfWithInterfaces()
|
||||
.WithSingletonLifetime());
|
||||
|
||||
//initialize Services
|
||||
Services = svcs.BuildServiceProvider();
|
||||
Services.GetRequiredService<IBehaviorHandler>().Initialize();
|
||||
Services.GetRequiredService<CurrencyRewardService>();
|
||||
|
||||
if (Client.ShardId == 0)
|
||||
ApplyConfigMigrations();
|
||||
|
||||
_ = LoadTypeReaders(typeof(Bot).Assembly);
|
||||
|
||||
sw.Stop();
|
||||
Log.Information( "All services loaded in {ServiceLoadTime:F2}s", sw.Elapsed.TotalSeconds);
|
||||
}
|
||||
|
||||
private void ApplyConfigMigrations()
|
||||
{
|
||||
// execute all migrators
|
||||
var migrators = Services.GetServices<IConfigMigrator>();
|
||||
foreach (var migrator in migrators)
|
||||
migrator.EnsureMigrated();
|
||||
}
|
||||
|
||||
private IEnumerable<object> LoadTypeReaders(Assembly assembly)
|
||||
{
|
||||
Type[] allTypes;
|
||||
try
|
||||
{
|
||||
allTypes = assembly.GetTypes();
|
||||
}
|
||||
catch (ReflectionTypeLoadException ex)
|
||||
{
|
||||
Log.Warning(ex.LoaderExceptions[0], "Error getting types");
|
||||
return Enumerable.Empty<object>();
|
||||
}
|
||||
|
||||
var filteredTypes = allTypes.Where(x => x.IsSubclassOf(typeof(TypeReader))
|
||||
&& x.BaseType?.GetGenericArguments().Length > 0
|
||||
&& !x.IsAbstract);
|
||||
|
||||
var toReturn = new List<object>();
|
||||
foreach (var ft in filteredTypes)
|
||||
{
|
||||
var x = (TypeReader)ActivatorUtilities.CreateInstance(Services, ft);
|
||||
var baseType = ft.BaseType;
|
||||
if (baseType is null)
|
||||
continue;
|
||||
var typeArgs = baseType.GetGenericArguments();
|
||||
_commandService.AddTypeReader(typeArgs[0], x);
|
||||
toReturn.Add(x);
|
||||
}
|
||||
|
||||
return toReturn;
|
||||
}
|
||||
|
||||
private async Task LoginAsync(string token)
|
||||
{
|
||||
var clientReady = new TaskCompletionSource<bool>(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||
|
||||
async Task SetClientReady()
|
||||
{
|
||||
clientReady.TrySetResult(true);
|
||||
try
|
||||
{
|
||||
foreach (var chan in await Client.GetDMChannelsAsync())
|
||||
await chan.CloseAsync();
|
||||
}
|
||||
catch
|
||||
{
|
||||
// ignored
|
||||
}
|
||||
}
|
||||
|
||||
//connect
|
||||
Log.Information("Shard {ShardId} logging in ...", Client.ShardId);
|
||||
try
|
||||
{
|
||||
Client.Ready += SetClientReady;
|
||||
|
||||
await Client.LoginAsync(TokenType.Bot, token);
|
||||
await Client.StartAsync();
|
||||
}
|
||||
catch (HttpException ex)
|
||||
{
|
||||
LoginErrorHandler.Handle(ex);
|
||||
Helpers.ReadErrorAndExit(3);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LoginErrorHandler.Handle(ex);
|
||||
Helpers.ReadErrorAndExit(4);
|
||||
}
|
||||
|
||||
await clientReady.Task.ConfigureAwait(false);
|
||||
Client.Ready -= SetClientReady;
|
||||
|
||||
Client.JoinedGuild += Client_JoinedGuild;
|
||||
Client.LeftGuild += Client_LeftGuild;
|
||||
|
||||
// _ = Client.SetStatusAsync(UserStatus.Online);
|
||||
Log.Information("Shard {ShardId} logged in", Client.ShardId);
|
||||
}
|
||||
|
||||
private Task Client_LeftGuild(SocketGuild arg)
|
||||
{
|
||||
Log.Information("Left server: {GuildName} [{GuildId}]", arg?.Name, arg?.Id);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private Task Client_JoinedGuild(SocketGuild arg)
|
||||
{
|
||||
Log.Information("Joined server: {GuildName} [{GuildId}]", arg.Name, arg.Id);
|
||||
_ = Task.Run(async () =>
|
||||
{
|
||||
GuildConfig gc;
|
||||
await using (var uow = _db.GetDbContext())
|
||||
{
|
||||
gc = uow.GuildConfigsForId(arg.Id, null);
|
||||
}
|
||||
|
||||
await JoinedGuild.Invoke(gc);
|
||||
});
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public async Task RunAsync()
|
||||
{
|
||||
if (ShardId == 0)
|
||||
await _db.SetupAsync();
|
||||
|
||||
var sw = Stopwatch.StartNew();
|
||||
|
||||
await LoginAsync(_creds.Token);
|
||||
|
||||
Mention = Client.CurrentUser.Mention;
|
||||
Log.Information("Shard {ShardId} loading services...", Client.ShardId);
|
||||
try
|
||||
{
|
||||
AddServices();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Error(ex, "Error adding services");
|
||||
Helpers.ReadErrorAndExit(9);
|
||||
}
|
||||
|
||||
sw.Stop();
|
||||
Log.Information("Shard {ShardId} connected in {Elapsed:F2}s", Client.ShardId, sw.Elapsed.TotalSeconds);
|
||||
var commandHandler = Services.GetRequiredService<CommandHandler>();
|
||||
|
||||
// start handling messages received in commandhandler
|
||||
await commandHandler.StartHandling();
|
||||
|
||||
await _commandService.AddModulesAsync(typeof(Bot).Assembly, Services);
|
||||
// await _interactionService.AddModulesAsync(typeof(Bot).Assembly, Services);
|
||||
IsReady = true;
|
||||
|
||||
await EnsureBotOwnershipAsync();
|
||||
_ = Task.Run(ExecuteReadySubscriptions);
|
||||
Log.Information("Shard {ShardId} ready", Client.ShardId);
|
||||
}
|
||||
|
||||
private async ValueTask EnsureBotOwnershipAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (_creds.OwnerIds.Count != 0)
|
||||
return;
|
||||
|
||||
Log.Information("Initializing Owner Id...");
|
||||
var info = await Client.GetApplicationInfoAsync();
|
||||
_credsProvider.ModifyCredsFile(x => x.OwnerIds = new[] { info.Owner.Id });
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Warning("Getting application info failed: {ErrorMessage}", ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
private Task ExecuteReadySubscriptions()
|
||||
{
|
||||
var readyExecutors = Services.GetServices<IReadyExecutor>();
|
||||
var tasks = readyExecutors.Select(async toExec =>
|
||||
{
|
||||
try
|
||||
{
|
||||
await toExec.OnReadyAsync();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Error(ex,
|
||||
"Failed running OnReadyAsync method on {Type} type: {Message}",
|
||||
toExec.GetType().Name,
|
||||
ex.Message);
|
||||
}
|
||||
});
|
||||
|
||||
return tasks.WhenAll();
|
||||
}
|
||||
|
||||
private Task Client_Log(LogMessage arg)
|
||||
{
|
||||
if (arg.Message?.Contains("unknown dispatch", StringComparison.InvariantCultureIgnoreCase) ?? false)
|
||||
return Task.CompletedTask;
|
||||
|
||||
if (arg.Exception is { InnerException: WebSocketClosedException { CloseCode: 4014 } })
|
||||
{
|
||||
Log.Error(@"
|
||||
Login failed.
|
||||
|
||||
*** Please enable privileged intents ***
|
||||
|
||||
Certain Ellie features require Discord's privileged gateway intents.
|
||||
These include greeting and goodbye messages, as well as creating the Owner message channels for DM forwarding.
|
||||
|
||||
How to enable privileged intents:
|
||||
1. Head over to the Discord Developer Portal https://discord.com/developers/applications/
|
||||
2. Select your Application.
|
||||
3. Click on `Bot` in the left side navigation panel, and scroll down to the intents section.
|
||||
4. Enable all intents.
|
||||
5. Restart your bot.
|
||||
|
||||
Read this only if your bot is in 100 or more servers:
|
||||
|
||||
You'll need to apply to use the intents with Discord, but for small selfhosts, all that is required is enabling the intents in the developer portal.
|
||||
Yes, this is a new thing from Discord, as of October 2020. No, there's nothing we can do about it. Yes, we're aware it worked before.
|
||||
While waiting for your bot to be accepted, you can change the 'usePrivilegedIntents' inside your creds.yml to 'false', although this will break many of the ellie's features");
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
#if GLOBAL_ELLIE || DEBUG
|
||||
if (arg.Exception is not null)
|
||||
Log.Warning(arg.Exception, "{ErrorSource} | {ErrorMessage}", arg.Source, arg.Message);
|
||||
else
|
||||
Log.Warning("{ErrorSource} | {ErrorMessage}", arg.Source, arg.Message);
|
||||
#endif
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public async Task RunAndBlockAsync()
|
||||
{
|
||||
await RunAsync();
|
||||
await Task.Delay(-1);
|
||||
}
|
||||
}
|
|
@ -1,12 +0,0 @@
|
|||
using System.Runtime.CompilerServices;
|
||||
|
||||
namespace EllieBot.Common.Attributes;
|
||||
|
||||
[AttributeUsage(AttributeTargets.Method)]
|
||||
public sealed class AliasesAttribute : AliasAttribute
|
||||
{
|
||||
public AliasesAttribute([CallerMemberName] string memberName = "")
|
||||
: base(CommandNameLoadHelper.GetAliasesFor(memberName))
|
||||
{
|
||||
}
|
||||
}
|
|
@ -1,31 +0,0 @@
|
|||
using YamlDotNet.Serialization;
|
||||
|
||||
namespace EllieBot.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,11 +0,0 @@
|
|||
#nullable disable
|
||||
namespace EllieBot.Common;
|
||||
|
||||
/// <summary>
|
||||
/// Classed marked with this attribute will not be added to the service provider
|
||||
/// </summary>
|
||||
[AttributeUsage(AttributeTargets.Class)]
|
||||
public class DontAddToIocContainerAttribute : Attribute
|
||||
{
|
||||
|
||||
}
|
|
@ -1,18 +0,0 @@
|
|||
using System.Runtime.CompilerServices;
|
||||
|
||||
namespace EllieBot.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,10 +0,0 @@
|
|||
namespace EllieBot.Common.Attributes;
|
||||
|
||||
[AttributeUsage(AttributeTargets.Method)]
|
||||
public sealed class EllieOptionsAttribute : Attribute
|
||||
{
|
||||
public Type OptionType { get; set; }
|
||||
|
||||
public EllieOptionsAttribute(Type t)
|
||||
=> OptionType = t;
|
||||
}
|
|
@ -1,38 +0,0 @@
|
|||
#nullable disable
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
|
||||
namespace EllieBot.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/v4/)."));
|
||||
#else
|
||||
return Task.FromResult(PreconditionResult.FromSuccess());
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
[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 EllieBot.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 EllieBot.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,30 +0,0 @@
|
|||
using Microsoft.Extensions.DependencyInjection;
|
||||
using EllieBot.Modules.Administration.Services;
|
||||
|
||||
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<DiscordPermOverrideService>();
|
||||
if (permService.TryGetOverrides(context.Guild?.Id ?? 0, command.Name.ToUpperInvariant(), out _))
|
||||
return Task.FromResult(PreconditionResult.FromSuccess());
|
||||
|
||||
return base.CheckPermissionsAsync(context, command, services);
|
||||
}
|
||||
}
|
|
@ -1,46 +0,0 @@
|
|||
using OneOf;
|
||||
using OneOf.Types;
|
||||
|
||||
namespace EllieBot.Common;
|
||||
|
||||
public static class BotCacheExtensions
|
||||
{
|
||||
public static async ValueTask<T?> GetOrDefaultAsync<T>(this IBotCache cache, TypedKey<T> key)
|
||||
{
|
||||
var result = await cache.GetAsync(key);
|
||||
if (result.TryGetValue(out var val))
|
||||
return val;
|
||||
|
||||
return default;
|
||||
}
|
||||
|
||||
private static TypedKey<byte[]> GetImgKey(Uri uri)
|
||||
=> new($"image:{uri}");
|
||||
|
||||
public static ValueTask SetImageDataAsync(this IBotCache c, string key, byte[] data)
|
||||
=> c.SetImageDataAsync(new Uri(key), data);
|
||||
public static async ValueTask SetImageDataAsync(this IBotCache c, Uri key, byte[] data)
|
||||
=> await c.AddAsync(GetImgKey(key), data, expiry: TimeSpan.FromHours(48));
|
||||
|
||||
public static async ValueTask<OneOf<byte[], None>> GetImageDataAsync(this IBotCache c, Uri key)
|
||||
=> await c.GetAsync(GetImgKey(key));
|
||||
|
||||
public static async Task<TimeSpan?> GetRatelimitAsync(
|
||||
this IBotCache c,
|
||||
TypedKey<long> key,
|
||||
TimeSpan length)
|
||||
{
|
||||
var now = DateTime.UtcNow;
|
||||
var nowB = now.ToBinary();
|
||||
|
||||
var cachedValue = await c.GetOrAddAsync(key,
|
||||
() => Task.FromResult(now.ToBinary()),
|
||||
expiry: length);
|
||||
|
||||
if (cachedValue == nowB)
|
||||
return null;
|
||||
|
||||
var diff = now - DateTime.FromBinary(cachedValue);
|
||||
return length - diff;
|
||||
}
|
||||
}
|
|
@ -1,47 +0,0 @@
|
|||
using OneOf;
|
||||
using OneOf.Types;
|
||||
|
||||
namespace EllieBot.Common;
|
||||
|
||||
public interface IBotCache
|
||||
{
|
||||
/// <summary>
|
||||
/// Adds an item to the cache
|
||||
/// </summary>
|
||||
/// <param name="key">Key to add</param>
|
||||
/// <param name="value">Value to add to the cache</param>
|
||||
/// <param name="expiry">Optional expiry</param>
|
||||
/// <param name="overwrite">Whether old value should be overwritten</param>
|
||||
/// <typeparam name="T">Type of the value</typeparam>
|
||||
/// <returns>Returns whether add was sucessful. Always true unless ovewrite = false</returns>
|
||||
ValueTask<bool> AddAsync<T>(TypedKey<T> key, T value, TimeSpan? expiry = null, bool overwrite = true);
|
||||
|
||||
/// <summary>
|
||||
/// Get an element from the cache
|
||||
/// </summary>
|
||||
/// <param name="key">Key</param>
|
||||
/// <typeparam name="T">Type of the value</typeparam>
|
||||
/// <returns>Either a value or <see cref="None"/></returns>
|
||||
ValueTask<OneOf<T, None>> GetAsync<T>(TypedKey<T> key);
|
||||
|
||||
/// <summary>
|
||||
/// Remove a key from the cache
|
||||
/// </summary>
|
||||
/// <param name="key">Key to remove</param>
|
||||
/// <typeparam name="T">Type of the value</typeparam>
|
||||
/// <returns>Whether there was item</returns>
|
||||
ValueTask<bool> RemoveAsync<T>(TypedKey<T> key);
|
||||
|
||||
/// <summary>
|
||||
/// Get the key if it exists or add a new one
|
||||
/// </summary>
|
||||
/// <param name="key">Key to get and potentially add</param>
|
||||
/// <param name="createFactory">Value creation factory</param>
|
||||
/// <param name="expiry">Optional expiry</param>
|
||||
/// <typeparam name="T">Type of the value</typeparam>
|
||||
/// <returns>The retrieved or newly added value</returns>
|
||||
ValueTask<T?> GetOrAddAsync<T>(
|
||||
TypedKey<T> key,
|
||||
Func<Task<T?>> createFactory,
|
||||
TimeSpan? expiry = null);
|
||||
}
|
|
@ -1,71 +0,0 @@
|
|||
using Microsoft.Extensions.Caching.Memory;
|
||||
using OneOf;
|
||||
using OneOf.Types;
|
||||
|
||||
// ReSharper disable InconsistentlySynchronizedField
|
||||
|
||||
namespace EllieBot.Common;
|
||||
|
||||
public sealed class MemoryBotCache : IBotCache
|
||||
{
|
||||
// needed for overwrites and Delete return value
|
||||
private readonly object _cacheLock = new object();
|
||||
private readonly MemoryCache _cache;
|
||||
|
||||
public MemoryBotCache()
|
||||
{
|
||||
_cache = new MemoryCache(new MemoryCacheOptions());
|
||||
}
|
||||
|
||||
public ValueTask<bool> AddAsync<T>(TypedKey<T> key, T value, TimeSpan? expiry = null, bool overwrite = true)
|
||||
{
|
||||
if (overwrite)
|
||||
{
|
||||
using var item = _cache.CreateEntry(key.Key);
|
||||
item.Value = value;
|
||||
item.AbsoluteExpirationRelativeToNow = expiry;
|
||||
return new(true);
|
||||
}
|
||||
|
||||
lock (_cacheLock)
|
||||
{
|
||||
if (_cache.TryGetValue(key.Key, out var old) && old is not null)
|
||||
return new(false);
|
||||
|
||||
using var item = _cache.CreateEntry(key.Key);
|
||||
item.Value = value;
|
||||
item.AbsoluteExpirationRelativeToNow = expiry;
|
||||
return new(true);
|
||||
}
|
||||
}
|
||||
|
||||
public async ValueTask<T?> GetOrAddAsync<T>(
|
||||
TypedKey<T> key,
|
||||
Func<Task<T?>> createFactory,
|
||||
TimeSpan? expiry = null)
|
||||
=> await _cache.GetOrCreateAsync(key.Key,
|
||||
async ce =>
|
||||
{
|
||||
ce.AbsoluteExpirationRelativeToNow = expiry;
|
||||
var val = await createFactory();
|
||||
return val;
|
||||
});
|
||||
|
||||
public ValueTask<OneOf<T, None>> GetAsync<T>(TypedKey<T> key)
|
||||
{
|
||||
if (!_cache.TryGetValue(key.Key, out var val) || val is null)
|
||||
return new(new None());
|
||||
|
||||
return new((T)val);
|
||||
}
|
||||
|
||||
public ValueTask<bool> RemoveAsync<T>(TypedKey<T> key)
|
||||
{
|
||||
lock (_cacheLock)
|
||||
{
|
||||
var toReturn = _cache.TryGetValue(key.Key, out var old) && old is not null;
|
||||
_cache.Remove(key.Key);
|
||||
return new(toReturn);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,119 +0,0 @@
|
|||
using OneOf;
|
||||
using OneOf.Types;
|
||||
using StackExchange.Redis;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace EllieBot.Common;
|
||||
|
||||
public sealed class RedisBotCache : IBotCache
|
||||
{
|
||||
private static readonly Type[] _supportedTypes = new[]
|
||||
{
|
||||
typeof(bool), typeof(int), typeof(uint), typeof(long),
|
||||
typeof(ulong), typeof(float), typeof(double),
|
||||
typeof(string), typeof(byte[]), typeof(ReadOnlyMemory<byte>), typeof(Memory<byte>),
|
||||
typeof(RedisValue),
|
||||
};
|
||||
|
||||
private static readonly JsonSerializerOptions _opts = new()
|
||||
{
|
||||
PropertyNameCaseInsensitive = true,
|
||||
NumberHandling = JsonNumberHandling.AllowReadingFromString,
|
||||
AllowTrailingCommas = true,
|
||||
IgnoreReadOnlyProperties = false,
|
||||
};
|
||||
private readonly ConnectionMultiplexer _conn;
|
||||
|
||||
public RedisBotCache(ConnectionMultiplexer conn)
|
||||
{
|
||||
_conn = conn;
|
||||
}
|
||||
|
||||
public async ValueTask<bool> AddAsync<T>(TypedKey<T> key, T value, TimeSpan? expiry = null, bool overwrite = true)
|
||||
{
|
||||
// if a null value is passed, remove the key
|
||||
if (value is null)
|
||||
{
|
||||
await RemoveAsync(key);
|
||||
return false;
|
||||
}
|
||||
|
||||
var db = _conn.GetDatabase();
|
||||
RedisValue val = IsSupportedType(typeof(T))
|
||||
? RedisValue.Unbox(value)
|
||||
: JsonSerializer.Serialize(value, _opts);
|
||||
|
||||
var success = await db.StringSetAsync(key.Key,
|
||||
val,
|
||||
expiry: expiry,
|
||||
when: overwrite ? When.Always : When.NotExists);
|
||||
|
||||
return success;
|
||||
}
|
||||
|
||||
public bool IsSupportedType(Type type)
|
||||
{
|
||||
if (type.IsGenericType)
|
||||
{
|
||||
var typeDef = type.GetGenericTypeDefinition();
|
||||
if (typeDef == typeof(Nullable<>))
|
||||
return IsSupportedType(type.GenericTypeArguments[0]);
|
||||
}
|
||||
|
||||
foreach (var t in _supportedTypes)
|
||||
{
|
||||
if (type == t)
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public async ValueTask<OneOf<T, None>> GetAsync<T>(TypedKey<T> key)
|
||||
{
|
||||
var db = _conn.GetDatabase();
|
||||
var val = await db.StringGetAsync(key.Key);
|
||||
if (val == default)
|
||||
return new None();
|
||||
|
||||
if (IsSupportedType(typeof(T)))
|
||||
return (T)((IConvertible)val).ToType(typeof(T), null);
|
||||
|
||||
return JsonSerializer.Deserialize<T>(val.ToString(), _opts)!;
|
||||
}
|
||||
|
||||
public async ValueTask<bool> RemoveAsync<T>(TypedKey<T> key)
|
||||
{
|
||||
var db = _conn.GetDatabase();
|
||||
|
||||
return await db.KeyDeleteAsync(key.Key);
|
||||
}
|
||||
|
||||
public async ValueTask<T?> GetOrAddAsync<T>(TypedKey<T> key, Func<Task<T?>> createFactory, TimeSpan? expiry = null)
|
||||
{
|
||||
var result = await GetAsync(key);
|
||||
|
||||
return await result.Match<Task<T?>>(
|
||||
v => Task.FromResult<T?>(v),
|
||||
async _ =>
|
||||
{
|
||||
var factoryValue = await createFactory();
|
||||
|
||||
if (factoryValue is null)
|
||||
return default;
|
||||
|
||||
await AddAsync(key, factoryValue, expiry);
|
||||
|
||||
// get again to make sure it's the cached value
|
||||
// and not the late factory value, in case there's a race condition
|
||||
|
||||
var newResult = await GetAsync(key);
|
||||
|
||||
// it's fine to do this, it should blow up if something went wrong.
|
||||
return newResult.Match<T?>(
|
||||
v => v,
|
||||
_ => default);
|
||||
});
|
||||
}
|
||||
}
|
|
@ -1,185 +0,0 @@
|
|||
#nullable disable
|
||||
using Cloneable;
|
||||
using EllieBot.Common.Yml;
|
||||
using SixLabors.ImageSharp.PixelFormats;
|
||||
using System.Globalization;
|
||||
using YamlDotNet.Core;
|
||||
using YamlDotNet.Serialization;
|
||||
|
||||
namespace EllieBot.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://cdn.elliebot.net/Ellie.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.gg/etQdZxSyEH"",
|
||||
""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 EllieBot.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,82 +0,0 @@
|
|||
namespace EllieBot;
|
||||
|
||||
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 EllieBot;
|
||||
|
||||
/// <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 EllieBot;
|
||||
|
||||
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 EllieBot;
|
||||
|
||||
public interface IEllieInteractionService
|
||||
{
|
||||
public EllieInteraction Create<T>(
|
||||
ulong userId,
|
||||
SimpleInteraction<T> inter);
|
||||
}
|
|
@ -1,20 +0,0 @@
|
|||
namespace EllieBot;
|
||||
|
||||
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,14 +0,0 @@
|
|||
using System.Globalization;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace EllieBot.Common.JsonConverters;
|
||||
|
||||
public class CultureInfoConverter : JsonConverter<CultureInfo>
|
||||
{
|
||||
public override CultureInfo Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
||||
=> new(reader.GetString() ?? "en-US");
|
||||
|
||||
public override void Write(Utf8JsonWriter writer, CultureInfo value, JsonSerializerOptions options)
|
||||
=> writer.WriteStringValue(value.Name);
|
||||
}
|
|
@ -1,14 +0,0 @@
|
|||
using SixLabors.ImageSharp.PixelFormats;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace EllieBot.Common.JsonConverters;
|
||||
|
||||
public class Rgba32Converter : JsonConverter<Rgba32>
|
||||
{
|
||||
public override Rgba32 Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
||||
=> Rgba32.ParseHex(reader.GetString());
|
||||
|
||||
public override void Write(Utf8JsonWriter writer, Rgba32 value, JsonSerializerOptions options)
|
||||
=> writer.WriteStringValue(value.ToHex());
|
||||
}
|
|
@ -1,76 +0,0 @@
|
|||
#nullable disable
|
||||
|
||||
[DontAddToIocContainer]
|
||||
public sealed class BehaviorAdapter : ICustomBehavior
|
||||
{
|
||||
private readonly WeakReference<Canary> _canaryWr;
|
||||
private readonly IMarmaladeStrings _strings;
|
||||
private readonly IServiceProvider _services;
|
||||
private readonly string _name;
|
||||
|
||||
// unused
|
||||
public int Priority
|
||||
=> 0;
|
||||
|
||||
public BehaviorAdapter(WeakReference<Canary> canaryWr, IMarmaladeStrings strings, IServiceProvider services)
|
||||
{
|
||||
_canaryWr = canaryWr;
|
||||
_strings = strings;
|
||||
_services = services;
|
||||
|
||||
_name = canaryWr.TryGetTarget(out var canary)
|
||||
? $"canary/{canary.GetType().Name}"
|
||||
: "unknown";
|
||||
}
|
||||
|
||||
public async Task<bool> ExecPreCommandAsync(ICommandContext context, string moduleName, CommandInfo command)
|
||||
{
|
||||
if (!_canaryWr.TryGetTarget(out var canary))
|
||||
return false;
|
||||
|
||||
return await canary.ExecPreCommandAsync(ContextAdapterFactory.CreateNew(context, _strings, _services),
|
||||
moduleName,
|
||||
command.Name);
|
||||
}
|
||||
|
||||
public async Task<bool> ExecOnMessageAsync(IGuild? guild, IUserMessage msg)
|
||||
{
|
||||
if (!_canaryWr.TryGetTarget(out var canary))
|
||||
return false;
|
||||
|
||||
return await canary.ExecOnMessageAsync(guild, msg);
|
||||
}
|
||||
|
||||
public async Task<string?> TransformInput(
|
||||
IGuild guild,
|
||||
IMessageChannel channel,
|
||||
IUser user,
|
||||
string input)
|
||||
{
|
||||
if (!_canaryWr.TryGetTarget(out var canary))
|
||||
return null;
|
||||
|
||||
return await canary.ExecInputTransformAsync(guild, channel, user, input);
|
||||
}
|
||||
|
||||
public async Task ExecOnNoCommandAsync(IGuild? guild, IUserMessage msg)
|
||||
{
|
||||
if (!_canaryWr.TryGetTarget(out var canary))
|
||||
return;
|
||||
|
||||
await canary.ExecOnNoCommandAsync(guild, msg);
|
||||
}
|
||||
|
||||
public async ValueTask ExecPostCommandAsync(ICommandContext context, string moduleName, string commandName)
|
||||
{
|
||||
if (!_canaryWr.TryGetTarget(out var canary))
|
||||
return;
|
||||
|
||||
await canary.ExecPostCommandAsync(ContextAdapterFactory.CreateNew(context, _strings, _services),
|
||||
moduleName,
|
||||
commandName);
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
=> _name;
|
||||
}
|
|
@ -1,7 +0,0 @@
|
|||
internal class ContextFactory
|
||||
{
|
||||
public static AnyContext CreateNew(ICommandContext context, IMarmaladeStrings strings, IServiceProvider services)
|
||||
=> context.Guild is null
|
||||
? new DmContextAdapter(context, strings, services)
|
||||
: new GuildContextAdapter(context, strings, services);
|
||||
}
|
|
@ -1,49 +0,0 @@
|
|||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
public sealed class DmContextAdapter : DmContext
|
||||
{
|
||||
public override IMarmaladeStrings Strings { get; }
|
||||
public override IDMChannel Channel { get; }
|
||||
public override IUserMessage Message { get; }
|
||||
public override ISelfUser Bot { get; }
|
||||
public override IUser User
|
||||
=> Message.Author;
|
||||
|
||||
private readonly IServiceProvider _services;
|
||||
private readonly Lazy<IEmbedBuilderService> _ebs;
|
||||
private readonly Lazy<IBotStrings> _botStrings;
|
||||
private readonly Lazy<ILocalization> _localization;
|
||||
|
||||
public DmContextAdapter(ICommandContext ctx, IMarmaladeStrings strings, IServiceProvider services)
|
||||
{
|
||||
if (ctx is not { Channel: IDMChannel ch})
|
||||
{
|
||||
throw new ArgumentException("Can't use non-dm context to create DmContextAdapter", nameof(ctx));
|
||||
}
|
||||
|
||||
Strings = strings;
|
||||
|
||||
_services = services;
|
||||
|
||||
Channel = ch;
|
||||
Message = ctx.Message;
|
||||
Bot = ctx.Client.CurrentUser;
|
||||
|
||||
_ebs = new(_services.GetRequiredService<IEmbedBuilderService>());
|
||||
_botStrings = new(_services.GetRequiredService<IBotStrings>);
|
||||
_localization = new(_services.GetRequiredService<ILocalization>());
|
||||
}
|
||||
|
||||
public override IEmbedBuilder Embed()
|
||||
=> _ebs.Value.Create();
|
||||
|
||||
public override string GetText(string key, object[]? args = null)
|
||||
{
|
||||
var cultureInfo = _localization.Value.GetCultureInfo(default(ulong?));
|
||||
var output = Strings.GetText(key, cultureInfo, args ?? Array.Empty<object>());
|
||||
if (!string.IsNullOrEmpty(output))
|
||||
return output;
|
||||
|
||||
return _botStrings.Value.GetText(key, cultureInfo, args);
|
||||
}
|
||||
}
|
|
@ -1,31 +0,0 @@
|
|||
namespace Ellie.Marmalade.Adapters;
|
||||
|
||||
public class FilterAdapter : PreconditionAttribute
|
||||
{
|
||||
private readonly FilterAttribute _filterAttribute;
|
||||
private readonly IMarmaladeStrings _strings;
|
||||
|
||||
public FilterAdapter(FilterAttribute filterAttribute,
|
||||
IMarmaladeStrings strings)
|
||||
{
|
||||
_filterAttribute = filterAttribute;
|
||||
_strings = strings;
|
||||
}
|
||||
|
||||
public override async Task<PreconditionResult> CheckPermissionsAsync(
|
||||
ICommandContext context,
|
||||
CommandInfo command,
|
||||
IServiceProvider services)
|
||||
{
|
||||
var marmaladeContext = ContextAdapterFactory.CreateNew(context,
|
||||
_strings,
|
||||
services);
|
||||
|
||||
var result = await _filterAttribute.CheckAsync(marmaladeContext);
|
||||
|
||||
if (!result)
|
||||
return PreconditionResult.FromError($"Precondition '{_filterAttribute.GetType().Name}' failed.");
|
||||
|
||||
return PreconditionResult.FromSuccess();
|
||||
}
|
||||
}
|
|
@ -1,53 +0,0 @@
|
|||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
public sealed class GuildContextAdapter : GuildContext
|
||||
{
|
||||
private readonly IServiceProvider _services;
|
||||
private readonly ICommandContext _ctx;
|
||||
private readonly Lazy<IEmbedBuilderService> _ebs;
|
||||
private readonly Lazy<IBotStrings> _botStrings;
|
||||
private readonly Lazy<ILocalization> _localization;
|
||||
|
||||
public override IMarmaladeStrings Strings { get; }
|
||||
public override IGuild Guild { get; }
|
||||
public override ITextChannel Channel { get; }
|
||||
public override ISelfUser Bot { get; }
|
||||
public override IUserMessage Message
|
||||
=> _ctx.Message;
|
||||
|
||||
public override IGuildUser User { get; }
|
||||
|
||||
public override IEmbedBuilder Embed()
|
||||
=> _ebs.Value.Create();
|
||||
|
||||
public GuildContextAdapter(ICommandContext ctx, IMarmaladeStrings strings, IServiceProvider services)
|
||||
{
|
||||
if (ctx.Guild is not IGuild guild || ctx.Channel is not ITextChannel channel)
|
||||
{
|
||||
throw new ArgumentException("Can't use non-guild context to create GuildContextAdapter", nameof(ctx));
|
||||
}
|
||||
|
||||
Strings = strings;
|
||||
User = (IGuildUser)ctx.User;
|
||||
Bot = ctx.Client.CurrentUser;
|
||||
|
||||
_services = services;
|
||||
_ebs = new(_services.GetRequiredService<IEmbedBuilderService>());
|
||||
_botStrings = new(_services.GetRequiredService<IBotStrings>);
|
||||
_localization = new(_services.GetRequiredService<ILocalization>());
|
||||
|
||||
(_ctx, Guild, Channel) = (ctx, guild, channel);
|
||||
}
|
||||
|
||||
public override string GetText(string key, object[]? args = null)
|
||||
{
|
||||
args ??= Array.Empty<object>();
|
||||
|
||||
var cultureInfo = _localization.Value.GetCultureInfo(_ctx.Guild.Id);
|
||||
var output = Strings.GetText(key, cultureInfo, args);
|
||||
if (!string.IsNullOrWhiteSpace(output))
|
||||
return output;
|
||||
|
||||
return _botStrings.Value.GetText(key, cultureInfo, args);
|
||||
}
|
||||
}
|
|
@ -1,32 +0,0 @@
|
|||
public sealed class ParamParserAdapter<T> : TypeReader
|
||||
{
|
||||
private readonly ParamParser<T> _parser;
|
||||
private readonly IMarmaladeStrings _strings;
|
||||
private readonly IServiceProvider _services;
|
||||
|
||||
public ParamParserAdapter(ParamParser<T> parser,
|
||||
IMarmaladeStrings strings,
|
||||
IServiceProvider services)
|
||||
{
|
||||
_parser = parser;
|
||||
_strings = strings;
|
||||
_services = services;
|
||||
}
|
||||
|
||||
public override async Task<Discord.Commands.TypeReaderResult> ReadAsync(
|
||||
ICommandContext context,
|
||||
string input,
|
||||
IServiceProvider services)
|
||||
{
|
||||
var marmaladeContext = ContextAdapterFactory.CreateNew(context,
|
||||
_strings,
|
||||
_services);
|
||||
|
||||
var result = await _parser.TryParseAsync(marmaladeContext, input);
|
||||
|
||||
if (result.IsSuccess)
|
||||
return Discord.Commands.TypeReaderResult.FromSuccess(result.Data);
|
||||
|
||||
return Discord.Commands.TypeReaderResult.FromError(CommandError.Unsuccessful, "Invalid input");
|
||||
}
|
||||
}
|
|
@ -1,27 +0,0 @@
|
|||
namespace Ellie.Marmalade;
|
||||
|
||||
/// <summary>
|
||||
/// Enum specifying in which context the command can be executed
|
||||
/// </summary>
|
||||
public enum CommandContextType
|
||||
{
|
||||
/// <summary>
|
||||
/// Command can only be executed in a guild
|
||||
/// </summary>
|
||||
Guild,
|
||||
|
||||
/// <summary>
|
||||
/// Command can only be executed in DMs
|
||||
/// </summary>
|
||||
Dm,
|
||||
|
||||
/// <summary>
|
||||
/// Command can be executed anywhere
|
||||
/// </summary>
|
||||
Any,
|
||||
|
||||
/// <summary>
|
||||
/// Command can be executed anywhere, and it doesn't require context to be passed to it
|
||||
/// </summary>
|
||||
Unspecified
|
||||
}
|
|
@ -1,8 +0,0 @@
|
|||
namespace Ellie.Marmalade;
|
||||
|
||||
public interface IMarmaladeConfigService
|
||||
{
|
||||
IReadOnlyCollection<string> GetLoadedMarmalades();
|
||||
void AddLoadedMarmalade(string name);
|
||||
void RemoveLoadedMarmalade(string name);
|
||||
}
|
|
@ -1,20 +0,0 @@
|
|||
#nullable disable
|
||||
using Cloneable;
|
||||
using EllieBot.Common.Yml;
|
||||
|
||||
namespace Ellie.Marmalade;
|
||||
|
||||
[Cloneable]
|
||||
public sealed partial class MarmaladeConfig : ICloneable<MarmaladeConfig>
|
||||
{
|
||||
[Comment(@"DO NOT CHANGE")]
|
||||
public int Version { get; set; } = 1;
|
||||
|
||||
[Comment("List of marmalades automatically loaded at startup")]
|
||||
public List<string>? Loaded { get; set; }
|
||||
|
||||
public MarmaladeConfig()
|
||||
{
|
||||
Loaded = new();
|
||||
}
|
||||
}
|
|
@ -1,45 +0,0 @@
|
|||
using EllieBot.Common.Configs;
|
||||
|
||||
namespace Ellie.Marmalade;
|
||||
|
||||
public sealed class MarmaladeConfigService : ConfigServiceBase<MarmaladeConfig>, IMarmaladeConfigService
|
||||
{
|
||||
private const string FILE_PATH = "data/marmalades/marmalade.yml";
|
||||
private static readonly TypedKey<MarmaladeConfig> _changeKey = new("config.marmalade.updated");
|
||||
|
||||
public override string Name
|
||||
=> "marmalade";
|
||||
|
||||
public MarmaladeConfigService(
|
||||
IConfigSeria serializer,
|
||||
IPubSub pubSub)
|
||||
: base(FILE_PATH, serializer, pubSub, _changeKey)
|
||||
{
|
||||
}
|
||||
|
||||
public IReadOnlyCollection<string> GetLoadedMarmalades()
|
||||
=> Data.Loaded?.ToList() ?? new List<string>();
|
||||
|
||||
public void AddLoadedMarmalade(string name)
|
||||
{
|
||||
ModifyConfig(conf =>
|
||||
{
|
||||
if (conf.Loaded is null)
|
||||
conf.Loaded = new();
|
||||
|
||||
if (!conf.Loaded.Contains(name))
|
||||
conf.Loaded.Add(name);
|
||||
});
|
||||
}
|
||||
|
||||
public void RemoveLoadedMarmalade(string name)
|
||||
{
|
||||
ModifyConfig(conf =>
|
||||
{
|
||||
if (conf.Loaded is null)
|
||||
conf.Loaded = new();
|
||||
|
||||
conf.Loaded.Remove(name);
|
||||
});
|
||||
}
|
||||
}
|
|
@ -1,23 +0,0 @@
|
|||
using System.Globalization;
|
||||
|
||||
namespace Ellie.Marmalade;
|
||||
|
||||
public interface IMarmaladeLoaderService
|
||||
{
|
||||
Task<MarmaladeLoadResult> LoadMarmaladeAsync(string marmaladeName);
|
||||
Task<MarmaladeUnloadResult> UnloadMarmaladeAsync(string marmaladeName);
|
||||
string GetCommandDescription(string marmamaleName, string commandName, CultureInfo culture);
|
||||
string[] GetCommandExampleArgs(string marmamaleName, 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,
|
||||
IReadOnlyCollection<CanaryCommandStats> Commands);
|
||||
|
||||
public sealed record CanaryCommandStats(string Name);
|
|
@ -1,36 +0,0 @@
|
|||
using System.Reflection;
|
||||
using System.Runtime.Loader;
|
||||
|
||||
namespace Ellie.Marmalade;
|
||||
|
||||
public sealed class MarmaladeAssemblyLoadContext : AssemblyLoadContext
|
||||
{
|
||||
private readonly AssemblyDependencyResolver _depResolver;
|
||||
|
||||
public MarmaladeAssemblyLoadContext(string pluginPath) : base(isCollectible: true)
|
||||
{
|
||||
_depResolver = new(pluginPath);
|
||||
}
|
||||
|
||||
protected override Assembly? Load(AssemblyName assemblyName)
|
||||
{
|
||||
var assemblyPath = _depResolver.ResolveAssemblyToPath(assemblyName);
|
||||
if (assemblyPath != null)
|
||||
{
|
||||
return LoadFromAssemblyPath(assemblyPath);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
protected override IntPtr LoadUnmanagedDll(string unmanagedDllName)
|
||||
{
|
||||
var libraryPath = _depResolver.ResolveUnmanagedDllToPath(unmanagedDllName);
|
||||
if (libraryPath != null)
|
||||
{
|
||||
return LoadUnmanagedDllFromPath(libraryPath);
|
||||
}
|
||||
|
||||
return IntPtr.Zero;
|
||||
}
|
||||
}
|
|
@ -1,917 +0,0 @@
|
|||
using Discord.Commands.Builders;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Ellie.Marmalade.Adapters;
|
||||
using EllieBot.Common.ModuleBehaviors;
|
||||
using System.Collections.Immutable;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Globalization;
|
||||
using System.Reflection;
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
namespace Ellie.Marmalade;
|
||||
|
||||
// ReSharper disable RedundantAssignment
|
||||
public sealed class MarmaladeLoaderService : IMarmaladeLoaderService, IReadyExecutor, IEService
|
||||
{
|
||||
private readonly CommandService _cmdService;
|
||||
private readonly IServiceProvider _botServices;
|
||||
private readonly IBehaviorHandler _behHandler;
|
||||
private readonly IPubSub _pubSub;
|
||||
private readonly IMarmaladeConfigService _marmaladeConfig;
|
||||
|
||||
private readonly ConcurrentDictionary<string, ResolvedMarmalade> _resolved = new();
|
||||
#pragma warning disable IDE0090 // Use 'new(...)'
|
||||
private readonly SemaphoreSlim _lock = new SemaphoreSlim(1, 1);
|
||||
#pragma warning restore IDE0090 // Use 'new(...)'
|
||||
|
||||
private readonly TypedKey<string> _loadKey = new("marmalade:load");
|
||||
private readonly TypedKey<string> _unloadKey = new("marmalade:unload");
|
||||
|
||||
private readonly TypedKey<string> _stringsReload = new("marmalade:reload_strings");
|
||||
|
||||
private const string BASE_DIR = "data/marmalades";
|
||||
|
||||
public MarmaladeLoaderService(CommandService cmdService,
|
||||
IServiceProvider botServices,
|
||||
IBehaviorHandler behHandler,
|
||||
IPubSub pubSub,
|
||||
IMarmaladeConfigService marmaladeConfig)
|
||||
{
|
||||
_cmdService = cmdService;
|
||||
_botServices = botServices;
|
||||
_behHandler = behHandler;
|
||||
_pubSub = pubSub;
|
||||
_marmaladeConfig = marmaladeConfig;
|
||||
|
||||
// has to be done this way to support this feature on sharded bots
|
||||
_pubSub.Sub(_loadKey, async name => await InternalLoadAsync(name));
|
||||
_pubSub.Sub(_unloadKey, async name => await InternalUnloadAsync(name));
|
||||
|
||||
_pubSub.Sub(_stringsReload, async _ => await ReloadStringsInternal());
|
||||
}
|
||||
|
||||
public IReadOnlyCollection<string> GetAllMarmalades()
|
||||
{
|
||||
if (!Directory.Exists(BASE_DIR))
|
||||
return Array.Empty<string>();
|
||||
|
||||
return Directory.GetDirectories(BASE_DIR)
|
||||
.Select(x => Path.GetRelativePath(BASE_DIR, x))
|
||||
.ToArray();
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.NoInlining)]
|
||||
public IReadOnlyCollection<MarmaladeStats> GetLoadedMarmalades(CultureInfo? culture)
|
||||
{
|
||||
var toReturn = new List<MarmaladeStats>(_resolved.Count);
|
||||
foreach (var (name, resolvedData) in _resolved)
|
||||
{
|
||||
var canaries = new List<CanaryStats>(resolvedData.CanaryInfos.Count);
|
||||
|
||||
foreach (var canaryInfos in resolvedData.CanaryInfos.Concat(resolvedData.CanaryInfos.SelectMany(x => x.Subcanaries)))
|
||||
{
|
||||
var commands = new List<CanaryCommandStats>();
|
||||
|
||||
foreach (var command in canaryInfos.Commands)
|
||||
{
|
||||
commands.Add(new CanaryCommandStats(command.Aliases.First()));
|
||||
}
|
||||
|
||||
canaries.Add(new CanaryStats(canaryInfos.Name, commands));
|
||||
}
|
||||
|
||||
toReturn.Add(new MarmaladeStats(name, resolvedData.Strings.GetDescription(culture), canaries));
|
||||
}
|
||||
return toReturn;
|
||||
}
|
||||
|
||||
public async Task OnReadyAsync()
|
||||
{
|
||||
foreach (var name in _marmaladeConfig.GetLoadedMarmalades())
|
||||
{
|
||||
var result = await InternalLoadAsync(name);
|
||||
if (result != MarmaladeLoadResult.Success)
|
||||
Log.Warning("Unable to load '{MarmaladeName}' marmalade", name);
|
||||
else
|
||||
Log.Warning("Loaded marmalade '{MarmaladeName}'", name);
|
||||
}
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.NoInlining)]
|
||||
public async Task<MarmaladeLoadResult> LoadMarmaladeAsync(string marmaladeName)
|
||||
{
|
||||
// try loading on this shard first to see if it works
|
||||
var res = await InternalLoadAsync(marmaladeName);
|
||||
if (res == MarmaladeLoadResult.Success)
|
||||
{
|
||||
// if it does publish it so that other shards can load the medusa too
|
||||
// this method will be ran twice on this shard but it doesn't matter as
|
||||
// the second attempt will be ignored
|
||||
await _pubSub.Pub(_loadKey, marmaladeName);
|
||||
}
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.NoInlining)]
|
||||
public async Task<MarmaladeUnloadResult> UnloadMarmaladeAsync(string marmaladeName)
|
||||
{
|
||||
var res = await InternalUnloadAsync(marmaladeName);
|
||||
if (res == MarmaladeUnloadResult.Success)
|
||||
{
|
||||
await _pubSub.Pub(_unloadKey, marmaladeName);
|
||||
}
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.NoInlining)]
|
||||
public string[] GetCommandExampleArgs(string marmaladeName, string commandName, CultureInfo culture)
|
||||
{
|
||||
if (!_resolved.TryGetValue(marmaladeName, out var data))
|
||||
return Array.Empty<string>();
|
||||
|
||||
return data.Strings.GetCommandStrings(commandName, culture).Args
|
||||
?? data.CanaryInfos
|
||||
.SelectMany(x => x.Commands)
|
||||
.FirstOrDefault(x => x.Aliases.Any(alias
|
||||
=> alias.Equals(commandName, StringComparison.InvariantCultureIgnoreCase)))
|
||||
?.OptionalStrings
|
||||
.Args
|
||||
?? new[] { string.Empty };
|
||||
}
|
||||
|
||||
public Task ReloadStrings()
|
||||
=> _pubSub.Pub(_stringsReload, true);
|
||||
|
||||
[MethodImpl(MethodImplOptions.NoInlining)]
|
||||
private void ReloadStringsSync()
|
||||
{
|
||||
foreach (var resolved in _resolved.Values)
|
||||
{
|
||||
resolved.Strings.Reload();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task ReloadStringsInternal()
|
||||
{
|
||||
await _lock.WaitAsync();
|
||||
try
|
||||
{
|
||||
ReloadStringsSync();
|
||||
}
|
||||
finally
|
||||
{
|
||||
_lock.Release();
|
||||
}
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.NoInlining)]
|
||||
public string GetCommandDescription(string marmaladeName, string commandName, CultureInfo culture)
|
||||
{
|
||||
if (!_resolved.TryGetValue(marmaladeName, out var data))
|
||||
return string.Empty;
|
||||
|
||||
return data.Strings.GetCommandStrings(commandName, culture).Desc
|
||||
?? data.CanaryInfos
|
||||
.SelectMany(x => x.Commands)
|
||||
.FirstOrDefault(x => x.Aliases.Any(alias
|
||||
=> alias.Equals(commandName, StringComparison.InvariantCultureIgnoreCase)))
|
||||
?.OptionalStrings
|
||||
.Desc
|
||||
?? string.Empty;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.NoInlining)]
|
||||
private async ValueTask<MarmaladeLoadResult> InternalLoadAsync(string name)
|
||||
{
|
||||
if (_resolved.ContainsKey(name))
|
||||
return MarmaladeLoadResult.AlreadyLoaded;
|
||||
|
||||
var safeName = Uri.EscapeDataString(name);
|
||||
|
||||
await _lock.WaitAsync();
|
||||
try
|
||||
{
|
||||
if (LoadAssemblyInternal(safeName,
|
||||
out var ctx,
|
||||
out var canaryData,
|
||||
out var services,
|
||||
out var strings,
|
||||
out var typeReaders))
|
||||
{
|
||||
var moduleInfos = new List<ModuleInfo>();
|
||||
|
||||
LoadTypeReadersInternal(typeReaders);
|
||||
|
||||
foreach (var point in canaryData)
|
||||
{
|
||||
try
|
||||
{
|
||||
// initialize canary and subcanaries
|
||||
await point.Instance.InitializeAsync();
|
||||
foreach (var sub in point.Subcanaries)
|
||||
{
|
||||
await sub.Instance.InitializeAsync();
|
||||
}
|
||||
|
||||
var module = await LoadModuleInternalAsync(name, point, strings, services);
|
||||
moduleInfos.Add(module);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Warning(ex,
|
||||
"Error loading canary {CanaryName}",
|
||||
point.Name);
|
||||
}
|
||||
}
|
||||
|
||||
var execs = GetExecsInternal(canaryData, strings, services);
|
||||
await _behHandler.AddRangeAsync(execs);
|
||||
|
||||
_resolved[name] = new(LoadContext: ctx,
|
||||
ModuleInfos: moduleInfos.ToImmutableArray(),
|
||||
CanaryInfos: canaryData.ToImmutableArray(),
|
||||
strings,
|
||||
typeReaders,
|
||||
execs)
|
||||
{
|
||||
Services = services
|
||||
};
|
||||
|
||||
|
||||
services = null;
|
||||
_marmaladeConfig.AddLoadedMarmalade(safeName);
|
||||
return MarmaladeLoadResult.Success;
|
||||
}
|
||||
|
||||
return MarmaladeLoadResult.Empty;
|
||||
}
|
||||
catch (Exception ex) when (ex is FileNotFoundException or BadImageFormatException)
|
||||
{
|
||||
return MarmaladeLoadResult.NotFound;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Error(ex, "An error occurred loading a marmalade");
|
||||
return MarmaladeLoadResult.UnknownError;
|
||||
}
|
||||
finally
|
||||
{
|
||||
_lock.Release();
|
||||
}
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.NoInlining)]
|
||||
private IReadOnlyCollection<ICustomBehavior> GetExecsInternal(IReadOnlyCollection<CanaryInfo> canaryData, IMarmaladeStrings strings, IServiceProvider services)
|
||||
{
|
||||
var behs = new List<ICustomBehavior>();
|
||||
foreach (var canary in canaryData)
|
||||
{
|
||||
behs.Add(new BehaviorAdapter(new(canary.Instance), strings, services));
|
||||
|
||||
foreach (var sub in canary.Subcanaries)
|
||||
{
|
||||
behs.Add(new BehaviorAdapter(new(sub.Instance), strings, services));
|
||||
}
|
||||
}
|
||||
|
||||
return behs;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.NoInlining)]
|
||||
private void LoadTypeReadersInternal(Dictionary<Type, TypeReader> typeReaders)
|
||||
{
|
||||
var notAddedTypeReaders = new List<Type>();
|
||||
foreach (var (type, typeReader) in typeReaders)
|
||||
{
|
||||
// if type reader for this type already exists, it will not be replaced
|
||||
if (_cmdService.TypeReaders.Contains(type))
|
||||
{
|
||||
notAddedTypeReaders.Add(type);
|
||||
continue;
|
||||
}
|
||||
|
||||
_cmdService.AddTypeReader(type, typeReader);
|
||||
}
|
||||
|
||||
// remove the ones that were not added
|
||||
// to prevent them from being unloaded later
|
||||
// as they didn't come from this marmalade
|
||||
foreach (var toRemove in notAddedTypeReaders)
|
||||
{
|
||||
typeReaders.Remove(toRemove);
|
||||
}
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.NoInlining)]
|
||||
private bool LoadAssemblyInternal(
|
||||
string safeName,
|
||||
[NotNullWhen(true)] out WeakReference<MarmaladeAssemblyLoadContext>? ctxWr,
|
||||
[NotNullWhen(true)] out IReadOnlyCollection<CanaryInfo>? canaryData,
|
||||
out IServiceProvider services,
|
||||
out IMarmaladeStrings strings,
|
||||
out Dictionary<Type, TypeReader> typeReaders)
|
||||
{
|
||||
ctxWr = null;
|
||||
canaryData = null;
|
||||
|
||||
var path = $"{BASE_DIR}/{safeName}/{safeName}.dll";
|
||||
strings = MarmaladeStrings.CreateDefault($"{BASE_DIR}/{safeName}");
|
||||
var ctx = new MarmaladeAssemblyLoadContext(Path.GetDirectoryName(path));
|
||||
var a = ctx.LoadFromAssemblyPath(Path.GetFullPath(path));
|
||||
var sis = LoadCanariesFromAssembly(a, out services);
|
||||
typeReaders = LoadTypeReadersFromAssembly(a, strings, services);
|
||||
|
||||
if (sis.Count == 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
ctxWr = new(ctx);
|
||||
canaryData = sis;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
private static readonly Type _paramParserType = typeof(ParamParser<>);
|
||||
|
||||
[MethodImpl(MethodImplOptions.NoInlining)]
|
||||
private Dictionary<Type, TypeReader> LoadTypeReadersFromAssembly(
|
||||
Assembly assembly,
|
||||
IMarmaladeStrings strings,
|
||||
IServiceProvider services)
|
||||
{
|
||||
var paramParsers = assembly.GetExportedTypes()
|
||||
.Where(x => x.IsClass
|
||||
&& !x.IsAbstract
|
||||
&& x.BaseType is not null
|
||||
&& x.BaseType.IsGenericType
|
||||
&& x.BaseType.GetGenericTypeDefinition() == _paramParserType);
|
||||
|
||||
var typeReaders = new Dictionary<Type, TypeReader>();
|
||||
foreach (var parserType in paramParsers)
|
||||
{
|
||||
var parserObj = ActivatorUtilities.CreateInstance(services, parserType);
|
||||
|
||||
var targetType = parserType.BaseType!.GetGenericArguments()[0];
|
||||
var typeReaderInstance = (TypeReader)Activator.CreateInstance(
|
||||
typeof(ParamParserAdapter<>).MakeGenericType(targetType),
|
||||
args: new[] { parserObj, strings, services })!;
|
||||
|
||||
typeReaders.Add(targetType, typeReaderInstance);
|
||||
}
|
||||
|
||||
return typeReaders;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.NoInlining)]
|
||||
private async Task<ModuleInfo> LoadModuleInternalAsync(string marmaladeName, CanaryInfo canaryInfo, IMarmaladeStrings strings, IServiceProvider services)
|
||||
{
|
||||
var module = await _cmdService.CreateModuleAsync(canaryInfo.Instance.Prefix,
|
||||
CreateModuleFactory(marmaladeName, canaryInfo, strings, services));
|
||||
|
||||
return module;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.NoInlining)]
|
||||
private Action<ModuleBuilder> CreateModuleFactory(
|
||||
string marmaladeName,
|
||||
CanaryInfo canaryInfo,
|
||||
IMarmaladeStrings strings,
|
||||
IServiceProvider marmaladeServices)
|
||||
=> mb =>
|
||||
{
|
||||
var m = mb.WithName(canaryInfo.Name);
|
||||
|
||||
foreach (var f in canaryInfo.Filters)
|
||||
{
|
||||
m.AddPrecondition(new FilterAdapter(f, strings));
|
||||
}
|
||||
|
||||
foreach (var cmd in canaryInfo.Commands)
|
||||
{
|
||||
m.AddCommand(cmd.Aliases.First(),
|
||||
CreateCallback(cmd.ContextType,
|
||||
new(canaryInfo),
|
||||
new(cmd),
|
||||
new(marmaladeServices),
|
||||
strings),
|
||||
CreateCommandFactory(marmaladeName, cmd, strings));
|
||||
}
|
||||
|
||||
foreach (var subInfo in canaryInfo.Subcanaries)
|
||||
m.AddModule(subInfo.Instance.Prefix, CreateModuleFactory(marmaladeName, subInfo, strings, marmaladeServices));
|
||||
};
|
||||
|
||||
#pragma warning disable IDE0090 // Use 'new(...)'
|
||||
private static readonly RequireContextAttribute _reqGuild = new RequireContextAttribute(ContextType.Guild);
|
||||
private static readonly RequireContextAttribute _reqDm = new RequireContextAttribute(ContextType.DM);
|
||||
#pragma warning restore IDE0090 // Use 'new(...)'
|
||||
private Action<CommandBuilder> CreateCommandFactory(string marmaladeName, CanaryCommandData cmd, IMarmaladeStrings strings)
|
||||
=> (cb) =>
|
||||
{
|
||||
cb.AddAliases(cmd.Aliases.Skip(1).ToArray());
|
||||
|
||||
if (cmd.ContextType == CommandContextType.Guild)
|
||||
cb.AddPrecondition(_reqGuild);
|
||||
else if (cmd.ContextType == CommandContextType.Dm)
|
||||
cb.AddPrecondition(_reqDm);
|
||||
|
||||
foreach (var f in cmd.Filters)
|
||||
cb.AddPrecondition(new FilterAdapter(f, strings));
|
||||
|
||||
foreach (var ubp in cmd.UserAndBotPerms)
|
||||
{
|
||||
if (ubp is user_permAttribute up)
|
||||
{
|
||||
if (up.GuildPerm is { } gp)
|
||||
cb.AddPrecondition(new UserPermAttribute(gp));
|
||||
else if (up.ChannelPerm is { } cp)
|
||||
cb.AddPrecondition(new UserPermAttribute(cp));
|
||||
}
|
||||
else if (ubp is bot_permAttribute bp)
|
||||
{
|
||||
if (bp.GuildPerm is { } gp)
|
||||
cb.AddPrecondition(new BotPermAttribute(gp));
|
||||
else if (bp.ChannelPerm is { } cp)
|
||||
cb.AddPrecondition(new BotPermAttribute(cp));
|
||||
}
|
||||
else if (ubp is bot_owner_onlyAttribute)
|
||||
{
|
||||
cb.AddPrecondition(new OwnerOnlyAttribute());
|
||||
}
|
||||
}
|
||||
|
||||
cb.WithPriority(cmd.Priority);
|
||||
|
||||
// using summary to save method name
|
||||
// method name is used to retrieve desc/usages
|
||||
cb.WithRemarks($"marmalade///{marmaladeName}");
|
||||
cb.WithSummary(cmd.MethodInfo.Name.ToLowerInvariant());
|
||||
|
||||
foreach (var param in cmd.Parameters)
|
||||
{
|
||||
cb.AddParameter(param.Name, param.Type, CreateParamFactory(param));
|
||||
}
|
||||
};
|
||||
|
||||
private Action<ParameterBuilder> CreateParamFactory(ParamData paramData)
|
||||
=> (pb) =>
|
||||
{
|
||||
pb.WithIsMultiple(paramData.IsParams)
|
||||
.WithIsOptional(paramData.IsOptional)
|
||||
.WithIsRemainder(paramData.IsLeftover);
|
||||
|
||||
if (paramData.IsOptional)
|
||||
pb.WithDefault(paramData.DefaultValue);
|
||||
};
|
||||
|
||||
[MethodImpl(MethodImplOptions.NoInlining)]
|
||||
private Func<ICommandContext, object[], IServiceProvider, CommandInfo, Task> CreateCallback(
|
||||
CommandContextType contextType,
|
||||
WeakReference<CanaryInfo> canaryDataWr,
|
||||
WeakReference<CanaryCommandData> canaryCommandDataWr,
|
||||
WeakReference<IServiceProvider> marmaladeServicesWr,
|
||||
IMarmaladeStrings strings)
|
||||
=> async (context, parameters, svcs, _) =>
|
||||
{
|
||||
if (!canaryCommandDataWr.TryGetTarget(out var cmdData)
|
||||
|| !canaryDataWr.TryGetTarget(out var canaryData)
|
||||
|| !marmaladeServicesWr.TryGetTarget(out var marmaladeServices))
|
||||
{
|
||||
Log.Warning("Attempted to run an unloaded canary's command");
|
||||
return;
|
||||
}
|
||||
|
||||
var paramObjs = ParamObjs(contextType, cmdData, parameters, context, svcs, marmaladeServices, strings);
|
||||
|
||||
try
|
||||
{
|
||||
var methodInfo = cmdData.MethodInfo;
|
||||
if (methodInfo.ReturnType == typeof(Task)
|
||||
|| (methodInfo.ReturnType.IsGenericType
|
||||
&& methodInfo.ReturnType.GetGenericTypeDefinition() == typeof(Task<>)))
|
||||
{
|
||||
await (Task)methodInfo.Invoke(canaryData.Instance, paramObjs)!;
|
||||
}
|
||||
else if (methodInfo.ReturnType == typeof(ValueTask))
|
||||
{
|
||||
await ((ValueTask)methodInfo.Invoke(canaryData.Instance, paramObjs)!).AsTask();
|
||||
}
|
||||
else // if (methodInfo.ReturnType == typeof(void))
|
||||
{
|
||||
methodInfo.Invoke(canaryData.Instance, paramObjs);
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
paramObjs = null;
|
||||
cmdData = null;
|
||||
|
||||
canaryData = null;
|
||||
marmaladeServices = null;
|
||||
}
|
||||
};
|
||||
|
||||
[MethodImpl(MethodImplOptions.NoInlining)]
|
||||
private static object[] ParamObjs(
|
||||
CommandContextType contextType,
|
||||
CanaryCommandData cmdData,
|
||||
object[] parameters,
|
||||
ICommandContext context,
|
||||
IServiceProvider svcs,
|
||||
IServiceProvider svcProvider,
|
||||
IMarmaladeStrings strings)
|
||||
{
|
||||
var extraParams = contextType == CommandContextType.Unspecified ? 0 : 1;
|
||||
extraParams += cmdData.InjectedParams.Count;
|
||||
|
||||
var paramObjs = new object[parameters.Length + extraParams];
|
||||
|
||||
var startAt = 0;
|
||||
if (contextType != CommandContextType.Unspecified)
|
||||
{
|
||||
paramObjs[0] = ContextAdapterFactory.CreateNew(context, strings, svcs);
|
||||
|
||||
startAt = 1;
|
||||
}
|
||||
|
||||
for (var i = 0; i < cmdData.InjectedParams.Count; i++)
|
||||
{
|
||||
var svc = svcProvider.GetService(cmdData.InjectedParams[i]);
|
||||
if (svc is null)
|
||||
{
|
||||
throw new ArgumentException($"Cannot inject a service of type {cmdData.InjectedParams[i]}");
|
||||
}
|
||||
|
||||
paramObjs[i + startAt] = svc;
|
||||
|
||||
svc = null;
|
||||
}
|
||||
|
||||
startAt += cmdData.InjectedParams.Count;
|
||||
|
||||
for (var i = 0; i < parameters.Length; i++)
|
||||
paramObjs[startAt + i] = parameters[i];
|
||||
|
||||
return paramObjs;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.NoInlining)]
|
||||
private async Task<MarmaladeUnloadResult> InternalUnloadAsync(string name)
|
||||
{
|
||||
if (!_resolved.Remove(name, out var lsi))
|
||||
return MarmaladeUnloadResult.NotLoaded;
|
||||
|
||||
await _lock.WaitAsync();
|
||||
try
|
||||
{
|
||||
UnloadTypeReaders(lsi.TypeReaders);
|
||||
|
||||
foreach (var mi in lsi.ModuleInfos)
|
||||
{
|
||||
await _cmdService.RemoveModuleAsync(mi);
|
||||
}
|
||||
|
||||
await _behHandler.RemoveRangeAsync(lsi.Execs);
|
||||
|
||||
await DisposeCanaryInstances(lsi);
|
||||
|
||||
var lc = lsi.LoadContext;
|
||||
|
||||
// removing this line will prevent assembly from being unloaded quickly
|
||||
// as this local variable will be held for a long time potentially
|
||||
// due to how async works
|
||||
lsi.Services = null!;
|
||||
lsi = null;
|
||||
|
||||
_marmaladeConfig.RemoveLoadedMarmalade(name);
|
||||
return UnloadInternal(lc)
|
||||
? MarmaladeUnloadResult.Success
|
||||
: MarmaladeUnloadResult.PossiblyUnable;
|
||||
}
|
||||
finally
|
||||
{
|
||||
_lock.Release();
|
||||
}
|
||||
}
|
||||
|
||||
private void UnloadTypeReaders(Dictionary<Type, TypeReader> valueTypeReaders)
|
||||
{
|
||||
foreach (var tr in valueTypeReaders)
|
||||
{
|
||||
_cmdService.TryRemoveTypeReader(tr.Key, false, out _);
|
||||
}
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.NoInlining)]
|
||||
private async Task DisposeCanaryInstances(ResolvedMarmalade marmalade)
|
||||
{
|
||||
foreach (var si in marmalade.CanaryInfos)
|
||||
{
|
||||
try
|
||||
{
|
||||
await si.Instance.DisposeAsync();
|
||||
foreach (var sub in si.Subcanaries)
|
||||
{
|
||||
await sub.Instance.DisposeAsync();
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Warning(ex,
|
||||
"Failed cleanup of Canary {CanaryName}. This marmalade might not unload correctly",
|
||||
si.Instance.Name);
|
||||
}
|
||||
}
|
||||
|
||||
// marmalades = null;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.NoInlining)]
|
||||
private bool UnloadInternal(WeakReference<MarmaladeAssemblyLoadContext> lsi)
|
||||
{
|
||||
UnloadContext(lsi);
|
||||
GcCleanup();
|
||||
|
||||
return !lsi.TryGetTarget(out _);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.NoInlining)]
|
||||
private void UnloadContext(WeakReference<MarmaladeAssemblyLoadContext> lsiLoadContext)
|
||||
{
|
||||
if (lsiLoadContext.TryGetTarget(out var ctx))
|
||||
ctx.Unload();
|
||||
}
|
||||
|
||||
private void GcCleanup()
|
||||
{
|
||||
// cleanup
|
||||
for (var i = 0; i < 10; i++)
|
||||
{
|
||||
GC.Collect();
|
||||
GC.WaitForPendingFinalizers();
|
||||
GC.WaitForFullGCComplete();
|
||||
GC.Collect();
|
||||
}
|
||||
}
|
||||
|
||||
private static readonly Type _canaryType = typeof(Canary);
|
||||
|
||||
[MethodImpl(MethodImplOptions.NoInlining)]
|
||||
private IServiceProvider LoadMarmaladeServicesInternal(Assembly a)
|
||||
=> new ServiceCollection()
|
||||
.Scan(x => x.FromAssemblies(a)
|
||||
.AddClasses(static x => x.WithAttribute<svcAttribute>(x => x.Lifetime == Lifetime.Transient))
|
||||
.AsSelfWithInterfaces()
|
||||
.WithTransientLifetime()
|
||||
.AddClasses(static x => x.WithAttribute<svcAttribute>(x => x.Lifetime == Lifetime.Singleton))
|
||||
.AsSelfWithInterfaces()
|
||||
.WithSingletonLifetime())
|
||||
.BuildServiceProvider();
|
||||
|
||||
|
||||
[MethodImpl(MethodImplOptions.NoInlining)]
|
||||
public IReadOnlyCollection<CanaryInfo> LoadCanariesFromAssembly(Assembly a, out IServiceProvider services)
|
||||
{
|
||||
var marmaladeServices = LoadMarmaladeServicesInternal(a);
|
||||
services = new MarmaladeServiceProvider(_botServices, marmaladeServices);
|
||||
|
||||
// find all types in teh assembly
|
||||
var types = a.GetExportedTypes();
|
||||
// snek is always a public non abstract class
|
||||
var classes = types.Where(static x => x.IsClass
|
||||
&& (x.IsNestedPublic || x.IsPublic)
|
||||
&& !x.IsAbstract
|
||||
&& x.BaseType == _canaryType
|
||||
&& (x.DeclaringType is null || x.DeclaringType.IsAssignableTo(_canaryType)))
|
||||
.ToList();
|
||||
|
||||
var topModules = new Dictionary<Type, CanaryInfo>();
|
||||
|
||||
foreach (var cl in classes)
|
||||
{
|
||||
if (cl.DeclaringType is not null)
|
||||
continue;
|
||||
|
||||
// get module data, and add it to the topModules dictionary
|
||||
var module = GetModuleData(cl, services);
|
||||
topModules.Add(cl, module);
|
||||
}
|
||||
|
||||
foreach (var c in classes)
|
||||
{
|
||||
if (c.DeclaringType is not Type dt)
|
||||
continue;
|
||||
|
||||
// if there is no top level module which this module is a child of
|
||||
// just print a warning and skip it
|
||||
if (!topModules.TryGetValue(dt, out var parentData))
|
||||
{
|
||||
Log.Warning("Can't load submodule {SubName} because parent module {Name} does not exist",
|
||||
c.Name,
|
||||
dt.Name);
|
||||
continue;
|
||||
}
|
||||
|
||||
GetModuleData(c, services, parentData);
|
||||
}
|
||||
|
||||
return topModules.Values.ToArray();
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.NoInlining)]
|
||||
private CanaryInfo GetModuleData(Type type, IServiceProvider services, CanaryInfo? parentData = null)
|
||||
{
|
||||
var filters = type.GetCustomAttributes<FilterAttribute>(true)
|
||||
.ToArray();
|
||||
|
||||
var instance = (Canary)ActivatorUtilities.CreateInstance(services, type);
|
||||
|
||||
var module = new CanaryInfo(instance.Name,
|
||||
parentData,
|
||||
instance,
|
||||
GetCommands(instance, type),
|
||||
filters);
|
||||
|
||||
if (parentData is not null)
|
||||
parentData.Subcanaries.Add(module);
|
||||
|
||||
return module;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.NoInlining)]
|
||||
private IReadOnlyCollection<CanaryCommandData> GetCommands(Canary instance, Type type)
|
||||
{
|
||||
var methodInfos = type
|
||||
.GetMethods(BindingFlags.Instance
|
||||
| BindingFlags.DeclaredOnly
|
||||
| BindingFlags.Public)
|
||||
.Where(static x =>
|
||||
{
|
||||
if (x.GetCustomAttribute<cmdAttribute>(true) is null)
|
||||
return false;
|
||||
|
||||
if (x.ReturnType.IsGenericType)
|
||||
{
|
||||
var genericType = x.ReturnType.GetGenericTypeDefinition();
|
||||
if (genericType == typeof(Task<>))
|
||||
return true;
|
||||
|
||||
// if (genericType == typeof(ValueTask<>))
|
||||
// return true;
|
||||
|
||||
Log.Warning("Method {MethodName} has an invalid return type: {ReturnType}",
|
||||
x.Name,
|
||||
x.ReturnType);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
var succ = x.ReturnType == typeof(Task)
|
||||
|| x.ReturnType == typeof(ValueTask)
|
||||
|| x.ReturnType == typeof(void);
|
||||
|
||||
if (!succ)
|
||||
{
|
||||
Log.Warning("Method {MethodName} has an invalid return type: {ReturnType}",
|
||||
x.Name,
|
||||
x.ReturnType);
|
||||
}
|
||||
|
||||
return succ;
|
||||
});
|
||||
|
||||
|
||||
var cmds = new List<CanaryCommandData>();
|
||||
foreach (var method in methodInfos)
|
||||
{
|
||||
var filters = method.GetCustomAttributes<FilterAttribute>(true).ToArray();
|
||||
var userAndBotPerms = method.GetCustomAttributes<MarmaladePermAttribute>(true)
|
||||
.ToArray();
|
||||
var prio = method.GetCustomAttribute<prioAttribute>(true)?.Priority ?? 0;
|
||||
|
||||
var paramInfos = method.GetParameters();
|
||||
var cmdParams = new List<ParamData>();
|
||||
var diParams = new List<Type>();
|
||||
var cmdContext = CommandContextType.Unspecified;
|
||||
var canInject = false;
|
||||
for (var paramCounter = 0; paramCounter < paramInfos.Length; paramCounter++)
|
||||
{
|
||||
var pi = paramInfos[paramCounter];
|
||||
|
||||
var paramName = pi.Name ?? "unnamed";
|
||||
var isContext = paramCounter == 0 && pi.ParameterType.IsAssignableTo(typeof(AnyContext));
|
||||
|
||||
var leftoverAttribute = pi.GetCustomAttribute<leftoverAttribute>(true);
|
||||
var hasDefaultValue = pi.HasDefaultValue;
|
||||
var defaultValue = pi.DefaultValue;
|
||||
var isLeftover = leftoverAttribute != null;
|
||||
var isParams = pi.GetCustomAttribute<ParamArrayAttribute>() is not null;
|
||||
var paramType = pi.ParameterType;
|
||||
var isInjected = pi.GetCustomAttribute<injectAttribute>(true) is not null;
|
||||
|
||||
if (isContext)
|
||||
{
|
||||
if (hasDefaultValue || leftoverAttribute != null || isParams)
|
||||
throw new ArgumentException("IContext parameter cannot be optional, leftover, constant or params. " + GetErrorPath(method, pi));
|
||||
|
||||
if (paramCounter != 0)
|
||||
throw new ArgumentException($"IContext parameter has to be first. {GetErrorPath(method, pi)}");
|
||||
|
||||
canInject = true;
|
||||
|
||||
if (paramType.IsAssignableTo(typeof(GuildContext)))
|
||||
cmdContext = CommandContextType.Guild;
|
||||
else if (paramType.IsAssignableTo(typeof(DmContext)))
|
||||
cmdContext = CommandContextType.Dm;
|
||||
else
|
||||
cmdContext = CommandContextType.Any;
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if (isInjected)
|
||||
{
|
||||
if (!canInject && paramCounter != 0)
|
||||
throw new ArgumentException($"Parameters marked as [Injected] have to come after IContext");
|
||||
|
||||
canInject = true;
|
||||
|
||||
diParams.Add(paramType);
|
||||
continue;
|
||||
}
|
||||
|
||||
canInject = false;
|
||||
|
||||
if (isParams)
|
||||
{
|
||||
if (hasDefaultValue)
|
||||
throw new NotSupportedException("Params can't have const values at the moment. "
|
||||
+ GetErrorPath(method, pi));
|
||||
// if it's params, it means it's an array, and i only need a parser for the actual type,
|
||||
// as the parser will run on each array element, it can't be null
|
||||
paramType = paramType.GetElementType()!;
|
||||
}
|
||||
|
||||
// leftover can only be the last parameter.
|
||||
if (isLeftover && paramCounter != paramInfos.Length - 1)
|
||||
{
|
||||
var path = GetErrorPath(method, pi);
|
||||
Log.Error("Only one parameter can be marked [Leftover] and it has to be the last one. {Path} ",
|
||||
path);
|
||||
throw new ArgumentException("Leftover attribute error.");
|
||||
}
|
||||
|
||||
cmdParams.Add(new ParamData(paramType, paramName, hasDefaultValue, defaultValue, isLeftover, isParams));
|
||||
}
|
||||
|
||||
|
||||
var cmdAttribute = method.GetCustomAttribute<cmdAttribute>(true)!;
|
||||
var aliases = cmdAttribute.Aliases;
|
||||
if (aliases.Length == 0)
|
||||
aliases = new[] { method.Name.ToLowerInvariant() };
|
||||
|
||||
cmds.Add(new(
|
||||
aliases,
|
||||
method,
|
||||
instance,
|
||||
filters,
|
||||
userAndBotPerms,
|
||||
cmdContext,
|
||||
diParams,
|
||||
cmdParams,
|
||||
new(cmdAttribute.desc, cmdAttribute.args),
|
||||
prio
|
||||
));
|
||||
}
|
||||
|
||||
return cmds;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.NoInlining)]
|
||||
private string GetErrorPath(MethodInfo m, System.Reflection.ParameterInfo pi)
|
||||
=> $@"Module: {m.DeclaringType?.Name}
|
||||
Command: {m.Name}
|
||||
ParamName: {pi.Name}
|
||||
ParamType: {pi.ParameterType.Name}";
|
||||
}
|
||||
|
||||
public enum MarmaladeLoadResult
|
||||
{
|
||||
Success,
|
||||
NotFound,
|
||||
AlreadyLoaded,
|
||||
Empty,
|
||||
UnknownError,
|
||||
}
|
||||
|
||||
public enum MarmaladeUnloadResult
|
||||
{
|
||||
Success,
|
||||
NotLoaded,
|
||||
PossiblyUnable,
|
||||
NotFound,
|
||||
}
|
|
@ -1,24 +0,0 @@
|
|||
using System.Runtime.CompilerServices;
|
||||
|
||||
namespace Ellie.Marmalade;
|
||||
|
||||
public class MarmaladeServiceProvider : IServiceProvider
|
||||
{
|
||||
private readonly IServiceProvider _ellieServices;
|
||||
private readonly IServiceProvider _marmaladeServices;
|
||||
|
||||
public MarmaladeServiceProvider(IServiceProvider ellieServices, IServiceProvider marmaladeServices)
|
||||
{
|
||||
_ellieServices = ellieServices;
|
||||
_marmaladeServices = marmaladeServices;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.NoInlining)]
|
||||
public object? GetService(Type serviceType)
|
||||
{
|
||||
if (!serviceType.Assembly.IsCollectible)
|
||||
return _ellieServices.GetService(serviceType);
|
||||
|
||||
return _marmaladeServices.GetService(serviceType);
|
||||
}
|
||||
}
|
|
@ -1,46 +0,0 @@
|
|||
using Microsoft.VisualBasic;
|
||||
using System.Reflection;
|
||||
using CommandStrings = Ellie.Marmalade.CommandStrings;
|
||||
|
||||
namespace Ellie.Marmalade;
|
||||
|
||||
public sealed class CanaryCommandData
|
||||
{
|
||||
public CanaryCommandData(
|
||||
IReadOnlyCollection<string> aliases,
|
||||
MethodInfo methodInfo,
|
||||
Canary module,
|
||||
FilterAttribute[] filters,
|
||||
MarmaladePermAttribute[] userAndBotPerms,
|
||||
CommandContextType contextType,
|
||||
IReadOnlyList<Type> injectedParams,
|
||||
IReadOnlyList<ParamData> parameters,
|
||||
CommandStrings strings,
|
||||
int priority)
|
||||
{
|
||||
Aliases = aliases;
|
||||
MethodInfo = methodInfo;
|
||||
Module = module;
|
||||
Filters = filters;
|
||||
UserAndBotPerms = userAndBotPerms;
|
||||
ContextType = contextType;
|
||||
InjectedParams = injectedParams;
|
||||
Parameters = parameters;
|
||||
Priority = priority;
|
||||
OptionalStrings = strings;
|
||||
}
|
||||
|
||||
public MarmaladePermAttribute[] UserAndBotPerms { get; set; }
|
||||
|
||||
public CommandStrings OptionalStrings { get; set; }
|
||||
|
||||
public IReadOnlyCollection<string> Aliases { get; }
|
||||
public MethodInfo MethodInfo { get; set; }
|
||||
public Canary Module { get; set; }
|
||||
public FilterAttribute[] Filters { get; set; }
|
||||
public CommandContextType ContextType { get; }
|
||||
public IReadOnlyList<Type> InjectedParams { get; }
|
||||
public IReadOnlyList<ParamData> Parameters { get; }
|
||||
public int Priority { get; }
|
||||
|
||||
}
|
|
@ -1,11 +0,0 @@
|
|||
namespace Ellie.Marmalade;
|
||||
|
||||
public sealed record CanaryInfo(
|
||||
string Name,
|
||||
CanaryInfo? Parent,
|
||||
Canary Instance,
|
||||
IReadOnlyCollection<CanaryCommandData> Commands,
|
||||
IReadOnlyCollection<FilterAttribute> Filters)
|
||||
{
|
||||
public List<CanaryInfo> Subcanaries { get; set; } = new();
|
||||
}
|
|
@ -1,10 +0,0 @@
|
|||
namespace Ellie.Marmalade;
|
||||
|
||||
public sealed record ParamData(
|
||||
Type Type,
|
||||
string Name,
|
||||
bool IsOptional,
|
||||
object? DefaultValue,
|
||||
bool IsLeftover,
|
||||
bool IsParams
|
||||
);
|
|
@ -1,14 +0,0 @@
|
|||
using System.Collections.Immutable;
|
||||
|
||||
namespace Ellie.Marmalade;
|
||||
|
||||
public sealed record ResolvedMarmalade(
|
||||
WeakReference<MarmaladeAssemblyLoadContext> LoadContext,
|
||||
IImmutableList<ModuleInfo> ModuleInfos,
|
||||
IImmutableList<CanaryInfo> CanaryInfos,
|
||||
IMarmaladeStrings Strings,
|
||||
Dictionary<Type, TypeReader> TypeReaders,
|
||||
IReadOnlyCollection<ICustomBehavior> Execs)
|
||||
{
|
||||
public IServiceProvider Services { get; set; } = null!;
|
||||
}
|
|
@ -1,19 +0,0 @@
|
|||
namespace EllieBot.Common.ModuleBehaviors;
|
||||
|
||||
/// <summary>
|
||||
/// Executed if no command was found for this message
|
||||
/// </summary>
|
||||
public interface IExecNoCommand
|
||||
{
|
||||
/// <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 EllieBot.Common.ModuleBehaviors;
|
||||
|
||||
/// <summary>
|
||||
/// Implemented by modules to handle non-bot messages received
|
||||
/// </summary>
|
||||
public interface IExecOnMessage
|
||||
{
|
||||
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 EllieBot.Common.ModuleBehaviors;
|
||||
|
||||
/// <summary>
|
||||
/// This interface's method is executed after the command successfully finished execution.
|
||||
/// ***There is no support for this method in EllieBot services.***
|
||||
/// It is only meant to be used in marmalade system
|
||||
/// </summary>
|
||||
public interface IExecPostCommand
|
||||
{
|
||||
/// <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 EllieBot.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
|
||||
{
|
||||
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 EllieBot.Common.ModuleBehaviors;
|
||||
|
||||
/// <summary>
|
||||
/// Implemented by services which may transform input before a command is searched for
|
||||
/// </summary>
|
||||
public interface IInputTransformer
|
||||
{
|
||||
/// <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 EllieBot.Common.ModuleBehaviors;
|
||||
|
||||
/// <summary>
|
||||
/// All services which need to execute something after
|
||||
/// the bot is ready should implement this interface
|
||||
/// </summary>
|
||||
public interface IReadyExecutor
|
||||
{
|
||||
/// <summary>
|
||||
/// Executed when bot is ready
|
||||
/// </summary>
|
||||
public Task OnReadyAsync();
|
||||
}
|
|
@ -1,80 +0,0 @@
|
|||
namespace EllieBot.Common;
|
||||
|
||||
public class EventPubSub : IPubSub
|
||||
{
|
||||
private readonly Dictionary<string, Dictionary<Delegate, List<Func<object, ValueTask>>>> _actions = new();
|
||||
private readonly object _locker = new();
|
||||
|
||||
public Task Sub<TData>(in TypedKey<TData> key, Func<TData, ValueTask> action)
|
||||
where TData : notnull
|
||||
{
|
||||
Func<object, ValueTask> localAction = obj => action((TData)obj);
|
||||
lock (_locker)
|
||||
{
|
||||
if (!_actions.TryGetValue(key.Key, out var keyActions))
|
||||
{
|
||||
keyActions = new();
|
||||
_actions[key.Key] = keyActions;
|
||||
}
|
||||
|
||||
if (!keyActions.TryGetValue(action, out var sameActions))
|
||||
{
|
||||
sameActions = new();
|
||||
keyActions[action] = sameActions;
|
||||
}
|
||||
|
||||
sameActions.Add(localAction);
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
|
||||
public Task Pub<TData>(in TypedKey<TData> key, TData data)
|
||||
where TData : notnull
|
||||
{
|
||||
lock (_locker)
|
||||
{
|
||||
if (_actions.TryGetValue(key.Key, out var actions))
|
||||
// if this class ever gets used, this needs to be properly implemented
|
||||
// 1. ignore all valuetasks which are completed
|
||||
// 2. run all other tasks in parallel
|
||||
return actions.SelectMany(kvp => kvp.Value).Select(action => action(data).AsTask()).WhenAll();
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
|
||||
public Task Unsub<TData>(in TypedKey<TData> key, Func<TData, ValueTask> action)
|
||||
{
|
||||
lock (_locker)
|
||||
{
|
||||
// get subscriptions for this action
|
||||
if (_actions.TryGetValue(key.Key, out var actions))
|
||||
// get subscriptions which have the same action hash code
|
||||
// note: having this as a list allows for multiple subscriptions of
|
||||
// the same insance's/static method
|
||||
{
|
||||
if (actions.TryGetValue(action, out var sameActions))
|
||||
{
|
||||
// remove last subscription
|
||||
sameActions.RemoveAt(sameActions.Count - 1);
|
||||
|
||||
// if the last subscription was the only subscription
|
||||
// we can safely remove this action's dictionary entry
|
||||
if (sameActions.Count == 0)
|
||||
{
|
||||
actions.Remove(action);
|
||||
|
||||
// if our dictionary has no more elements after
|
||||
// removing the entry
|
||||
// it's safe to remove it from the key's subscriptions
|
||||
if (actions.Count == 0)
|
||||
_actions.Remove(key.Key);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,10 +0,0 @@
|
|||
namespace EllieBot.Common;
|
||||
|
||||
public interface IPubSub
|
||||
{
|
||||
public Task Pub<TData>(in TypedKey<TData> key, TData data)
|
||||
where TData : notnull;
|
||||
|
||||
public Task Sub<TData>(in TypedKey<TData> key, Func<TData, ValueTask> action)
|
||||
where TData : notnull;
|
||||
}
|
|
@ -1,7 +0,0 @@
|
|||
namespace EllieBot.Common;
|
||||
|
||||
public interface ISeria
|
||||
{
|
||||
byte[] Serialize<T>(T data);
|
||||
T? Deserialize<T>(byte[]? data);
|
||||
}
|
|
@ -1,27 +0,0 @@
|
|||
using EllieBot.Common.JsonConverters;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace EllieBot.Common;
|
||||
|
||||
public class JsonSeria : ISeria
|
||||
{
|
||||
private readonly JsonSerializerOptions _serializerOptions = new()
|
||||
{
|
||||
Converters =
|
||||
{
|
||||
new Rgba32Converter(),
|
||||
new CultureInfoConverter()
|
||||
}
|
||||
};
|
||||
|
||||
public byte[] Serialize<T>(T data)
|
||||
=> JsonSerializer.SerializeToUtf8Bytes(data, _serializerOptions);
|
||||
|
||||
public T? Deserialize<T>(byte[]? data)
|
||||
{
|
||||
if (data is null)
|
||||
return default;
|
||||
|
||||
return JsonSerializer.Deserialize<T>(data, _serializerOptions);
|
||||
}
|
||||
}
|
|
@ -1,52 +0,0 @@
|
|||
using StackExchange.Redis;
|
||||
|
||||
namespace EllieBot.Common;
|
||||
|
||||
public sealed class RedisPubSub : IPubSub
|
||||
{
|
||||
private readonly IBotCredentials _creds;
|
||||
private readonly ConnectionMultiplexer _multi;
|
||||
private readonly ISeria _serializer;
|
||||
|
||||
public RedisPubSub(ConnectionMultiplexer multi, ISeria serializer, IBotCredentials creds)
|
||||
{
|
||||
_multi = multi;
|
||||
_serializer = serializer;
|
||||
_creds = creds;
|
||||
}
|
||||
|
||||
public Task Pub<TData>(in TypedKey<TData> key, TData data)
|
||||
where TData : notnull
|
||||
{
|
||||
var serialized = _serializer.Serialize(data);
|
||||
return _multi.GetSubscriber()
|
||||
.PublishAsync($"{_creds.RedisKey()}:{key.Key}", serialized, CommandFlags.FireAndForget);
|
||||
}
|
||||
|
||||
public Task Sub<TData>(in TypedKey<TData> key, Func<TData, ValueTask> action)
|
||||
where TData : notnull
|
||||
{
|
||||
var eventName = key.Key;
|
||||
|
||||
async void OnSubscribeHandler(RedisChannel _, RedisValue data)
|
||||
{
|
||||
try
|
||||
{
|
||||
var dataObj = _serializer.Deserialize<TData>(data);
|
||||
if (dataObj is not null)
|
||||
await action(dataObj);
|
||||
else
|
||||
{
|
||||
Log.Warning("Publishing event {EventName} with a null value. This is not allowed",
|
||||
eventName);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Error("Error handling the event {EventName}: {ErrorMessage}", eventName, ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
return _multi.GetSubscriber().SubscribeAsync($"{_creds.RedisKey()}:{eventName}", OnSubscribeHandler);
|
||||
}
|
||||
}
|
|
@ -1,30 +0,0 @@
|
|||
namespace EllieBot.Common;
|
||||
|
||||
public readonly struct TypedKey<TData>
|
||||
{
|
||||
public string Key { get; }
|
||||
|
||||
public TypedKey(in string key)
|
||||
=> Key = key;
|
||||
|
||||
public static implicit operator TypedKey<TData>(in string input)
|
||||
=> new(input);
|
||||
|
||||
public static implicit operator string(in TypedKey<TData> input)
|
||||
=> input.Key;
|
||||
|
||||
public static bool operator ==(in TypedKey<TData> left, in TypedKey<TData> right)
|
||||
=> left.Key == right.Key;
|
||||
|
||||
public static bool operator !=(in TypedKey<TData> left, in TypedKey<TData> right)
|
||||
=> !(left == right);
|
||||
|
||||
public override bool Equals(object? obj)
|
||||
=> obj is TypedKey<TData> o && o == this;
|
||||
|
||||
public override int GetHashCode()
|
||||
=> Key?.GetHashCode() ?? 0;
|
||||
|
||||
public override string ToString()
|
||||
=> Key;
|
||||
}
|
|
@ -1,39 +0,0 @@
|
|||
using EllieBot.Common.Configs;
|
||||
using EllieBot.Common.Yml;
|
||||
using System.Text.RegularExpressions;
|
||||
using YamlDotNet.Serialization;
|
||||
|
||||
namespace EllieBot.Common;
|
||||
|
||||
public class YamlSeria : IConfigSeria
|
||||
{
|
||||
private static readonly Regex _codePointRegex =
|
||||
new(@"(\\U(?<code>[a-zA-Z0-9]{8})|\\u(?<code>[a-zA-Z0-9]{4})|\\x(?<code>[a-zA-Z0-9]{2}))",
|
||||
RegexOptions.Compiled);
|
||||
|
||||
private readonly IDeserializer _deserializer;
|
||||
private readonly ISerializer _serializer;
|
||||
|
||||
public YamlSeria()
|
||||
{
|
||||
_serializer = Yaml.Serializer;
|
||||
_deserializer = Yaml.Deserializer;
|
||||
}
|
||||
|
||||
public string Serialize<T>(T obj)
|
||||
where T : notnull
|
||||
{
|
||||
var escapedOutput = _serializer.Serialize(obj);
|
||||
var output = _codePointRegex.Replace(escapedOutput,
|
||||
me =>
|
||||
{
|
||||
var str = me.Groups["code"].Value;
|
||||
var newString = YamlHelper.UnescapeUnicodeCodePoint(str);
|
||||
return newString;
|
||||
});
|
||||
return output;
|
||||
}
|
||||
|
||||
public T Deserialize<T>(string data)
|
||||
=> _deserializer.Deserialize<T>(data);
|
||||
}
|
|
@ -1,11 +0,0 @@
|
|||
#nullable disable
|
||||
namespace EllieBot.Common.Yml;
|
||||
|
||||
[AttributeUsage(AttributeTargets.Property)]
|
||||
public class CommentAttribute : Attribute
|
||||
{
|
||||
public string Comment { get; }
|
||||
|
||||
public CommentAttribute(string comment)
|
||||
=> Comment = comment;
|
||||
}
|
|
@ -1,65 +0,0 @@
|
|||
#nullable disable
|
||||
using YamlDotNet.Core;
|
||||
using YamlDotNet.Serialization;
|
||||
using YamlDotNet.Serialization.TypeInspectors;
|
||||
|
||||
namespace EllieBot.Common.Yml;
|
||||
|
||||
public class CommentGatheringTypeInspector : TypeInspectorSkeleton
|
||||
{
|
||||
private readonly ITypeInspector _innerTypeDescriptor;
|
||||
|
||||
public CommentGatheringTypeInspector(ITypeInspector innerTypeDescriptor)
|
||||
=> _innerTypeDescriptor = innerTypeDescriptor ?? throw new ArgumentNullException(nameof(innerTypeDescriptor));
|
||||
|
||||
public override IEnumerable<IPropertyDescriptor> GetProperties(Type type, object container)
|
||||
=> _innerTypeDescriptor.GetProperties(type, container).Select(d => new CommentsPropertyDescriptor(d));
|
||||
|
||||
private sealed class CommentsPropertyDescriptor : IPropertyDescriptor
|
||||
{
|
||||
public string Name { get; }
|
||||
|
||||
public Type Type
|
||||
=> _baseDescriptor.Type;
|
||||
|
||||
public Type TypeOverride
|
||||
{
|
||||
get => _baseDescriptor.TypeOverride;
|
||||
set => _baseDescriptor.TypeOverride = value;
|
||||
}
|
||||
|
||||
public int Order { get; set; }
|
||||
|
||||
public ScalarStyle ScalarStyle
|
||||
{
|
||||
get => _baseDescriptor.ScalarStyle;
|
||||
set => _baseDescriptor.ScalarStyle = value;
|
||||
}
|
||||
|
||||
public bool CanWrite
|
||||
=> _baseDescriptor.CanWrite;
|
||||
|
||||
private readonly IPropertyDescriptor _baseDescriptor;
|
||||
|
||||
public CommentsPropertyDescriptor(IPropertyDescriptor baseDescriptor)
|
||||
{
|
||||
_baseDescriptor = baseDescriptor;
|
||||
Name = baseDescriptor.Name;
|
||||
}
|
||||
|
||||
public void Write(object target, object value)
|
||||
=> _baseDescriptor.Write(target, value);
|
||||
|
||||
public T GetCustomAttribute<T>()
|
||||
where T : Attribute
|
||||
=> _baseDescriptor.GetCustomAttribute<T>();
|
||||
|
||||
public IObjectDescriptor Read(object target)
|
||||
{
|
||||
var comment = _baseDescriptor.GetCustomAttribute<CommentAttribute>();
|
||||
return comment is not null
|
||||
? new CommentsObjectDescriptor(_baseDescriptor.Read(target), comment.Comment)
|
||||
: _baseDescriptor.Read(target);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,30 +0,0 @@
|
|||
#nullable disable
|
||||
using YamlDotNet.Core;
|
||||
using YamlDotNet.Serialization;
|
||||
|
||||
namespace EllieBot.Common.Yml;
|
||||
|
||||
public sealed class CommentsObjectDescriptor : IObjectDescriptor
|
||||
{
|
||||
public string Comment { get; }
|
||||
|
||||
public object Value
|
||||
=> _innerDescriptor.Value;
|
||||
|
||||
public Type Type
|
||||
=> _innerDescriptor.Type;
|
||||
|
||||
public Type StaticType
|
||||
=> _innerDescriptor.StaticType;
|
||||
|
||||
public ScalarStyle ScalarStyle
|
||||
=> _innerDescriptor.ScalarStyle;
|
||||
|
||||
private readonly IObjectDescriptor _innerDescriptor;
|
||||
|
||||
public CommentsObjectDescriptor(IObjectDescriptor innerDescriptor, string comment)
|
||||
{
|
||||
_innerDescriptor = innerDescriptor;
|
||||
Comment = comment;
|
||||
}
|
||||
}
|
|
@ -1,29 +0,0 @@
|
|||
#nullable disable
|
||||
using YamlDotNet.Core;
|
||||
using YamlDotNet.Core.Events;
|
||||
using YamlDotNet.Serialization;
|
||||
using YamlDotNet.Serialization.ObjectGraphVisitors;
|
||||
|
||||
namespace EllieBot.Common.Yml;
|
||||
|
||||
public class CommentsObjectGraphVisitor : ChainedObjectGraphVisitor
|
||||
{
|
||||
public CommentsObjectGraphVisitor(IObjectGraphVisitor<IEmitter> nextVisitor)
|
||||
: base(nextVisitor)
|
||||
{
|
||||
}
|
||||
|
||||
public override bool EnterMapping(IPropertyDescriptor key, IObjectDescriptor value, IEmitter context)
|
||||
{
|
||||
if (value is CommentsObjectDescriptor commentsDescriptor
|
||||
&& !string.IsNullOrWhiteSpace(commentsDescriptor.Comment))
|
||||
{
|
||||
var parts = commentsDescriptor.Comment.Split('\n');
|
||||
|
||||
foreach (var part in parts)
|
||||
context.Emit(new Comment(part.Trim(), false));
|
||||
}
|
||||
|
||||
return base.EnterMapping(key, value, context);
|
||||
}
|
||||
}
|
|
@ -1,35 +0,0 @@
|
|||
#nullable disable
|
||||
using YamlDotNet.Core;
|
||||
using YamlDotNet.Serialization;
|
||||
using YamlDotNet.Serialization.EventEmitters;
|
||||
|
||||
namespace EllieBot.Common.Yml;
|
||||
|
||||
public class MultilineScalarFlowStyleEmitter : ChainedEventEmitter
|
||||
{
|
||||
public MultilineScalarFlowStyleEmitter(IEventEmitter nextEmitter)
|
||||
: base(nextEmitter)
|
||||
{
|
||||
}
|
||||
|
||||
public override void Emit(ScalarEventInfo eventInfo, IEmitter emitter)
|
||||
{
|
||||
if (typeof(string).IsAssignableFrom(eventInfo.Source.Type))
|
||||
{
|
||||
var value = eventInfo.Source.Value as string;
|
||||
if (!string.IsNullOrEmpty(value))
|
||||
{
|
||||
var isMultiLine = value.IndexOfAny(new[] { '\r', '\n', '\x85', '\x2028', '\x2029' }) >= 0;
|
||||
if (isMultiLine)
|
||||
{
|
||||
eventInfo = new(eventInfo.Source)
|
||||
{
|
||||
Style = ScalarStyle.Literal
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
nextEmitter.Emit(eventInfo, emitter);
|
||||
}
|
||||
}
|
|
@ -1,47 +0,0 @@
|
|||
#nullable disable
|
||||
using SixLabors.ImageSharp.PixelFormats;
|
||||
using System.Globalization;
|
||||
using YamlDotNet.Core;
|
||||
using YamlDotNet.Core.Events;
|
||||
using YamlDotNet.Serialization;
|
||||
|
||||
namespace EllieBot.Common.Yml;
|
||||
|
||||
public class Rgba32Converter : IYamlTypeConverter
|
||||
{
|
||||
public bool Accepts(Type type)
|
||||
=> type == typeof(Rgba32);
|
||||
|
||||
public object ReadYaml(IParser parser, Type type)
|
||||
{
|
||||
var scalar = parser.Consume<Scalar>();
|
||||
var result = Rgba32.ParseHex(scalar.Value);
|
||||
return result;
|
||||
}
|
||||
|
||||
public void WriteYaml(IEmitter emitter, object value, Type type)
|
||||
{
|
||||
var color = (Rgba32)value;
|
||||
var val = (uint)((color.B << 0) | (color.G << 8) | (color.R << 16));
|
||||
emitter.Emit(new Scalar(val.ToString("X6").ToLower()));
|
||||
}
|
||||
}
|
||||
|
||||
public class CultureInfoConverter : IYamlTypeConverter
|
||||
{
|
||||
public bool Accepts(Type type)
|
||||
=> type == typeof(CultureInfo);
|
||||
|
||||
public object ReadYaml(IParser parser, Type type)
|
||||
{
|
||||
var scalar = parser.Consume<Scalar>();
|
||||
var result = new CultureInfo(scalar.Value);
|
||||
return result;
|
||||
}
|
||||
|
||||
public void WriteYaml(IEmitter emitter, object value, Type type)
|
||||
{
|
||||
var ci = (CultureInfo)value;
|
||||
emitter.Emit(new Scalar(ci.Name));
|
||||
}
|
||||
}
|
|
@ -1,25 +0,0 @@
|
|||
#nullable disable
|
||||
using YamlDotNet.Core;
|
||||
using YamlDotNet.Core.Events;
|
||||
using YamlDotNet.Serialization;
|
||||
|
||||
namespace EllieBot.Common.Yml;
|
||||
|
||||
public class UriConverter : IYamlTypeConverter
|
||||
{
|
||||
public bool Accepts(Type type)
|
||||
=> type == typeof(Uri);
|
||||
|
||||
public object ReadYaml(IParser parser, Type type)
|
||||
{
|
||||
var scalar = parser.Consume<Scalar>();
|
||||
var result = new Uri(scalar.Value);
|
||||
return result;
|
||||
}
|
||||
|
||||
public void WriteYaml(IEmitter emitter, object value, Type type)
|
||||
{
|
||||
var uri = (Uri)value;
|
||||
emitter.Emit(new Scalar(uri.ToString()));
|
||||
}
|
||||
}
|
|
@ -1,28 +0,0 @@
|
|||
#nullable disable
|
||||
using YamlDotNet.Serialization;
|
||||
using YamlDotNet.Serialization.NamingConventions;
|
||||
|
||||
namespace EllieBot.Common.Yml;
|
||||
|
||||
public class Yaml
|
||||
{
|
||||
public static ISerializer Serializer
|
||||
=> new SerializerBuilder().WithTypeInspector(inner => new CommentGatheringTypeInspector(inner))
|
||||
.WithEmissionPhaseObjectGraphVisitor(args
|
||||
=> new CommentsObjectGraphVisitor(args.InnerVisitor))
|
||||
.WithEventEmitter(args => new MultilineScalarFlowStyleEmitter(args))
|
||||
.WithNamingConvention(CamelCaseNamingConvention.Instance)
|
||||
.WithIndentedSequences()
|
||||
.WithTypeConverter(new Rgba32Converter())
|
||||
.WithTypeConverter(new CultureInfoConverter())
|
||||
.WithTypeConverter(new UriConverter())
|
||||
.Build();
|
||||
|
||||
public static IDeserializer Deserializer
|
||||
=> new DeserializerBuilder().WithNamingConvention(CamelCaseNamingConvention.Instance)
|
||||
.WithTypeConverter(new Rgba32Converter())
|
||||
.WithTypeConverter(new CultureInfoConverter())
|
||||
.WithTypeConverter(new UriConverter())
|
||||
.IgnoreUnmatchedProperties()
|
||||
.Build();
|
||||
}
|
|
@ -1,48 +0,0 @@
|
|||
#nullable disable
|
||||
namespace EllieBot.Common.Yml;
|
||||
|
||||
public class YamlHelper
|
||||
{
|
||||
// https://github.com/aaubry/YamlDotNet/blob/0f4cc205e8b2dd8ef6589d96de32bf608a687c6f/YamlDotNet/Core/Scanner.cs#L1687
|
||||
/// <summary>
|
||||
/// This is modified code from yamldotnet's repo which handles parsing unicode code points
|
||||
/// it is needed as yamldotnet doesn't support unescaped unicode characters
|
||||
/// </summary>
|
||||
/// <param name="point">Unicode code point</param>
|
||||
/// <returns>Actual character</returns>
|
||||
public static string UnescapeUnicodeCodePoint(string point)
|
||||
{
|
||||
var character = 0;
|
||||
|
||||
// Scan the character value.
|
||||
|
||||
foreach (var c in point)
|
||||
{
|
||||
if (!IsHex(c))
|
||||
return point;
|
||||
|
||||
character = (character << 4) + AsHex(c);
|
||||
}
|
||||
|
||||
// Check the value and write the character.
|
||||
|
||||
if (character is (>= 0xD800 and <= 0xDFFF) or > 0x10FFFF)
|
||||
return point;
|
||||
|
||||
return char.ConvertFromUtf32(character);
|
||||
}
|
||||
|
||||
public static bool IsHex(char c)
|
||||
=> c is (>= '0' and <= '9') or (>= 'A' and <= 'F') or (>= 'a' and <= 'f');
|
||||
|
||||
public static int AsHex(char c)
|
||||
{
|
||||
if (c <= '9')
|
||||
return c - '0';
|
||||
|
||||
if (c <= 'F')
|
||||
return c - 'A' + 10;
|
||||
|
||||
return c - 'a' + 10;
|
||||
}
|
||||
}
|
|
@ -1,491 +0,0 @@
|
|||
#nullable disable
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using EllieBot.Db.Models;
|
||||
using EllieBot.Services.Database.Models;
|
||||
|
||||
// ReSharper disable UnusedAutoPropertyAccessor.Global
|
||||
|
||||
namespace EllieBot.Services.Database;
|
||||
|
||||
public abstract class EllieContext : DbContext
|
||||
{
|
||||
public DbSet<GuildConfig> GuildConfigs { get; set; }
|
||||
|
||||
public DbSet<Quote> Quotes { get; set; }
|
||||
public DbSet<Reminder> Reminders { get; set; }
|
||||
public DbSet<SelfAssignedRole> SelfAssignableRoles { get; set; }
|
||||
public DbSet<MusicPlaylist> MusicPlaylists { get; set; }
|
||||
public DbSet<EllieExpression> Expressions { get; set; }
|
||||
public DbSet<CurrencyTransaction> CurrencyTransactions { get; set; }
|
||||
public DbSet<WaifuUpdate> WaifuUpdates { get; set; }
|
||||
public DbSet<WaifuItem> WaifuItem { get; set; }
|
||||
public DbSet<Warning> Warnings { get; set; }
|
||||
public DbSet<UserXpStats> UserXpStats { get; set; }
|
||||
public DbSet<ClubInfo> Clubs { get; set; }
|
||||
public DbSet<ClubBans> ClubBans { get; set; }
|
||||
public DbSet<ClubApplicants> ClubApplicants { get; set; }
|
||||
|
||||
|
||||
//logging
|
||||
public DbSet<LogSetting> LogSettings { get; set; }
|
||||
public DbSet<IgnoredVoicePresenceChannel> IgnoredVoicePresenceCHannels { get; set; }
|
||||
public DbSet<IgnoredLogItem> IgnoredLogChannels { get; set; }
|
||||
|
||||
public DbSet<RotatingPlayingStatus> RotatingStatus { get; set; }
|
||||
public DbSet<BlacklistEntry> Blacklist { get; set; }
|
||||
public DbSet<AutoCommand> AutoCommands { get; set; }
|
||||
public DbSet<RewardedUser> RewardedUsers { get; set; }
|
||||
public DbSet<PlantedCurrency> PlantedCurrency { get; set; }
|
||||
public DbSet<BanTemplate> BanTemplates { get; set; }
|
||||
public DbSet<DiscordPermOverride> DiscordPermOverrides { get; set; }
|
||||
public DbSet<DiscordUser> DiscordUser { get; set; }
|
||||
public DbSet<MusicPlayerSettings> MusicPlayerSettings { get; set; }
|
||||
public DbSet<Repeater> Repeaters { get; set; }
|
||||
public DbSet<Poll> Poll { get; set; }
|
||||
public DbSet<WaifuInfo> WaifuInfo { get; set; }
|
||||
public DbSet<ImageOnlyChannel> ImageOnlyChannels { get; set; }
|
||||
public DbSet<NsfwBlacklistedTag> NsfwBlacklistedTags { get; set; }
|
||||
public DbSet<AutoTranslateChannel> AutoTranslateChannels { get; set; }
|
||||
public DbSet<AutoTranslateUser> AutoTranslateUsers { get; set; }
|
||||
|
||||
public DbSet<Permissionv2> Permissions { get; set; }
|
||||
|
||||
public DbSet<BankUser> BankUsers { get; set; }
|
||||
|
||||
public DbSet<ReactionRoleV2> ReactionRoles { get; set; }
|
||||
|
||||
public DbSet<PatronUser> Patrons { get; set; }
|
||||
|
||||
public DbSet<PatronQuota> PatronQuotas { get; set; }
|
||||
|
||||
public DbSet<StreamOnlineMessage> StreamOnlineMessages { get; set; }
|
||||
|
||||
|
||||
#region Mandatory Provider-Specific Values
|
||||
|
||||
protected abstract string CurrencyTransactionOtherIdDefaultValue { get; }
|
||||
|
||||
#endregion
|
||||
|
||||
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
||||
{
|
||||
#region QUOTES
|
||||
|
||||
var quoteEntity = modelBuilder.Entity<Quote>();
|
||||
quoteEntity.HasIndex(x => x.GuildId);
|
||||
quoteEntity.HasIndex(x => x.Keyword);
|
||||
|
||||
#endregion
|
||||
|
||||
#region GuildConfig
|
||||
|
||||
var configEntity = modelBuilder.Entity<GuildConfig>();
|
||||
configEntity.HasIndex(c => c.GuildId)
|
||||
.IsUnique();
|
||||
|
||||
configEntity.Property(x => x.VerboseErrors)
|
||||
.HasDefaultValue(true);
|
||||
|
||||
modelBuilder.Entity<AntiSpamSetting>().HasOne(x => x.GuildConfig).WithOne(x => x.AntiSpamSetting);
|
||||
|
||||
modelBuilder.Entity<AntiRaidSetting>().HasOne(x => x.GuildConfig).WithOne(x => x.AntiRaidSetting);
|
||||
|
||||
modelBuilder.Entity<GuildConfig>()
|
||||
.HasOne(x => x.AntiAltSetting)
|
||||
.WithOne()
|
||||
.HasForeignKey<AntiAltSetting>(x => x.GuildConfigId)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
|
||||
modelBuilder.Entity<FeedSub>()
|
||||
.HasAlternateKey(x => new
|
||||
{
|
||||
x.GuildConfigId,
|
||||
x.Url
|
||||
});
|
||||
|
||||
modelBuilder.Entity<PlantedCurrency>().HasIndex(x => x.MessageId).IsUnique();
|
||||
|
||||
modelBuilder.Entity<PlantedCurrency>().HasIndex(x => x.ChannelId);
|
||||
|
||||
configEntity.HasIndex(x => x.WarnExpireHours).IsUnique(false);
|
||||
|
||||
#endregion
|
||||
|
||||
#region streamrole
|
||||
|
||||
modelBuilder.Entity<StreamRoleSettings>().HasOne(x => x.GuildConfig).WithOne(x => x.StreamRole);
|
||||
|
||||
#endregion
|
||||
|
||||
#region Self Assignable Roles
|
||||
|
||||
var selfassignableRolesEntity = modelBuilder.Entity<SelfAssignedRole>();
|
||||
|
||||
selfassignableRolesEntity.HasIndex(s => new
|
||||
{
|
||||
s.GuildId,
|
||||
s.RoleId
|
||||
})
|
||||
.IsUnique();
|
||||
|
||||
selfassignableRolesEntity.Property(x => x.Group).HasDefaultValue(0);
|
||||
|
||||
#endregion
|
||||
|
||||
#region MusicPlaylists
|
||||
|
||||
var musicPlaylistEntity = modelBuilder.Entity<MusicPlaylist>();
|
||||
|
||||
musicPlaylistEntity.HasMany(p => p.Songs).WithOne().OnDelete(DeleteBehavior.Cascade);
|
||||
|
||||
#endregion
|
||||
|
||||
#region Waifus
|
||||
|
||||
var wi = modelBuilder.Entity<WaifuInfo>();
|
||||
wi.HasOne(x => x.Waifu).WithOne();
|
||||
|
||||
wi.HasIndex(x => x.Price);
|
||||
wi.HasIndex(x => x.ClaimerId);
|
||||
// wi.HasMany(x => x.Items)
|
||||
// .WithOne()
|
||||
// .OnDelete(DeleteBehavior.Cascade);
|
||||
|
||||
#endregion
|
||||
|
||||
#region DiscordUser
|
||||
|
||||
modelBuilder.Entity<DiscordUser>(du =>
|
||||
{
|
||||
du.Property(x => x.IsClubAdmin)
|
||||
.HasDefaultValue(false);
|
||||
|
||||
du.Property(x => x.NotifyOnLevelUp)
|
||||
.HasDefaultValue(XpNotificationLocation.None);
|
||||
|
||||
du.Property(x => x.TotalXp)
|
||||
.HasDefaultValue(0);
|
||||
|
||||
du.Property(x => x.CurrencyAmount)
|
||||
.HasDefaultValue(0);
|
||||
|
||||
du.HasAlternateKey(w => w.UserId);
|
||||
du.HasOne(x => x.Club)
|
||||
.WithMany(x => x.Members)
|
||||
.IsRequired(false)
|
||||
.OnDelete(DeleteBehavior.NoAction);
|
||||
|
||||
du.HasIndex(x => x.TotalXp);
|
||||
du.HasIndex(x => x.CurrencyAmount);
|
||||
du.HasIndex(x => x.UserId);
|
||||
});
|
||||
|
||||
#endregion
|
||||
|
||||
#region Warnings
|
||||
|
||||
modelBuilder.Entity<Warning>(warn =>
|
||||
{
|
||||
warn.HasIndex(x => x.GuildId);
|
||||
warn.HasIndex(x => x.UserId);
|
||||
warn.HasIndex(x => x.DateAdded);
|
||||
warn.Property(x => x.Weight).HasDefaultValue(1);
|
||||
});
|
||||
|
||||
#endregion
|
||||
|
||||
#region XpStats
|
||||
|
||||
var xps = modelBuilder.Entity<UserXpStats>();
|
||||
xps.HasIndex(x => new
|
||||
{
|
||||
x.UserId,
|
||||
x.GuildId
|
||||
})
|
||||
.IsUnique();
|
||||
|
||||
xps.HasIndex(x => x.UserId);
|
||||
xps.HasIndex(x => x.GuildId);
|
||||
xps.HasIndex(x => x.Xp);
|
||||
xps.HasIndex(x => x.AwardedXp);
|
||||
|
||||
#endregion
|
||||
|
||||
#region XpSettings
|
||||
|
||||
modelBuilder.Entity<XpSettings>().HasOne(x => x.GuildConfig).WithOne(x => x.XpSettings);
|
||||
|
||||
#endregion
|
||||
|
||||
#region XpRoleReward
|
||||
|
||||
modelBuilder.Entity<XpRoleReward>()
|
||||
.HasIndex(x => new
|
||||
{
|
||||
x.XpSettingsId,
|
||||
x.Level
|
||||
})
|
||||
.IsUnique();
|
||||
|
||||
#endregion
|
||||
|
||||
#region Club
|
||||
|
||||
var ci = modelBuilder.Entity<ClubInfo>();
|
||||
ci.HasOne(x => x.Owner)
|
||||
.WithOne()
|
||||
.HasForeignKey<ClubInfo>(x => x.OwnerId)
|
||||
.OnDelete(DeleteBehavior.SetNull);
|
||||
|
||||
ci.HasAlternateKey(x => new
|
||||
{
|
||||
x.Name
|
||||
});
|
||||
|
||||
#endregion
|
||||
|
||||
#region ClubManytoMany
|
||||
|
||||
modelBuilder.Entity<ClubApplicants>()
|
||||
.HasKey(t => new
|
||||
{
|
||||
t.ClubId,
|
||||
t.UserId
|
||||
});
|
||||
|
||||
modelBuilder.Entity<ClubApplicants>()
|
||||
.HasOne(pt => pt.User)
|
||||
.WithMany();
|
||||
|
||||
modelBuilder.Entity<ClubApplicants>()
|
||||
.HasOne(pt => pt.Club)
|
||||
.WithMany(x => x.Applicants);
|
||||
|
||||
modelBuilder.Entity<ClubBans>()
|
||||
.HasKey(t => new
|
||||
{
|
||||
t.ClubId,
|
||||
t.UserId
|
||||
});
|
||||
|
||||
modelBuilder.Entity<ClubBans>()
|
||||
.HasOne(pt => pt.User)
|
||||
.WithMany();
|
||||
|
||||
modelBuilder.Entity<ClubBans>()
|
||||
.HasOne(pt => pt.Club)
|
||||
.WithMany(x => x.Bans);
|
||||
|
||||
#endregion
|
||||
|
||||
#region Polls
|
||||
|
||||
modelBuilder.Entity<Poll>().HasIndex(x => x.GuildId).IsUnique();
|
||||
|
||||
#endregion
|
||||
|
||||
#region CurrencyTransactions
|
||||
|
||||
modelBuilder.Entity<CurrencyTransaction>(e =>
|
||||
{
|
||||
e.HasIndex(x => x.UserId)
|
||||
.IsUnique(false);
|
||||
|
||||
e.Property(x => x.OtherId)
|
||||
.HasDefaultValueSql(CurrencyTransactionOtherIdDefaultValue);
|
||||
|
||||
e.Property(x => x.Type)
|
||||
.IsRequired();
|
||||
|
||||
e.Property(x => x.Extra)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
#endregion
|
||||
|
||||
#region Reminders
|
||||
|
||||
modelBuilder.Entity<Reminder>().HasIndex(x => x.When);
|
||||
|
||||
#endregion
|
||||
|
||||
#region GroupName
|
||||
|
||||
modelBuilder.Entity<GroupName>()
|
||||
.HasIndex(x => new
|
||||
{
|
||||
x.GuildConfigId,
|
||||
x.Number
|
||||
})
|
||||
.IsUnique();
|
||||
|
||||
modelBuilder.Entity<GroupName>()
|
||||
.HasOne(x => x.GuildConfig)
|
||||
.WithMany(x => x.SelfAssignableRoleGroupNames)
|
||||
.IsRequired();
|
||||
|
||||
#endregion
|
||||
|
||||
#region BanTemplate
|
||||
|
||||
modelBuilder.Entity<BanTemplate>().HasIndex(x => x.GuildId).IsUnique();
|
||||
modelBuilder.Entity<BanTemplate>()
|
||||
.Property(x => x.PruneDays)
|
||||
.HasDefaultValue(null)
|
||||
.IsRequired(false);
|
||||
|
||||
#endregion
|
||||
|
||||
#region Perm Override
|
||||
|
||||
modelBuilder.Entity<DiscordPermOverride>()
|
||||
.HasIndex(x => new
|
||||
{
|
||||
x.GuildId,
|
||||
x.Command
|
||||
})
|
||||
.IsUnique();
|
||||
|
||||
#endregion
|
||||
|
||||
#region Music
|
||||
|
||||
modelBuilder.Entity<MusicPlayerSettings>().HasIndex(x => x.GuildId).IsUnique();
|
||||
|
||||
modelBuilder.Entity<MusicPlayerSettings>().Property(x => x.Volume).HasDefaultValue(100);
|
||||
|
||||
#endregion
|
||||
|
||||
#region Reaction roles
|
||||
|
||||
modelBuilder.Entity<ReactionRoleV2>(rr2 =>
|
||||
{
|
||||
rr2.HasIndex(x => x.GuildId)
|
||||
.IsUnique(false);
|
||||
|
||||
rr2.HasIndex(x => new
|
||||
{
|
||||
x.MessageId,
|
||||
x.Emote
|
||||
})
|
||||
.IsUnique();
|
||||
});
|
||||
|
||||
#endregion
|
||||
|
||||
#region LogSettings
|
||||
|
||||
modelBuilder.Entity<LogSetting>(ls => ls.HasIndex(x => x.GuildId).IsUnique());
|
||||
|
||||
modelBuilder.Entity<LogSetting>(ls => ls
|
||||
.HasMany(x => x.LogIgnores)
|
||||
.WithOne(x => x.LogSetting)
|
||||
.OnDelete(DeleteBehavior.Cascade));
|
||||
|
||||
modelBuilder.Entity<IgnoredLogItem>(ili => ili
|
||||
.HasIndex(x => new
|
||||
{
|
||||
x.LogSettingId,
|
||||
x.LogItemId,
|
||||
x.ItemType
|
||||
})
|
||||
.IsUnique());
|
||||
|
||||
#endregion
|
||||
|
||||
modelBuilder.Entity<ImageOnlyChannel>(ioc => ioc.HasIndex(x => x.ChannelId).IsUnique());
|
||||
|
||||
modelBuilder.Entity<NsfwBlacklistedTag>(nbt => nbt.HasIndex(x => x.GuildId).IsUnique(false));
|
||||
|
||||
var atch = modelBuilder.Entity<AutoTranslateChannel>();
|
||||
atch.HasIndex(x => x.GuildId).IsUnique(false);
|
||||
|
||||
atch.HasIndex(x => x.ChannelId).IsUnique();
|
||||
|
||||
atch.HasMany(x => x.Users).WithOne(x => x.Channel).OnDelete(DeleteBehavior.Cascade);
|
||||
|
||||
modelBuilder.Entity<AutoTranslateUser>(atu => atu.HasAlternateKey(x => new
|
||||
{
|
||||
x.ChannelId,
|
||||
x.UserId
|
||||
}));
|
||||
|
||||
#region BANK
|
||||
|
||||
modelBuilder.Entity<BankUser>(bu => bu.HasIndex(x => x.UserId).IsUnique());
|
||||
|
||||
#endregion
|
||||
|
||||
|
||||
#region Patron
|
||||
|
||||
// currency rewards
|
||||
var pr = modelBuilder.Entity<RewardedUser>();
|
||||
pr.HasIndex(x => x.PlatformUserId).IsUnique();
|
||||
|
||||
// patrons
|
||||
// patrons are not identified by their user id, but by their platform user id
|
||||
// as multiple accounts (even maybe on different platforms) could have
|
||||
// the same account connected to them
|
||||
modelBuilder.Entity<PatronUser>(pu =>
|
||||
{
|
||||
pu.HasIndex(x => x.UniquePlatformUserId).IsUnique();
|
||||
pu.HasKey(x => x.UserId);
|
||||
});
|
||||
|
||||
// quotes are per user id
|
||||
modelBuilder.Entity<PatronQuota>(pq =>
|
||||
{
|
||||
pq.HasIndex(x => x.UserId).IsUnique(false);
|
||||
pq.HasKey(x => new
|
||||
{
|
||||
x.UserId,
|
||||
x.FeatureType,
|
||||
x.Feature
|
||||
});
|
||||
});
|
||||
|
||||
#endregion
|
||||
|
||||
#region Xp Item Shop
|
||||
|
||||
modelBuilder.Entity<XpShopOwnedItem>(
|
||||
x =>
|
||||
{
|
||||
// user can own only one of each item
|
||||
x.HasIndex(model => new
|
||||
{
|
||||
model.UserId,
|
||||
model.ItemType,
|
||||
model.ItemKey
|
||||
})
|
||||
.IsUnique();
|
||||
});
|
||||
|
||||
#endregion
|
||||
|
||||
#region AutoPublish
|
||||
|
||||
modelBuilder.Entity<AutoPublishChannel>(apc => apc
|
||||
.HasIndex(x => x.GuildId)
|
||||
.IsUnique());
|
||||
|
||||
#endregion
|
||||
|
||||
#region GamblingStats
|
||||
|
||||
modelBuilder.Entity<GamblingStats>(gs => gs
|
||||
.HasIndex(x => x.Feature)
|
||||
.IsUnique());
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
||||
#if DEBUG
|
||||
private static readonly ILoggerFactory _debugLoggerFactory = LoggerFactory.Create(x => x.AddConsole());
|
||||
|
||||
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
|
||||
=> optionsBuilder.UseLoggerFactory(_debugLoggerFactory);
|
||||
#endif
|
||||
}
|
|
@ -1,34 +0,0 @@
|
|||
#nullable disable
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using EllieBot.Db.Models;
|
||||
|
||||
namespace EllieBot.Db;
|
||||
|
||||
public static class ClubExtensions
|
||||
{
|
||||
private static IQueryable<ClubInfo> Include(this DbSet<ClubInfo> clubs)
|
||||
=> clubs.Include(x => x.Owner)
|
||||
.Include(x => x.Applicants)
|
||||
.ThenInclude(x => x.User)
|
||||
.Include(x => x.Bans)
|
||||
.ThenInclude(x => x.User)
|
||||
.Include(x => x.Members)
|
||||
.AsQueryable();
|
||||
|
||||
public static ClubInfo GetByOwner(this DbSet<ClubInfo> clubs, ulong userId)
|
||||
=> Include(clubs).FirstOrDefault(c => c.Owner.UserId == userId);
|
||||
|
||||
public static ClubInfo GetByOwnerOrAdmin(this DbSet<ClubInfo> clubs, ulong userId)
|
||||
=> Include(clubs)
|
||||
.FirstOrDefault(c => c.Owner.UserId == userId || c.Members.Any(u => u.UserId == userId && u.IsClubAdmin));
|
||||
|
||||
public static ClubInfo GetByMember(this DbSet<ClubInfo> clubs, ulong userId)
|
||||
=> Include(clubs).FirstOrDefault(c => c.Members.Any(u => u.UserId == userId));
|
||||
|
||||
public static ClubInfo GetByName(this DbSet<ClubInfo> clubs, string name)
|
||||
=> Include(clubs)
|
||||
.FirstOrDefault(c => c.Name == name);
|
||||
|
||||
public static List<ClubInfo> GetClubLeaderboardPage(this DbSet<ClubInfo> clubs, int page)
|
||||
=> clubs.AsNoTracking().OrderByDescending(x => x.Xp).Skip(page * 9).Take(9).ToList();
|
||||
}
|
|
@ -1,20 +0,0 @@
|
|||
#nullable disable
|
||||
using LinqToDB.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using EllieBot.Services.Database.Models;
|
||||
|
||||
namespace EllieBot.Db;
|
||||
|
||||
public static class CurrencyTransactionExtensions
|
||||
{
|
||||
public static Task<List<CurrencyTransaction>> GetPageFor(
|
||||
this DbSet<CurrencyTransaction> set,
|
||||
ulong userId,
|
||||
int page)
|
||||
=> set.ToLinqToDBTable()
|
||||
.Where(x => x.UserId == userId)
|
||||
.OrderByDescending(x => x.DateAdded)
|
||||
.Skip(15 * page)
|
||||
.Take(15)
|
||||
.ToListAsyncLinqToDB();
|
||||
}
|
|
@ -1,12 +0,0 @@
|
|||
#nullable disable
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using EllieBot.Services.Database.Models;
|
||||
|
||||
namespace EllieBot.Db;
|
||||
|
||||
public static class DbExtensions
|
||||
{
|
||||
public static T GetById<T>(this DbSet<T> set, int id)
|
||||
where T : DbEntity
|
||||
=> set.FirstOrDefault(x => x.Id == id);
|
||||
}
|
|
@ -1,179 +0,0 @@
|
|||
#nullable disable
|
||||
using LinqToDB;
|
||||
using LinqToDB.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using EllieBot.Db.Models;
|
||||
using EllieBot.Services.Database;
|
||||
using System.Collections.Immutable;
|
||||
|
||||
namespace EllieBot.Db;
|
||||
|
||||
public static class DiscordUserExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Adds the specified <paramref name="users"/> to the database. If a database user with placeholder name
|
||||
/// and discriminator is present in <paramref name="users"/>, their name and discriminator get updated accordingly.
|
||||
/// </summary>
|
||||
/// <param name="ctx">This database context.</param>
|
||||
/// <param name="users">The users to add or update in the database.</param>
|
||||
/// <returns>A tuple with the amount of new users added and old users updated.</returns>
|
||||
public static async Task<(long UsersAdded, long UsersUpdated)> RefreshUsersAsync(this EllieContext ctx, List<IUser> users)
|
||||
{
|
||||
var presentDbUsers = await ctx.DiscordUser
|
||||
.Select(x => new { x.UserId, x.Username, x.Discriminator })
|
||||
.Where(x => users.Select(y => y.Id).Contains(x.UserId))
|
||||
.ToArrayAsyncEF();
|
||||
|
||||
var usersToAdd = users
|
||||
.Where(x => !presentDbUsers.Select(x => x.UserId).Contains(x.Id))
|
||||
.Select(x => new DiscordUser()
|
||||
{
|
||||
UserId = x.Id,
|
||||
AvatarId = x.AvatarId,
|
||||
Username = x.Username,
|
||||
Discriminator = x.Discriminator
|
||||
});
|
||||
|
||||
var added = (await ctx.BulkCopyAsync(usersToAdd)).RowsCopied;
|
||||
var toUpdateUserIds = presentDbUsers
|
||||
.Where(x => x.Username == "Unknown" && x.Discriminator == "????")
|
||||
.Select(x => x.UserId)
|
||||
.ToArray();
|
||||
|
||||
foreach (var user in users.Where(x => toUpdateUserIds.Contains(x.Id)))
|
||||
{
|
||||
await ctx.DiscordUser
|
||||
.Where(x => x.UserId == user.Id)
|
||||
.UpdateAsync(x => new DiscordUser()
|
||||
{
|
||||
Username = user.Username,
|
||||
Discriminator = user.Discriminator,
|
||||
|
||||
// .award tends to set AvatarId and DateAdded to NULL, so account for that.
|
||||
AvatarId = user.AvatarId,
|
||||
DateAdded = x.DateAdded ?? DateTime.UtcNow
|
||||
});
|
||||
}
|
||||
|
||||
return (added, toUpdateUserIds.Length);
|
||||
}
|
||||
|
||||
public static Task<DiscordUser> GetByUserIdAsync(
|
||||
this IQueryable<DiscordUser> set,
|
||||
ulong userId)
|
||||
=> set.FirstOrDefaultAsyncLinqToDB(x => x.UserId == userId);
|
||||
|
||||
public static void EnsureUserCreated(
|
||||
this EllieContext ctx,
|
||||
ulong userId,
|
||||
string username,
|
||||
string discrim,
|
||||
string avatarId)
|
||||
=> ctx.DiscordUser.ToLinqToDBTable()
|
||||
.InsertOrUpdate(
|
||||
() => new()
|
||||
{
|
||||
UserId = userId,
|
||||
Username = username,
|
||||
Discriminator = discrim,
|
||||
AvatarId = avatarId,
|
||||
TotalXp = 0,
|
||||
CurrencyAmount = 0
|
||||
},
|
||||
old => new()
|
||||
{
|
||||
Username = username,
|
||||
Discriminator = discrim,
|
||||
AvatarId = avatarId
|
||||
},
|
||||
() => new()
|
||||
{
|
||||
UserId = userId
|
||||
});
|
||||
|
||||
public static Task EnsureUserCreatedAsync(
|
||||
this EllieContext ctx,
|
||||
ulong userId)
|
||||
=> ctx.DiscordUser
|
||||
.ToLinqToDBTable()
|
||||
.InsertOrUpdateAsync(
|
||||
() => new()
|
||||
{
|
||||
UserId = userId,
|
||||
Username = "Unknown",
|
||||
Discriminator = "????",
|
||||
AvatarId = string.Empty,
|
||||
TotalXp = 0,
|
||||
CurrencyAmount = 0
|
||||
},
|
||||
old => new()
|
||||
{
|
||||
|
||||
},
|
||||
() => new()
|
||||
{
|
||||
UserId = userId
|
||||
});
|
||||
|
||||
//temp is only used in updatecurrencystate, so that i don't overwrite real usernames/discrims with Unknown
|
||||
public static DiscordUser GetOrCreateUser(
|
||||
this EllieContext ctx,
|
||||
ulong userId,
|
||||
string username,
|
||||
string discrim,
|
||||
string avatarId,
|
||||
Func<IQueryable<DiscordUser>, IQueryable<DiscordUser>> includes = null)
|
||||
{
|
||||
ctx.EnsureUserCreated(userId, username, discrim, avatarId);
|
||||
|
||||
IQueryable<DiscordUser> queryable = ctx.DiscordUser;
|
||||
if (includes is not null)
|
||||
queryable = includes(queryable);
|
||||
return queryable.First(u => u.UserId == userId);
|
||||
}
|
||||
|
||||
public static DiscordUser GetOrCreateUser(this EllieContext ctx, IUser original, Func<IQueryable<DiscordUser>, IQueryable<DiscordUser>> includes = null)
|
||||
=> ctx.GetOrCreateUser(original.Id, original.Username, original.Discriminator, original.AvatarId, includes);
|
||||
|
||||
public static int GetUserGlobalRank(this DbSet<DiscordUser> users, ulong id)
|
||||
=> users.AsQueryable()
|
||||
.Where(x => x.TotalXp
|
||||
> users.AsQueryable().Where(y => y.UserId == id).Select(y => y.TotalXp).FirstOrDefault())
|
||||
.Count()
|
||||
+ 1;
|
||||
|
||||
public static DiscordUser[] GetUsersXpLeaderboardFor(this DbSet<DiscordUser> users, int page)
|
||||
=> users.AsQueryable().OrderByDescending(x => x.TotalXp).Skip(page * 9).Take(9).AsEnumerable().ToArray();
|
||||
|
||||
public static List<DiscordUser> GetTopRichest(
|
||||
this DbSet<DiscordUser> users,
|
||||
ulong botId,
|
||||
int count,
|
||||
int page = 0)
|
||||
=> users.AsQueryable()
|
||||
.Where(c => c.CurrencyAmount > 0 && botId != c.UserId)
|
||||
.OrderByDescending(c => c.CurrencyAmount)
|
||||
.Skip(page * 9)
|
||||
.Take(count)
|
||||
.ToList();
|
||||
|
||||
public static async Task<long> GetUserCurrencyAsync(this DbSet<DiscordUser> users, ulong userId)
|
||||
=> (await users.FirstOrDefaultAsyncLinqToDB(x => x.UserId == userId))?.CurrencyAmount ?? 0;
|
||||
|
||||
public static void RemoveFromMany(this DbSet<DiscordUser> users, IEnumerable<ulong> ids)
|
||||
{
|
||||
var items = users.AsQueryable().Where(x => ids.Contains(x.UserId));
|
||||
foreach (var item in items)
|
||||
item.CurrencyAmount = 0;
|
||||
}
|
||||
|
||||
public static decimal GetTotalCurrency(this DbSet<DiscordUser> users)
|
||||
=> users.Sum((Func<DiscordUser, decimal>)(x => x.CurrencyAmount));
|
||||
|
||||
public static decimal GetTopOnePercentCurrency(this DbSet<DiscordUser> users, ulong botId)
|
||||
=> users.AsQueryable()
|
||||
.Where(x => x.UserId != botId)
|
||||
.OrderByDescending(x => x.CurrencyAmount)
|
||||
.Take(users.Count() / 100 == 0 ? 1 : users.Count() / 100)
|
||||
.Sum(x => x.CurrencyAmount);
|
||||
}
|
|
@ -1,15 +0,0 @@
|
|||
#nullable disable
|
||||
using LinqToDB;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using EllieBot.Services.Database.Models;
|
||||
|
||||
namespace EllieBot.Db;
|
||||
|
||||
public static class EllieExpressionExtensions
|
||||
{
|
||||
public static int ClearFromGuild(this DbSet<EllieExpression> exprs, ulong guildId)
|
||||
=> exprs.Delete(x => x.GuildId == guildId);
|
||||
|
||||
public static IEnumerable<EllieExpression> ForId(this DbSet<EllieExpression> exprs, ulong id)
|
||||
=> exprs.AsNoTracking().AsQueryable().Where(x => x.GuildId == id).ToList();
|
||||
}
|
|
@ -1,227 +0,0 @@
|
|||
#nullable disable
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using EllieBot.Db.Models;
|
||||
using EllieBot.Services.Database;
|
||||
using EllieBot.Services.Database.Models;
|
||||
|
||||
namespace EllieBot.Db;
|
||||
|
||||
public static class GuildConfigExtensions
|
||||
{
|
||||
private static List<WarningPunishment> DefaultWarnPunishments
|
||||
=> new()
|
||||
{
|
||||
new()
|
||||
{
|
||||
Count = 3,
|
||||
Punishment = PunishmentAction.Kick
|
||||
},
|
||||
new()
|
||||
{
|
||||
Count = 5,
|
||||
Punishment = PunishmentAction.Ban
|
||||
}
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Gets full stream role settings for the guild with the specified id.
|
||||
/// </summary>
|
||||
/// <param name="ctx">Db Context</param>
|
||||
/// <param name="guildId">Id of the guild to get stream role settings for.</param>
|
||||
/// <returns>Guild'p stream role settings</returns>
|
||||
public static StreamRoleSettings GetStreamRoleSettings(this EllieContext ctx, ulong guildId)
|
||||
{
|
||||
var conf = ctx.GuildConfigsForId(guildId,
|
||||
set => set.Include(y => y.StreamRole)
|
||||
.Include(y => y.StreamRole.Whitelist)
|
||||
.Include(y => y.StreamRole.Blacklist));
|
||||
|
||||
if (conf.StreamRole is null)
|
||||
conf.StreamRole = new();
|
||||
|
||||
return conf.StreamRole;
|
||||
}
|
||||
|
||||
private static IQueryable<GuildConfig> IncludeEverything(this DbSet<GuildConfig> configs)
|
||||
=> configs.AsQueryable()
|
||||
.AsSplitQuery()
|
||||
.Include(gc => gc.CommandCooldowns)
|
||||
.Include(gc => gc.FollowedStreams)
|
||||
.Include(gc => gc.StreamRole)
|
||||
.Include(gc => gc.XpSettings)
|
||||
.ThenInclude(x => x.ExclusionList)
|
||||
.Include(gc => gc.DelMsgOnCmdChannels);
|
||||
|
||||
public static IEnumerable<GuildConfig> GetAllGuildConfigs(
|
||||
this DbSet<GuildConfig> configs,
|
||||
List<ulong> availableGuilds)
|
||||
=> configs.IncludeEverything().AsNoTracking().Where(x => availableGuilds.Contains(x.GuildId)).ToList();
|
||||
|
||||
/// <summary>
|
||||
/// Gets and creates if it doesn't exist a config for a guild.
|
||||
/// </summary>
|
||||
/// <param name="ctx">Context</param>
|
||||
/// <param name="guildId">Id of the guide</param>
|
||||
/// <param name="includes">Use to manipulate the set however you want. Pass null to include everything</param>
|
||||
/// <returns>Config for the guild</returns>
|
||||
public static GuildConfig GuildConfigsForId(
|
||||
this EllieContext ctx,
|
||||
ulong guildId,
|
||||
Func<DbSet<GuildConfig>, IQueryable<GuildConfig>> includes)
|
||||
{
|
||||
GuildConfig config;
|
||||
|
||||
if (includes is null)
|
||||
config = ctx.GuildConfigs.IncludeEverything().FirstOrDefault(c => c.GuildId == guildId);
|
||||
else
|
||||
{
|
||||
var set = includes(ctx.GuildConfigs);
|
||||
config = set.FirstOrDefault(c => c.GuildId == guildId);
|
||||
}
|
||||
|
||||
if (config is null)
|
||||
{
|
||||
ctx.GuildConfigs.Add(config = new()
|
||||
{
|
||||
GuildId = guildId,
|
||||
Permissions = Permissionv2.GetDefaultPermlist,
|
||||
WarningsInitialized = true,
|
||||
WarnPunishments = DefaultWarnPunishments
|
||||
});
|
||||
ctx.SaveChanges();
|
||||
}
|
||||
|
||||
if (!config.WarningsInitialized)
|
||||
{
|
||||
config.WarningsInitialized = true;
|
||||
config.WarnPunishments = DefaultWarnPunishments;
|
||||
}
|
||||
|
||||
return config;
|
||||
|
||||
// ctx.GuildConfigs
|
||||
// .ToLinqToDBTable()
|
||||
// .InsertOrUpdate(() => new()
|
||||
// {
|
||||
// GuildId = guildId,
|
||||
// Permissions = Permissionv2.GetDefaultPermlist,
|
||||
// WarningsInitialized = true,
|
||||
// WarnPunishments = DefaultWarnPunishments
|
||||
// },
|
||||
// _ => new(),
|
||||
// () => new()
|
||||
// {
|
||||
// GuildId = guildId
|
||||
// });
|
||||
//
|
||||
// if(includes is null)
|
||||
// return ctx.GuildConfigs
|
||||
// .ToLinqToDBTable()
|
||||
// .First(x => x.GuildId == guildId);
|
||||
}
|
||||
|
||||
public static LogSetting LogSettingsFor(this EllieContext ctx, ulong guildId)
|
||||
{
|
||||
var logSetting = ctx.LogSettings.AsQueryable()
|
||||
.Include(x => x.LogIgnores)
|
||||
.Where(x => x.GuildId == guildId)
|
||||
.FirstOrDefault();
|
||||
|
||||
if (logSetting is null)
|
||||
{
|
||||
ctx.LogSettings.Add(logSetting = new()
|
||||
{
|
||||
GuildId = guildId
|
||||
});
|
||||
ctx.SaveChanges();
|
||||
}
|
||||
|
||||
return logSetting;
|
||||
}
|
||||
|
||||
public static IEnumerable<GuildConfig> PermissionsForAll(this DbSet<GuildConfig> configs, List<ulong> include)
|
||||
{
|
||||
var query = configs.AsQueryable().Where(x => include.Contains(x.GuildId)).Include(gc => gc.Permissions);
|
||||
|
||||
return query.ToList();
|
||||
}
|
||||
|
||||
public static GuildConfig GcWithPermissionsFor(this EllieContext ctx, ulong guildId)
|
||||
{
|
||||
var config = ctx.GuildConfigs.AsQueryable()
|
||||
.Where(gc => gc.GuildId == guildId)
|
||||
.Include(gc => gc.Permissions)
|
||||
.FirstOrDefault();
|
||||
|
||||
if (config is null) // if there is no guildconfig, create new one
|
||||
{
|
||||
ctx.GuildConfigs.Add(config = new()
|
||||
{
|
||||
GuildId = guildId,
|
||||
Permissions = Permissionv2.GetDefaultPermlist
|
||||
});
|
||||
ctx.SaveChanges();
|
||||
}
|
||||
else if (config.Permissions is null || !config.Permissions.Any()) // if no perms, add default ones
|
||||
{
|
||||
config.Permissions = Permissionv2.GetDefaultPermlist;
|
||||
ctx.SaveChanges();
|
||||
}
|
||||
|
||||
return config;
|
||||
}
|
||||
|
||||
public static IEnumerable<FollowedStream> GetFollowedStreams(this DbSet<GuildConfig> configs)
|
||||
=> configs.AsQueryable().Include(x => x.FollowedStreams).SelectMany(gc => gc.FollowedStreams).ToArray();
|
||||
|
||||
public static IEnumerable<FollowedStream> GetFollowedStreams(this DbSet<GuildConfig> configs, List<ulong> included)
|
||||
=> configs.AsQueryable()
|
||||
.Where(gc => included.Contains(gc.GuildId))
|
||||
.Include(gc => gc.FollowedStreams)
|
||||
.SelectMany(gc => gc.FollowedStreams)
|
||||
.ToList();
|
||||
|
||||
public static void SetCleverbotEnabled(this DbSet<GuildConfig> configs, ulong id, bool cleverbotEnabled)
|
||||
{
|
||||
var conf = configs.FirstOrDefault(gc => gc.GuildId == id);
|
||||
|
||||
if (conf is null)
|
||||
return;
|
||||
|
||||
conf.CleverbotEnabled = cleverbotEnabled;
|
||||
}
|
||||
|
||||
public static XpSettings XpSettingsFor(this EllieContext ctx, ulong guildId)
|
||||
{
|
||||
var gc = ctx.GuildConfigsForId(guildId,
|
||||
set => set.Include(x => x.XpSettings)
|
||||
.ThenInclude(x => x.RoleRewards)
|
||||
.Include(x => x.XpSettings)
|
||||
.ThenInclude(x => x.CurrencyRewards)
|
||||
.Include(x => x.XpSettings)
|
||||
.ThenInclude(x => x.ExclusionList));
|
||||
|
||||
if (gc.XpSettings is null)
|
||||
gc.XpSettings = new();
|
||||
|
||||
return gc.XpSettings;
|
||||
}
|
||||
|
||||
public static IEnumerable<GeneratingChannel> GetGeneratingChannels(this DbSet<GuildConfig> configs)
|
||||
=> configs.AsQueryable()
|
||||
.Include(x => x.GenerateCurrencyChannelIds)
|
||||
.Where(x => x.GenerateCurrencyChannelIds.Any())
|
||||
.SelectMany(x => x.GenerateCurrencyChannelIds)
|
||||
.Select(x => new GeneratingChannel
|
||||
{
|
||||
ChannelId = x.ChannelId,
|
||||
GuildId = x.GuildConfig.GuildId
|
||||
})
|
||||
.ToArray();
|
||||
|
||||
public class GeneratingChannel
|
||||
{
|
||||
public ulong GuildId { get; set; }
|
||||
public ulong ChannelId { get; set; }
|
||||
}
|
||||
}
|
|
@ -1,27 +0,0 @@
|
|||
#nullable disable
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using EllieBot.Services.Database.Models;
|
||||
|
||||
namespace EllieBot.Db;
|
||||
|
||||
public static class MusicPlayerSettingsExtensions
|
||||
{
|
||||
public static async Task<MusicPlayerSettings> ForGuildAsync(this DbSet<MusicPlayerSettings> settings, ulong guildId)
|
||||
{
|
||||
var toReturn = await settings.AsQueryable().FirstOrDefaultAsync(x => x.GuildId == guildId);
|
||||
|
||||
if (toReturn is null)
|
||||
{
|
||||
var newSettings = new MusicPlayerSettings
|
||||
{
|
||||
GuildId = guildId,
|
||||
PlayerRepeat = PlayerRepeatType.Queue
|
||||
};
|
||||
|
||||
await settings.AddAsync(newSettings);
|
||||
return newSettings;
|
||||
}
|
||||
|
||||
return toReturn;
|
||||
}
|
||||
}
|
|
@ -1,19 +0,0 @@
|
|||
#nullable disable
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using EllieBot.Services.Database.Models;
|
||||
|
||||
namespace EllieBot.Db;
|
||||
|
||||
public static class MusicPlaylistExtensions
|
||||
{
|
||||
public static List<MusicPlaylist> GetPlaylistsOnPage(this DbSet<MusicPlaylist> playlists, int num)
|
||||
{
|
||||
if (num < 1)
|
||||
throw new ArgumentOutOfRangeException(nameof(num));
|
||||
|
||||
return playlists.AsQueryable().Skip((num - 1) * 20).Take(20).Include(pl => pl.Songs).ToList();
|
||||
}
|
||||
|
||||
public static MusicPlaylist GetWithSongs(this DbSet<MusicPlaylist> playlists, int id)
|
||||
=> playlists.Include(mpl => mpl.Songs).FirstOrDefault(mpl => mpl.Id == id);
|
||||
}
|
|
@ -1,34 +0,0 @@
|
|||
#nullable disable
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using EllieBot.Services.Database;
|
||||
using EllieBot.Services.Database.Models;
|
||||
|
||||
namespace EllieBot.Db;
|
||||
|
||||
public static class PollExtensions
|
||||
{
|
||||
public static IEnumerable<Poll> GetAllPolls(this DbSet<Poll> polls)
|
||||
=> polls.Include(x => x.Answers).Include(x => x.Votes).ToArray();
|
||||
|
||||
public static void RemovePoll(this EllieContext ctx, int id)
|
||||
{
|
||||
var p = ctx.Poll.Include(x => x.Answers).Include(x => x.Votes).FirstOrDefault(x => x.Id == id);
|
||||
|
||||
if (p is null)
|
||||
return;
|
||||
|
||||
if (p.Votes is not null)
|
||||
{
|
||||
ctx.RemoveRange(p.Votes);
|
||||
p.Votes.Clear();
|
||||
}
|
||||
|
||||
if (p.Answers is not null)
|
||||
{
|
||||
ctx.RemoveRange(p.Answers);
|
||||
p.Answers.Clear();
|
||||
}
|
||||
|
||||
ctx.Poll.Remove(p);
|
||||
}
|
||||
}
|
|
@ -1,57 +0,0 @@
|
|||
#nullable disable
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using EllieBot.Services.Database.Models;
|
||||
|
||||
namespace EllieBot.Db;
|
||||
|
||||
public static class QuoteExtensions
|
||||
{
|
||||
public static IEnumerable<Quote> GetForGuild(this DbSet<Quote> quotes, ulong guildId)
|
||||
=> quotes.AsQueryable().Where(x => x.GuildId == guildId);
|
||||
|
||||
public static IReadOnlyCollection<Quote> GetGroup(
|
||||
this DbSet<Quote> quotes,
|
||||
ulong guildId,
|
||||
int page,
|
||||
OrderType order)
|
||||
{
|
||||
var q = quotes.AsQueryable().Where(x => x.GuildId == guildId);
|
||||
if (order == OrderType.Keyword)
|
||||
q = q.OrderBy(x => x.Keyword);
|
||||
else
|
||||
q = q.OrderBy(x => x.Id);
|
||||
|
||||
return q.Skip(15 * page).Take(15).ToArray();
|
||||
}
|
||||
|
||||
public static async Task<Quote> GetRandomQuoteByKeywordAsync(
|
||||
this DbSet<Quote> quotes,
|
||||
ulong guildId,
|
||||
string keyword)
|
||||
{
|
||||
var rng = new EllieRandom();
|
||||
return (await quotes.AsQueryable().Where(q => q.GuildId == guildId && q.Keyword == keyword).ToListAsync())
|
||||
.OrderBy(_ => rng.Next())
|
||||
.FirstOrDefault();
|
||||
}
|
||||
|
||||
public static async Task<Quote> SearchQuoteKeywordTextAsync(
|
||||
this DbSet<Quote> quotes,
|
||||
ulong guildId,
|
||||
string keyword,
|
||||
string text)
|
||||
{
|
||||
var rngk = new EllieRandom();
|
||||
return (await quotes.AsQueryable()
|
||||
.Where(q => q.GuildId == guildId
|
||||
&& (keyword == null || q.Keyword == keyword)
|
||||
&& (EF.Functions.Like(q.Text.ToUpper(), $"%{text.ToUpper()}%")
|
||||
|| EF.Functions.Like(q.AuthorName, text)))
|
||||
.ToListAsync())
|
||||
.OrderBy(_ => rngk.Next())
|
||||
.FirstOrDefault();
|
||||
}
|
||||
|
||||
public static void RemoveAllByKeyword(this DbSet<Quote> quotes, ulong guildId, string keyword)
|
||||
=> quotes.RemoveRange(quotes.AsQueryable().Where(x => x.GuildId == guildId && x.Keyword.ToUpper() == keyword));
|
||||
}
|
|
@ -1,23 +0,0 @@
|
|||
#nullable disable
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using EllieBot.Services.Database.Models;
|
||||
|
||||
namespace EllieBot.Db;
|
||||
|
||||
public static class ReminderExtensions
|
||||
{
|
||||
public static IEnumerable<Reminder> GetIncludedReminders(
|
||||
this DbSet<Reminder> reminders,
|
||||
IEnumerable<ulong> guildIds)
|
||||
=> reminders.AsQueryable().Where(x => guildIds.Contains(x.ServerId) || x.ServerId == 0).ToList();
|
||||
|
||||
public static IEnumerable<Reminder> RemindersFor(this DbSet<Reminder> reminders, ulong userId, int page)
|
||||
=> reminders.AsQueryable().Where(x => x.UserId == userId).OrderBy(x => x.DateAdded).Skip(page * 10).Take(10);
|
||||
|
||||
public static IEnumerable<Reminder> RemindersForServer(this DbSet<Reminder> reminders, ulong serverId, int page)
|
||||
=> reminders.AsQueryable()
|
||||
.Where(x => x.ServerId == serverId)
|
||||
.OrderBy(x => x.DateAdded)
|
||||
.Skip(page * 10)
|
||||
.Take(10);
|
||||
}
|
|
@ -1,22 +0,0 @@
|
|||
#nullable disable
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using EllieBot.Services.Database.Models;
|
||||
|
||||
namespace EllieBot.Db;
|
||||
|
||||
public static class SelfAssignableRolesExtensions
|
||||
{
|
||||
public static bool DeleteByGuildAndRoleId(this DbSet<SelfAssignedRole> roles, ulong guildId, ulong roleId)
|
||||
{
|
||||
var role = roles.FirstOrDefault(s => s.GuildId == guildId && s.RoleId == roleId);
|
||||
|
||||
if (role is null)
|
||||
return false;
|
||||
|
||||
roles.Remove(role);
|
||||
return true;
|
||||
}
|
||||
|
||||
public static IReadOnlyCollection<SelfAssignedRole> GetFromGuild(this DbSet<SelfAssignedRole> roles, ulong guildId)
|
||||
=> roles.AsQueryable().Where(s => s.GuildId == guildId).ToArray();
|
||||
}
|
|
@ -1,63 +0,0 @@
|
|||
#nullable disable
|
||||
using LinqToDB;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using EllieBot.Services.Database;
|
||||
using EllieBot.Services.Database.Models;
|
||||
|
||||
namespace EllieBot.Db;
|
||||
|
||||
public static class UserXpExtensions
|
||||
{
|
||||
public static UserXpStats GetOrCreateUserXpStats(this EllieContext ctx, ulong guildId, ulong userId)
|
||||
{
|
||||
var usr = ctx.UserXpStats.FirstOrDefault(x => x.UserId == userId && x.GuildId == guildId);
|
||||
|
||||
if (usr is null)
|
||||
{
|
||||
ctx.Add(usr = new()
|
||||
{
|
||||
Xp = 0,
|
||||
UserId = userId,
|
||||
NotifyOnLevelUp = XpNotificationLocation.None,
|
||||
GuildId = guildId
|
||||
});
|
||||
}
|
||||
|
||||
return usr;
|
||||
}
|
||||
|
||||
public static List<UserXpStats> GetUsersFor(this DbSet<UserXpStats> xps, ulong guildId, int page)
|
||||
=> xps.AsQueryable()
|
||||
.AsNoTracking()
|
||||
.Where(x => x.GuildId == guildId)
|
||||
.OrderByDescending(x => x.Xp + x.AwardedXp)
|
||||
.Skip(page * 9)
|
||||
.Take(9)
|
||||
.ToList();
|
||||
|
||||
public static List<UserXpStats> GetTopUserXps(this DbSet<UserXpStats> xps, ulong guildId, int count)
|
||||
=> xps.AsQueryable()
|
||||
.AsNoTracking()
|
||||
.Where(x => x.GuildId == guildId)
|
||||
.OrderByDescending(x => x.Xp + x.AwardedXp)
|
||||
.Take(count)
|
||||
.ToList();
|
||||
|
||||
public static int GetUserGuildRanking(this DbSet<UserXpStats> xps, ulong userId, ulong guildId)
|
||||
=> xps.AsQueryable()
|
||||
.AsNoTracking()
|
||||
.Where(x => x.GuildId == guildId
|
||||
&& x.Xp + x.AwardedXp
|
||||
> xps.AsQueryable()
|
||||
.Where(y => y.UserId == userId && y.GuildId == guildId)
|
||||
.Select(y => y.Xp + y.AwardedXp)
|
||||
.FirstOrDefault())
|
||||
.Count()
|
||||
+ 1;
|
||||
|
||||
public static void ResetGuildUserXp(this DbSet<UserXpStats> xps, ulong userId, ulong guildId)
|
||||
=> xps.Delete(x => x.UserId == userId && x.GuildId == guildId);
|
||||
|
||||
public static void ResetGuildXp(this DbSet<UserXpStats> xps, ulong guildId)
|
||||
=> xps.Delete(x => x.GuildId == guildId);
|
||||
}
|
|
@ -1,145 +0,0 @@
|
|||
#nullable disable
|
||||
using LinqToDB;
|
||||
using LinqToDB.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using EllieBot.Db.Models;
|
||||
using EllieBot.Services.Database;
|
||||
using EllieBot.Services.Database.Models;
|
||||
|
||||
namespace EllieBot.Db;
|
||||
|
||||
public class WaifuInfoStats
|
||||
{
|
||||
public int WaifuId { get; init; }
|
||||
public string FullName { get; init; }
|
||||
public long Price { get; init; }
|
||||
public string ClaimerName { get; init; }
|
||||
public string AffinityName { get; init; }
|
||||
public int AffinityCount { get; init; }
|
||||
public int DivorceCount { get; init; }
|
||||
public int ClaimCount { get; init; }
|
||||
}
|
||||
|
||||
public static class WaifuExtensions
|
||||
{
|
||||
public static WaifuInfo ByWaifuUserId(
|
||||
this DbSet<WaifuInfo> waifus,
|
||||
ulong userId,
|
||||
Func<DbSet<WaifuInfo>, IQueryable<WaifuInfo>> includes = null)
|
||||
{
|
||||
if (includes is null)
|
||||
{
|
||||
return waifus.Include(wi => wi.Waifu)
|
||||
.Include(wi => wi.Affinity)
|
||||
.Include(wi => wi.Claimer)
|
||||
.Include(wi => wi.Items)
|
||||
.FirstOrDefault(wi => wi.Waifu.UserId == userId);
|
||||
}
|
||||
|
||||
return includes(waifus).AsQueryable().FirstOrDefault(wi => wi.Waifu.UserId == userId);
|
||||
}
|
||||
|
||||
public static IEnumerable<WaifuLbResult> GetTop(this DbSet<WaifuInfo> waifus, int count, int skip = 0)
|
||||
{
|
||||
if (count < 0)
|
||||
throw new ArgumentOutOfRangeException(nameof(count));
|
||||
if (count == 0)
|
||||
return new List<WaifuLbResult>();
|
||||
|
||||
return waifus.Include(wi => wi.Waifu)
|
||||
.Include(wi => wi.Affinity)
|
||||
.Include(wi => wi.Claimer)
|
||||
.OrderByDescending(wi => wi.Price)
|
||||
.Skip(skip)
|
||||
.Take(count)
|
||||
.Select(x => new WaifuLbResult
|
||||
{
|
||||
Affinity = x.Affinity == null ? null : x.Affinity.Username,
|
||||
AffinityDiscrim = x.Affinity == null ? null : x.Affinity.Discriminator,
|
||||
Claimer = x.Claimer == null ? null : x.Claimer.Username,
|
||||
ClaimerDiscrim = x.Claimer == null ? null : x.Claimer.Discriminator,
|
||||
Username = x.Waifu.Username,
|
||||
Discrim = x.Waifu.Discriminator,
|
||||
Price = x.Price
|
||||
})
|
||||
.ToList();
|
||||
}
|
||||
|
||||
public static decimal GetTotalValue(this DbSet<WaifuInfo> waifus)
|
||||
=> waifus.AsQueryable().Where(x => x.ClaimerId != null).Sum(x => x.Price);
|
||||
|
||||
public static ulong GetWaifuUserId(this DbSet<WaifuInfo> waifus, ulong ownerId, string name)
|
||||
=> waifus.AsQueryable()
|
||||
.AsNoTracking()
|
||||
.Where(x => x.Claimer.UserId == ownerId && x.Waifu.Username + "#" + x.Waifu.Discriminator == name)
|
||||
.Select(x => x.Waifu.UserId)
|
||||
.FirstOrDefault();
|
||||
|
||||
public static async Task<WaifuInfoStats> GetWaifuInfoAsync(this EllieContext ctx, ulong userId)
|
||||
{
|
||||
await ctx.WaifuInfo
|
||||
.ToLinqToDBTable()
|
||||
.InsertOrUpdateAsync(() => new()
|
||||
{
|
||||
AffinityId = null,
|
||||
ClaimerId = null,
|
||||
Price = 1,
|
||||
WaifuId = ctx.DiscordUser.Where(x => x.UserId == userId).Select(x => x.Id).First()
|
||||
},
|
||||
_ => new(),
|
||||
() => new()
|
||||
{
|
||||
WaifuId = ctx.DiscordUser.Where(x => x.UserId == userId).Select(x => x.Id).First()
|
||||
});
|
||||
|
||||
var toReturn = ctx.WaifuInfo.AsQueryable()
|
||||
.Where(w => w.WaifuId
|
||||
== ctx.Set<DiscordUser>()
|
||||
.AsQueryable()
|
||||
.Where(u => u.UserId == userId)
|
||||
.Select(u => u.Id)
|
||||
.FirstOrDefault())
|
||||
.Select(w => new WaifuInfoStats
|
||||
{
|
||||
WaifuId = w.WaifuId,
|
||||
FullName =
|
||||
ctx.Set<DiscordUser>()
|
||||
.AsQueryable()
|
||||
.Where(u => u.UserId == userId)
|
||||
.Select(u => u.Username + "#" + u.Discriminator)
|
||||
.FirstOrDefault(),
|
||||
AffinityCount =
|
||||
ctx.Set<WaifuUpdate>()
|
||||
.AsQueryable()
|
||||
.Count(x => x.UserId == w.WaifuId
|
||||
&& x.UpdateType == WaifuUpdateType.AffinityChanged
|
||||
&& x.NewId != null),
|
||||
AffinityName =
|
||||
ctx.Set<DiscordUser>()
|
||||
.AsQueryable()
|
||||
.Where(u => u.Id == w.AffinityId)
|
||||
.Select(u => u.Username + "#" + u.Discriminator)
|
||||
.FirstOrDefault(),
|
||||
ClaimCount = ctx.WaifuInfo.AsQueryable().Count(x => x.ClaimerId == w.WaifuId),
|
||||
ClaimerName =
|
||||
ctx.Set<DiscordUser>()
|
||||
.AsQueryable()
|
||||
.Where(u => u.Id == w.ClaimerId)
|
||||
.Select(u => u.Username + "#" + u.Discriminator)
|
||||
.FirstOrDefault(),
|
||||
DivorceCount =
|
||||
ctx.Set<WaifuUpdate>()
|
||||
.AsQueryable()
|
||||
.Count(x => x.OldId == w.WaifuId
|
||||
&& x.NewId == null
|
||||
&& x.UpdateType == WaifuUpdateType.Claimed),
|
||||
Price = w.Price,
|
||||
})
|
||||
.FirstOrDefault();
|
||||
|
||||
if (toReturn is null)
|
||||
return null;
|
||||
|
||||
return toReturn;
|
||||
}
|
||||
}
|
|
@ -1,60 +0,0 @@
|
|||
#nullable disable
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using EllieBot.Services.Database.Models;
|
||||
|
||||
namespace EllieBot.Db;
|
||||
|
||||
public static class WarningExtensions
|
||||
{
|
||||
public static Warning[] ForId(this DbSet<Warning> warnings, ulong guildId, ulong userId)
|
||||
{
|
||||
var query = warnings.AsQueryable()
|
||||
.Where(x => x.GuildId == guildId && x.UserId == userId)
|
||||
.OrderByDescending(x => x.DateAdded);
|
||||
|
||||
return query.ToArray();
|
||||
}
|
||||
|
||||
public static bool Forgive(
|
||||
this DbSet<Warning> warnings,
|
||||
ulong guildId,
|
||||
ulong userId,
|
||||
string mod,
|
||||
int index)
|
||||
{
|
||||
if (index < 0)
|
||||
throw new ArgumentOutOfRangeException(nameof(index));
|
||||
|
||||
var warn = warnings.AsQueryable()
|
||||
.Where(x => x.GuildId == guildId && x.UserId == userId)
|
||||
.OrderByDescending(x => x.DateAdded)
|
||||
.Skip(index)
|
||||
.FirstOrDefault();
|
||||
|
||||
if (warn is null || warn.Forgiven)
|
||||
return false;
|
||||
|
||||
warn.Forgiven = true;
|
||||
warn.ForgivenBy = mod;
|
||||
return true;
|
||||
}
|
||||
|
||||
public static async Task ForgiveAll(
|
||||
this DbSet<Warning> warnings,
|
||||
ulong guildId,
|
||||
ulong userId,
|
||||
string mod)
|
||||
=> await warnings.AsQueryable()
|
||||
.Where(x => x.GuildId == guildId && x.UserId == userId)
|
||||
.ForEachAsync(x =>
|
||||
{
|
||||
if (x.Forgiven != true)
|
||||
{
|
||||
x.Forgiven = true;
|
||||
x.ForgivenBy = mod;
|
||||
}
|
||||
});
|
||||
|
||||
public static Warning[] GetForGuild(this DbSet<Warning> warnings, ulong id)
|
||||
=> warnings.AsQueryable().Where(x => x.GuildId == id).ToArray();
|
||||
}
|
|
@ -1,65 +0,0 @@
|
|||
#nullable disable
|
||||
namespace EllieBot.Services.Database.Models;
|
||||
|
||||
public class AntiRaidSetting : DbEntity
|
||||
{
|
||||
public int GuildConfigId { get; set; }
|
||||
public GuildConfig GuildConfig { get; set; }
|
||||
|
||||
public int UserThreshold { get; set; }
|
||||
public int Seconds { get; set; }
|
||||
public PunishmentAction Action { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Duration of the punishment, in minutes. This works only for supported Actions, like:
|
||||
/// Mute, Chatmute, Voicemute, etc...
|
||||
/// </summary>
|
||||
public int PunishDuration { get; set; }
|
||||
}
|
||||
|
||||
public class AntiSpamSetting : DbEntity
|
||||
{
|
||||
public int GuildConfigId { get; set; }
|
||||
public GuildConfig GuildConfig { get; set; }
|
||||
|
||||
public PunishmentAction Action { get; set; }
|
||||
public int MessageThreshold { get; set; } = 3;
|
||||
public int MuteTime { get; set; }
|
||||
public ulong? RoleId { get; set; }
|
||||
public HashSet<AntiSpamIgnore> IgnoredChannels { get; set; } = new();
|
||||
}
|
||||
|
||||
public class AntiAltSetting
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public int GuildConfigId { get; set; }
|
||||
public TimeSpan MinAge { get; set; }
|
||||
public PunishmentAction Action { get; set; }
|
||||
public int ActionDurationMinutes { get; set; }
|
||||
public ulong? RoleId { get; set; }
|
||||
}
|
||||
|
||||
public enum PunishmentAction
|
||||
{
|
||||
Mute,
|
||||
Kick,
|
||||
Ban,
|
||||
Softban,
|
||||
RemoveRoles,
|
||||
ChatMute,
|
||||
VoiceMute,
|
||||
AddRole,
|
||||
Warn,
|
||||
TimeOut
|
||||
}
|
||||
|
||||
public class AntiSpamIgnore : DbEntity
|
||||
{
|
||||
public ulong ChannelId { get; set; }
|
||||
|
||||
public override int GetHashCode()
|
||||
=> ChannelId.GetHashCode();
|
||||
|
||||
public override bool Equals(object obj)
|
||||
=> obj is AntiSpamIgnore inst ? inst.ChannelId == ChannelId : false;
|
||||
}
|
|
@ -1,14 +0,0 @@
|
|||
#nullable disable
|
||||
namespace EllieBot.Services.Database.Models;
|
||||
|
||||
public class AutoCommand : DbEntity
|
||||
{
|
||||
public string CommandText { get; set; }
|
||||
public ulong ChannelId { get; set; }
|
||||
public string ChannelName { get; set; }
|
||||
public ulong? GuildId { get; set; }
|
||||
public string GuildName { get; set; }
|
||||
public ulong? VoiceChannelId { get; set; }
|
||||
public string VoiceChannelName { get; set; }
|
||||
public int Interval { get; set; }
|
||||
}
|
|
@ -1,9 +0,0 @@
|
|||
using EllieBot.Services.Database.Models;
|
||||
|
||||
namespace EllieBot.Db.Models;
|
||||
|
||||
public class AutoPublishChannel : DbEntity
|
||||
{
|
||||
public ulong GuildId { get; set; }
|
||||
public ulong ChannelId { get; set; }
|
||||
}
|
|
@ -1,10 +0,0 @@
|
|||
#nullable disable
|
||||
namespace EllieBot.Services.Database.Models;
|
||||
|
||||
public class AutoTranslateChannel : DbEntity
|
||||
{
|
||||
public ulong GuildId { get; set; }
|
||||
public ulong ChannelId { get; set; }
|
||||
public bool AutoDelete { get; set; }
|
||||
public IList<AutoTranslateUser> Users { get; set; } = new List<AutoTranslateUser>();
|
||||
}
|
|
@ -1,11 +0,0 @@
|
|||
#nullable disable
|
||||
namespace EllieBot.Services.Database.Models;
|
||||
|
||||
public class AutoTranslateUser : DbEntity
|
||||
{
|
||||
public int ChannelId { get; set; }
|
||||
public AutoTranslateChannel Channel { get; set; }
|
||||
public ulong UserId { get; set; }
|
||||
public string Source { get; set; }
|
||||
public string Target { get; set; }
|
||||
}
|
|
@ -1,9 +0,0 @@
|
|||
#nullable disable
|
||||
namespace EllieBot.Services.Database.Models;
|
||||
|
||||
public class BanTemplate : DbEntity
|
||||
{
|
||||
public ulong GuildId { get; set; }
|
||||
public string Text { get; set; }
|
||||
public int? PruneDays { get; set; }
|
||||
}
|
|
@ -1,9 +0,0 @@
|
|||
using EllieBot.Services.Database.Models;
|
||||
|
||||
namespace EllieBot.Db.Models;
|
||||
|
||||
public class BankUser : DbEntity
|
||||
{
|
||||
public ulong UserId { get; set; }
|
||||
public long Balance { get; set; }
|
||||
}
|
|
@ -1,15 +0,0 @@
|
|||
#nullable disable
|
||||
namespace EllieBot.Services.Database.Models;
|
||||
|
||||
public class BlacklistEntry : DbEntity
|
||||
{
|
||||
public ulong ItemId { get; set; }
|
||||
public BlacklistType Type { get; set; }
|
||||
}
|
||||
|
||||
public enum BlacklistType
|
||||
{
|
||||
Server,
|
||||
Channel,
|
||||
User
|
||||
}
|
|
@ -1,42 +0,0 @@
|
|||
#nullable disable
|
||||
using EllieBot.Services.Database.Models;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace EllieBot.Db.Models;
|
||||
|
||||
public class ClubInfo : DbEntity
|
||||
{
|
||||
[MaxLength(20)]
|
||||
public string Name { get; set; }
|
||||
public string Description { get; set; }
|
||||
public string ImageUrl { get; set; } = string.Empty;
|
||||
|
||||
public int Xp { get; set; } = 0;
|
||||
public int? OwnerId { get; set; }
|
||||
public DiscordUser Owner { get; set; }
|
||||
|
||||
public List<DiscordUser> Members { get; set; } = new();
|
||||
public List<ClubApplicants> Applicants { get; set; } = new();
|
||||
public List<ClubBans> Bans { get; set; } = new();
|
||||
|
||||
public override string ToString()
|
||||
=> Name;
|
||||
}
|
||||
|
||||
public class ClubApplicants
|
||||
{
|
||||
public int ClubId { get; set; }
|
||||
public ClubInfo Club { get; set; }
|
||||
|
||||
public int UserId { get; set; }
|
||||
public DiscordUser User { get; set; }
|
||||
}
|
||||
|
||||
public class ClubBans
|
||||
{
|
||||
public int ClubId { get; set; }
|
||||
public ClubInfo Club { get; set; }
|
||||
|
||||
public int UserId { get; set; }
|
||||
public DiscordUser User { get; set; }
|
||||
}
|
|
@ -1,8 +0,0 @@
|
|||
#nullable disable
|
||||
namespace EllieBot.Services.Database.Models;
|
||||
|
||||
public class CommandAlias : DbEntity
|
||||
{
|
||||
public string Trigger { get; set; }
|
||||
public string Mapping { get; set; }
|
||||
}
|
|
@ -1,8 +0,0 @@
|
|||
#nullable disable
|
||||
namespace EllieBot.Services.Database.Models;
|
||||
|
||||
public class CommandCooldown : DbEntity
|
||||
{
|
||||
public int Seconds { get; set; }
|
||||
public string CommandName { get; set; }
|
||||
}
|
|
@ -1,12 +0,0 @@
|
|||
#nullable disable
|
||||
namespace EllieBot.Services.Database.Models;
|
||||
|
||||
public class CurrencyTransaction : DbEntity
|
||||
{
|
||||
public long Amount { get; set; }
|
||||
public string Note { get; set; }
|
||||
public ulong UserId { get; set; }
|
||||
public string Type { get; set; }
|
||||
public string Extra { get; set; }
|
||||
public ulong? OtherId { get; set; }
|
||||
}
|
|
@ -1,12 +0,0 @@
|
|||
#nullable disable
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace EllieBot.Services.Database.Models;
|
||||
|
||||
public class DbEntity
|
||||
{
|
||||
[Key]
|
||||
public int Id { get; set; }
|
||||
|
||||
public DateTime? DateAdded { get; set; } = DateTime.UtcNow;
|
||||
}
|
|
@ -1,14 +0,0 @@
|
|||
#nullable disable
|
||||
namespace EllieBot.Services.Database.Models;
|
||||
|
||||
public class DelMsgOnCmdChannel : DbEntity
|
||||
{
|
||||
public ulong ChannelId { get; set; }
|
||||
public bool State { get; set; }
|
||||
|
||||
public override int GetHashCode()
|
||||
=> ChannelId.GetHashCode();
|
||||
|
||||
public override bool Equals(object obj)
|
||||
=> obj is DelMsgOnCmdChannel x && x.ChannelId == ChannelId;
|
||||
}
|
|
@ -1,10 +0,0 @@
|
|||
#nullable disable
|
||||
namespace EllieBot.Services.Database.Models;
|
||||
|
||||
public class DiscordPermOverride : DbEntity
|
||||
{
|
||||
public GuildPerm Perm { get; set; }
|
||||
|
||||
public ulong? GuildId { get; set; }
|
||||
public string Command { get; set; }
|
||||
}
|
|
@ -1,31 +0,0 @@
|
|||
#nullable disable
|
||||
using EllieBot.Services.Database.Models;
|
||||
|
||||
namespace EllieBot.Db.Models;
|
||||
|
||||
// FUTURE remove LastLevelUp from here and UserXpStats
|
||||
public class DiscordUser : DbEntity
|
||||
{
|
||||
public ulong UserId { get; set; }
|
||||
public string Username { get; set; }
|
||||
public string Discriminator { get; set; }
|
||||
public string AvatarId { get; set; }
|
||||
|
||||
public int? ClubId { get; set; }
|
||||
public ClubInfo Club { get; set; }
|
||||
public bool IsClubAdmin { get; set; }
|
||||
|
||||
public long TotalXp { get; set; }
|
||||
public XpNotificationLocation NotifyOnLevelUp { get; set; }
|
||||
|
||||
public long CurrencyAmount { get; set; }
|
||||
|
||||
public override bool Equals(object obj)
|
||||
=> obj is DiscordUser du ? du.UserId == UserId : false;
|
||||
|
||||
public override int GetHashCode()
|
||||
=> UserId.GetHashCode();
|
||||
|
||||
public override string ToString()
|
||||
=> Username + "#" + Discriminator;
|
||||
}
|
|
@ -1,27 +0,0 @@
|
|||
#nullable disable
|
||||
namespace EllieBot.Services.Database.Models;
|
||||
|
||||
public class EllieExpression : DbEntity
|
||||
{
|
||||
public ulong? GuildId { get; set; }
|
||||
public string Response { get; set; }
|
||||
public string Trigger { get; set; }
|
||||
|
||||
public bool AutoDeleteTrigger { get; set; }
|
||||
public bool DmResponse { get; set; }
|
||||
public bool ContainsAnywhere { get; set; }
|
||||
public bool AllowTarget { get; set; }
|
||||
public string Reactions { get; set; }
|
||||
|
||||
public string[] GetReactions()
|
||||
=> string.IsNullOrWhiteSpace(Reactions) ? Array.Empty<string>() : Reactions.Split("@@@");
|
||||
|
||||
public bool IsGlobal()
|
||||
=> GuildId is null or 0;
|
||||
}
|
||||
|
||||
public class ReactionResponse : DbEntity
|
||||
{
|
||||
public bool OwnerOnly { get; set; }
|
||||
public string Text { get; set; }
|
||||
}
|
|
@ -1,49 +0,0 @@
|
|||
#nullable disable
|
||||
namespace EllieBot.Services.Database.Models;
|
||||
|
||||
public class CurrencyEvent
|
||||
{
|
||||
public enum Type
|
||||
{
|
||||
Reaction,
|
||||
|
||||
GameStatus
|
||||
//NotRaid,
|
||||
}
|
||||
|
||||
public ulong ServerId { get; set; }
|
||||
public ulong ChannelId { get; set; }
|
||||
public ulong MessageId { get; set; }
|
||||
public Type EventType { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Amount of currency that the user will be rewarded.
|
||||
/// </summary>
|
||||
public long Amount { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Maximum amount of currency that can be handed out.
|
||||
/// </summary>
|
||||
public long PotSize { get; set; }
|
||||
|
||||
public List<AwardedUser> AwardedUsers { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Used as extra data storage for events which need it.
|
||||
/// </summary>
|
||||
public ulong ExtraId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// May be used for some future event.
|
||||
/// </summary>
|
||||
public ulong ExtraId2 { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// May be used for some future event.
|
||||
/// </summary>
|
||||
public string ExtraString { get; set; }
|
||||
}
|
||||
|
||||
public class AwardedUser
|
||||
{
|
||||
}
|
|
@ -1,19 +0,0 @@
|
|||
#nullable disable
|
||||
namespace EllieBot.Services.Database.Models;
|
||||
|
||||
public class FeedSub : DbEntity
|
||||
{
|
||||
public int GuildConfigId { get; set; }
|
||||
public GuildConfig GuildConfig { get; set; }
|
||||
|
||||
public ulong ChannelId { get; set; }
|
||||
public string Url { get; set; }
|
||||
|
||||
public string Message { get; set; }
|
||||
|
||||
public override int GetHashCode()
|
||||
=> Url.GetHashCode(StringComparison.InvariantCulture) ^ GuildConfigId.GetHashCode();
|
||||
|
||||
public override bool Equals(object obj)
|
||||
=> obj is FeedSub s && s.Url.ToLower() == Url.ToLower() && s.GuildConfigId == GuildConfigId;
|
||||
}
|
|
@ -1,30 +0,0 @@
|
|||
#nullable disable
|
||||
namespace EllieBot.Services.Database.Models;
|
||||
|
||||
public class FilterChannelId : DbEntity
|
||||
{
|
||||
public ulong ChannelId { get; set; }
|
||||
|
||||
public bool Equals(FilterChannelId other)
|
||||
=> ChannelId == other.ChannelId;
|
||||
|
||||
public override bool Equals(object obj)
|
||||
=> obj is FilterChannelId fci && Equals(fci);
|
||||
|
||||
public override int GetHashCode()
|
||||
=> ChannelId.GetHashCode();
|
||||
}
|
||||
|
||||
public class FilterWordsChannelId : DbEntity
|
||||
{
|
||||
public ulong ChannelId { get; set; }
|
||||
|
||||
public bool Equals(FilterWordsChannelId other)
|
||||
=> ChannelId == other.ChannelId;
|
||||
|
||||
public override bool Equals(object obj)
|
||||
=> obj is FilterWordsChannelId fci && Equals(fci);
|
||||
|
||||
public override int GetHashCode()
|
||||
=> ChannelId.GetHashCode();
|
||||
}
|
|
@ -1,13 +0,0 @@
|
|||
#nullable disable
|
||||
namespace EllieBot.Services.Database.Models;
|
||||
|
||||
public class FilterLinksChannelId : DbEntity
|
||||
{
|
||||
public ulong ChannelId { get; set; }
|
||||
|
||||
public override bool Equals(object obj)
|
||||
=> obj is FilterLinksChannelId f && f.ChannelId == ChannelId;
|
||||
|
||||
public override int GetHashCode()
|
||||
=> ChannelId.GetHashCode();
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue