migrations, query fixes, fixes for mysql and postgres. In progress

This commit is contained in:
Toastie (DCS Team) 2024-09-12 15:44:35 +12:00
parent b04768633c
commit 40b4ebf0fa
Signed by: toastie_t0ast
GPG key ID: 27F3B6855AFD40A4
29 changed files with 18970 additions and 8070 deletions

5
.gitignore vendored
View file

@ -20,9 +20,8 @@ src/EllieBot/credentials.json
src/EllieBot/old_credentials.json src/EllieBot/old_credentials.json
src/EllieBot/credentials.json.bak src/EllieBot/credentials.json.bak
src/EllieBot/data/EllieBot.db src/EllieBot/data/EllieBot.db
build.ps1 ellie-menu.ps1
build.sh package.sh
test.ps1
# Created by https://www.gitignore.io/api/visualstudio,visualstudiocode,windows,linux,macos # Created by https://www.gitignore.io/api/visualstudio,visualstudiocode,windows,linux,macos

View file

@ -1,76 +0,0 @@
using System.Linq;
using System.Threading.Tasks;
using Ellie.Common;
using EllieBot.Services;
using NUnit.Framework;
namespace EllieBot.Tests
{
public class GroupGreetTests
{
private GreetGrouper<int> _grouper;
[SetUp]
public void Setup()
=> _grouper = new GreetGrouper<int>();
[Test]
public void CreateTest()
{
var created = _grouper.CreateOrAdd(0, 5);
Assert.True(created);
}
[Test]
public void CreateClearTest()
{
_grouper.CreateOrAdd(0, 5);
_grouper.ClearGroup(0, 5, out var items);
Assert.AreEqual(0, items.Count());
}
[Test]
public void NotCreatedTest()
{
_grouper.CreateOrAdd(0, 5);
var created = _grouper.CreateOrAdd(0, 4);
Assert.False(created);
}
[Test]
public void ClearAddedTest()
{
_grouper.CreateOrAdd(0, 5);
_grouper.CreateOrAdd(0, 4);
_grouper.ClearGroup(0, 5, out var items);
var list = items.ToList();
Assert.AreEqual(1, list.Count, $"Count was {list.Count}");
Assert.AreEqual(4, list[0]);
}
[Test]
public async Task ClearManyTest()
{
_grouper.CreateOrAdd(0, 5);
// add 15 items
await Enumerable.Range(10, 15)
.Select(x => Task.Run(() => _grouper.CreateOrAdd(0, x))).WhenAll();
// get 5 at most
_grouper.ClearGroup(0, 5, out var items);
var list = items.ToList();
Assert.AreEqual(5, list.Count, $"Count was {list.Count}");
// try to get 15, but there should be 10 left
_grouper.ClearGroup(0, 15, out items);
list = items.ToList();
Assert.AreEqual(10, list.Count, $"Count was {list.Count}");
}
}
}

View file

