add: added addrolereward and removerolereward events for .notify

add: added .notify with no params showing events with descriptions
add: .winlb
docs: updated docs
dev: updated discord.net, redid migrations
This commit is contained in:
Kwoth
2024-12-07 16:46:06 +00:00
parent 0622236523
commit 97f1405a94
34 changed files with 687 additions and 200 deletions

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

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

View File

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

View File

@@ -5,6 +5,16 @@ namespace NadekoBot.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

@@ -12,7 +12,7 @@ using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
namespace NadekoBot.Migrations.PostgreSql
{
[DbContext(typeof(PostgreSqlContext))]
[Migration("20241205052146_awardedxp-temprole-notify")]
[Migration("20241207150050_awardedxp-temprole-notify")]
partial class awardedxptemprolenotify
{
/// <inheritdoc />
@@ -3485,6 +3485,9 @@ namespace NadekoBot.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

@@ -16,6 +16,8 @@ namespace NadekoBot.Migrations.PostgreSql
name: "ix_userxpstats_awardedxp",
table: "userxpstats");
MigrationQueries.MergeAwardedXp(migrationBuilder);
migrationBuilder.DropColumn(
name: "awardedxp",
table: "userxpstats");
@@ -59,6 +61,11 @@ namespace NadekoBot.Migrations.PostgreSql
table.UniqueConstraint("ak_temprole_guildid_userid_roleid", x => new { x.guildid, x.userid, x.roleid });
});
migrationBuilder.CreateIndex(
name: "ix_userbetstats_maxwin",
table: "userbetstats",
column: "maxwin");
migrationBuilder.CreateIndex(
name: "ix_temprole_expiresat",
table: "temprole",
@@ -74,6 +81,10 @@ namespace NadekoBot.Migrations.PostgreSql
migrationBuilder.DropTable(
name: "temprole");
migrationBuilder.DropIndex(
name: "ix_userbetstats_maxwin",
table: "userbetstats");
migrationBuilder.AddColumn<long>(
name: "awardedxp",
table: "userxpstats",

View File

@@ -3482,6 +3482,9 @@ namespace NadekoBot.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

@@ -11,7 +11,7 @@ using NadekoBot.Db;
namespace NadekoBot.Migrations
{
[DbContext(typeof(SqliteContext))]
[Migration("20241205052137_awardedxp-temprole-notify")]
[Migration("20241207150041_awardedxp-temprole-notify")]
partial class awardedxptemprolenotify
{
/// <inheritdoc />
@@ -2593,6 +2593,8 @@ namespace NadekoBot.Migrations
b.HasKey("Id");
b.HasIndex("MaxWin");
b.HasIndex("UserId", "Game")
.IsUnique();

View File

@@ -15,6 +15,7 @@ namespace NadekoBot.Migrations
name: "IX_UserXpStats_AwardedXp",
table: "UserXpStats");
MigrationQueries.MergeAwardedXp(migrationBuilder);
migrationBuilder.DropColumn(
name: "AwardedXp",
table: "UserXpStats");
@@ -58,6 +59,11 @@ namespace NadekoBot.Migrations
table.UniqueConstraint("AK_TempRole_GuildId_UserId_RoleId", x => new { x.GuildId, x.UserId, x.RoleId });
});
migrationBuilder.CreateIndex(
name: "IX_UserBetStats_MaxWin",
table: "UserBetStats",
column: "MaxWin");
migrationBuilder.CreateIndex(
name: "IX_TempRole_ExpiresAt",
table: "TempRole",
@@ -73,6 +79,10 @@ namespace NadekoBot.Migrations
migrationBuilder.DropTable(
name: "TempRole");
migrationBuilder.DropIndex(
name: "IX_UserBetStats_MaxWin",
table: "UserBetStats");
migrationBuilder.AddColumn<long>(
name: "AwardedXp",
table: "UserXpStats",

View File

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

View File

@@ -0,0 +1,36 @@
using NadekoBot.Db.Models;
using NadekoBot.Modules.Administration;
namespace NadekoBot.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() },
};
}
@@ -34,11 +35,4 @@ public record struct LevelUpNotifyModel(
userId = UserId;
return true;
}
}
public static class INotifyModelExtensions
{
public static TypedKey<T> GetTypedKey<T>(this T model)
where T : struct, INotifyModel
=> new(T.KeyName);
}

View File

@@ -0,0 +1,34 @@
#nullable disable
using NadekoBot.Db.Models;
namespace NadekoBot.Modules.Administration.Services;
public record struct ProtectionNotifyModel(ulong GuildId, ProtectionType ProtType, ulong UserId) : INotifyModel
{
public static string KeyName
=> "notify.protection";
public static NotifyType NotifyType
=> NotifyType.Protection;
public IReadOnlyDictionary<string, Func<SocketGuild, string>> GetReplacements()
{
var data = this;
return new Dictionary<string, Func<SocketGuild, string>>()
{
{ "%event.type%", g => data.ProtType.ToString() },
};
}
public bool TryGetUserId(out ulong userId)
{
userId = UserId;
return true;
}
public bool TryGetGuildId(out ulong guildId)
{
guildId = GuildId;
return true;
}
}

