#nullable disable warnings using EllieBot.Modules.Xp.Services; using EllieBot.Db.Models; using EllieBot.Modules.Patronage; using EllieBot.Modules.Utility; namespace EllieBot.Modules.Xp; public partial class Xp : EllieModule { public enum Channel { Channel } public enum NotifyPlace { Server = 0, Guild = 0, Global = 1 } public enum Role { Role } public enum Server { Server } private readonly DownloadTracker _tracker; private readonly ICurrencyProvider _gss; public Xp(DownloadTracker tracker, ICurrencyProvider gss) { _tracker = tracker; _gss = gss; } [Cmd] [RequireContext(ContextType.Guild)] public async Task Experience([Leftover] IUser user = null) { user ??= ctx.User; await ctx.Channel.TriggerTypingAsync(); var (img, fmt) = await _service.GenerateXpImageAsync((IGuildUser)user); await using (img) { await ctx.Channel.SendFileAsync(img, $"{ctx.Guild.Id}_{user.Id}_xp.{fmt.FileExtensions.FirstOrDefault()}"); } } [Cmd] [RequireContext(ContextType.Guild)] public async Task XpNotify() { var globalSetting = _service.GetNotificationType(ctx.User); var serverSetting = _service.GetNotificationType(ctx.User.Id, ctx.Guild.Id); var embed = _sender.CreateEmbed() .WithOkColor() .AddField(GetText(strs.xpn_setting_global), GetNotifLocationString(globalSetting)) .AddField(GetText(strs.xpn_setting_server), GetNotifLocationString(serverSetting)); await Response().Embed(embed).SendAsync(); } [Cmd] [RequireContext(ContextType.Guild)] public async Task XpNotify(NotifyPlace place, XpNotificationLocation type) { if (place == NotifyPlace.Guild) await _service.ChangeNotificationType(ctx.User.Id, ctx.Guild.Id, type); else await _service.ChangeNotificationType(ctx.User, type); await ctx.OkAsync(); } [Cmd] [RequireContext(ContextType.Guild)] [UserPerm(GuildPerm.Administrator)] public async Task XpExclude(Server _) { var ex = _service.ToggleExcludeServer(ctx.Guild.Id); if (ex) await Response().Confirm(strs.excluded(Format.Bold(ctx.Guild.ToString()))).SendAsync(); else await Response().Confirm(strs.not_excluded(Format.Bold(ctx.Guild.ToString()))).SendAsync(); } [Cmd] [UserPerm(GuildPerm.ManageRoles)] [RequireContext(ContextType.Guild)] public async Task XpExclude(Role _, [Leftover] IRole role) { var ex = _service.ToggleExcludeRole(ctx.Guild.Id, role.Id); if (ex) await Response().Confirm(strs.excluded(Format.Bold(role.ToString()))).SendAsync(); else await Response().Confirm(strs.not_excluded(Format.Bold(role.ToString()))).SendAsync(); } [Cmd] [UserPerm(GuildPerm.ManageChannels)] [RequireContext(ContextType.Guild)] public async Task XpExclude(Channel _, [Leftover] IChannel channel = null) { if (channel is null) channel = ctx.Channel; var ex = _service.ToggleExcludeChannel(ctx.Guild.Id, channel.Id); if (ex) await Response().Confirm(strs.excluded(Format.Bold(channel.ToString()))).SendAsync(); else await Response().Confirm(strs.not_excluded(Format.Bold(channel.ToString()))).SendAsync(); } [Cmd] [RequireContext(ContextType.Guild)] public async Task XpExclusionList() { var serverExcluded = _service.IsServerExcluded(ctx.Guild.Id); var roles = _service.GetExcludedRoles(ctx.Guild.Id) .Select(x => ctx.Guild.GetRole(x)) .Where(x => x is not null) .Select(x => $"`role` {x.Mention}") .ToList(); var chans = (await _service.GetExcludedChannels(ctx.Guild.Id) .Select(x => ctx.Guild.GetChannelAsync(x)) .WhenAll()).Where(x => x is not null) .Select(x => $"`channel` <#{x.Id}>") .ToList(); var rolesStr = roles.Any() ? string.Join("\n", roles) + "\n" : string.Empty; var chansStr = chans.Count > 0 ? string.Join("\n", chans) + "\n" : string.Empty; var desc = Format.Code(serverExcluded ? GetText(strs.server_is_excluded) : GetText(strs.server_is_not_excluded)); desc += "\n\n" + rolesStr + chansStr; var lines = desc.Split('\n'); await Response() .Paginated() .Items(lines) .PageSize(15) .CurrentPage(0) .Page((items, _) => { var embed = _sender.CreateEmbed() .WithTitle(GetText(strs.exclusion_list)) .WithDescription(string.Join('\n', items)) .WithOkColor(); return embed; }) .SendAsync(); } [Cmd] [EllieOptions] [Priority(0)] [RequireContext(ContextType.Guild)] public Task XpLeaderboard(params string[] args) => XpLeaderboard(1, args); [Cmd] [EllieOptions] [Priority(1)] [RequireContext(ContextType.Guild)] public async Task XpLeaderboard(int page = 1, params string[] args) { if (--page < 0 || page > 100) return; var (opts, _) = OptionsParser.ParseFrom(new LbOpts(), args); await ctx.Channel.TriggerTypingAsync(); var socketGuild = (SocketGuild)ctx.Guild; var allCleanUsers = new List(); if (opts.Clean) { await ctx.Channel.TriggerTypingAsync(); await _tracker.EnsureUsersDownloadedAsync(ctx.Guild); allCleanUsers = (await _service.GetTopUserXps(ctx.Guild.Id, 1000)) .Where(user => socketGuild.GetUser(user.UserId) is not null) .ToList(); } var res = opts.Clean ? Response() .Paginated() .Items(allCleanUsers) : Response() .Paginated() .PageItems((curPage) => _service.GetUserXps(ctx.Guild.Id, curPage)); await res .PageSize(9) .CurrentPage(page) .Page((users, curPage) => { var embed = _sender.CreateEmbed().WithTitle(GetText(strs.server_leaderboard)).WithOkColor(); if (!users.Any()) return embed.WithDescription("-"); for (var i = 0; i < users.Count; i++) { var levelStats = new LevelStats(users[i].Xp + users[i].AwardedXp); var user = ((SocketGuild)ctx.Guild).GetUser(users[i].UserId); var userXpData = users[i]; var awardStr = string.Empty; if (userXpData.AwardedXp > 0) awardStr = $"(+{userXpData.AwardedXp})"; else if (userXpData.AwardedXp < 0) awardStr = $"({userXpData.AwardedXp})"; embed.AddField($"#{i + 1 + (curPage * 9)} {user?.ToString() ?? users[i].UserId.ToString()}", $"{GetText(strs.level_x(levelStats.Level))} - {levelStats.TotalXp}xp {awardStr}"); } return embed; }) .SendAsync(); } [Cmd] [RequireContext(ContextType.Guild)] public async Task XpGlobalLeaderboard(int page = 1) { if (--page < 0 || page > 99) return; await Response() .Paginated() .PageItems(async curPage => await _service.GetUserXps(curPage)) .PageSize(9) .Page((users, curPage) => { var embed = _sender.CreateEmbed() .WithOkColor() .WithTitle(GetText(strs.global_leaderboard)); if (!users.Any()) { embed.WithDescription("-"); return embed; } for (var i = 0; i < users.Count; i++) { var user = users[i]; embed.AddField($"#{i + 1 + (curPage * 9)} {user}", $"{GetText(strs.level_x(new LevelStats(users[i].TotalXp).Level))} - {users[i].TotalXp}xp"); } return embed; }) .SendAsync(); } [Cmd] [RequireContext(ContextType.Guild)] [UserPerm(GuildPerm.Administrator)] [Priority(2)] public async Task XpAdd(long amount, [Remainder] SocketRole role) { if (amount == 0) return; if (role.IsManaged) return; var count = await _service.AddXpToUsersAsync(ctx.Guild.Id, amount, role.Members.Select(x => x.Id).ToArray()); await Response() .Confirm( strs.xpadd_users(Format.Bold(amount.ToString()), Format.Bold(count.ToString()))) .SendAsync(); } [Cmd] [RequireContext(ContextType.Guild)] [UserPerm(GuildPerm.Administrator)] [Priority(3)] public async Task XpAdd(int amount, ulong userId) { if (amount == 0) return; _service.AddXp(userId, ctx.Guild.Id, amount); var usr = ((SocketGuild)ctx.Guild).GetUser(userId)?.ToString() ?? userId.ToString(); await Response().Confirm(strs.modified(Format.Bold(usr), Format.Bold(amount.ToString()))).SendAsync(); } [Cmd] [RequireContext(ContextType.Guild)] [UserPerm(GuildPerm.Administrator)] [Priority(4)] public Task XpAdd(int amount, [Leftover] IGuildUser user) => XpAdd(amount, user.Id); [Cmd] [RequireContext(ContextType.Guild)] [OwnerOnly] public async Task XpTemplateReload() { _service.ReloadXpTemplate(); await Task.Delay(1000); await Response().Confirm(strs.template_reloaded).SendAsync(); } [Cmd] [RequireContext(ContextType.Guild)] [UserPerm(GuildPerm.Administrator)] public Task XpReset(IGuildUser user) => XpReset(user.Id); [Cmd] [RequireContext(ContextType.Guild)] [UserPerm(GuildPerm.Administrator)] public async Task XpReset(ulong userId) { var embed = _sender.CreateEmbed() .WithTitle(GetText(strs.reset)) .WithDescription(GetText(strs.reset_user_confirm)); if (!await PromptUserConfirmAsync(embed)) return; _service.XpReset(ctx.Guild.Id, userId); await Response().Confirm(strs.reset_user(userId)).SendAsync(); } [Cmd] [RequireContext(ContextType.Guild)] [UserPerm(GuildPerm.Administrator)] public async Task XpReset() { var embed = _sender.CreateEmbed() .WithTitle(GetText(strs.reset)) .WithDescription(GetText(strs.reset_server_confirm)); if (!await PromptUserConfirmAsync(embed)) return; _service.XpReset(ctx.Guild.Id); await Response().Confirm(strs.reset_server).SendAsync(); } public enum XpShopInputType { Backgrounds = 0, B = 0, Bg = 0, Bgs = 0, Frames = 1, F = 1, Fr = 1, Frs = 1, Fs = 1, } [Cmd] public async Task XpShop() { if (!_service.IsShopEnabled()) { await Response().Error(strs.xp_shop_disabled).SendAsync(); return; } await Response() .Confirm(GetText(strs.available_commands), $""" `{prefix}xpshop bgs` `{prefix}xpshop frames` *{GetText(strs.xpshop_website)}* """) .SendAsync(); } [Cmd] public async Task XpShop(XpShopInputType type, int page = 1) { --page; if (page < 0) return; var allItems = type == XpShopInputType.Backgrounds ? await _service.GetShopBgs() : await _service.GetShopFrames(); if (allItems is null) { await Response().Error(strs.xp_shop_disabled).SendAsync(); return; } if (allItems.Count == 0) { await Response().Error(strs.not_found).SendAsync(); return; } await Response() .Paginated() .Items(allItems) .PageSize(1) .CurrentPage(page) .AddFooter(false) .Page((items, _) => { if (!items.Any()) return _sender.CreateEmbed() .WithDescription(GetText(strs.not_found)) .WithErrorColor(); var (key, item) = items.FirstOrDefault(); var eb = _sender.CreateEmbed() .WithOkColor() .WithTitle(item.Name) .AddField(GetText(strs.price), CurrencyHelper.N(item.Price, Culture, _gss.GetCurrencySign()), true) .WithImageUrl(string.IsNullOrWhiteSpace(item.Preview) ? item.Url : item.Preview); if (!string.IsNullOrWhiteSpace(item.Desc)) eb.AddField(GetText(strs.desc), item.Desc); if (key == "default") eb.WithDescription(GetText(strs.xpshop_website)); var tier = _service.GetXpShopTierRequirement(type); if (tier != PatronTier.None) { eb.WithFooter(GetText(strs.xp_shop_buy_required_tier(tier.ToString()))); } return eb; }) .Interaction(async current => { var (key, _) = allItems.Skip(current).First(); var itemType = type == XpShopInputType.Backgrounds ? XpShopItemType.Background : XpShopItemType.Frame; var ownedItem = await _service.GetUserItemAsync(ctx.User.Id, itemType, key); if (ownedItem is not null) { var button = new ButtonBuilder(ownedItem.IsUsing ? GetText(strs.in_use) : GetText(strs.use), "xpshop:use", emote: Emoji.Parse("👐"), isDisabled: ownedItem.IsUsing); var inter = _inter.Create( ctx.User.Id, button, OnShopUse, (key, itemType), clearAfter: false); return inter; } else { var button = new ButtonBuilder(GetText(strs.buy), "xpshop:buy", emote: Emoji.Parse("💰")); var inter = _inter.Create( ctx.User.Id, button, OnShopBuy, (key, itemType), singleUse: true, clearAfter: false); return inter; } }) .SendAsync(); } [Cmd] public async Task XpShopBuy(XpShopInputType type, string key) { var result = await _service.BuyShopItemAsync(ctx.User.Id, (XpShopItemType)type, key); EllieInteractionBase GetUseInteraction() { return _inter.Create(ctx.User.Id, new(label: "Use", customId: "xpshop:use_item", emote: Emoji.Parse("👐")), async (_, state) => await XpShopUse(state.type, state.key), (type, key) ); } if (result != BuyResult.Success) { var _ = result switch { BuyResult.XpShopDisabled => await Response().Error(strs.xp_shop_disabled).SendAsync(), BuyResult.InsufficientFunds => await Response() .Error(strs.not_enough(_gss.GetCurrencySign())) .SendAsync(), BuyResult.AlreadyOwned => await Response().Error(strs.xpshop_already_owned).Interaction(GetUseInteraction()).SendAsync(), BuyResult.UnknownItem => await Response().Error(strs.xpshop_item_not_found).SendAsync(), BuyResult.InsufficientPatronTier => await Response().Error(strs.patron_insuff_tier).SendAsync(), _ => throw new ArgumentOutOfRangeException() }; return; } await Response() .Confirm(strs.xpshop_buy_success(type.ToString().ToLowerInvariant(), key.ToLowerInvariant())) .Interaction(GetUseInteraction()) .SendAsync(); } [Cmd] public async Task XpShopUse(XpShopInputType type, string key) { var result = await _service.UseShopItemAsync(ctx.User.Id, (XpShopItemType)type, key); if (!result) { await Response().Confirm(strs.xp_shop_item_cant_use).SendAsync(); return; } await ctx.OkAsync(); } private async Task OnShopUse(SocketMessageComponent smc, (string key, XpShopItemType type) state) { var (key, type) = state; var result = await _service.UseShopItemAsync(ctx.User.Id, type, key); if (!result) { await Response().Confirm(strs.xp_shop_item_cant_use).SendAsync(); } } private async Task OnShopBuy(SocketMessageComponent smc, (string key, XpShopItemType type) state) { var (key, type) = state; var result = await _service.BuyShopItemAsync(ctx.User.Id, type, key); if (result == BuyResult.InsufficientFunds) { await Response().Error(strs.not_enough(_gss.GetCurrencySign())).SendAsync(); } else if (result == BuyResult.Success) { await _service.UseShopItemAsync(ctx.User.Id, type, key); } } private string GetNotifLocationString(XpNotificationLocation loc) { if (loc == XpNotificationLocation.Channel) return GetText(strs.xpn_notif_channel); if (loc == XpNotificationLocation.Dm) return GetText(strs.xpn_notif_dm); return GetText(strs.xpn_notif_disabled); } }