@ -1,5 +1,6 @@
#nullable disable #nullable disable
using DryIoc; using DryIoc;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using EllieBot.Common.Configs; using EllieBot.Common.Configs;
@ -88,18 +89,18 @@ public sealed class Bot : IBot
public IReadOnlyList<ulong> GetCurrentGuildIds() public IReadOnlyList<ulong> GetCurrentGuildIds()
=> Client.Guilds.Select(x => x.Id).ToList().ToList(); => Client.Guilds.Select(x => x.Id).ToList().AsReadOnly();
private void AddServices() private async Task AddServices()
{ {
var startingGuildIdList = GetCurrentGuildIds(); var startingGuildIdList = GetCurrentGuildIds().ToList();
var startTime = Stopwatch.GetTimestamp(); var startTime = Stopwatch.GetTimestamp();
var bot = Client.CurrentUser; var bot = Client.CurrentUser;
using (var uow = _db.GetDbContext()) await using (var uow = _db.GetDbContext())
{ {
AllGuildConfigs = await uow.GuildConfigs.GetAllGuildConfigs(startingGuildIdList);
uow.EnsureUserCreated(bot.Id, bot.Username, bot.Discriminator, bot.AvatarId); uow.EnsureUserCreated(bot.Id, bot.Username, bot.Discriminator, bot.AvatarId);
AllGuildConfigs = uow.Set<GuildConfig>().GetAllGuildConfigs(startingGuildIdList).ToImmutableArray();
} }
// var svcs = new StandardKernel(new NinjectSettings() // var svcs = new StandardKernel(new NinjectSettings()
@ -161,7 +162,8 @@ public sealed class Bot : IBot
LoadTypeReaders(a); LoadTypeReaders(a);
} }
Log.Information("All services loaded in {ServiceLoadTime:F2}s", Stopwatch.GetElapsedTime(startTime).TotalSeconds); Log.Information("All services loaded in {ServiceLoadTime:F2}s",
Stopwatch.GetElapsedTime(startTime).TotalSeconds);
} }
private void LoadTypeReaders(Assembly assembly) private void LoadTypeReaders(Assembly assembly)
@ -265,7 +267,7 @@ public sealed class Bot : IBot
Log.Information("Shard {ShardId} loading services...", Client.ShardId); Log.Information("Shard {ShardId} loading services...", Client.ShardId);
try try
{ {
AddServices(); await AddServices();
} }
catch (Exception ex) catch (Exception ex)
{ {
@ -273,7 +275,9 @@ public sealed class Bot : IBot
Helpers.ReadErrorAndExit(9); Helpers.ReadErrorAndExit(9);
} }
Log.Information("Shard {ShardId} connected in {Elapsed:F2}s", Client.ShardId, Stopwatch.GetElapsedTime(startTime).TotalSeconds); Log.Information("Shard {ShardId} connected in {Elapsed:F2}s",
Client.ShardId,
Stopwatch.GetElapsedTime(startTime).TotalSeconds);
var commandHandler = Services.GetRequiredService<CommandHandler>(); var commandHandler = Services.GetRequiredService<CommandHandler>();
// start handling messages received in commandhandler // start handling messages received in commandhandler
@ -338,26 +342,26 @@ public sealed class Bot : IBot
if (arg.Exception is { InnerException: WebSocketClosedException { CloseCode: 4014 } }) if (arg.Exception is { InnerException: WebSocketClosedException { CloseCode: 4014 } })
{ {
Log.Error(""" Log.Error("""
Login failed. Login failed.
*** Please enable privileged intents *** *** Please enable privileged intents ***
Certain Ellie features require Discord's privileged gateway 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. These include greeting and goodbye messages, as well as creating the Owner message channels for DM forwarding.
How to enable privileged intents: How to enable privileged intents:
1. Head over to the Discord Developer Portal https://discord.com/developers/applications/ 1. Head over to the Discord Developer Portal https://discord.com/developers/applications/
2. Select your Application. 2. Select your Application.
3. Click on `Bot` in the left side navigation panel, and scroll down to the intents section. 3. Click on `Bot` in the left side navigation panel, and scroll down to the intents section.
4. Enable all intents. 4. Enable all intents.
5. Restart your bot. 5. Restart your bot.
Read this only if your bot is in 100 or more servers: 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. 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. 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 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; return Task.CompletedTask;
} }

View file

@ -10,6 +10,7 @@ namespace EllieBot.Db;
public abstract class EllieContext : DbContext public abstract class EllieContext : DbContext
{ {
public DbSet<GuildConfig> GuildConfigs { get; set; } public DbSet<GuildConfig> GuildConfigs { get; set; }
public DbSet<GreetSettings> GreetSettings { get; set; }
public DbSet<Quote> Quotes { get; set; } public DbSet<Quote> Quotes { get; set; }
public DbSet<Reminder> Reminders { get; set; } public DbSet<Reminder> Reminders { get; set; }
@ -678,6 +679,18 @@ public abstract class EllieContext : DbContext
.OnDelete(DeleteBehavior.Cascade); .OnDelete(DeleteBehavior.Cascade);
#endregion #endregion
#region GreetSettings
modelBuilder
.Entity<GreetSettings>(gs => gs.HasIndex(x => new
{
x.GuildId,
x.GreetType
})
.IsUnique());
#endregion
} }
#if DEBUG #if DEBUG

View file

@ -1,4 +1,5 @@
#nullable disable #nullable disable
using LinqToDB.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using EllieBot.Db.Models; using EllieBot.Db.Models;
@ -32,8 +33,8 @@ public static class GuildConfigExtensions
{ {
var conf = ctx.GuildConfigsForId(guildId, var conf = ctx.GuildConfigsForId(guildId,
set => set.Include(y => y.StreamRole) set => set.Include(y => y.StreamRole)
.Include(y => y.StreamRole.Whitelist) .Include(y => y.StreamRole.Whitelist)
.Include(y => y.StreamRole.Blacklist)); .Include(y => y.StreamRole.Blacklist));
if (conf.StreamRole is null) if (conf.StreamRole is null)
conf.StreamRole = new(); conf.StreamRole = new();
@ -42,19 +43,28 @@ public static class GuildConfigExtensions
} }
private static IQueryable<GuildConfig> IncludeEverything(this DbSet<GuildConfig> configs) private static IQueryable<GuildConfig> IncludeEverything(this DbSet<GuildConfig> configs)
=> configs.AsQueryable() => configs
.AsSplitQuery() .AsSplitQuery()
.Include(gc => gc.CommandCooldowns) .Include(gc => gc.CommandCooldowns)
.Include(gc => gc.FollowedStreams) .Include(gc => gc.FollowedStreams)
.Include(gc => gc.StreamRole) .Include(gc => gc.StreamRole)
.Include(gc => gc.XpSettings) .Include(gc => gc.DelMsgOnCmdChannels)
.ThenInclude(x => x.ExclusionList) .Include(gc => gc.XpSettings)
.Include(gc => gc.DelMsgOnCmdChannels); .ThenInclude(x => x.ExclusionList);
public static IEnumerable<GuildConfig> GetAllGuildConfigs( public static async Task<GuildConfig[]> GetAllGuildConfigs(
this DbSet<GuildConfig> configs, this DbSet<GuildConfig> configs,
IReadOnlyList<ulong> availableGuilds) List<ulong> availableGuilds)
=> configs.IncludeEverything().AsNoTracking().Where(x => availableGuilds.Contains(x.GuildId)).ToList(); {
var result = await configs
.AsQueryable()
.Include(x => x.CommandCooldowns)
.Where(x => availableGuilds.Contains(x.GuildId))
.AsNoTracking()
.ToArrayAsync();
return result;
}
/// <summary> /// <summary>
/// Gets and creates if it doesn't exist a config for a guild. /// Gets and creates if it doesn't exist a config for a guild.
@ -80,13 +90,14 @@ public static class GuildConfigExtensions
if (config is null) if (config is null)
{ {
ctx.Set<GuildConfig>().Add(config = new() ctx.Set<GuildConfig>()
{ .Add(config = new()
GuildId = guildId, {
Permissions = Permissionv2.GetDefaultPermlist, GuildId = guildId,
WarningsInitialized = true, Permissions = Permissionv2.GetDefaultPermlist,
WarnPunishments = DefaultWarnPunishments WarningsInitialized = true,
}); WarnPunishments = DefaultWarnPunishments
});
ctx.SaveChanges(); ctx.SaveChanges();
} }
@ -122,18 +133,18 @@ public static class GuildConfigExtensions
public static LogSetting LogSettingsFor(this DbContext ctx, ulong guildId) public static LogSetting LogSettingsFor(this DbContext ctx, ulong guildId)
{ {
var logSetting = ctx.Set<LogSetting>() var logSetting = ctx.Set<LogSetting>()
.AsQueryable() .AsQueryable()
.Include(x => x.LogIgnores) .Include(x => x.LogIgnores)
.Where(x => x.GuildId == guildId) .Where(x => x.GuildId == guildId)
.FirstOrDefault(); .FirstOrDefault();
if (logSetting is null) if (logSetting is null)
{ {
ctx.Set<LogSetting>() ctx.Set<LogSetting>()
.Add(logSetting = new() .Add(logSetting = new()
{ {
GuildId = guildId GuildId = guildId
}); });
ctx.SaveChanges(); ctx.SaveChanges();
} }
@ -149,18 +160,20 @@ public static class GuildConfigExtensions
public static GuildConfig GcWithPermissionsFor(this DbContext ctx, ulong guildId) public static GuildConfig GcWithPermissionsFor(this DbContext ctx, ulong guildId)
{ {
var config = ctx.Set<GuildConfig>().AsQueryable() var config = ctx.Set<GuildConfig>()
.Where(gc => gc.GuildId == guildId) .AsQueryable()
.Include(gc => gc.Permissions) .Where(gc => gc.GuildId == guildId)
.FirstOrDefault(); .Include(gc => gc.Permissions)
.FirstOrDefault();
if (config is null) // if there is no guildconfig, create new one if (config is null) // if there is no guildconfig, create new one
{ {
ctx.Set<GuildConfig>().Add(config = new() ctx.Set<GuildConfig>()
{ .Add(config = new()
GuildId = guildId, {
Permissions = Permissionv2.GetDefaultPermlist GuildId = guildId,
}); Permissions = Permissionv2.GetDefaultPermlist
});
ctx.SaveChanges(); ctx.SaveChanges();
} }
else if (config.Permissions is null || !config.Permissions.Any()) // if no perms, add default ones else if (config.Permissions is null || !config.Permissions.Any()) // if no perms, add default ones
@ -177,20 +190,21 @@ public static class GuildConfigExtensions
public static IEnumerable<FollowedStream> GetFollowedStreams(this DbSet<GuildConfig> configs, List<ulong> included) public static IEnumerable<FollowedStream> GetFollowedStreams(this DbSet<GuildConfig> configs, List<ulong> included)
=> configs.AsQueryable() => configs.AsQueryable()
.Where(gc => included.Contains(gc.GuildId)) .Where(gc => included.Contains(gc.GuildId))
.Include(gc => gc.FollowedStreams) .Include(gc => gc.FollowedStreams)
.SelectMany(gc => gc.FollowedStreams) .SelectMany(gc => gc.FollowedStreams)
.ToList(); .ToList();
public static XpSettings XpSettingsFor(this DbContext ctx, ulong guildId) public static XpSettings XpSettingsFor(this DbContext ctx, ulong guildId)
{ {
var gc = ctx.GuildConfigsForId(guildId, var gc = ctx.GuildConfigsForId(guildId,
set => set.Include(x => x.XpSettings) set => set.Include(x => x.XpSettings)
.ThenInclude(x => x.RoleRewards) .ThenInclude(x => x.RoleRewards)
.Include(x => x.XpSettings) .Include(x => x.XpSettings)
.ThenInclude(x => x.CurrencyRewards) .ThenInclude(x => x.CurrencyRewards)
.Include(x => x.XpSettings) .Include(x => x.XpSettings)
.ThenInclude(x => x.ExclusionList)); .ThenInclude(x => x.ExclusionList));
if (gc.XpSettings is null) if (gc.XpSettings is null)
gc.XpSettings = new(); gc.XpSettings = new();
@ -200,15 +214,15 @@ public static class GuildConfigExtensions
public static IEnumerable<GeneratingChannel> GetGeneratingChannels(this DbSet<GuildConfig> configs) public static IEnumerable<GeneratingChannel> GetGeneratingChannels(this DbSet<GuildConfig> configs)
=> configs.AsQueryable() => configs.AsQueryable()
.Include(x => x.GenerateCurrencyChannelIds) .Include(x => x.GenerateCurrencyChannelIds)
.Where(x => x.GenerateCurrencyChannelIds.Any()) .Where(x => x.GenerateCurrencyChannelIds.Any())
.SelectMany(x => x.GenerateCurrencyChannelIds) .SelectMany(x => x.GenerateCurrencyChannelIds)
.Select(x => new GeneratingChannel .Select(x => new GeneratingChannel
{ {
ChannelId = x.ChannelId, ChannelId = x.ChannelId,
GuildId = x.GuildConfig.GuildId GuildId = x.GuildConfig.GuildId
}) })
.ToArray(); .ToArray();
public class GeneratingChannel public class GeneratingChannel
{ {

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,202 @@
using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace EllieBot.Migrations.Mysql
{
/// <inheritdoc />
public partial class greetsettings : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "autodeletebyemessagestimer",
table: "guildconfigs");
migrationBuilder.DropColumn(
name: "autodeletegreetmessagestimer",
table: "guildconfigs");
migrationBuilder.DropColumn(
name: "boostmessage",
table: "guildconfigs");
migrationBuilder.DropColumn(
name: "boostmessagechannelid",
table: "guildconfigs");
migrationBuilder.DropColumn(
name: "boostmessagedeleteafter",
table: "guildconfigs");
migrationBuilder.DropColumn(
name: "byemessagechannelid",
table: "guildconfigs");
migrationBuilder.DropColumn(
name: "channelbyemessagetext",
table: "guildconfigs");
migrationBuilder.DropColumn(
name: "channelgreetmessagetext",
table: "guildconfigs");
migrationBuilder.DropColumn(
name: "dmgreetmessagetext",
table: "guildconfigs");
migrationBuilder.DropColumn(
name: "greetmessagechannelid",
table: "guildconfigs");
migrationBuilder.DropColumn(
name: "sendboostmessage",
table: "guildconfigs");
migrationBuilder.DropColumn(
name: "sendchannelbyemessage",
table: "guildconfigs");
migrationBuilder.DropColumn(
name: "sendchannelgreetmessage",
table: "guildconfigs");
migrationBuilder.DropColumn(
name: "senddmgreetmessage",
table: "guildconfigs");
migrationBuilder.CreateTable(
name: "greetsettings",
columns: table => new
{
id = table.Column<int>(type: "int", nullable: false)
.Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn),
guildid = table.Column<ulong>(type: "bigint unsigned", nullable: false),
greettype = table.Column<int>(type: "int", nullable: false),
messagetext = table.Column<string>(type: "longtext", nullable: true)
.Annotation("MySql:CharSet", "utf8mb4"),
isenabled = table.Column<bool>(type: "tinyint(1)", nullable: false),
channelid = table.Column<ulong>(type: "bigint unsigned", nullable: true),
autodeletetimer = table.Column<int>(type: "int", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("pk_greetsettings", x => x.id);
})
.Annotation("MySql:CharSet", "utf8mb4");
migrationBuilder.CreateIndex(
name: "ix_greetsettings_guildid_greettype",
table: "greetsettings",
columns: new[] { "guildid", "greettype" },
unique: true);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "greetsettings");
migrationBuilder.AddColumn<int>(
name: "autodeletebyemessagestimer",
table: "guildconfigs",
type: "int",
nullable: false,
defaultValue: 0);
migrationBuilder.AddColumn<int>(
name: "autodeletegreetmessagestimer",
table: "guildconfigs",
type: "int",
nullable: false,
defaultValue: 0);
migrationBuilder.AddColumn<string>(
name: "boostmessage",
table: "guildconfigs",
type: "longtext",
nullable: true)
.Annotation("MySql:CharSet", "utf8mb4");
migrationBuilder.AddColumn<ulong>(
name: "boostmessagechannelid",
table: "guildconfigs",
type: "bigint unsigned",
nullable: false,
defaultValue: 0ul);
migrationBuilder.AddColumn<int>(
name: "boostmessagedeleteafter",
table: "guildconfigs",
type: "int",
nullable: false,
defaultValue: 0);
migrationBuilder.AddColumn<ulong>(
name: "byemessagechannelid",
table: "guildconfigs",
type: "bigint unsigned",
nullable: false,
defaultValue: 0ul);
migrationBuilder.AddColumn<string>(
name: "channelbyemessagetext",
table: "guildconfigs",
type: "longtext",
nullable: true)
.Annotation("MySql:CharSet", "utf8mb4");
migrationBuilder.AddColumn<string>(
name: "channelgreetmessagetext",
table: "guildconfigs",
type: "longtext",
nullable: true)
.Annotation("MySql:CharSet", "utf8mb4");
migrationBuilder.AddColumn<string>(
name: "dmgreetmessagetext",
table: "guildconfigs",
type: "longtext",
nullable: true)
.Annotation("MySql:CharSet", "utf8mb4");
migrationBuilder.AddColumn<ulong>(
name: "greetmessagechannelid",
table: "guildconfigs",
type: "bigint unsigned",
nullable: false,
defaultValue: 0ul);
migrationBuilder.AddColumn<bool>(
name: "sendboostmessage",
table: "guildconfigs",
type: "tinyint(1)",
nullable: false,
defaultValue: false);
migrationBuilder.AddColumn<bool>(
name: "sendchannelbyemessage",
table: "guildconfigs",
type: "tinyint(1)",
nullable: false,
defaultValue: false);
migrationBuilder.AddColumn<bool>(
name: "sendchannelgreetmessage",
table: "guildconfigs",
type: "tinyint(1)",
nullable: false,
defaultValue: false);
migrationBuilder.AddColumn<bool>(
name: "senddmgreetmessage",
table: "guildconfigs",
type: "tinyint(1)",
nullable: false,
defaultValue: false);
}
}
}

File diff suppressed because it is too large Load diff

View file

@ -9,12 +9,6 @@ namespace EllieBot.Migrations.PostgreSql
{ {
protected override void Up(MigrationBuilder migrationBuilder) protected override void Up(MigrationBuilder migrationBuilder)
{ {
migrationBuilder.AddColumn<decimal>(
name: "rolerequirement",
table: "shopentry",
type: "numeric(20,0)",
nullable: true);
migrationBuilder.CreateTable( migrationBuilder.CreateTable(
name: "autopublishchannel", name: "autopublishchannel",
columns: table => new columns: table => new
@ -41,10 +35,6 @@ namespace EllieBot.Migrations.PostgreSql
{ {
migrationBuilder.DropTable( migrationBuilder.DropTable(
name: "autopublishchannel"); name: "autopublishchannel");
migrationBuilder.DropColumn(
name: "rolerequirement",
table: "shopentry");
} }
} }
} }

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,196 @@
using Microsoft.EntityFrameworkCore.Migrations;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
#nullable disable
namespace EllieBot.Migrations.PostgreSql
{
/// <inheritdoc />
public partial class greetsettings : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "autodeletebyemessagestimer",
table: "guildconfigs");
migrationBuilder.DropColumn(
name: "autodeletegreetmessagestimer",
table: "guildconfigs");
migrationBuilder.DropColumn(
name: "boostmessage",
table: "guildconfigs");
migrationBuilder.DropColumn(
name: "boostmessagechannelid",
table: "guildconfigs");
migrationBuilder.DropColumn(
name: "boostmessagedeleteafter",
table: "guildconfigs");
migrationBuilder.DropColumn(
name: "byemessagechannelid",
table: "guildconfigs");
migrationBuilder.DropColumn(
name: "channelbyemessagetext",
table: "guildconfigs");
migrationBuilder.DropColumn(
name: "channelgreetmessagetext",
table: "guildconfigs");
migrationBuilder.DropColumn(
name: "dmgreetmessagetext",
table: "guildconfigs");
migrationBuilder.DropColumn(
name: "greetmessagechannelid",
table: "guildconfigs");
migrationBuilder.DropColumn(
name: "sendboostmessage",
table: "guildconfigs");
migrationBuilder.DropColumn(
name: "sendchannelbyemessage",
table: "guildconfigs");
migrationBuilder.DropColumn(
name: "sendchannelgreetmessage",
table: "guildconfigs");
migrationBuilder.DropColumn(
name: "senddmgreetmessage",
table: "guildconfigs");
migrationBuilder.CreateTable(
name: "greetsettings",
columns: table => new
{
id = table.Column<int>(type: "integer", nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
guildid = table.Column<decimal>(type: "numeric(20,0)", nullable: false),
greettype = table.Column<int>(type: "integer", nullable: false),
messagetext = table.Column<string>(type: "text", nullable: true),
isenabled = table.Column<bool>(type: "boolean", nullable: false),
channelid = table.Column<decimal>(type: "numeric(20,0)", nullable: true),
autodeletetimer = table.Column<int>(type: "integer", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("pk_greetsettings", x => x.id);
});
migrationBuilder.CreateIndex(
name: "ix_greetsettings_guildid_greettype",
table: "greetsettings",
columns: new[] { "guildid", "greettype" },
unique: true);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "greetsettings");
migrationBuilder.AddColumn<int>(
name: "autodeletebyemessagestimer",
table: "guildconfigs",
type: "integer",
nullable: false,
defaultValue: 0);
migrationBuilder.AddColumn<int>(
name: "autodeletegreetmessagestimer",
table: "guildconfigs",
type: "integer",
nullable: false,
defaultValue: 0);
migrationBuilder.AddColumn<string>(
name: "boostmessage",
table: "guildconfigs",
type: "text",
nullable: true);
migrationBuilder.AddColumn<decimal>(
name: "boostmessagechannelid",
table: "guildconfigs",
type: "numeric(20,0)",
nullable: false,
defaultValue: 0m);
migrationBuilder.AddColumn<int>(
name: "boostmessagedeleteafter",
table: "guildconfigs",
type: "integer",
nullable: false,
defaultValue: 0);
migrationBuilder.AddColumn<decimal>(
name: "byemessagechannelid",
table: "guildconfigs",
type: "numeric(20,0)",
nullable: false,
defaultValue: 0m);
migrationBuilder.AddColumn<string>(
name: "channelbyemessagetext",
table: "guildconfigs",
type: "text",
nullable: true);
migrationBuilder.AddColumn<string>(
name: "channelgreetmessagetext",
table: "guildconfigs",
type: "text",
nullable: true);
migrationBuilder.AddColumn<string>(
name: "dmgreetmessagetext",
table: "guildconfigs",
type: "text",
nullable: true);
migrationBuilder.AddColumn<decimal>(
name: "greetmessagechannelid",
table: "guildconfigs",
type: "numeric(20,0)",
nullable: false,
defaultValue: 0m);
migrationBuilder.AddColumn<bool>(
name: "sendboostmessage",
table: "guildconfigs",
type: "boolean",
nullable: false,
defaultValue: false);
migrationBuilder.AddColumn<bool>(
name: "sendchannelbyemessage",
table: "guildconfigs",
type: "boolean",
nullable: false,
defaultValue: false);
migrationBuilder.AddColumn<bool>(
name: "sendchannelgreetmessage",
table: "guildconfigs",
type: "boolean",
nullable: false,
defaultValue: false);
migrationBuilder.AddColumn<bool>(
name: "senddmgreetmessage",
table: "guildconfigs",
type: "boolean",
nullable: false,
defaultValue: false);
}
}
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,219 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace EllieBot.Migrations
{
/// <inheritdoc />
public partial class greetsettings : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "GreetSettings",
columns: table => new
{
Id = table.Column<int>(type: "INTEGER", nullable: false)
.Annotation("Sqlite:Autoincrement", true),
GuildId = table.Column<ulong>(type: "INTEGER", nullable: false),
GreetType = table.Column<int>(type: "INTEGER", nullable: false),
MessageText = table.Column<string>(type: "TEXT", nullable: true),
IsEnabled = table.Column<bool>(type: "INTEGER", nullable: false),
ChannelId = table.Column<ulong>(type: "INTEGER", nullable: true),
AutoDeleteTimer = table.Column<int>(type: "INTEGER", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_GreetSettings", x => x.Id);
});
migrationBuilder.CreateIndex(
name: "IX_GreetSettings_GuildId_GreetType",
table: "GreetSettings",
columns: new[] { "GuildId", "GreetType" },
unique: true);
migrationBuilder.Sql("""
INSERT INTO GreetSettings (GuildId, GreetType, MessageText, IsEnabled, ChannelId, AutoDeleteTimer)
SELECT GuildId, 0, ChannelGreetMessageText, SendChannelGreetMessage, GreetMessageChannelId, AutoDeleteGreetMessagesTimer
FROM GuildConfigs
WHERE SendChannelGreetMessage = 1;
INSERT INTO GreetSettings (GuildId, GreetType, MessageText, IsEnabled, ChannelId, AutoDeleteTimer)
SELECT GuildId, 1, DmGreetMessageText, SendDmGreetMessage, GreetMessageChannelId, 0
FROM GuildConfigs
WHERE SendDmGreetMessage = 1;
INSERT INTO GreetSettings (GuildId, GreetType, MessageText, IsEnabled, ChannelId, AutoDeleteTimer)
SELECT GuildId, 2, ChannelByeMessageText, SendChannelByeMessage, ByeMessageChannelId, AutoDeleteByeMessagesTimer
FROM GuildConfigs
WHERE SendChannelByeMessage = 1;
INSERT INTO GreetSettings (GuildId, GreetType, MessageText, IsEnabled, ChannelId, AutoDeleteTimer)
SELECT GuildId, 3, BoostMessage, SendBoostMessage, BoostMessageChannelId, BoostMessageDeleteAfter
FROM GuildConfigs
WHERE SendBoostMessage = 1;
""");
migrationBuilder.DropColumn(
name: "AutoDeleteByeMessagesTimer",
table: "GuildConfigs");
migrationBuilder.DropColumn(
name: "AutoDeleteGreetMessagesTimer",
table: "GuildConfigs");
migrationBuilder.DropColumn(
name: "BoostMessage",
table: "GuildConfigs");
migrationBuilder.DropColumn(
name: "BoostMessageChannelId",
table: "GuildConfigs");
migrationBuilder.DropColumn(
name: "BoostMessageDeleteAfter",
table: "GuildConfigs");
migrationBuilder.DropColumn(
name: "ByeMessageChannelId",
table: "GuildConfigs");
migrationBuilder.DropColumn(
name: "ChannelByeMessageText",
table: "GuildConfigs");
migrationBuilder.DropColumn(
name: "ChannelGreetMessageText",
table: "GuildConfigs");
migrationBuilder.DropColumn(
name: "DmGreetMessageText",
table: "GuildConfigs");
migrationBuilder.DropColumn(
name: "GreetMessageChannelId",
table: "GuildConfigs");
migrationBuilder.DropColumn(
name: "SendBoostMessage",
table: "GuildConfigs");
migrationBuilder.DropColumn(
name: "SendChannelByeMessage",
table: "GuildConfigs");
migrationBuilder.DropColumn(
name: "SendChannelGreetMessage",
table: "GuildConfigs");
migrationBuilder.DropColumn(
name: "SendDmGreetMessage",
table: "GuildConfigs");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "GreetSettings");
migrationBuilder.AddColumn<int>(
name: "AutoDeleteByeMessagesTimer",
table: "GuildConfigs",
type: "INTEGER",
nullable: false,
defaultValue: 0);
migrationBuilder.AddColumn<int>(
name: "AutoDeleteGreetMessagesTimer",
table: "GuildConfigs",
type: "INTEGER",
nullable: false,
defaultValue: 0);
migrationBuilder.AddColumn<string>(
name: "BoostMessage",
table: "GuildConfigs",
type: "TEXT",
nullable: true);
migrationBuilder.AddColumn<ulong>(
name: "BoostMessageChannelId",
table: "GuildConfigs",
type: "INTEGER",
nullable: false,
defaultValue: 0ul);
migrationBuilder.AddColumn<int>(
name: "BoostMessageDeleteAfter",
table: "GuildConfigs",
type: "INTEGER",
nullable: false,
defaultValue: 0);
migrationBuilder.AddColumn<ulong>(
name: "ByeMessageChannelId",
table: "GuildConfigs",
type: "INTEGER",
nullable: false,
defaultValue: 0ul);
migrationBuilder.AddColumn<string>(
name: "ChannelByeMessageText",
table: "GuildConfigs",
type: "TEXT",
nullable: true);
migrationBuilder.AddColumn<string>(
name: "ChannelGreetMessageText",
table: "GuildConfigs",
type: "TEXT",
nullable: true);
migrationBuilder.AddColumn<string>(
name: "DmGreetMessageText",
table: "GuildConfigs",
type: "TEXT",
nullable: true);
migrationBuilder.AddColumn<ulong>(
name: "GreetMessageChannelId",
table: "GuildConfigs",
type: "INTEGER",
nullable: false,
defaultValue: 0ul);
migrationBuilder.AddColumn<bool>(
name: "SendBoostMessage",
table: "GuildConfigs",
type: "INTEGER",
nullable: false,
defaultValue: false);
migrationBuilder.AddColumn<bool>(
name: "SendChannelByeMessage",
table: "GuildConfigs",
type: "INTEGER",
nullable: false,
defaultValue: false);
migrationBuilder.AddColumn<bool>(
name: "SendChannelGreetMessage",
table: "GuildConfigs",
type: "INTEGER",
nullable: false,
defaultValue: false);
migrationBuilder.AddColumn<bool>(
name: "SendDmGreetMessage",
table: "GuildConfigs",
type: "INTEGER",
nullable: false,
defaultValue: false);
}
}
}

View file

@ -5,238 +5,223 @@ public partial class Administration
[Group] [Group]
public partial class GreetCommands : EllieModule<GreetService> public partial class GreetCommands : EllieModule<GreetService>
{ {
[Cmd] public async Task Toggle(GreetType type)
[RequireContext(ContextType.Guild)]
[UserPerm(GuildPerm.ManageGuild)]
public async Task Boost()
{ {
var enabled = await _service.ToggleBoost(ctx.Guild.Id, ctx.Channel.Id); var enabled = await _service.SetGreet(ctx.Guild.Id, ctx.Channel.Id, type);
if (enabled) if (enabled)
await Response().Confirm(strs.boost_on).SendAsync(); await Response()
.Confirm(
type switch
{
GreetType.Boost => strs.boost_on,
GreetType.Greet => strs.greet_on,
GreetType.Bye => strs.bye_on,
GreetType.GreetDm => strs.greetdm_on,
_ => strs.error
}
)
.SendAsync();
else else
await Response().Pending(strs.boost_off).SendAsync(); await Response()
.Pending(
type switch
{
GreetType.Boost => strs.boost_off,
GreetType.Greet => strs.greet_off,
GreetType.Bye => strs.bye_off,
GreetType.GreetDm => strs.greetdm_off,
_ => strs.error
}
)
.SendAsync();
} }
[Cmd]
[RequireContext(ContextType.Guild)] public async Task SetDel(GreetType type, int timer)
[UserPerm(GuildPerm.ManageGuild)]
public async Task BoostDel(int timer = 30)
{ {
if (timer is < 0 or > 600) if (timer is < 0 or > 600)
return; return;
await _service.SetBoostDel(ctx.Guild.Id, timer); await _service.SetDeleteTimer(ctx.Guild.Id, type, timer);
if (timer > 0) if (timer > 0)
await Response().Confirm(strs.boostdel_on(timer)).SendAsync(); await Response()
.Confirm(
type switch
{
GreetType.Boost => strs.boostdel_on(timer),
GreetType.Greet => strs.greetdel_on(timer),
GreetType.Bye => strs.byedel_on(timer),
_ => strs.error
}
)
.SendAsync();
else else
await Response().Pending(strs.boostdel_off).SendAsync(); await Response()
.Pending(
type switch
{
GreetType.Boost => strs.boostdel_off,
GreetType.Greet => strs.greetdel_off,
GreetType.Bye => strs.byedel_off,
_ => strs.error
})
.SendAsync();
} }
[Cmd]
[RequireContext(ContextType.Guild)] public async Task SetMsg(GreetType type, string? text = null)
[UserPerm(GuildPerm.ManageGuild)]
public async Task BoostMsg([Leftover] string? text = null)
{ {
if (string.IsNullOrWhiteSpace(text)) if (string.IsNullOrWhiteSpace(text))
{ {
var boostMessage = _service.GetBoostMessage(ctx.Guild.Id); await _service.SetMessage(ctx.Guild.Id, type, null);
await Response().Confirm(strs.boostmsg_cur(boostMessage?.SanitizeMentions())).SendAsync(); var conf = await _service.GetGreetSettingsAsync(ctx.Guild.Id, type);
var msg = conf?.MessageText ?? "No message set.";
await Response()
.Confirm(
type switch
{
GreetType.Boost => strs.boostmsg_cur(msg.SanitizeMentions()),
GreetType.Greet => strs.greetmsg_cur(msg.SanitizeMentions()),
GreetType.Bye => strs.byemsg_cur(msg.SanitizeMentions()),
GreetType.GreetDm => strs.greetdmmsg_cur(msg.SanitizeMentions()),
_ => strs.error
})
.SendAsync();
return; return;
} }
var sendBoostEnabled = _service.SetBoostMessage(ctx.Guild.Id, ref text); var isEnabled = await _service.SetMessage(ctx.Guild.Id, type, text);
await Response().Confirm(strs.boostmsg_new).SendAsync(); await Response()
if (!sendBoostEnabled) .Confirm(type switch
await Response().Pending(strs.boostmsg_enable($"`{prefix}boost`")).SendAsync(); {
} GreetType.Boost => strs.boostmsg_new,
GreetType.Greet => strs.greetmsg_new,
GreetType.Bye => strs.byemsg_new,
GreetType.GreetDm => strs.greetdmmsg_new,
_ => strs.error
})
.SendAsync();
[Cmd]
[RequireContext(ContextType.Guild)]
[UserPerm(GuildPerm.ManageGuild)]
public async Task GreetDel(int timer = 30)
{
if (timer is < 0 or > 600)
return;
await _service.SetGreetDel(ctx.Guild.Id, timer); if (!isEnabled)
if (timer > 0)
await Response().Confirm(strs.greetdel_on(timer)).SendAsync();
else
await Response().Pending(strs.greetdel_off).SendAsync();
}
[Cmd]
[RequireContext(ContextType.Guild)]
[UserPerm(GuildPerm.ManageGuild)]
public async Task Greet()
{
var enabled = await _service.SetGreet(ctx.Guild.Id, ctx.Channel.Id);
if (enabled)
await Response().Confirm(strs.greet_on).SendAsync();
else
await Response().Pending(strs.greet_off).SendAsync();
}
[Cmd]
[RequireContext(ContextType.Guild)]
[UserPerm(GuildPerm.ManageGuild)]
public async Task GreetMsg([Leftover] string? text = null)
{
if (string.IsNullOrWhiteSpace(text))
{ {
var greetMsg = _service.GetGreetMsg(ctx.Guild.Id); var cmdName = type switch
await Response().Confirm(strs.greetmsg_cur(greetMsg?.SanitizeMentions())).SendAsync(); {
return; GreetType.Greet => "greet",
GreetType.Bye => "bye",
GreetType.Boost => "boost",
GreetType.GreetDm => "greetdm",
_ => "unknown_command"
};
await Response().Pending(strs.boostmsg_enable($"`{prefix}{cmdName}`")).SendAsync();
} }
var sendGreetEnabled = _service.SetGreetMessage(ctx.Guild.Id, ref text);
await Response().Confirm(strs.greetmsg_new).SendAsync();
if (!sendGreetEnabled)
await Response().Pending(strs.greetmsg_enable($"`{prefix}greet`")).SendAsync();
} }
[Cmd] [Cmd]
[RequireContext(ContextType.Guild)] [RequireContext(ContextType.Guild)]
[UserPerm(GuildPerm.ManageGuild)] [UserPerm(GuildPerm.ManageGuild)]
public async Task GreetDm() public Task Boost()
{ => Toggle(GreetType.Boost);
var enabled = await _service.SetGreetDm(ctx.Guild.Id);
if (enabled)
await Response().Confirm(strs.greetdm_on).SendAsync();
else
await Response().Confirm(strs.greetdm_off).SendAsync();
}
[Cmd] [Cmd]
[RequireContext(ContextType.Guild)] [RequireContext(ContextType.Guild)]
[UserPerm(GuildPerm.ManageGuild)] [UserPerm(GuildPerm.ManageGuild)]
public async Task GreetDmMsg([Leftover] string? text = null) public Task BoostDel(int timer = 30)
{ => SetDel(GreetType.Boost, timer);
if (string.IsNullOrWhiteSpace(text))
{
var dmGreetMsg = _service.GetDmGreetMsg(ctx.Guild.Id);
await Response().Confirm(strs.greetdmmsg_cur(dmGreetMsg?.SanitizeMentions())).SendAsync();
return;
}
var sendGreetEnabled = _service.SetGreetDmMessage(ctx.Guild.Id, ref text);
await Response().Confirm(strs.greetdmmsg_new).SendAsync();
if (!sendGreetEnabled)
await Response().Pending(strs.greetdmmsg_enable($"`{prefix}greetdm`")).SendAsync();
}
[Cmd] [Cmd]
[RequireContext(ContextType.Guild)] [RequireContext(ContextType.Guild)]
[UserPerm(GuildPerm.ManageGuild)] [UserPerm(GuildPerm.ManageGuild)]
public async Task Bye() public Task BoostMsg([Leftover] string? text = null)
{ => SetMsg(GreetType.Boost, text);
var enabled = await _service.SetBye(ctx.Guild.Id, ctx.Channel.Id);
if (enabled)
await Response().Confirm(strs.bye_on).SendAsync();
else
await Response().Confirm(strs.bye_off).SendAsync();
}
[Cmd] [Cmd]
[RequireContext(ContextType.Guild)] [RequireContext(ContextType.Guild)]
[UserPerm(GuildPerm.ManageGuild)] [UserPerm(GuildPerm.ManageGuild)]
public async Task ByeMsg([Leftover] string? text = null) public Task Greet()
{ => Toggle(GreetType.Greet);
if (string.IsNullOrWhiteSpace(text))
{
var byeMsg = _service.GetByeMessage(ctx.Guild.Id);
await Response().Confirm(strs.byemsg_cur(byeMsg?.SanitizeMentions())).SendAsync();
return;
}
var sendByeEnabled = _service.SetByeMessage(ctx.Guild.Id, ref text);
await Response().Confirm(strs.byemsg_new).SendAsync();
if (!sendByeEnabled)
await Response().Pending(strs.byemsg_enable($"`{prefix}bye`")).SendAsync();
}
[Cmd] [Cmd]
[RequireContext(ContextType.Guild)] [RequireContext(ContextType.Guild)]
[UserPerm(GuildPerm.ManageGuild)] [UserPerm(GuildPerm.ManageGuild)]
public async Task ByeDel(int timer = 30) public Task GreetDel(int timer = 30)
{ => SetDel(GreetType.Greet, timer);
await _service.SetByeDel(ctx.Guild.Id, timer);
if (timer > 0) [Cmd]
await Response().Confirm(strs.byedel_on(timer)).SendAsync(); [RequireContext(ContextType.Guild)]
else [UserPerm(GuildPerm.ManageGuild)]
await Response().Pending(strs.byedel_off).SendAsync(); public Task GreetMsg([Leftover] string? text = null)
} => SetMsg(GreetType.Greet, text);
[Cmd]
[RequireContext(ContextType.Guild)]
[UserPerm(GuildPerm.ManageGuild)]
public Task GreetDm()
=> Toggle(GreetType.GreetDm);
[Cmd]
[RequireContext(ContextType.Guild)]
[UserPerm(GuildPerm.ManageGuild)]
public Task GreetDmMsg([Leftover] string? text = null)
=> SetMsg(GreetType.GreetDm, text);
[Cmd]
[RequireContext(ContextType.Guild)]
[UserPerm(GuildPerm.ManageGuild)]
public Task Bye()
=> Toggle(GreetType.Bye);
[Cmd]
[RequireContext(ContextType.Guild)]
[UserPerm(GuildPerm.ManageGuild)]
public Task ByeDel(int timer = 30)
=> SetDel(GreetType.Bye, timer);
[Cmd]
[RequireContext(ContextType.Guild)]
[UserPerm(GuildPerm.ManageGuild)]
public Task ByeMsg([Leftover] string? text = null)
=> SetMsg(GreetType.Bye, text);
[Cmd] [Cmd]
[RequireContext(ContextType.Guild)] [RequireContext(ContextType.Guild)]
[UserPerm(GuildPerm.ManageGuild)] [UserPerm(GuildPerm.ManageGuild)]
[Ratelimit(5)] [Ratelimit(5)]
public async Task ByeTest([Leftover] IGuildUser? user = null) public Task GreetTest([Leftover] IGuildUser? user = null)
{ => Test(GreetType.Greet, user);
user ??= (IGuildUser)ctx.User;
await _service.ByeTest((ITextChannel)ctx.Channel, user);
var enabled = _service.GetByeEnabled(ctx.Guild.Id);
if (!enabled)
await Response().Pending(strs.byemsg_enable($"`{prefix}bye`")).SendAsync();
}
[Cmd] [Cmd]
[RequireContext(ContextType.Guild)] [RequireContext(ContextType.Guild)]
[UserPerm(GuildPerm.ManageGuild)] [UserPerm(GuildPerm.ManageGuild)]
[Ratelimit(5)] [Ratelimit(5)]
public async Task GreetTest([Leftover] IGuildUser? user = null) public Task GreetDmTest([Leftover] IGuildUser? user = null)
{ => Test(GreetType.GreetDm, user);
user ??= (IGuildUser)ctx.User;
await _service.GreetTest((ITextChannel)ctx.Channel, user);
var enabled = _service.GetGreetEnabled(ctx.Guild.Id);
if (!enabled)
await Response().Pending(strs.greetmsg_enable($"`{prefix}greet`")).SendAsync();
}
[Cmd] [Cmd]
[RequireContext(ContextType.Guild)] [RequireContext(ContextType.Guild)]
[UserPerm(GuildPerm.ManageGuild)] [UserPerm(GuildPerm.ManageGuild)]
[Ratelimit(5)] [Ratelimit(5)]
public async Task GreetDmTest([Leftover] IGuildUser? user = null) public Task ByeTest([Leftover] IGuildUser? user = null)
{ => Test(GreetType.Bye, user);
user ??= (IGuildUser)ctx.User;
var success = await _service.GreetDmTest(user);
if (success)
await ctx.OkAsync();
else
await ctx.WarningAsync();
var enabled = _service.GetGreetDmEnabled(ctx.Guild.Id);
if (!enabled)
await Response().Pending(strs.greetdmmsg_enable($"`{prefix}greetdm`")).SendAsync();
}
[Cmd] [Cmd]
[RequireContext(ContextType.Guild)] [RequireContext(ContextType.Guild)]
[UserPerm(GuildPerm.ManageGuild)] [UserPerm(GuildPerm.ManageGuild)]
[Ratelimit(5)] [Ratelimit(5)]
public async Task BoostTest([Leftover] IGuildUser? user = null) public Task BoostTest([Leftover] IGuildUser? user = null)
=> Test(GreetType.Boost, user);
public async Task Test(GreetType type, IGuildUser? user = null)
{ {
user ??= (IGuildUser)ctx.User; user ??= (IGuildUser)ctx.User;
await _service.BoostTest((ITextChannel)ctx.Channel, user); await _service.Test(ctx.Guild.Id, type, (ITextChannel)ctx.Channel, user);
var enabled = _service.GetBoostEnabled(ctx.Guild.Id); var conf = await _service.GetGreetSettingsAsync(ctx.Guild.Id, type);
if (!enabled) if (conf?.IsEnabled is not true)
await Response().Pending(strs.boostmsg_enable($"`{prefix}boost`")).SendAsync(); await Response().Pending(strs.boostmsg_enable($"`{prefix}boost`")).SendAsync();
} }
} }

View file

@ -1,71 +0,0 @@
namespace EllieBot.Services;
public class GreetGrouper<T>
{
private readonly Dictionary<ulong, HashSet<T>> _group;
private readonly object _locker = new();
public GreetGrouper()
=> _group = new();
/// <summary>
/// Creates a group, if group already exists, adds the specified user
/// </summary>
/// <param name="guildId">Id of the server for which to create group for</param>
/// <param name="toAddIfExists">User to add if group already exists</param>
/// <returns></returns>
public bool CreateOrAdd(ulong guildId, T toAddIfExists)
{
lock (_locker)
{
if (_group.TryGetValue(guildId, out var list))
{
list.Add(toAddIfExists);
return false;
}
_group[guildId] = new();
return true;
}
}
/// <summary>
/// Remove the specified amount of items from the group. If all items are removed, group will be removed.
/// </summary>
/// <param name="guildId">Id of the group</param>
/// <param name="count">Maximum number of items to retrieve</param>
/// <param name="items">Items retrieved</param>
/// <returns>Whether the group has no more items left and is deleted</returns>
public bool ClearGroup(ulong guildId, int count, out IReadOnlyCollection<T> items)
{
lock (_locker)
{
if (_group.TryGetValue(guildId, out var set))
{
// if we want more than there are, return everything
if (count >= set.Count)
{
items = set;
_group.Remove(guildId);
return true;
}
// if there are more in the group than what's needed
// take the requested number, remove them from the set
// and return them
var toReturn = set.TakeWhile(_ => count-- != 0).ToList();
foreach (var item in toReturn)
set.Remove(item);
items = toReturn;
// returning falsemeans group is not yet deleted
// because there are items left
return false;
}
items = Array.Empty<T>();
return true;
}
}
}

View file

@ -8,36 +8,38 @@ namespace EllieBot.Services;
public class GreetService : IEService, IReadyExecutor public class GreetService : IEService, IReadyExecutor
{ {
public bool GroupGreets
=> _bss.Data.GroupGreets;
private readonly DbService _db; private readonly DbService _db;
private ConcurrentHashSet<ulong> _greetDmEnabledGuilds = new(); private ConcurrentDictionary<GreetType, ConcurrentHashSet<ulong>> _enabled = 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<IUser> _byes = new();
private readonly BotConfigService _bss; private readonly BotConfigService _bss;
private readonly IReplacementService _repSvc; private readonly IReplacementService _repSvc;
private readonly IBotCache _cache;
private readonly IMessageSenderService _sender; private readonly IMessageSenderService _sender;
private readonly Channel<(GreetSettings, IUser, ITextChannel?)> _greetQueue =
Channel.CreateBounded<(GreetSettings, IUser, ITextChannel?)>(
new BoundedChannelOptions(60)
{
FullMode = BoundedChannelFullMode.DropOldest
});
public GreetService( public GreetService(
DiscordSocketClient client, DiscordSocketClient client,
DbService db, DbService db,
BotConfigService bss, BotConfigService bss,
IMessageSenderService sender, IMessageSenderService sender,
IReplacementService repSvc IReplacementService repSvc,
IBotCache cache
) )
{ {
_db = db; _db = db;
_client = client; _client = client;
_bss = bss; _bss = bss;
_repSvc = repSvc; _repSvc = repSvc;
_cache = cache;
_sender = sender; _sender = sender;
} }
@ -49,16 +51,12 @@ public class GreetService : IEService, IReadyExecutor
var guilds = _client.Guilds.Select(x => x.Id).ToList(); var guilds = _client.Guilds.Select(x => x.Id).ToList();
var enabled = await uow.GetTable<GreetSettings>() var enabled = await uow.GetTable<GreetSettings>()
.Where(x => x.GuildId.In(guilds)) .Where(x => x.GuildId.In(guilds))
.Where(x => x.SendChannelGreetMessage .Where(x => x.IsEnabled)
|| x.SendBoostMessage
|| x.SendChannelByeMessage
|| x.SendDmGreetMessage)
.ToListAsync(); .ToListAsync();
_boostEnabledGuilds = new(enabled.Where(x => x.SendBoostMessage).Select(x => x.GuildId)); _enabled = enabled.GroupBy(x => x.GreetType, v => v.GuildId)
_byeEnabledGuilds = new(enabled.Where(x => x.SendChannelByeMessage).Select(x => x.GuildId)); .ToDictionary(x => x.Key, x => x.ToHashSet().ToConcurrentSet())
_greetDmEnabledGuilds = new(enabled.Where(x => x.SendDmGreetMessage).Select(x => x.GuildId)); .ToConcurrent();
_greetEnabledGuilds = new(enabled.Where(x => x.SendChannelGreetMessage).Select(x => x.GuildId));
} }
_client.UserJoined += OnUserJoined; _client.UserJoined += OnUserJoined;
@ -71,9 +69,8 @@ public class GreetService : IEService, IReadyExecutor
var timer = new PeriodicTimer(TimeSpan.FromSeconds(2)); var timer = new PeriodicTimer(TimeSpan.FromSeconds(2));
while (await timer.WaitForNextTickAsync()) while (await timer.WaitForNextTickAsync())
{ {
var (conf, user, compl) = await _greetDmQueue.Reader.ReadAsync(); var (conf, user, ch) = await _greetQueue.Reader.ReadAsync();
var res = await GreetDmUserInternal(conf, user); await GreetUsers(conf, ch, user);
compl.TrySetResult(res);
} }
} }
@ -88,62 +85,35 @@ public class GreetService : IEService, IReadyExecutor
{ {
_ = Task.Run(async () => _ = Task.Run(async () =>
{ {
var conf = await GetGreetSettingsAsync(newUser.Guild.Id); var conf = await GetGreetSettingsAsync(newUser.Guild.Id, GreetType.Boost);
if (conf is null || !conf.SendBoostMessage) if (conf is null || !conf.IsEnabled)
return; return;
await TriggerBoostMessage(conf, newUser); ITextChannel? channel = null;
if (conf.ChannelId is { } cid)
channel = newUser.Guild.GetTextChannel(cid);
if (channel is null)
return;
await GreetUsers(conf, channel, newUser);
}); });
} }
return Task.CompletedTask; return Task.CompletedTask;
} }
private async Task TriggerBoostMessage(GreetSettings conf, SocketGuildUser user) private async Task OnClientLeftGuild(SocketGuild guild)
{ {
var channel = user.Guild.GetTextChannel(conf.BoostMessageChannelId); foreach (var gt in Enum.GetValues<GreetType>())
if (channel is null)
return;
await SendBoostMessage(conf, user, channel);
}
private async Task<bool> SendBoostMessage(GreetSettings conf, IGuildUser user, ITextChannel channel)
{
if (string.IsNullOrWhiteSpace(conf.BoostMessage))
return false;
var toSend = SmartText.CreateFrom(conf.BoostMessage);
try
{ {
var newContent = await _repSvc.ReplaceAsync(toSend, _enabled[gt].TryRemove(guild.Id);
new(client: _client, guild: user.Guild, channel: channel, users: user));
var toDelete = await _sender.Response(channel).Text(newContent).Sanitize(false).SendAsync();
if (conf.BoostMessageDeleteAfter > 0)
toDelete.DeleteAfter(conf.BoostMessageDeleteAfter);
return true;
} }
catch (Exception ex)
{
Log.Error(ex, "Error sending boost message");
}
return false;
}
private async Task OnClientLeftGuild(SocketGuild arg)
{
_boostEnabledGuilds.TryRemove(arg.Id);
_byeEnabledGuilds.TryRemove(arg.Id);
_greetDmEnabledGuilds.TryRemove(arg.Id);
_greetEnabledGuilds.TryRemove(arg.Id);
await using var uow = _db.GetDbContext(); await using var uow = _db.GetDbContext();
await uow.GetTable<GreetSettings>() await uow.GetTable<GreetSettings>()
.Where(x => x.GuildId == arg.Id) .Where(x => x.GuildId == guild.Id)
.DeleteAsync(); .DeleteAsync();
} }
@ -153,38 +123,20 @@ public class GreetService : IEService, IReadyExecutor
{ {
try try
{ {
var conf = await GetGreetSettingsAsync(guild.Id); var conf = await GetGreetSettingsAsync(guild.Id, GreetType.Bye);
if (conf is null) 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.ChannelId);
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
{
await SetGreet(guild.Id, null, GreetType.Bye, false);
return; return;
}
if (GroupGreets) await _greetQueue.Writer.WriteAsync((conf, user, channel));
{
// if group is newly created, greet that user right away,
// but any user which joins in the next 5 seconds will
// be greeted in a group greet
if (_byes.CreateOrAdd(guild.Id, user))
{
// greet single user
await ByeUsers(conf, channel, new[] { user });
var groupClear = false;
while (!groupClear)
{
await Task.Delay(5000);
groupClear = _byes.ClearGroup(guild.Id, 5, out var toBye);
await ByeUsers(conf, channel, toBye);
}
}
}
else
{
await ByeUsers(conf, channel, new[] { user });
}
} }
catch catch
{ {
@ -194,7 +146,14 @@ public class GreetService : IEService, IReadyExecutor
return Task.CompletedTask; return Task.CompletedTask;
} }
private readonly TypedKey<GreetSettings?> _greetSettingsKey = new();
public async Task<GreetSettings?> GetGreetSettingsAsync(ulong gid, GreetType type) public async Task<GreetSettings?> GetGreetSettingsAsync(ulong gid, GreetType type)
=> await _cache.GetOrAddAsync<GreetSettings?>(_greetSettingsKey,
() => InternalGetGreetSettingsAsync(gid, type),
TimeSpan.FromSeconds(3));
private async Task<GreetSettings?> InternalGetGreetSettingsAsync(ulong gid, GreetType type)
{ {
await using var uow = _db.GetDbContext(); await using var uow = _db.GetDbContext();
var res = await uow.GetTable<GreetSettings>() var res = await uow.GetTable<GreetSettings>()
@ -204,62 +163,32 @@ public class GreetService : IEService, IReadyExecutor
return res; return res;
} }
private Task ByeUsers(GreetSettings conf, ITextChannel channel, IUser user) private async Task GreetUsers(GreetSettings conf, ITextChannel? channel, IUser user)
=> ByeUsers(conf, channel, new[] { user });
private async Task ByeUsers(GreetSettings conf, ITextChannel channel, IReadOnlyCollection<IUser> users)
{ {
if (!users.Any()) if (conf.GreetType == GreetType.GreetDm)
{
if (user is not IGuildUser gu)
return;
await GreetDmUserInternal(conf, gu);
return;
}
if (channel is null)
return; return;
var repCtx = new ReplacementContext(client: _client, var repCtx = new ReplacementContext(client: _client,
guild: channel.Guild, guild: channel.Guild,
channel: channel, channel: channel,
users: users.ToArray()); user: user);
var text = SmartText.CreateFrom(conf.ChannelByeMessageText); var text = SmartText.CreateFrom(conf.MessageText);
text = await _repSvc.ReplaceAsync(text, repCtx); text = await _repSvc.ReplaceAsync(text, repCtx);
try try
{ {
var toDelete = await _sender.Response(channel).Text(text).Sanitize(false).SendAsync(); var toDelete = await _sender.Response(channel).Text(text).Sanitize(false).SendAsync();
if (conf.AutoDeleteByeMessagesTimer > 0) if (conf.AutoDeleteTimer > 0)
toDelete.DeleteAfter(conf.AutoDeleteByeMessagesTimer); toDelete.DeleteAfter(conf.AutoDeleteTimer);
}
catch (HttpException ex) when (ex.DiscordCode == DiscordErrorCode.InsufficientPermissions
|| ex.DiscordCode == DiscordErrorCode.MissingPermissions
|| ex.DiscordCode == DiscordErrorCode.UnknownChannel)
{
Log.Warning(ex,
"Missing permissions to send a bye message, the bye message will be disabled on server: {GuildId}",
channel.GuildId);
await SetGreet(channel.GuildId, channel.Id, GreetType.Bye, false);
}
catch (Exception ex)
{
Log.Warning(ex, "Error embeding bye message");
}
}
private Task GreetUsers(GreetSettings conf, ITextChannel channel, IGuildUser user)
=> GreetUsers(conf, channel, new[] { user });
private async Task GreetUsers(GreetSettings conf, ITextChannel channel, IReadOnlyCollection<IGuildUser> users)
{
if (users.Count == 0)
return;
var repCtx = new ReplacementContext(client: _client,
guild: channel.Guild,
channel: channel,
users: users.ToArray());
var text = SmartText.CreateFrom(conf.ChannelGreetMessageText);
text = await _repSvc.ReplaceAsync(text, repCtx);
try
{
var toDelete = await _sender.Response(channel).Text(text).Sanitize(false).SendAsync();
if (conf.AutoDeleteGreetMessagesTimer > 0)
toDelete.DeleteAfter(conf.AutoDeleteGreetMessagesTimer);
} }
catch (HttpException ex) when (ex.DiscordCode is DiscordErrorCode.InsufficientPermissions catch (HttpException ex) when (ex.DiscordCode is DiscordErrorCode.InsufficientPermissions
or DiscordErrorCode.MissingPermissions or DiscordErrorCode.MissingPermissions
@ -276,19 +205,11 @@ public class GreetService : IEService, IReadyExecutor
} }
} }
private readonly Channel<(GreetSettings, IGuildUser, TaskCompletionSource<bool>)> _greetDmQueue =
Channel.CreateBounded<(GreetSettings, IGuildUser, TaskCompletionSource<bool>)>(new BoundedChannelOptions(60)
{
// The limit of 60 users should be only hit when there's a raid. In that case
// probably the best thing to do is to drop newest (raiding) users
FullMode = BoundedChannelFullMode.DropNewest
});
private async Task<bool> GreetDmUser(GreetSettings conf, IGuildUser user) private async Task<bool> GreetDmUser(GreetSettings conf, IGuildUser user)
{ {
var completionSource = new TaskCompletionSource<bool>(TaskCreationOptions.RunContinuationsAsynchronously); var completionSource = new TaskCompletionSource<bool>(TaskCreationOptions.RunContinuationsAsynchronously);
await _greetDmQueue.Writer.WriteAsync((conf, user, completionSource)); await _greetQueue.Writer.WriteAsync((conf, user, null));
return await completionSource.Task; return await completionSource.Task;
} }
@ -296,8 +217,8 @@ public class GreetService : IEService, IReadyExecutor
{ {
try try
{ {
var repCtx = new ReplacementContext(client: _client, guild: user.Guild, users: user); var repCtx = new ReplacementContext(client: _client, guild: user.Guild, user: user);
var smartText = SmartText.CreateFrom(conf.DmGreetMessageText); var smartText = SmartText.CreateFrom(conf.MessageText);
smartText = await _repSvc.ReplaceAsync(smartText, repCtx); smartText = await _repSvc.ReplaceAsync(smartText, repCtx);
if (smartText is SmartPlainText pt) if (smartText is SmartPlainText pt)
@ -378,40 +299,21 @@ public class GreetService : IEService, IReadyExecutor
{ {
try try
{ {
var conf = await GetGreetSettingsAsync(user.GuildId); var conf = await GetGreetSettingsAsync(user.GuildId, GreetType.Greet);
if (conf is null)
return;
if (conf.SendChannelGreetMessage) if (conf is not null && conf.IsEnabled && conf.ChannelId is { } channelId)
{ {
var channel = await user.Guild.GetTextChannelAsync(conf.GreetMessageChannelId); var channel = await user.Guild.GetTextChannelAsync(channelId);
if (channel is not null) if (channel is not null)
{ {
if (GroupGreets) await _greetQueue.Writer.WriteAsync((conf, user, channel));
{
// if group is newly created, greet that user right away,
// but any user which joins in the next 5 seconds will
// be greeted in a group greet
if (_greets.CreateOrAdd(user.GuildId, user))
{
// greet single user
await GreetUsers(conf, channel, new[] { user });
var groupClear = false;
while (!groupClear)
{
await Task.Delay(5000);
groupClear = _greets.ClearGroup(user.GuildId, 5, out var toGreet);
await GreetUsers(conf, channel, toGreet);
}
}
}
else
await GreetUsers(conf, channel, new[] { user });
} }
} }
if (conf.SendDmGreetMessage) var confDm = await GetGreetSettingsAsync(user.GuildId, GreetType.GreetDm);
await GreetDmUser(conf, user);
if (confDm?.IsEnabled ?? false)
await GreetDmUser(confDm, user);
} }
catch catch
{ {
@ -421,20 +323,16 @@ public class GreetService : IEService, IReadyExecutor
return Task.CompletedTask; return Task.CompletedTask;
} }
// public GreetSettings GetOrAddSettingsForGuild(ulong guildId)
// { private static string GetDefaultGreet(GreetType greetType)
// if (_greetDmEnabledGuilds.TryGetValue(guildId, out var settings)) => greetType switch
// return settings; {
// GreetType.Boost => "%user.name% has boosted the server!",
// using (var uow = _db.GetDbContext()) GreetType.Greet => "%user.name% has joined the server!",
// { GreetType.Bye => "%user.name has left the server!",
// var gc = uow.GuildConfigsForId(guildId, set => set); GreetType.GreetDm => "Welcome to the server %user.name%",
// settings = GreetSettings.Create(gc); _ => "%user.name% did something new!"
// } };
//
// _greetDmEnabledGuilds.TryAdd(guildId, settings);
// return settings;
// }
public async Task<bool> SetGreet( public async Task<bool> SetGreet(
ulong guildId, ulong guildId,
@ -447,9 +345,14 @@ public class GreetService : IEService, IReadyExecutor
if (value is { } v) if (value is { } v)
{ {
var defaultGreet = GetDefaultGreet(greetType);
await q await q
.InsertOrUpdateAsync(() => new() .InsertOrUpdateAsync(() => new()
{ {
GuildId = guildId,
GreetType = greetType,
MessageText = defaultGreet,
IsEnabled = v, IsEnabled = v,
ChannelId = channelId, ChannelId = channelId,
}, },
@ -466,29 +369,45 @@ public class GreetService : IEService, IReadyExecutor
} }
else else
{ {
await q var result = await q
.Where(x => x.GuildId == guildId && x.GreetType == greetType) .Where(x => x.GuildId == guildId && x.GreetType == greetType)
.UpdateAsync((old) => new() .UpdateWithOutputAsync((old) => new()
{ {
IsEnabled = !old.IsEnabled IsEnabled = !old.IsEnabled
}); },
(o, n) => n.IsEnabled);
if (result.Length > 0)
value = result[0];
}
if (value is true)
{
_enabled[greetType].Add(guildId);
}
else
{
_enabled[greetType].TryRemove(guildId);
} }
return true; return true;
} }
public async Task<bool> SetGreetTypeMessage(ulong guildId, GreetType greetType, string message)
public async Task<bool> SetMessage(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)); message = GetDefaultGreet(greetType);
await using (var uow = _db.GetDbContext()) await using (var uow = _db.GetDbContext())
{ {
await uow.GetTable<GreetSettings>() await uow.GetTable<GreetSettings>()
.InsertOrUpdateAsync(() => new() .InsertOrUpdateAsync(() => new()
{ {
GuildId = guildId,
GreetType = greetType,
MessageText = message MessageText = message
}, },
x => new() x => new()
@ -502,11 +421,42 @@ public class GreetService : IEService, IReadyExecutor
}); });
} }
var conf = await GetGreetSettingsAsync(guildId, type); var conf = await GetGreetSettingsAsync(guildId, greetType);
return conf?.IsEnabled ?? false; return conf?.IsEnabled ?? false;
} }
public async Task<bool> SetDeleteTimer(ulong guildId, GreetType greetType, int timer)
{
if (timer < 0 || timer > 3600)
throw new ArgumentOutOfRangeException(nameof(timer));
await using (var uow = _db.GetDbContext())
{
await uow.GetTable<GreetSettings>()
.InsertOrUpdateAsync(() => new()
{
GuildId = guildId,
GreetType = greetType,
AutoDeleteTimer = timer,
},
x => new()
{
AutoDeleteTimer = timer
},
() => new()
{
GuildId = guildId,
GreetType = greetType
});
}
var conf = await GetGreetSettingsAsync(guildId, greetType);
return conf?.IsEnabled ?? false;
}
public async Task<bool> Test( public async Task<bool> Test(
ulong guildId, ulong guildId,
GreetType type, GreetType type,
@ -514,14 +464,24 @@ public class GreetService : IEService, IReadyExecutor
IGuildUser user) IGuildUser user)
{ {
var conf = await GetGreetSettingsAsync(guildId, type); var conf = await GetGreetSettingsAsync(guildId, type);
return SendMessage(conf, user, channel); if (conf is null)
return false;
await SendMessage(conf, channel, user);
return true;
} }
public async Task<bool> SendMessage(GreetSettings conf, IMessageChannel channel, IGuildUser user) public async Task<bool> SendMessage(GreetSettings conf, IMessageChannel channel, IGuildUser user)
{ {
if (conf.GreetType == GreetType.GreetDm) if (conf.GreetType == GreetType.GreetDm)
{ {
await GreetDmUser(conf, user); return await GreetDmUser(conf, user);
} }
if (channel is not ITextChannel ch)
return false;
await GreetUsers(conf, ch, user);
return true;
} }
} }

View file

@ -2,44 +2,43 @@ using EllieBot.Db.Models;
namespace EllieBot.Services; namespace EllieBot.Services;
public enum GreetType
{
Greet,
GreetDm,
Bye,
Boost,
}
public class GreetSettings public class GreetSettings
{ {
public int AutoDeleteGreetMessagesTimer { get; set; } public int Id { get; set; }
public int AutoDeleteByeMessagesTimer { get; set; }
public ulong GreetMessageChannelId { get; set; } public ulong GuildId { get; set; }
public ulong ByeMessageChannelId { get; set; } public GreetType GreetType { get; set; }
public string? MessageText { get; set; }
public bool IsEnabled { get; set; }
public ulong? ChannelId { get; set; }
public bool SendDmGreetMessage { get; set; } public int AutoDeleteTimer { get; set; }
public string? DmGreetMessageText { get; set; }
public bool SendChannelGreetMessage { get; set; } // public int AutoDeleteGreetMessagesTimer { get; set; }
public string? ChannelGreetMessageText { get; set; } // public int AutoDeleteByeMessagesTimer { get; set; }
//
public bool SendChannelByeMessage { get; set; } // public ulong GreetMessageChannelId { get; set; }
public string? ChannelByeMessageText { get; set; } // public ulong ByeMessageChannelId { get; set; }
//
public bool SendBoostMessage { get; set; } // public bool SendDmGreetMessage { get; set; }
public string? BoostMessage { get; set; } // public string? DmGreetMessageText { get; set; }
public int BoostMessageDeleteAfter { get; set; } //
public ulong BoostMessageChannelId { get; set; } // public bool SendChannelGreetMessage { get; set; }
// public string? ChannelGreetMessageText { get; set; }
public static GreetSettings Create(GuildConfig g) //
=> new() // public bool SendChannelByeMessage { get; set; }
{ // public string? ChannelByeMessageText { get; set; }
AutoDeleteByeMessagesTimer = g.AutoDeleteByeMessagesTimer, //
AutoDeleteGreetMessagesTimer = g.AutoDeleteGreetMessagesTimer, // public bool SendBoostMessage { get; set; }
GreetMessageChannelId = g.GreetMessageChannelId, // public string? BoostMessage { get; set; }
ByeMessageChannelId = g.ByeMessageChannelId, // public int BoostMessageDeleteAfter { get; set; }
SendDmGreetMessage = g.SendDmGreetMessage, // public ulong BoostMessageChannelId { get; set; }
DmGreetMessageText = g.DmGreetMessageText,
SendChannelGreetMessage = g.SendChannelGreetMessage,
ChannelGreetMessageText = g.ChannelGreetMessageText,
SendChannelByeMessage = g.SendChannelByeMessage,
ChannelByeMessageText = g.ChannelByeMessageText,
SendBoostMessage = g.SendBoostMessage,
BoostMessage = g.BoostMessage,
BoostMessageDeleteAfter = g.BoostMessageDeleteAfter,
BoostMessageChannelId = g.BoostMessageChannelId
};
} }

View file

@ -243,14 +243,16 @@ public class UserPunishService : IEService, IReadyExecutor
public async Task CheckAllWarnExpiresAsync() public async Task CheckAllWarnExpiresAsync()
{ {
await using var uow = _db.GetDbContext(); await using var uow = _db.GetDbContext();
var cleared = await uow.Set<Warning>()
.Where(x => uow.Set<GuildConfig>() var cleared = await uow.GetTable<Warning>()
.Any(y => y.GuildId == x.GuildId .Where(x => uow.GetTable<GuildConfig>()
&& y.WarnExpireHours > 0 .Count(y => y.GuildId == x.GuildId
&& y.WarnExpireAction == WarnExpireAction.Clear) && y.WarnExpireHours > 0
&& y.WarnExpireAction == WarnExpireAction.Clear)
> 0
&& x.Forgiven == false && x.Forgiven == false
&& x.DateAdded && x.DateAdded
< DateTime.UtcNow.AddHours(-uow.Set<GuildConfig>() < DateTime.UtcNow.AddHours(-uow.GetTable<GuildConfig>()
.Where(y => x.GuildId == y.GuildId) .Where(y => x.GuildId == y.GuildId)
.Select(y => y.WarnExpireHours) .Select(y => y.WarnExpireHours)
.First())) .First()))
@ -260,13 +262,14 @@ public class UserPunishService : IEService, IReadyExecutor
ForgivenBy = "expiry" ForgivenBy = "expiry"
}); });
var deleted = await uow.Set<Warning>() var deleted = await uow.GetTable<Warning>()
.Where(x => uow.Set<GuildConfig>() .Where(x => uow.GetTable<GuildConfig>()
.Any(y => y.GuildId == x.GuildId .Count(y => y.GuildId == x.GuildId
&& y.WarnExpireHours > 0 && y.WarnExpireHours > 0
&& y.WarnExpireAction == WarnExpireAction.Delete) && y.WarnExpireAction == WarnExpireAction.Delete)
> 0
&& x.DateAdded && x.DateAdded
< DateTime.UtcNow.AddHours(-uow.Set<GuildConfig>() < DateTime.UtcNow.AddHours(-uow.GetTable<GuildConfig>()
.Where(y => x.GuildId == y.GuildId) .Where(y => x.GuildId == y.GuildId)
.Select(y => y.WarnExpireHours) .Select(y => y.WarnExpireHours)
.First())) .First()))
@ -278,8 +281,6 @@ public class UserPunishService : IEService, IReadyExecutor
cleared, cleared,
deleted); deleted);
} }
await uow.SaveChangesAsync();
} }
public async Task CheckWarnExpiresAsync(ulong guildId) public async Task CheckWarnExpiresAsync(ulong guildId)

