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