add: Added .notify and migrations, added levelup and protection events for notify, removed xpnotify completely

This commit is contained in:
Kwoth
2024-12-05 14:35:42 +00:00
parent 0f240925e8
commit 0622236523
25 changed files with 625 additions and 244 deletions

View File

@@ -357,3 +357,4 @@ resharper_arrange_redundant_parentheses_highlighting = hint
# IDE0011: Add braces # IDE0011: Add braces
dotnet_diagnostic.IDE0011.severity = warning dotnet_diagnostic.IDE0011.severity = warning
resharper_arrange_type_member_modifiers_highlighting = hint

View File

@@ -6,15 +6,17 @@ public class Notify
{ {
[Key] [Key]
public int Id { get; set; } public int Id { get; set; }
public ulong GuildId { get; set; } public ulong GuildId { get; set; }
public ulong ChannelId { get; set; } public ulong ChannelId { get; set; }
public NotifyEvent Event { get; set; } public NotifyType Type { get; set; }
[MaxLength(10_000)] [MaxLength(10_000)]
public string Message { get; set; } = string.Empty; public string Message { get; set; } = string.Empty;
} }
public enum NotifyEvent public enum NotifyType
{ {
UserLevelUp LevelUp = 0,
Protection = 1, Prot = 1,
} }

View File

@@ -1,4 +1,4 @@
namespace NadekoBot.Db.Models; namespace NadekoBot.Db.Models;
public enum XpNotificationLocation public enum XpNotificationLocation
{ {

View File

@@ -81,7 +81,7 @@ public abstract class NadekoContext : DbContext
e.HasAlternateKey(x => new e.HasAlternateKey(x => new
{ {
x.GuildId, x.GuildId,
x.Event Event = x.Type
}); });
}); });

View File

