added .notifyph

This commit is contained in:
Toastie 2025-02-08 17:01:15 +13:00
parent 37601286f5
commit 5a235b0565
Signed by: toastie_t0ast
GPG key ID: 0861BE54AD481DC7
13 changed files with 171 additions and 101 deletions

View file

@ -19,14 +19,12 @@ public class DiscordUser : DbEntity
public long CurrencyAmount { get; set; } 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; => obj is DiscordUser du ? du.UserId == UserId : false;
public override int GetHashCode() public override int GetHashCode()
=> UserId.GetHashCode(); => UserId.GetHashCode();
public override string ToString() public override string ToString()
{ => Username ?? DEFAULT_USERNAME;
return Username;
}
} }

View file

@ -1,13 +1,13 @@
using EllieBot.Db.Models; using EllieBot.Db.Models;
using System.Collections;
namespace EllieBot.Modules.Administration; namespace EllieBot.Modules.Administration;
public interface INotifyModel public interface 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; }
IReadOnlyDictionary<string, Func<SocketGuild, string>> GetReplacements(); static abstract IReadOnlyList<NotifyModelPlaceholderData<T>> GetReplacements();
public virtual bool TryGetGuildId(out ulong guildId) public virtual bool TryGetGuildId(out ulong guildId)
{ {
@ -20,4 +20,6 @@ public interface INotifyModel
userId = 0; userId = 0;
return false; return false;
} }
} }
public readonly record struct NotifyModelPlaceholderData<T>(string Name, Func<T, SocketGuild, string> Func);

View file

