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
 | 
					        #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
 | 
					#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
 | 
					#pragma warning disable 612, 618
 | 
				
			||||||
            modelBuilder
 | 
					            modelBuilder
 | 
				
			||||||
                .HasAnnotation("ProductVersion", "6.0.6")
 | 
					                .HasAnnotation("ProductVersion", "6.0.7")
 | 
				
			||||||
                .HasAnnotation("Relational:MaxIdentifierLength", 64);
 | 
					                .HasAnnotation("Relational:MaxIdentifierLength", 64);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            modelBuilder.Entity("NadekoBot.Db.Models.BankUser", b =>
 | 
					            modelBuilder.Entity("NadekoBot.Db.Models.BankUser", b =>
 | 
				
			||||||
@@ -366,6 +366,44 @@ namespace NadekoBot.Migrations.Mysql
 | 
				
			|||||||
                    b.ToTable("streamonlinemessages", (string)null);
 | 
					                    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 =>
 | 
					            modelBuilder.Entity("NadekoBot.Services.Database.Models.AntiAltSetting", b =>
 | 
				
			||||||
                {
 | 
					                {
 | 
				
			||||||
                    b.Property<int>("Id")
 | 
					                    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
 | 
					#pragma warning disable 612, 618
 | 
				
			||||||
            modelBuilder
 | 
					            modelBuilder
 | 
				
			||||||
                .HasAnnotation("ProductVersion", "6.0.6")
 | 
					                .HasAnnotation("ProductVersion", "6.0.7")
 | 
				
			||||||
                .HasAnnotation("Relational:MaxIdentifierLength", 63);
 | 
					                .HasAnnotation("Relational:MaxIdentifierLength", 63);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
 | 
					            NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
 | 
				
			||||||
@@ -378,6 +378,46 @@ namespace NadekoBot.Migrations.PostgreSql
 | 
				
			|||||||
                    b.ToTable("streamonlinemessages", (string)null);
 | 
					                    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 =>
 | 
					            modelBuilder.Entity("NadekoBot.Services.Database.Models.AntiAltSetting", b =>
 | 
				
			||||||
                {
 | 
					                {
 | 
				
			||||||
                    b.Property<int>("Id")
 | 
					                    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)
 | 
					        protected override void BuildModel(ModelBuilder modelBuilder)
 | 
				
			||||||
        {
 | 
					        {
 | 
				
			||||||
#pragma warning disable 612, 618
 | 
					#pragma warning disable 612, 618
 | 
				
			||||||
            modelBuilder.HasAnnotation("ProductVersion", "6.0.6");
 | 
					            modelBuilder.HasAnnotation("ProductVersion", "6.0.7");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            modelBuilder.Entity("NadekoBot.Db.Models.BankUser", b =>
 | 
					            modelBuilder.Entity("NadekoBot.Db.Models.BankUser", b =>
 | 
				
			||||||
                {
 | 
					                {
 | 
				
			||||||
@@ -288,6 +288,36 @@ namespace NadekoBot.Migrations
 | 
				
			|||||||
                    b.ToTable("StreamOnlineMessages");
 | 
					                    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 =>
 | 
					            modelBuilder.Entity("NadekoBot.Services.Database.Models.AntiAltSetting", b =>
 | 
				
			||||||
                {
 | 
					                {
 | 
				
			||||||
                    b.Property<int>("Id")
 | 
					                    b.Property<int>("Id")
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,9 +1,10 @@
 | 
				
			|||||||
#nullable disable
 | 
					#nullable disable warnings
 | 
				
			||||||
using NadekoBot.Modules.Gambling.Services;
 | 
					using NadekoBot.Modules.Gambling.Services;
 | 
				
			||||||
using NadekoBot.Modules.Xp.Services;
 | 
					using NadekoBot.Modules.Xp.Services;
 | 
				
			||||||
using NadekoBot.Services.Database.Models;
 | 
					using NadekoBot.Services.Database.Models;
 | 
				
			||||||
using System.Diagnostics;
 | 
					using System.Globalization;
 | 
				
			||||||
using Nadeko.Common;
 | 
					using NadekoBot.Db.Models;
 | 
				
			||||||
 | 
					using NadekoBot.Modules.Utility.Patronage;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
namespace NadekoBot.Modules.Xp;
 | 
					namespace NadekoBot.Modules.Xp;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -313,6 +314,154 @@ public partial class Xp : NadekoModule<XpService>
 | 
				
			|||||||
        await ReplyConfirmLocalizedAsync(strs.reset_server);
 | 
					        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)
 | 
					    private string GetNotifLocationString(XpNotificationLocation loc)
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        if (loc == XpNotificationLocation.Channel)
 | 
					        if (loc == XpNotificationLocation.Channel)
 | 
				
			||||||
@@ -324,3 +473,22 @@ public partial class Xp : NadekoModule<XpService>
 | 
				
			|||||||
        return GetText(strs.xpn_notif_disabled);
 | 
					        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 Cloneable;
 | 
				
			||||||
using NadekoBot.Common.Yml;
 | 
					using NadekoBot.Common.Yml;
 | 
				
			||||||
 | 
					using NadekoBot.Db.Models;
 | 
				
			||||||
 | 
					using NadekoBot.Modules.Utility.Patronage;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
namespace NadekoBot.Modules.Xp;
 | 
					namespace NadekoBot.Modules.Xp;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -8,7 +10,7 @@ namespace NadekoBot.Modules.Xp;
 | 
				
			|||||||
public sealed partial class XpConfig : ICloneable<XpConfig>
 | 
					public sealed partial class XpConfig : ICloneable<XpConfig>
 | 
				
			||||||
{
 | 
					{
 | 
				
			||||||
    [Comment(@"DO NOT CHANGE")]
 | 
					    [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")]
 | 
					    [Comment(@"How much XP will the users receive per message")]
 | 
				
			||||||
    public int XpPerMessage { get; set; } = 3;
 | 
					    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")]
 | 
					    [Comment(@"The maximum amount of minutes the bot will keep track of a user in a voice channel")]
 | 
				
			||||||
    public int VoiceMaxMinutes { get; set; } = 720;
 | 
					    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;
 | 
					                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.PixelFormats;
 | 
				
			||||||
using SixLabors.ImageSharp.Processing;
 | 
					using SixLabors.ImageSharp.Processing;
 | 
				
			||||||
using System.Threading.Channels;
 | 
					using System.Threading.Channels;
 | 
				
			||||||
 | 
					using LinqToDB.EntityFrameworkCore;
 | 
				
			||||||
using Color = SixLabors.ImageSharp.Color;
 | 
					using Color = SixLabors.ImageSharp.Color;
 | 
				
			||||||
 | 
					using Exception = System.Exception;
 | 
				
			||||||
using Image = SixLabors.ImageSharp.Image;
 | 
					using Image = SixLabors.ImageSharp.Image;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
namespace NadekoBot.Modules.Xp.Services;
 | 
					namespace NadekoBot.Modules.Xp.Services;
 | 
				
			||||||
@@ -1124,21 +1126,48 @@ public class XpService : INService, IReadyExecutor, IExecNoCommand
 | 
				
			|||||||
            return output;
 | 
					            return output;
 | 
				
			||||||
        });
 | 
					        });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    private async Task<byte[]?> GetXpBackgroundAsync(ulong _)
 | 
					    private async Task<byte[]?> GetXpBackgroundAsync(ulong userId)
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        var img = await _images.GetXpBackgroundImageAsync();
 | 
					        var item = await GetItemInUse(userId, XpShopItemType.Background);
 | 
				
			||||||
        return img;
 | 
					        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
 | 
					    // #if GLOBAL_NADEKO
 | 
				
			||||||
    private async Task DrawFrame(Image<Rgba32> img, ulong userId)
 | 
					    private async Task DrawFrame(Image<Rgba32> img, ulong userId)
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        var patron = await _ps.GetPatronAsync(userId);
 | 
					        var patron = await _ps.GetPatronAsync(userId);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        var item = await GetItemInUse(userId, XpShopItemType.Frame);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        Image? frame = null;
 | 
					        Image? frame = null;
 | 
				
			||||||
        if (patron.Tier == PatronTier.V)
 | 
					        if (item is null)
 | 
				
			||||||
            frame = Image.Load<Rgba32>(File.OpenRead("data/images/frame_silver.png"));
 | 
					        {
 | 
				
			||||||
        else if (patron.Tier >= PatronTier.X || _creds.IsOwner(userId))
 | 
					            if (patron.Tier == PatronTier.V)
 | 
				
			||||||
            frame = Image.Load<Rgba32>(File.OpenRead("data/images/frame_gold.png"));
 | 
					                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)
 | 
					        if (frame is not null)
 | 
				
			||||||
            img.Mutate(x => x.DrawImage(frame, new Point(0, 0), new GraphicsOptions()));
 | 
					            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);
 | 
					        uow.RemoveRange(guildConfig.XpSettings.CurrencyRewards);
 | 
				
			||||||
        await uow.SaveChangesAsync();
 | 
					        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[]?> GetSlotBgAsync();
 | 
				
			||||||
    Task<byte[]?> GetRipBgAsync();
 | 
					    Task<byte[]?> GetRipBgAsync();
 | 
				
			||||||
    Task<byte[]?> GetRipOverlayAsync();
 | 
					    Task<byte[]?> GetRipOverlayAsync();
 | 
				
			||||||
 | 
					    Task<byte[]?> GetImageDataAsync(Uri url);
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
public sealed class ImageCache : IImageCache, INService
 | 
					public sealed class ImageCache : IImageCache, INService
 | 
				
			||||||
@@ -54,7 +55,7 @@ public sealed class ImageCache : IImageCache, INService
 | 
				
			|||||||
    private static TypedKey<byte[]> GetImageKey(Uri url)
 | 
					    private static TypedKey<byte[]> GetImageKey(Uri url)
 | 
				
			||||||
        => new($"image:{url}");
 | 
					        => new($"image:{url}");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    private async Task<byte[]?> GetImageDataAsync(Uri url)
 | 
					    public async Task<byte[]?> GetImageDataAsync(Uri url)
 | 
				
			||||||
        => await _cache.GetOrAddAsync(
 | 
					        => await _cache.GetOrAddAsync(
 | 
				
			||||||
            GetImageKey(url),
 | 
					            GetImageKey(url),
 | 
				
			||||||
            async () =>
 | 
					            async () =>
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,4 +1,5 @@
 | 
				
			|||||||
using Nadeko.Common;
 | 
					using Nadeko.Common;
 | 
				
			||||||
 | 
					using NadekoBot.Modules.Xp;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
namespace NadekoBot.Extensions;
 | 
					namespace NadekoBot.Extensions;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -194,13 +195,28 @@ public static class MessageChannelExtensions
 | 
				
			|||||||
    private static readonly IEmote _arrowLeft = Emote.Parse("<:x:969658061805465651>");
 | 
					    private static readonly IEmote _arrowLeft = Emote.Parse("<:x:969658061805465651>");
 | 
				
			||||||
    private static readonly IEmote _arrowRight = Emote.Parse("<:x:969658062220701746>");
 | 
					    private static readonly IEmote _arrowRight = Emote.Parse("<:x:969658062220701746>");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    public static async Task SendPaginatedConfirmAsync(
 | 
					    public static Task SendPaginatedConfirmAsync(
 | 
				
			||||||
        this ICommandContext ctx,
 | 
					        this ICommandContext ctx,
 | 
				
			||||||
        int currentPage,
 | 
					        int currentPage,
 | 
				
			||||||
        Func<int, Task<IEmbedBuilder>> pageFunc,
 | 
					        Func<int, Task<IEmbedBuilder>> pageFunc,
 | 
				
			||||||
        int totalElements,
 | 
					        int totalElements,
 | 
				
			||||||
        int itemsPerPage,
 | 
					        int itemsPerPage,
 | 
				
			||||||
        bool addPaginatedFooter = true)
 | 
					        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;
 | 
					        var lastPage = (totalElements - 1) / itemsPerPage;
 | 
				
			||||||
        
 | 
					        
 | 
				
			||||||
@@ -209,70 +225,93 @@ public static class MessageChannelExtensions
 | 
				
			|||||||
        if (addPaginatedFooter)
 | 
					        if (addPaginatedFooter)
 | 
				
			||||||
            embed.AddPaginatedFooter(currentPage, lastPage);
 | 
					            embed.AddPaginatedFooter(currentPage, lastPage);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        var component = new ComponentBuilder()
 | 
					        SimpleInteraction<T>? maybeInter = null;
 | 
				
			||||||
                        .WithButton(new ButtonBuilder()
 | 
					        async Task<ComponentBuilder> GetComponentBuilder()
 | 
				
			||||||
                                    .WithStyle(ButtonStyle.Primary)
 | 
					        {
 | 
				
			||||||
                                    .WithCustomId(BUTTON_LEFT)
 | 
					            var cb = new ComponentBuilder();
 | 
				
			||||||
                                    .WithDisabled(lastPage == 0)
 | 
					 | 
				
			||||||
                                    .WithEmote(_arrowLeft))
 | 
					 | 
				
			||||||
                        .WithButton(new ButtonBuilder()
 | 
					 | 
				
			||||||
                                    .WithStyle(ButtonStyle.Primary)
 | 
					 | 
				
			||||||
                                    .WithCustomId(BUTTON_RIGHT)
 | 
					 | 
				
			||||||
                                    .WithDisabled(lastPage == 0)
 | 
					 | 
				
			||||||
                                    .WithEmote(_arrowRight))
 | 
					 | 
				
			||||||
                        .Build();
 | 
					 | 
				
			||||||
                
 | 
					                
 | 
				
			||||||
 | 
					            cb.WithButton(new ButtonBuilder()
 | 
				
			||||||
 | 
					                .WithStyle(ButtonStyle.Primary)
 | 
				
			||||||
 | 
					                .WithCustomId(BUTTON_LEFT)
 | 
				
			||||||
 | 
					                .WithDisabled(lastPage == 0)
 | 
				
			||||||
 | 
					                .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 || 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);
 | 
					        var msg = await ctx.Channel.SendAsync(null, embed: embed.Build(), components: component);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        Task OnInteractionAsync(SocketInteraction si)
 | 
					        async Task OnInteractionAsync(SocketInteraction si)
 | 
				
			||||||
        {
 | 
					        {
 | 
				
			||||||
            _ = Task.Run(async () =>
 | 
					            try
 | 
				
			||||||
            {
 | 
					            {
 | 
				
			||||||
                try
 | 
					                if (si is not SocketMessageComponent smc)
 | 
				
			||||||
 | 
					                    return;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                if (smc.Message.Id != msg.Id)
 | 
				
			||||||
 | 
					                    return;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                await si.DeferAsync();
 | 
				
			||||||
 | 
					                if (smc.User.Id != ctx.User.Id)
 | 
				
			||||||
 | 
					                    return;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                if (smc.Data.CustomId == BUTTON_LEFT)
 | 
				
			||||||
                {
 | 
					                {
 | 
				
			||||||
                    if (si is not SocketMessageComponent smc)
 | 
					                    if (currentPage == 0)
 | 
				
			||||||
                        return;
 | 
					                        return;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                    if (smc.Message.Id != msg.Id)
 | 
					                    --currentPage;
 | 
				
			||||||
                        return;
 | 
					                    _ = UpdatePageAsync(smc);
 | 
				
			||||||
 | 
					 | 
				
			||||||
                    if (smc.Data.CustomId != BUTTON_LEFT && smc.Data.CustomId != BUTTON_RIGHT)
 | 
					 | 
				
			||||||
                        return;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                    await si.DeferAsync();
 | 
					 | 
				
			||||||
                    if (smc.User.Id != ctx.User.Id)
 | 
					 | 
				
			||||||
                        return;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                    if (smc.Data.CustomId == BUTTON_LEFT)
 | 
					 | 
				
			||||||
                    {
 | 
					 | 
				
			||||||
                        if (currentPage == 0)
 | 
					 | 
				
			||||||
                            return;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                        var toSend = await pageFunc(--currentPage);
 | 
					 | 
				
			||||||
                        if (addPaginatedFooter)
 | 
					 | 
				
			||||||
                            toSend.AddPaginatedFooter(currentPage, lastPage);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                        await smc.ModifyOriginalResponseAsync(x => x.Embed = toSend.Build());
 | 
					 | 
				
			||||||
                    }
 | 
					 | 
				
			||||||
                    else if (smc.Data.CustomId == BUTTON_RIGHT)
 | 
					 | 
				
			||||||
                    {
 | 
					 | 
				
			||||||
                        if (lastPage > currentPage)
 | 
					 | 
				
			||||||
                        {
 | 
					 | 
				
			||||||
                            var toSend = await pageFunc(++currentPage);
 | 
					 | 
				
			||||||
                            if (addPaginatedFooter)
 | 
					 | 
				
			||||||
                                toSend.AddPaginatedFooter(currentPage, lastPage);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                            await smc.ModifyOriginalResponseAsync(x => x.Embed = toSend.Build());
 | 
					 | 
				
			||||||
                        }
 | 
					 | 
				
			||||||
                    }
 | 
					 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
                catch (Exception ex)
 | 
					                else if (smc.Data.CustomId == BUTTON_RIGHT)
 | 
				
			||||||
                {
 | 
					                {
 | 
				
			||||||
                    Log.Error(ex, "Error in pagination: {ErrorMessage}", ex.Message);
 | 
					                    if (currentPage >= lastPage)
 | 
				
			||||||
                }
 | 
					                        return;
 | 
				
			||||||
            });
 | 
					 | 
				
			||||||
                    
 | 
					                    
 | 
				
			||||||
            return Task.CompletedTask;
 | 
					                    ++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);
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if (lastPage == 0)
 | 
					        if (lastPage == 0)
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1059,6 +1059,8 @@ xpgloballeaderboard:
 | 
				
			|||||||
  - xpglb
 | 
					  - xpglb
 | 
				
			||||||
xpadd:
 | 
					xpadd:
 | 
				
			||||||
  - xpadd
 | 
					  - xpadd
 | 
				
			||||||
 | 
					xpshop:
 | 
				
			||||||
 | 
					  - xpshop
 | 
				
			||||||
clubcreate:
 | 
					clubcreate:
 | 
				
			||||||
  - clubcreate
 | 
					  - clubcreate
 | 
				
			||||||
clubtransfer:
 | 
					clubtransfer:
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -2117,6 +2117,12 @@ xpreset:
 | 
				
			|||||||
  args:
 | 
					  args:
 | 
				
			||||||
    - "@Someone"
 | 
					    - "@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:
 | 
					bible:
 | 
				
			||||||
  desc: "Shows bible verse. You need to supply book name and chapter:verse"
 | 
					  desc: "Shows bible verse. You need to supply book name and chapter:verse"
 | 
				
			||||||
  args:
 | 
					  args:
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -807,6 +807,7 @@
 | 
				
			|||||||
  "xp_role_reward_remove_role": "Users who reach level {0} will lose {1} role.",
 | 
					  "xp_role_reward_remove_role": "Users who reach level {0} will lose {1} role.",
 | 
				
			||||||
  "xp_receive_role": "Receive {0} role.",
 | 
					  "xp_receive_role": "Receive {0} role.",
 | 
				
			||||||
  "xp_lose_role": "Lose {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}",
 | 
					  "role_not_found": "Role not found: {0}",
 | 
				
			||||||
  "cur_reward_cleared": "Reaching level {0} will no longer reward any {1}.",
 | 
					  "cur_reward_cleared": "Reaching level {0} will no longer reward any {1}.",
 | 
				
			||||||
  "cur_reward_added": "Users who reach level {0} will receive {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_reset": "All users will be able to claim timely currency again.",
 | 
				
			||||||
  "timely_time": "It's time for your timely reward.",
 | 
					  "timely_time": "It's time for your timely reward.",
 | 
				
			||||||
  "price": "Price",
 | 
					  "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": "Market Cap",
 | 
				
			||||||
  "market_cap_dominance": "Dominance",
 | 
					  "market_cap_dominance": "Dominance",
 | 
				
			||||||
  "circulating_supply": "Circulating Supply",
 | 
					  "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_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_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_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",
 | 
					  "tier": "Tier",
 | 
				
			||||||
  "pledge": "Pledge",
 | 
					  "pledge": "Pledge",
 | 
				
			||||||
  "expires": "Expires",
 | 
					  "expires": "Expires",
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,5 +1,5 @@
 | 
				
			|||||||
# DO NOT CHANGE
 | 
					# DO NOT CHANGE
 | 
				
			||||||
version: 2
 | 
					version: 3
 | 
				
			||||||
# How much XP will the users receive per message
 | 
					# How much XP will the users receive per message
 | 
				
			||||||
xpPerMessage: 3
 | 
					xpPerMessage: 3
 | 
				
			||||||
# How often can the users receive XP in minutes
 | 
					# How often can the users receive XP in minutes
 | 
				
			||||||
@@ -10,3 +10,40 @@ xpFromImage: 0
 | 
				
			|||||||
voiceXpPerMinute: 0
 | 
					voiceXpPerMinute: 0
 | 
				
			||||||
# The maximum amount of minutes the bot will keep track of a user in a voice channel
 | 
					# The maximum amount of minutes the bot will keep track of a user in a voice channel
 | 
				
			||||||
voiceMaxMinutes: 720
 | 
					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