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

This commit is contained in:
Toastie 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/credentials.json.bak
src/EllieBot/data/EllieBot.db
build.ps1
build.sh
test.ps1
ellie-menu.ps1
package.sh
# 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
using DryIoc;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.DependencyInjection;
using EllieBot.Common.Configs;
@ -88,18 +89,18 @@ public sealed class Bot : IBot
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 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);
AllGuildConfigs = uow.Set<GuildConfig>().GetAllGuildConfigs(startingGuildIdList).ToImmutableArray();
}
// var svcs = new StandardKernel(new NinjectSettings()
@ -161,7 +162,8 @@ public sealed class Bot : IBot
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)
@ -265,7 +267,7 @@ public sealed class Bot : IBot
Log.Information("Shard {ShardId} loading services...", Client.ShardId);
try
{
AddServices();
await AddServices();
}
catch (Exception ex)
{
@ -273,7 +275,9 @@ public sealed class Bot : IBot
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>();
// start handling messages received in commandhandler
@ -338,26 +342,26 @@ public sealed class Bot : IBot
if (arg.Exception is { InnerException: WebSocketClosedException { CloseCode: 4014 } })
{
Log.Error("""
Login failed.
*** Please enable privileged intents ***
Certain Ellie features require Discord's privileged gateway intents.
These include greeting and goodbye messages, as well as creating the Owner message channels for DM forwarding.
How to enable privileged intents:
1. Head over to the Discord Developer Portal https://discord.com/developers/applications/
2. Select your Application.
3. Click on `Bot` in the left side navigation panel, and scroll down to the intents section.
4. Enable all intents.
5. Restart your bot.
Read this only if your bot is in 100 or more servers:
You'll need to apply to use the intents with Discord, but for small selfhosts, all that is required is enabling the intents in the developer portal.
Yes, this is a new thing from Discord, as of October 2020. No, there's nothing we can do about it. Yes, we're aware it worked before.
While waiting for your bot to be accepted, you can change the 'usePrivilegedIntents' inside your creds.yml to 'false', although this will break many of the ellie's features
""");
Login failed.
*** Please enable privileged intents ***
Certain Ellie features require Discord's privileged gateway intents.
These include greeting and goodbye messages, as well as creating the Owner message channels for DM forwarding.
How to enable privileged intents:
1. Head over to the Discord Developer Portal https://discord.com/developers/applications/
2. Select your Application.
3. Click on `Bot` in the left side navigation panel, and scroll down to the intents section.
4. Enable all intents.
5. Restart your bot.
Read this only if your bot is in 100 or more servers:
You'll need to apply to use the intents with Discord, but for small selfhosts, all that is required is enabling the intents in the developer portal.
Yes, this is a new thing from Discord, as of October 2020. No, there's nothing we can do about it. Yes, we're aware it worked before.
While waiting for your bot to be accepted, you can change the 'usePrivilegedIntents' inside your creds.yml to 'false', although this will break many of the ellie's features
""");
return Task.CompletedTask;
}

View file

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

View file

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

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)
{
migrationBuilder.AddColumn<decimal>(
name: "rolerequirement",
table: "shopentry",
type: "numeric(20,0)",
nullable: true);
migrationBuilder.CreateTable(
name: "autopublishchannel",
columns: table => new
@ -41,10 +35,6 @@ namespace EllieBot.Migrations.PostgreSql
{
migrationBuilder.DropTable(
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]
public partial class GreetCommands : EllieModule<GreetService>
{
[Cmd]
[RequireContext(ContextType.Guild)]
[UserPerm(GuildPerm.ManageGuild)]
public async Task Boost()
public async Task Toggle(GreetType type)
{
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)
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
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)]
[UserPerm(GuildPerm.ManageGuild)]
public async Task BoostDel(int timer = 30)
public async Task SetDel(GreetType type, int timer)
{
if (timer is < 0 or > 600)
return;
await _service.SetBoostDel(ctx.Guild.Id, timer);
await _service.SetDeleteTimer(ctx.Guild.Id, type, timer);
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
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)]
[UserPerm(GuildPerm.ManageGuild)]
public async Task BoostMsg([Leftover] string? text = null)
public async Task SetMsg(GreetType type, string? text = null)
{
if (string.IsNullOrWhiteSpace(text))
{
var boostMessage = _service.GetBoostMessage(ctx.Guild.Id);
await Response().Confirm(strs.boostmsg_cur(boostMessage?.SanitizeMentions())).SendAsync();
await _service.SetMessage(ctx.Guild.Id, type, null);
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;
}
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();
if (!sendBoostEnabled)
await Response().Pending(strs.boostmsg_enable($"`{prefix}boost`")).SendAsync();
}
await Response()
.Confirm(type switch
{
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 (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))
if (!isEnabled)
{
var greetMsg = _service.GetGreetMsg(ctx.Guild.Id);
await Response().Confirm(strs.greetmsg_cur(greetMsg?.SanitizeMentions())).SendAsync();
return;
var cmdName = type switch
{
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]
[RequireContext(ContextType.Guild)]
[UserPerm(GuildPerm.ManageGuild)]
public async Task GreetDm()
{
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();
}
public Task Boost()
=> Toggle(GreetType.Boost);
[Cmd]
[RequireContext(ContextType.Guild)]
[UserPerm(GuildPerm.ManageGuild)]
public async Task GreetDmMsg([Leftover] string? text = null)
{
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();
}
public Task BoostDel(int timer = 30)
=> SetDel(GreetType.Boost, timer);
[Cmd]
[RequireContext(ContextType.Guild)]
[UserPerm(GuildPerm.ManageGuild)]
public async Task Bye()
{
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();
}
public Task BoostMsg([Leftover] string? text = null)
=> SetMsg(GreetType.Boost, text);
[Cmd]
[RequireContext(ContextType.Guild)]
[UserPerm(GuildPerm.ManageGuild)]
public async Task ByeMsg([Leftover] string? text = null)
{
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();
}
public Task Greet()
=> Toggle(GreetType.Greet);
[Cmd]
[RequireContext(ContextType.Guild)]
[UserPerm(GuildPerm.ManageGuild)]
public async Task ByeDel(int timer = 30)
{
await _service.SetByeDel(ctx.Guild.Id, timer);
public Task GreetDel(int timer = 30)
=> SetDel(GreetType.Greet, timer);
if (timer > 0)
await Response().Confirm(strs.byedel_on(timer)).SendAsync();
else
await Response().Pending(strs.byedel_off).SendAsync();
}
[Cmd]
[RequireContext(ContextType.Guild)]
[UserPerm(GuildPerm.ManageGuild)]
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]
[RequireContext(ContextType.Guild)]
[UserPerm(GuildPerm.ManageGuild)]
[Ratelimit(5)]
public async Task ByeTest([Leftover] IGuildUser? user = null)
{
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();
}
public Task GreetTest([Leftover] IGuildUser? user = null)
=> Test(GreetType.Greet, user);
[Cmd]
[RequireContext(ContextType.Guild)]
[UserPerm(GuildPerm.ManageGuild)]
[Ratelimit(5)]
public async Task GreetTest([Leftover] IGuildUser? user = null)
{
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();
}
public Task GreetDmTest([Leftover] IGuildUser? user = null)
=> Test(GreetType.GreetDm, user);
[Cmd]
[RequireContext(ContextType.Guild)]
[UserPerm(GuildPerm.ManageGuild)]
[Ratelimit(5)]
public async Task GreetDmTest([Leftover] IGuildUser? user = null)
{
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();
}
public Task ByeTest([Leftover] IGuildUser? user = null)
=> Test(GreetType.Bye, user);
[Cmd]
[RequireContext(ContextType.Guild)]
[UserPerm(GuildPerm.ManageGuild)]
[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;
await _service.BoostTest((ITextChannel)ctx.Channel, user);
var enabled = _service.GetBoostEnabled(ctx.Guild.Id);
if (!enabled)
await _service.Test(ctx.Guild.Id, type, (ITextChannel)ctx.Channel, user);
var conf = await _service.GetGreetSettingsAsync(ctx.Guild.Id, type);
if (conf?.IsEnabled is not true)
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 bool GroupGreets
=> _bss.Data.GroupGreets;
private readonly DbService _db;
private ConcurrentHashSet<ulong> _greetDmEnabledGuilds = new();
private ConcurrentHashSet<ulong> _boostEnabledGuilds = new();
private ConcurrentHashSet<ulong> _greetEnabledGuilds = new();
private ConcurrentHashSet<ulong> _byeEnabledGuilds = new();
private ConcurrentDictionary<GreetType, ConcurrentHashSet<ulong>> _enabled = new();
private readonly DiscordSocketClient _client;
private readonly GreetGrouper<IGuildUser> _greets = new();
private readonly GreetGrouper<IUser> _byes = new();
private readonly BotConfigService _bss;
private readonly IReplacementService _repSvc;
private readonly IBotCache _cache;
private readonly IMessageSenderService _sender;
private readonly Channel<(GreetSettings, IUser, ITextChannel?)> _greetQueue =
Channel.CreateBounded<(GreetSettings, IUser, ITextChannel?)>(
new BoundedChannelOptions(60)
{
FullMode = BoundedChannelFullMode.DropOldest
});
public GreetService(
DiscordSocketClient client,
DbService db,
BotConfigService bss,
IMessageSenderService sender,
IReplacementService repSvc
IReplacementService repSvc,
IBotCache cache
)
{
_db = db;
_client = client;
_bss = bss;
_repSvc = repSvc;
_cache = cache;
_sender = sender;
}
@ -49,16 +51,12 @@ public class GreetService : IEService, IReadyExecutor
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)
.Where(x => x.IsEnabled)
.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));
_enabled = enabled.GroupBy(x => x.GreetType, v => v.GuildId)
.ToDictionary(x => x.Key, x => x.ToHashSet().ToConcurrentSet())
.ToConcurrent();
}
_client.UserJoined += OnUserJoined;
@ -71,9 +69,8 @@ public class GreetService : IEService, IReadyExecutor
var timer = new PeriodicTimer(TimeSpan.FromSeconds(2));
while (await timer.WaitForNextTickAsync())
{
var (conf, user, compl) = await _greetDmQueue.Reader.ReadAsync();
var res = await GreetDmUserInternal(conf, user);
compl.TrySetResult(res);
var (conf, user, ch) = await _greetQueue.Reader.ReadAsync();
await GreetUsers(conf, ch, user);
}
}
@ -88,62 +85,35 @@ public class GreetService : IEService, IReadyExecutor
{
_ = 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;
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;
}
private async Task TriggerBoostMessage(GreetSettings conf, SocketGuildUser user)
private async Task OnClientLeftGuild(SocketGuild guild)
{
var channel = user.Guild.GetTextChannel(conf.BoostMessageChannelId);
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
foreach (var gt in Enum.GetValues<GreetType>())
{
var newContent = await _repSvc.ReplaceAsync(toSend,
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;
_enabled[gt].TryRemove(guild.Id);
}
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 uow.GetTable<GreetSettings>()
.Where(x => x.GuildId == arg.Id)
.Where(x => x.GuildId == guild.Id)
.DeleteAsync();
}
@ -153,38 +123,20 @@ public class GreetService : IEService, IReadyExecutor
{
try
{
var conf = await GetGreetSettingsAsync(guild.Id);
var conf = await GetGreetSettingsAsync(guild.Id, GreetType.Bye);
if (conf is null)
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
{
await SetGreet(guild.Id, null, GreetType.Bye, false);
return;
}
if (GroupGreets)
{
// 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 });
}
await _greetQueue.Writer.WriteAsync((conf, user, channel));
}
catch
{
@ -194,7 +146,14 @@ public class GreetService : IEService, IReadyExecutor
return Task.CompletedTask;
}
private readonly TypedKey<GreetSettings?> _greetSettingsKey = new();
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();
var res = await uow.GetTable<GreetSettings>()
@ -204,62 +163,32 @@ public class GreetService : IEService, IReadyExecutor
return res;
}
private Task ByeUsers(GreetSettings conf, ITextChannel channel, IUser user)
=> ByeUsers(conf, channel, new[] { user });
private async Task ByeUsers(GreetSettings conf, ITextChannel channel, IReadOnlyCollection<IUser> users)
private async Task GreetUsers(GreetSettings conf, ITextChannel? channel, IUser user)
{
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;
var repCtx = new ReplacementContext(client: _client,
guild: channel.Guild,
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);
try
{
var toDelete = await _sender.Response(channel).Text(text).Sanitize(false).SendAsync();
if (conf.AutoDeleteByeMessagesTimer > 0)
toDelete.DeleteAfter(conf.AutoDeleteByeMessagesTimer);
}
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);
if (conf.AutoDeleteTimer > 0)
toDelete.DeleteAfter(conf.AutoDeleteTimer);
}
catch (HttpException ex) when (ex.DiscordCode is DiscordErrorCode.InsufficientPermissions
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)
{
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;
}
@ -296,8 +217,8 @@ public class GreetService : IEService, IReadyExecutor
{
try
{
var repCtx = new ReplacementContext(client: _client, guild: user.Guild, users: user);
var smartText = SmartText.CreateFrom(conf.DmGreetMessageText);
var repCtx = new ReplacementContext(client: _client, guild: user.Guild, user: user);
var smartText = SmartText.CreateFrom(conf.MessageText);
smartText = await _repSvc.ReplaceAsync(smartText, repCtx);
if (smartText is SmartPlainText pt)
@ -378,40 +299,21 @@ public class GreetService : IEService, IReadyExecutor
{
try
{
var conf = await GetGreetSettingsAsync(user.GuildId);
if (conf is null)
return;
var conf = await GetGreetSettingsAsync(user.GuildId, GreetType.Greet);
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 (GroupGreets)
{
// 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 });
await _greetQueue.Writer.WriteAsync((conf, user, channel));
}
}
if (conf.SendDmGreetMessage)
await GreetDmUser(conf, user);
var confDm = await GetGreetSettingsAsync(user.GuildId, GreetType.GreetDm);
if (confDm?.IsEnabled ?? false)
await GreetDmUser(confDm, user);
}
catch
{
@ -421,20 +323,16 @@ public class GreetService : IEService, IReadyExecutor
return Task.CompletedTask;
}
// public GreetSettings GetOrAddSettingsForGuild(ulong guildId)
// {
// if (_greetDmEnabledGuilds.TryGetValue(guildId, out var settings))
// return settings;
//
// using (var uow = _db.GetDbContext())
// {
// var gc = uow.GuildConfigsForId(guildId, set => set);
// settings = GreetSettings.Create(gc);
// }
//
// _greetDmEnabledGuilds.TryAdd(guildId, settings);
// return settings;
// }
private static string GetDefaultGreet(GreetType greetType)
=> greetType switch
{
GreetType.Boost => "%user.name% has boosted the server!",
GreetType.Greet => "%user.name% has joined the server!",
GreetType.Bye => "%user.name has left the server!",
GreetType.GreetDm => "Welcome to the server %user.name%",
_ => "%user.name% did something new!"
};
public async Task<bool> SetGreet(
ulong guildId,
@ -447,9 +345,14 @@ public class GreetService : IEService, IReadyExecutor
if (value is { } v)
{
var defaultGreet = GetDefaultGreet(greetType);
await q
.InsertOrUpdateAsync(() => new()
{
GuildId = guildId,
GreetType = greetType,
MessageText = defaultGreet,
IsEnabled = v,
ChannelId = channelId,
},
@ -466,29 +369,45 @@ public class GreetService : IEService, IReadyExecutor
}
else
{
await q
.Where(x => x.GuildId == guildId && x.GreetType == greetType)
.UpdateAsync((old) => new()
{
IsEnabled = !old.IsEnabled
});
var result = await q
.Where(x => x.GuildId == guildId && x.GreetType == greetType)
.UpdateWithOutputAsync((old) => new()
{
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;
}
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))
throw new ArgumentNullException(nameof(message));
message = GetDefaultGreet(greetType);
await using (var uow = _db.GetDbContext())
{
await uow.GetTable<GreetSettings>()
.InsertOrUpdateAsync(() => new()
{
GuildId = guildId,
GreetType = greetType,
MessageText = message
},
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;
}
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(
ulong guildId,
GreetType type,
@ -514,14 +464,24 @@ public class GreetService : IEService, IReadyExecutor
IGuildUser user)
{
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)
{
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;
public enum GreetType
{
Greet,
GreetDm,
Bye,
Boost,
}
public class GreetSettings
{
public int AutoDeleteGreetMessagesTimer { get; set; }
public int AutoDeleteByeMessagesTimer { get; set; }
public int Id { get; set; }
public ulong GreetMessageChannelId { get; set; }
public ulong ByeMessageChannelId { get; set; }
public ulong GuildId { 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 string? DmGreetMessageText { get; set; }
public int AutoDeleteTimer { get; set; }
public bool SendChannelGreetMessage { get; set; }
public string? ChannelGreetMessageText { get; set; }
public bool SendChannelByeMessage { get; set; }
public string? ChannelByeMessageText { get; set; }
public bool SendBoostMessage { get; set; }
public string? BoostMessage { get; set; }
public int BoostMessageDeleteAfter { get; set; }
public ulong BoostMessageChannelId { get; set; }
public static GreetSettings Create(GuildConfig g)
=> new()
{
AutoDeleteByeMessagesTimer = g.AutoDeleteByeMessagesTimer,
AutoDeleteGreetMessagesTimer = g.AutoDeleteGreetMessagesTimer,
GreetMessageChannelId = g.GreetMessageChannelId,
ByeMessageChannelId = g.ByeMessageChannelId,
SendDmGreetMessage = g.SendDmGreetMessage,
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
};
// public int AutoDeleteGreetMessagesTimer { get; set; }
// public int AutoDeleteByeMessagesTimer { get; set; }
//
// public ulong GreetMessageChannelId { get; set; }
// public ulong ByeMessageChannelId { get; set; }
//
// public bool SendDmGreetMessage { get; set; }
// public string? DmGreetMessageText { get; set; }
//
// public bool SendChannelGreetMessage { get; set; }
// public string? ChannelGreetMessageText { get; set; }
//
// public bool SendChannelByeMessage { get; set; }
// public string? ChannelByeMessageText { get; set; }
//
// public bool SendBoostMessage { get; set; }
// public string? BoostMessage { get; set; }
// public int BoostMessageDeleteAfter { get; set; }
// public ulong BoostMessageChannelId { get; set; }
}

View file

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

View file

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

View file

@ -31,7 +31,7 @@ public class HelpService : IExecNoCommand, IEService
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("%bot.prefix%", () => _bss.Data.Prefix);

View file

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

View file

@ -1,6 +1,5 @@
#nullable disable
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Primitives;
using EllieBot.Common.Yml;
namespace EllieBot.Services;
@ -22,7 +21,6 @@ public sealed class BotCredsProvider : IBotCredsProvider
private readonly object _reloadLock = new();
private readonly IDisposable _changeToken;
public BotCredsProvider(int? totalShards = null, string credPath = null)
{
@ -49,18 +47,18 @@ public sealed class BotCredsProvider : IBotCredsProvider
// 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
{
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)
.AddEnvironmentVariables("EllieBot_")
.Build();
@ -70,7 +68,6 @@ public sealed class BotCredsProvider : IBotCredsProvider
Console.WriteLine(ex.ToString());
}
_changeToken = ChangeToken.OnChange(() => _config.GetReloadToken(), Reload);
Reload();
}
@ -141,6 +138,7 @@ public sealed class BotCredsProvider : IBotCredsProvider
{
creds.BotCache = BotCacheImplemenation.Redis;
}
if (creds.Version <= 6)
{
creds.Version = 7;

View file

@ -72,6 +72,22 @@ public static class EnumerableExtensions
where TKey : notnull
=> 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)
where T : class, IIndexed
=> 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""")]
public string Prefix { get; set; }
[Comment("""
Toggles whether your bot will group greet/bye messages into a single message every 5 seconds.
1st user who joins will get greeted immediately
If more users join within the next 5 seconds, they will be greeted in groups of 5.
This will cause %user.mention% and other placeholders to be replaced with multiple users.
Keep in mind this might break some of your embeds - for example if you have %user.avatar% in the thumbnail,
it will become invalid, as it will resolve to a list of avatars of grouped users.
note: This setting is primarily used if you're afraid of raids, or you're running medium/large bots where some
servers might get hundreds of people join at once. This is used to prevent the bot from getting ratelimited,
and (slightly) reduce the greet spam in those servers.
""")]
public bool GroupGreets { get; set; }
[Comment("""
Whether the bot will rotate through all specified statuses.
This setting can be changed via .ropl command.
@ -144,7 +131,6 @@ public sealed partial class BotConfig : ICloneable<BotConfig>
Blocked = blocked;
Prefix = ".";
RotateStatuses = false;
GroupGreets = false;
DmHelpTextKeywords =
[
"help",

View file

@ -7,7 +7,7 @@ public sealed class ReplacementContext
public DiscordSocketClient? Client { get; }
public IGuild? Guild { get; }
public IMessageChannel? Channel { get; }
public IUser[]? Users { get; }
public IUser? User { get; }
private readonly List<ReplacementInfo> _overrides = new();
private readonly HashSet<string> _tokens = new();
@ -21,10 +21,11 @@ public sealed class ReplacementContext
public IReadOnlyList<RegexReplacementInfo> RegexOverrides
=> _regexOverrides.AsReadOnly();
public ReplacementContext(ICommandContext cmdContext) : this(cmdContext.Client as DiscordSocketClient,
cmdContext.Guild,
cmdContext.Channel,
cmdContext.User)
public ReplacementContext(ICommandContext cmdContext)
: this(cmdContext.Client as DiscordSocketClient,
cmdContext.Guild,
cmdContext.Channel,
cmdContext.User)
{
}
@ -32,12 +33,12 @@ public sealed class ReplacementContext
DiscordSocketClient? client = null,
IGuild? guild = null,
IMessageChannel? channel = null,
params IUser[]? users)
IUser? user = null)
{
Client = client;
Guild = guild;
Channel = channel;
Users = users;
User = user;
}
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)
obj.Add(repCtx.Guild);
if (repCtx.Users is not null)
obj.Add(repCtx.Users);
if (repCtx.User is not null)
obj.Add(repCtx.User);
if (repCtx.Channel is not null)
obj.Add(repCtx.Channel);
@ -86,9 +86,9 @@ public sealed class ReplacementService : IReplacementService, IEService
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)
@ -117,9 +117,9 @@ public sealed class ReplacementService : IReplacementService, IEService
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)

View file

@ -1623,12 +1623,12 @@ gencurlist:
- page:
desc: "The current page number for pagination."
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:
- Get up;Sleep;Sleep more
params:
- list:
desc: "The type of items in the collection being searched."
desc: "The items separated by ;"
rps:
desc: |-
Play a game of Rocket-Paperclip-Scissors with Ellie.