Removed a bunch of things out of EllieBot

This commit is contained in:
Toastie 2024-05-12 19:30:05 +12:00
parent 9adb5a9906
commit 957790a85b
Signed by: toastie_t0ast
GPG key ID: 27F3B6855AFD40A4
150 changed files with 435 additions and 5984 deletions

404
src/EllieBot/Bot.cs Normal file
View 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);
}
}

View file

@ -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))
{
}
}

View file

@ -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;
}
}

View file

@ -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
{
}

View file

@ -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();
}
}

View file

@ -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;
}

View file

@ -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
}
}

View file

@ -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"));
}
}

View file

@ -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);
}
}

View file

@ -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);
}
}

View file

@ -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;
}
}

View file

@ -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);
}

View file

@ -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);
}
}
}

View file

@ -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);
});
}
}

View file

@ -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
}

View file

@ -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);
}

View file

@ -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);
}

View file

@ -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);

View file

@ -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);
}

View file

@ -1,8 +0,0 @@
namespace EllieBot;
public interface IEllieInteractionService
{
public EllieInteraction Create<T>(
ulong userId,
SimpleInteraction<T> inter);
}

View file

@ -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!);
}
}

View file

@ -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);
}

View file

@ -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());
}

View file

@ -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;
}

View file

@ -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);
}

View file

@ -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);
}
}

View file

@ -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();
}
}

View file

@ -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);
}
}

View file

@ -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");
}
}

View file

@ -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
}

View file

@ -1,8 +0,0 @@
namespace Ellie.Marmalade;
public interface IMarmaladeConfigService
{
IReadOnlyCollection<string> GetLoadedMarmalades();
void AddLoadedMarmalade(string name);
void RemoveLoadedMarmalade(string name);
}

View file

@ -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();
}
}

View file

@ -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);
});
}
}

View file

@ -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);

View file

@ -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;
}
}

View file

@ -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,
}

View file

@ -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);
}
}

View file

@ -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; }
}

View file

@ -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();
}

View file

@ -1,10 +0,0 @@
namespace Ellie.Marmalade;
public sealed record ParamData(
Type Type,
string Name,
bool IsOptional,
object? DefaultValue,
bool IsLeftover,
bool IsParams
);

View file

@ -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!;
}

View file

@ -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);
}

View file

@ -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);
}

View file

@ -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);
}

View file

@ -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);
}

View file

@ -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);
}

View file

@ -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();
}

View file

@ -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;
}
}
}

View file

@ -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;
}

View file

@ -1,7 +0,0 @@
namespace EllieBot.Common;
public interface ISeria
{
byte[] Serialize<T>(T data);
T? Deserialize<T>(byte[]? data);
}

View file

@ -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);
}
}

View file

@ -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);
}
}

View file

@ -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;
}

View file

@ -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);
}

View file

@ -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;
}

View file

@ -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);
}
}
}

View file

@ -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;
}
}

View file

@ -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);
}
}

View file

@ -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);
}
}

View file

@ -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));
}
}

View file

@ -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()));
}
}

View file

@ -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();
}

View file

@ -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;
}
}

View file

@ -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
}

View file

@ -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();
}

View file

@ -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();
}

View file

@ -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);
}

View file

@ -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);
}

View file

@ -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();
}

View file

@ -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; }
}
}

View file

@ -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;
}
}

View file

@ -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);
}

View file

@ -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);
}
}

View file

@ -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));
}

View file

@ -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);
}

View file

@ -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();
}

View file

@ -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);
}

View file

@ -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;
}
}

View file

@ -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();
}

View file

@ -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;
}

View file

@ -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; }
}

View file

@ -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; }
}

View file

@ -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>();
}

View file

@ -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; }
}

View file

@ -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; }
}

View file

@ -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; }
}

View file

@ -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
}

View file

@ -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; }
}

View file

@ -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; }
}

View file

@ -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; }
}

View file

@ -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; }
}

View file

@ -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;
}

View file

@ -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;
}

View file

@ -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; }
}

View file

@ -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;
}

View file

@ -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; }
}

View file

@ -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
{
}

View file

@ -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;
}

View file

@ -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();
}

View file

@ -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