From 06222365233a8a682acfb5f4ea1585b279ffe98a Mon Sep 17 00:00:00 2001 From: Kwoth Date: Thu, 5 Dec 2024 14:35:42 +0000 Subject: [PATCH] add: Added .notify and migrations, added levelup and protection events for notify, removed xpnotify completely --- src/NadekoBot/.editorconfig | 1 + src/NadekoBot/Db/Models/Notify.cs | 8 +- .../Db/Models/xp/XpNotificationLocation.cs | 2 +- src/NadekoBot/Db/NadekoContext.cs | 2 +- ...146_awardedxp-temprole-notify.Designer.cs} | 44 +++- ...241205052146_awardedxp-temprole-notify.cs} | 33 ++- .../PostgreSqlContextModelSnapshot.cs | 40 +++- ...137_awardedxp-temprole-notify.Designer.cs} | 34 ++- ...241205052137_awardedxp-temprole-notify.cs} | 33 ++- .../NadekoSqliteContextModelSnapshot.cs | 30 ++- .../Administration/Notify/INotifyModel.cs | 23 ++ .../Notify/INotifySubscriber.cs | 7 + .../Notify/Models/LevelUpNotifyModel.cs | 44 ++++ .../Administration/Notify/NotifyCommands.cs | 80 +------ .../Administration/Notify/NotifyKeys.cs | 6 + .../Administration/Notify/NotifyService.cs | 202 ++++++++++++++++++ .../Protection/ProtectionService.cs | 44 +++- src/NadekoBot/Modules/Xp/BuyResult.cs | 11 + src/NadekoBot/Modules/Xp/Xp.cs | 31 --- src/NadekoBot/Modules/Xp/XpService.cs | 143 +++---------- src/NadekoBot/NadekoBot.csproj.DotSettings | 2 + .../_common/Services/IUserService.cs | 8 + src/NadekoBot/_common/Services/UserService.cs | 24 +++ src/NadekoBot/data/aliases.yml | 5 +- .../data/strings/commands/commands.en-US.yml | 12 +- 25 files changed, 625 insertions(+), 244 deletions(-) rename src/NadekoBot/Migrations/PostgreSql/{20241203093815_awarded-xp-and-notify-removed.Designer.cs => 20241205052146_awardedxp-temprole-notify.Designer.cs} (99%) rename src/NadekoBot/Migrations/PostgreSql/{20241203093815_awarded-xp-and-notify-removed.cs => 20241205052146_awardedxp-temprole-notify.cs} (62%) rename src/NadekoBot/Migrations/Sqlite/{20241203093804_awarded-xp-and-notify-removed.Designer.cs => 20241205052137_awardedxp-temprole-notify.Designer.cs} (99%) rename src/NadekoBot/Migrations/Sqlite/{20241203093804_awarded-xp-and-notify-removed.cs => 20241205052137_awardedxp-temprole-notify.cs} (62%) create mode 100644 src/NadekoBot/Modules/Administration/Notify/INotifyModel.cs create mode 100644 src/NadekoBot/Modules/Administration/Notify/INotifySubscriber.cs create mode 100644 src/NadekoBot/Modules/Administration/Notify/Models/LevelUpNotifyModel.cs create mode 100644 src/NadekoBot/Modules/Administration/Notify/NotifyKeys.cs create mode 100644 src/NadekoBot/Modules/Administration/Notify/NotifyService.cs create mode 100644 src/NadekoBot/Modules/Xp/BuyResult.cs create mode 100644 src/NadekoBot/NadekoBot.csproj.DotSettings create mode 100644 src/NadekoBot/_common/Services/IUserService.cs create mode 100644 src/NadekoBot/_common/Services/UserService.cs diff --git a/src/NadekoBot/.editorconfig b/src/NadekoBot/.editorconfig index 9f35ed645..981495805 100644 --- a/src/NadekoBot/.editorconfig +++ b/src/NadekoBot/.editorconfig @@ -357,3 +357,4 @@ 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/NadekoBot/Db/Models/Notify.cs b/src/NadekoBot/Db/Models/Notify.cs index bcdcd4dfa..ec8d629ff 100644 --- a/src/NadekoBot/Db/Models/Notify.cs +++ b/src/NadekoBot/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/NadekoBot/Db/Models/xp/XpNotificationLocation.cs b/src/NadekoBot/Db/Models/xp/XpNotificationLocation.cs index ea8398a6e..4af98d065 100644 --- a/src/NadekoBot/Db/Models/xp/XpNotificationLocation.cs +++ b/src/NadekoBot/Db/Models/xp/XpNotificationLocation.cs @@ -1,4 +1,4 @@ -namespace NadekoBot.Db.Models; +namespace NadekoBot.Db.Models; public enum XpNotificationLocation { diff --git a/src/NadekoBot/Db/NadekoContext.cs b/src/NadekoBot/Db/NadekoContext.cs index 40c319217..b2af16e65 100644 --- a/src/NadekoBot/Db/NadekoContext.cs +++ b/src/NadekoBot/Db/NadekoContext.cs @@ -81,7 +81,7 @@ public abstract class NadekoContext : DbContext e.HasAlternateKey(x => new { x.GuildId, - x.Event + Event = x.Type }); }); diff --git a/src/NadekoBot/Migrations/PostgreSql/20241203093815_awarded-xp-and-notify-removed.Designer.cs b/src/NadekoBot/Migrations/PostgreSql/20241205052146_awardedxp-temprole-notify.Designer.cs similarity index 99% rename from src/NadekoBot/Migrations/PostgreSql/20241203093815_awarded-xp-and-notify-removed.Designer.cs rename to src/NadekoBot/Migrations/PostgreSql/20241205052146_awardedxp-temprole-notify.Designer.cs index 91847c24a..9eb8e5fc5 100644 --- a/src/NadekoBot/Migrations/PostgreSql/20241203093815_awarded-xp-and-notify-removed.Designer.cs +++ b/src/NadekoBot/Migrations/PostgreSql/20241205052146_awardedxp-temprole-notify.Designer.cs @@ -12,8 +12,8 @@ using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; namespace NadekoBot.Migrations.PostgreSql { [DbContext(typeof(PostgreSqlContext))] - [Migration("20241203093815_awarded-xp-and-notify-removed")] - partial class awardedxpandnotifyremoved + [Migration("20241205052146_awardedxp-temprole-notify")] + partial class awardedxptemprolenotify { /// protected override void BuildTargetModel(ModelBuilder modelBuilder) @@ -1820,6 +1820,42 @@ namespace NadekoBot.Migrations.PostgreSql b.ToTable("expressions", (string)null); }); + modelBuilder.Entity("NadekoBot.Db.Models.Notify", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ChannelId") + .HasColumnType("numeric(20,0)") + .HasColumnName("channelid"); + + b.Property("GuildId") + .HasColumnType("numeric(20,0)") + .HasColumnName("guildid"); + + b.Property("Message") + .IsRequired() + .HasMaxLength(10000) + .HasColumnType("character varying(10000)") + .HasColumnName("message"); + + b.Property("Type") + .HasColumnType("integer") + .HasColumnName("type"); + + b.HasKey("Id") + .HasName("pk_notify"); + + b.HasAlternateKey("GuildId", "Type") + .HasName("ak_notify_guildid_type"); + + b.ToTable("notify", (string)null); + }); + modelBuilder.Entity("NadekoBot.Db.Models.PatronUser", b => { b.Property("UserId") @@ -2910,10 +2946,6 @@ namespace NadekoBot.Migrations.PostgreSql .HasColumnType("numeric(20,0)") .HasColumnName("guildid"); - b.Property("NotifyOnLevelUp") - .HasColumnType("integer") - .HasColumnName("notifyonlevelup"); - b.Property("UserId") .HasColumnType("numeric(20,0)") .HasColumnName("userid"); diff --git a/src/NadekoBot/Migrations/PostgreSql/20241203093815_awarded-xp-and-notify-removed.cs b/src/NadekoBot/Migrations/PostgreSql/20241205052146_awardedxp-temprole-notify.cs similarity index 62% rename from src/NadekoBot/Migrations/PostgreSql/20241203093815_awarded-xp-and-notify-removed.cs rename to src/NadekoBot/Migrations/PostgreSql/20241205052146_awardedxp-temprole-notify.cs index ff6853272..4cfd5e8a9 100644 --- a/src/NadekoBot/Migrations/PostgreSql/20241203093815_awarded-xp-and-notify-removed.cs +++ b/src/NadekoBot/Migrations/PostgreSql/20241205052146_awardedxp-temprole-notify.cs @@ -7,7 +7,7 @@ using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; namespace NadekoBot.Migrations.PostgreSql { /// - public partial class awardedxpandnotifyremoved : Migration + public partial class awardedxptemprolenotify : Migration { /// protected override void Up(MigrationBuilder migrationBuilder) @@ -20,6 +20,27 @@ namespace NadekoBot.Migrations.PostgreSql name: "awardedxp", table: "userxpstats"); + migrationBuilder.DropColumn( + name: "notifyonlevelup", + table: "userxpstats"); + + 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), + type = table.Column(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_type", x => new { x.guildid, x.type }); + }); + migrationBuilder.CreateTable( name: "temprole", columns: table => new @@ -47,6 +68,9 @@ namespace NadekoBot.Migrations.PostgreSql /// protected override void Down(MigrationBuilder migrationBuilder) { + migrationBuilder.DropTable( + name: "notify"); + migrationBuilder.DropTable( name: "temprole"); @@ -57,6 +81,13 @@ namespace NadekoBot.Migrations.PostgreSql nullable: false, defaultValue: 0L); + migrationBuilder.AddColumn( + name: "notifyonlevelup", + table: "userxpstats", + type: "integer", + nullable: false, + defaultValue: 0); + migrationBuilder.CreateIndex( name: "ix_userxpstats_awardedxp", table: "userxpstats", diff --git a/src/NadekoBot/Migrations/PostgreSql/PostgreSqlContextModelSnapshot.cs b/src/NadekoBot/Migrations/PostgreSql/PostgreSqlContextModelSnapshot.cs index 65caf94f9..637fcd8bb 100644 --- a/src/NadekoBot/Migrations/PostgreSql/PostgreSqlContextModelSnapshot.cs +++ b/src/NadekoBot/Migrations/PostgreSql/PostgreSqlContextModelSnapshot.cs @@ -1817,6 +1817,42 @@ namespace NadekoBot.Migrations.PostgreSql b.ToTable("expressions", (string)null); }); + modelBuilder.Entity("NadekoBot.Db.Models.Notify", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ChannelId") + .HasColumnType("numeric(20,0)") + .HasColumnName("channelid"); + + b.Property("GuildId") + .HasColumnType("numeric(20,0)") + .HasColumnName("guildid"); + + b.Property("Message") + .IsRequired() + .HasMaxLength(10000) + .HasColumnType("character varying(10000)") + .HasColumnName("message"); + + b.Property("Type") + .HasColumnType("integer") + .HasColumnName("type"); + + b.HasKey("Id") + .HasName("pk_notify"); + + b.HasAlternateKey("GuildId", "Type") + .HasName("ak_notify_guildid_type"); + + b.ToTable("notify", (string)null); + }); + modelBuilder.Entity("NadekoBot.Db.Models.PatronUser", b => { b.Property("UserId") @@ -2907,10 +2943,6 @@ namespace NadekoBot.Migrations.PostgreSql .HasColumnType("numeric(20,0)") .HasColumnName("guildid"); - b.Property("NotifyOnLevelUp") - .HasColumnType("integer") - .HasColumnName("notifyonlevelup"); - b.Property("UserId") .HasColumnType("numeric(20,0)") .HasColumnName("userid"); diff --git a/src/NadekoBot/Migrations/Sqlite/20241203093804_awarded-xp-and-notify-removed.Designer.cs b/src/NadekoBot/Migrations/Sqlite/20241205052137_awardedxp-temprole-notify.Designer.cs similarity index 99% rename from src/NadekoBot/Migrations/Sqlite/20241203093804_awarded-xp-and-notify-removed.Designer.cs rename to src/NadekoBot/Migrations/Sqlite/20241205052137_awardedxp-temprole-notify.Designer.cs index 37db0118d..5872afd4e 100644 --- a/src/NadekoBot/Migrations/Sqlite/20241203093804_awarded-xp-and-notify-removed.Designer.cs +++ b/src/NadekoBot/Migrations/Sqlite/20241205052137_awardedxp-temprole-notify.Designer.cs @@ -11,8 +11,8 @@ using NadekoBot.Db; namespace NadekoBot.Migrations { [DbContext(typeof(SqliteContext))] - [Migration("20241203093804_awarded-xp-and-notify-removed")] - partial class awardedxpandnotifyremoved + [Migration("20241205052137_awardedxp-temprole-notify")] + partial class awardedxptemprolenotify { /// protected override void BuildTargetModel(ModelBuilder modelBuilder) @@ -1359,6 +1359,33 @@ namespace NadekoBot.Migrations b.ToTable("Expressions"); }); + modelBuilder.Entity("NadekoBot.Db.Models.Notify", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ChannelId") + .HasColumnType("INTEGER"); + + b.Property("GuildId") + .HasColumnType("INTEGER"); + + b.Property("Message") + .IsRequired() + .HasMaxLength(10000) + .HasColumnType("TEXT"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasAlternateKey("GuildId", "Type"); + + b.ToTable("Notify"); + }); + modelBuilder.Entity("NadekoBot.Db.Models.PatronUser", b => { b.Property("UserId") @@ -2166,9 +2193,6 @@ namespace NadekoBot.Migrations b.Property("GuildId") .HasColumnType("INTEGER"); - b.Property("NotifyOnLevelUp") - .HasColumnType("INTEGER"); - b.Property("UserId") .HasColumnType("INTEGER"); diff --git a/src/NadekoBot/Migrations/Sqlite/20241203093804_awarded-xp-and-notify-removed.cs b/src/NadekoBot/Migrations/Sqlite/20241205052137_awardedxp-temprole-notify.cs similarity index 62% rename from src/NadekoBot/Migrations/Sqlite/20241203093804_awarded-xp-and-notify-removed.cs rename to src/NadekoBot/Migrations/Sqlite/20241205052137_awardedxp-temprole-notify.cs index 201015a19..9828d13be 100644 --- a/src/NadekoBot/Migrations/Sqlite/20241203093804_awarded-xp-and-notify-removed.cs +++ b/src/NadekoBot/Migrations/Sqlite/20241205052137_awardedxp-temprole-notify.cs @@ -6,7 +6,7 @@ using Microsoft.EntityFrameworkCore.Migrations; namespace NadekoBot.Migrations { /// - public partial class awardedxpandnotifyremoved : Migration + public partial class awardedxptemprolenotify : Migration { /// protected override void Up(MigrationBuilder migrationBuilder) @@ -19,6 +19,27 @@ namespace NadekoBot.Migrations name: "AwardedXp", table: "UserXpStats"); + migrationBuilder.DropColumn( + name: "NotifyOnLevelUp", + table: "UserXpStats"); + + 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), + Type = 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_Type", x => new { x.GuildId, x.Type }); + }); + migrationBuilder.CreateTable( name: "TempRole", columns: table => new @@ -46,6 +67,9 @@ namespace NadekoBot.Migrations /// protected override void Down(MigrationBuilder migrationBuilder) { + migrationBuilder.DropTable( + name: "Notify"); + migrationBuilder.DropTable( name: "TempRole"); @@ -56,6 +80,13 @@ namespace NadekoBot.Migrations nullable: false, defaultValue: 0L); + migrationBuilder.AddColumn( + name: "NotifyOnLevelUp", + table: "UserXpStats", + type: "INTEGER", + nullable: false, + defaultValue: 0); + migrationBuilder.CreateIndex( name: "IX_UserXpStats_AwardedXp", table: "UserXpStats", diff --git a/src/NadekoBot/Migrations/Sqlite/NadekoSqliteContextModelSnapshot.cs b/src/NadekoBot/Migrations/Sqlite/NadekoSqliteContextModelSnapshot.cs index 9ec5f2c18..8e9d53939 100644 --- a/src/NadekoBot/Migrations/Sqlite/NadekoSqliteContextModelSnapshot.cs +++ b/src/NadekoBot/Migrations/Sqlite/NadekoSqliteContextModelSnapshot.cs @@ -1356,6 +1356,33 @@ namespace NadekoBot.Migrations b.ToTable("Expressions"); }); + modelBuilder.Entity("NadekoBot.Db.Models.Notify", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ChannelId") + .HasColumnType("INTEGER"); + + b.Property("GuildId") + .HasColumnType("INTEGER"); + + b.Property("Message") + .IsRequired() + .HasMaxLength(10000) + .HasColumnType("TEXT"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasAlternateKey("GuildId", "Type"); + + b.ToTable("Notify"); + }); + modelBuilder.Entity("NadekoBot.Db.Models.PatronUser", b => { b.Property("UserId") @@ -2163,9 +2190,6 @@ namespace NadekoBot.Migrations b.Property("GuildId") .HasColumnType("INTEGER"); - b.Property("NotifyOnLevelUp") - .HasColumnType("INTEGER"); - b.Property("UserId") .HasColumnType("INTEGER"); diff --git a/src/NadekoBot/Modules/Administration/Notify/INotifyModel.cs b/src/NadekoBot/Modules/Administration/Notify/INotifyModel.cs new file mode 100644 index 000000000..a1c3a1128 --- /dev/null +++ b/src/NadekoBot/Modules/Administration/Notify/INotifyModel.cs @@ -0,0 +1,23 @@ +using NadekoBot.Db.Models; +using System.Collections; + +namespace NadekoBot.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/NadekoBot/Modules/Administration/Notify/INotifySubscriber.cs b/src/NadekoBot/Modules/Administration/Notify/INotifySubscriber.cs new file mode 100644 index 000000000..362d06a2c --- /dev/null +++ b/src/NadekoBot/Modules/Administration/Notify/INotifySubscriber.cs @@ -0,0 +1,7 @@ +namespace NadekoBot.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/NadekoBot/Modules/Administration/Notify/Models/LevelUpNotifyModel.cs b/src/NadekoBot/Modules/Administration/Notify/Models/LevelUpNotifyModel.cs new file mode 100644 index 000000000..7affec529 --- /dev/null +++ b/src/NadekoBot/Modules/Administration/Notify/Models/LevelUpNotifyModel.cs @@ -0,0 +1,44 @@ +using NadekoBot.Db.Models; + +namespace NadekoBot.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/NadekoBot/Modules/Administration/Notify/NotifyCommands.cs b/src/NadekoBot/Modules/Administration/Notify/NotifyCommands.cs index 20cece58c..4d9ae3e74 100644 --- a/src/NadekoBot/Modules/Administration/Notify/NotifyCommands.cs +++ b/src/NadekoBot/Modules/Administration/Notify/NotifyCommands.cs @@ -1,92 +1,24 @@ -using LinqToDB; -using LinqToDB.EntityFrameworkCore; -using NadekoBot.Common.ModuleBehaviors; -using NadekoBot.Db.Models; +using NadekoBot.Db.Models; namespace NadekoBot.Modules.Administration; -public sealed class NotifyService : IReadyExecutor, INService -{ - 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 : NadekoModule { [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/NadekoBot/Modules/Administration/Notify/NotifyKeys.cs b/src/NadekoBot/Modules/Administration/Notify/NotifyKeys.cs new file mode 100644 index 000000000..397b7925a --- /dev/null +++ b/src/NadekoBot/Modules/Administration/Notify/NotifyKeys.cs @@ -0,0 +1,6 @@ +namespace NadekoBot.Modules.Administration; + +public static class NotifyKeys +{ + public static TypedKey LevelUp { get; } = new("notify:levelup"); +} \ No newline at end of file diff --git a/src/NadekoBot/Modules/Administration/Notify/NotifyService.cs b/src/NadekoBot/Modules/Administration/Notify/NotifyService.cs new file mode 100644 index 000000000..06e031466 --- /dev/null +++ b/src/NadekoBot/Modules/Administration/Notify/NotifyService.cs @@ -0,0 +1,202 @@ +using LinqToDB; +using LinqToDB.EntityFrameworkCore; +using NadekoBot.Common.ModuleBehaviors; +using NadekoBot.Db.Models; + +namespace NadekoBot.Modules.Administration; + +public sealed class NotifyService : IReadyExecutor, INotifySubscriber, INService +{ + 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 _); + } +} \ No newline at end of file diff --git a/src/NadekoBot/Modules/Administration/Protection/ProtectionService.cs b/src/NadekoBot/Modules/Administration/Protection/ProtectionService.cs index 457e28091..9f5141c55 100644 --- a/src/NadekoBot/Modules/Administration/Protection/ProtectionService.cs +++ b/src/NadekoBot/Modules/Administration/Protection/ProtectionService.cs @@ -5,6 +5,36 @@ using System.Threading.Channels; namespace NadekoBot.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 : INService { public event Func OnAntiProtectionTriggered = delegate @@ -22,6 +52,7 @@ public class ProtectionService : INService 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 : INService 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 : INService alts.RoleId, user); + await _notifySub.NotifyAsync(new ProtectionNotifyModel(user.Guild.Id, + ProtectionType.Alting, + user.Id)); return; } } @@ -194,6 +230,8 @@ public class ProtectionService : INService 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 : INService settings.MuteTime, settings.RoleId, (IGuildUser)msg.Author); + + await _notifySub.NotifyAsync(new ProtectionNotifyModel(channel.GuildId, + ProtectionType.Spamming, + msg.Author.Id)); } } } diff --git a/src/NadekoBot/Modules/Xp/BuyResult.cs b/src/NadekoBot/Modules/Xp/BuyResult.cs new file mode 100644 index 000000000..5720bfdc7 --- /dev/null +++ b/src/NadekoBot/Modules/Xp/BuyResult.cs @@ -0,0 +1,11 @@ +namespace NadekoBot.Modules.Xp.Services; + +public enum BuyResult +{ + Success, + XpShopDisabled, + AlreadyOwned, + InsufficientFunds, + UnknownItem, + InsufficientPatronTier, +} \ No newline at end of file diff --git a/src/NadekoBot/Modules/Xp/Xp.cs b/src/NadekoBot/Modules/Xp/Xp.cs index 43779b47d..b0f35c210 100644 --- a/src/NadekoBot/Modules/Xp/Xp.cs +++ b/src/NadekoBot/Modules/Xp/Xp.cs @@ -51,26 +51,6 @@ public partial class Xp : NadekoModule } } - [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 : NadekoModule 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/NadekoBot/Modules/Xp/XpService.cs b/src/NadekoBot/Modules/Xp/XpService.cs index 039f9d6d2..348461a04 100644 --- a/src/NadekoBot/Modules/Xp/XpService.cs +++ b/src/NadekoBot/Modules/Xp/XpService.cs @@ -13,6 +13,7 @@ using SixLabors.ImageSharp.Processing; using System.Threading.Channels; using LinqToDB.EntityFrameworkCore; using LinqToDB.Tools; +using NadekoBot.Modules.Administration; using NadekoBot.Modules.Patronage; using Color = SixLabors.ImageSharp.Color; using Exception = System.Exception; @@ -20,31 +21,6 @@ using Image = SixLabors.ImageSharp.Image; namespace NadekoBot.Modules.Xp.Services; -public interface IUserService -{ - Task GetUserAsync(ulong userId); -} - -public sealed class UserService : IUserService, INService -{ - 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 : INService, IReadyExecutor, IExecNoCommand { private readonly DbService _db; @@ -72,6 +48,7 @@ public class XpService : INService, 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 : INService, 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 : INService, IReadyExecutor, IExecNoCommand _xpConfig = xpConfig; _pubSub = pubSub; _sender = sender; + _notifySub = notifySub; _excludedServers = new(); _excludedChannels = new(); _client = client; @@ -189,9 +168,9 @@ public class XpService : INService, 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 : INService, IReadyExecutor, IExecNoCommand du.UserId, false, oldLevel.Level, - newLevel.Level, - du.NotifyOnLevelUp)); + newLevel.Level)); } } @@ -328,8 +306,7 @@ public class XpService : INService, 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 : INService, 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 : INService, 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 : INService, 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 () => @@ -1639,28 +1568,20 @@ public class XpService : INService, IReadyExecutor, IExecNoCommand await using var ctx = _db.GetDbContext(); await ctx.GetTable() .InsertOrUpdateAsync(() => new() - { - GuildId = guildId, - UserId = userId, - Xp = lvlStats.TotalXp, - DateAdded = DateTime.UtcNow - }, (old) => new() - { - Xp = lvlStats.TotalXp - }, () => new() - { - GuildId = guildId, - UserId = userId - }); + { + GuildId = guildId, + UserId = userId, + Xp = lvlStats.TotalXp, + DateAdded = DateTime.UtcNow + }, + (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/NadekoBot/NadekoBot.csproj.DotSettings b/src/NadekoBot/NadekoBot.csproj.DotSettings new file mode 100644 index 000000000..7ea7914e9 --- /dev/null +++ b/src/NadekoBot/NadekoBot.csproj.DotSettings @@ -0,0 +1,2 @@ + + True \ No newline at end of file diff --git a/src/NadekoBot/_common/Services/IUserService.cs b/src/NadekoBot/_common/Services/IUserService.cs new file mode 100644 index 000000000..3e2cef8ec --- /dev/null +++ b/src/NadekoBot/_common/Services/IUserService.cs @@ -0,0 +1,8 @@ +using NadekoBot.Db.Models; + +namespace NadekoBot.Modules.Xp.Services; + +public interface IUserService +{ + Task GetUserAsync(ulong userId); +} \ No newline at end of file diff --git a/src/NadekoBot/_common/Services/UserService.cs b/src/NadekoBot/_common/Services/UserService.cs new file mode 100644 index 000000000..a293a7858 --- /dev/null +++ b/src/NadekoBot/_common/Services/UserService.cs @@ -0,0 +1,24 @@ +using LinqToDB.EntityFrameworkCore; +using NadekoBot.Db.Models; + +namespace NadekoBot.Modules.Xp.Services; + +public sealed class UserService : IUserService, INService +{ + 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/NadekoBot/data/aliases.yml b/src/NadekoBot/data/aliases.yml index 7699e0bc1..c8df7b6fb 100644 --- a/src/NadekoBot/data/aliases.yml +++ b/src/NadekoBot/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/NadekoBot/data/strings/commands/commands.en-US.yml b/src/NadekoBot/data/strings/commands/commands.en-US.yml index ae993d398..a31e37794 100644 --- a/src/NadekoBot/data/strings/commands/commands.en-US.yml +++ b/src/NadekoBot/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