.notify now lets you not specify a channel in which case the event message will be sent to the channel from which the event originated - but only if that event has an origin channel.

This commit is contained in:
Toastie 2025-03-01 19:46:50 +13:00
parent 6f64a15cd4
commit c5442f9144
Signed by: toastie_t0ast
GPG key ID: 0861BE54AD481DC7
19 changed files with 261 additions and 150 deletions

View file

@ -8,7 +8,7 @@ public class Notify
public int Id { get; set; } public int Id { get; set; }
public ulong GuildId { get; set; } public ulong GuildId { get; set; }
public ulong ChannelId { get; set; } public ulong? ChannelId { get; set; }
public NotifyType Type { get; set; } public NotifyType Type { get; set; }
[MaxLength(10_000)] [MaxLength(10_000)]

View file

@ -0,0 +1,7 @@
START TRANSACTION;
ALTER TABLE notify ALTER COLUMN channelid DROP NOT NULL;
INSERT INTO "__EFMigrationsHistory" (migrationid, productversion)
VALUES ('20250228044141_notify-allow-origin-channel', '9.0.1');
COMMIT;

View file

@ -12,7 +12,7 @@ using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
namespace EllieBot.Migrations.PostgreSql namespace EllieBot.Migrations.PostgreSql
{ {
[DbContext(typeof(PostgreSqlContext))] [DbContext(typeof(PostgreSqlContext))]
[Migration("20250226215222_init")] [Migration("20250228044209_init")]
partial class init partial class init
{ {
/// <inheritdoc /> /// <inheritdoc />
@ -1809,7 +1809,7 @@ namespace EllieBot.Migrations.PostgreSql
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id")); NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<decimal>("ChannelId") b.Property<decimal?>("ChannelId")
.HasColumnType("numeric(20,0)") .HasColumnType("numeric(20,0)")
.HasColumnName("channelid"); .HasColumnName("channelid");

View file

@ -658,7 +658,7 @@ namespace EllieBot.Migrations.PostgreSql
id = table.Column<int>(type: "integer", nullable: false) id = table.Column<int>(type: "integer", nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
guildid = table.Column<decimal>(type: "numeric(20,0)", nullable: false), guildid = table.Column<decimal>(type: "numeric(20,0)", nullable: false),
channelid = table.Column<decimal>(type: "numeric(20,0)", nullable: false), channelid = table.Column<decimal>(type: "numeric(20,0)", nullable: true),
type = table.Column<int>(type: "integer", nullable: false), type = table.Column<int>(type: "integer", nullable: false),
message = table.Column<string>(type: "character varying(10000)", maxLength: 10000, nullable: false) message = table.Column<string>(type: "character varying(10000)", maxLength: 10000, nullable: false)
}, },

View file

@ -1806,7 +1806,7 @@ namespace EllieBot.Migrations.PostgreSql
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id")); NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<decimal>("ChannelId") b.Property<decimal?>("ChannelId")
.HasColumnType("numeric(20,0)") .HasColumnType("numeric(20,0)")
.HasColumnName("channelid"); .HasColumnName("channelid");

View file

@ -0,0 +1,29 @@
BEGIN TRANSACTION;
CREATE TABLE "ef_temp_Notify" (
"Id" INTEGER NOT NULL CONSTRAINT "PK_Notify" PRIMARY KEY AUTOINCREMENT,
"ChannelId" INTEGER NULL,
"GuildId" INTEGER NOT NULL,
"Message" TEXT NOT NULL,
"Type" INTEGER NOT NULL,
CONSTRAINT "AK_Notify_GuildId_Type" UNIQUE ("GuildId", "Type")
);
INSERT INTO "ef_temp_Notify" ("Id", "ChannelId", "GuildId", "Message", "Type")
SELECT "Id", "ChannelId", "GuildId", "Message", "Type"
FROM "Notify";
COMMIT;
PRAGMA foreign_keys = 0;
BEGIN TRANSACTION;
DROP TABLE "Notify";
ALTER TABLE "ef_temp_Notify" RENAME TO "Notify";
COMMIT;
PRAGMA foreign_keys = 1;
INSERT INTO "__EFMigrationsHistory" ("MigrationId", "ProductVersion")
VALUES ('20250228044138_notify-allow-origin-channel', '9.0.1');

View file

@ -11,7 +11,7 @@ using EllieBot.Db;
namespace EllieBot.Migrations.Sqlite namespace EllieBot.Migrations.Sqlite
{ {
[DbContext(typeof(SqliteContext))] [DbContext(typeof(SqliteContext))]
[Migration("20250226215220_init")] [Migration("20250228044206_init")]
partial class init partial class init
{ {
/// <inheritdoc /> /// <inheritdoc />
@ -1351,7 +1351,7 @@ namespace EllieBot.Migrations.Sqlite
.ValueGeneratedOnAdd() .ValueGeneratedOnAdd()
.HasColumnType("INTEGER"); .HasColumnType("INTEGER");
b.Property<ulong>("ChannelId") b.Property<ulong?>("ChannelId")
.HasColumnType("INTEGER"); .HasColumnType("INTEGER");
b.Property<ulong>("GuildId") b.Property<ulong>("GuildId")

View file

@ -658,7 +658,7 @@ namespace EllieBot.Migrations.Sqlite
Id = table.Column<int>(type: "INTEGER", nullable: false) Id = table.Column<int>(type: "INTEGER", nullable: false)
.Annotation("Sqlite:Autoincrement", true), .Annotation("Sqlite:Autoincrement", true),
GuildId = table.Column<ulong>(type: "INTEGER", nullable: false), GuildId = table.Column<ulong>(type: "INTEGER", nullable: false),
ChannelId = table.Column<ulong>(type: "INTEGER", nullable: false), ChannelId = table.Column<ulong>(type: "INTEGER", nullable: true),
Type = table.Column<int>(type: "INTEGER", nullable: false), Type = table.Column<int>(type: "INTEGER", nullable: false),
Message = table.Column<string>(type: "TEXT", maxLength: 10000, nullable: false) Message = table.Column<string>(type: "TEXT", maxLength: 10000, nullable: false)
}, },

View file

@ -1348,7 +1348,7 @@ namespace EllieBot.Migrations.Sqlite
.ValueGeneratedOnAdd() .ValueGeneratedOnAdd()
.HasColumnType("INTEGER"); .HasColumnType("INTEGER");
b.Property<ulong>("ChannelId") b.Property<ulong?>("ChannelId")
.HasColumnType("INTEGER"); .HasColumnType("INTEGER");
b.Property<ulong>("GuildId") b.Property<ulong>("GuildId")

View file

@ -3,15 +3,25 @@
namespace EllieBot.Modules.Administration; namespace EllieBot.Modules.Administration;
public interface INotifyModel<T> public interface INotifyModel<T>
where T: struct, INotifyModel<T> where T : struct, INotifyModel<T>
{ {
static abstract string KeyName { get; } static abstract string KeyName { get; }
static abstract NotifyType NotifyType { get; } static abstract NotifyType NotifyType { get; }
static abstract IReadOnlyList<NotifyModelPlaceholderData<T>> GetReplacements(); static abstract IReadOnlyList<NotifyModelPlaceholderData<T>> GetReplacements();
static virtual bool SupportsOriginTarget
=> false;
public virtual bool TryGetGuildId(out ulong guildId) public virtual bool TryGetGuildId(out ulong guildId)
{ {
guildId = 0; guildId = 0;
return false;
}
public virtual bool TryGetChannelId(out ulong channelId)
{
channelId = 0;
return false; return false;
} }

View file

@ -13,4 +13,7 @@ public interface INotifySubscriber
NotifyModelData GetRegisteredModel(NotifyType nType); NotifyModelData GetRegisteredModel(NotifyType nType);
} }
public readonly record struct NotifyModelData(NotifyType Type, IReadOnlyList<string> Replacements); public readonly record struct NotifyModelData(
NotifyType Type,
bool SupportsOriginTarget,
IReadOnlyList<string> Replacements);

View file

@ -3,7 +3,12 @@ using EllieBot.Modules.Administration;
namespace EllieBot.Modules.Xp.Services; namespace EllieBot.Modules.Xp.Services;
public record struct AddRoleRewardNotifyModel(ulong GuildId, ulong RoleId, ulong UserId, long Level) : INotifyModel<AddRoleRewardNotifyModel> public record struct AddRoleRewardNotifyModel(
ulong GuildId,
ulong RoleId,
ulong UserId,
long Level)
: INotifyModel<AddRoleRewardNotifyModel>
{ {
public static string KeyName public static string KeyName
=> "notify.reward.addrole"; => "notify.reward.addrole";
@ -18,9 +23,9 @@ public record struct AddRoleRewardNotifyModel(ulong GuildId, ulong RoleId, ulong
public static IReadOnlyList<NotifyModelPlaceholderData<AddRoleRewardNotifyModel>> GetReplacements() public static IReadOnlyList<NotifyModelPlaceholderData<AddRoleRewardNotifyModel>> GetReplacements()
=> =>
[ [
new(PH_LEVEL, static (data, g) => data.Level.ToString() ), new(PH_LEVEL, static (data, g) => data.Level.ToString()),
new(PH_USER, static (data, g) => g.GetUser(data.UserId)?.ToString() ?? data.UserId.ToString() ), new(PH_USER, static (data, g) => g.GetUser(data.UserId)?.ToString() ?? data.UserId.ToString()),
new(PH_ROLE, static (data, g) => g.GetRole(data.RoleId)?.ToString() ?? data.RoleId.ToString() ) new(PH_ROLE, static (data, g) => g.GetRole(data.RoleId)?.ToString() ?? data.RoleId.ToString())
]; ];
public bool TryGetUserId(out ulong userId) public bool TryGetUserId(out ulong userId)

View file

@ -4,7 +4,7 @@ namespace EllieBot.Modules.Administration;
public readonly record struct LevelUpNotifyModel( public readonly record struct LevelUpNotifyModel(
ulong GuildId, ulong GuildId,
ulong ChannelId, ulong? ChannelId,
ulong UserId, ulong UserId,
long Level) : INotifyModel<LevelUpNotifyModel> long Level) : INotifyModel<LevelUpNotifyModel>
{ {
@ -21,17 +21,32 @@ public readonly record struct LevelUpNotifyModel(
{ {
return return
[ [
new(PH_LEVEL, static (data, g) => data.Level.ToString() ), new(PH_LEVEL, static (data, g) => data.Level.ToString()),
new(PH_USER, static (data, g) => g.GetUser(data.UserId)?.ToString() ?? data.UserId.ToString() ) new(PH_USER, static (data, g) => g.GetUser(data.UserId)?.ToString() ?? data.UserId.ToString())
]; ];
} }
public static bool SupportsOriginTarget
=> true;
public readonly bool TryGetGuildId(out ulong guildId) public readonly bool TryGetGuildId(out ulong guildId)
{ {
guildId = GuildId; guildId = GuildId;
return true; return true;
} }
public readonly bool TryGetChannelId(out ulong channelId)
{
if (ChannelId is ulong cid)
{
channelId = cid;
return true;
}
channelId = 0;
return false;
}
public readonly bool TryGetUserId(out ulong userId) public readonly bool TryGetUserId(out ulong userId)
{ {
userId = UserId; userId = UserId;

View file

@ -12,23 +12,23 @@ public partial class Administration
public async Task Notify() public async Task Notify()
{ {
await Response() await Response()
.Paginated() .Paginated()
.Items(Enum.GetValues<NotifyType>().DistinctBy(x => (int)x).ToList()) .Items(Enum.GetValues<NotifyType>().DistinctBy(x => (int)x).ToList())
.PageSize(5) .PageSize(5)
.Page((items, page) => .Page((items, page) =>
{ {
var eb = CreateEmbed() var eb = CreateEmbed()
.WithOkColor() .WithOkColor()
.WithTitle(GetText(strs.notify_available)); .WithTitle(GetText(strs.notify_available));
foreach (var item in items) foreach (var item in items)
{ {
eb.AddField(item.ToString(), GetText(GetDescription(item)), false); eb.AddField(item.ToString(), GetText(GetDescription(item)), false);
} }
return eb; return eb;
}) })
.SendAsync(); .SendAsync();
} }
private LocStr GetDescription(NotifyType item) private LocStr GetDescription(NotifyType item)
@ -43,36 +43,56 @@ public partial class Administration
[Cmd] [Cmd]
[UserPerm(GuildPerm.Administrator)] [UserPerm(GuildPerm.Administrator)]
public async Task Notify(NotifyType nType, [Leftover] string? message = null) public async Task Notify(NotifyType nType)
{ {
if (string.IsNullOrWhiteSpace(message)) // show msg
var conf = await _service.GetNotifyAsync(ctx.Guild.Id, nType);
if (conf is null)
{ {
// show msg await Response().Confirm(strs.notify_msg_not_set).SendAsync();
var conf = await _service.GetNotifyAsync(ctx.Guild.Id, nType);
if (conf is null)
{
await Response().Confirm(strs.notify_msg_not_set).SendAsync();
return;
}
var eb = CreateEmbed()
.WithOkColor()
.WithTitle(GetText(strs.notify_msg))
.WithDescription(conf.Message.TrimTo(2048))
.AddField(GetText(strs.notify_type), conf.Type.ToString(), true)
.AddField(GetText(strs.channel),
$"""
<#{conf.ChannelId}>
`{conf.ChannelId}`
""",
true);
await Response().Embed(eb).SendAsync();
return; return;
} }
await _service.EnableAsync(ctx.Guild.Id, ctx.Channel.Id, nType, message); var outChannel = conf.ChannelId is null
await Response().Confirm(strs.notify_on($"<#{ctx.Channel.Id}>", Format.Bold(nType.ToString()))).SendAsync(); ? """
from which the event originated
`origin`
"""
: $"""
<#{conf.ChannelId}>
`{conf.ChannelId}`
""";
var eb = CreateEmbed()
.WithOkColor()
.WithTitle(GetText(strs.notify_msg))
.WithDescription(conf.Message.TrimTo(2048))
.AddField(GetText(strs.notify_type), conf.Type.ToString(), true)
.AddField(GetText(strs.channel),
outChannel,
true);
await Response().Embed(eb).SendAsync();
return;
}
[Cmd]
[UserPerm(GuildPerm.Administrator)]
public async Task Notify(NotifyType nType, [Leftover] string message)
=> await NotifyInternalAsync(nType, null, message);
[Cmd]
[UserPerm(GuildPerm.Administrator)]
public async Task Notify(NotifyType nType, IMessageChannel channel, [Leftover] string message)
=> await NotifyInternalAsync(nType, channel, message);
private async Task NotifyInternalAsync(NotifyType nType, IMessageChannel? channel, [Leftover] string message)
{
var result = await _service.EnableAsync(ctx.Guild.Id, channel?.Id, nType, message);
var outChannel = channel is null ? "origin" : $"<#{channel.Id}>";
await Response()
.Confirm(strs.notify_on(outChannel, Format.Bold(nType.ToString())))
.SendAsync();
} }
[Cmd] [Cmd]
@ -82,13 +102,12 @@ public partial class Administration
var data = _service.GetRegisteredModel(nType); var data = _service.GetRegisteredModel(nType);
var eb = CreateEmbed() var eb = CreateEmbed()
.WithOkColor() .WithOkColor()
.WithTitle(GetText(strs.notify_placeholders(nType.ToString().ToLower()))); .WithTitle(GetText(strs.notify_placeholders(nType.ToString().ToLower())));
eb.WithDescription(data.Replacements.Join("\n---\n", x => $"`%event.{x}%`")); eb.WithDescription(data.Replacements.Join("\n---\n", x => $"`%event.{x}%`"));
await Response().Embed(eb).SendAsync(); await Response().Embed(eb).SendAsync();
} }
[Cmd] [Cmd]
@ -115,8 +134,8 @@ public partial class Administration
sb.AppendLine(GetText(strs.notify_none)); sb.AppendLine(GetText(strs.notify_none));
await Response() await Response()
.Confirm(GetText(strs.notify_list), text: sb.ToString()) .Confirm(GetText(strs.notify_list), text: sb.ToString())
.SendAsync(); .SendAsync();
} }
[Cmd] [Cmd]

View file

@ -16,6 +16,7 @@ public sealed class NotifyService : IReadyExecutor, INotifySubscriber, IEService
private readonly IBotCreds _creds; private readonly IBotCreds _creds;
private readonly IReplacementService _repSvc; private readonly IReplacementService _repSvc;
private readonly IPubSub _pubSub; private readonly IPubSub _pubSub;
private ConcurrentDictionary<NotifyType, ConcurrentDictionary<ulong, Notify>> _events = new(); private ConcurrentDictionary<NotifyType, ConcurrentDictionary<ulong, Notify>> _events = new();
public NotifyService( public NotifyService(
@ -36,12 +37,10 @@ public sealed class NotifyService : IReadyExecutor, INotifySubscriber, IEService
private void RegisterModels() private void RegisterModels()
{ {
RegisterModel<LevelUpNotifyModel>(); RegisterModel<LevelUpNotifyModel>();
RegisterModel<ProtectionNotifyModel>(); RegisterModel<ProtectionNotifyModel>();
RegisterModel<AddRoleRewardNotifyModel>(); RegisterModel<AddRoleRewardNotifyModel>();
RegisterModel<RemoveRoleRewardNotifyModel>(); RegisterModel<RemoveRoleRewardNotifyModel>();
} }
public async Task OnReadyAsync() public async Task OnReadyAsync()
@ -84,7 +83,7 @@ public sealed class NotifyService : IReadyExecutor, INotifySubscriber, IEService
catch (Exception ex) catch (Exception ex)
{ {
Log.Warning(ex, Log.Warning(ex,
"Unknown error occurred while trying to triger {NotifyEvent} for {NotifyModel}", "Unknown error occurred while trying to trigger {NotifyEvent} for {NotifyModel}",
T.KeyName, T.KeyName,
data); data);
} }
@ -93,36 +92,39 @@ public sealed class NotifyService : IReadyExecutor, INotifySubscriber, IEService
private async Task OnEvent<T>(T model) private async Task OnEvent<T>(T model)
where T : struct, INotifyModel<T> where T : struct, INotifyModel<T>
{ {
if (_events.TryGetValue(T.NotifyType, out var subs)) if (!_events.TryGetValue(T.NotifyType, out var subs))
return;
// make sure the event is consumed
// only in the guild it was meant for
if (model.TryGetGuildId(out var gid))
{ {
if (model.TryGetGuildId(out var gid)) if (!subs.TryGetValue(gid, out var conf))
{
if (!subs.TryGetValue(gid, out var conf))
return;
await HandleNotifyEvent(conf, model);
return; return;
}
foreach (var key in subs.Keys.ToArray()) await HandleNotifyEvent(conf, model);
return;
}
// todo optimize this
foreach (var key in subs.Keys)
{
if (subs.TryGetValue(key, out var notif))
{ {
if (subs.TryGetValue(key, out var notif)) try
{ {
try await HandleNotifyEvent(notif, model);
{
await HandleNotifyEvent(notif, model);
}
catch (Exception ex)
{
Log.Error(ex,
"Error occured while sending notification {NotifyEvent} to guild {GuildId}: {ErrorMessage}",
T.NotifyType,
key,
ex.Message);
}
await Task.Delay(500);
} }
catch (Exception ex)
{
Log.Error(ex,
"Error occured while sending notification {NotifyEvent} to guild {GuildId}: {ErrorMessage}",
T.NotifyType,
key,
ex.Message);
}
await Task.Delay(500);
} }
} }
} }
@ -131,9 +133,27 @@ public sealed class NotifyService : IReadyExecutor, INotifySubscriber, IEService
where T : struct, INotifyModel<T> where T : struct, INotifyModel<T>
{ {
var guild = _client.GetGuild(conf.GuildId); var guild = _client.GetGuild(conf.GuildId);
var channel = guild?.GetTextChannel(conf.ChannelId);
if (guild is null || channel is null) // bot probably left the guild, cleanup?
if (guild is null)
return;
IMessageChannel? channel;
// if notify channel is specified for this event, send the event to that channel
if (conf.ChannelId is ulong confCid)
{
channel = guild.GetTextChannel(confCid);
}
else
{
// otherwise get the origin channel of the event
if (!model.TryGetChannelId(out var cid))
return;
channel = guild.GetChannel(cid) as IMessageChannel;
}
if (channel is null)
return; return;
IUser? user = null; IUser? user = null;
@ -165,14 +185,24 @@ public sealed class NotifyService : IReadyExecutor, INotifySubscriber, IEService
.SendAsync(); .SendAsync();
} }
private static string GetPhToken(string name) => $"%event.{name}%"; private static string GetPhToken(string name)
=> $"%event.{name}%";
public async Task EnableAsync( public async Task<bool> EnableAsync(
ulong guildId, ulong guildId,
ulong channelId, ulong? channelId,
NotifyType nType, NotifyType nType,
string message) string message)
{ {
// check if the notify type model supports null channel
if (channelId is null)
{
var model = GetRegisteredModel(nType);
if (!model.SupportsOriginTarget)
return false;
}
await using var uow = _db.GetDbContext(); await using var uow = _db.GetDbContext();
await uow.GetTable<Notify>() await uow.GetTable<Notify>()
.InsertOrUpdateAsync(() => new() .InsertOrUpdateAsync(() => new()
@ -201,6 +231,8 @@ public sealed class NotifyService : IReadyExecutor, INotifySubscriber, IEService
Type = nType, Type = nType,
Message = message Message = message
}; };
return true;
} }
public async Task DisableAsync(ulong guildId, NotifyType nType) public async Task DisableAsync(ulong guildId, NotifyType nType)
@ -244,11 +276,15 @@ public sealed class NotifyService : IReadyExecutor, INotifySubscriber, IEService
// messed up big time, it was supposed to be fully extensible, but it's stored as an enum in the database already... // messed up big time, it was supposed to be fully extensible, but it's stored as an enum in the database already...
private readonly ConcurrentDictionary<NotifyType, NotifyModelData> _models = new(); private readonly ConcurrentDictionary<NotifyType, NotifyModelData> _models = new();
public void RegisterModel<T>() where T : struct, INotifyModel<T> public void RegisterModel<T>() where T : struct, INotifyModel<T>
{ {
var data = new NotifyModelData(T.NotifyType, T.GetReplacements().Map(x => x.Name)); var data = new NotifyModelData(T.NotifyType,
T.SupportsOriginTarget,
T.GetReplacements().Map(x => x.Name));
_models[T.NotifyType] = data; _models[T.NotifyType] = data;
} }
public NotifyModelData GetRegisteredModel(NotifyType nType) => _models[nType]; public NotifyModelData GetRegisteredModel(NotifyType nType)
} => _models[nType];
}

View file

@ -85,6 +85,8 @@ public class GuildConfigXpService(DbService db, ShardData shardData, XpConfigSer
GuildId = guildId, GuildId = guildId,
RateType = type, RateType = type,
}); });
_guildRates[(type, guildId)] = new XpRate(type, amount, cooldown);
} }
public async Task SetChannelXpRateAsync(ulong guildId, public async Task SetChannelXpRateAsync(ulong guildId,
@ -119,6 +121,9 @@ public class GuildConfigXpService(DbService db, ShardData shardData, XpConfigSer
ChannelId = channelId, ChannelId = channelId,
RateType = type, RateType = type,
}); });
_channelRates.GetOrAdd(guildId, _ => new())
[(type, channelId)] = new XpRate(type, amount, cooldown);
} }
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
@ -137,6 +142,9 @@ public class GuildConfigXpService(DbService db, ShardData shardData, XpConfigSer
var deleted = await uow.GetTable<GuildXpConfig>() var deleted = await uow.GetTable<GuildXpConfig>()
.Where(x => x.GuildId == guildId) .Where(x => x.GuildId == guildId)
.DeleteAsync(); .DeleteAsync();
_guildRates.TryRemove((XpRateType.Text, guildId), out _);
return deleted > 0; return deleted > 0;
} }
@ -146,6 +154,10 @@ public class GuildConfigXpService(DbService db, ShardData shardData, XpConfigSer
var deleted = await uow.GetTable<ChannelXpConfig>() var deleted = await uow.GetTable<ChannelXpConfig>()
.Where(x => x.GuildId == guildId && x.ChannelId == channelId) .Where(x => x.GuildId == guildId && x.ChannelId == channelId)
.DeleteAsync(); .DeleteAsync();
if (_channelRates.TryGetValue(guildId, out var channelRates))
channelRates.TryRemove((XpRateType.Text, channelId), out _);
return deleted > 0; return deleted > 0;
} }

