From 29bac7739d6be7b59ce785f3f14210d52f60db1b Mon Sep 17 00:00:00 2001 From: Toastie Date: Sun, 8 Dec 2024 19:37:22 +1300 Subject: [PATCH] added addrolereward and removerolereward events for .notify added .notify with no params showing events with descriptions added .winlb updated discord.net, redid migrations --- CHANGELOG.md | 38 ++++++ src/Ellie.Marmalade/Ellie.Marmalade.csproj | 2 +- src/EllieBot/Db/EllieContext.cs | 19 ++- src/EllieBot/Db/Models/Notify.cs | 2 + src/EllieBot/EllieBot.csproj | 2 +- src/EllieBot/Migrations/MigrationQueries.cs | 10 ++ ...0241208053644_awardedxp-temprole-notify.cs | 46 ------- ...342_awardedxp-temprole-notify.Designer.cs} | 5 +- ...0241208063342_awardedxp-temprole-notify.cs | 27 ++++ .../PostgreSqlContextModelSnapshot.cs | 3 + ...0241208053549_awardedxp-temprole-notify.cs | 46 ------- ...257_awardedxp-temprole-notify.Designer.cs} | 4 +- ...0241208063257_awardedxp-temprole-notify.cs | 27 ++++ .../Sqlite/EllieSqliteContextModelSnapshot.cs | 2 + .../Notify/Models/AddRoleRewardNotifyModel.cs | 36 +++++ .../Notify/Models/LevelUpNotifyModel.cs | 8 +- .../Notify/Models/ProtectionNotifyModel.cs | 34 +++++ .../Models/RemoveRoleRewardNotifyModel.cs | 36 +++++ .../Administration/Notify/NotifyCommands.cs | 96 ++++++++++++- .../Notify/NotifyModelExtensions.cs | 8 ++ .../Administration/Notify/NotifyService.cs | 24 ++++ .../Protection/ProtectionService.cs | 30 ----- .../Modules/Gambling/BetStatsCommands.cs | 127 ++++++++++++++---- .../Modules/Gambling/UserBetStatsService.cs | 12 ++ src/EllieBot/Modules/Searches/Searches.cs | 94 +++++++------ src/EllieBot/Modules/Xp/XpService.cs | 40 +++++- src/EllieBot/_common/DoAsUserMessage.cs | 3 + src/EllieBot/_common/Services/UserService.cs | 2 +- src/EllieBot/data/aliases.yml | 15 ++- .../data/strings/commands/commands.en-US.yml | 39 +++++- .../strings/responses/responses.en-US.json | 17 ++- 31 files changed, 635 insertions(+), 219 deletions(-) delete mode 100644 src/EllieBot/Migrations/PostgreSql/20241208053644_awardedxp-temprole-notify.cs rename src/EllieBot/Migrations/PostgreSql/{20241208053644_awardedxp-temprole-notify.Designer.cs => 20241208063342_awardedxp-temprole-notify.Designer.cs} (99%) create mode 100644 src/EllieBot/Migrations/PostgreSql/20241208063342_awardedxp-temprole-notify.cs delete mode 100644 src/EllieBot/Migrations/Sqlite/20241208053549_awardedxp-temprole-notify.cs rename src/EllieBot/Migrations/Sqlite/{20241208053549_awardedxp-temprole-notify.Designer.cs => 20241208063257_awardedxp-temprole-notify.Designer.cs} (99%) create mode 100644 src/EllieBot/Migrations/Sqlite/20241208063257_awardedxp-temprole-notify.cs create mode 100644 src/EllieBot/Modules/Administration/Notify/Models/AddRoleRewardNotifyModel.cs create mode 100644 src/EllieBot/Modules/Administration/Notify/Models/ProtectionNotifyModel.cs create mode 100644 src/EllieBot/Modules/Administration/Notify/Models/RemoveRoleRewardNotifyModel.cs create mode 100644 src/EllieBot/Modules/Administration/Notify/NotifyModelExtensions.cs diff --git a/CHANGELOG.md b/CHANGELOG.md index 8c6a779..d084c6a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,44 @@ Mostly based on [keepachangelog](https://keepachangelog.com/en/1.1.0/) except date format. a-c-f-r-o +## [5.3.0] - 08.12.2024 + +## Added + +- Added `.minesweeper` / `.mw` command - spoiler-based minesweeper minigame. Just for fun +- Added `.temprole` command - add a role to a user for a certain amount of time, after which the role will be removed +- Added `.xplevelset` - you can now set a level for a user in your server +- Added `.winlb` command - leaderboard of top gambling wins +- Added `.notify` command + - Specify an event to be notified about, and the bot will post the specified message in the current channel when the event occurs + - A few events supported right now: + - `UserLevelUp` when user levels up in the server + - `AddRoleReward` when a role is added to a user through .xpreward system + - `RemoveRoleReward` when a role is removed from a user through .xpreward system + - `Protection` when antialt, antiraid or antispam protection is triggered +- Added `.banner` command to see someone's banner +- Selfhosters: + - Added `.dmmod` and `.dmcmd` - you can now disable or enable whether commands or modules can be executed in bot's DMs + +## Changed + +- Giveaway improvements + - Now mentions winners in a separate message + - Shows the timestamp of when the giveaway ends +- Xp Changes + - Removed awarded xp (the number in the brackets on the xp card) + - Awarded xp, (or the new level set) now directly apply to user's real xp + - Server xp notifications are now set by the server admin/manager in a specified channel +- `.sclr show` will now show hex code of the current color +- Queueing a song will now restart the playback if the queue is on the last track and stopped (there were no more tracks to play) + +## Fixed + +- .setstream and .setactivity will now pause .ropl (rotating statuses) + +## Removed + + ## [5.2.4] - 29.11.2024 ## Fixed diff --git a/src/Ellie.Marmalade/Ellie.Marmalade.csproj b/src/Ellie.Marmalade/Ellie.Marmalade.csproj index 8371f74..b64d92b 100644 --- a/src/Ellie.Marmalade/Ellie.Marmalade.csproj +++ b/src/Ellie.Marmalade/Ellie.Marmalade.csproj @@ -9,7 +9,7 @@ - + diff --git a/src/EllieBot/Db/EllieContext.cs b/src/EllieBot/Db/EllieContext.cs index 6b86daf..011fdb1 100644 --- a/src/EllieBot/Db/EllieContext.cs +++ b/src/EllieBot/Db/EllieContext.cs @@ -164,13 +164,18 @@ public abstract class EllieContext : DbContext #region UserBetStats - modelBuilder.Entity() - .HasIndex(x => new - { - x.UserId, - x.Game - }) - .IsUnique(); + modelBuilder.Entity(ubs => + { + ubs.HasIndex(x => new + { + x.UserId, + x.Game + }) + .IsUnique(); + + ubs.HasIndex(x => x.MaxWin) + .IsUnique(false); + }); #endregion diff --git a/src/EllieBot/Db/Models/Notify.cs b/src/EllieBot/Db/Models/Notify.cs index 90d30d3..caa8db3 100644 --- a/src/EllieBot/Db/Models/Notify.cs +++ b/src/EllieBot/Db/Models/Notify.cs @@ -19,4 +19,6 @@ public enum NotifyType { LevelUp = 0, Protection = 1, Prot = 1, + AddRoleReward = 2, + RemoveRoleReward = 3, } \ No newline at end of file diff --git a/src/EllieBot/EllieBot.csproj b/src/EllieBot/EllieBot.csproj index 11c77a3..53e24f2 100644 --- a/src/EllieBot/EllieBot.csproj +++ b/src/EllieBot/EllieBot.csproj @@ -29,7 +29,7 @@ - + diff --git a/src/EllieBot/Migrations/MigrationQueries.cs b/src/EllieBot/Migrations/MigrationQueries.cs index c68376c..4e116bc 100644 --- a/src/EllieBot/Migrations/MigrationQueries.cs +++ b/src/EllieBot/Migrations/MigrationQueries.cs @@ -5,6 +5,16 @@ namespace EllieBot.Migrations; public static class MigrationQueries { + public static void MergeAwardedXp(MigrationBuilder migrationBuilder) + { + migrationBuilder.Sql(""" + UPDATE UserXpStats + SET Xp = AwardedXp + Xp, + AwardedXp = 0 + WHERE AwardedXp > 0; + """); + } + public static void MigrateSar(MigrationBuilder migrationBuilder) { migrationBuilder.Sql(""" diff --git a/src/EllieBot/Migrations/PostgreSql/20241208053644_awardedxp-temprole-notify.cs b/src/EllieBot/Migrations/PostgreSql/20241208053644_awardedxp-temprole-notify.cs deleted file mode 100644 index a76cc4a..0000000 --- a/src/EllieBot/Migrations/PostgreSql/20241208053644_awardedxp-temprole-notify.cs +++ /dev/null @@ -1,46 +0,0 @@ -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/20241208053644_awardedxp-temprole-notify.Designer.cs b/src/EllieBot/Migrations/PostgreSql/20241208063342_awardedxp-temprole-notify.Designer.cs similarity index 99% rename from src/EllieBot/Migrations/PostgreSql/20241208053644_awardedxp-temprole-notify.Designer.cs rename to src/EllieBot/Migrations/PostgreSql/20241208063342_awardedxp-temprole-notify.Designer.cs index b56d51a..af3a0b8 100644 --- a/src/EllieBot/Migrations/PostgreSql/20241208053644_awardedxp-temprole-notify.Designer.cs +++ b/src/EllieBot/Migrations/PostgreSql/20241208063342_awardedxp-temprole-notify.Designer.cs @@ -12,7 +12,7 @@ using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; namespace EllieBot.Migrations.PostgreSql { [DbContext(typeof(PostgreSqlContext))] - [Migration("20241208053644_awardedxp-temprole-notify")] + [Migration("20241208063342_awardedxp-temprole-notify")] partial class awardedxptemprolenotify { /// @@ -3485,6 +3485,9 @@ namespace EllieBot.Migrations.PostgreSql b.HasKey("Id") .HasName("pk_userbetstats"); + b.HasIndex("MaxWin") + .HasDatabaseName("ix_userbetstats_maxwin"); + b.HasIndex("UserId", "Game") .IsUnique() .HasDatabaseName("ix_userbetstats_userid_game"); diff --git a/src/EllieBot/Migrations/PostgreSql/20241208063342_awardedxp-temprole-notify.cs b/src/EllieBot/Migrations/PostgreSql/20241208063342_awardedxp-temprole-notify.cs new file mode 100644 index 0000000..fd8e7f3 --- /dev/null +++ b/src/EllieBot/Migrations/PostgreSql/20241208063342_awardedxp-temprole-notify.cs @@ -0,0 +1,27 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace EllieBot.Migrations.PostgreSql +{ + /// + public partial class awardedxptemprolenotify : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateIndex( + name: "ix_userbetstats_maxwin", + table: "userbetstats", + column: "maxwin"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropIndex( + name: "ix_userbetstats_maxwin", + table: "userbetstats"); + } + } +} diff --git a/src/EllieBot/Migrations/PostgreSql/PostgreSqlContextModelSnapshot.cs b/src/EllieBot/Migrations/PostgreSql/PostgreSqlContextModelSnapshot.cs index e691bbc..ac77d7b 100644 --- a/src/EllieBot/Migrations/PostgreSql/PostgreSqlContextModelSnapshot.cs +++ b/src/EllieBot/Migrations/PostgreSql/PostgreSqlContextModelSnapshot.cs @@ -3482,6 +3482,9 @@ namespace EllieBot.Migrations.PostgreSql b.HasKey("Id") .HasName("pk_userbetstats"); + b.HasIndex("MaxWin") + .HasDatabaseName("ix_userbetstats_maxwin"); + b.HasIndex("UserId", "Game") .IsUnique() .HasDatabaseName("ix_userbetstats_userid_game"); diff --git a/src/EllieBot/Migrations/Sqlite/20241208053549_awardedxp-temprole-notify.cs b/src/EllieBot/Migrations/Sqlite/20241208053549_awardedxp-temprole-notify.cs deleted file mode 100644 index 8281303..0000000 --- a/src/EllieBot/Migrations/Sqlite/20241208053549_awardedxp-temprole-notify.cs +++ /dev/null @@ -1,46 +0,0 @@ -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/20241208053549_awardedxp-temprole-notify.Designer.cs b/src/EllieBot/Migrations/Sqlite/20241208063257_awardedxp-temprole-notify.Designer.cs similarity index 99% rename from src/EllieBot/Migrations/Sqlite/20241208053549_awardedxp-temprole-notify.Designer.cs rename to src/EllieBot/Migrations/Sqlite/20241208063257_awardedxp-temprole-notify.Designer.cs index c9b5e7e..9199fe0 100644 --- a/src/EllieBot/Migrations/Sqlite/20241208053549_awardedxp-temprole-notify.Designer.cs +++ b/src/EllieBot/Migrations/Sqlite/20241208063257_awardedxp-temprole-notify.Designer.cs @@ -11,7 +11,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion; namespace EllieBot.Migrations { [DbContext(typeof(SqliteContext))] - [Migration("20241208053549_awardedxp-temprole-notify")] + [Migration("20241208063257_awardedxp-temprole-notify")] partial class awardedxptemprolenotify { /// @@ -2593,6 +2593,8 @@ namespace EllieBot.Migrations b.HasKey("Id"); + b.HasIndex("MaxWin"); + b.HasIndex("UserId", "Game") .IsUnique(); diff --git a/src/EllieBot/Migrations/Sqlite/20241208063257_awardedxp-temprole-notify.cs b/src/EllieBot/Migrations/Sqlite/20241208063257_awardedxp-temprole-notify.cs new file mode 100644 index 0000000..6caf35b --- /dev/null +++ b/src/EllieBot/Migrations/Sqlite/20241208063257_awardedxp-temprole-notify.cs @@ -0,0 +1,27 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace EllieBot.Migrations +{ + /// + public partial class awardedxptemprolenotify : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateIndex( + name: "IX_UserBetStats_MaxWin", + table: "UserBetStats", + column: "MaxWin"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropIndex( + name: "IX_UserBetStats_MaxWin", + table: "UserBetStats"); + } + } +} diff --git a/src/EllieBot/Migrations/Sqlite/EllieSqliteContextModelSnapshot.cs b/src/EllieBot/Migrations/Sqlite/EllieSqliteContextModelSnapshot.cs index 657bd0a..d249af3 100644 --- a/src/EllieBot/Migrations/Sqlite/EllieSqliteContextModelSnapshot.cs +++ b/src/EllieBot/Migrations/Sqlite/EllieSqliteContextModelSnapshot.cs @@ -2590,6 +2590,8 @@ namespace EllieBot.Migrations b.HasKey("Id"); + b.HasIndex("MaxWin"); + b.HasIndex("UserId", "Game") .IsUnique(); diff --git a/src/EllieBot/Modules/Administration/Notify/Models/AddRoleRewardNotifyModel.cs b/src/EllieBot/Modules/Administration/Notify/Models/AddRoleRewardNotifyModel.cs new file mode 100644 index 0000000..d6f8d37 --- /dev/null +++ b/src/EllieBot/Modules/Administration/Notify/Models/AddRoleRewardNotifyModel.cs @@ -0,0 +1,36 @@ +using EllieBot.Db.Models; +using EllieBot.Modules.Administration; + +namespace EllieBot.Modules.Xp.Services; + +public record struct AddRoleRewardNotifyModel(ulong GuildId, ulong RoleId, ulong UserId, long Level) : INotifyModel +{ + public static string KeyName + => "notify.reward.addrole"; + + public static NotifyType NotifyType + => NotifyType.AddRoleReward; + + public IReadOnlyDictionary> GetReplacements() + { + var model = this; + return new Dictionary>() + { + { "%event.user%", g => g.GetUser(model.UserId)?.ToString() ?? model.UserId.ToString() }, + { "%event.role%", g => g.GetRole(model.RoleId)?.ToString() ?? model.RoleId.ToString() }, + { "%event.level%", g => model.Level.ToString() } + }; + } + + public bool TryGetUserId(out ulong userId) + { + userId = UserId; + return true; + } + + public bool TryGetGuildId(out ulong guildId) + { + guildId = GuildId; + return true; + } +} diff --git a/src/EllieBot/Modules/Administration/Notify/Models/LevelUpNotifyModel.cs b/src/EllieBot/Modules/Administration/Notify/Models/LevelUpNotifyModel.cs index 3495c09..dc85d3e 100644 --- a/src/EllieBot/Modules/Administration/Notify/Models/LevelUpNotifyModel.cs +++ b/src/EllieBot/Modules/Administration/Notify/Models/LevelUpNotifyModel.cs @@ -20,6 +20,7 @@ public record struct LevelUpNotifyModel( return new Dictionary>() { { "%event.level%", g => data.Level.ToString() }, + { "%event.user%", g => g.GetUser(data.UserId)?.ToString() ?? data.UserId.ToString() }, }; } @@ -34,11 +35,4 @@ public record struct LevelUpNotifyModel( 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/Models/ProtectionNotifyModel.cs b/src/EllieBot/Modules/Administration/Notify/Models/ProtectionNotifyModel.cs new file mode 100644 index 0000000..bc23531 --- /dev/null +++ b/src/EllieBot/Modules/Administration/Notify/Models/ProtectionNotifyModel.cs @@ -0,0 +1,34 @@ +#nullable disable +using EllieBot.Db.Models; + +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; + } +} \ No newline at end of file diff --git a/src/EllieBot/Modules/Administration/Notify/Models/RemoveRoleRewardNotifyModel.cs b/src/EllieBot/Modules/Administration/Notify/Models/RemoveRoleRewardNotifyModel.cs new file mode 100644 index 0000000..f078925 --- /dev/null +++ b/src/EllieBot/Modules/Administration/Notify/Models/RemoveRoleRewardNotifyModel.cs @@ -0,0 +1,36 @@ +using EllieBot.Db.Models; +using EllieBot.Modules.Administration; + +namespace EllieBot.Modules.Xp.Services; + +public record struct RemoveRoleRewardNotifyModel(ulong GuildId, ulong RoleId, ulong UserId, long Level) : INotifyModel +{ + public static string KeyName + => "notify.reward.removerole"; + + public static NotifyType NotifyType + => NotifyType.RemoveRoleReward; + + public IReadOnlyDictionary> GetReplacements() + { + var model = this; + return new Dictionary>() + { + { "%event.user%", g => g.GetUser(model.UserId)?.ToString() ?? model.UserId.ToString() }, + { "%event.role%", g => g.GetRole(model.RoleId)?.ToString() ?? model.RoleId.ToString() }, + { "%event.level%", g => model.Level.ToString() }, + }; + } + + public bool TryGetUserId(out ulong userId) + { + userId = UserId; + return true; + } + + public bool TryGetGuildId(out ulong guildId) + { + guildId = GuildId; + return true; + } +} diff --git a/src/EllieBot/Modules/Administration/Notify/NotifyCommands.cs b/src/EllieBot/Modules/Administration/Notify/NotifyCommands.cs index 592573a..479bdff 100644 --- a/src/EllieBot/Modules/Administration/Notify/NotifyCommands.cs +++ b/src/EllieBot/Modules/Administration/Notify/NotifyCommands.cs @@ -1,4 +1,5 @@ using EllieBot.Db.Models; +using System.Text; namespace EllieBot.Modules.Administration; @@ -6,19 +7,108 @@ public partial class Administration { public class NotifyCommands : EllieModule { + [Cmd] + [OwnerOnly] + public async Task Notify() + { + await Response() + .Paginated() + .Items(Enum.GetValues()) + .PageSize(5) + .Page((items, page) => + { + var eb = CreateEmbed() + .WithOkColor() + .WithTitle(GetText(strs.notify_available)); + + foreach (var item in items) + { + eb.AddField(item.ToString(), GetText(GetDescription(item)), false); + } + + return eb; + }) + .SendAsync(); + } + + private LocStr GetDescription(NotifyType item) + => item switch + { + NotifyType.LevelUp => strs.notify_desc_levelup, + NotifyType.Protection => strs.notify_desc_protection, + NotifyType.AddRoleReward => strs.notify_desc_addrolerew, + NotifyType.RemoveRoleReward => strs.notify_desc_removerolerew, + _ => strs.notify_desc_not_found + }; + [Cmd] [OwnerOnly] public async Task Notify(NotifyType nType, [Leftover] string? message = null) { if (string.IsNullOrWhiteSpace(message)) { - await _service.DisableAsync(ctx.Guild.Id, nType); - await Response().Confirm(strs.notify_off(nType)).SendAsync(); + // show msg + var conf = await _service.GetNotifyAsync(ctx.Guild.Id, nType); + if (conf is null) + { + await Response().Confirm(strs.notify_msg_not_set).SendAsync(); + return; + } + + var eb = CreateEmbed() + .WithOkColor() + .WithTitle(GetText(strs.notify_msg)) + .WithDescription(conf.Message.TrimTo(2048)) + .AddField(GetText(strs.notify_type), conf.Type.ToString(), true) + .AddField(GetText(strs.channel), + $""" + <#{conf.ChannelId}> + `{conf.ChannelId}` + """, + true); + + await Response().Embed(eb).SendAsync(); return; } await _service.EnableAsync(ctx.Guild.Id, ctx.Channel.Id, nType, message); - await Response().Confirm(strs.notify_on(nType.ToString())).SendAsync(); + await Response().Confirm(strs.notify_on($"<#{ctx.Channel.Id}>", Format.Bold(nType.ToString()))).SendAsync(); + } + + [Cmd] + [OwnerOnly] + public async Task NotifyList(int page = 1) + { + if (--page < 0) + return; + + var notifs = await _service.GetForGuildAsync(ctx.Guild.Id); + + var sb = new StringBuilder(); + + foreach (var notif in notifs) + { + sb.AppendLine($""" + - **{notif.Type}** + <#{notif.ChannelId}> `{notif.ChannelId}` + + """); + } + + if (notifs.Count == 0) + sb.AppendLine(GetText(strs.notify_none)); + + await Response() + .Confirm(GetText(strs.notify_list), text: sb.ToString()) + .SendAsync(); + } + + [Cmd] + [OwnerOnly] + public async Task NotifyClear(NotifyType nType) + { + await _service.DisableAsync(ctx.Guild.Id, nType); + await Response().Confirm(strs.notify_off(nType)).SendAsync(); } } } \ No newline at end of file diff --git a/src/EllieBot/Modules/Administration/Notify/NotifyModelExtensions.cs b/src/EllieBot/Modules/Administration/Notify/NotifyModelExtensions.cs new file mode 100644 index 0000000..9aca824 --- /dev/null +++ b/src/EllieBot/Modules/Administration/Notify/NotifyModelExtensions.cs @@ -0,0 +1,8 @@ +namespace EllieBot.Modules.Administration; + +public static class NotifyModelExtensions +{ + 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/NotifyService.cs b/src/EllieBot/Modules/Administration/Notify/NotifyService.cs index 892dde9..3d2fadb 100644 --- a/src/EllieBot/Modules/Administration/Notify/NotifyService.cs +++ b/src/EllieBot/Modules/Administration/Notify/NotifyService.cs @@ -2,6 +2,7 @@ using LinqToDB.EntityFrameworkCore; using EllieBot.Common.ModuleBehaviors; using EllieBot.Db.Models; +using EllieBot.Generators; namespace EllieBot.Modules.Administration; @@ -199,4 +200,27 @@ public sealed class NotifyService : IReadyExecutor, INotifySubscriber, IEService guildsDict.TryRemove(guildId, out _); } + + public async Task> GetForGuildAsync(ulong guildId, int page = 0) + { + ArgumentOutOfRangeException.ThrowIfNegative(page); + + await using var ctx = _db.GetDbContext(); + var list = await ctx.GetTable() + .Where(x => x.GuildId == guildId) + .OrderBy(x => x.Type) + .Skip(page * 10) + .Take(10) + .ToListAsyncLinqToDB(); + + return list; + } + + public async Task GetNotifyAsync(ulong guildId, NotifyType nType) + { + await using var ctx = _db.GetDbContext(); + return await ctx.GetTable() + .Where(x => x.GuildId == guildId && x.Type == nType) + .FirstOrDefaultAsyncLinqToDB(); + } } diff --git a/src/EllieBot/Modules/Administration/Protection/ProtectionService.cs b/src/EllieBot/Modules/Administration/Protection/ProtectionService.cs index 2d0ba0e..b43f128 100644 --- a/src/EllieBot/Modules/Administration/Protection/ProtectionService.cs +++ b/src/EllieBot/Modules/Administration/Protection/ProtectionService.cs @@ -5,36 +5,6 @@ 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 diff --git a/src/EllieBot/Modules/Gambling/BetStatsCommands.cs b/src/EllieBot/Modules/Gambling/BetStatsCommands.cs index f9ba829..8eb19b7 100644 --- a/src/EllieBot/Modules/Gambling/BetStatsCommands.cs +++ b/src/EllieBot/Modules/Gambling/BetStatsCommands.cs @@ -1,6 +1,7 @@ #nullable disable using EllieBot.Modules.Gambling.Common; using EllieBot.Modules.Gambling.Services; +using EllieBot.Modules.Xp.Services; namespace EllieBot.Modules.Gambling; @@ -10,13 +11,19 @@ public partial class Gambling public sealed class BetStatsCommands : GamblingModule { private readonly GamblingTxTracker _gamblingTxTracker; + private readonly IBotCache _cache; + private readonly IUserService _userService; public BetStatsCommands( GamblingTxTracker gamblingTxTracker, - GamblingConfigService gcs) + GamblingConfigService gcs, + IBotCache cache, + IUserService userService) : base(gcs) { _gamblingTxTracker = gamblingTxTracker; + _cache = cache; + _userService = userService; } [Cmd] @@ -25,12 +32,12 @@ public partial class Gambling var price = await _service.GetResetStatsPriceAsync(ctx.User.Id, game); var result = await PromptUserConfirmAsync(CreateEmbed() - .WithDescription( - $""" - Are you sure you want to reset your bet stats for **{GetGameName(game)}**? + .WithDescription( + $""" + Are you sure you want to reset your bet stats for **{GetGameName(game)}**? - It will cost you {N(price)} - """)); + It will cost you {N(price)} + """)); if (!result) return; @@ -88,15 +95,15 @@ public partial class Gambling }; var eb = CreateEmbed() - .WithOkColor() - .WithAuthor(user) - .AddField("Total Won", N(stats.Sum(x => x.PaidOut)), true) - .AddField("Biggest Win", N(stats.Max(x => x.MaxWin)), true) - .AddField("Biggest Bet", N(stats.Max(x => x.MaxBet)), true) - .AddField("# Bets", stats.Sum(x => x.WinCount + x.LoseCount), true) - .AddField("Payout", - (stats.Sum(x => x.PaidOut) / stats.Sum(x => x.TotalBet)).ToString("P2", Culture), - true); + .WithOkColor() + .WithAuthor(user) + .AddField("Total Won", N(stats.Sum(x => x.PaidOut)), true) + .AddField("Biggest Win", N(stats.Max(x => x.MaxWin)), true) + .AddField("Biggest Bet", N(stats.Max(x => x.MaxBet)), true) + .AddField("# Bets", stats.Sum(x => x.WinCount + x.LoseCount), true) + .AddField("Payout", + (stats.Sum(x => x.PaidOut) / stats.Sum(x => x.TotalBet)).ToString("P2", Culture), + true); if (game == null) { var favGame = stats.MaxBy(x => x.WinCount + x.LoseCount); @@ -115,23 +122,85 @@ public partial class Gambling .SendAsync(); } + private readonly record struct WinLbStat( + int Rank, + string User, + GamblingGame Game, + long MaxWin); + + private TypedKey> GetWinLbKey(int page) + => new($"winlb:{page}"); + + private async Task> GetCachedWinLbAsync(int page) + { + return await _cache.GetOrAddAsync(GetWinLbKey(page), + async () => + { + var items = await _service.GetWinLbAsync(page); + + if (items.Count == 0) + return []; + + var outputItems = new List(items.Count); + for (var i = 0; i < items.Count; i++) + { + var x = items[i]; + var user = (await ctx.Client.GetUserAsync(x.UserId, CacheMode.CacheOnly))?.ToString() + ?? (await _userService.GetUserAsync(x.UserId))?.Username + ?? x.UserId.ToString(); + + outputItems.Add(new WinLbStat(i + 1 + (page * 10), user, x.Game, x.MaxWin)); + } + + return outputItems; + }, + expiry: TimeSpan.FromMinutes(5)); + } + + [Cmd] + public async Task WinLb(int page = 1) + { + if (--page < 0) + return; + + await Response() + .Paginated() + .PageItems(p => GetCachedWinLbAsync(p)) + .PageSize(10) + .Page((items, curPage) => + { + var eb = CreateEmbed() + .WithOkColor(); + + for (var i = 0; i < items.Count; i++) + { + var item = items[i]; + eb.AddField($"#{item.Rank} {item.User}", + $"[{item.Game}]{N(item.MaxWin)}"); + } + + return eb; + }) + .SendAsync(); + } + [Cmd] public async Task GambleStats() { var stats = await _gamblingTxTracker.GetAllAsync(); var eb = CreateEmbed() - .WithOkColor(); + .WithOkColor(); - var str = "` Feature `|` Bet `|`Paid Out`|` RoI `\n"; + var str = "` Feature `|`   Bet  `|`Paid Out`|`  RoI  `\n"; str += "――――――――――――――――――――\n"; foreach (var stat in stats) { var perc = (stat.PaidOut / stat.Bet).ToString("P2", Culture); str += $"`{stat.Feature.PadBoth(9)}`" - + $"|`{stat.Bet.ToString("N0").PadLeft(8, ' ')}`" - + $"|`{stat.PaidOut.ToString("N0").PadLeft(8, ' ')}`" - + $"|`{perc.PadLeft(6, ' ')}`\n"; + + $"|`{stat.Bet.ToString("N0").PadLeft(8, ' ')}`" + + $"|`{stat.PaidOut.ToString("N0").PadLeft(8, ' ')}`" + + $"|`{perc.PadLeft(6, ' ')}`\n"; } var bet = stats.Sum(x => x.Bet); @@ -143,9 +212,9 @@ public partial class Gambling var tPerc = (paidOut / bet).ToString("P2", Culture); str += "――――――――――――――――――――\n"; str += $"` {("TOTAL").PadBoth(7)}` " - + $"|**{N(bet).PadLeft(8, ' ')}**" - + $"|**{N(paidOut).PadLeft(8, ' ')}**" - + $"|`{tPerc.PadLeft(6, ' ')}`"; + + $"|**{N(bet).PadLeft(8, ' ')}**" + + $"|**{N(paidOut).PadLeft(8, ' ')}**" + + $"|`{tPerc.PadLeft(6, ' ')}`"; eb.WithDescription(str); @@ -157,13 +226,13 @@ public partial class Gambling public async Task GambleStatsReset() { if (!await PromptUserConfirmAsync(CreateEmbed() - .WithDescription( - """ - Are you sure? - This will completely reset Gambling Stats. + .WithDescription( + """ + Are you sure? + This will completely reset Gambling Stats. - This action is irreversible. - """))) + This action is irreversible. + """))) return; await GambleStats(); diff --git a/src/EllieBot/Modules/Gambling/UserBetStatsService.cs b/src/EllieBot/Modules/Gambling/UserBetStatsService.cs index 76f7781..6b5e5bb 100644 --- a/src/EllieBot/Modules/Gambling/UserBetStatsService.cs +++ b/src/EllieBot/Modules/Gambling/UserBetStatsService.cs @@ -52,4 +52,16 @@ public sealed class UserBetStatsService : IEService await ctx.GetTable() .DeleteAsync(); } + + public async Task> GetWinLbAsync(int page) + { + ArgumentOutOfRangeException.ThrowIfNegative(page); + + await using var ctx = _db.GetDbContext(); + return await ctx.GetTable() + .OrderByDescending(x => x.MaxWin) + .Skip(page * 10) + .Take(10) + .ToArrayAsyncLinqToDB(); + } } \ No newline at end of file diff --git a/src/EllieBot/Modules/Searches/Searches.cs b/src/EllieBot/Modules/Searches/Searches.cs index de40406..00459ab 100644 --- a/src/EllieBot/Modules/Searches/Searches.cs +++ b/src/EllieBot/Modules/Searches/Searches.cs @@ -103,11 +103,11 @@ public partial class Searches : EllieModule } var eb = CreateEmbed() - .WithOkColor() - .WithTitle(GetText(strs.time_new)) - .WithDescription(Format.Code(data.Time.ToString(Culture))) - .AddField(GetText(strs.location), string.Join('\n', data.Address.Split(", ")), true) - .AddField(GetText(strs.timezone), data.TimeZoneName, true); + .WithOkColor() + .WithTitle(GetText(strs.time_new)) + .WithDescription(Format.Code(data.Time.ToString(Culture))) + .AddField(GetText(strs.location), string.Join('\n', data.Address.Split(", ")), true) + .AddField(GetText(strs.timezone), data.TimeZoneName, true); await Response().Embed(eb).SendAsync(); } @@ -129,16 +129,16 @@ public partial class Searches : EllieModule await Response() .Embed(CreateEmbed() - .WithOkColor() - .WithTitle(movie.Title) - .WithUrl($"https://www.imdb.com/title/{movie.ImdbId}/") - .WithDescription(movie.Plot.TrimTo(1000)) - .AddField("Rating", movie.ImdbRating, true) - .AddField("Genre", movie.Genre, true) - .AddField("Year", movie.Year, true) - .WithImageUrl(Uri.IsWellFormedUriString(movie.Poster, UriKind.Absolute) - ? movie.Poster - : null)) + .WithOkColor() + .WithTitle(movie.Title) + .WithUrl($"https://www.imdb.com/title/{movie.ImdbId}/") + .WithDescription(movie.Plot.TrimTo(1000)) + .AddField("Rating", movie.ImdbRating, true) + .AddField("Genre", movie.Genre, true) + .AddField("Year", movie.Year, true) + .WithImageUrl(Uri.IsWellFormedUriString(movie.Poster, UriKind.Absolute) + ? movie.Poster + : null)) .SendAsync(); } @@ -191,9 +191,9 @@ public partial class Searches : EllieModule await Response() .Embed(CreateEmbed() - .WithOkColor() - .AddField(GetText(strs.original_url), $"<{query}>") - .AddField(GetText(strs.short_url), $"<{shortLink}>")) + .WithOkColor() + .AddField(GetText(strs.original_url), $"<{query}>") + .AddField(GetText(strs.short_url), $"<{shortLink}>")) .SendAsync(); } @@ -214,13 +214,13 @@ public partial class Searches : EllieModule } var embed = CreateEmbed() - .WithOkColor() - .WithTitle(card.Name) - .WithDescription(card.Description) - .WithImageUrl(card.ImageUrl) - .AddField(GetText(strs.store_url), card.StoreUrl, true) - .AddField(GetText(strs.cost), card.ManaCost, true) - .AddField(GetText(strs.types), card.Types, true); + .WithOkColor() + .WithTitle(card.Name) + .WithDescription(card.Description) + .WithImageUrl(card.ImageUrl) + .AddField(GetText(strs.store_url), card.StoreUrl, true) + .AddField(GetText(strs.cost), card.ManaCost, true) + .AddField(GetText(strs.types), card.Types, true); await Response().Embed(embed).SendAsync(); } @@ -281,10 +281,10 @@ public partial class Searches : EllieModule { var item = items[0]; return CreateEmbed() - .WithOkColor() - .WithUrl(item.Permalink) - .WithTitle(item.Word) - .WithDescription(item.Definition); + .WithOkColor() + .WithUrl(item.Permalink) + .WithTitle(item.Word) + .WithDescription(item.Definition); }) .SendAsync(); } @@ -312,11 +312,11 @@ public partial class Searches : EllieModule { var model = items.First(); var embed = CreateEmbed() - .WithDescription(ctx.User.Mention) - .AddField(GetText(strs.word), model.Word, true) - .AddField(GetText(strs._class), model.WordType, true) - .AddField(GetText(strs.definition), model.Definition) - .WithOkColor(); + .WithDescription(ctx.User.Mention) + .AddField(GetText(strs.word), model.Word, true) + .AddField(GetText(strs._class), model.WordType, true) + .AddField(GetText(strs.definition), model.Definition) + .WithOkColor(); if (!string.IsNullOrWhiteSpace(model.Example)) embed.AddField(GetText(strs.example), model.Example); @@ -404,10 +404,28 @@ public partial class Searches : EllieModule await Response() .Embed( CreateEmbed() - .WithOkColor() - .AddField("Username", usr.ToString()) - .AddField("Avatar Url", avatarUrl) - .WithThumbnailUrl(avatarUrl.ToString())) + .WithOkColor() + .AddField("Username", usr.ToString()) + .AddField("Avatar Url", avatarUrl) + .WithThumbnailUrl(avatarUrl.ToString())) + .SendAsync(); + } + + [Cmd] + [RequireContext(ContextType.Guild)] + public async Task Banner([Leftover] IGuildUser? usr = null) + { + usr ??= (IGuildUser)ctx.User; + + var bannerUrl = usr.GetGuildBannerUrl(); + + await Response() + .Embed( + CreateEmbed() + .WithOkColor() + .AddField("Username", usr.ToString()) + .AddField("Banner Url", bannerUrl) + .WithThumbnailUrl(bannerUrl)) .SendAsync(); } diff --git a/src/EllieBot/Modules/Xp/XpService.cs b/src/EllieBot/Modules/Xp/XpService.cs index afff7d8..de539a4 100644 --- a/src/EllieBot/Modules/Xp/XpService.cs +++ b/src/EllieBot/Modules/Xp/XpService.cs @@ -344,9 +344,45 @@ public class XpService : IEService, IReadyExecutor, IExecNoCommand if (role is not null && user is not null) { if (rrew.Remove) - _ = user.RemoveRoleAsync(role); + { + try + { + await user.RemoveRoleAsync(role); + await _notifySub.NotifyAsync(new RemoveRoleRewardNotifyModel(guild.Id, + role.Id, + user.Id, + newLevel), + isShardLocal: true); + } + catch (Exception ex) + { + Log.Warning(ex, + "Unable to remove role {RoleId} from user {UserId}: {Message}", + role.Id, + user.Id, + ex.Message); + } + } else - _ = user.AddRoleAsync(role); + { + try + { + await user.AddRoleAsync(role); + await _notifySub.NotifyAsync(new AddRoleRewardNotifyModel(guild.Id, + role.Id, + user.Id, + newLevel), + isShardLocal: true); + } + catch (Exception ex) + { + Log.Warning(ex, + "Unable to add role {RoleId} to user {UserId}: {Message}", + role.Id, + user.Id, + ex.Message); + } + } } } diff --git a/src/EllieBot/_common/DoAsUserMessage.cs b/src/EllieBot/_common/DoAsUserMessage.cs index 68a9188..e9c9d55 100644 --- a/src/EllieBot/_common/DoAsUserMessage.cs +++ b/src/EllieBot/_common/DoAsUserMessage.cs @@ -157,6 +157,9 @@ public sealed class DoAsUserMessage : IUserMessage public MessageCallData? CallData => _msg.CallData; + public IReadOnlyCollection ForwardedMessages + => _msg.ForwardedMessages; + public Task ModifyAsync(Action func, RequestOptions? options = null) { return _msg.ModifyAsync(func, options); diff --git a/src/EllieBot/_common/Services/UserService.cs b/src/EllieBot/_common/Services/UserService.cs index 7d5ad70..6ce71e7 100644 --- a/src/EllieBot/_common/Services/UserService.cs +++ b/src/EllieBot/_common/Services/UserService.cs @@ -12,7 +12,7 @@ public sealed class UserService : IUserService, IEService _db = db; } - public async Task GetUserAsync(ulong userId) + public async Task GetUserAsync(ulong userId) { await using var uow = _db.GetDbContext(); var user = await uow diff --git a/src/EllieBot/data/aliases.yml b/src/EllieBot/data/aliases.yml index f43750e..8bd9a7d 100644 --- a/src/EllieBot/data/aliases.yml +++ b/src/EllieBot/data/aliases.yml @@ -715,6 +715,8 @@ color: avatar: - avatar - av +banner: + - banner translate: - translate - trans @@ -1549,4 +1551,15 @@ temprole: - temprole notify: - notify - - nfy \ No newline at end of file + - nfy +notifylist: + - notifylist + - notifyl +notifyclear: + - notifyclear + - notifyremove + - notifyrm + - notifclr +winlb: + - winlb + - wins \ 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 68d1348..3c3d54d 100644 --- a/src/EllieBot/data/strings/commands/commands.en-US.yml +++ b/src/EllieBot/data/strings/commands/commands.en-US.yml @@ -2170,6 +2170,13 @@ avatar: params: - usr: desc: "The user whose avatar is being displayed." +banner: + desc: Shows a mentioned person's banner. + ex: + - '@Someone' + params: + - usr: + desc: "The user whose banner is being displayed." translate: desc: Translates text from the given language to the destination language. ex: @@ -4857,10 +4864,38 @@ minesweeper: notify: desc: |- Sends a message to the current channel once the specified event occurs. + Provide no parameters to see all available events. 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 + - event: + desc: "The event to notify on." + message: + desc: "The message to send." +notifylist: + desc: |- + Lists all active notifications in this server. + ex: + - '' + params: + - { } +notifyclear: + desc: |- + Removes the specified notify event. + ex: + - 'levelup' + params: + - event: + desc: "The notify event to clear." +winlb: + desc: |- + Shows the biggest wins leaderboard + ex: + - '' + - '5' + params: + - page: + desc: "The optional page to display." \ No newline at end of file diff --git a/src/EllieBot/data/strings/responses/responses.en-US.json b/src/EllieBot/data/strings/responses/responses.en-US.json index bca49eb..bce5753 100644 --- a/src/EllieBot/data/strings/responses/responses.en-US.json +++ b/src/EllieBot/data/strings/responses/responses.en-US.json @@ -1128,7 +1128,7 @@ "choose_one": "Choose one", "requires_role": "Requires role: {0}", "invalid_message_id": "Invalid Message Id.", - "invalid_message_link": "The message link must be from this server.", + "invalid_message_link": "The message link must be this Bot's message. The bot can't add buttons to other users' messages.", "btnrole_message_max": "Limit reached. You may have up to 25 button roles per message.", "btnrole_not_found": "No button role found on that message.", "btnrole_none": "There are no button roles on this page.", @@ -1145,6 +1145,17 @@ "level_set": "Level of user {0} set to {1} on this server.", "temp_role_added": "User {0} has been given {1} role temporarily. The role expires {2}", "user_afk": "User {0} is AFK.", - "notify_on": "Notification message will be sent on this channel when {0} event triggers.", - "notify_off": "Notification message will no longer be sent when {0} event triggers." + "notify_on": "Notification message will be sent in {0} channel when {1} event triggers.", + "notify_off": "Notification message will no longer be sent when {0} event triggers.", + "notify_none": "No notifications on this page.", + "notify_msg_not_set": "Notification message is not set for this event.", + "notify_list": "Notify List", + "notify_type": "Type", + "notify_msg": "Notify Message", + "notify_available": "List of available notify events", + "notify_desc_levelup": "Triggers when a user levels up on this server.", + "notify_desc_protection": "Triggers when antialt, antispam or antiraid is triggered.", + "notify_desc_addrolerew": "Triggers when a user gets a role as a reward for reaching a level (xprew).", + "notify_desc_removerolerew": "Triggers when a user loses a role as a reward for reaching a level (xprew).", + "notify_desc_not_found": "No description found for this notify event. Please report this." }