more work on the greet cleanup

This commit is contained in:
Toastie 2024-09-04 22:33:34 +12:00
parent 742d98a4c1
commit b04768633c
Signed by: toastie_t0ast
GPG key ID: 27F3B6855AFD40A4
5 changed files with 185 additions and 326 deletions

View file

@ -1,5 +1,7 @@
# EllieBot # EllieBot
## ? This branch is a heavy work in progress and is not stable at all.
[![Please don't upload to GitHub](https://nogithub.codeberg.page/badge.svg)](https://nogithub.codeberg.page) [![Please don't upload to GitHub](https://nogithub.codeberg.page/badge.svg)](https://nogithub.codeberg.page)
## Guides ## Guides

View file

@ -13,21 +13,23 @@ public class GuildConfig : DbEntity
public string AutoAssignRoleIds { get; set; } public string AutoAssignRoleIds { get; set; }
//greet stuff // //greet stuff
public int AutoDeleteGreetMessagesTimer { get; set; } = 30; // public int AutoDeleteGreetMessagesTimer { get; set; } = 30;
public int AutoDeleteByeMessagesTimer { get; set; } = 30; // public int AutoDeleteByeMessagesTimer { get; set; } = 30;
//
public ulong GreetMessageChannelId { get; set; } // public ulong GreetMessageChannelId { get; set; }
public ulong ByeMessageChannelId { get; set; } // public ulong ByeMessageChannelId { get; set; }
//
public bool SendDmGreetMessage { get; set; } // public bool SendDmGreetMessage { get; set; }
public string DmGreetMessageText { get; set; } = "Welcome to the %server% server, %user%!"; // public string DmGreetMessageText { get; set; } = "Welcome to the %server% server, %user%!";
//
public bool SendChannelGreetMessage { get; set; } // public bool SendChannelGreetMessage { get; set; }
public string ChannelGreetMessageText { get; set; } = "Welcome to the %server% server, %user%!"; // public string ChannelGreetMessageText { get; set; } = "Welcome to the %server% server, %user%!";
//
public bool SendChannelByeMessage { get; set; } // public bool SendChannelByeMessage { get; set; }
public string ChannelByeMessageText { get; set; } = "%user% has left!"; // public string ChannelByeMessageText { get; set; } = "%user% has left!";
// public bool SendBoostMessage { get; set; }
// pulic int BoostMessageDeleteAfter { get; set; }
//self assignable roles //self assignable roles
public bool ExclusiveSelfAssignedRoles { get; set; } public bool ExclusiveSelfAssignedRoles { get; set; }
@ -98,10 +100,6 @@ public class GuildConfig : DbEntity
#region Boost Message #region Boost Message
public bool SendBoostMessage { get; set; }
public string BoostMessage { get; set; } = "%user% just boosted this server!";
public ulong BoostMessageChannelId { get; set; }
public int BoostMessageDeleteAfter { get; set; }
public bool StickyRoles { get; set; } public bool StickyRoles { get; set; }
#endregion #endregion

View file

@ -72,9 +72,8 @@ public sealed class CleanupService : ICleanupService, IReadyExecutor, IEService
dontDelete = dontDeleteList.ToHashSet(); dontDelete = dontDeleteList.ToHashSet();
} }
Log.Information("Leaving {RemainingCount} guilds every {Delay} seconds, {DontDeleteCount} will remain", Log.Information("Leaving {RemainingCount} guilds every, 1 seconds. {DontDeleteCount} will remain",
allGuildIds.Length - dontDelete.Count, allGuildIds.Length - dontDelete.Count,
shardId,
dontDelete.Count); dontDelete.Count);
foreach (var guildId in allGuildIds) foreach (var guildId in allGuildIds)
{ {

View file

@ -1,5 +1,7 @@
using LinqToDB;
using LinqToDB.EntityFrameworkCore;
using LinqToDB.Tools;
using EllieBot.Common.ModuleBehaviors; using EllieBot.Common.ModuleBehaviors;
using EllieBot.Db.Models;
using System.Threading.Channels; using System.Threading.Channels;
namespace EllieBot.Services; namespace EllieBot.Services;
@ -11,7 +13,11 @@ public class GreetService : IEService, IReadyExecutor
private readonly DbService _db; private readonly DbService _db;
private readonly ConcurrentDictionary<ulong, GreetSettings> _guildConfigsCache; private ConcurrentHashSet<ulong> _greetDmEnabledGuilds = new();
private ConcurrentHashSet<ulong> _boostEnabledGuilds = new();
private ConcurrentHashSet<ulong> _greetEnabledGuilds = new();
private ConcurrentHashSet<ulong> _byeEnabledGuilds = new();
private readonly DiscordSocketClient _client; private readonly DiscordSocketClient _client;
private readonly GreetGrouper<IGuildUser> _greets = new(); private readonly GreetGrouper<IGuildUser> _greets = new();
@ -22,37 +28,52 @@ public class GreetService : IEService, IReadyExecutor
public GreetService( public GreetService(
DiscordSocketClient client, DiscordSocketClient client,
IBot bot,
DbService db, DbService db,
BotConfigService bss, BotConfigService bss,
IMessageSenderService sender, IMessageSenderService sender,
IReplacementService repSvc) IReplacementService repSvc
)
{ {
_db = db; _db = db;
_client = client; _client = client;
_bss = bss; _bss = bss;
_repSvc = repSvc; _repSvc = repSvc;
_sender = sender; _sender = sender;
_guildConfigsCache = new(bot.AllGuildConfigs.ToDictionary(g => g.GuildId, GreetSettings.Create));
_client.UserJoined += OnUserJoined;
_client.UserLeft += OnUserLeft;
bot.JoinedGuild += OnBotJoinedGuild;
_client.LeftGuild += OnClientLeftGuild;
_client.GuildMemberUpdated += ClientOnGuildMemberUpdated;
} }
public async Task OnReadyAsync() public async Task OnReadyAsync()
{ {
while (true) // cache all enabled guilds
await using (var uow = _db.GetDbContext())
{
var guilds = _client.Guilds.Select(x => x.Id).ToList();
var enabled = await uow.GetTable<GreetSettings>()
.Where(x => x.GuildId.In(guilds))
.Where(x => x.SendChannelGreetMessage
|| x.SendBoostMessage
|| x.SendChannelByeMessage
|| x.SendDmGreetMessage)
.ToListAsync();
_boostEnabledGuilds = new(enabled.Where(x => x.SendBoostMessage).Select(x => x.GuildId));
_byeEnabledGuilds = new(enabled.Where(x => x.SendChannelByeMessage).Select(x => x.GuildId));
_greetDmEnabledGuilds = new(enabled.Where(x => x.SendDmGreetMessage).Select(x => x.GuildId));
_greetEnabledGuilds = new(enabled.Where(x => x.SendChannelGreetMessage).Select(x => x.GuildId));
}
_client.UserJoined += OnUserJoined;
_client.UserLeft += OnUserLeft;
_client.LeftGuild += OnClientLeftGuild;
_client.GuildMemberUpdated += ClientOnGuildMemberUpdated;
var timer = new PeriodicTimer(TimeSpan.FromSeconds(2));
while (await timer.WaitForNextTickAsync())
{ {
var (conf, user, compl) = await _greetDmQueue.Reader.ReadAsync(); var (conf, user, compl) = await _greetDmQueue.Reader.ReadAsync();
var res = await GreetDmUserInternal(conf, user); var res = await GreetDmUserInternal(conf, user);
compl.TrySetResult(res); compl.TrySetResult(res);
await Task.Delay(2000);
} }
} }
@ -65,25 +86,28 @@ public class GreetService : IEService, IReadyExecutor
&& newUser.PremiumSince is { } newDate && newUser.PremiumSince is { } newDate
&& newDate > oldDate)) && newDate > oldDate))
{ {
var conf = GetOrAddSettingsForGuild(newUser.Guild.Id); _ = Task.Run(async () =>
if (!conf.SendBoostMessage) {
return Task.CompletedTask; var conf = await GetGreetSettingsAsync(newUser.Guild.Id);
_ = Task.Run(TriggerBoostMessage(conf, newUser)); if (conf is null || !conf.SendBoostMessage)
return;
await TriggerBoostMessage(conf, newUser);
});
} }
return Task.CompletedTask; return Task.CompletedTask;
} }
private Func<Task> TriggerBoostMessage(GreetSettings conf, SocketGuildUser user) private async Task TriggerBoostMessage(GreetSettings conf, SocketGuildUser user)
=> async () =>
{ {
var channel = user.Guild.GetTextChannel(conf.BoostMessageChannelId); var channel = user.Guild.GetTextChannel(conf.BoostMessageChannelId);
if (channel is null) if (channel is null)
return; return;
await SendBoostMessage(conf, user, channel); await SendBoostMessage(conf, user, channel);
}; }
private async Task<bool> SendBoostMessage(GreetSettings conf, IGuildUser user, ITextChannel channel) private async Task<bool> SendBoostMessage(GreetSettings conf, IGuildUser user, ITextChannel channel)
{ {
@ -110,16 +134,17 @@ public class GreetService : IEService, IReadyExecutor
return false; return false;
} }
private Task OnClientLeftGuild(SocketGuild arg) private async Task OnClientLeftGuild(SocketGuild arg)
{ {
_guildConfigsCache.TryRemove(arg.Id, out _); _boostEnabledGuilds.TryRemove(arg.Id);
return Task.CompletedTask; _byeEnabledGuilds.TryRemove(arg.Id);
} _greetDmEnabledGuilds.TryRemove(arg.Id);
_greetEnabledGuilds.TryRemove(arg.Id);
private Task OnBotJoinedGuild(GuildConfig gc) await using var uow = _db.GetDbContext();
{ await uow.GetTable<GreetSettings>()
_guildConfigsCache[gc.GuildId] = GreetSettings.Create(gc); .Where(x => x.GuildId == arg.Id)
return Task.CompletedTask; .DeleteAsync();
} }
private Task OnUserLeft(SocketGuild guild, SocketUser user) private Task OnUserLeft(SocketGuild guild, SocketUser user)
@ -128,10 +153,11 @@ public class GreetService : IEService, IReadyExecutor
{ {
try try
{ {
var conf = GetOrAddSettingsForGuild(guild.Id); var conf = await GetGreetSettingsAsync(guild.Id);
if (!conf.SendChannelByeMessage) if (conf is null)
return; return;
var channel = guild.TextChannels.FirstOrDefault(c => c.Id == conf.ByeMessageChannelId); var channel = guild.TextChannels.FirstOrDefault(c => c.Id == conf.ByeMessageChannelId);
if (channel is null) //maybe warn the server owner that the channel is missing if (channel is null) //maybe warn the server owner that the channel is missing
@ -156,8 +182,10 @@ public class GreetService : IEService, IReadyExecutor
} }
} }
else else
{
await ByeUsers(conf, channel, new[] { user }); await ByeUsers(conf, channel, new[] { user });
} }
}
catch catch
{ {
// ignored // ignored
@ -166,31 +194,14 @@ public class GreetService : IEService, IReadyExecutor
return Task.CompletedTask; return Task.CompletedTask;
} }
public string? GetDmGreetMsg(ulong id) public async Task<GreetSettings?> GetGreetSettingsAsync(ulong gid, GreetType type)
{ {
using var uow = _db.GetDbContext(); await using var uow = _db.GetDbContext();
return uow.GuildConfigsForId(id, set => set).DmGreetMessageText; var res = await uow.GetTable<GreetSettings>()
} .Where(x => x.GuildId == gid && x.GreetType == type)
.FirstOrDefaultAsync();
public string? GetGreetMsg(ulong gid) return res;
{
using var uow = _db.GetDbContext();
return uow.GuildConfigsForId(gid, set => set).ChannelGreetMessageText;
}
public string? GetBoostMessage(ulong gid)
{
using var uow = _db.GetDbContext();
return uow.GuildConfigsForId(gid, set => set).BoostMessage;
}
public GreetSettings GetGreetSettings(ulong gid)
{
if (_guildConfigsCache.TryGetValue(gid, out var gs))
return gs;
using var uow = _db.GetDbContext();
return GreetSettings.Create(uow.GuildConfigsForId(gid, set => set));
} }
private Task ByeUsers(GreetSettings conf, ITextChannel channel, IUser user) private Task ByeUsers(GreetSettings conf, ITextChannel channel, IUser user)
@ -221,7 +232,7 @@ public class GreetService : IEService, IReadyExecutor
Log.Warning(ex, Log.Warning(ex,
"Missing permissions to send a bye message, the bye message will be disabled on server: {GuildId}", "Missing permissions to send a bye message, the bye message will be disabled on server: {GuildId}",
channel.GuildId); channel.GuildId);
await SetBye(channel.GuildId, channel.Id, false); await SetGreet(channel.GuildId, channel.Id, GreetType.Bye, false);
} }
catch (Exception ex) catch (Exception ex)
{ {
@ -250,14 +261,14 @@ public class GreetService : IEService, IReadyExecutor
if (conf.AutoDeleteGreetMessagesTimer > 0) if (conf.AutoDeleteGreetMessagesTimer > 0)
toDelete.DeleteAfter(conf.AutoDeleteGreetMessagesTimer); toDelete.DeleteAfter(conf.AutoDeleteGreetMessagesTimer);
} }
catch (HttpException ex) when (ex.DiscordCode == DiscordErrorCode.InsufficientPermissions catch (HttpException ex) when (ex.DiscordCode is DiscordErrorCode.InsufficientPermissions
|| ex.DiscordCode == DiscordErrorCode.MissingPermissions or DiscordErrorCode.MissingPermissions
|| ex.DiscordCode == DiscordErrorCode.UnknownChannel) or DiscordErrorCode.UnknownChannel)
{ {
Log.Warning(ex, Log.Warning(ex,
"Missing permissions to send a bye message, the greet message will be disabled on server: {GuildId}", "Missing permissions to send a bye message, the greet message will be disabled on server: {GuildId}",
channel.GuildId); channel.GuildId);
await SetGreet(channel.GuildId, channel.Id, false); await SetGreet(channel.GuildId, channel.Id, GreetType.Greet, false);
} }
catch (Exception ex) catch (Exception ex)
{ {
@ -285,11 +296,6 @@ public class GreetService : IEService, IReadyExecutor
{ {
try try
{ {
// var rep = new ReplacementBuilder()
// .WithUser(user)
// .WithServer(_client, (SocketGuild)user.Guild)
// .Build();
var repCtx = new ReplacementContext(client: _client, guild: user.Guild, users: user); var repCtx = new ReplacementContext(client: _client, guild: user.Guild, users: user);
var smartText = SmartText.CreateFrom(conf.DmGreetMessageText); var smartText = SmartText.CreateFrom(conf.DmGreetMessageText);
smartText = await _repSvc.ReplaceAsync(smartText, repCtx); smartText = await _repSvc.ReplaceAsync(smartText, repCtx);
@ -372,7 +378,9 @@ public class GreetService : IEService, IReadyExecutor
{ {
try try
{ {
var conf = GetOrAddSettingsForGuild(user.GuildId); var conf = await GetGreetSettingsAsync(user.GuildId);
if (conf is null)
return;
if (conf.SendChannelGreetMessage) if (conf.SendChannelGreetMessage)
{ {
@ -413,256 +421,107 @@ public class GreetService : IEService, IReadyExecutor
return Task.CompletedTask; return Task.CompletedTask;
} }
public string? GetByeMessage(ulong gid) // public GreetSettings GetOrAddSettingsForGuild(ulong guildId)
{ // {
using var uow = _db.GetDbContext(); // if (_greetDmEnabledGuilds.TryGetValue(guildId, out var settings))
return uow.GuildConfigsForId(gid, set => set).ChannelByeMessageText; // return settings;
} //
// using (var uow = _db.GetDbContext())
// {
// var gc = uow.GuildConfigsForId(guildId, set => set);
// settings = GreetSettings.Create(gc);
// }
//
// _greetDmEnabledGuilds.TryAdd(guildId, settings);
// return settings;
// }
public GreetSettings GetOrAddSettingsForGuild(ulong guildId) public async Task<bool> SetGreet(
{ ulong guildId,
if (_guildConfigsCache.TryGetValue(guildId, out var settings)) ulong? channelId,
return settings; GreetType greetType,
bool? value = null)
using (var uow = _db.GetDbContext())
{
var gc = uow.GuildConfigsForId(guildId, set => set);
settings = GreetSettings.Create(gc);
}
_guildConfigsCache.TryAdd(guildId, settings);
return settings;
}
public async Task<bool> SetGreet(ulong guildId, ulong channelId, bool? value = null)
{ {
await using var uow = _db.GetDbContext(); await using var uow = _db.GetDbContext();
var conf = uow.GuildConfigsForId(guildId, set => set); var q = uow.GetTable<GreetSettings>();
var enabled = conf.SendChannelGreetMessage = value ?? !conf.SendChannelGreetMessage;
conf.GreetMessageChannelId = channelId;
var toAdd = GreetSettings.Create(conf); if (value is { } v)
_guildConfigsCache[guildId] = toAdd; {
await q
await uow.SaveChangesAsync(); .InsertOrUpdateAsync(() => new()
return enabled; {
IsEnabled = v,
ChannelId = channelId,
},
(old) => new()
{
IsEnabled = v,
ChannelId = channelId,
},
() => new()
{
GuildId = guildId,
GreetType = greetType,
});
}
else
{
await q
.Where(x => x.GuildId == guildId && x.GreetType == greetType)
.UpdateAsync((old) => new()
{
IsEnabled = !old.IsEnabled
});
} }
public bool SetGreetMessage(ulong guildId, ref string message) return true;
}
public async Task<bool> SetGreetTypeMessage(ulong guildId, GreetType greetType, string message)
{ {
message = message.SanitizeMentions(); message = message.SanitizeMentions();
if (string.IsNullOrWhiteSpace(message)) if (string.IsNullOrWhiteSpace(message))
throw new ArgumentNullException(nameof(message)); throw new ArgumentNullException(nameof(message));
using var uow = _db.GetDbContext(); await using (var uow = _db.GetDbContext())
var conf = uow.GuildConfigsForId(guildId, set => set);
conf.ChannelGreetMessageText = message;
var greetMsgEnabled = conf.SendChannelGreetMessage;
var toAdd = GreetSettings.Create(conf);
_guildConfigsCache.AddOrUpdate(guildId, toAdd, (_, _) => toAdd);
uow.SaveChanges();
return greetMsgEnabled;
}
public async Task<bool> SetGreetDm(ulong guildId, bool? value = null)
{ {
await using var uow = _db.GetDbContext(); await uow.GetTable<GreetSettings>()
var conf = uow.GuildConfigsForId(guildId, set => set); .InsertOrUpdateAsync(() => new()
var enabled = conf.SendDmGreetMessage = value ?? !conf.SendDmGreetMessage;
var toAdd = GreetSettings.Create(conf);
_guildConfigsCache[guildId] = toAdd;
await uow.SaveChangesAsync();
return enabled;
}
public bool SetGreetDmMessage(ulong guildId, ref string? message)
{ {
message = message?.SanitizeMentions(); MessageText = message
},
if (string.IsNullOrWhiteSpace(message)) x => new()
throw new ArgumentNullException(nameof(message));
using var uow = _db.GetDbContext();
var conf = uow.GuildConfigsForId(guildId, set => set);
conf.DmGreetMessageText = message;
var toAdd = GreetSettings.Create(conf);
_guildConfigsCache[guildId] = toAdd;
uow.SaveChanges();
return conf.SendDmGreetMessage;
}
public async Task<bool> SetBye(ulong guildId, ulong channelId, bool? value = null)
{ {
await using var uow = _db.GetDbContext(); MessageText = message
var conf = uow.GuildConfigsForId(guildId, set => set); },
var enabled = conf.SendChannelByeMessage = value ?? !conf.SendChannelByeMessage; () => new()
conf.ByeMessageChannelId = channelId;
var toAdd = GreetSettings.Create(conf);
_guildConfigsCache[guildId] = toAdd;
await uow.SaveChangesAsync();
return enabled;
}
public bool SetByeMessage(ulong guildId, ref string? message)
{ {
message = message?.SanitizeMentions(); GuildId = guildId,
GreetType = greetType
if (string.IsNullOrWhiteSpace(message)) });
throw new ArgumentNullException(nameof(message));
using var uow = _db.GetDbContext();
var conf = uow.GuildConfigsForId(guildId, set => set);
conf.ChannelByeMessageText = message;
var toAdd = GreetSettings.Create(conf);
_guildConfigsCache[guildId] = toAdd;
uow.SaveChanges();
return conf.SendChannelByeMessage;
} }
public async Task SetByeDel(ulong guildId, int timer) var conf = await GetGreetSettingsAsync(guildId, type);
return conf?.IsEnabled ?? false;
}
public async Task<bool> Test(
ulong guildId,
GreetType type,
IMessageChannel channel,
IGuildUser user)
{ {
if (timer is < 0 or > 600) var conf = await GetGreetSettingsAsync(guildId, type);
return; return SendMessage(conf, user, channel);
await using var uow = _db.GetDbContext();
var conf = uow.GuildConfigsForId(guildId, set => set);
conf.AutoDeleteByeMessagesTimer = timer;
var toAdd = GreetSettings.Create(conf);
_guildConfigsCache[guildId] = toAdd;
await uow.SaveChangesAsync();
} }
public async Task SetGreetDel(ulong guildId, int timer) public async Task<bool> SendMessage(GreetSettings conf, IMessageChannel channel, IGuildUser user)
{ {
if (timer is < 0 or > 600) if (conf.GreetType == GreetType.GreetDm)
return;
await using var uow = _db.GetDbContext();
var conf = uow.GuildConfigsForId(guildId, set => set);
conf.AutoDeleteGreetMessagesTimer = timer;
var toAdd = GreetSettings.Create(conf);
_guildConfigsCache[guildId] = toAdd;
await uow.SaveChangesAsync();
}
public bool SetBoostMessage(ulong guildId, ref string message)
{ {
using var uow = _db.GetDbContext(); await GreetDmUser(conf, user);
var conf = uow.GuildConfigsForId(guildId, set => set);
conf.BoostMessage = message;
var toAdd = GreetSettings.Create(conf);
_guildConfigsCache[guildId] = toAdd;
uow.SaveChanges();
return conf.SendBoostMessage;
} }
public async Task SetBoostDel(ulong guildId, int timer)
{
if (timer is < 0 or > 600)
throw new ArgumentOutOfRangeException(nameof(timer));
await using var uow = _db.GetDbContext();
var conf = uow.GuildConfigsForId(guildId, set => set);
conf.BoostMessageDeleteAfter = timer;
var toAdd = GreetSettings.Create(conf);
_guildConfigsCache[guildId] = toAdd;
await uow.SaveChangesAsync();
} }
public async Task<bool> ToggleBoost(ulong guildId, ulong channelId, bool? forceState = null)
{
await using var uow = _db.GetDbContext();
var conf = uow.GuildConfigsForId(guildId, set => set);
if (forceState is not bool fs)
conf.SendBoostMessage = !conf.SendBoostMessage;
else
conf.SendBoostMessage = fs;
conf.BoostMessageChannelId = channelId;
await uow.SaveChangesAsync();
var toAdd = GreetSettings.Create(conf);
_guildConfigsCache[guildId] = toAdd;
return conf.SendBoostMessage;
}
#region Get Enabled Status
public bool GetGreetDmEnabled(ulong guildId)
{
using var uow = _db.GetDbContext();
var conf = uow.GuildConfigsForId(guildId, set => set);
return conf.SendDmGreetMessage;
}
public bool GetGreetEnabled(ulong guildId)
{
using var uow = _db.GetDbContext();
var conf = uow.GuildConfigsForId(guildId, set => set);
return conf.SendChannelGreetMessage;
}
public bool GetByeEnabled(ulong guildId)
{
using var uow = _db.GetDbContext();
var conf = uow.GuildConfigsForId(guildId, set => set);
return conf.SendChannelByeMessage;
}
public bool GetBoostEnabled(ulong guildId)
{
using var uow = _db.GetDbContext();
var conf = uow.GuildConfigsForId(guildId, set => set);
return conf.SendBoostMessage;
}
#endregion
#region Test Messages
public Task ByeTest(ITextChannel channel, IGuildUser user)
{
var conf = GetOrAddSettingsForGuild(user.GuildId);
return ByeUsers(conf, channel, user);
}
public Task GreetTest(ITextChannel channel, IGuildUser user)
{
var conf = GetOrAddSettingsForGuild(user.GuildId);
return GreetUsers(conf, channel, user);
}
public Task<bool> GreetDmTest(IGuildUser user)
{
var conf = GetOrAddSettingsForGuild(user.GuildId);
return GreetDmUser(conf, user);
}
public Task<bool> BoostTest(ITextChannel channel, IGuildUser user)
{
var conf = GetOrAddSettingsForGuild(user.GuildId);
return SendBoostMessage(conf, user, channel);
}
#endregion
} }

View file

@ -1,5 +1,6 @@
#nullable disable #nullable disable
using LinqToDB; using LinqToDB;
using LinqToDB.EntityFrameworkCore;
using System.Linq.Expressions; using System.Linq.Expressions;
namespace EllieBot.Common; namespace EllieBot.Common;