mirror of
https://gitlab.com/Kwoth/nadekobot.git
synced 2025-11-03 08:14:28 -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 (item is null)
|
||||||
|
{
|
||||||
if (patron.Tier == PatronTier.V)
|
if (patron.Tier == PatronTier.V)
|
||||||
frame = Image.Load<Rgba32>(File.OpenRead("data/images/frame_silver.png"));
|
frame = Image.Load<Rgba32>(File.OpenRead("data/images/frame_silver.png"));
|
||||||
else if (patron.Tier >= PatronTier.X || _creds.IsOwner(userId))
|
else if (patron.Tier >= PatronTier.X || _creds.IsOwner(userId))
|
||||||
frame = Image.Load<Rgba32>(File.OpenRead("data/images/frame_gold.png"));
|
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,24 +225,54 @@ 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()
|
||||||
|
{
|
||||||
|
var cb = new ComponentBuilder();
|
||||||
|
|
||||||
|
cb.WithButton(new ButtonBuilder()
|
||||||
.WithStyle(ButtonStyle.Primary)
|
.WithStyle(ButtonStyle.Primary)
|
||||||
.WithCustomId(BUTTON_LEFT)
|
.WithCustomId(BUTTON_LEFT)
|
||||||
.WithDisabled(lastPage == 0)
|
.WithDisabled(lastPage == 0)
|
||||||
.WithEmote(_arrowLeft))
|
.WithEmote(_arrowLeft)
|
||||||
.WithButton(new ButtonBuilder()
|
.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)
|
.WithStyle(ButtonStyle.Primary)
|
||||||
.WithCustomId(BUTTON_RIGHT)
|
.WithCustomId(BUTTON_RIGHT)
|
||||||
.WithDisabled(lastPage == 0)
|
.WithDisabled(lastPage == 0 || currentPage >= lastPage)
|
||||||
.WithEmote(_arrowRight))
|
.WithEmote(_arrowRight));
|
||||||
.Build();
|
|
||||||
|
|
||||||
|
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
|
||||||
{
|
{
|
||||||
@@ -236,9 +282,6 @@ public static class MessageChannelExtensions
|
|||||||
if (smc.Message.Id != msg.Id)
|
if (smc.Message.Id != msg.Id)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
if (smc.Data.CustomId != BUTTON_LEFT && smc.Data.CustomId != BUTTON_RIGHT)
|
|
||||||
return;
|
|
||||||
|
|
||||||
await si.DeferAsync();
|
await si.DeferAsync();
|
||||||
if (smc.User.Id != ctx.User.Id)
|
if (smc.User.Id != ctx.User.Id)
|
||||||
return;
|
return;
|
||||||
@@ -248,31 +291,27 @@ public static class MessageChannelExtensions
|
|||||||
if (currentPage == 0)
|
if (currentPage == 0)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
var toSend = await pageFunc(--currentPage);
|
--currentPage;
|
||||||
if (addPaginatedFooter)
|
_ = UpdatePageAsync(smc);
|
||||||
toSend.AddPaginatedFooter(currentPage, lastPage);
|
|
||||||
|
|
||||||
await smc.ModifyOriginalResponseAsync(x => x.Embed = toSend.Build());
|
|
||||||
}
|
}
|
||||||
else if (smc.Data.CustomId == BUTTON_RIGHT)
|
else if (smc.Data.CustomId == BUTTON_RIGHT)
|
||||||
{
|
{
|
||||||
if (lastPage > currentPage)
|
if (currentPage >= lastPage)
|
||||||
{
|
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 (maybeInter is { } inter && inter.Button.CustomId == smc.Data.CustomId)
|
||||||
|
{
|
||||||
|
await inter.TriggerAsync(smc);
|
||||||
|
_ = UpdatePageAsync(smc);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
Log.Error(ex, "Error in pagination: {ErrorMessage}", ex.Message);
|
Log.Error(ex, "Error in pagination: {ErrorMessage}", ex.Message);
|
||||||
}
|
}
|
||||||
});
|
|
||||||
|
|
||||||
return Task.CompletedTask;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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