@ -1,7 +1,16 @@
namespace EllieBot.Modules.Administration; using EllieBot.Db.Models;
namespace EllieBot.Modules.Administration;
public interface INotifySubscriber public interface INotifySubscriber
{ {
Task NotifyAsync<T>(T data, bool isShardLocal = false) Task NotifyAsync<T>(T data, bool isShardLocal = false)
where T : struct, INotifyModel; 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);

View file

@ -3,7 +3,7 @@ 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 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";
@ -11,16 +11,17 @@ public record struct AddRoleRewardNotifyModel(ulong GuildId, ulong RoleId, ulong
public static NotifyType NotifyType public static NotifyType NotifyType
=> NotifyType.AddRoleReward; => NotifyType.AddRoleReward;
public IReadOnlyDictionary<string, Func<SocketGuild, string>> GetReplacements() public const string PH_LEVEL = "level";
{ public const string PH_USER = "user";
var model = this; public const string PH_ROLE = "role";
return new Dictionary<string, Func<SocketGuild, string>>()
{ public static IReadOnlyList<NotifyModelPlaceholderData<AddRoleRewardNotifyModel>> GetReplacements()
{ "%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() } 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) public bool TryGetUserId(out ulong userId)
{ {
@ -33,4 +34,4 @@ public record struct AddRoleRewardNotifyModel(ulong GuildId, ulong RoleId, ulong
guildId = GuildId; guildId = GuildId;
return true; return true;
} }
} }

View file

@ -2,11 +2,11 @@ using EllieBot.Db.Models;
namespace EllieBot.Modules.Administration; namespace EllieBot.Modules.Administration;
public record struct LevelUpNotifyModel( public readonly record struct LevelUpNotifyModel(
ulong GuildId, ulong GuildId,
ulong ChannelId, ulong ChannelId,
ulong UserId, ulong UserId,
long Level) : INotifyModel long Level) : INotifyModel<LevelUpNotifyModel>
{ {
public static string KeyName public static string KeyName
=> "notify.levelup"; => "notify.levelup";
@ -14,23 +14,25 @@ public record struct LevelUpNotifyModel(
public static NotifyType NotifyType public static NotifyType NotifyType
=> NotifyType.LevelUp; => 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
return new Dictionary<string, Func<SocketGuild, string>>() [
{ new(PH_LEVEL, static (data, g) => data.Level.ToString() ),
{ "%event.level%", g => data.Level.ToString() }, new(PH_USER, static (data, g) => g.GetUser(data.UserId)?.ToString() ?? data.UserId.ToString() )
{ "%event.user%", g => g.GetUser(data.UserId)?.ToString() ?? data.UserId.ToString() }, ];
};
} }
public bool TryGetGuildId(out ulong guildId) public readonly bool TryGetGuildId(out ulong guildId)
{ {
guildId = GuildId; guildId = GuildId;
return true; return true;
} }
public bool TryGetUserId(out ulong userId) public readonly bool TryGetUserId(out ulong userId)
{ {
userId = UserId; userId = UserId;
return true; return true;

View file

@ -3,7 +3,7 @@ using EllieBot.Db.Models;
namespace EllieBot.Modules.Administration.Services; 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 public static string KeyName
=> "notify.protection"; => "notify.protection";
@ -11,13 +11,13 @@ public record struct ProtectionNotifyModel(ulong GuildId, ProtectionType ProtTyp
public static NotifyType NotifyType public static NotifyType NotifyType
=> NotifyType.Protection; => 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 [
return new Dictionary<string, Func<SocketGuild, string>>() new(PH_TYPE, static (data, g) => data.ProtType.ToString() )
{ ];
{ "%event.type%", g => data.ProtType.ToString() },
};
} }
public bool TryGetUserId(out ulong userId) public bool TryGetUserId(out ulong userId)

View file

@ -3,7 +3,7 @@ using EllieBot.Modules.Administration;
namespace EllieBot.Modules.Xp.Services; 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 public static string KeyName
=> "notify.reward.removerole"; => "notify.reward.removerole";
@ -11,17 +11,6 @@ public record struct RemoveRoleRewardNotifyModel(ulong GuildId, ulong RoleId, ul
public static NotifyType NotifyType public static NotifyType NotifyType
=> NotifyType.RemoveRoleReward; => 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) public bool TryGetUserId(out ulong userId)
{ {
userId = UserId; userId = UserId;
@ -33,4 +22,17 @@ public record struct RemoveRoleRewardNotifyModel(ulong GuildId, ulong RoleId, ul
guildId = GuildId; guildId = GuildId;
return true; 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() ),
];
}
}

View file

@ -13,7 +13,7 @@ public partial class Administration
{ {
await Response() await Response()
.Paginated() .Paginated()
.Items(Enum.GetValues<NotifyType>()) .Items(Enum.GetValues<NotifyType>().DistinctBy(x => (int)x).ToList())
.PageSize(5) .PageSize(5)
.Page((items, page) => .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(); 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] [Cmd]
[UserPerm(GuildPerm.Administrator)] [UserPerm(GuildPerm.Administrator)]
public async Task NotifyList(int page = 1) public async Task NotifyList(int page = 1)

View file

@ -3,6 +3,6 @@
public static class NotifyModelExtensions public static class NotifyModelExtensions
{ {
public static TypedKey<T> GetTypedKey<T>(this T model) public static TypedKey<T> GetTypedKey<T>(this T model)
where T : struct, INotifyModel where T : struct, INotifyModel<T>
=> new(T.KeyName); => new(T.KeyName);
} }

View file

@ -3,6 +3,8 @@ using LinqToDB.EntityFrameworkCore;
using EllieBot.Common.ModuleBehaviors; using EllieBot.Common.ModuleBehaviors;
using EllieBot.Db.Models; using EllieBot.Db.Models;
using EllieBot.Generators; using EllieBot.Generators;
using EllieBot.Modules.Administration.Services;
using EllieBot.Modules.Xp.Services;
namespace EllieBot.Modules.Administration; namespace EllieBot.Modules.Administration;
@ -32,30 +34,42 @@ public sealed class NotifyService : IReadyExecutor, INotifySubscriber, IEService
_pubSub = pubSub; _pubSub = pubSub;
} }
private void RegisterModels()
{
RegisterModel<LevelUpNotifyModel>();
RegisterModel<ProtectionNotifyModel>();
RegisterModel<AddRoleRewardNotifyModel>();
RegisterModel<RemoveRoleRewardNotifyModel>();
}
public async Task OnReadyAsync() public async Task OnReadyAsync()
{ {
RegisterModels();
await using var uow = _db.GetDbContext(); await using var uow = _db.GetDbContext();
_events = (await uow.GetTable<Notify>() _events = (await uow.GetTable<Notify>()
.Where(x => Queries.GuildOnShard(x.GuildId, .Where(x => Queries.GuildOnShard(x.GuildId,
_creds.TotalShards, _creds.TotalShards,
_client.ShardId)) _client.ShardId))
.ToListAsyncLinqToDB()) .ToListAsyncLinqToDB())
.GroupBy(x => x.Type) .GroupBy(x => x.Type)
.ToDictionary(x => x.Key, x => x.ToDictionary(x => x.GuildId).ToConcurrent()) .ToDictionary(x => x.Key, x => x.ToDictionary(x => x.GuildId).ToConcurrent())
.ToConcurrent(); .ToConcurrent();
await SubscribeToEvent<LevelUpNotifyModel>(); await SubscribeToEvent<LevelUpNotifyModel>();
} }
private async Task SubscribeToEvent<T>() 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)); await _pubSub.Sub(new TypedKey<T>(T.KeyName), async (model) => await OnEvent(model));
} }
public async Task NotifyAsync<T>(T data, bool isShardLocal = false) public async Task NotifyAsync<T>(T data, bool isShardLocal = false)
where T : struct, INotifyModel where T : struct, INotifyModel<T>
{ {
try try
{ {
@ -77,7 +91,7 @@ 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 where T : struct, INotifyModel<T>
{ {
if (_events.TryGetValue(T.NotifyType, out var subs)) 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 guild = _client.GetGuild(conf.GuildId);
var channel = guild?.GetTextChannel(conf.ChannelId); 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 rctx = new ReplacementContext(guild: guild, channel: channel, user: user);
var st = SmartText.CreateFrom(conf.Message); 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); st = await _repSvc.ReplaceAsync(st, rctx);
if (st is SmartPlainText spt) if (st is SmartPlainText spt)
{ {
await _mss.Response(channel) await _mss.Response(channel)
.Confirm(spt.Text) .Confirm(spt.Text)
.SendAsync(); .SendAsync();
return; return;
} }
await _mss.Response(channel) await _mss.Response(channel)
.Text(st) .Text(st)
.Sanitize(false) .Sanitize(false)
.SendAsync(); .SendAsync();
} }
private static string GetPhToken(string name) => $"%event.{name}%";
public async Task EnableAsync( public async Task EnableAsync(
ulong guildId, ulong guildId,
ulong channelId, ulong channelId,
@ -158,23 +175,23 @@ public sealed class NotifyService : IReadyExecutor, INotifySubscriber, IEService
{ {
await using var uow = _db.GetDbContext(); await using var uow = _db.GetDbContext();
await uow.GetTable<Notify>() await uow.GetTable<Notify>()
.InsertOrUpdateAsync(() => new() .InsertOrUpdateAsync(() => new()
{ {
GuildId = guildId, GuildId = guildId,
ChannelId = channelId, ChannelId = channelId,
Type = nType, Type = nType,
Message = message, Message = message,
}, },
(_) => new() (_) => new()
{ {
Message = message, Message = message,
ChannelId = channelId ChannelId = channelId
}, },
() => new() () => new()
{ {
GuildId = guildId, GuildId = guildId,
Type = nType Type = nType
}); });
var eventDict = _events.GetOrAdd(nType, _ => new()); var eventDict = _events.GetOrAdd(nType, _ => new());
eventDict[guildId] = new() eventDict[guildId] = new()
@ -190,8 +207,8 @@ public sealed class NotifyService : IReadyExecutor, INotifySubscriber, IEService
{ {
await using var uow = _db.GetDbContext(); await using var uow = _db.GetDbContext();
var deleted = await uow.GetTable<Notify>() var deleted = await uow.GetTable<Notify>()
.Where(x => x.GuildId == guildId && x.Type == nType) .Where(x => x.GuildId == guildId && x.Type == nType)
.DeleteAsync(); .DeleteAsync();
if (deleted == 0) if (deleted == 0)
return; return;
@ -208,11 +225,11 @@ public sealed class NotifyService : IReadyExecutor, INotifySubscriber, IEService
await using var ctx = _db.GetDbContext(); await using var ctx = _db.GetDbContext();
var list = await ctx.GetTable<Notify>() var list = await ctx.GetTable<Notify>()
.Where(x => x.GuildId == guildId) .Where(x => x.GuildId == guildId)
.OrderBy(x => x.Type) .OrderBy(x => x.Type)
.Skip(page * 10) .Skip(page * 10)
.Take(10) .Take(10)
.ToListAsyncLinqToDB(); .ToListAsyncLinqToDB();
return list; return list;
} }
@ -221,7 +238,17 @@ public sealed class NotifyService : IReadyExecutor, INotifySubscriber, IEService
{ {
await using var ctx = _db.GetDbContext(); await using var ctx = _db.GetDbContext();
return await ctx.GetTable<Notify>() return await ctx.GetTable<Notify>()
.Where(x => x.GuildId == guildId && x.Type == nType) .Where(x => x.GuildId == guildId && x.Type == nType)
.FirstOrDefaultAsyncLinqToDB(); .FirstOrDefaultAsyncLinqToDB();
} }
}
// 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];
}

View file

@ -1562,6 +1562,10 @@ notifyclear:
- notifyremove - notifyremove
- notifyrm - notifyrm
- notifclr - notifclr
notifyphs:
- notifyphs
- notifyph
- notifyplaceholders
winlb: winlb:
- winlb - winlb
- wins - wins

View file

@ -4911,6 +4911,14 @@ notifyclear:
params: params:
- event: - event:
desc: "The notify event to clear." 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: winlb:
desc: |- desc: |-
Shows the biggest wins leaderboard Shows the biggest wins leaderboard

View file

@ -1158,6 +1158,7 @@
"notify_desc_addrolerew": "Triggers when a user gets a role as a reward for reaching a level (xprew).", "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_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_desc_not_found": "No description found for this notify event. Please report this.",
"notify_placeholders": "Placeholders for '{0}' notify event",
"winlb": "Biggest Wins Leaderboard", "winlb": "Biggest Wins Leaderboard",
"no_banner": "No banner set.", "no_banner": "No banner set.",
"fish_nothing": "You caught nothing, try again.", "fish_nothing": "You caught nothing, try again.",