@@ -12,8 +12,8 @@ using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
namespace NadekoBot.Migrations.PostgreSql namespace NadekoBot.Migrations.PostgreSql
{ {
[DbContext(typeof(PostgreSqlContext))] [DbContext(typeof(PostgreSqlContext))]
[Migration("20241203093815_awarded-xp-and-notify-removed")] [Migration("20241205052146_awardedxp-temprole-notify")]
partial class awardedxpandnotifyremoved partial class awardedxptemprolenotify
{ {
/// <inheritdoc /> /// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder) protected override void BuildTargetModel(ModelBuilder modelBuilder)
@@ -1820,6 +1820,42 @@ namespace NadekoBot.Migrations.PostgreSql
b.ToTable("expressions", (string)null); b.ToTable("expressions", (string)null);
}); });
modelBuilder.Entity("NadekoBot.Db.Models.Notify", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer")
.HasColumnName("id");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<decimal>("ChannelId")
.HasColumnType("numeric(20,0)")
.HasColumnName("channelid");
b.Property<decimal>("GuildId")
.HasColumnType("numeric(20,0)")
.HasColumnName("guildid");
b.Property<string>("Message")
.IsRequired()
.HasMaxLength(10000)
.HasColumnType("character varying(10000)")
.HasColumnName("message");
b.Property<int>("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 => modelBuilder.Entity("NadekoBot.Db.Models.PatronUser", b =>
{ {
b.Property<decimal>("UserId") b.Property<decimal>("UserId")
@@ -2910,10 +2946,6 @@ namespace NadekoBot.Migrations.PostgreSql
.HasColumnType("numeric(20,0)") .HasColumnType("numeric(20,0)")
.HasColumnName("guildid"); .HasColumnName("guildid");
b.Property<int>("NotifyOnLevelUp")
.HasColumnType("integer")
.HasColumnName("notifyonlevelup");
b.Property<decimal>("UserId") b.Property<decimal>("UserId")
.HasColumnType("numeric(20,0)") .HasColumnType("numeric(20,0)")
.HasColumnName("userid"); .HasColumnName("userid");

View File

@@ -7,7 +7,7 @@ using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
namespace NadekoBot.Migrations.PostgreSql namespace NadekoBot.Migrations.PostgreSql
{ {
/// <inheritdoc /> /// <inheritdoc />
public partial class awardedxpandnotifyremoved : Migration public partial class awardedxptemprolenotify : Migration
{ {
/// <inheritdoc /> /// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder) protected override void Up(MigrationBuilder migrationBuilder)
@@ -20,6 +20,27 @@ namespace NadekoBot.Migrations.PostgreSql
name: "awardedxp", name: "awardedxp",
table: "userxpstats"); table: "userxpstats");
migrationBuilder.DropColumn(
name: "notifyonlevelup",
table: "userxpstats");
migrationBuilder.CreateTable(
name: "notify",
columns: table => new
{
id = table.Column<int>(type: "integer", nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
guildid = table.Column<decimal>(type: "numeric(20,0)", nullable: false),
channelid = table.Column<decimal>(type: "numeric(20,0)", nullable: false),
type = table.Column<int>(type: "integer", nullable: false),
message = table.Column<string>(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( migrationBuilder.CreateTable(
name: "temprole", name: "temprole",
columns: table => new columns: table => new
@@ -47,6 +68,9 @@ namespace NadekoBot.Migrations.PostgreSql
/// <inheritdoc /> /// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder) protected override void Down(MigrationBuilder migrationBuilder)
{ {
migrationBuilder.DropTable(
name: "notify");
migrationBuilder.DropTable( migrationBuilder.DropTable(
name: "temprole"); name: "temprole");
@@ -57,6 +81,13 @@ namespace NadekoBot.Migrations.PostgreSql
nullable: false, nullable: false,
defaultValue: 0L); defaultValue: 0L);
migrationBuilder.AddColumn<int>(
name: "notifyonlevelup",
table: "userxpstats",
type: "integer",
nullable: false,
defaultValue: 0);
migrationBuilder.CreateIndex( migrationBuilder.CreateIndex(
name: "ix_userxpstats_awardedxp", name: "ix_userxpstats_awardedxp",
table: "userxpstats", table: "userxpstats",

View File

@@ -1817,6 +1817,42 @@ namespace NadekoBot.Migrations.PostgreSql
b.ToTable("expressions", (string)null); b.ToTable("expressions", (string)null);
}); });
modelBuilder.Entity("NadekoBot.Db.Models.Notify", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer")
.HasColumnName("id");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<decimal>("ChannelId")
.HasColumnType("numeric(20,0)")
.HasColumnName("channelid");
b.Property<decimal>("GuildId")
.HasColumnType("numeric(20,0)")
.HasColumnName("guildid");
b.Property<string>("Message")
.IsRequired()
.HasMaxLength(10000)
.HasColumnType("character varying(10000)")
.HasColumnName("message");
b.Property<int>("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 => modelBuilder.Entity("NadekoBot.Db.Models.PatronUser", b =>
{ {
b.Property<decimal>("UserId") b.Property<decimal>("UserId")
@@ -2907,10 +2943,6 @@ namespace NadekoBot.Migrations.PostgreSql
.HasColumnType("numeric(20,0)") .HasColumnType("numeric(20,0)")
.HasColumnName("guildid"); .HasColumnName("guildid");
b.Property<int>("NotifyOnLevelUp")
.HasColumnType("integer")
.HasColumnName("notifyonlevelup");
b.Property<decimal>("UserId") b.Property<decimal>("UserId")
.HasColumnType("numeric(20,0)") .HasColumnType("numeric(20,0)")
.HasColumnName("userid"); .HasColumnName("userid");

View File

@@ -11,8 +11,8 @@ using NadekoBot.Db;
namespace NadekoBot.Migrations namespace NadekoBot.Migrations
{ {
[DbContext(typeof(SqliteContext))] [DbContext(typeof(SqliteContext))]
[Migration("20241203093804_awarded-xp-and-notify-removed")] [Migration("20241205052137_awardedxp-temprole-notify")]
partial class awardedxpandnotifyremoved partial class awardedxptemprolenotify
{ {
/// <inheritdoc /> /// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder) protected override void BuildTargetModel(ModelBuilder modelBuilder)
@@ -1359,6 +1359,33 @@ namespace NadekoBot.Migrations
b.ToTable("Expressions"); b.ToTable("Expressions");
}); });
modelBuilder.Entity("NadekoBot.Db.Models.Notify", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<ulong>("ChannelId")
.HasColumnType("INTEGER");
b.Property<ulong>("GuildId")
.HasColumnType("INTEGER");
b.Property<string>("Message")
.IsRequired()
.HasMaxLength(10000)
.HasColumnType("TEXT");
b.Property<int>("Type")
.HasColumnType("INTEGER");
b.HasKey("Id");
b.HasAlternateKey("GuildId", "Type");
b.ToTable("Notify");
});
modelBuilder.Entity("NadekoBot.Db.Models.PatronUser", b => modelBuilder.Entity("NadekoBot.Db.Models.PatronUser", b =>
{ {
b.Property<ulong>("UserId") b.Property<ulong>("UserId")
@@ -2166,9 +2193,6 @@ namespace NadekoBot.Migrations
b.Property<ulong>("GuildId") b.Property<ulong>("GuildId")
.HasColumnType("INTEGER"); .HasColumnType("INTEGER");
b.Property<int>("NotifyOnLevelUp")
.HasColumnType("INTEGER");
b.Property<ulong>("UserId") b.Property<ulong>("UserId")
.HasColumnType("INTEGER"); .HasColumnType("INTEGER");

View File

@@ -6,7 +6,7 @@ using Microsoft.EntityFrameworkCore.Migrations;
namespace NadekoBot.Migrations namespace NadekoBot.Migrations
{ {
/// <inheritdoc /> /// <inheritdoc />
public partial class awardedxpandnotifyremoved : Migration public partial class awardedxptemprolenotify : Migration
{ {
/// <inheritdoc /> /// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder) protected override void Up(MigrationBuilder migrationBuilder)
@@ -19,6 +19,27 @@ namespace NadekoBot.Migrations
name: "AwardedXp", name: "AwardedXp",
table: "UserXpStats"); table: "UserXpStats");
migrationBuilder.DropColumn(
name: "NotifyOnLevelUp",
table: "UserXpStats");
migrationBuilder.CreateTable(
name: "Notify",
columns: table => new
{
Id = table.Column<int>(type: "INTEGER", nullable: false)
.Annotation("Sqlite:Autoincrement", true),
GuildId = table.Column<ulong>(type: "INTEGER", nullable: false),
ChannelId = table.Column<ulong>(type: "INTEGER", nullable: false),
Type = table.Column<int>(type: "INTEGER", nullable: false),
Message = table.Column<string>(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( migrationBuilder.CreateTable(
name: "TempRole", name: "TempRole",
columns: table => new columns: table => new
@@ -46,6 +67,9 @@ namespace NadekoBot.Migrations
/// <inheritdoc /> /// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder) protected override void Down(MigrationBuilder migrationBuilder)
{ {
migrationBuilder.DropTable(
name: "Notify");
migrationBuilder.DropTable( migrationBuilder.DropTable(
name: "TempRole"); name: "TempRole");
@@ -56,6 +80,13 @@ namespace NadekoBot.Migrations
nullable: false, nullable: false,
defaultValue: 0L); defaultValue: 0L);
migrationBuilder.AddColumn<int>(
name: "NotifyOnLevelUp",
table: "UserXpStats",
type: "INTEGER",
nullable: false,
defaultValue: 0);
migrationBuilder.CreateIndex( migrationBuilder.CreateIndex(
name: "IX_UserXpStats_AwardedXp", name: "IX_UserXpStats_AwardedXp",
table: "UserXpStats", table: "UserXpStats",

View File

@@ -1356,6 +1356,33 @@ namespace NadekoBot.Migrations
b.ToTable("Expressions"); b.ToTable("Expressions");
}); });
modelBuilder.Entity("NadekoBot.Db.Models.Notify", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<ulong>("ChannelId")
.HasColumnType("INTEGER");
b.Property<ulong>("GuildId")
.HasColumnType("INTEGER");
b.Property<string>("Message")
.IsRequired()
.HasMaxLength(10000)
.HasColumnType("TEXT");
b.Property<int>("Type")
.HasColumnType("INTEGER");
b.HasKey("Id");
b.HasAlternateKey("GuildId", "Type");
b.ToTable("Notify");
});
modelBuilder.Entity("NadekoBot.Db.Models.PatronUser", b => modelBuilder.Entity("NadekoBot.Db.Models.PatronUser", b =>
{ {
b.Property<ulong>("UserId") b.Property<ulong>("UserId")
@@ -2163,9 +2190,6 @@ namespace NadekoBot.Migrations
b.Property<ulong>("GuildId") b.Property<ulong>("GuildId")
.HasColumnType("INTEGER"); .HasColumnType("INTEGER");
b.Property<int>("NotifyOnLevelUp")
.HasColumnType("INTEGER");
b.Property<ulong>("UserId") b.Property<ulong>("UserId")
.HasColumnType("INTEGER"); .HasColumnType("INTEGER");

View File

@@ -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<string, Func<SocketGuild, string>> GetReplacements();
public virtual bool TryGetGuildId(out ulong guildId)
{
guildId = 0;
return false;
}
public virtual bool TryGetUserId(out ulong userId)
{
userId = 0;
return false;
}
}

View File

@@ -0,0 +1,7 @@
namespace NadekoBot.Modules.Administration;
public interface INotifySubscriber
{
Task NotifyAsync<T>(T data, bool isShardLocal = false)
where T : struct, INotifyModel;
}

View File

@@ -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<string, Func<SocketGuild, string>> GetReplacements()
{
var data = this;
return new Dictionary<string, Func<SocketGuild, string>>()
{
{ "%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<T> GetTypedKey<T>(this T model)
where T : struct, INotifyModel
=> new(T.KeyName);
}

View File

@@ -1,92 +1,24 @@
using LinqToDB; using NadekoBot.Db.Models;
using LinqToDB.EntityFrameworkCore;
using NadekoBot.Common.ModuleBehaviors;
using NadekoBot.Db.Models;
namespace NadekoBot.Modules.Administration; 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<Notify>()
.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<Notify>()
.Where(x => x.GuildId == guildId && x.Event == nEvent)
.DeleteAsync();
if (deleted > 0)
return;
}
}
public partial class Administration public partial class Administration
{ {
public class NotifyCommands : NadekoModule<NotifyService> public class NotifyCommands : NadekoModule<NotifyService>
{ {
[Cmd] [Cmd]
[OwnerOnly] [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)) if (string.IsNullOrWhiteSpace(message))
{ {
await _service.DisableAsync(ctx.Guild.Id, nEvent); await _service.DisableAsync(ctx.Guild.Id, nType);
await Response().Confirm(strs.notify_off(nEvent)).SendAsync(); await Response().Confirm(strs.notify_off(nType)).SendAsync();
return; return;
} }
await _service.EnableAsync(ctx.Guild.Id, ctx.Channel.Id, nEvent, message); await _service.EnableAsync(ctx.Guild.Id, ctx.Channel.Id, nType, message);
await Response().Confirm(strs.notify_on(nEvent.ToString())).SendAsync(); await Response().Confirm(strs.notify_on(nType.ToString())).SendAsync();
} }
} }
} }

View File

@@ -0,0 +1,6 @@
namespace NadekoBot.Modules.Administration;
public static class NotifyKeys
{
public static TypedKey<LevelUpNotifyModel> LevelUp { get; } = new("notify:levelup");
}

View File

@@ -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<NotifyType, ConcurrentDictionary<ulong, Notify>> _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<Notify>()
.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<LevelUpNotifyModel>();
}
private async Task SubscribeToEvent<T>()
where T : struct, INotifyModel
{
await _pubSub.Sub(new TypedKey<T>(T.KeyName), async (model) => await OnEvent(model));
}
public async Task NotifyAsync<T>(T data, bool isShardLocal = false)
where T : struct, INotifyModel
{
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>(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<Notify>()
.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<Notify>()
.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 _);
}
}

View File

@@ -5,6 +5,36 @@ using System.Threading.Channels;
namespace NadekoBot.Modules.Administration.Services; 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<string, Func<SocketGuild, string>> GetReplacements()
{
var data = this;
return new Dictionary<string, Func<SocketGuild, string>>()
{
{ "%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 class ProtectionService : INService
{ {
public event Func<PunishmentAction, ProtectionType, IGuildUser[], Task> OnAntiProtectionTriggered = delegate public event Func<PunishmentAction, ProtectionType, IGuildUser[], Task> OnAntiProtectionTriggered = delegate
@@ -22,6 +52,7 @@ public class ProtectionService : INService
private readonly MuteService _mute; private readonly MuteService _mute;
private readonly DbService _db; private readonly DbService _db;
private readonly UserPunishService _punishService; private readonly UserPunishService _punishService;
private readonly INotifySubscriber _notifySub;
private readonly Channel<PunishQueueItem> _punishUserQueue = private readonly Channel<PunishQueueItem> _punishUserQueue =
Channel.CreateUnbounded<PunishQueueItem>(new() Channel.CreateUnbounded<PunishQueueItem>(new()
@@ -35,12 +66,14 @@ public class ProtectionService : INService
IBot bot, IBot bot,
MuteService mute, MuteService mute,
DbService db, DbService db,
UserPunishService punishService) UserPunishService punishService,
INotifySubscriber notifySub)
{ {
_client = client; _client = client;
_mute = mute; _mute = mute;
_db = db; _db = db;
_punishService = punishService; _punishService = punishService;
_notifySub = notifySub;
var ids = client.GetGuildIds(); var ids = client.GetGuildIds();
using (var uow = db.GetDbContext()) using (var uow = db.GetDbContext())
@@ -175,6 +208,9 @@ public class ProtectionService : INService
alts.RoleId, alts.RoleId,
user); user);
await _notifySub.NotifyAsync(new ProtectionNotifyModel(user.Guild.Id,
ProtectionType.Alting,
user.Id));
return; return;
} }
} }
@@ -194,6 +230,8 @@ public class ProtectionService : INService
var settings = stats.AntiRaidSettings; var settings = stats.AntiRaidSettings;
await PunishUsers(settings.Action, ProtectionType.Raiding, settings.PunishDuration, null, users); 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); await Task.Delay(1000 * stats.AntiRaidSettings.Seconds);
@@ -246,6 +284,10 @@ public class ProtectionService : INService
settings.MuteTime, settings.MuteTime,
settings.RoleId, settings.RoleId,
(IGuildUser)msg.Author); (IGuildUser)msg.Author);
await _notifySub.NotifyAsync(new ProtectionNotifyModel(channel.GuildId,
ProtectionType.Spamming,
msg.Author.Id));
} }
} }
} }

View File

@@ -0,0 +1,11 @@
namespace NadekoBot.Modules.Xp.Services;
public enum BuyResult
{
Success,
XpShopDisabled,
AlreadyOwned,
InsufficientFunds,
UnknownItem,
InsufficientPatronTier,
}

View File

@@ -51,26 +51,6 @@ public partial class Xp : NadekoModule<XpService>
} }
} }
[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] [Cmd]
[RequireContext(ContextType.Guild)] [RequireContext(ContextType.Guild)]
[UserPerm(GuildPerm.Administrator)] [UserPerm(GuildPerm.Administrator)]
@@ -615,15 +595,4 @@ public partial class Xp : NadekoModule<XpService>
await _service.UseShopItemAsync(ctx.User.Id, type, key); 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);
}
} }

View File

@@ -13,6 +13,7 @@ using SixLabors.ImageSharp.Processing;
using System.Threading.Channels; using System.Threading.Channels;
using LinqToDB.EntityFrameworkCore; using LinqToDB.EntityFrameworkCore;
using LinqToDB.Tools; using LinqToDB.Tools;
using NadekoBot.Modules.Administration;
using NadekoBot.Modules.Patronage; using NadekoBot.Modules.Patronage;
using Color = SixLabors.ImageSharp.Color; using Color = SixLabors.ImageSharp.Color;
using Exception = System.Exception; using Exception = System.Exception;
@@ -20,31 +21,6 @@ using Image = SixLabors.ImageSharp.Image;
namespace NadekoBot.Modules.Xp.Services; namespace NadekoBot.Modules.Xp.Services;
public interface IUserService
{
Task<DiscordUser?> GetUserAsync(ulong userId);
}
public sealed class UserService : IUserService, INService
{
private readonly DbService _db;
public UserService(DbService db)
{
_db = db;
}
public async Task<DiscordUser> GetUserAsync(ulong userId)
{
await using var uow = _db.GetDbContext();
var user = await uow
.GetTable<DiscordUser>()
.FirstOrDefaultAsyncLinqToDB(u => u.UserId == userId);
return user;
}
}
public class XpService : INService, IReadyExecutor, IExecNoCommand public class XpService : INService, IReadyExecutor, IExecNoCommand
{ {
private readonly DbService _db; private readonly DbService _db;
@@ -72,6 +48,7 @@ public class XpService : INService, IReadyExecutor, IExecNoCommand
private readonly QueueRunner _levelUpQueue = new QueueRunner(0, 50); private readonly QueueRunner _levelUpQueue = new QueueRunner(0, 50);
private readonly Channel<UserXpGainData> _xpGainQueue = Channel.CreateUnbounded<UserXpGainData>(); private readonly Channel<UserXpGainData> _xpGainQueue = Channel.CreateUnbounded<UserXpGainData>();
private readonly IMessageSenderService _sender; private readonly IMessageSenderService _sender;
private readonly INotifySubscriber _notifySub;
public XpService( public XpService(
DiscordSocketClient client, DiscordSocketClient client,
@@ -87,7 +64,8 @@ public class XpService : INService, IReadyExecutor, IExecNoCommand
XpConfigService xpConfig, XpConfigService xpConfig,
IPubSub pubSub, IPubSub pubSub,
IPatronageService ps, IPatronageService ps,
IMessageSenderService sender) IMessageSenderService sender,
INotifySubscriber notifySub)
{ {
_db = db; _db = db;
_images = images; _images = images;
@@ -99,6 +77,7 @@ public class XpService : INService, IReadyExecutor, IExecNoCommand
_xpConfig = xpConfig; _xpConfig = xpConfig;
_pubSub = pubSub; _pubSub = pubSub;
_sender = sender; _sender = sender;
_notifySub = notifySub;
_excludedServers = new(); _excludedServers = new();
_excludedChannels = new(); _excludedChannels = new();
_client = client; _client = client;
@@ -189,9 +168,9 @@ public class XpService : INService, IReadyExecutor, IExecNoCommand
var dus = new List<DiscordUser>(globalToAdd.Count); var dus = new List<DiscordUser>(globalToAdd.Count);
var gxps = new List<UserXpStats>(globalToAdd.Count); var gxps = new List<UserXpStats>(globalToAdd.Count);
var conf = _xpConfig.Data;
await using (var ctx = _db.GetDbContext()) await using (var ctx = _db.GetDbContext())
{ {
var conf = _xpConfig.Data;
if (conf.CurrencyPerXp > 0) if (conf.CurrencyPerXp > 0)
{ {
foreach (var user in globalToAdd) foreach (var user in globalToAdd)
@@ -290,8 +269,7 @@ public class XpService : INService, IReadyExecutor, IExecNoCommand
du.UserId, du.UserId,
false, false,
oldLevel.Level, oldLevel.Level,
newLevel.Level, newLevel.Level));
du.NotifyOnLevelUp));
} }
} }
@@ -328,8 +306,7 @@ public class XpService : INService, IReadyExecutor, IExecNoCommand
ulong userId, ulong userId,
bool isServer, bool isServer,
long oldLevel, long oldLevel,
long newLevel, long newLevel)
XpNotificationLocation notifyLoc = XpNotificationLocation.None)
=> async () => => async () =>
{ {
if (isServer) if (isServer)
@@ -337,7 +314,7 @@ public class XpService : INService, IReadyExecutor, IExecNoCommand
await HandleRewardsInternalAsync(guildId, userId, oldLevel, newLevel); 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( private async Task HandleRewardsInternalAsync(
@@ -388,59 +365,25 @@ public class XpService : INService, IReadyExecutor, IExecNoCommand
ulong channelId, ulong channelId,
ulong userId, ulong userId,
bool isServer, bool isServer,
long newLevel, long newLevel)
XpNotificationLocation notifyLoc)
{ {
if (notifyLoc == XpNotificationLocation.None)
return;
var guild = _client.GetGuild(guildId); var guild = _client.GetGuild(guildId);
var user = guild?.GetUser(userId); var user = guild?.GetUser(userId);
var ch = guild?.GetTextChannel(channelId);
if (guild is null || user is null) if (guild is null || user is null)
return; return;
if (isServer) if (isServer)
{ {
if (notifyLoc == XpNotificationLocation.Dm) var model = new LevelUpNotifyModel()
{ {
await _sender.Response(user) GuildId = guildId,
.Confirm(_strings.GetText(strs.level_up_dm(user.Mention, UserId = userId,
Format.Bold(newLevel.ToString()), ChannelId = channelId,
Format.Bold(guild.ToString() ?? "-")), Level = newLevel
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
}; };
await _notifySub.NotifyAsync(model, true);
if (chan is null) return;
return;
await _sender.Response(chan)
.Confirm(_strings.GetText(strs.level_up_global(user.Mention,
Format.Bold(newLevel.ToString())),
guild.Id))
.SendAsync();
} }
} }
@@ -624,20 +567,6 @@ public class XpService : INService, IReadyExecutor, IExecNoCommand
.ToArrayAsyncLinqToDB(); .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) private Task Client_OnGuildAvailable(SocketGuild guild)
{ {
Task.Run(async () => Task.Run(async () =>
@@ -1639,28 +1568,20 @@ public class XpService : INService, IReadyExecutor, IExecNoCommand
await using var ctx = _db.GetDbContext(); await using var ctx = _db.GetDbContext();
await ctx.GetTable<UserXpStats>() await ctx.GetTable<UserXpStats>()
.InsertOrUpdateAsync(() => new() .InsertOrUpdateAsync(() => new()
{ {
GuildId = guildId, GuildId = guildId,
UserId = userId, UserId = userId,
Xp = lvlStats.TotalXp, Xp = lvlStats.TotalXp,
DateAdded = DateTime.UtcNow DateAdded = DateTime.UtcNow
}, (old) => new() },
{ (old) => new()
Xp = lvlStats.TotalXp {
}, () => new() Xp = lvlStats.TotalXp
{ },
GuildId = guildId, () => new()
UserId = userId {
}); GuildId = guildId,
UserId = userId
});
} }
} }
public enum BuyResult
{
Success,
XpShopDisabled,
AlreadyOwned,
InsufficientFunds,
UnknownItem,
InsufficientPatronTier,
}

View File

@@ -0,0 +1,2 @@
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=modules_005Cadministration_005Cnotify_005Cmodels/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>

View File

@@ -0,0 +1,8 @@
using NadekoBot.Db.Models;
namespace NadekoBot.Modules.Xp.Services;
public interface IUserService
{
Task<DiscordUser?> GetUserAsync(ulong userId);
}

View File

@@ -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<DiscordUser> GetUserAsync(ulong userId)
{
await using var uow = _db.GetDbContext();
var user = await uow
.GetTable<DiscordUser>()
.FirstOrDefaultAsyncLinqToDB(u => u.UserId == userId);
return user;
}
}

View File

@@ -1547,3 +1547,6 @@ minesweeper:
- mw - mw
temprole: temprole:
- temprole - temprole
notify:
- notify
- nfy

View File

@@ -4854,3 +4854,13 @@ minesweeper:
params: params:
- mines: - mines:
desc: "The number of mines to create." 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."