mirror of
				https://gitlab.com/Kwoth/nadekobot.git
				synced 2025-11-03 16:24:27 -05:00 
			
		
		
		
	Finished implementing xp shop. It allows users to buy frames and backgrounds if the user enables the feature in data/xp.yml. It can also be available only to patrons
This commit is contained in:
		
							
								
								
									
										17
									
								
								src/NadekoBot/Db/Models/XpShopOwnedItem.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								src/NadekoBot/Db/Models/XpShopOwnedItem.cs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,17 @@
 | 
			
		||||
using NadekoBot.Services.Database.Models;
 | 
			
		||||
 | 
			
		||||
namespace NadekoBot.Db.Models;
 | 
			
		||||
 | 
			
		||||
public class XpShopOwnedItem : DbEntity
 | 
			
		||||
{
 | 
			
		||||
    public ulong UserId { get; set; }
 | 
			
		||||
    public XpShopItemType ItemType { get; set; }
 | 
			
		||||
    public bool IsUsing { get; set; }
 | 
			
		||||
    public string ItemKey { get; set; }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
public enum XpShopItemType
 | 
			
		||||
{
 | 
			
		||||
    Background,
 | 
			
		||||
    Frame,
 | 
			
		||||
}
 | 
			
		||||
@@ -454,6 +454,23 @@ public abstract class NadekoContext : DbContext
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        #endregion
 | 
			
		||||
 
 | 
			
		||||
        #region Xp Item Shop
 | 
			
		||||
 | 
			
		||||
        modelBuilder.Entity<XpShopOwnedItem>(
 | 
			
		||||
            x =>
 | 
			
		||||
            {
 | 
			
		||||
                // user can own only one of each item
 | 
			
		||||
                x.HasIndex(model => new
 | 
			
		||||
                    {
 | 
			
		||||
                        model.UserId,
 | 
			
		||||
                        model.ItemType,
 | 
			
		||||
                        model.ItemKey
 | 
			
		||||
                    })
 | 
			
		||||
                    .IsUnique();
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
        #endregion
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
#if DEBUG
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										3556
									
								
								src/NadekoBot/Migrations/MySql/20220725155953_xpitemshop.Designer.cs
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										3556
									
								
								src/NadekoBot/Migrations/MySql/20220725155953_xpitemshop.Designer.cs
									
									
									
										generated
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										45
									
								
								src/NadekoBot/Migrations/MySql/20220725155953_xpitemshop.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										45
									
								
								src/NadekoBot/Migrations/MySql/20220725155953_xpitemshop.cs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,45 @@
 | 
			
		||||
using System;
 | 
			
		||||
using Microsoft.EntityFrameworkCore.Metadata;
 | 
			
		||||
using Microsoft.EntityFrameworkCore.Migrations;
 | 
			
		||||
 | 
			
		||||
#nullable disable
 | 
			
		||||
 | 
			
		||||
namespace NadekoBot.Migrations.Mysql
 | 
			
		||||
{
 | 
			
		||||
    public partial class xpitemshop : Migration
 | 
			
		||||
    {
 | 
			
		||||
        protected override void Up(MigrationBuilder migrationBuilder)
 | 
			
		||||
        {
 | 
			
		||||
            migrationBuilder.CreateTable(
 | 
			
		||||
                name: "xpshopowneditem",
 | 
			
		||||
                columns: table => new
 | 
			
		||||
                {
 | 
			
		||||
                    id = table.Column<int>(type: "int", nullable: false)
 | 
			
		||||
                        .Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn),
 | 
			
		||||
                    userid = table.Column<ulong>(type: "bigint unsigned", nullable: false),
 | 
			
		||||
                    itemtype = table.Column<int>(type: "int", nullable: false),
 | 
			
		||||
                    isusing = table.Column<bool>(type: "tinyint(1)", nullable: false),
 | 
			
		||||
                    itemkey = table.Column<string>(type: "varchar(255)", nullable: false)
 | 
			
		||||
                        .Annotation("MySql:CharSet", "utf8mb4"),
 | 
			
		||||
                    dateadded = table.Column<DateTime>(type: "datetime(6)", nullable: true)
 | 
			
		||||
                },
 | 
			
		||||
                constraints: table =>
 | 
			
		||||
                {
 | 
			
		||||
                    table.PrimaryKey("pk_xpshopowneditem", x => x.id);
 | 
			
		||||
                })
 | 
			
		||||
                .Annotation("MySql:CharSet", "utf8mb4");
 | 
			
		||||
 | 
			
		||||
            migrationBuilder.CreateIndex(
 | 
			
		||||
                name: "ix_xpshopowneditem_userid_itemtype_itemkey",
 | 
			
		||||
                table: "xpshopowneditem",
 | 
			
		||||
                columns: new[] { "userid", "itemtype", "itemkey" },
 | 
			
		||||
                unique: true);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        protected override void Down(MigrationBuilder migrationBuilder)
 | 
			
		||||
        {
 | 
			
		||||
            migrationBuilder.DropTable(
 | 
			
		||||
                name: "xpshopowneditem");
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -16,7 +16,7 @@ namespace NadekoBot.Migrations.Mysql
 | 
			
		||||
        {
 | 
			
		||||
#pragma warning disable 612, 618
 | 
			
		||||
            modelBuilder
 | 
			
		||||
                .HasAnnotation("ProductVersion", "6.0.6")
 | 
			
		||||
                .HasAnnotation("ProductVersion", "6.0.7")
 | 
			
		||||
                .HasAnnotation("Relational:MaxIdentifierLength", 64);
 | 
			
		||||
 | 
			
		||||
            modelBuilder.Entity("NadekoBot.Db.Models.BankUser", b =>
 | 
			
		||||
@@ -366,6 +366,44 @@ namespace NadekoBot.Migrations.Mysql
 | 
			
		||||
                    b.ToTable("streamonlinemessages", (string)null);
 | 
			
		||||
                });
 | 
			
		||||
 | 
			
		||||
            modelBuilder.Entity("NadekoBot.Db.Models.XpShopOwnedItem", b =>
 | 
			
		||||
                {
 | 
			
		||||
                    b.Property<int>("Id")
 | 
			
		||||
                        .ValueGeneratedOnAdd()
 | 
			
		||||
                        .HasColumnType("int")
 | 
			
		||||
                        .HasColumnName("id");
 | 
			
		||||
 | 
			
		||||
                    b.Property<DateTime?>("DateAdded")
 | 
			
		||||
                        .HasColumnType("datetime(6)")
 | 
			
		||||
                        .HasColumnName("dateadded");
 | 
			
		||||
 | 
			
		||||
                    b.Property<bool>("IsUsing")
 | 
			
		||||
                        .HasColumnType("tinyint(1)")
 | 
			
		||||
                        .HasColumnName("isusing");
 | 
			
		||||
 | 
			
		||||
                    b.Property<string>("ItemKey")
 | 
			
		||||
                        .IsRequired()
 | 
			
		||||
                        .HasColumnType("varchar(255)")
 | 
			
		||||
                        .HasColumnName("itemkey");
 | 
			
		||||
 | 
			
		||||
                    b.Property<int>("ItemType")
 | 
			
		||||
                        .HasColumnType("int")
 | 
			
		||||
                        .HasColumnName("itemtype");
 | 
			
		||||
 | 
			
		||||
                    b.Property<ulong>("UserId")
 | 
			
		||||
                        .HasColumnType("bigint unsigned")
 | 
			
		||||
                        .HasColumnName("userid");
 | 
			
		||||
 | 
			
		||||
                    b.HasKey("Id")
 | 
			
		||||
                        .HasName("pk_xpshopowneditem");
 | 
			
		||||
 | 
			
		||||
                    b.HasIndex("UserId", "ItemType", "ItemKey")
 | 
			
		||||
                        .IsUnique()
 | 
			
		||||
                        .HasDatabaseName("ix_xpshopowneditem_userid_itemtype_itemkey");
 | 
			
		||||
 | 
			
		||||
                    b.ToTable("xpshopowneditem", (string)null);
 | 
			
		||||
                });
 | 
			
		||||
 | 
			
		||||
            modelBuilder.Entity("NadekoBot.Services.Database.Models.AntiAltSetting", b =>
 | 
			
		||||
                {
 | 
			
		||||
                    b.Property<int>("Id")
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										3700
									
								
								src/NadekoBot/Migrations/Postgresql/20220725155941_xpitemshop.Designer.cs
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										3700
									
								
								src/NadekoBot/Migrations/Postgresql/20220725155941_xpitemshop.Designer.cs
									
									
									
										generated
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							@@ -0,0 +1,43 @@
 | 
			
		||||
using System;
 | 
			
		||||
using Microsoft.EntityFrameworkCore.Migrations;
 | 
			
		||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
 | 
			
		||||
 | 
			
		||||
#nullable disable
 | 
			
		||||
 | 
			
		||||
namespace NadekoBot.Migrations.PostgreSql
 | 
			
		||||
{
 | 
			
		||||
    public partial class xpitemshop : Migration
 | 
			
		||||
    {
 | 
			
		||||
        protected override void Up(MigrationBuilder migrationBuilder)
 | 
			
		||||
        {
 | 
			
		||||
            migrationBuilder.CreateTable(
 | 
			
		||||
                name: "xpshopowneditem",
 | 
			
		||||
                columns: table => new
 | 
			
		||||
                {
 | 
			
		||||
                    id = table.Column<int>(type: "integer", nullable: false)
 | 
			
		||||
                        .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
 | 
			
		||||
                    userid = table.Column<decimal>(type: "numeric(20,0)", nullable: false),
 | 
			
		||||
                    itemtype = table.Column<int>(type: "integer", nullable: false),
 | 
			
		||||
                    isusing = table.Column<bool>(type: "boolean", nullable: false),
 | 
			
		||||
                    itemkey = table.Column<string>(type: "text", nullable: false),
 | 
			
		||||
                    dateadded = table.Column<DateTime>(type: "timestamp with time zone", nullable: true)
 | 
			
		||||
                },
 | 
			
		||||
                constraints: table =>
 | 
			
		||||
                {
 | 
			
		||||
                    table.PrimaryKey("pk_xpshopowneditem", x => x.id);
 | 
			
		||||
                });
 | 
			
		||||
 | 
			
		||||
            migrationBuilder.CreateIndex(
 | 
			
		||||
                name: "ix_xpshopowneditem_userid_itemtype_itemkey",
 | 
			
		||||
                table: "xpshopowneditem",
 | 
			
		||||
                columns: new[] { "userid", "itemtype", "itemkey" },
 | 
			
		||||
                unique: true);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        protected override void Down(MigrationBuilder migrationBuilder)
 | 
			
		||||
        {
 | 
			
		||||
            migrationBuilder.DropTable(
 | 
			
		||||
                name: "xpshopowneditem");
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -17,7 +17,7 @@ namespace NadekoBot.Migrations.PostgreSql
 | 
			
		||||
        {
 | 
			
		||||
#pragma warning disable 612, 618
 | 
			
		||||
            modelBuilder
 | 
			
		||||
                .HasAnnotation("ProductVersion", "6.0.6")
 | 
			
		||||
                .HasAnnotation("ProductVersion", "6.0.7")
 | 
			
		||||
                .HasAnnotation("Relational:MaxIdentifierLength", 63);
 | 
			
		||||
 | 
			
		||||
            NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
 | 
			
		||||
@@ -378,6 +378,46 @@ namespace NadekoBot.Migrations.PostgreSql
 | 
			
		||||
                    b.ToTable("streamonlinemessages", (string)null);
 | 
			
		||||
                });
 | 
			
		||||
 | 
			
		||||
            modelBuilder.Entity("NadekoBot.Db.Models.XpShopOwnedItem", b =>
 | 
			
		||||
                {
 | 
			
		||||
                    b.Property<int>("Id")
 | 
			
		||||
                        .ValueGeneratedOnAdd()
 | 
			
		||||
                        .HasColumnType("integer")
 | 
			
		||||
                        .HasColumnName("id");
 | 
			
		||||
 | 
			
		||||
                    NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
 | 
			
		||||
 | 
			
		||||
                    b.Property<DateTime?>("DateAdded")
 | 
			
		||||
                        .HasColumnType("timestamp with time zone")
 | 
			
		||||
                        .HasColumnName("dateadded");
 | 
			
		||||
 | 
			
		||||
                    b.Property<bool>("IsUsing")
 | 
			
		||||
                        .HasColumnType("boolean")
 | 
			
		||||
                        .HasColumnName("isusing");
 | 
			
		||||
 | 
			
		||||
                    b.Property<string>("ItemKey")
 | 
			
		||||
                        .IsRequired()
 | 
			
		||||
                        .HasColumnType("text")
 | 
			
		||||
                        .HasColumnName("itemkey");
 | 
			
		||||
 | 
			
		||||
                    b.Property<int>("ItemType")
 | 
			
		||||
                        .HasColumnType("integer")
 | 
			
		||||
                        .HasColumnName("itemtype");
 | 
			
		||||
 | 
			
		||||
                    b.Property<decimal>("UserId")
 | 
			
		||||
                        .HasColumnType("numeric(20,0)")
 | 
			
		||||
                        .HasColumnName("userid");
 | 
			
		||||
 | 
			
		||||
                    b.HasKey("Id")
 | 
			
		||||
                        .HasName("pk_xpshopowneditem");
 | 
			
		||||
 | 
			
		||||
                    b.HasIndex("UserId", "ItemType", "ItemKey")
 | 
			
		||||
                        .IsUnique()
 | 
			
		||||
                        .HasDatabaseName("ix_xpshopowneditem_userid_itemtype_itemkey");
 | 
			
		||||
 | 
			
		||||
                    b.ToTable("xpshopowneditem", (string)null);
 | 
			
		||||
                });
 | 
			
		||||
 | 
			
		||||
            modelBuilder.Entity("NadekoBot.Services.Database.Models.AntiAltSetting", b =>
 | 
			
		||||
                {
 | 
			
		||||
                    b.Property<int>("Id")
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										2855
									
								
								src/NadekoBot/Migrations/Sqlite/20220725112348_xpitemshop.Designer.cs
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										2855
									
								
								src/NadekoBot/Migrations/Sqlite/20220725112348_xpitemshop.Designer.cs
									
									
									
										generated
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										42
									
								
								src/NadekoBot/Migrations/Sqlite/20220725112348_xpitemshop.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										42
									
								
								src/NadekoBot/Migrations/Sqlite/20220725112348_xpitemshop.cs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,42 @@
 | 
			
		||||
using System;
 | 
			
		||||
using Microsoft.EntityFrameworkCore.Migrations;
 | 
			
		||||
 | 
			
		||||
#nullable disable
 | 
			
		||||
 | 
			
		||||
namespace NadekoBot.Migrations
 | 
			
		||||
{
 | 
			
		||||
    public partial class xpitemshop : Migration
 | 
			
		||||
    {
 | 
			
		||||
        protected override void Up(MigrationBuilder migrationBuilder)
 | 
			
		||||
        {
 | 
			
		||||
            migrationBuilder.CreateTable(
 | 
			
		||||
                name: "XpShopOwnedItem",
 | 
			
		||||
                columns: table => new
 | 
			
		||||
                {
 | 
			
		||||
                    Id = table.Column<int>(type: "INTEGER", nullable: false)
 | 
			
		||||
                        .Annotation("Sqlite:Autoincrement", true),
 | 
			
		||||
                    UserId = table.Column<ulong>(type: "INTEGER", nullable: false),
 | 
			
		||||
                    ItemType = table.Column<int>(type: "INTEGER", nullable: false),
 | 
			
		||||
                    IsUsing = table.Column<bool>(type: "INTEGER", nullable: false),
 | 
			
		||||
                    ItemKey = table.Column<string>(type: "TEXT", nullable: false),
 | 
			
		||||
                    DateAdded = table.Column<DateTime>(type: "TEXT", nullable: true)
 | 
			
		||||
                },
 | 
			
		||||
                constraints: table =>
 | 
			
		||||
                {
 | 
			
		||||
                    table.PrimaryKey("PK_XpShopOwnedItem", x => x.Id);
 | 
			
		||||
                });
 | 
			
		||||
 | 
			
		||||
            migrationBuilder.CreateIndex(
 | 
			
		||||
                name: "IX_XpShopOwnedItem_UserId_ItemType_ItemKey",
 | 
			
		||||
                table: "XpShopOwnedItem",
 | 
			
		||||
                columns: new[] { "UserId", "ItemType", "ItemKey" },
 | 
			
		||||
                unique: true);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        protected override void Down(MigrationBuilder migrationBuilder)
 | 
			
		||||
        {
 | 
			
		||||
            migrationBuilder.DropTable(
 | 
			
		||||
                name: "XpShopOwnedItem");
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -15,7 +15,7 @@ namespace NadekoBot.Migrations
 | 
			
		||||
        protected override void BuildModel(ModelBuilder modelBuilder)
 | 
			
		||||
        {
 | 
			
		||||
#pragma warning disable 612, 618
 | 
			
		||||
            modelBuilder.HasAnnotation("ProductVersion", "6.0.6");
 | 
			
		||||
            modelBuilder.HasAnnotation("ProductVersion", "6.0.7");
 | 
			
		||||
 | 
			
		||||
            modelBuilder.Entity("NadekoBot.Db.Models.BankUser", b =>
 | 
			
		||||
                {
 | 
			
		||||
@@ -288,6 +288,36 @@ namespace NadekoBot.Migrations
 | 
			
		||||
                    b.ToTable("StreamOnlineMessages");
 | 
			
		||||
                });
 | 
			
		||||
 | 
			
		||||
            modelBuilder.Entity("NadekoBot.Db.Models.XpShopOwnedItem", b =>
 | 
			
		||||
                {
 | 
			
		||||
                    b.Property<int>("Id")
 | 
			
		||||
                        .ValueGeneratedOnAdd()
 | 
			
		||||
                        .HasColumnType("INTEGER");
 | 
			
		||||
 | 
			
		||||
                    b.Property<DateTime?>("DateAdded")
 | 
			
		||||
                        .HasColumnType("TEXT");
 | 
			
		||||
 | 
			
		||||
                    b.Property<bool>("IsUsing")
 | 
			
		||||
                        .HasColumnType("INTEGER");
 | 
			
		||||
 | 
			
		||||
                    b.Property<string>("ItemKey")
 | 
			
		||||
                        .IsRequired()
 | 
			
		||||
                        .HasColumnType("TEXT");
 | 
			
		||||
 | 
			
		||||
                    b.Property<int>("ItemType")
 | 
			
		||||
                        .HasColumnType("INTEGER");
 | 
			
		||||
 | 
			
		||||
                    b.Property<ulong>("UserId")
 | 
			
		||||
                        .HasColumnType("INTEGER");
 | 
			
		||||
 | 
			
		||||
                    b.HasKey("Id");
 | 
			
		||||
 | 
			
		||||
                    b.HasIndex("UserId", "ItemType", "ItemKey")
 | 
			
		||||
                        .IsUnique();
 | 
			
		||||
 | 
			
		||||
                    b.ToTable("XpShopOwnedItem");
 | 
			
		||||
                });
 | 
			
		||||
 | 
			
		||||
            modelBuilder.Entity("NadekoBot.Services.Database.Models.AntiAltSetting", b =>
 | 
			
		||||
                {
 | 
			
		||||
                    b.Property<int>("Id")
 | 
			
		||||
 
 | 
			
		||||
@@ -1,9 +1,10 @@
 | 
			
		||||
#nullable disable
 | 
			
		||||
#nullable disable warnings
 | 
			
		||||
using NadekoBot.Modules.Gambling.Services;
 | 
			
		||||
using NadekoBot.Modules.Xp.Services;
 | 
			
		||||
using NadekoBot.Services.Database.Models;
 | 
			
		||||
using System.Diagnostics;
 | 
			
		||||
using Nadeko.Common;
 | 
			
		||||
using System.Globalization;
 | 
			
		||||
using NadekoBot.Db.Models;
 | 
			
		||||
using NadekoBot.Modules.Utility.Patronage;
 | 
			
		||||
 | 
			
		||||
namespace NadekoBot.Modules.Xp;
 | 
			
		||||
 | 
			
		||||
@@ -313,6 +314,154 @@ public partial class Xp : NadekoModule<XpService>
 | 
			
		||||
        await ReplyConfirmLocalizedAsync(strs.reset_server);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public enum XpShopInputType
 | 
			
		||||
    {
 | 
			
		||||
        F = 0,
 | 
			
		||||
        Frs = 0,
 | 
			
		||||
        Fs = 0,
 | 
			
		||||
        Frames = 0,
 | 
			
		||||
        B = 1,
 | 
			
		||||
        Bg = 1,
 | 
			
		||||
        Bgs = 1,
 | 
			
		||||
        Backgrounds = 1
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    [Cmd]
 | 
			
		||||
    public async Task XpShop()
 | 
			
		||||
    {
 | 
			
		||||
        await SendConfirmAsync(GetText(strs.available_commands), $@"`{prefix}xpshop bgs`
 | 
			
		||||
`{prefix}xpshop frames`");
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    [Cmd]
 | 
			
		||||
    public async Task XpShop(XpShopInputType type, int page = 1)
 | 
			
		||||
    {
 | 
			
		||||
        --page;
 | 
			
		||||
 | 
			
		||||
        if (page < 0)
 | 
			
		||||
            return;
 | 
			
		||||
        
 | 
			
		||||
        var items = type == XpShopInputType.Backgrounds
 | 
			
		||||
            ? await _service.GetShopBgs()
 | 
			
		||||
            : await _service.GetShopFrames();
 | 
			
		||||
 | 
			
		||||
        if (items is null)
 | 
			
		||||
        {
 | 
			
		||||
            await ReplyErrorLocalizedAsync(strs.xp_shop_disabled);
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (items.Count == 0)
 | 
			
		||||
        {
 | 
			
		||||
            await ReplyErrorLocalizedAsync(strs.not_found);
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
        
 | 
			
		||||
        var culture = (CultureInfo)Culture.Clone();
 | 
			
		||||
        culture.NumberFormat.CurrencySymbol = _gss.Data.Currency.Sign;
 | 
			
		||||
        culture.NumberFormat.CurrencyNegativePattern = 5;
 | 
			
		||||
 | 
			
		||||
        await ctx.SendPaginatedConfirmAsync<(string, XpShopItemType)?>(page,
 | 
			
		||||
            current =>
 | 
			
		||||
            {
 | 
			
		||||
                var (key, item) = items.Skip(current).First();
 | 
			
		||||
 | 
			
		||||
                var eb = _eb.Create(ctx)
 | 
			
		||||
                    .WithOkColor()
 | 
			
		||||
                    .WithTitle(item.Name)
 | 
			
		||||
                    .AddField(GetText(strs.price), Gambling.Gambling.N(item.Price, culture), true)
 | 
			
		||||
                    // .AddField(GetText(strs.buy), $"{prefix}xpbuy {key}", true)
 | 
			
		||||
                    .WithImageUrl(item.Url.ToString());
 | 
			
		||||
 | 
			
		||||
                if (!string.IsNullOrWhiteSpace(item.Desc))
 | 
			
		||||
                    eb.WithDescription(item.Desc);
 | 
			
		||||
 | 
			
		||||
                var tier = _service.GetXpShopTierRequirement();
 | 
			
		||||
                if (tier != PatronTier.None)
 | 
			
		||||
                {
 | 
			
		||||
                    eb.WithFooter(GetText(strs.feature_required_tier(tier.ToString())));
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                return Task.FromResult(eb);
 | 
			
		||||
            },
 | 
			
		||||
            async current =>
 | 
			
		||||
            {
 | 
			
		||||
 | 
			
		||||
                var (key, _) = items.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),
 | 
			
		||||
                        "XP_SHOP_USE",
 | 
			
		||||
                        ButtonStyle.Primary,
 | 
			
		||||
                        emote: Emoji.Parse("👐"),
 | 
			
		||||
                        isDisabled: ownedItem.IsUsing);
 | 
			
		||||
 | 
			
		||||
                    var inter = new SimpleInteraction<(string key, XpShopItemType type)?>(
 | 
			
		||||
                        button,
 | 
			
		||||
                        OnShopUse,
 | 
			
		||||
                        (key, itemType));
 | 
			
		||||
 | 
			
		||||
                    return inter;
 | 
			
		||||
                }
 | 
			
		||||
                else
 | 
			
		||||
                {
 | 
			
		||||
                    var button = new ButtonBuilder(GetText(strs.buy),
 | 
			
		||||
                        "XP_SHOP_BUY",
 | 
			
		||||
                        ButtonStyle.Primary,
 | 
			
		||||
                        emote: Emoji.Parse("💰"));
 | 
			
		||||
 | 
			
		||||
                    var inter = new SimpleInteraction<(string key, XpShopItemType type)?>(
 | 
			
		||||
                        button,
 | 
			
		||||
                        OnShopBuy,
 | 
			
		||||
                        (key, itemType));
 | 
			
		||||
 | 
			
		||||
                    return inter;
 | 
			
		||||
                }
 | 
			
		||||
            },
 | 
			
		||||
            items.Count,
 | 
			
		||||
            1,
 | 
			
		||||
            addPaginatedFooter: false);
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    private async Task OnShopUse(SocketMessageComponent smc, (string? key, XpShopItemType type)? maybeState)
 | 
			
		||||
    {
 | 
			
		||||
        if (maybeState is not { } state)
 | 
			
		||||
            return;
 | 
			
		||||
        
 | 
			
		||||
        var (key, type) = state;
 | 
			
		||||
        
 | 
			
		||||
        var result = await _service.UseShopItemAsync(ctx.User.Id, type, key);
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
        if (!result)
 | 
			
		||||
        {
 | 
			
		||||
            await ReplyConfirmLocalizedAsync(strs.xp_shop_item_cant_use);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    private async Task OnShopBuy(SocketMessageComponent smc, (string? key, XpShopItemType type)? maybeState)
 | 
			
		||||
    {
 | 
			
		||||
        if (maybeState is not { } state)
 | 
			
		||||
            return;
 | 
			
		||||
        
 | 
			
		||||
        var (key, type) = state;
 | 
			
		||||
        
 | 
			
		||||
        var result = await _service.BuyShopItemAsync(ctx.User.Id, type, key);
 | 
			
		||||
 | 
			
		||||
        if (result == BuyResult.InsufficientFunds)
 | 
			
		||||
        {
 | 
			
		||||
            await ReplyErrorLocalizedAsync(strs.not_enough(_gss.Data.Currency.Sign));
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private string GetNotifLocationString(XpNotificationLocation loc)
 | 
			
		||||
    {
 | 
			
		||||
        if (loc == XpNotificationLocation.Channel)
 | 
			
		||||
@@ -324,3 +473,22 @@ public partial class Xp : NadekoModule<XpService>
 | 
			
		||||
        return GetText(strs.xpn_notif_disabled);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
public class SimpleInteraction<T>
 | 
			
		||||
{
 | 
			
		||||
    public ButtonBuilder Button { get; }
 | 
			
		||||
    private readonly Func<SocketMessageComponent, T, Task> _onClick;
 | 
			
		||||
    private readonly T? _state;
 | 
			
		||||
 | 
			
		||||
    public SimpleInteraction(ButtonBuilder button, Func<SocketMessageComponent, T?, Task> onClick, T state = default)
 | 
			
		||||
    {
 | 
			
		||||
        Button = button;
 | 
			
		||||
        _onClick = onClick;
 | 
			
		||||
        _state = state;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public async Task TriggerAsync(SocketMessageComponent smc)
 | 
			
		||||
    {
 | 
			
		||||
        await _onClick(smc, _state);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -1,6 +1,8 @@
 | 
			
		||||
#nullable disable
 | 
			
		||||
#nullable disable warnings
 | 
			
		||||
using Cloneable;
 | 
			
		||||
using NadekoBot.Common.Yml;
 | 
			
		||||
using NadekoBot.Db.Models;
 | 
			
		||||
using NadekoBot.Modules.Utility.Patronage;
 | 
			
		||||
 | 
			
		||||
namespace NadekoBot.Modules.Xp;
 | 
			
		||||
 | 
			
		||||
@@ -8,7 +10,7 @@ namespace NadekoBot.Modules.Xp;
 | 
			
		||||
public sealed partial class XpConfig : ICloneable<XpConfig>
 | 
			
		||||
{
 | 
			
		||||
    [Comment(@"DO NOT CHANGE")]
 | 
			
		||||
    public int Version { get; set; } = 2;
 | 
			
		||||
    public int Version { get; set; } = 3;
 | 
			
		||||
 | 
			
		||||
    [Comment(@"How much XP will the users receive per message")]
 | 
			
		||||
    public int XpPerMessage { get; set; } = 3;
 | 
			
		||||
@@ -24,4 +26,64 @@ public sealed partial class XpConfig : ICloneable<XpConfig>
 | 
			
		||||
 | 
			
		||||
    [Comment(@"The maximum amount of minutes the bot will keep track of a user in a voice channel")]
 | 
			
		||||
    public int VoiceMaxMinutes { get; set; } = 720;
 | 
			
		||||
 | 
			
		||||
    [Comment(@"Xp Shop config")]
 | 
			
		||||
    public ShopConfig Shop { get; set; } = new();
 | 
			
		||||
 | 
			
		||||
    public sealed class ShopConfig
 | 
			
		||||
    {
 | 
			
		||||
        [Comment(@"Whether the xp shop is enabled
 | 
			
		||||
True -> Users can access the xp shop using .xpshop command
 | 
			
		||||
False -> Users can't access the xp shop")]
 | 
			
		||||
        public bool IsEnabled { get; set; } = false;
 | 
			
		||||
 | 
			
		||||
        [Comment(@"Which patron tier do users need in order to use the .xpshop command
 | 
			
		||||
Leave at 'None' if patron system is disabled or you don't want any restrictions")]
 | 
			
		||||
        public PatronTier TierRequirement { get; set; } = PatronTier.None;
 | 
			
		||||
        
 | 
			
		||||
        [Comment(@"Frames available for sale. Keys are unique IDs.
 | 
			
		||||
Do not change keys as they are not publicly visible. Only change properties (name, price, id)
 | 
			
		||||
Removing a key compeltely means all previous purchases will also be unusable.
 | 
			
		||||
To remove an item from the shop, but keep previous purchases, set the price to -1")]
 | 
			
		||||
        public Dictionary<string, ShopItemInfo>? Frames { get; set; } = new()
 | 
			
		||||
        {
 | 
			
		||||
            {"default", new() {Name = "No frame", Price = 0, Url = string.Empty}}
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        [Comment(@"Backgrounds available for sale. Keys are unique IDs. 
 | 
			
		||||
Do not change keys as they are not publicly visible. Only change properties (name, price, id)
 | 
			
		||||
Removing a key compeltely means all previous purchases will also be unusable.
 | 
			
		||||
To remove an item from the shop, but keep previous purchases, set the price to -1")]
 | 
			
		||||
        public Dictionary<string, ShopItemInfo>? Bgs { get; set; } = new()
 | 
			
		||||
        {
 | 
			
		||||
            {"default", new() {Name = "Default Background", Price = 0, Url = string.Empty}}
 | 
			
		||||
        };
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public sealed class ShopItemInfo
 | 
			
		||||
    {
 | 
			
		||||
        [Comment(@"Visible name of the item")]
 | 
			
		||||
        public string Name { get; set; }
 | 
			
		||||
        
 | 
			
		||||
        [Comment(@"Price of the item. Set to -1 if you no longer want to sell the item but want the users to be able to keep their old purchase")]
 | 
			
		||||
        public int Price { get; set; }
 | 
			
		||||
        
 | 
			
		||||
        [Comment(@"Direct url to the .png image which will be applied to the user's XP card")]
 | 
			
		||||
        public string Url { get; set; }
 | 
			
		||||
        
 | 
			
		||||
        [Comment(@"Optional description of the item")]
 | 
			
		||||
        public string Desc { get; set; }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
public static class XpShopConfigExtensions
 | 
			
		||||
{
 | 
			
		||||
    public static string? GetItemUrl(this XpConfig.ShopConfig sc, XpShopItemType type, string key)
 | 
			
		||||
        => (type switch
 | 
			
		||||
        {
 | 
			
		||||
            XpShopItemType.Background => sc.Bgs,
 | 
			
		||||
            _ => sc.Frames
 | 
			
		||||
        })?.TryGetValue(key, out var item) ?? false
 | 
			
		||||
            ? item.Url
 | 
			
		||||
            : null;
 | 
			
		||||
}
 | 
			
		||||
@@ -46,5 +46,13 @@ public sealed class XpConfigService : ConfigServiceBase<XpConfig>
 | 
			
		||||
                c.XpFromImage = 0;
 | 
			
		||||
            });
 | 
			
		||||
        }
 | 
			
		||||
        
 | 
			
		||||
        if (data.Version < 3)
 | 
			
		||||
        {
 | 
			
		||||
            ModifyConfig(c =>
 | 
			
		||||
            {
 | 
			
		||||
                c.Version = 3;
 | 
			
		||||
            });
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -13,7 +13,9 @@ using SixLabors.ImageSharp.Formats;
 | 
			
		||||
using SixLabors.ImageSharp.PixelFormats;
 | 
			
		||||
using SixLabors.ImageSharp.Processing;
 | 
			
		||||
using System.Threading.Channels;
 | 
			
		||||
using LinqToDB.EntityFrameworkCore;
 | 
			
		||||
using Color = SixLabors.ImageSharp.Color;
 | 
			
		||||
using Exception = System.Exception;
 | 
			
		||||
using Image = SixLabors.ImageSharp.Image;
 | 
			
		||||
 | 
			
		||||
namespace NadekoBot.Modules.Xp.Services;
 | 
			
		||||
@@ -1124,21 +1126,48 @@ public class XpService : INService, IReadyExecutor, IExecNoCommand
 | 
			
		||||
            return output;
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
    private async Task<byte[]?> GetXpBackgroundAsync(ulong _)
 | 
			
		||||
    private async Task<byte[]?> GetXpBackgroundAsync(ulong userId)
 | 
			
		||||
    {
 | 
			
		||||
        var img = await _images.GetXpBackgroundImageAsync();
 | 
			
		||||
        return img;
 | 
			
		||||
        var item = await GetItemInUse(userId, XpShopItemType.Background);
 | 
			
		||||
        if (item is null)
 | 
			
		||||
        {
 | 
			
		||||
            return await _images.GetXpBackgroundImageAsync();
 | 
			
		||||
        }
 | 
			
		||||
        
 | 
			
		||||
        var url = _xpConfig.Data.Shop.GetItemUrl(XpShopItemType.Background, item.ItemKey);
 | 
			
		||||
        if (!string.IsNullOrWhiteSpace(url))
 | 
			
		||||
        {
 | 
			
		||||
            var data = await _images.GetImageDataAsync(new Uri(url));
 | 
			
		||||
            return data;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return await _images.GetXpBackgroundImageAsync();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // #if GLOBAL_NADEKO
 | 
			
		||||
    private async Task DrawFrame(Image<Rgba32> img, ulong userId)
 | 
			
		||||
    {
 | 
			
		||||
        var patron = await _ps.GetPatronAsync(userId);
 | 
			
		||||
 | 
			
		||||
        var item = await GetItemInUse(userId, XpShopItemType.Frame);
 | 
			
		||||
 | 
			
		||||
        Image? frame = null;
 | 
			
		||||
        if (item is null)
 | 
			
		||||
        {
 | 
			
		||||
            if (patron.Tier == PatronTier.V)
 | 
			
		||||
                frame = Image.Load<Rgba32>(File.OpenRead("data/images/frame_silver.png"));
 | 
			
		||||
            else if (patron.Tier >= PatronTier.X || _creds.IsOwner(userId))
 | 
			
		||||
                frame = Image.Load<Rgba32>(File.OpenRead("data/images/frame_gold.png"));
 | 
			
		||||
        }
 | 
			
		||||
        else
 | 
			
		||||
        {
 | 
			
		||||
            var url = _xpConfig.Data.Shop.GetItemUrl(XpShopItemType.Frame, item.ItemKey);
 | 
			
		||||
            if (!string.IsNullOrWhiteSpace(url))
 | 
			
		||||
            {
 | 
			
		||||
                var data = await _images.GetImageDataAsync(new Uri(url));
 | 
			
		||||
                frame = Image.Load<Rgba32>(data);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (frame is not null)
 | 
			
		||||
            img.Mutate(x => x.DrawImage(frame, new Point(0, 0), new GraphicsOptions()));
 | 
			
		||||
@@ -1270,4 +1299,171 @@ public class XpService : INService, IReadyExecutor, IExecNoCommand
 | 
			
		||||
        uow.RemoveRange(guildConfig.XpSettings.CurrencyRewards);
 | 
			
		||||
        await uow.SaveChangesAsync();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public ValueTask<Dictionary<string, XpConfig.ShopItemInfo>?> GetShopBgs()
 | 
			
		||||
    {
 | 
			
		||||
        var data = _xpConfig.Data;
 | 
			
		||||
        if (!data.Shop.IsEnabled)
 | 
			
		||||
            return new(default(Dictionary<string, XpConfig.ShopItemInfo>));
 | 
			
		||||
 | 
			
		||||
        return new(_xpConfig.Data.Shop.Bgs?.Where(x => x.Value.Price >= 0).ToDictionary(x => x.Key, x => x.Value));
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    public ValueTask<Dictionary<string, XpConfig.ShopItemInfo>?> GetShopFrames()
 | 
			
		||||
    {
 | 
			
		||||
        var data = _xpConfig.Data;
 | 
			
		||||
        if (!data.Shop.IsEnabled)
 | 
			
		||||
            return new(default(Dictionary<string, XpConfig.ShopItemInfo>));
 | 
			
		||||
        
 | 
			
		||||
        return new(_xpConfig.Data.Shop.Frames?.Where(x => x.Value.Price >= 0).ToDictionary(x => x.Key, x => x.Value));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public async Task<BuyResult> BuyShopItemAsync(ulong userId, XpShopItemType type, string key)
 | 
			
		||||
    {
 | 
			
		||||
        var conf = _xpConfig.Data;
 | 
			
		||||
 | 
			
		||||
        if (!conf.Shop.IsEnabled)
 | 
			
		||||
            return BuyResult.UnknownItem;
 | 
			
		||||
 | 
			
		||||
        if (conf.Shop.TierRequirement != PatronTier.None)
 | 
			
		||||
        {
 | 
			
		||||
            var patron = await _ps.GetPatronAsync(userId);
 | 
			
		||||
 | 
			
		||||
            if ((int)patron.Tier < (int)conf.Shop.TierRequirement)
 | 
			
		||||
                return BuyResult.InsufficientPatronTier;
 | 
			
		||||
        }
 | 
			
		||||
        
 | 
			
		||||
        await using var ctx = _db.GetDbContext();
 | 
			
		||||
        // await using var tran = await ctx.Database.BeginTransactionAsync();
 | 
			
		||||
        try
 | 
			
		||||
        {
 | 
			
		||||
            if (await ctx.GetTable<XpShopOwnedItem>().AnyAsyncLinqToDB(x => x.ItemKey == key && x.ItemType == type))
 | 
			
		||||
                return BuyResult.AlreadyOwned;
 | 
			
		||||
 | 
			
		||||
            var item = GetShopItem(type, key);
 | 
			
		||||
 | 
			
		||||
            if (item is null || item.Price < 0)
 | 
			
		||||
                return BuyResult.UnknownItem;
 | 
			
		||||
 | 
			
		||||
            if (item.Price > 0 && !await _cs.RemoveAsync(userId, item.Price, new("xpshop", "buy", $"Background {key}")))
 | 
			
		||||
                return BuyResult.InsufficientFunds;
 | 
			
		||||
                
 | 
			
		||||
 | 
			
		||||
            await ctx.GetTable<XpShopOwnedItem>()
 | 
			
		||||
                .InsertAsync(() => new XpShopOwnedItem()
 | 
			
		||||
                {
 | 
			
		||||
                    UserId = userId,
 | 
			
		||||
                    IsUsing = false,
 | 
			
		||||
                    ItemKey = key,
 | 
			
		||||
                    ItemType = type
 | 
			
		||||
                });
 | 
			
		||||
 | 
			
		||||
            return BuyResult.Success;
 | 
			
		||||
        }
 | 
			
		||||
        catch (Exception ex)
 | 
			
		||||
        {
 | 
			
		||||
            Log.Error(ex, "Error buying shop item: {ErrorMessage}", ex.Message);
 | 
			
		||||
            return BuyResult.UnknownItem;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private XpConfig.ShopItemInfo? GetShopItem(XpShopItemType type, string key)
 | 
			
		||||
    {
 | 
			
		||||
        var data = _xpConfig.Data;
 | 
			
		||||
        if (type == XpShopItemType.Background)
 | 
			
		||||
        {
 | 
			
		||||
            if (data.Shop.Bgs is {} bgs && bgs.TryGetValue(key, out var item))
 | 
			
		||||
                return item;
 | 
			
		||||
            
 | 
			
		||||
            return null;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (type == XpShopItemType.Frame)
 | 
			
		||||
        {
 | 
			
		||||
            if (data.Shop.Frames is {} fs && fs.TryGetValue(key, out var item))
 | 
			
		||||
                return item;
 | 
			
		||||
            
 | 
			
		||||
            return null;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        throw new ArgumentOutOfRangeException(nameof(type));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public async Task<bool> OwnsItemAsync(ulong userId,
 | 
			
		||||
        XpShopItemType itemType,
 | 
			
		||||
        string key)
 | 
			
		||||
    {
 | 
			
		||||
        await using var ctx = _db.GetDbContext();
 | 
			
		||||
        return await ctx.GetTable<XpShopOwnedItem>()
 | 
			
		||||
            .AnyAsyncLinqToDB(x => x.UserId == userId
 | 
			
		||||
                                   && x.ItemType == itemType
 | 
			
		||||
                                   && x.ItemKey == key);
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    
 | 
			
		||||
    public async Task<XpShopOwnedItem?> GetUserItemAsync(ulong userId,
 | 
			
		||||
        XpShopItemType itemType,
 | 
			
		||||
        string key)
 | 
			
		||||
    {
 | 
			
		||||
        await using var ctx = _db.GetDbContext();
 | 
			
		||||
        return await ctx.GetTable<XpShopOwnedItem>()
 | 
			
		||||
            .FirstOrDefaultAsyncLinqToDB(x => x.UserId == userId
 | 
			
		||||
                                              && x.ItemType == itemType
 | 
			
		||||
                                              && x.ItemKey == key);
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    public async Task<XpShopOwnedItem?> GetItemInUse(ulong userId,
 | 
			
		||||
        XpShopItemType itemType)
 | 
			
		||||
    {
 | 
			
		||||
        await using var ctx = _db.GetDbContext();
 | 
			
		||||
        return await ctx.GetTable<XpShopOwnedItem>()
 | 
			
		||||
            .FirstOrDefaultAsyncLinqToDB(x => x.UserId == userId
 | 
			
		||||
                                              && x.ItemType == itemType
 | 
			
		||||
                                              && x.IsUsing);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public async Task<bool> UseShopItemAsync(ulong userId, XpShopItemType itemType, string key)
 | 
			
		||||
    {
 | 
			
		||||
        var data = _xpConfig.Data;
 | 
			
		||||
        XpConfig.ShopItemInfo? item = null;
 | 
			
		||||
        if (itemType == XpShopItemType.Background)
 | 
			
		||||
        {
 | 
			
		||||
            data.Shop.Bgs?.TryGetValue(key, out item);
 | 
			
		||||
        }
 | 
			
		||||
        else
 | 
			
		||||
        {
 | 
			
		||||
            data.Shop.Frames?.TryGetValue(key, out item);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (item is null)
 | 
			
		||||
            return false;
 | 
			
		||||
 | 
			
		||||
        await using var ctx = _db.GetDbContext();
 | 
			
		||||
 | 
			
		||||
        if (await OwnsItemAsync(userId, itemType, key))
 | 
			
		||||
        {
 | 
			
		||||
            await ctx.GetTable<XpShopOwnedItem>()
 | 
			
		||||
                .Where(x => x.UserId == userId && x.ItemType == itemType)
 | 
			
		||||
                .UpdateAsync(old => new()
 | 
			
		||||
                {
 | 
			
		||||
                    IsUsing = key == old.ItemKey
 | 
			
		||||
                });
 | 
			
		||||
 | 
			
		||||
            return true;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return false;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public PatronTier GetXpShopTierRequirement()
 | 
			
		||||
        => _xpConfig.Data.Shop.TierRequirement;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
public enum BuyResult
 | 
			
		||||
{
 | 
			
		||||
    Success,
 | 
			
		||||
    AlreadyOwned,
 | 
			
		||||
    InsufficientFunds,
 | 
			
		||||
    UnknownItem,
 | 
			
		||||
    InsufficientPatronTier,
 | 
			
		||||
}
 | 
			
		||||
@@ -31,6 +31,7 @@ public interface IImageCache
 | 
			
		||||
    Task<byte[]?> GetSlotBgAsync();
 | 
			
		||||
    Task<byte[]?> GetRipBgAsync();
 | 
			
		||||
    Task<byte[]?> GetRipOverlayAsync();
 | 
			
		||||
    Task<byte[]?> GetImageDataAsync(Uri url);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
public sealed class ImageCache : IImageCache, INService
 | 
			
		||||
@@ -54,7 +55,7 @@ public sealed class ImageCache : IImageCache, INService
 | 
			
		||||
    private static TypedKey<byte[]> GetImageKey(Uri url)
 | 
			
		||||
        => new($"image:{url}");
 | 
			
		||||
 | 
			
		||||
    private async Task<byte[]?> GetImageDataAsync(Uri url)
 | 
			
		||||
    public async Task<byte[]?> GetImageDataAsync(Uri url)
 | 
			
		||||
        => await _cache.GetOrAddAsync(
 | 
			
		||||
            GetImageKey(url),
 | 
			
		||||
            async () =>
 | 
			
		||||
 
 | 
			
		||||
@@ -1,4 +1,5 @@
 | 
			
		||||
using Nadeko.Common;
 | 
			
		||||
using NadekoBot.Modules.Xp;
 | 
			
		||||
 | 
			
		||||
namespace NadekoBot.Extensions;
 | 
			
		||||
 | 
			
		||||
@@ -194,13 +195,28 @@ public static class MessageChannelExtensions
 | 
			
		||||
    private static readonly IEmote _arrowLeft = Emote.Parse("<:x:969658061805465651>");
 | 
			
		||||
    private static readonly IEmote _arrowRight = Emote.Parse("<:x:969658062220701746>");
 | 
			
		||||
 | 
			
		||||
    public static async Task SendPaginatedConfirmAsync(
 | 
			
		||||
    public static Task SendPaginatedConfirmAsync(
 | 
			
		||||
        this ICommandContext ctx,
 | 
			
		||||
        int currentPage,
 | 
			
		||||
        Func<int, Task<IEmbedBuilder>> pageFunc,
 | 
			
		||||
        int totalElements,
 | 
			
		||||
        int itemsPerPage,
 | 
			
		||||
        bool addPaginatedFooter = true)
 | 
			
		||||
        => ctx.SendPaginatedConfirmAsync(currentPage,
 | 
			
		||||
            pageFunc,
 | 
			
		||||
            default(Func<int, ValueTask<SimpleInteraction<object>?>>),
 | 
			
		||||
            totalElements,
 | 
			
		||||
            itemsPerPage,
 | 
			
		||||
            addPaginatedFooter);
 | 
			
		||||
    
 | 
			
		||||
    public static async Task SendPaginatedConfirmAsync<T>(
 | 
			
		||||
        this ICommandContext ctx,
 | 
			
		||||
        int currentPage,
 | 
			
		||||
        Func<int, Task<IEmbedBuilder>> pageFunc,
 | 
			
		||||
        Func<int, ValueTask<SimpleInteraction<T>?>>? interFactory,
 | 
			
		||||
        int totalElements,
 | 
			
		||||
        int itemsPerPage,
 | 
			
		||||
        bool addPaginatedFooter = true)
 | 
			
		||||
    {
 | 
			
		||||
        var lastPage = (totalElements - 1) / itemsPerPage;
 | 
			
		||||
        
 | 
			
		||||
@@ -209,24 +225,54 @@ public static class MessageChannelExtensions
 | 
			
		||||
        if (addPaginatedFooter)
 | 
			
		||||
            embed.AddPaginatedFooter(currentPage, lastPage);
 | 
			
		||||
 | 
			
		||||
        var component = new ComponentBuilder()
 | 
			
		||||
                        .WithButton(new ButtonBuilder()
 | 
			
		||||
        SimpleInteraction<T>? maybeInter = null;
 | 
			
		||||
        async Task<ComponentBuilder> GetComponentBuilder()
 | 
			
		||||
        {
 | 
			
		||||
            var cb = new ComponentBuilder();
 | 
			
		||||
                
 | 
			
		||||
            cb.WithButton(new ButtonBuilder()
 | 
			
		||||
                .WithStyle(ButtonStyle.Primary)
 | 
			
		||||
                .WithCustomId(BUTTON_LEFT)
 | 
			
		||||
                .WithDisabled(lastPage == 0)
 | 
			
		||||
                                    .WithEmote(_arrowLeft))
 | 
			
		||||
                        .WithButton(new ButtonBuilder()
 | 
			
		||||
                .WithEmote(_arrowLeft)
 | 
			
		||||
                .WithDisabled(currentPage <= 0));
 | 
			
		||||
 | 
			
		||||
            if (interFactory is not null)
 | 
			
		||||
            {
 | 
			
		||||
                maybeInter = await interFactory(currentPage);
 | 
			
		||||
 | 
			
		||||
                if (maybeInter is not null)
 | 
			
		||||
                    cb.WithButton(maybeInter.Button);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            cb.WithButton(new ButtonBuilder()
 | 
			
		||||
                .WithStyle(ButtonStyle.Primary)
 | 
			
		||||
                .WithCustomId(BUTTON_RIGHT)
 | 
			
		||||
                                    .WithDisabled(lastPage == 0)
 | 
			
		||||
                                    .WithEmote(_arrowRight))
 | 
			
		||||
                        .Build();
 | 
			
		||||
                .WithDisabled(lastPage == 0 || currentPage >= lastPage)
 | 
			
		||||
                .WithEmote(_arrowRight));
 | 
			
		||||
 | 
			
		||||
            return cb;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        async Task UpdatePageAsync(SocketMessageComponent smc)
 | 
			
		||||
        {
 | 
			
		||||
            var toSend = await pageFunc(currentPage);
 | 
			
		||||
            if (addPaginatedFooter)
 | 
			
		||||
                toSend.AddPaginatedFooter(currentPage, lastPage);
 | 
			
		||||
            
 | 
			
		||||
            var component = (await GetComponentBuilder()).Build();
 | 
			
		||||
 | 
			
		||||
            await smc.ModifyOriginalResponseAsync(x =>
 | 
			
		||||
            {
 | 
			
		||||
                x.Embed = toSend.Build();
 | 
			
		||||
                x.Components = component;
 | 
			
		||||
            });
 | 
			
		||||
        }
 | 
			
		||||
        
 | 
			
		||||
        var component = (await GetComponentBuilder()).Build();
 | 
			
		||||
        var msg = await ctx.Channel.SendAsync(null, embed: embed.Build(), components: component);
 | 
			
		||||
 | 
			
		||||
        Task OnInteractionAsync(SocketInteraction si)
 | 
			
		||||
        {
 | 
			
		||||
            _ = Task.Run(async () =>
 | 
			
		||||
        async Task OnInteractionAsync(SocketInteraction si)
 | 
			
		||||
        {
 | 
			
		||||
            try
 | 
			
		||||
            {
 | 
			
		||||
@@ -236,9 +282,6 @@ public static class MessageChannelExtensions
 | 
			
		||||
                if (smc.Message.Id != msg.Id)
 | 
			
		||||
                    return;
 | 
			
		||||
 | 
			
		||||
                    if (smc.Data.CustomId != BUTTON_LEFT && smc.Data.CustomId != BUTTON_RIGHT)
 | 
			
		||||
                        return;
 | 
			
		||||
 | 
			
		||||
                await si.DeferAsync();
 | 
			
		||||
                if (smc.User.Id != ctx.User.Id)
 | 
			
		||||
                    return;
 | 
			
		||||
@@ -248,31 +291,27 @@ public static class MessageChannelExtensions
 | 
			
		||||
                    if (currentPage == 0)
 | 
			
		||||
                        return;
 | 
			
		||||
 | 
			
		||||
                        var toSend = await pageFunc(--currentPage);
 | 
			
		||||
                        if (addPaginatedFooter)
 | 
			
		||||
                            toSend.AddPaginatedFooter(currentPage, lastPage);
 | 
			
		||||
 | 
			
		||||
                        await smc.ModifyOriginalResponseAsync(x => x.Embed = toSend.Build());
 | 
			
		||||
                    --currentPage;
 | 
			
		||||
                    _ = UpdatePageAsync(smc);
 | 
			
		||||
                }
 | 
			
		||||
                else if (smc.Data.CustomId == BUTTON_RIGHT)
 | 
			
		||||
                {
 | 
			
		||||
                        if (lastPage > currentPage)
 | 
			
		||||
                        {
 | 
			
		||||
                            var toSend = await pageFunc(++currentPage);
 | 
			
		||||
                            if (addPaginatedFooter)
 | 
			
		||||
                                toSend.AddPaginatedFooter(currentPage, lastPage);
 | 
			
		||||
                    if (currentPage >= lastPage)
 | 
			
		||||
                        return;
 | 
			
		||||
                    
 | 
			
		||||
                            await smc.ModifyOriginalResponseAsync(x => x.Embed = toSend.Build());
 | 
			
		||||
                    ++currentPage;
 | 
			
		||||
                    _ = UpdatePageAsync(smc);
 | 
			
		||||
                }
 | 
			
		||||
                else if (maybeInter is { } inter && inter.Button.CustomId == smc.Data.CustomId)
 | 
			
		||||
                {
 | 
			
		||||
                    await inter.TriggerAsync(smc);
 | 
			
		||||
                    _ = UpdatePageAsync(smc);
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            catch (Exception ex)
 | 
			
		||||
            {
 | 
			
		||||
                Log.Error(ex, "Error in pagination: {ErrorMessage}", ex.Message);
 | 
			
		||||
            }
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            return Task.CompletedTask;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (lastPage == 0)
 | 
			
		||||
 
 | 
			
		||||
@@ -1059,6 +1059,8 @@ xpgloballeaderboard:
 | 
			
		||||
  - xpglb
 | 
			
		||||
xpadd:
 | 
			
		||||
  - xpadd
 | 
			
		||||
xpshop:
 | 
			
		||||
  - xpshop
 | 
			
		||||
clubcreate:
 | 
			
		||||
  - clubcreate
 | 
			
		||||
clubtransfer:
 | 
			
		||||
 
 | 
			
		||||
@@ -2117,6 +2117,12 @@ xpreset:
 | 
			
		||||
  args:
 | 
			
		||||
    - "@Someone"
 | 
			
		||||
    - ""
 | 
			
		||||
xpshop:
 | 
			
		||||
  desc: "Access the xp shop (if enabled). You can purchase either xp card frames or backgrounds. You can optionally provide a page number"
 | 
			
		||||
  args:
 | 
			
		||||
    - "bgs"
 | 
			
		||||
    - "frames"
 | 
			
		||||
    - "bgs 3"
 | 
			
		||||
bible:
 | 
			
		||||
  desc: "Shows bible verse. You need to supply book name and chapter:verse"
 | 
			
		||||
  args:
 | 
			
		||||
 
 | 
			
		||||
@@ -807,6 +807,7 @@
 | 
			
		||||
  "xp_role_reward_remove_role": "Users who reach level {0} will lose {1} role.",
 | 
			
		||||
  "xp_receive_role": "Receive {0} role.",
 | 
			
		||||
  "xp_lose_role": "Lose {0} role.",
 | 
			
		||||
  "xp_shop_disabled": "Xp shop is disabled by the owner, or there are no items for sale.",
 | 
			
		||||
  "role_not_found": "Role not found: {0}",
 | 
			
		||||
  "cur_reward_cleared": "Reaching level {0} will no longer reward any {1}.",
 | 
			
		||||
  "cur_reward_added": "Users who reach level {0} will receive {1}.",
 | 
			
		||||
@@ -882,6 +883,10 @@
 | 
			
		||||
  "timely_reset": "All users will be able to claim timely currency again.",
 | 
			
		||||
  "timely_time": "It's time for your timely reward.",
 | 
			
		||||
  "price": "Price",
 | 
			
		||||
  "buy": "Buy",
 | 
			
		||||
  "use": "Use",
 | 
			
		||||
  "in_use": "In use",
 | 
			
		||||
  "xp_shop_item_cant_use": "You can't use this item as it doesn't exist or you don't own it.",
 | 
			
		||||
  "market_cap": "Market Cap",
 | 
			
		||||
  "market_cap_dominance": "Dominance",
 | 
			
		||||
  "circulating_supply": "Circulating Supply",
 | 
			
		||||
@@ -994,6 +999,8 @@
 | 
			
		||||
  "feature_limit_reached_you": "You've reached the limit of {0} for the {1} feature. You may be able to increase this limit by upgrading your patron tier.",
 | 
			
		||||
  "feature_limit_reached_owner": "Server owner has reached the limit of {0} for the {1} feature. Server owner may be able to upgrade this limit by upgrading patron tier.",
 | 
			
		||||
  "feature_limit_reached_either": "The limit of {0} for the {1} feature has been reached. Either you or the server owner may able to upgrade this limit by upgrading the patron tier.",
 | 
			
		||||
  "feature_required_tier": "Using this feature requires patron tier {0} or higher.",
 | 
			
		||||
  "available_commands": "Available Commands",
 | 
			
		||||
  "tier": "Tier",
 | 
			
		||||
  "pledge": "Pledge",
 | 
			
		||||
  "expires": "Expires",
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,5 @@
 | 
			
		||||
# DO NOT CHANGE
 | 
			
		||||
version: 2
 | 
			
		||||
version: 3
 | 
			
		||||
# How much XP will the users receive per message
 | 
			
		||||
xpPerMessage: 3
 | 
			
		||||
# How often can the users receive XP in minutes
 | 
			
		||||
@@ -10,3 +10,40 @@ xpFromImage: 0
 | 
			
		||||
voiceXpPerMinute: 0
 | 
			
		||||
# The maximum amount of minutes the bot will keep track of a user in a voice channel
 | 
			
		||||
voiceMaxMinutes: 720
 | 
			
		||||
# Xp Shop config
 | 
			
		||||
shop:
 | 
			
		||||
# Whether the xp shop is enabled
 | 
			
		||||
# True -> Users can access the xp shop using .xpshop command
 | 
			
		||||
# False -> Users can't access the xp shop
 | 
			
		||||
  isEnabled: true
 | 
			
		||||
  # Which patron tier do users need in order to use the .xpshop command
 | 
			
		||||
  # Leave at 'None' if patron system is disabled or you don't want any restrictions
 | 
			
		||||
  tierRequirement: None
 | 
			
		||||
  # Frames available for sale. Keys are unique IDs.
 | 
			
		||||
  # Do not change keys as they are not publicly visible. Only change properties (name, price, id)
 | 
			
		||||
  # Removing a key completely means all previous purchases will also be unusable.
 | 
			
		||||
  # To remove an item from the shop, but keep previous purchases, set the price to -1
 | 
			
		||||
  frames:
 | 
			
		||||
    default:
 | 
			
		||||
    # Visible name of the item
 | 
			
		||||
      name: No frame
 | 
			
		||||
      # Price of the item. Set to -1 if you no longer want to sell the item but want the users to be able to keep their old purchase
 | 
			
		||||
      price: 0
 | 
			
		||||
      # Direct url to the .png image which will be applied to the user's XP card
 | 
			
		||||
      url: ''
 | 
			
		||||
      # Optional description of the item
 | 
			
		||||
      desc: 
 | 
			
		||||
  # Backgrounds available for sale. Keys are unique IDs.
 | 
			
		||||
  # Do not change keys as they are not publicly visible. Only change properties (name, price, id)
 | 
			
		||||
  # Removing a key completely means all previous purchases will also be unusable.
 | 
			
		||||
  # To remove an item from the shop, but keep previous purchases, set the price to -1
 | 
			
		||||
  bgs:
 | 
			
		||||
    default:
 | 
			
		||||
    # Visible name of the item
 | 
			
		||||
      name: Default Background
 | 
			
		||||
      # Price of the item. Set to -1 if you no longer want to sell the item but want the users to be able to keep their old purchase
 | 
			
		||||
      price: 0
 | 
			
		||||
      # Direct url to the .png image which will be applied to the user's XP card
 | 
			
		||||
      url: ''
 | 
			
		||||
      # Optional description of the item
 | 
			
		||||
      desc: 
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user