View File

@@ -0,0 +1,36 @@
using NadekoBot.Db.Models;
using NadekoBot.Modules.Administration;
namespace NadekoBot.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 NadekoBot.Db.Models;
using System.Text;
namespace NadekoBot.Modules.Administration;
@@ -6,19 +7,108 @@ public partial class Administration
{
public class NotifyCommands : NadekoModule<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 NadekoBot.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 NadekoBot.Common.ModuleBehaviors;
using NadekoBot.Db.Models;
using NadekoBot.Generators;
namespace NadekoBot.Modules.Administration;
@@ -199,4 +200,27 @@ public sealed class NotifyService : IReadyExecutor, INotifySubscriber, INService
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 NadekoBot.Modules.Administration.Services;
public record struct ProtectionNotifyModel(ulong GuildId, ProtectionType ProtType, ulong UserId) : INotifyModel
{
public static string KeyName
=> "notify.protection";
public static NotifyType NotifyType
=> NotifyType.Protection;
public IReadOnlyDictionary<string, Func<SocketGuild, string>> GetReplacements()
{
var data = this;
return new Dictionary<string, Func<SocketGuild, string>>()
{
{ "%event.type%", g => data.ProtType.ToString() },
};
}
public bool TryGetUserId(out ulong userId)
{
userId = UserId;
return true;
}
public bool TryGetGuildId(out ulong guildId)
{
guildId = GuildId;
return true;
}
}
public class ProtectionService : INService
{
public event Func<PunishmentAction, ProtectionType, IGuildUser[], Task> OnAntiProtectionTriggered = delegate

View File

@@ -1,6 +1,7 @@
#nullable disable
using NadekoBot.Modules.Gambling.Common;
using NadekoBot.Modules.Gambling.Services;
using NadekoBot.Modules.Xp.Services;
namespace NadekoBot.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]
@@ -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,13 +122,75 @@ 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()
{
var stats = await _gamblingTxTracker.GetAllAsync();
var eb = CreateEmbed()
.WithOkColor();
.WithOkColor();
var str = "` Feature `` Bet ``Paid Out`` RoI `\n";
str += "――――――――――――――――――――\n";
@@ -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();

View File

@@ -42,7 +42,7 @@ public sealed class UserBetStatsService : INService
await using var ctx = _db.GetDbContext();
await ctx.GetTable<UserBetStats>()
.DeleteAsync(x => x.UserId == userId && (game == null || x.Game == game));
return true;
}
@@ -52,4 +52,16 @@ public sealed class UserBetStatsService : INService
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

@@ -103,11 +103,11 @@ public partial class Searches : NadekoModule<SearchesService>
}
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 : NadekoModule<SearchesService>
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 : NadekoModule<SearchesService>
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 : NadekoModule<SearchesService>
}
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 : NadekoModule<SearchesService>
{
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 : NadekoModule<SearchesService>
{
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 : NadekoModule<SearchesService>
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();
}

View File

@@ -344,9 +344,45 @@ public class XpService : INService, 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

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

@@ -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, INService
_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
@@ -1549,4 +1551,15 @@ temprole:
- temprole
notify:
- notify
- nfy
- 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:
desc: "The message to send."
- 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

@@ -1087,7 +1087,7 @@
"giveaway_starting": "Starting giveaway...",
"winner": "Winner",
"giveaway_list": "List of active giveways",
"todo_list_empty": "Your todo list is empty." ,
"todo_list_empty": "Your todo list is empty.",
"todo_list": "Todo List",
"todo_stats": "{0} items | {1} completed | {2} remaining",
"todo_add_max_limit": "You'reached the maximum amount of todos you can have.",
@@ -1102,7 +1102,7 @@
"search_results": "Search results",
"queue_search_results": "Type the number of the search result to queue up that track.",
"overloads": "Overloads",
"honeypot_on": "Honeypot enabled on this channel." ,
"honeypot_on": "Honeypot enabled on this channel.",
"honeypot_off": "Honeypot disabled.",
"afk_set": "AFK message set. Type a message in any channel to clear.",
"rero_message_not_found": "The specified message wasn't found. Make sure you've specified the message from this channel.",
@@ -1115,7 +1115,7 @@
"nc_hint": "Use `{0}nczoom x y` command to zoom in. Image is {1}x{2} pixels.",
"nc_insuff_payment": "Invalid payment.",
"invalid_img_size": "Image must to be {0}x{1} pixels.",
"no_attach_found": "No attachment found. Please send the image along with this command." ,
"no_attach_found": "No attachment found. Please send the image along with this command.",
"trfl_enabled": "Flag translation enabled on this channel. Reacting to a message with a flag will translate it to that language.",
"trfl_disabled": "Flag translation disabled.",
"rakeback_claimed": "You've claimed {0} as rakeback!",
@@ -1125,10 +1125,10 @@
"self_assign_group_role_req": "Users can now self-assign a role from group {0} only if they have {1} role.",
"sar_group_not_found": "Group with that number doesn't exist.",
"sar_group_deleted": "Group {0} deleted.",
"choose_one": "Choose one" ,
"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."
}