added addrolereward and removerolereward events for .notify

added .notify with no params showing events with descriptions
added .winlb
updated discord.net, redid migrations
This commit is contained in:
Toastie 2024-12-08 19:37:22 +13:00
parent f8eb585093
commit 29bac7739d
Signed by: toastie_t0ast
GPG key ID: 27F3B6855AFD40A4
31 changed files with 635 additions and 219 deletions

View file

@ -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

View file

@ -9,7 +9,7 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Discord.Net.Core" Version="3.15.3" />
<PackageReference Include="Discord.Net.Core" Version="3.16.0" />
<PackageReference Include="Serilog" Version="3.1.1" />
<PackageReference Include="YamlDotNet" Version="15.1.4" />
</ItemGroup>

View file

@ -164,14 +164,19 @@ public abstract class EllieContext : DbContext
#region UserBetStats
modelBuilder.Entity<UserBetStats>()
.HasIndex(x => new
modelBuilder.Entity<UserBetStats>(ubs =>
{
ubs.HasIndex(x => new
{
x.UserId,
x.Game
})
.IsUnique();
ubs.HasIndex(x => x.MaxWin)
.IsUnique(false);
});
#endregion
#region Flag Translate

View file

@ -19,4 +19,6 @@ public enum NotifyType
{
LevelUp = 0,
Protection = 1, Prot = 1,
AddRoleReward = 2,
RemoveRoleReward = 3,
}

View file

@ -29,7 +29,7 @@
</PackageReference>
<PackageReference Include="CodeHollow.FeedReader" Version="1.2.6" />
<PackageReference Include="CommandLineParser" Version="2.9.1" />
<PackageReference Include="Discord.Net" Version="3.15.3" />
<PackageReference Include="Discord.Net" Version="3.16.0" />
<PackageReference Include="CoreCLR-NCalc" Version="3.1.246" />
<PackageReference Include="Google.Apis.Urlshortener.v1" Version="1.41.1.138" />
<PackageReference Include="Google.Apis.YouTube.v3" Version="1.68.0.3414" />

View file

@ -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("""

View file

@ -1,46 +0,0 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace EllieBot.Migrations.PostgreSql
{
/// <inheritdoc />
public partial class awardedxptemprolenotify : Migration
{
/// <inheritdoc />
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" });
}
/// <inheritdoc />
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" });
}
}
}

View file

@ -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
{
/// <inheritdoc />
@ -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");

View file

@ -0,0 +1,27 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace EllieBot.Migrations.PostgreSql
{
/// <inheritdoc />
public partial class awardedxptemprolenotify : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateIndex(
name: "ix_userbetstats_maxwin",
table: "userbetstats",
column: "maxwin");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropIndex(
name: "ix_userbetstats_maxwin",
table: "userbetstats");
}
}
}

View file

@ -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");

View file

@ -1,46 +0,0 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace EllieBot.Migrations
{
/// <inheritdoc />
public partial class awardedxptemprolenotify : Migration
{
/// <inheritdoc />
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" });
}
/// <inheritdoc />
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" });
}
}
}

View file

@ -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
{
/// <inheritdoc />
@ -2593,6 +2593,8 @@ namespace EllieBot.Migrations
b.HasKey("Id");
b.HasIndex("MaxWin");
b.HasIndex("UserId", "Game")
.IsUnique();

View file

@ -0,0 +1,27 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace EllieBot.Migrations
{
/// <inheritdoc />
public partial class awardedxptemprolenotify : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateIndex(
name: "IX_UserBetStats_MaxWin",
table: "UserBetStats",
column: "MaxWin");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropIndex(
name: "IX_UserBetStats_MaxWin",
table: "UserBetStats");
}
}
}

View file

@ -2590,6 +2590,8 @@ namespace EllieBot.Migrations
b.HasKey("Id");
b.HasIndex("MaxWin");
b.HasIndex("UserId", "Game")
.IsUnique();

View file

@ -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<string, Func<SocketGuild, string>> GetReplacements()
{
var model = this;
return new Dictionary<string, Func<SocketGuild, string>>()
{
{ "%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;
}
}

View file

@ -20,6 +20,7 @@ public record struct LevelUpNotifyModel(
return new Dictionary<string, Func<SocketGuild, string>>()
{
{ "%event.level%", g => data.Level.ToString() },
{ "%event.user%", g => g.GetUser(data.UserId)?.ToString() ?? data.UserId.ToString() },
};
}
@ -35,10 +36,3 @@ public record struct LevelUpNotifyModel(
return true;
}
}
public static class INotifyModelExtensions
{
public static TypedKey<T> GetTypedKey<T>(this T model)
where T : struct, INotifyModel
=> new(T.KeyName);
}

View file

@ -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<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;
}
}

View file

@ -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<string, Func<SocketGuild, string>> GetReplacements()
{
var model = this;
return new Dictionary<string, Func<SocketGuild, string>>()
{
{ "%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;
}
}

View file

@ -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<NotifyService>
{
[Cmd]
[OwnerOnly]
public async Task Notify()
{
await Response()
.Paginated()
.Items(Enum.GetValues<NotifyType>())
.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();
}
}
}

View file

@ -0,0 +1,8 @@
namespace EllieBot.Modules.Administration;
public static class NotifyModelExtensions
{
public static TypedKey<T> GetTypedKey<T>(this T model)
where T : struct, INotifyModel
=> new(T.KeyName);
}

View file

@ -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<IReadOnlyCollection<Notify>> GetForGuildAsync(ulong guildId, int page = 0)
{
ArgumentOutOfRangeException.ThrowIfNegative(page);
await using var ctx = _db.GetDbContext();
var list = await ctx.GetTable<Notify>()
.Where(x => x.GuildId == guildId)
.OrderBy(x => x.Type)
.Skip(page * 10)
.Take(10)
.ToListAsyncLinqToDB();
return list;
}
public async Task<Notify?> GetNotifyAsync(ulong guildId, NotifyType nType)
{
await using var ctx = _db.GetDbContext();
return await ctx.GetTable<Notify>()
.Where(x => x.GuildId == guildId && x.Type == nType)
.FirstOrDefaultAsyncLinqToDB();
}
}

View file

@ -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<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 : IEService
{
public event Func<PunishmentAction, ProtectionType, IGuildUser[], Task> OnAntiProtectionTriggered = delegate

View file

@ -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<UserBetStatsService>
{
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]
@ -115,6 +122,68 @@ public partial class Gambling
.SendAsync();
}
private readonly record struct WinLbStat(
int Rank,
string User,
GamblingGame Game,
long MaxWin);
private TypedKey<List<WinLbStat>> GetWinLbKey(int page)
=> new($"winlb:{page}");
private async Task<IReadOnlyCollection<WinLbStat>> 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<WinLbStat>(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()
{
@ -123,15 +192,15 @@ public partial class Gambling
var eb = CreateEmbed()
.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);

View file

@ -52,4 +52,16 @@ public sealed class UserBetStatsService : IEService
await ctx.GetTable<GamblingStats>()
.DeleteAsync();
}
public async Task<IReadOnlyList<UserBetStats>> GetWinLbAsync(int page)
{
ArgumentOutOfRangeException.ThrowIfNegative(page);
await using var ctx = _db.GetDbContext();
return await ctx.GetTable<UserBetStats>()
.OrderByDescending(x => x.MaxWin)
.Skip(page * 10)
.Take(10)
.ToArrayAsyncLinqToDB();
}
}

View file

@ -411,6 +411,24 @@ public partial class Searches : EllieModule<SearchesService>
.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();
}
[Cmd]
public async Task Wikia(string target, [Leftover] string query)
{

View file

@ -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);
}
}
}
}

View file

@ -157,6 +157,9 @@ public sealed class DoAsUserMessage : IUserMessage
public MessageCallData? CallData
=> _msg.CallData;
public IReadOnlyCollection<MessageSnapshot> ForwardedMessages
=> _msg.ForwardedMessages;
public Task ModifyAsync(Action<MessageProperties> func, RequestOptions? options = null)
{
return _msg.ModifyAsync(func, options);

View file

@ -12,7 +12,7 @@ public sealed class UserService : IUserService, IEService
_db = db;
}
public async Task<DiscordUser> GetUserAsync(ulong userId)
public async Task<DiscordUser?> GetUserAsync(ulong userId)
{
await using var uow = _db.GetDbContext();
var user = await uow

View file

@ -715,6 +715,8 @@ color:
avatar:
- avatar
- av
banner:
- banner
translate:
- translate
- trans
@ -1550,3 +1552,14 @@ temprole:
notify:
- notify
- nfy
notifylist:
- notifylist
- notifyl
notifyclear:
- notifyclear
- notifyremove
- notifyrm
- notifclr
winlb:
- winlb
- wins

View file

@ -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:
- 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."

View file

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