From 5a235b05659be737e61d1fd4c3a83914d0fbd706 Mon Sep 17 00:00:00 2001 From: Toastie <toastie@toastiet0ast.com> Date: Sat, 8 Feb 2025 17:01:15 +1300 Subject: [PATCH] added .notifyph --- src/EllieBot/Db/Models/DiscordUser.cs | 6 +- .../Administration/Notify/INotifyModel.cs | 10 +- .../Notify/INotifySubscriber.cs | 15 ++- .../Notify/Models/AddRoleRewardNotifyModel.cs | 25 ++-- .../Notify/Models/LevelUpNotifyModel.cs | 24 ++-- .../Notify/Models/ProtectionNotifyModel.cs | 14 +-- .../Models/RemoveRoleRewardNotifyModel.cs | 28 +++-- .../Administration/Notify/NotifyCommands.cs | 18 ++- .../Notify/NotifyModelExtensions.cs | 2 +- .../Administration/Notify/NotifyService.cs | 117 +++++++++++------- src/EllieBot/strings/aliases.yml | 4 + .../strings/commands/commands.en-US.yml | 8 ++ .../strings/responses/responses.en-US.json | 1 + 13 files changed, 171 insertions(+), 101 deletions(-) diff --git a/src/EllieBot/Db/Models/DiscordUser.cs b/src/EllieBot/Db/Models/DiscordUser.cs index f400694..b07da85 100644 --- a/src/EllieBot/Db/Models/DiscordUser.cs +++ b/src/EllieBot/Db/Models/DiscordUser.cs @@ -19,14 +19,12 @@ public class DiscordUser : DbEntity public long CurrencyAmount { get; set; } - public override bool Equals(object obj) + public override bool Equals(object? obj) => obj is DiscordUser du ? du.UserId == UserId : false; public override int GetHashCode() => UserId.GetHashCode(); public override string ToString() - { - return Username; - } + => Username ?? DEFAULT_USERNAME; } \ No newline at end of file diff --git a/src/EllieBot/Modules/Administration/Notify/INotifyModel.cs b/src/EllieBot/Modules/Administration/Notify/INotifyModel.cs index 9fb1d80..bbfa70d 100644 --- a/src/EllieBot/Modules/Administration/Notify/INotifyModel.cs +++ b/src/EllieBot/Modules/Administration/Notify/INotifyModel.cs @@ -1,13 +1,13 @@ using EllieBot.Db.Models; -using System.Collections; namespace EllieBot.Modules.Administration; -public interface INotifyModel +public interface INotifyModel<T> + where T: struct, INotifyModel<T> { static abstract string KeyName { get; } static abstract NotifyType NotifyType { get; } - IReadOnlyDictionary<string, Func<SocketGuild, string>> GetReplacements(); + static abstract IReadOnlyList<NotifyModelPlaceholderData<T>> GetReplacements(); public virtual bool TryGetGuildId(out ulong guildId) { @@ -20,4 +20,6 @@ public interface INotifyModel userId = 0; return false; } -} \ No newline at end of file +} + +public readonly record struct NotifyModelPlaceholderData<T>(string Name, Func<T, SocketGuild, string> Func); \ No newline at end of file diff --git a/src/EllieBot/Modules/Administration/Notify/INotifySubscriber.cs b/src/EllieBot/Modules/Administration/Notify/INotifySubscriber.cs index f0a8731..23d4008 100644 --- a/src/EllieBot/Modules/Administration/Notify/INotifySubscriber.cs +++ b/src/EllieBot/Modules/Administration/Notify/INotifySubscriber.cs @@ -1,7 +1,16 @@ -namespace EllieBot.Modules.Administration; +using EllieBot.Db.Models; + +namespace EllieBot.Modules.Administration; public interface INotifySubscriber { Task NotifyAsync<T>(T data, bool isShardLocal = false) - where T : struct, INotifyModel; -} \ No newline at end of file + where T : struct, INotifyModel<T>; + + void RegisterModel<T>() + where T : struct, INotifyModel<T>; + + NotifyModelData GetRegisteredModel(NotifyType nType); +} + +public readonly record struct NotifyModelData(NotifyType Type, IReadOnlyList<string> Replacements); \ No newline at end of file diff --git a/src/EllieBot/Modules/Administration/Notify/Models/AddRoleRewardNotifyModel.cs b/src/EllieBot/Modules/Administration/Notify/Models/AddRoleRewardNotifyModel.cs index d6f8d37..60e263c 100644 --- a/src/EllieBot/Modules/Administration/Notify/Models/AddRoleRewardNotifyModel.cs +++ b/src/EllieBot/Modules/Administration/Notify/Models/AddRoleRewardNotifyModel.cs @@ -3,7 +3,7 @@ using EllieBot.Modules.Administration; namespace EllieBot.Modules.Xp.Services; -public record struct AddRoleRewardNotifyModel(ulong GuildId, ulong RoleId, ulong UserId, long Level) : INotifyModel +public record struct AddRoleRewardNotifyModel(ulong GuildId, ulong RoleId, ulong UserId, long Level) : INotifyModel<AddRoleRewardNotifyModel> { public static string KeyName => "notify.reward.addrole"; @@ -11,16 +11,17 @@ public record struct AddRoleRewardNotifyModel(ulong GuildId, ulong RoleId, ulong public static NotifyType NotifyType => NotifyType.AddRoleReward; - public IReadOnlyDictionary<string, Func<SocketGuild, string>> GetReplacements() - { - var model = this; - return new Dictionary<string, Func<SocketGuild, string>>() - { - { "%event.user%", g => g.GetUser(model.UserId)?.ToString() ?? model.UserId.ToString() }, - { "%event.role%", g => g.GetRole(model.RoleId)?.ToString() ?? model.RoleId.ToString() }, - { "%event.level%", g => model.Level.ToString() } - }; - } + public const string PH_LEVEL = "level"; + public const string PH_USER = "user"; + public const string PH_ROLE = "role"; + + public static IReadOnlyList<NotifyModelPlaceholderData<AddRoleRewardNotifyModel>> GetReplacements() + => + [ + 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_ROLE, static (data, g) => g.GetRole(data.RoleId)?.ToString() ?? data.RoleId.ToString() ) + ]; public bool TryGetUserId(out ulong userId) { @@ -33,4 +34,4 @@ public record struct AddRoleRewardNotifyModel(ulong GuildId, ulong RoleId, ulong guildId = GuildId; return true; } -} +} \ No newline at end of file diff --git a/src/EllieBot/Modules/Administration/Notify/Models/LevelUpNotifyModel.cs b/src/EllieBot/Modules/Administration/Notify/Models/LevelUpNotifyModel.cs index dc85d3e..2643e22 100644 --- a/src/EllieBot/Modules/Administration/Notify/Models/LevelUpNotifyModel.cs +++ b/src/EllieBot/Modules/Administration/Notify/Models/LevelUpNotifyModel.cs @@ -2,11 +2,11 @@ using EllieBot.Db.Models; namespace EllieBot.Modules.Administration; -public record struct LevelUpNotifyModel( +public readonly record struct LevelUpNotifyModel( ulong GuildId, ulong ChannelId, ulong UserId, - long Level) : INotifyModel + long Level) : INotifyModel<LevelUpNotifyModel> { public static string KeyName => "notify.levelup"; @@ -14,23 +14,25 @@ public record struct LevelUpNotifyModel( public static NotifyType NotifyType => NotifyType.LevelUp; - public IReadOnlyDictionary<string, Func<SocketGuild, string>> GetReplacements() + public const string PH_USER = "user"; + public const string PH_LEVEL = "level"; + + public static IReadOnlyList<NotifyModelPlaceholderData<LevelUpNotifyModel>> GetReplacements() { - var data = this; - return new Dictionary<string, Func<SocketGuild, string>>() - { - { "%event.level%", g => data.Level.ToString() }, - { "%event.user%", g => g.GetUser(data.UserId)?.ToString() ?? data.UserId.ToString() }, - }; + return + [ + new(PH_LEVEL, static (data, g) => data.Level.ToString() ), + new(PH_USER, static (data, g) => g.GetUser(data.UserId)?.ToString() ?? data.UserId.ToString() ) + ]; } - public bool TryGetGuildId(out ulong guildId) + public readonly bool TryGetGuildId(out ulong guildId) { guildId = GuildId; return true; } - public bool TryGetUserId(out ulong userId) + public readonly bool TryGetUserId(out ulong userId) { userId = UserId; return true; diff --git a/src/EllieBot/Modules/Administration/Notify/Models/ProtectionNotifyModel.cs b/src/EllieBot/Modules/Administration/Notify/Models/ProtectionNotifyModel.cs index bc23531..d27307f 100644 --- a/src/EllieBot/Modules/Administration/Notify/Models/ProtectionNotifyModel.cs +++ b/src/EllieBot/Modules/Administration/Notify/Models/ProtectionNotifyModel.cs @@ -3,7 +3,7 @@ using EllieBot.Db.Models; namespace EllieBot.Modules.Administration.Services; -public record struct ProtectionNotifyModel(ulong GuildId, ProtectionType ProtType, ulong UserId) : INotifyModel +public record struct ProtectionNotifyModel(ulong GuildId, ProtectionType ProtType, ulong UserId) : INotifyModel<ProtectionNotifyModel> { public static string KeyName => "notify.protection"; @@ -11,13 +11,13 @@ public record struct ProtectionNotifyModel(ulong GuildId, ProtectionType ProtTyp public static NotifyType NotifyType => NotifyType.Protection; - public IReadOnlyDictionary<string, Func<SocketGuild, string>> GetReplacements() + public const string PH_TYPE = "type"; + + public static IReadOnlyList<NotifyModelPlaceholderData<ProtectionNotifyModel>> GetReplacements() { - var data = this; - return new Dictionary<string, Func<SocketGuild, string>>() - { - { "%event.type%", g => data.ProtType.ToString() }, - }; + return [ + new(PH_TYPE, static (data, g) => data.ProtType.ToString() ) + ]; } public bool TryGetUserId(out ulong userId) diff --git a/src/EllieBot/Modules/Administration/Notify/Models/RemoveRoleRewardNotifyModel.cs b/src/EllieBot/Modules/Administration/Notify/Models/RemoveRoleRewardNotifyModel.cs index f078925..5997692 100644 --- a/src/EllieBot/Modules/Administration/Notify/Models/RemoveRoleRewardNotifyModel.cs +++ b/src/EllieBot/Modules/Administration/Notify/Models/RemoveRoleRewardNotifyModel.cs @@ -3,7 +3,7 @@ using EllieBot.Modules.Administration; namespace EllieBot.Modules.Xp.Services; -public record struct RemoveRoleRewardNotifyModel(ulong GuildId, ulong RoleId, ulong UserId, long Level) : INotifyModel +public record struct RemoveRoleRewardNotifyModel(ulong GuildId, ulong RoleId, ulong UserId, long Level) : INotifyModel<RemoveRoleRewardNotifyModel> { public static string KeyName => "notify.reward.removerole"; @@ -11,17 +11,6 @@ public record struct RemoveRoleRewardNotifyModel(ulong GuildId, ulong RoleId, ul public static NotifyType NotifyType => NotifyType.RemoveRoleReward; - public IReadOnlyDictionary<string, Func<SocketGuild, string>> GetReplacements() - { - var model = this; - return new Dictionary<string, Func<SocketGuild, string>>() - { - { "%event.user%", g => g.GetUser(model.UserId)?.ToString() ?? model.UserId.ToString() }, - { "%event.role%", g => g.GetRole(model.RoleId)?.ToString() ?? model.RoleId.ToString() }, - { "%event.level%", g => model.Level.ToString() }, - }; - } - public bool TryGetUserId(out ulong userId) { userId = UserId; @@ -33,4 +22,17 @@ public record struct RemoveRoleRewardNotifyModel(ulong GuildId, ulong RoleId, ul guildId = GuildId; return true; } -} + + public const string PH_USER = "user"; + public const string PH_ROLE = "role"; + public const string PH_LEVEL = "level"; + + public static IReadOnlyList<NotifyModelPlaceholderData<RemoveRoleRewardNotifyModel>> GetReplacements() + { + return [ + 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_LEVEL, static (data, g) => data.Level.ToString() ), + ]; + } +} \ No newline at end of file diff --git a/src/EllieBot/Modules/Administration/Notify/NotifyCommands.cs b/src/EllieBot/Modules/Administration/Notify/NotifyCommands.cs index 3b75f24..2dd39b7 100644 --- a/src/EllieBot/Modules/Administration/Notify/NotifyCommands.cs +++ b/src/EllieBot/Modules/Administration/Notify/NotifyCommands.cs @@ -13,7 +13,7 @@ public partial class Administration { await Response() .Paginated() - .Items(Enum.GetValues<NotifyType>()) + .Items(Enum.GetValues<NotifyType>().DistinctBy(x => (int)x).ToList()) .PageSize(5) .Page((items, page) => { @@ -75,6 +75,22 @@ public partial class Administration await Response().Confirm(strs.notify_on($"<#{ctx.Channel.Id}>", Format.Bold(nType.ToString()))).SendAsync(); } + [Cmd] + [UserPerm(GuildPerm.Administrator)] + public async Task NotifyPlaceholders(NotifyType nType) + { + var data = _service.GetRegisteredModel(nType); + + var eb = CreateEmbed() + .WithOkColor() + .WithTitle(GetText(strs.notify_placeholders(nType.ToString().ToLower()))); + + eb.WithDescription(data.Replacements.Join("\n---\n", x => $"`%event.{x}%`")); + + await Response().Embed(eb).SendAsync(); + + } + [Cmd] [UserPerm(GuildPerm.Administrator)] public async Task NotifyList(int page = 1) diff --git a/src/EllieBot/Modules/Administration/Notify/NotifyModelExtensions.cs b/src/EllieBot/Modules/Administration/Notify/NotifyModelExtensions.cs index 9aca824..6f02324 100644 --- a/src/EllieBot/Modules/Administration/Notify/NotifyModelExtensions.cs +++ b/src/EllieBot/Modules/Administration/Notify/NotifyModelExtensions.cs @@ -3,6 +3,6 @@ public static class NotifyModelExtensions { public static TypedKey<T> GetTypedKey<T>(this T model) - where T : struct, INotifyModel + where T : struct, INotifyModel<T> => new(T.KeyName); } \ No newline at end of file diff --git a/src/EllieBot/Modules/Administration/Notify/NotifyService.cs b/src/EllieBot/Modules/Administration/Notify/NotifyService.cs index 4509552..9fc7ea7 100644 --- a/src/EllieBot/Modules/Administration/Notify/NotifyService.cs +++ b/src/EllieBot/Modules/Administration/Notify/NotifyService.cs @@ -3,6 +3,8 @@ using LinqToDB.EntityFrameworkCore; using EllieBot.Common.ModuleBehaviors; using EllieBot.Db.Models; using EllieBot.Generators; +using EllieBot.Modules.Administration.Services; +using EllieBot.Modules.Xp.Services; namespace EllieBot.Modules.Administration; @@ -32,30 +34,42 @@ public sealed class NotifyService : IReadyExecutor, INotifySubscriber, IEService _pubSub = pubSub; } + private void RegisterModels() + { + + RegisterModel<LevelUpNotifyModel>(); + RegisterModel<ProtectionNotifyModel>(); + RegisterModel<AddRoleRewardNotifyModel>(); + RegisterModel<RemoveRoleRewardNotifyModel>(); + + } + public async Task OnReadyAsync() { + RegisterModels(); + await using var uow = _db.GetDbContext(); _events = (await uow.GetTable<Notify>() - .Where(x => Queries.GuildOnShard(x.GuildId, - _creds.TotalShards, - _client.ShardId)) - .ToListAsyncLinqToDB()) - .GroupBy(x => x.Type) - .ToDictionary(x => x.Key, x => x.ToDictionary(x => x.GuildId).ToConcurrent()) - .ToConcurrent(); + .Where(x => Queries.GuildOnShard(x.GuildId, + _creds.TotalShards, + _client.ShardId)) + .ToListAsyncLinqToDB()) + .GroupBy(x => x.Type) + .ToDictionary(x => x.Key, x => x.ToDictionary(x => x.GuildId).ToConcurrent()) + .ToConcurrent(); await SubscribeToEvent<LevelUpNotifyModel>(); } private async Task SubscribeToEvent<T>() - where T : struct, INotifyModel + where T : struct, INotifyModel<T> { await _pubSub.Sub(new TypedKey<T>(T.KeyName), async (model) => await OnEvent(model)); } public async Task NotifyAsync<T>(T data, bool isShardLocal = false) - where T : struct, INotifyModel + where T : struct, INotifyModel<T> { try { @@ -77,7 +91,7 @@ public sealed class NotifyService : IReadyExecutor, INotifySubscriber, IEService } private async Task OnEvent<T>(T model) - where T : struct, INotifyModel + where T : struct, INotifyModel<T> { if (_events.TryGetValue(T.NotifyType, out var subs)) { @@ -113,7 +127,8 @@ public sealed class NotifyService : IReadyExecutor, INotifySubscriber, IEService } } - private async Task HandleNotifyEvent(Notify conf, INotifyModel model) + private async Task HandleNotifyEvent<T>(Notify conf, T model) + where T : struct, INotifyModel<T> { var guild = _client.GetGuild(conf.GuildId); var channel = guild?.GetTextChannel(conf.ChannelId); @@ -130,26 +145,28 @@ public sealed class NotifyService : IReadyExecutor, INotifySubscriber, IEService var rctx = new ReplacementContext(guild: guild, channel: channel, user: user); var st = SmartText.CreateFrom(conf.Message); - foreach (var modelRep in model.GetReplacements()) + foreach (var modelRep in T.GetReplacements()) { - rctx.WithOverride(modelRep.Key, () => modelRep.Value(guild)); + rctx.WithOverride(GetPhToken(modelRep.Name), () => modelRep.Func(model, guild)); } st = await _repSvc.ReplaceAsync(st, rctx); if (st is SmartPlainText spt) { await _mss.Response(channel) - .Confirm(spt.Text) - .SendAsync(); + .Confirm(spt.Text) + .SendAsync(); return; } await _mss.Response(channel) - .Text(st) - .Sanitize(false) - .SendAsync(); + .Text(st) + .Sanitize(false) + .SendAsync(); } + private static string GetPhToken(string name) => $"%event.{name}%"; + public async Task EnableAsync( ulong guildId, ulong channelId, @@ -158,23 +175,23 @@ public sealed class NotifyService : IReadyExecutor, INotifySubscriber, IEService { await using var uow = _db.GetDbContext(); await uow.GetTable<Notify>() - .InsertOrUpdateAsync(() => new() - { - GuildId = guildId, - ChannelId = channelId, - Type = nType, - Message = message, - }, - (_) => new() - { - Message = message, - ChannelId = channelId - }, - () => new() - { - GuildId = guildId, - Type = nType - }); + .InsertOrUpdateAsync(() => new() + { + GuildId = guildId, + ChannelId = channelId, + Type = nType, + Message = message, + }, + (_) => new() + { + Message = message, + ChannelId = channelId + }, + () => new() + { + GuildId = guildId, + Type = nType + }); var eventDict = _events.GetOrAdd(nType, _ => new()); eventDict[guildId] = new() @@ -190,8 +207,8 @@ public sealed class NotifyService : IReadyExecutor, INotifySubscriber, IEService { await using var uow = _db.GetDbContext(); var deleted = await uow.GetTable<Notify>() - .Where(x => x.GuildId == guildId && x.Type == nType) - .DeleteAsync(); + .Where(x => x.GuildId == guildId && x.Type == nType) + .DeleteAsync(); if (deleted == 0) return; @@ -208,11 +225,11 @@ public sealed class NotifyService : IReadyExecutor, INotifySubscriber, IEService await using var ctx = _db.GetDbContext(); var list = await ctx.GetTable<Notify>() - .Where(x => x.GuildId == guildId) - .OrderBy(x => x.Type) - .Skip(page * 10) - .Take(10) - .ToListAsyncLinqToDB(); + .Where(x => x.GuildId == guildId) + .OrderBy(x => x.Type) + .Skip(page * 10) + .Take(10) + .ToListAsyncLinqToDB(); return list; } @@ -221,7 +238,17 @@ public sealed class NotifyService : IReadyExecutor, INotifySubscriber, IEService { await using var ctx = _db.GetDbContext(); return await ctx.GetTable<Notify>() - .Where(x => x.GuildId == guildId && x.Type == nType) - .FirstOrDefaultAsyncLinqToDB(); + .Where(x => x.GuildId == guildId && x.Type == nType) + .FirstOrDefaultAsyncLinqToDB(); } -} \ No newline at end of file + + // 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(); + public void RegisterModel<T>() where T : struct, INotifyModel<T> + { + var data = new NotifyModelData(T.NotifyType, T.GetReplacements().Map(x => x.Name)); + _models[T.NotifyType] = data; + } + + public NotifyModelData GetRegisteredModel(NotifyType nType) => _models[nType]; +} diff --git a/src/EllieBot/strings/aliases.yml b/src/EllieBot/strings/aliases.yml index 969bfe0..77e41e6 100644 --- a/src/EllieBot/strings/aliases.yml +++ b/src/EllieBot/strings/aliases.yml @@ -1562,6 +1562,10 @@ notifyclear: - notifyremove - notifyrm - notifclr +notifyphs: + - notifyphs + - notifyph + - notifyplaceholders winlb: - winlb - wins diff --git a/src/EllieBot/strings/commands/commands.en-US.yml b/src/EllieBot/strings/commands/commands.en-US.yml index 849bf15..a390f53 100644 --- a/src/EllieBot/strings/commands/commands.en-US.yml +++ b/src/EllieBot/strings/commands/commands.en-US.yml @@ -4911,6 +4911,14 @@ notifyclear: params: - event: desc: "The notify event to clear." +notifyphs: + desc: |- + Lists the placeholders for a given notify event type + ex: + - 'levelup' + params: + - event: + desc: "The notify event to list placeholders for." winlb: desc: |- Shows the biggest wins leaderboard diff --git a/src/EllieBot/strings/responses/responses.en-US.json b/src/EllieBot/strings/responses/responses.en-US.json index de8f197..13c177e 100644 --- a/src/EllieBot/strings/responses/responses.en-US.json +++ b/src/EllieBot/strings/responses/responses.en-US.json @@ -1158,6 +1158,7 @@ "notify_desc_addrolerew": "Triggers when a user gets a role as a reward for reaching a level (xprew).", "notify_desc_removerolerew": "Triggers when a user loses a role as a reward for reaching a level (xprew).", "notify_desc_not_found": "No description found for this notify event. Please report this.", + "notify_placeholders": "Placeholders for '{0}' notify event", "winlb": "Biggest Wins Leaderboard", "no_banner": "No banner set.", "fish_nothing": "You caught nothing, try again.",