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."
}