A powerful and extendable Discord bot, with it's own module system :3 thevoid.cafe/projects/voidy

✨ Add Minecraft Shop generation for currency system

Changed files
+198 -5
packages
api
routes
bot
src
modules
economy
commands
minecraft
schemas
+1 -1
packages/api/routes/api/v1/currency.ts
··· 1 1 import { Hono } from "hono"; 2 - import { UserCurrency, UserCurrencyType, UserIntegration } from "@voidy/bot/db" 2 + import { UserCurrency, UserCurrencyType, UserIntegration } from "@voidy/bot/db"; 3 3 import { isAuthenticated } from "../../../middlewares/isAuthenticated"; 4 4 5 5 export const currency = new Hono();
+2
packages/api/routes/api/v1/index.ts
··· 1 1 import { Hono } from "hono"; 2 2 import { currency } from "./currency"; 3 + import { shop } from "./shop"; 3 4 4 5 export const v1 = new Hono(); 5 6 6 7 v1.route("/currency", currency); 8 + v1.route("/shop", shop);
+80
packages/api/routes/api/v1/shop.ts
··· 1 + import { Hono } from "hono"; 2 + import { MinecraftShopItem, MinecraftShop } from "@voidy/bot/db"; 3 + import { isAuthenticated } from "../../../middlewares/isAuthenticated"; 4 + 5 + export const shop = new Hono(); 6 + 7 + shop.get("/", isAuthenticated, async (c) => { 8 + const latestShop = await MinecraftShop.findOne().sort({ createdAt: -1 }); 9 + 10 + if (!latestShop) { 11 + return c.json({ error: "No shop found" }, 404); 12 + } 13 + 14 + return c.json(latestShop); 15 + }); 16 + 17 + shop.post("/generate", isAuthenticated, async (c) => { 18 + const body = await c.req.json().catch(() => ({})); 19 + const { highlight } = body as { highlight?: string }; 20 + 21 + const allItems = await MinecraftShopItem.find(); 22 + 23 + if (!allItems.length) { 24 + return c.json( 25 + { error: "No shop items available to generate from" }, 26 + 400, 27 + ); 28 + } 29 + 30 + // Config 31 + const MIN_ITEMS = 4; 32 + const MAX_ITEMS = 8; 33 + 34 + const selected: typeof allItems = []; 35 + 36 + // --- Force highlighted item --- 37 + if (highlight) { 38 + const highlightedItem = allItems.find( 39 + (item) => item.item === highlight, 40 + ); 41 + 42 + if (!highlightedItem) { 43 + return c.json( 44 + { error: `Highlighted item '${highlight}' not found` }, 45 + 400, 46 + ); 47 + } 48 + 49 + selected.push(highlightedItem); 50 + } 51 + 52 + // --- Random fill --- 53 + const pool = allItems.filter( 54 + (item) => !selected.some((s) => s.id === item.id), 55 + ); 56 + 57 + const targetCount = 58 + Math.floor(Math.random() * (MAX_ITEMS - MIN_ITEMS + 1)) + MIN_ITEMS; 59 + 60 + while (selected.length < targetCount && pool.length > 0) { 61 + const index = Math.floor(Math.random() * pool.length); 62 + selected.push(pool.splice(index, 1)[0]); 63 + } 64 + 65 + // --- Snapshot normalization --- 66 + const items = selected.map((item) => ({ 67 + label: item.label, 68 + icon: item.icon, 69 + item: item.item, 70 + price: item.price, 71 + defaultOptions: { 72 + quantity: item.defaultOptions.quantity, 73 + stackLimit: item.defaultOptions.stackLimit, 74 + }, 75 + })); 76 + 77 + const newShop = await MinecraftShop.create({ items }); 78 + 79 + return c.json(newShop, 201); 80 + });
+5 -4
packages/bot/.env.example
··· 1 - BOT_TOKEN=#Get this from https://discord.com/developers - REQUIRED 2 - BOT_CLIENT_ID=#Get this from https://discord.com/developers - REQUIRED 3 - BOT_ADMINS=#Discord Ids of bot administrators, comma separated 4 - DB_URI=#Your MongoDB connection URI - REQUIRED 1 + BOT_TOKEN: #Get this from https://discord.com/developers - REQUIRED 2 + BOT_CLIENT_ID: #Get this from https://discord.com/developers - REQUIRED 3 + BOT_ADMINS: #Discord Ids of bot administrators, comma separated 4 + DB_URI: #Your MongoDB connection URI - REQUIRED 5 + VOIDY_API_TOKEN: # 123456789
+62
packages/bot/src/modules/economy/commands/minecraft/shop/generate.ts
··· 1 + import { MessageFlags, SlashCommandSubcommandBuilder } from "discord.js"; 2 + import type { Command } from "@voidy/framework"; 3 + 4 + export default { 5 + id: "minecraft.shop.generate", 6 + devOnly: true, 7 + data: new SlashCommandSubcommandBuilder() 8 + .setName("generate") 9 + .setDescription("Generate a new shop for the minecraft currency integration.") 10 + .addStringOption((option) => 11 + option 12 + .setName("highlight") 13 + .setDescription("Force pick an item to highlight.") 14 + ), 15 + 16 + execute: async (interaction, _client) => { 17 + await interaction.deferReply({ flags: [MessageFlags.Ephemeral] }); 18 + 19 + const highlight = interaction.options.getString("highlight"); 20 + 21 + try { 22 + const res = await fetch( 23 + "https://voidy.thevoid.cafe/api/v1/shop/generate", 24 + { 25 + method: "POST", 26 + headers: { 27 + "Content-Type": "application/json", 28 + Authorization: `Bearer ${process.env.VOIDY_API_TOKEN}`, 29 + }, 30 + body: highlight 31 + ? JSON.stringify({ highlight }) 32 + : undefined, 33 + }, 34 + ); 35 + 36 + if (!res.ok) { 37 + const text = await res.text(); 38 + throw new Error(`API error (${res.status}): ${text}`); 39 + } 40 + 41 + if (!highlight) { 42 + await interaction.followUp({ 43 + content: "Generated new shop for the Minecraft currency integration.", 44 + flags: [MessageFlags.Ephemeral], 45 + }); 46 + return; 47 + } 48 + 49 + await interaction.followUp({ 50 + content: `Generated new shop for the Minecraft currency integration, with highlighted item: \`${highlight}\``, 51 + flags: [MessageFlags.Ephemeral], 52 + }); 53 + } catch (err) { 54 + console.error("Failed to generate shop:", err); 55 + 56 + await interaction.followUp({ 57 + content: "❌ Failed to generate a new shop. Check logs for details.", 58 + flags: [MessageFlags.Ephemeral], 59 + }); 60 + } 61 + }, 62 + } as Command;
+28
packages/bot/src/modules/economy/schemas/MinecraftShop.ts
··· 1 + import { Schema, model } from "mongoose"; 2 + 3 + export const MinecraftShop = model( 4 + "MinecraftShop", 5 + new Schema({ 6 + createdAt: { 7 + type: Date, 8 + required: true, 9 + default: Date.now, 10 + }, 11 + items: { 12 + type: Array, 13 + required: true, 14 + default: [ 15 + { 16 + label: "Bread", 17 + icon: "textures/items/bread", 18 + item: "minecraft:bread", 19 + price: 12, 20 + defaultOptions: { 21 + quantity: 16, 22 + stackLimit: 64, 23 + }, 24 + }, 25 + ] 26 + } 27 + }), 28 + );
+18
packages/bot/src/modules/economy/schemas/MinecraftShopItem.ts
··· 1 + import { Schema, model } from "mongoose"; 2 + 3 + export const MinecraftShopItem = model( 4 + "MinecraftShopItem", 5 + new Schema({ 6 + label: { type: String, required: true }, 7 + icon: { type: String, required: true }, 8 + item: { type: String, required: true }, 9 + price: { type: Number, required: true }, 10 + defaultOptions: { 11 + type: { 12 + quantity: { type: Number, required: true, default: 16 }, 13 + stackLimit: { type: Number, required: true, default: 64 }, 14 + }, 15 + required: true 16 + }, 17 + }), 18 + );
+2
packages/bot/src/modules/economy/schemas/index.ts
··· 1 1 export * from "./UserCurrency"; 2 + export * from "./MinecraftShop"; 3 + export * from "./MinecraftShopItem";