View file

@ -36,7 +36,7 @@ public static class EllieExpressionExtensions
var repCtx = new ReplacementContext(client: client, var repCtx = new ReplacementContext(client: client,
guild: (ctx.Channel as ITextChannel)?.Guild as SocketGuild, guild: (ctx.Channel as ITextChannel)?.Guild as SocketGuild,
channel: ctx.Channel, channel: ctx.Channel,
users: ctx.Author user: ctx.Author
) )
.WithOverride("%target%", .WithOverride("%target%",
() => canMentionEveryone () => canMentionEveryone

View file

@ -31,7 +31,7 @@ public class HelpService : IExecNoCommand, IEService
return; return;
} }
var repCtx = new ReplacementContext(guild: guild, channel: msg.Channel, users: msg.Author) var repCtx = new ReplacementContext(guild: guild, channel: msg.Channel, user: msg.Author)
.WithOverride("%prefix%", () => _bss.Data.Prefix) .WithOverride("%prefix%", () => _bss.Data.Prefix)
.WithOverride("%bot.prefix%", () => _bss.Data.Prefix); .WithOverride("%bot.prefix%", () => _bss.Data.Prefix);

View file

@ -262,7 +262,7 @@ public sealed class RepeaterService : IReadyExecutor, IEService
var repCtx = new ReplacementContext(client: _client, var repCtx = new ReplacementContext(client: _client,
guild: guild, guild: guild,
channel: channel, channel: channel,
users: guild.CurrentUser); user: guild.CurrentUser);
try try
{ {

View file

@ -1,6 +1,5 @@
#nullable disable #nullable disable
using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Primitives;
using EllieBot.Common.Yml; using EllieBot.Common.Yml;
namespace EllieBot.Services; namespace EllieBot.Services;
@ -22,7 +21,6 @@ public sealed class BotCredsProvider : IBotCredsProvider
private readonly object _reloadLock = new(); private readonly object _reloadLock = new();
private readonly IDisposable _changeToken;
public BotCredsProvider(int? totalShards = null, string credPath = null) public BotCredsProvider(int? totalShards = null, string credPath = null)
{ {
@ -49,18 +47,18 @@ public sealed class BotCredsProvider : IBotCredsProvider
// this can fail in docker containers // this can fail in docker containers
} }
MigrateCredentials();
if (!File.Exists(CredsPath))
{
Log.Warning(
"{CredsPath} is missing. Attempting to load creds from environment variables prefixed with 'EllieBot_'. Example is in {CredsExamplePath}",
CredsPath,
CredsExamplePath);
}
try try
{ {
MigrateCredentials();
if (!File.Exists(CredsPath))
{
Log.Warning(
"{CredsPath} is missing. Attempting to load creds from environment variables prefixed with 'EllieBot_'. Example is in {CredsExamplePath}",
CredsPath,
CredsExamplePath);
}
_config = new ConfigurationBuilder().AddYamlFile(CredsPath, false, true) _config = new ConfigurationBuilder().AddYamlFile(CredsPath, false, true)
.AddEnvironmentVariables("EllieBot_") .AddEnvironmentVariables("EllieBot_")
.Build(); .Build();
@ -70,7 +68,6 @@ public sealed class BotCredsProvider : IBotCredsProvider
Console.WriteLine(ex.ToString()); Console.WriteLine(ex.ToString());
} }
_changeToken = ChangeToken.OnChange(() => _config.GetReloadToken(), Reload);
Reload(); Reload();
} }
@ -141,6 +138,7 @@ public sealed class BotCredsProvider : IBotCredsProvider
{ {
creds.BotCache = BotCacheImplemenation.Redis; creds.BotCache = BotCacheImplemenation.Redis;
} }
if (creds.Version <= 6) if (creds.Version <= 6)
{ {
creds.Version = 7; creds.Version = 7;

View file

@ -72,6 +72,22 @@ public static class EnumerableExtensions
where TKey : notnull where TKey : notnull
=> new(dict); => new(dict);
/// <summary>
/// Initializes a new instance of the <see cref="ConcurrentDictionary{TKey,TValue}" /> class
/// that contains elements copied from the specified <see cref="IEnumerable{T}" />
/// has the default concurrency level, has the default initial capacity,
/// and uses the default comparer for the key type.
/// </summary>
/// <param name="dict">
/// The <see cref="IEnumerable{T}" /> whose elements are copied to the new
/// <see cref="ConcurrentDictionary{TKey,TValue}" />.
/// </param>
/// <returns>A new instance of the <see cref="ConcurrentDictionary{TKey,TValue}" /> class</returns>
public static ConcurrentHashSet<TValue> ToConcurrentSet<TValue>(
this IReadOnlyCollection<TValue> dict)
where TValue : notnull
=> new(dict);
public static IndexedCollection<T> ToIndexed<T>(this IEnumerable<T> enumerable) public static IndexedCollection<T> ToIndexed<T>(this IEnumerable<T> enumerable)
where T : class, IIndexed where T : class, IIndexed
=> new(enumerable); => new(enumerable);

View file

@ -84,19 +84,6 @@ public sealed partial class BotConfig : ICloneable<BotConfig>
[Comment("""Which string will be used to recognize the commands""")] [Comment("""Which string will be used to recognize the commands""")]
public string Prefix { get; set; } 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(""" [Comment("""
Whether the bot will rotate through all specified statuses. Whether the bot will rotate through all specified statuses.
This setting can be changed via .ropl command. This setting can be changed via .ropl command.
@ -144,7 +131,6 @@ public sealed partial class BotConfig : ICloneable<BotConfig>
Blocked = blocked; Blocked = blocked;
Prefix = "."; Prefix = ".";
RotateStatuses = false; RotateStatuses = false;
GroupGreets = false;
DmHelpTextKeywords = DmHelpTextKeywords =
[ [
"help", "help",

View file

@ -7,7 +7,7 @@ public sealed class ReplacementContext
public DiscordSocketClient? Client { get; } public DiscordSocketClient? Client { get; }
public IGuild? Guild { get; } public IGuild? Guild { get; }
public IMessageChannel? Channel { get; } public IMessageChannel? Channel { get; }
public IUser[]? Users { get; } public IUser? User { get; }
private readonly List<ReplacementInfo> _overrides = new(); private readonly List<ReplacementInfo> _overrides = new();
private readonly HashSet<string> _tokens = new(); private readonly HashSet<string> _tokens = new();
@ -21,10 +21,11 @@ public sealed class ReplacementContext
public IReadOnlyList<RegexReplacementInfo> RegexOverrides public IReadOnlyList<RegexReplacementInfo> RegexOverrides
=> _regexOverrides.AsReadOnly(); => _regexOverrides.AsReadOnly();
public ReplacementContext(ICommandContext cmdContext) : this(cmdContext.Client as DiscordSocketClient, public ReplacementContext(ICommandContext cmdContext)
cmdContext.Guild, : this(cmdContext.Client as DiscordSocketClient,
cmdContext.Channel, cmdContext.Guild,
cmdContext.User) cmdContext.Channel,
cmdContext.User)
{ {
} }
@ -32,12 +33,12 @@ public sealed class ReplacementContext
DiscordSocketClient? client = null, DiscordSocketClient? client = null,
IGuild? guild = null, IGuild? guild = null,
IMessageChannel? channel = null, IMessageChannel? channel = null,
params IUser[]? users) IUser? user = null)
{ {
Client = client; Client = client;
Guild = guild; Guild = guild;
Channel = channel; Channel = channel;
Users = users; User = user;
} }
public ReplacementContext WithOverride(string key, Func<ValueTask<string>> repFactory) public ReplacementContext WithOverride(string key, Func<ValueTask<string>> repFactory)

View file

@ -40,8 +40,8 @@ public sealed class ReplacementService : IReplacementService, IEService
if (repCtx.Guild is not null) if (repCtx.Guild is not null)
obj.Add(repCtx.Guild); obj.Add(repCtx.Guild);
if (repCtx.Users is not null) if (repCtx.User is not null)
obj.Add(repCtx.Users); obj.Add(repCtx.User);
if (repCtx.Channel is not null) if (repCtx.Channel is not null)
obj.Add(repCtx.Channel); obj.Add(repCtx.Channel);
@ -86,9 +86,9 @@ public sealed class ReplacementService : IReplacementService, IEService
objs.Add(repCtx.Channel); objs.Add(repCtx.Channel);
} }
if (repCtx.Users is not null) if (repCtx.User is not null)
{ {
objs.Add(repCtx.Users); objs.Add(repCtx.User);
} }
if (repCtx.Guild is not null) if (repCtx.Guild is not null)
@ -117,9 +117,9 @@ public sealed class ReplacementService : IReplacementService, IEService
objs.Add(repCtx.Channel); objs.Add(repCtx.Channel);
} }
if (repCtx.Users is not null) if (repCtx.User is not null)
{ {
objs.Add(repCtx.Users); objs.Add(repCtx.User);
} }
if (repCtx.Guild is not null) if (repCtx.Guild is not null)

View file

@ -1623,12 +1623,12 @@ gencurlist:
- page: - page:
desc: "The current page number for pagination." desc: "The current page number for pagination."
choose: choose:
desc: Chooses a thing from a list of things desc: Chooses a thing from a list of things. Seperate items with a semicolon ;
ex: ex:
- Get up;Sleep;Sleep more - Get up;Sleep;Sleep more
params: params:
- list: - list:
desc: "The type of items in the collection being searched." desc: "The items separated by ;"
rps: rps:
desc: |- desc: |-
Play a game of Rocket-Paperclip-Scissors with Ellie. Play a game of Rocket-Paperclip-Scissors with Ellie.