View file

@ -155,7 +155,7 @@ public class XpService : IEService, IReadyExecutor, IExecNoCommand
if (oldBatch.Contains(u)) if (oldBatch.Contains(u))
{ {
validUsers.Add(new(u, rate.Amount)); validUsers.Add(new(u, rate.Amount, vc.Id));
} }
_voiceXpBatch.Add(u); _voiceXpBatch.Add(u);
@ -218,13 +218,13 @@ public class XpService : IEService, IReadyExecutor, IExecNoCommand
(batch, stats) => stats) (batch, stats) => stats)
.ToListAsyncLinqToDB(); .ToListAsyncLinqToDB();
var userToXp = currentBatch.ToDictionary(x => x.User.Id, x => x.Xp); var userToXp = currentBatch.ToDictionary(x => x.User.Id, x => x);
foreach (var u in updated) foreach (var u in updated)
{ {
if (!userToXp.TryGetValue(u.UserId, out var xpGained)) if (!userToXp.TryGetValue(u.UserId, out var data))
continue; continue;
var oldStats = new LevelStats(u.Xp - xpGained); var oldStats = new LevelStats(u.Xp - data.Xp);
var newStats = new LevelStats(u.Xp); var newStats = new LevelStats(u.Xp);
Log.Information("User {User} xp updated from {OldLevel} to {NewLevel}", Log.Information("User {User} xp updated from {OldLevel} to {NewLevel}",
@ -235,9 +235,8 @@ public class XpService : IEService, IReadyExecutor, IExecNoCommand
if (oldStats.Level < newStats.Level) if (oldStats.Level < newStats.Level)
{ {
await _levelUpQueue.EnqueueAsync(NotifyUser(u.GuildId, await _levelUpQueue.EnqueueAsync(NotifyUser(u.GuildId,
0, data.ChannelId,
u.UserId, u.UserId,
true,
oldStats.Level, oldStats.Level,
newStats.Level)); newStats.Level));
} }
@ -246,19 +245,15 @@ public class XpService : IEService, IReadyExecutor, IExecNoCommand
private Func<Task> NotifyUser( private Func<Task> NotifyUser(
ulong guildId, ulong guildId,
ulong channelId, ulong? channelId,
ulong userId, ulong userId,
bool isServer,
long oldLevel, long oldLevel,
long newLevel) long newLevel)
=> async () => => async () =>
{ {
if (isServer) await HandleRewardsInternalAsync(guildId, userId, oldLevel, newLevel);
{
await HandleRewardsInternalAsync(guildId, userId, oldLevel, newLevel);
}
await HandleNotifyInternalAsync(guildId, channelId, userId, isServer, newLevel); await HandleNotifyInternalAsync(guildId, channelId, userId, newLevel);
}; };
private async Task HandleRewardsInternalAsync( private async Task HandleRewardsInternalAsync(
@ -338,9 +333,8 @@ public class XpService : IEService, IReadyExecutor, IExecNoCommand
private async Task HandleNotifyInternalAsync( private async Task HandleNotifyInternalAsync(
ulong guildId, ulong guildId,
ulong channelId, ulong? channelId,
ulong userId, ulong userId,
bool isServer,
long newLevel) long newLevel)
{ {
var guild = _client.GetGuild(guildId); var guild = _client.GetGuild(guildId);
@ -349,18 +343,15 @@ public class XpService : IEService, IReadyExecutor, IExecNoCommand
if (guild is null || user is null) if (guild is null || user is null)
return; return;
if (isServer) var model = new LevelUpNotifyModel()
{ {
var model = new LevelUpNotifyModel() GuildId = guildId,
{ UserId = userId,
GuildId = guildId, ChannelId = channelId,
UserId = userId, Level = newLevel
ChannelId = channelId, };
Level = newLevel await _notifySub.NotifyAsync(model, true);
}; return;
await _notifySub.NotifyAsync(model, true);
return;
}
} }
public async Task SetCurrencyReward(ulong guildId, int level, int amount) public async Task SetCurrencyReward(ulong guildId, int level, int amount)
@ -552,7 +543,7 @@ public class XpService : IEService, IReadyExecutor, IExecNoCommand
if (!await TryAddUserGainedXpAsync(user.Id, rate.Cooldown)) if (!await TryAddUserGainedXpAsync(user.Id, rate.Cooldown))
return; return;
_usersBatch.Add(new(user, rate.Amount)); _usersBatch.Add(new(user, rate.Amount, gc.Id));
}); });
return Task.CompletedTask; return Task.CompletedTask;
@ -1169,7 +1160,7 @@ public class XpService : IEService, IReadyExecutor, IExecNoCommand
} }
} }
public readonly record struct XpQueueEntry(IGuildUser User, long Xp) public readonly record struct XpQueueEntry(IGuildUser User, long Xp, ulong? ChannelId)
{ {
public bool Equals(XpQueueEntry? other) public bool Equals(XpQueueEntry? other)
=> other?.User == User; => other?.User == User;

View file

@ -4,16 +4,6 @@ version: 3
isEnabled: false isEnabled: false
# Who can do how much of what # Who can do how much of what
limits: limits:
100:
ChatBot:
quota: 50000000
quotaPeriod: PerMonth
ReactionRole:
quota: -1
quotaPeriod: Total
Prune:
quota: -1
quotaPeriod: PerDay
50: 50:
ChatBot: ChatBot:
quota: 20000000 quota: 20000000
@ -24,16 +14,6 @@ limits:
Prune: Prune:
quota: -1 quota: -1
quotaPeriod: PerDay quotaPeriod: PerDay
20:
ChatBot:
quota: 6500000
quotaPeriod: PerMonth
ReactionRole:
quota: -1
quotaPeriod: Total
Prune:
quota: 20
quotaPeriod: PerDay
10: 10:
ChatBot: ChatBot:
quota: 2500000 quota: 2500000
@ -44,7 +24,7 @@ limits:
Prune: Prune:
quota: 5 quota: 5
quotaPeriod: PerDay quotaPeriod: PerDay
5: 2:
ChatBot: ChatBot:
quota: 1000000 quota: 1000000
quotaPeriod: PerMonth quotaPeriod: PerMonth
@ -53,4 +33,4 @@ limits:
quotaPeriod: Total quotaPeriod: Total
Prune: Prune:
quota: 2 quota: 2
quotaPeriod: PerDay quotaPeriod: PerDay

View file

@ -4862,14 +4862,18 @@ minesweeper:
desc: "The number of mines to create." desc: "The number of mines to create."
notify: notify:
desc: |- desc: |-
Sends a message to the current channel once the specified event occurs. Sends a message to the specified channel once the specified event occurs.
If no channel is specified, the message will be sent to the channel from which the event originated.
*note: this is only possible for events that have an origin channel (for example `levelup`)*
Provide no parameters to see all available events. Provide no parameters to see all available events.
ex: ex:
- 'levelup Congratulations to user %user.name% for reaching level %event.level%' - 'levelup Congratulations to user %user.name% for reaching level %event.level%'
params: params:
- { } - { }
- event: - event:
desc: "The event to notify on." desc: "The event for which to see the current message."
- event: - event:
desc: "The event to notify on." desc: "The event to notify on."
message: message: