From f8eb58509352dd3718f7232db3aa2a24fc66efe5 Mon Sep 17 00:00:00 2001 From: Toastie Date: Sun, 8 Dec 2024 18:51:31 +1300 Subject: [PATCH] Added .notify and migrations, added levelup and protection events for notify, removed xpnotify completely --- src/EllieBot/.editorconfig | 2 + src/EllieBot/Db/EllieContext.cs | 2 +- src/EllieBot/Db/Models/Notify.cs | 8 +- ...208035947_awarded-xp-and-notify-removed.cs | 107 ---------- ...644_awardedxp-temprole-notify.Designer.cs} | 16 +- ...0241208053644_awardedxp-temprole-notify.cs | 46 ++++ .../PostgreSqlContextModelSnapshot.cs | 12 +- ...208035845_awarded-xp-and-notify-removed.cs | 106 --------- ...549_awardedxp-temprole-notify.Designer.cs} | 12 +- ...0241208053549_awardedxp-temprole-notify.cs | 46 ++++ .../Sqlite/EllieSqliteContextModelSnapshot.cs | 8 +- .../Administration/Notify/INotifyModel.cs | 23 ++ .../Notify/INotifySubscriber.cs | 7 + .../Notify/Models/LevelUpNotifyModel.cs | 44 ++++ .../Administration/Notify/NotifyCommands.cs | 82 +------ .../Administration/Notify/NotifyKeys.cs | 6 + .../Administration/Notify/NotifyService.cs | 202 ++++++++++++++++++ .../Protection/ProtectionService.cs | 44 +++- src/EllieBot/Modules/Xp/BuyResult.cs | 11 + src/EllieBot/Modules/Xp/Xp.cs | 31 --- src/EllieBot/Modules/Xp/XpService.cs | 133 +++--------- src/EllieBot/_common/Services/IUserService.cs | 8 + src/EllieBot/_common/Services/UserService.cs | 24 +++ src/EllieBot/data/aliases.yml | 5 +- .../data/strings/commands/commands.en-US.yml | 12 +- 25 files changed, 541 insertions(+), 456 deletions(-) delete mode 100644 src/EllieBot/Migrations/PostgreSql/20241208035947_awarded-xp-and-notify-removed.cs rename src/EllieBot/Migrations/PostgreSql/{20241208035947_awarded-xp-and-notify-removed.Designer.cs => 20241208053644_awardedxp-temprole-notify.Designer.cs} (99%) create mode 100644 src/EllieBot/Migrations/PostgreSql/20241208053644_awardedxp-temprole-notify.cs delete mode 100644 src/EllieBot/Migrations/Sqlite/20241208035845_awarded-xp-and-notify-removed.cs rename src/EllieBot/Migrations/Sqlite/{20241208035845_awarded-xp-and-notify-removed.Designer.cs => 20241208053549_awardedxp-temprole-notify.Designer.cs} (99%) create mode 100644 src/EllieBot/Migrations/Sqlite/20241208053549_awardedxp-temprole-notify.cs create mode 100644 src/EllieBot/Modules/Administration/Notify/INotifyModel.cs create mode 100644 src/EllieBot/Modules/Administration/Notify/INotifySubscriber.cs create mode 100644 src/EllieBot/Modules/Administration/Notify/Models/LevelUpNotifyModel.cs create mode 100644 src/EllieBot/Modules/Administration/Notify/NotifyKeys.cs create mode 100644 src/EllieBot/Modules/Administration/Notify/NotifyService.cs create mode 100644 src/EllieBot/Modules/Xp/BuyResult.cs create mode 100644 src/EllieBot/_common/Services/IUserService.cs create mode 100644 src/EllieBot/_common/Services/UserService.cs diff --git a/src/EllieBot/.editorconfig b/src/EllieBot/.editorconfig index 304861d..9814958 100644 --- a/src/EllieBot/.editorconfig +++ b/src/EllieBot/.editorconfig @@ -356,3 +356,5 @@ resharper_arrange_redundant_parentheses_highlighting = hint # IDE0011: Add braces dotnet_diagnostic.IDE0011.severity = warning + +resharper_arrange_type_member_modifiers_highlighting = hint \ No newline at end of file diff --git a/src/EllieBot/Db/EllieContext.cs b/src/EllieBot/Db/EllieContext.cs index a009eaa..6b86daf 100644 --- a/src/EllieBot/Db/EllieContext.cs +++ b/src/EllieBot/Db/EllieContext.cs @@ -81,7 +81,7 @@ public abstract class EllieContext : DbContext e.HasAlternateKey(x => new { x.GuildId, - x.Event + Event = x.Type }); }); diff --git a/src/EllieBot/Db/Models/Notify.cs b/src/EllieBot/Db/Models/Notify.cs index 77c2de0..90d30d3 100644 --- a/src/EllieBot/Db/Models/Notify.cs +++ b/src/EllieBot/Db/Models/Notify.cs @@ -6,15 +6,17 @@ public class Notify { [Key] public int Id { get; set; } + public ulong GuildId { get; set; } public ulong ChannelId { get; set; } - public NotifyEvent Event { get; set; } + public NotifyType Type { get; set; } [MaxLength(10_000)] public string Message { get; set; } = string.Empty; } -public enum NotifyEvent +public enum NotifyType { - UserLevelUp + LevelUp = 0, + Protection = 1, Prot = 1, } \ No newline at end of file diff --git a/src/EllieBot/Migrations/PostgreSql/20241208035947_awarded-xp-and-notify-removed.cs b/src/EllieBot/Migrations/PostgreSql/20241208035947_awarded-xp-and-notify-removed.cs deleted file mode 100644 index a292026..0000000 --- a/src/EllieBot/Migrations/PostgreSql/20241208035947_awarded-xp-and-notify-removed.cs +++ /dev/null @@ -1,107 +0,0 @@ -using System; -using Microsoft.EntityFrameworkCore.Migrations; -using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; - -#nullable disable - -namespace EllieBot.Migrations.PostgreSql -{ - /// - public partial class awardedxpandnotifyremoved : Migration - { - /// - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropIndex( - name: "ix_userxpstats_awardedxp", - table: "userxpstats"); - - migrationBuilder.DropColumn( - name: "awardedxp", - table: "userxpstats"); - - migrationBuilder.DropColumn( - name: "notifyonlevelup", - table: "userxpstats"); - - migrationBuilder.DropColumn( - name: "dateadded", - table: "sargroup"); - - migrationBuilder.CreateTable( - name: "notify", - columns: table => new - { - id = table.Column(type: "integer", nullable: false) - .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), - guildid = table.Column(type: "numeric(20,0)", nullable: false), - channelid = table.Column(type: "numeric(20,0)", nullable: false), - @event = table.Column(name: "event", type: "integer", nullable: false), - message = table.Column(type: "character varying(10000)", maxLength: 10000, nullable: false) - }, - constraints: table => - { - table.PrimaryKey("pk_notify", x => x.id); - table.UniqueConstraint("ak_notify_guildid_event", x => new { x.guildid, x.@event }); - }); - - migrationBuilder.CreateTable( - name: "temprole", - columns: table => new - { - id = table.Column(type: "integer", nullable: false) - .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), - guildid = table.Column(type: "numeric(20,0)", nullable: false), - remove = table.Column(type: "boolean", nullable: false), - roleid = table.Column(type: "numeric(20,0)", nullable: false), - userid = table.Column(type: "numeric(20,0)", nullable: false), - expiresat = table.Column(type: "timestamp without time zone", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("pk_temprole", x => x.id); - table.UniqueConstraint("ak_temprole_guildid_userid_roleid", x => new { x.guildid, x.userid, x.roleid }); - }); - - migrationBuilder.CreateIndex( - name: "ix_temprole_expiresat", - table: "temprole", - column: "expiresat"); - } - - /// - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropTable( - name: "notify"); - - migrationBuilder.DropTable( - name: "temprole"); - - migrationBuilder.AddColumn( - name: "awardedxp", - table: "userxpstats", - type: "bigint", - nullable: false, - defaultValue: 0L); - - migrationBuilder.AddColumn( - name: "notifyonlevelup", - table: "userxpstats", - type: "integer", - nullable: false, - defaultValue: 0); - - migrationBuilder.AddColumn( - name: "dateadded", - table: "sargroup", - type: "timestamp without time zone", - nullable: true); - - migrationBuilder.CreateIndex( - name: "ix_userxpstats_awardedxp", - table: "userxpstats", - column: "awardedxp"); - } - } -} diff --git a/src/EllieBot/Migrations/PostgreSql/20241208035947_awarded-xp-and-notify-removed.Designer.cs b/src/EllieBot/Migrations/PostgreSql/20241208053644_awardedxp-temprole-notify.Designer.cs similarity index 99% rename from src/EllieBot/Migrations/PostgreSql/20241208035947_awarded-xp-and-notify-removed.Designer.cs rename to src/EllieBot/Migrations/PostgreSql/20241208053644_awardedxp-temprole-notify.Designer.cs index 1a47e18..b56d51a 100644 --- a/src/EllieBot/Migrations/PostgreSql/20241208035947_awarded-xp-and-notify-removed.Designer.cs +++ b/src/EllieBot/Migrations/PostgreSql/20241208053644_awardedxp-temprole-notify.Designer.cs @@ -12,8 +12,8 @@ using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; namespace EllieBot.Migrations.PostgreSql { [DbContext(typeof(PostgreSqlContext))] - [Migration("20241208035947_awarded-xp-and-notify-removed")] - partial class awardedxpandnotifyremoved + [Migration("20241208053644_awardedxp-temprole-notify")] + partial class awardedxptemprolenotify { /// protected override void BuildTargetModel(ModelBuilder modelBuilder) @@ -1833,10 +1833,6 @@ namespace EllieBot.Migrations.PostgreSql .HasColumnType("numeric(20,0)") .HasColumnName("channelid"); - b.Property("Event") - .HasColumnType("integer") - .HasColumnName("event"); - b.Property("GuildId") .HasColumnType("numeric(20,0)") .HasColumnName("guildid"); @@ -1847,11 +1843,15 @@ namespace EllieBot.Migrations.PostgreSql .HasColumnType("character varying(10000)") .HasColumnName("message"); + b.Property("Type") + .HasColumnType("integer") + .HasColumnName("type"); + b.HasKey("Id") .HasName("pk_notify"); - b.HasAlternateKey("GuildId", "Event") - .HasName("ak_notify_guildid_event"); + b.HasAlternateKey("GuildId", "Type") + .HasName("ak_notify_guildid_type"); b.ToTable("notify", (string)null); }); diff --git a/src/EllieBot/Migrations/PostgreSql/20241208053644_awardedxp-temprole-notify.cs b/src/EllieBot/Migrations/PostgreSql/20241208053644_awardedxp-temprole-notify.cs new file mode 100644 index 0000000..a76cc4a --- /dev/null +++ b/src/EllieBot/Migrations/PostgreSql/20241208053644_awardedxp-temprole-notify.cs @@ -0,0 +1,46 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace EllieBot.Migrations.PostgreSql +{ + /// + public partial class awardedxptemprolenotify : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropUniqueConstraint( + name: "ak_notify_guildid_event", + table: "notify"); + + migrationBuilder.RenameColumn( + name: "event", + table: "notify", + newName: "type"); + + migrationBuilder.AddUniqueConstraint( + name: "ak_notify_guildid_type", + table: "notify", + columns: new[] { "guildid", "type" }); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropUniqueConstraint( + name: "ak_notify_guildid_type", + table: "notify"); + + migrationBuilder.RenameColumn( + name: "type", + table: "notify", + newName: "event"); + + migrationBuilder.AddUniqueConstraint( + name: "ak_notify_guildid_event", + table: "notify", + columns: new[] { "guildid", "event" }); + } + } +} diff --git a/src/EllieBot/Migrations/PostgreSql/PostgreSqlContextModelSnapshot.cs b/src/EllieBot/Migrations/PostgreSql/PostgreSqlContextModelSnapshot.cs index bb98853..e691bbc 100644 --- a/src/EllieBot/Migrations/PostgreSql/PostgreSqlContextModelSnapshot.cs +++ b/src/EllieBot/Migrations/PostgreSql/PostgreSqlContextModelSnapshot.cs @@ -1830,10 +1830,6 @@ namespace EllieBot.Migrations.PostgreSql .HasColumnType("numeric(20,0)") .HasColumnName("channelid"); - b.Property("Event") - .HasColumnType("integer") - .HasColumnName("event"); - b.Property("GuildId") .HasColumnType("numeric(20,0)") .HasColumnName("guildid"); @@ -1844,11 +1840,15 @@ namespace EllieBot.Migrations.PostgreSql .HasColumnType("character varying(10000)") .HasColumnName("message"); + b.Property("Type") + .HasColumnType("integer") + .HasColumnName("type"); + b.HasKey("Id") .HasName("pk_notify"); - b.HasAlternateKey("GuildId", "Event") - .HasName("ak_notify_guildid_event"); + b.HasAlternateKey("GuildId", "Type") + .HasName("ak_notify_guildid_type"); b.ToTable("notify", (string)null); }); diff --git a/src/EllieBot/Migrations/Sqlite/20241208035845_awarded-xp-and-notify-removed.cs b/src/EllieBot/Migrations/Sqlite/20241208035845_awarded-xp-and-notify-removed.cs deleted file mode 100644 index af5984c..0000000 --- a/src/EllieBot/Migrations/Sqlite/20241208035845_awarded-xp-and-notify-removed.cs +++ /dev/null @@ -1,106 +0,0 @@ -using System; -using Microsoft.EntityFrameworkCore.Migrations; - -#nullable disable - -namespace EllieBot.Migrations -{ - /// - public partial class awardedxpandnotifyremoved : Migration - { - /// - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropIndex( - name: "IX_UserXpStats_AwardedXp", - table: "UserXpStats"); - - migrationBuilder.DropColumn( - name: "AwardedXp", - table: "UserXpStats"); - - migrationBuilder.DropColumn( - name: "NotifyOnLevelUp", - table: "UserXpStats"); - - migrationBuilder.DropColumn( - name: "DateAdded", - table: "SarGroup"); - - migrationBuilder.CreateTable( - name: "Notify", - columns: table => new - { - Id = table.Column(type: "INTEGER", nullable: false) - .Annotation("Sqlite:Autoincrement", true), - GuildId = table.Column(type: "INTEGER", nullable: false), - ChannelId = table.Column(type: "INTEGER", nullable: false), - Event = table.Column(type: "INTEGER", nullable: false), - Message = table.Column(type: "TEXT", maxLength: 10000, nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_Notify", x => x.Id); - table.UniqueConstraint("AK_Notify_GuildId_Event", x => new { x.GuildId, x.Event }); - }); - - migrationBuilder.CreateTable( - name: "TempRole", - columns: table => new - { - Id = table.Column(type: "INTEGER", nullable: false) - .Annotation("Sqlite:Autoincrement", true), - GuildId = table.Column(type: "INTEGER", nullable: false), - Remove = table.Column(type: "INTEGER", nullable: false), - RoleId = table.Column(type: "INTEGER", nullable: false), - UserId = table.Column(type: "INTEGER", nullable: false), - ExpiresAt = table.Column(type: "TEXT", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_TempRole", x => x.Id); - table.UniqueConstraint("AK_TempRole_GuildId_UserId_RoleId", x => new { x.GuildId, x.UserId, x.RoleId }); - }); - - migrationBuilder.CreateIndex( - name: "IX_TempRole_ExpiresAt", - table: "TempRole", - column: "ExpiresAt"); - } - - /// - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropTable( - name: "Notify"); - - migrationBuilder.DropTable( - name: "TempRole"); - - migrationBuilder.AddColumn( - name: "AwardedXp", - table: "UserXpStats", - type: "INTEGER", - nullable: false, - defaultValue: 0L); - - migrationBuilder.AddColumn( - name: "NotifyOnLevelUp", - table: "UserXpStats", - type: "INTEGER", - nullable: false, - defaultValue: 0); - - migrationBuilder.AddColumn( - name: "DateAdded", - table: "SarGroup", - type: "TEXT", - nullable: true); - - migrationBuilder.CreateIndex( - name: "IX_UserXpStats_AwardedXp", - table: "UserXpStats", - column: "AwardedXp"); - } - } -} diff --git a/src/EllieBot/Migrations/Sqlite/20241208035845_awarded-xp-and-notify-removed.Designer.cs b/src/EllieBot/Migrations/Sqlite/20241208053549_awardedxp-temprole-notify.Designer.cs similarity index 99% rename from src/EllieBot/Migrations/Sqlite/20241208035845_awarded-xp-and-notify-removed.Designer.cs rename to src/EllieBot/Migrations/Sqlite/20241208053549_awardedxp-temprole-notify.Designer.cs index 5424538..c9b5e7e 100644 --- a/src/EllieBot/Migrations/Sqlite/20241208035845_awarded-xp-and-notify-removed.Designer.cs +++ b/src/EllieBot/Migrations/Sqlite/20241208053549_awardedxp-temprole-notify.Designer.cs @@ -11,8 +11,8 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion; namespace EllieBot.Migrations { [DbContext(typeof(SqliteContext))] - [Migration("20241208035845_awarded-xp-and-notify-removed")] - partial class awardedxpandnotifyremoved + [Migration("20241208053549_awardedxp-temprole-notify")] + partial class awardedxptemprolenotify { /// protected override void BuildTargetModel(ModelBuilder modelBuilder) @@ -1368,9 +1368,6 @@ namespace EllieBot.Migrations b.Property("ChannelId") .HasColumnType("INTEGER"); - b.Property("Event") - .HasColumnType("INTEGER"); - b.Property("GuildId") .HasColumnType("INTEGER"); @@ -1379,9 +1376,12 @@ namespace EllieBot.Migrations .HasMaxLength(10000) .HasColumnType("TEXT"); + b.Property("Type") + .HasColumnType("INTEGER"); + b.HasKey("Id"); - b.HasAlternateKey("GuildId", "Event"); + b.HasAlternateKey("GuildId", "Type"); b.ToTable("Notify"); }); diff --git a/src/EllieBot/Migrations/Sqlite/20241208053549_awardedxp-temprole-notify.cs b/src/EllieBot/Migrations/Sqlite/20241208053549_awardedxp-temprole-notify.cs new file mode 100644 index 0000000..8281303 --- /dev/null +++ b/src/EllieBot/Migrations/Sqlite/20241208053549_awardedxp-temprole-notify.cs @@ -0,0 +1,46 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace EllieBot.Migrations +{ + /// + public partial class awardedxptemprolenotify : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropUniqueConstraint( + name: "AK_Notify_GuildId_Event", + table: "Notify"); + + migrationBuilder.RenameColumn( + name: "Event", + table: "Notify", + newName: "Type"); + + migrationBuilder.AddUniqueConstraint( + name: "AK_Notify_GuildId_Type", + table: "Notify", + columns: new[] { "GuildId", "Type" }); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropUniqueConstraint( + name: "AK_Notify_GuildId_Type", + table: "Notify"); + + migrationBuilder.RenameColumn( + name: "Type", + table: "Notify", + newName: "Event"); + + migrationBuilder.AddUniqueConstraint( + name: "AK_Notify_GuildId_Event", + table: "Notify", + columns: new[] { "GuildId", "Event" }); + } + } +} diff --git a/src/EllieBot/Migrations/Sqlite/EllieSqliteContextModelSnapshot.cs b/src/EllieBot/Migrations/Sqlite/EllieSqliteContextModelSnapshot.cs index 4f03454..657bd0a 100644 --- a/src/EllieBot/Migrations/Sqlite/EllieSqliteContextModelSnapshot.cs +++ b/src/EllieBot/Migrations/Sqlite/EllieSqliteContextModelSnapshot.cs @@ -1365,9 +1365,6 @@ namespace EllieBot.Migrations b.Property("ChannelId") .HasColumnType("INTEGER"); - b.Property("Event") - .HasColumnType("INTEGER"); - b.Property("GuildId") .HasColumnType("INTEGER"); @@ -1376,9 +1373,12 @@ namespace EllieBot.Migrations .HasMaxLength(10000) .HasColumnType("TEXT"); + b.Property("Type") + .HasColumnType("INTEGER"); + b.HasKey("Id"); - b.HasAlternateKey("GuildId", "Event"); + b.HasAlternateKey("GuildId", "Type"); b.ToTable("Notify"); }); diff --git a/src/EllieBot/Modules/Administration/Notify/INotifyModel.cs b/src/EllieBot/Modules/Administration/Notify/INotifyModel.cs new file mode 100644 index 0000000..9fb1d80 --- /dev/null +++ b/src/EllieBot/Modules/Administration/Notify/INotifyModel.cs @@ -0,0 +1,23 @@ +using EllieBot.Db.Models; +using System.Collections; + +namespace EllieBot.Modules.Administration; + +public interface INotifyModel +{ + static abstract string KeyName { get; } + static abstract NotifyType NotifyType { get; } + IReadOnlyDictionary> GetReplacements(); + + public virtual bool TryGetGuildId(out ulong guildId) + { + guildId = 0; + return false; + } + + public virtual bool TryGetUserId(out ulong userId) + { + userId = 0; + return false; + } +} \ No newline at end of file diff --git a/src/EllieBot/Modules/Administration/Notify/INotifySubscriber.cs b/src/EllieBot/Modules/Administration/Notify/INotifySubscriber.cs new file mode 100644 index 0000000..f0a8731 --- /dev/null +++ b/src/EllieBot/Modules/Administration/Notify/INotifySubscriber.cs @@ -0,0 +1,7 @@ +namespace EllieBot.Modules.Administration; + +public interface INotifySubscriber +{ + Task NotifyAsync(T data, bool isShardLocal = false) + where T : struct, INotifyModel; +} \ 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 new file mode 100644 index 0000000..3495c09 --- /dev/null +++ b/src/EllieBot/Modules/Administration/Notify/Models/LevelUpNotifyModel.cs @@ -0,0 +1,44 @@ +using EllieBot.Db.Models; + +namespace EllieBot.Modules.Administration; + +public record struct LevelUpNotifyModel( + ulong GuildId, + ulong ChannelId, + ulong UserId, + long Level) : INotifyModel +{ + public static string KeyName + => "notify.levelup"; + + public static NotifyType NotifyType + => NotifyType.LevelUp; + + public IReadOnlyDictionary> GetReplacements() + { + var data = this; + return new Dictionary>() + { + { "%event.level%", g => data.Level.ToString() }, + }; + } + + public bool TryGetGuildId(out ulong guildId) + { + guildId = GuildId; + return true; + } + + public bool TryGetUserId(out ulong userId) + { + userId = UserId; + return true; + } +} + +public static class INotifyModelExtensions +{ + public static TypedKey GetTypedKey(this T model) + where T : struct, INotifyModel + => new(T.KeyName); +} \ 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 2b33f34..592573a 100644 --- a/src/EllieBot/Modules/Administration/Notify/NotifyCommands.cs +++ b/src/EllieBot/Modules/Administration/Notify/NotifyCommands.cs @@ -1,92 +1,24 @@ -using LinqToDB; -using LinqToDB.EntityFrameworkCore; -using EllieBot.Common.ModuleBehaviors; -using EllieBot.Db.Models; +using EllieBot.Db.Models; namespace EllieBot.Modules.Administration; -public sealed class NotifyService : IReadyExecutor, IEService -{ - private readonly DbService _db; - private readonly IMessageSenderService _mss; - private readonly DiscordSocketClient _client; - private readonly IBotCreds _creds; - - public NotifyService( - DbService db, - IMessageSenderService mss, - DiscordSocketClient client, - IBotCreds creds) - { - _db = db; - _mss = mss; - _client = client; - _creds = creds; - } - - public async Task OnReadyAsync() - { - // .Where(x => Linq2DbExpressions.GuildOnShard(guildId, - // _creds.TotalShards, - // _client.ShardId)) - } - - public async Task EnableAsync( - ulong guildId, - ulong channelId, - NotifyEvent nEvent, - string message) - { - await using var uow = _db.GetDbContext(); - await uow.GetTable() - .InsertOrUpdateAsync(() => new() - { - GuildId = guildId, - ChannelId = channelId, - Event = nEvent, - Message = message, - }, - (_) => new() - { - Message = message, - ChannelId = channelId - }, - () => new() - { - GuildId = guildId, - Event = nEvent - }); - } - - public async Task DisableAsync(ulong guildId, NotifyEvent nEvent) - { - await using var uow = _db.GetDbContext(); - var deleted = await uow.GetTable() - .Where(x => x.GuildId == guildId && x.Event == nEvent) - .DeleteAsync(); - - if (deleted > 0) - return; - } -} - public partial class Administration { public class NotifyCommands : EllieModule { [Cmd] [OwnerOnly] - public async Task Notify(NotifyEvent nEvent, [Leftover] string message = null) + public async Task Notify(NotifyType nType, [Leftover] string? message = null) { if (string.IsNullOrWhiteSpace(message)) { - await _service.DisableAsync(ctx.Guild.Id, nEvent); - await Response().Confirm(strs.notify_off(nEvent)).SendAsync(); + await _service.DisableAsync(ctx.Guild.Id, nType); + await Response().Confirm(strs.notify_off(nType)).SendAsync(); return; } - await _service.EnableAsync(ctx.Guild.Id, ctx.Channel.Id, nEvent, message); - await Response().Confirm(strs.notify_on(nEvent.ToString())).SendAsync(); + await _service.EnableAsync(ctx.Guild.Id, ctx.Channel.Id, nType, message); + await Response().Confirm(strs.notify_on(nType.ToString())).SendAsync(); } } -} +} \ No newline at end of file diff --git a/src/EllieBot/Modules/Administration/Notify/NotifyKeys.cs b/src/EllieBot/Modules/Administration/Notify/NotifyKeys.cs new file mode 100644 index 0000000..48bfd3c --- /dev/null +++ b/src/EllieBot/Modules/Administration/Notify/NotifyKeys.cs @@ -0,0 +1,6 @@ +namespace EllieBot.Modules.Administration; + +public static class NotifyKeys +{ + public static TypedKey LevelUp { get; } = new("notify:levelup"); +} \ No newline at end of file diff --git a/src/EllieBot/Modules/Administration/Notify/NotifyService.cs b/src/EllieBot/Modules/Administration/Notify/NotifyService.cs new file mode 100644 index 0000000..892dde9 --- /dev/null +++ b/src/EllieBot/Modules/Administration/Notify/NotifyService.cs @@ -0,0 +1,202 @@ +using LinqToDB; +using LinqToDB.EntityFrameworkCore; +using EllieBot.Common.ModuleBehaviors; +using EllieBot.Db.Models; + +namespace EllieBot.Modules.Administration; + +public sealed class NotifyService : IReadyExecutor, INotifySubscriber, IEService +{ + private readonly DbService _db; + private readonly IMessageSenderService _mss; + private readonly DiscordSocketClient _client; + private readonly IBotCreds _creds; + private readonly IReplacementService _repSvc; + private readonly IPubSub _pubSub; + private ConcurrentDictionary> _events = new(); + + public NotifyService( + DbService db, + IMessageSenderService mss, + DiscordSocketClient client, + IBotCreds creds, + IReplacementService repSvc, + IPubSub pubSub) + { + _db = db; + _mss = mss; + _client = client; + _creds = creds; + _repSvc = repSvc; + _pubSub = pubSub; + } + + public async Task OnReadyAsync() + { + await using var uow = _db.GetDbContext(); + _events = (await uow.GetTable() + .Where(x => Linq2DbExpressions.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(); + } + + private async Task SubscribeToEvent() + where T : struct, INotifyModel + { + await _pubSub.Sub(new TypedKey(T.KeyName), async (model) => await OnEvent(model)); + } + + public async Task NotifyAsync(T data, bool isShardLocal = false) + where T : struct, INotifyModel + { + try + { + if (isShardLocal) + { + await OnEvent(data); + return; + } + + await _pubSub.Pub(data.GetTypedKey(), data); + } + catch (Exception ex) + { + Log.Warning(ex, + "Unknown error occurred while trying to triger {NotifyEvent} for {NotifyModel}", + T.KeyName, + data); + } + } + + private async Task OnEvent(T model) + where T : struct, INotifyModel + { + if (_events.TryGetValue(T.NotifyType, out var subs)) + { + if (model.TryGetGuildId(out var gid)) + { + if (!subs.TryGetValue(gid, out var conf)) + return; + + await HandleNotifyEvent(conf, model); + return; + } + + foreach (var key in subs.Keys.ToArray()) + { + if (subs.TryGetValue(key, out var notif)) + { + try + { + 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); + } + } + } + } + + private async Task HandleNotifyEvent(Notify conf, INotifyModel model) + { + var guild = _client.GetGuild(conf.GuildId); + var channel = guild?.GetTextChannel(conf.ChannelId); + + if (guild is null || channel is null) + return; + + IUser? user = null; + if (model.TryGetUserId(out var userId)) + { + user = guild.GetUser(userId) ?? _client.GetUser(userId); + } + + var rctx = new ReplacementContext(guild: guild, channel: channel, user: user); + + var st = SmartText.CreateFrom(conf.Message); + foreach (var modelRep in model.GetReplacements()) + { + rctx.WithOverride(modelRep.Key, () => modelRep.Value(guild)); + } + + st = await _repSvc.ReplaceAsync(st, rctx); + if (st is SmartPlainText spt) + { + await _mss.Response(channel) + .Confirm(spt.Text) + .SendAsync(); + return; + } + + await _mss.Response(channel) + .Text(st) + .SendAsync(); + } + + public async Task EnableAsync( + ulong guildId, + ulong channelId, + NotifyType nType, + string message) + { + await using var uow = _db.GetDbContext(); + await uow.GetTable() + .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() + { + GuildId = guildId, + ChannelId = channelId, + Type = nType, + Message = message + }; + } + + public async Task DisableAsync(ulong guildId, NotifyType nType) + { + await using var uow = _db.GetDbContext(); + var deleted = await uow.GetTable() + .Where(x => x.GuildId == guildId && x.Type == nType) + .DeleteAsync(); + + if (deleted == 0) + return; + + if (!_events.TryGetValue(nType, out var guildsDict)) + return; + + guildsDict.TryRemove(guildId, out _); + } +} diff --git a/src/EllieBot/Modules/Administration/Protection/ProtectionService.cs b/src/EllieBot/Modules/Administration/Protection/ProtectionService.cs index c72a941..2d0ba0e 100644 --- a/src/EllieBot/Modules/Administration/Protection/ProtectionService.cs +++ b/src/EllieBot/Modules/Administration/Protection/ProtectionService.cs @@ -5,6 +5,36 @@ using System.Threading.Channels; namespace EllieBot.Modules.Administration.Services; +public record struct ProtectionNotifyModel(ulong GuildId, ProtectionType ProtType, ulong UserId) : INotifyModel +{ + public static string KeyName + => "notify.protection"; + + public static NotifyType NotifyType + => NotifyType.Protection; + + public IReadOnlyDictionary> GetReplacements() + { + var data = this; + return new Dictionary>() + { + { "%event.type%", g => data.ProtType.ToString() }, + }; + } + + public bool TryGetUserId(out ulong userId) + { + userId = UserId; + return true; + } + + public bool TryGetGuildId(out ulong guildId) + { + guildId = GuildId; + return true; + } +} + public class ProtectionService : IEService { public event Func OnAntiProtectionTriggered = delegate @@ -22,6 +52,7 @@ public class ProtectionService : IEService private readonly MuteService _mute; private readonly DbService _db; private readonly UserPunishService _punishService; + private readonly INotifySubscriber _notifySub; private readonly Channel _punishUserQueue = Channel.CreateUnbounded(new() @@ -35,12 +66,14 @@ public class ProtectionService : IEService IBot bot, MuteService mute, DbService db, - UserPunishService punishService) + UserPunishService punishService, + INotifySubscriber notifySub) { _client = client; _mute = mute; _db = db; _punishService = punishService; + _notifySub = notifySub; var ids = client.GetGuildIds(); using (var uow = db.GetDbContext()) @@ -175,6 +208,9 @@ public class ProtectionService : IEService alts.RoleId, user); + await _notifySub.NotifyAsync(new ProtectionNotifyModel(user.Guild.Id, + ProtectionType.Alting, + user.Id)); return; } } @@ -194,6 +230,8 @@ public class ProtectionService : IEService var settings = stats.AntiRaidSettings; await PunishUsers(settings.Action, ProtectionType.Raiding, settings.PunishDuration, null, users); + await _notifySub.NotifyAsync( + new ProtectionNotifyModel(user.Guild.Id, ProtectionType.Raiding, users[0].Id)); } await Task.Delay(1000 * stats.AntiRaidSettings.Seconds); @@ -246,6 +284,10 @@ public class ProtectionService : IEService settings.MuteTime, settings.RoleId, (IGuildUser)msg.Author); + + await _notifySub.NotifyAsync(new ProtectionNotifyModel(channel.GuildId, + ProtectionType.Spamming, + msg.Author.Id)); } } } diff --git a/src/EllieBot/Modules/Xp/BuyResult.cs b/src/EllieBot/Modules/Xp/BuyResult.cs new file mode 100644 index 0000000..3c4c464 --- /dev/null +++ b/src/EllieBot/Modules/Xp/BuyResult.cs @@ -0,0 +1,11 @@ +namespace EllieBot.Modules.Xp.Services; + +public enum BuyResult +{ + Success, + XpShopDisabled, + AlreadyOwned, + InsufficientFunds, + UnknownItem, + InsufficientPatronTier, +} \ No newline at end of file diff --git a/src/EllieBot/Modules/Xp/Xp.cs b/src/EllieBot/Modules/Xp/Xp.cs index fa6b534..a386b91 100644 --- a/src/EllieBot/Modules/Xp/Xp.cs +++ b/src/EllieBot/Modules/Xp/Xp.cs @@ -51,26 +51,6 @@ public partial class Xp : EllieModule } } - [Cmd] - [RequireContext(ContextType.Guild)] - public async Task XpNotify() - { - var globalSetting = _service.GetNotificationType(ctx.User); - - var embed = CreateEmbed() - .WithOkColor() - .AddField(GetText(strs.xpn_setting_global), GetNotifLocationString(globalSetting)); - - await Response().Embed(embed).SendAsync(); - } - - [Cmd] - public async Task XpNotify(XpNotificationLocation type) - { - await _service.ChangeNotificationType(ctx.User, type); - await ctx.OkAsync(); - } - [Cmd] [RequireContext(ContextType.Guild)] [UserPerm(GuildPerm.Administrator)] @@ -615,15 +595,4 @@ public partial class Xp : EllieModule await _service.UseShopItemAsync(ctx.User.Id, type, key); } } - - private string GetNotifLocationString(XpNotificationLocation loc) - { - if (loc == XpNotificationLocation.Channel) - return GetText(strs.xpn_notif_channel); - - if (loc == XpNotificationLocation.Dm) - return GetText(strs.xpn_notif_dm); - - return GetText(strs.xpn_notif_disabled); - } } \ No newline at end of file diff --git a/src/EllieBot/Modules/Xp/XpService.cs b/src/EllieBot/Modules/Xp/XpService.cs index 7de2285..afff7d8 100644 --- a/src/EllieBot/Modules/Xp/XpService.cs +++ b/src/EllieBot/Modules/Xp/XpService.cs @@ -13,6 +13,7 @@ using SixLabors.ImageSharp.Processing; using System.Threading.Channels; using LinqToDB.EntityFrameworkCore; using LinqToDB.Tools; +using EllieBot.Modules.Administration; using EllieBot.Modules.Patronage; using Color = SixLabors.ImageSharp.Color; using Exception = System.Exception; @@ -20,31 +21,6 @@ using Image = SixLabors.ImageSharp.Image; namespace EllieBot.Modules.Xp.Services; -public interface IUserService -{ - Task GetUserAsync(ulong userId); -} - -public sealed class UserService : IUserService, IEService -{ - private readonly DbService _db; - - public UserService(DbService db) - { - _db = db; - } - - public async Task GetUserAsync(ulong userId) - { - await using var uow = _db.GetDbContext(); - var user = await uow - .GetTable() - .FirstOrDefaultAsyncLinqToDB(u => u.UserId == userId); - - return user; - } -} - public class XpService : IEService, IReadyExecutor, IExecNoCommand { private readonly DbService _db; @@ -72,6 +48,7 @@ public class XpService : IEService, IReadyExecutor, IExecNoCommand private readonly QueueRunner _levelUpQueue = new QueueRunner(0, 50); private readonly Channel _xpGainQueue = Channel.CreateUnbounded(); private readonly IMessageSenderService _sender; + private readonly INotifySubscriber _notifySub; public XpService( DiscordSocketClient client, @@ -87,7 +64,8 @@ public class XpService : IEService, IReadyExecutor, IExecNoCommand XpConfigService xpConfig, IPubSub pubSub, IPatronageService ps, - IMessageSenderService sender) + IMessageSenderService sender, + INotifySubscriber notifySub) { _db = db; _images = images; @@ -99,6 +77,7 @@ public class XpService : IEService, IReadyExecutor, IExecNoCommand _xpConfig = xpConfig; _pubSub = pubSub; _sender = sender; + _notifySub = notifySub; _excludedServers = new(); _excludedChannels = new(); _client = client; @@ -189,9 +168,9 @@ public class XpService : IEService, IReadyExecutor, IExecNoCommand var dus = new List(globalToAdd.Count); var gxps = new List(globalToAdd.Count); + var conf = _xpConfig.Data; await using (var ctx = _db.GetDbContext()) { - var conf = _xpConfig.Data; if (conf.CurrencyPerXp > 0) { foreach (var user in globalToAdd) @@ -290,8 +269,7 @@ public class XpService : IEService, IReadyExecutor, IExecNoCommand du.UserId, false, oldLevel.Level, - newLevel.Level, - du.NotifyOnLevelUp)); + newLevel.Level)); } } @@ -328,8 +306,7 @@ public class XpService : IEService, IReadyExecutor, IExecNoCommand ulong userId, bool isServer, long oldLevel, - long newLevel, - XpNotificationLocation notifyLoc = XpNotificationLocation.None) + long newLevel) => async () => { if (isServer) @@ -337,7 +314,7 @@ public class XpService : IEService, IReadyExecutor, IExecNoCommand await HandleRewardsInternalAsync(guildId, userId, oldLevel, newLevel); } - await HandleNotifyInternalAsync(guildId, channelId, userId, isServer, newLevel, notifyLoc); + await HandleNotifyInternalAsync(guildId, channelId, userId, isServer, newLevel); }; private async Task HandleRewardsInternalAsync( @@ -388,59 +365,25 @@ public class XpService : IEService, IReadyExecutor, IExecNoCommand ulong channelId, ulong userId, bool isServer, - long newLevel, - XpNotificationLocation notifyLoc) + long newLevel) { - if (notifyLoc == XpNotificationLocation.None) - return; - var guild = _client.GetGuild(guildId); var user = guild?.GetUser(userId); - var ch = guild?.GetTextChannel(channelId); if (guild is null || user is null) return; if (isServer) { - if (notifyLoc == XpNotificationLocation.Dm) + var model = new LevelUpNotifyModel() { - await _sender.Response(user) - .Confirm(_strings.GetText(strs.level_up_dm(user.Mention, - Format.Bold(newLevel.ToString()), - Format.Bold(guild.ToString() ?? "-")), - guild.Id)) - .SendAsync(); - } - else // channel - { - if (ch is not null) - { - await _sender.Response(ch) - .Confirm(_strings.GetText(strs.level_up_channel(user.Mention, - Format.Bold(newLevel.ToString())), - guild.Id)) - .SendAsync(); - } - } - } - else // global level - { - var chan = notifyLoc switch - { - XpNotificationLocation.Dm => (IMessageChannel)await user.CreateDMChannelAsync(), - XpNotificationLocation.Channel => ch, - _ => null + GuildId = guildId, + UserId = userId, + ChannelId = channelId, + Level = newLevel }; - - if (chan is null) - return; - - await _sender.Response(chan) - .Confirm(_strings.GetText(strs.level_up_global(user.Mention, - Format.Bold(newLevel.ToString())), - guild.Id)) - .SendAsync(); + await _notifySub.NotifyAsync(model, true); + return; } } @@ -624,20 +567,6 @@ public class XpService : IEService, IReadyExecutor, IExecNoCommand .ToArrayAsyncLinqToDB(); } - public XpNotificationLocation GetNotificationType(IUser user) - { - using var uow = _db.GetDbContext(); - return uow.GetOrCreateUser(user).NotifyOnLevelUp; - } - - public async Task ChangeNotificationType(IUser user, XpNotificationLocation type) - { - await using var uow = _db.GetDbContext(); - var du = uow.GetOrCreateUser(user); - du.NotifyOnLevelUp = type; - await uow.SaveChangesAsync(); - } - private Task Client_OnGuildAvailable(SocketGuild guild) { Task.Run(async () => @@ -1644,23 +1573,15 @@ public class XpService : IEService, IReadyExecutor, IExecNoCommand UserId = userId, Xp = lvlStats.TotalXp, DateAdded = DateTime.UtcNow - }, (old) => new() - { - Xp = lvlStats.TotalXp - }, () => new() - { - GuildId = guildId, - UserId = userId - }); + }, + (old) => new() + { + Xp = lvlStats.TotalXp + }, + () => new() + { + GuildId = guildId, + UserId = userId + }); } -} - -public enum BuyResult -{ - Success, - XpShopDisabled, - AlreadyOwned, - InsufficientFunds, - UnknownItem, - InsufficientPatronTier, } \ No newline at end of file diff --git a/src/EllieBot/_common/Services/IUserService.cs b/src/EllieBot/_common/Services/IUserService.cs new file mode 100644 index 0000000..a1757e2 --- /dev/null +++ b/src/EllieBot/_common/Services/IUserService.cs @@ -0,0 +1,8 @@ +using EllieBot.Db.Models; + +namespace EllieBot.Modules.Xp.Services; + +public interface IUserService +{ + Task GetUserAsync(ulong userId); +} \ No newline at end of file diff --git a/src/EllieBot/_common/Services/UserService.cs b/src/EllieBot/_common/Services/UserService.cs new file mode 100644 index 0000000..7d5ad70 --- /dev/null +++ b/src/EllieBot/_common/Services/UserService.cs @@ -0,0 +1,24 @@ +using LinqToDB.EntityFrameworkCore; +using EllieBot.Db.Models; + +namespace EllieBot.Modules.Xp.Services; + +public sealed class UserService : IUserService, IEService +{ + private readonly DbService _db; + + public UserService(DbService db) + { + _db = db; + } + + public async Task GetUserAsync(ulong userId) + { + await using var uow = _db.GetDbContext(); + var user = await uow + .GetTable() + .FirstOrDefaultAsyncLinqToDB(u => u.UserId == userId); + + return user; + } +} \ No newline at end of file diff --git a/src/EllieBot/data/aliases.yml b/src/EllieBot/data/aliases.yml index 0f9f9fe..f43750e 100644 --- a/src/EllieBot/data/aliases.yml +++ b/src/EllieBot/data/aliases.yml @@ -1546,4 +1546,7 @@ minesweeper: - minesweeper - mw temprole: - - temprole \ No newline at end of file + - temprole +notify: + - notify + - nfy \ No newline at end of file diff --git a/src/EllieBot/data/strings/commands/commands.en-US.yml b/src/EllieBot/data/strings/commands/commands.en-US.yml index 384d00b..68d1348 100644 --- a/src/EllieBot/data/strings/commands/commands.en-US.yml +++ b/src/EllieBot/data/strings/commands/commands.en-US.yml @@ -4853,4 +4853,14 @@ minesweeper: - '15' params: - mines: - desc: "The number of mines to create." \ No newline at end of file + desc: "The number of mines to create." +notify: + desc: |- + Sends a message to the current channel once the specified event occurs. + ex: + - 'levelup Congratulations to user %user.name% for reaching level %event.level%' + params: + - event: + desc: "The event to notify on." + - message: + desc: "The message to send." \ No newline at end of file