diff --git a/.gitignore b/.gitignore index 0fc94da..3db8cf7 100644 --- a/.gitignore +++ b/.gitignore @@ -4,4 +4,4 @@ flopobot.db flopobot.db-shm flopobot.db-wal .idea -flopobot_bc.db +*.db \ No newline at end of file diff --git a/flopobot.db b/flopobot.db deleted file mode 100644 index fa11a41..0000000 Binary files a/flopobot.db and /dev/null differ diff --git a/src/bot/commands/valorant.js b/src/bot/commands/valorant.js index 748b4e5..de25bef 100644 --- a/src/bot/commands/valorant.js +++ b/src/bot/commands/valorant.js @@ -1,7 +1,5 @@ -import { InteractionResponseType, InteractionResponseFlags } from "discord-interactions"; -import { EmbedBuilder, ActionRowBuilder, ButtonBuilder, ButtonStyle } from "discord.js"; - -import { postAPOBuy } from "../../utils/index.js"; +import { InteractionResponseFlags, InteractionResponseType } from "discord-interactions"; +import { ActionRowBuilder, ButtonBuilder, ButtonStyle, EmbedBuilder } from "discord.js"; import { DiscordRequest } from "../../api/discord.js"; import { getAllAvailableSkins, getUser, insertLog, updateSkin, updateUserCoins } from "../../database/index.js"; import { skins } from "../../game/state.js"; @@ -14,6 +12,14 @@ import { skins } from "../../game/state.js"; * @param {object} client - The Discord.js client instance. */ export async function handleValorantCommand(req, res, client) { + return res.send({ + type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE, + data: { + content: `L'ouverture de caisses Valorant en commande discord est désactivée. Tu peux aller en ouvrir sur FlopoSite.`, + flags: InteractionResponseFlags.EPHEMERAL, + }, + }); + const { member, token } = req.body; const userId = member.user.id; const valoPrice = parseInt(process.env.VALO_PRICE, 10) || 500; @@ -125,8 +131,7 @@ export async function handleValorantCommand(req, res, client) { await DiscordRequest(webhookEndpoint, { method: "PATCH", body: { - content: - "Oups, il y a eu un petit problème lors de l'ouverture de la caisse. L'administrateur a été notifié.", + content: "Oups, il y a eu un petit problème lors de l'ouverture de la caisse.", embeds: [], }, }); diff --git a/src/bot/events.js b/src/bot/events.js index 2ca60cc..e08e02e 100644 --- a/src/bot/events.js +++ b/src/bot/events.js @@ -1,5 +1,5 @@ import { handleMessageCreate } from "./handlers/messageCreate.js"; -import { getAkhys, setupCronJobs } from "../utils/index.js"; +import { getAkhys } from "../utils/index.js"; /** * Initializes and attaches all necessary event listeners to the Discord client. @@ -17,7 +17,7 @@ export function initializeEvents(client, io) { console.log("[Startup] Bot is ready, performing initial data sync..."); await getAkhys(client); console.log("[Startup] Setting up scheduled tasks..."); - setupCronJobs(client, io); + //setupCronJobs(client, io); console.log("--- FlopoBOT is fully operational ---"); }); diff --git a/src/database/index.js b/src/database/index.js index 0979c10..6dac74b 100644 --- a/src/database/index.js +++ b/src/database/index.js @@ -8,110 +8,266 @@ export const flopoDB = new Database("flopobot.db"); flopoDB.exec(` CREATE TABLE IF NOT EXISTS users ( - id TEXT PRIMARY KEY, - username TEXT NOT NULL, - globalName TEXT, - warned BOOLEAN DEFAULT 0, - warns INTEGER DEFAULT 0, - allTimeWarns INTEGER DEFAULT 0, - totalRequests INTEGER DEFAULT 0, - coins INTEGER DEFAULT 0, - dailyQueried BOOLEAN DEFAULT 0, - avatarUrl TEXT DEFAULT NULL, - isAkhy BOOLEAN DEFAULT 0 + id + TEXT + PRIMARY + KEY, + username + TEXT + NOT + NULL, + globalName + TEXT, + warned + BOOLEAN + DEFAULT + 0, + warns + INTEGER + DEFAULT + 0, + allTimeWarns + INTEGER + DEFAULT + 0, + totalRequests + INTEGER + DEFAULT + 0, + coins + INTEGER + DEFAULT + 0, + dailyQueried + BOOLEAN + DEFAULT + 0, + avatarUrl + TEXT + DEFAULT + NULL, + isAkhy + BOOLEAN + DEFAULT + 0 ); CREATE TABLE IF NOT EXISTS skins ( - uuid TEXT PRIMARY KEY, - displayName TEXT, - contentTierUuid TEXT, - displayIcon TEXT, - user_id TEXT REFERENCES users, - tierRank TEXT, - tierColor TEXT, - tierText TEXT, - basePrice TEXT, - currentLvl INTEGER DEFAULT NULL, - currentChroma INTEGER DEFAULT NULL, - currentPrice INTEGER DEFAULT NULL, - maxPrice INTEGER DEFAULT NULL + uuid + TEXT + PRIMARY + KEY, + displayName + TEXT, + contentTierUuid + TEXT, + displayIcon + TEXT, + user_id + TEXT + REFERENCES + users, + tierRank + TEXT, + tierColor + TEXT, + tierText + TEXT, + basePrice + TEXT, + currentLvl + INTEGER + DEFAULT + NULL, + currentChroma + INTEGER + DEFAULT + NULL, + currentPrice + INTEGER + DEFAULT + NULL, + maxPrice + INTEGER + DEFAULT + NULL ); CREATE TABLE IF NOT EXISTS market_offers ( - id PRIMARY KEY, - skin_uuid TEXT REFERENCES skins, - seller_id TEXT REFERENCES users, - starting_price INTEGER NOT NULL, - buyout_price INTEGER DEFAULT NULL, - final_price INTEGER DEFAULT NULL, - status TEXT NOT NULL, - posted_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - opening_at TIMESTAMP NOT NULL, - closing_at TIMESTAMP NOT NULL, - buyer_id TEXT REFERENCES users DEFAULT NULL + id + PRIMARY + KEY, + skin_uuid + TEXT + REFERENCES + skins, + seller_id + TEXT + REFERENCES + users, + starting_price + INTEGER + NOT + NULL, + buyout_price + INTEGER + DEFAULT + NULL, + final_price + INTEGER + DEFAULT + NULL, + status + TEXT + NOT + NULL, + posted_at + TIMESTAMP + DEFAULT + CURRENT_TIMESTAMP, + opening_at + TIMESTAMP + NOT + NULL, + closing_at + TIMESTAMP + NOT + NULL, + buyer_id + TEXT + REFERENCES + users + DEFAULT + NULL ); CREATE TABLE IF NOT EXISTS bids ( - id PRIMARY KEY, - bidder_id TEXT REFERENCES users, - market_offer_id REFERENCES market_offers, - offer_amount INTEGER, - offered_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP + id + PRIMARY + KEY, + bidder_id + TEXT + REFERENCES + users, + market_offer_id + REFERENCES + market_offers, + offer_amount + INTEGER, + offered_at + TIMESTAMP + DEFAULT + CURRENT_TIMESTAMP ); CREATE TABLE IF NOT EXISTS logs ( - id PRIMARY KEY, - user_id TEXT REFERENCES users, - action TEXT, - target_user_id TEXT REFERENCES users, - coins_amount INTEGER, - user_new_amount INTEGER, - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP + id + PRIMARY + KEY, + user_id + TEXT + REFERENCES + users, + action + TEXT, + target_user_id + TEXT + REFERENCES + users, + coins_amount + INTEGER, + user_new_amount + INTEGER, + created_at + TIMESTAMP + DEFAULT + CURRENT_TIMESTAMP ); CREATE TABLE IF NOT EXISTS games ( - id PRIMARY KEY, - p1 TEXT REFERENCES users, - p2 TEXT REFERENCES users, - p1_score INTEGER, - p2_score INTEGER, - p1_elo INTEGER, - p2_elo INTEGER, - p1_new_elo INTEGER, - p2_new_elo INTEGER, - type TEXT, - timestamp TIMESTAMP + id + PRIMARY + KEY, + p1 + TEXT + REFERENCES + users, + p2 + TEXT + REFERENCES + users, + p1_score + INTEGER, + p2_score + INTEGER, + p1_elo + INTEGER, + p2_elo + INTEGER, + p1_new_elo + INTEGER, + p2_new_elo + INTEGER, + type + TEXT, + timestamp + TIMESTAMP ); CREATE TABLE IF NOT EXISTS elos ( - id PRIMARY KEY REFERENCES users, - elo INTEGER + id + PRIMARY + KEY + REFERENCES + users, + elo + INTEGER ); CREATE TABLE IF NOT EXISTS sotd ( - id INT PRIMARY KEY, - tableauPiles TEXT, - foundationPiles TEXT, - stockPile TEXT, - wastePile TEXT, - isDone BOOLEAN DEFAULT false, - seed TEXT + id + INT + PRIMARY + KEY, + tableauPiles + TEXT, + foundationPiles + TEXT, + stockPile + TEXT, + wastePile + TEXT, + isDone + BOOLEAN + DEFAULT + false, + seed + TEXT ); CREATE TABLE IF NOT EXISTS sotd_stats ( - id TEXT PRIMARY KEY, - user_id TEXT REFERENCES users, - time INTEGER, - moves INTEGER, - score INTEGER + id + TEXT + PRIMARY + KEY, + user_id + TEXT + REFERENCES + users, + time + INTEGER, + moves + INTEGER, + score + INTEGER ); `); @@ -122,17 +278,48 @@ flopoDB.exec(` export const stmtUsers = flopoDB.prepare(` CREATE TABLE IF NOT EXISTS users ( - id TEXT PRIMARY KEY, - username TEXT NOT NULL, - globalName TEXT, - warned BOOLEAN DEFAULT 0, - warns INTEGER DEFAULT 0, - allTimeWarns INTEGER DEFAULT 0, - totalRequests INTEGER DEFAULT 0, - coins INTEGER DEFAULT 0, - dailyQueried BOOLEAN DEFAULT 0, - avatarUrl TEXT DEFAULT NULL, - isAkhy BOOLEAN DEFAULT 0 + id + TEXT + PRIMARY + KEY, + username + TEXT + NOT + NULL, + globalName + TEXT, + warned + BOOLEAN + DEFAULT + 0, + warns + INTEGER + DEFAULT + 0, + allTimeWarns + INTEGER + DEFAULT + 0, + totalRequests + INTEGER + DEFAULT + 0, + coins + INTEGER + DEFAULT + 0, + dailyQueried + BOOLEAN + DEFAULT + 0, + avatarUrl + TEXT + DEFAULT + NULL, + isAkhy + BOOLEAN + DEFAULT + 0 ) `); stmtUsers.run(); @@ -140,19 +327,44 @@ stmtUsers.run(); export const stmtSkins = flopoDB.prepare(` CREATE TABLE IF NOT EXISTS skins ( - uuid TEXT PRIMARY KEY, - displayName TEXT, - contentTierUuid TEXT, - displayIcon TEXT, - user_id TEXT REFERENCES users, - tierRank TEXT, - tierColor TEXT, - tierText TEXT, - basePrice TEXT, - currentLvl INTEGER DEFAULT NULL, - currentChroma INTEGER DEFAULT NULL, - currentPrice INTEGER DEFAULT NULL, - maxPrice INTEGER DEFAULT NULL + uuid + TEXT + PRIMARY + KEY, + displayName + TEXT, + contentTierUuid + TEXT, + displayIcon + TEXT, + user_id + TEXT + REFERENCES + users, + tierRank + TEXT, + tierColor + TEXT, + tierText + TEXT, + basePrice + TEXT, + currentLvl + INTEGER + DEFAULT + NULL, + currentChroma + INTEGER + DEFAULT + NULL, + currentPrice + INTEGER + DEFAULT + NULL, + maxPrice + INTEGER + DEFAULT + NULL ) `); stmtSkins.run(); @@ -160,17 +372,51 @@ stmtSkins.run(); export const stmtMarketOffers = flopoDB.prepare(` CREATE TABLE IF NOT EXISTS market_offers ( - id PRIMARY KEY, - skin_uuid TEXT REFERENCES skins, - seller_id TEXT REFERENCES users, - starting_price INTEGER NOT NULL, - buyout_price INTEGER DEFAULT NULL, - final_price INTEGER DEFAULT NULL, - status TEXT NOT NULL, - posted_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - opening_at TIMESTAMP NOT NULL, - closing_at TIMESTAMP NOT NULL, - buyer_id TEXT REFERENCES users DEFAULT NULL + id + PRIMARY + KEY, + skin_uuid + TEXT + REFERENCES + skins, + seller_id + TEXT + REFERENCES + users, + starting_price + INTEGER + NOT + NULL, + buyout_price + INTEGER + DEFAULT + NULL, + final_price + INTEGER + DEFAULT + NULL, + status + TEXT + NOT + NULL, + posted_at + TIMESTAMP + DEFAULT + CURRENT_TIMESTAMP, + opening_at + TIMESTAMP + NOT + NULL, + closing_at + TIMESTAMP + NOT + NULL, + buyer_id + TEXT + REFERENCES + users + DEFAULT + NULL ) `); stmtMarketOffers.run(); @@ -178,11 +424,22 @@ stmtMarketOffers.run(); export const stmtBids = flopoDB.prepare(` CREATE TABLE IF NOT EXISTS bids ( - id PRIMARY KEY, - bidder_id TEXT REFERENCES users, - market_offer_id REFERENCES market_offers, - offer_amount INTEGER, - offered_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP + id + PRIMARY + KEY, + bidder_id + TEXT + REFERENCES + users, + market_offer_id + REFERENCES + market_offers, + offer_amount + INTEGER, + offered_at + TIMESTAMP + DEFAULT + CURRENT_TIMESTAMP ) `); stmtBids.run(); @@ -190,13 +447,27 @@ stmtBids.run(); export const stmtLogs = flopoDB.prepare(` CREATE TABLE IF NOT EXISTS logs ( - id PRIMARY KEY, - user_id TEXT REFERENCES users, - action TEXT, - target_user_id TEXT REFERENCES users, - coins_amount INTEGER, - user_new_amount INTEGER, - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP + id + PRIMARY + KEY, + user_id + TEXT + REFERENCES + users, + action + TEXT, + target_user_id + TEXT + REFERENCES + users, + coins_amount + INTEGER, + user_new_amount + INTEGER, + created_at + TIMESTAMP + DEFAULT + CURRENT_TIMESTAMP ) `); stmtLogs.run(); @@ -204,17 +475,33 @@ stmtLogs.run(); export const stmtGames = flopoDB.prepare(` CREATE TABLE IF NOT EXISTS games ( - id PRIMARY KEY, - p1 TEXT REFERENCES users, - p2 TEXT REFERENCES users, - p1_score INTEGER, - p2_score INTEGER, - p1_elo INTEGER, - p2_elo INTEGER, - p1_new_elo INTEGER, - p2_new_elo INTEGER, - type TEXT, - timestamp TIMESTAMP + id + PRIMARY + KEY, + p1 + TEXT + REFERENCES + users, + p2 + TEXT + REFERENCES + users, + p1_score + INTEGER, + p2_score + INTEGER, + p1_elo + INTEGER, + p2_elo + INTEGER, + p1_new_elo + INTEGER, + p2_new_elo + INTEGER, + type + TEXT, + timestamp + TIMESTAMP ) `); stmtGames.run(); @@ -222,8 +509,13 @@ stmtGames.run(); export const stmtElos = flopoDB.prepare(` CREATE TABLE IF NOT EXISTS elos ( - id PRIMARY KEY REFERENCES users, - elo INTEGER + id + PRIMARY + KEY + REFERENCES + users, + elo + INTEGER ) `); stmtElos.run(); @@ -231,13 +523,24 @@ stmtElos.run(); export const stmtSOTD = flopoDB.prepare(` CREATE TABLE IF NOT EXISTS sotd ( - id INT PRIMARY KEY, - tableauPiles TEXT, - foundationPiles TEXT, - stockPile TEXT, - wastePile TEXT, - isDone BOOLEAN DEFAULT false, - seed TEXT + id + INT + PRIMARY + KEY, + tableauPiles + TEXT, + foundationPiles + TEXT, + stockPile + TEXT, + wastePile + TEXT, + isDone + BOOLEAN + DEFAULT + false, + seed + TEXT ) `); stmtSOTD.run(); @@ -245,11 +548,20 @@ stmtSOTD.run(); export const stmtSOTDStats = flopoDB.prepare(` CREATE TABLE IF NOT EXISTS sotd_stats ( - id TEXT PRIMARY KEY, - user_id TEXT REFERENCES users, - time INTEGER, - moves INTEGER, - score INTEGER + id + TEXT + PRIMARY + KEY, + user_id + TEXT + REFERENCES + users, + time + INTEGER, + moves + INTEGER, + score + INTEGER ) `); stmtSOTDStats.run(); @@ -291,9 +603,9 @@ export const getAllAkhys = flopoDB.prepare( ----------------------------*/ export const insertSkin = flopoDB.prepare( `INSERT INTO skins (uuid, displayName, contentTierUuid, displayIcon, user_id, tierRank, tierColor, tierText, - basePrice, currentLvl, currentChroma, currentPrice, maxPrice) + basePrice, maxPrice) VALUES (@uuid, @displayName, @contentTierUuid, @displayIcon, @user_id, @tierRank, @tierColor, @tierText, - @basePrice, @currentLvl, @currentChroma, @currentPrice, @maxPrice)`, + @basePrice, @maxPrice)`, ); export const updateSkin = flopoDB.prepare( `UPDATE skins @@ -371,6 +683,20 @@ export const insertMarketOffer = flopoDB.prepare(` VALUES (@id, @skin_uuid, @seller_id, @starting_price, @buyout_price, @status, @opening_at, @closing_at) `); +export const updateMarketOffer = flopoDB.prepare(` + UPDATE market_offers + SET final_price = @final_price, + status = @status, + buyer_id = @buyer_id + WHERE id = @id +`); + +export const deleteMarketOffer = flopoDB.prepare(` + DELETE + FROM market_offers + WHERE id = ? +`); + /* ------------------------- BIDS ----------------------------*/ @@ -401,6 +727,12 @@ export const insertBid = flopoDB.prepare(` VALUES (@id, @bidder_id, @market_offer_id, @offer_amount) `); +export const deleteBid = flopoDB.prepare(` + DELETE + FROM bids + WHERE id = ? +`); + /* ------------------------- BULK TRANSACTIONS (synchronous) ----------------------------*/ diff --git a/src/server/routes/api.js b/src/server/routes/api.js index 697c134..9b7e702 100644 --- a/src/server/routes/api.js +++ b/src/server/routes/api.js @@ -4,6 +4,7 @@ import { sleep } from "openai/core"; // --- Database Imports --- import { getAllAkhys, + getAllAvailableSkins, getAllUsers, getLogs, getMarketOffersBySkin, @@ -19,6 +20,7 @@ import { insertUser, pruneOldLogs, queryDailyReward, + updateSkin, updateUserCoins, } from "../../database/index.js"; @@ -32,6 +34,7 @@ import { DiscordRequest } from "../../api/discord.js"; // --- Discord.js Builder Imports --- import { ActionRowBuilder, ButtonBuilder, ButtonStyle, EmbedBuilder } from "discord.js"; import { emitDataUpdated, socketEmit } from "../socket.js"; +import { handleCaseOpening } from "../../utils/marketNotifs.js"; // Create a new router instance const router = express.Router(); @@ -114,6 +117,151 @@ export function apiRoutes(client, io) { } }); + router.post("/open-case", async (req, res) => { + const { userId, caseType } = req.body; + + let caseTypeVal, tierWeights; + switch (caseType) { + case "standard": + caseTypeVal = 1; + tierWeights = { + "12683d76-48d7-84a3-4e09-6985794f0445": 50, // Select + "0cebb8be-46d7-c12a-d306-e9907bfc5a25": 30, // Deluxe + "60bca009-4182-7998-dee7-b8a2558dc369": 15, // Premium + "e046854e-406c-37f4-6607-19a9ba8426fc": 4, // Exclusive + "411e4a55-4e59-7757-41f0-86a53f101bb5": 1, // Ultra + }; + break; + case "premium": + caseTypeVal = 2; + tierWeights = { + "12683d76-48d7-84a3-4e09-6985794f0445": 35, // Select + "0cebb8be-46d7-c12a-d306-e9907bfc5a25": 30, // Deluxe + "60bca009-4182-7998-dee7-b8a2558dc369": 30, // Premium + "e046854e-406c-37f4-6607-19a9ba8426fc": 4, // Exclusive + "411e4a55-4e59-7757-41f0-86a53f101bb5": 1, // Ultra + }; + break; + case "ultra": + caseTypeVal = 4; + tierWeights = { + "12683d76-48d7-84a3-4e09-6985794f0445": 33, // Select + "0cebb8be-46d7-c12a-d306-e9907bfc5a25": 28, // Deluxe + "60bca009-4182-7998-dee7-b8a2558dc369": 28, // Premium + "e046854e-406c-37f4-6607-19a9ba8426fc": 8, // Exclusive + "411e4a55-4e59-7757-41f0-86a53f101bb5": 3, // Ultra + }; + break; + default: + return res.status(400).json({ error: "Invalid case type." }); + } + const commandUser = getUser.get(userId); + if (!commandUser) return res.status(404).json({ error: "User not found." }); + const valoPrice = (parseInt(process.env.VALO_PRICE, 10) || 500) * caseTypeVal; + if (commandUser.coins < valoPrice) return res.status(403).json({ error: "Not enough FlopoCoins." }); + + try { + const dbSkins = getAllAvailableSkins.all(); + const filteredSkins = skins.filter((s) => dbSkins.find((dbSkin) => dbSkin.uuid === s.uuid)); + filteredSkins.forEach((s) => { + let dbSkin = getSkin.get(s.uuid); + s.tierColor = dbSkin?.tierColor; + }); + filteredSkins.forEach((s) => { + s.weight = tierWeights[s.tierUuid] ?? 1; // fallback if missing + }); + + function weightedSample(arr, count) { + let totalWeight = arr.reduce((sum, x) => sum + x.weight, 0); + const list = [...arr]; + const result = []; + + for (let i = 0; i < count && list.length > 0; i++) { + let r = Math.random() * totalWeight; + let running = 0; + let pickIndex = -1; + + for (let j = 0; j < list.length; j++) { + running += list[j].weight; + if (r <= running) { + pickIndex = j; + break; + } + } + + if (pickIndex < 0) break; + + const picked = list.splice(pickIndex, 1)[0]; + result.push(picked); + + // Subtract removed weight + totalWeight -= picked.weight; + } + + return result; + } + + const selectedSkins = weightedSample(filteredSkins, 100); + + const randomSelectedSkinIndex = Math.floor(Math.random() * (selectedSkins.length - 1)); + const randomSelectedSkinUuid = selectedSkins[randomSelectedSkinIndex].uuid; + + const dbSkin = getSkin.get(randomSelectedSkinUuid); + const randomSkinData = skins.find((skin) => skin.uuid === dbSkin.uuid); + if (!randomSkinData) { + throw new Error(`Could not find skin data for UUID: ${dbSkin.uuid}`); + } + + // --- Randomize Level and Chroma --- + const randomLevel = Math.floor(Math.random() * randomSkinData.levels.length) + 1; + let randomChroma = 1; + if (randomLevel === randomSkinData.levels.length && randomSkinData.chromas.length > 1) { + // Ensure chroma is at least 1 and not greater than the number of chromas + randomChroma = Math.floor(Math.random() * randomSkinData.chromas.length) + 1; + } + + // --- Calculate Price --- + const calculatePrice = () => { + let result = parseFloat(dbSkin.basePrice); + result *= 1 + randomLevel / Math.max(randomSkinData.levels.length, 2); + result *= 1 + randomChroma / 4; + return parseFloat(result.toFixed(0)); + }; + const finalPrice = calculatePrice(); + + // --- Update Database --- + insertLog.run({ + id: `${userId}-${Date.now()}`, + user_id: userId, + action: "VALO_CASE_OPEN", + target_user_id: null, + coins_amount: -valoPrice, + user_new_amount: commandUser.coins - valoPrice, + }); + updateUserCoins.run({ + id: userId, + coins: commandUser.coins - valoPrice, + }); + updateSkin.run({ + uuid: randomSkinData.uuid, + user_id: userId, + currentLvl: randomLevel, + currentChroma: randomChroma, + currentPrice: finalPrice, + }); + + console.log( + `[${Date.now()}] ${userId} opened a ${caseType} Valorant case and received skin ${randomSelectedSkinUuid}`, + ); + const updatedSkin = getSkin.get(randomSkinData.uuid); + await handleCaseOpening(caseType, userId, randomSelectedSkinUuid, client); + res.json({ selectedSkins, randomSelectedSkinUuid, randomSelectedSkinIndex, updatedSkin }); + } catch (error) { + console.error("Error fetching skins:", error); + res.status(500).json({ error: "Failed to fetch skins." }); + } + }); + router.get("/skin/:id", (req, res) => { try { const skinData = skins.find((s) => s.uuid === req.params.id); @@ -168,6 +316,14 @@ export function apiRoutes(client, io) { }); // --- User-Specific Routes --- + router.get("/user/:id", async (req, res) => { + try { + const user = getUser.get(req.params.id); + res.json({ user }); + } catch (error) { + res.status(404).json({ error: "User not found." }); + } + }); router.get("/user/:id/avatar", async (req, res) => { try { @@ -188,6 +344,15 @@ export function apiRoutes(client, io) { } }); + router.get("/user/:id/coins", async (req, res) => { + try { + const user = getUser.get(req.params.id); + res.json({ coins: user.coins }); + } catch (error) { + res.status(404).json({ error: "User not found." }); + } + }); + router.get("/user/:id/sparkline", (req, res) => { try { const logs = getUserLogs.all({ user_id: req.params.id }); diff --git a/src/server/routes/market.js b/src/server/routes/market.js index 63eadc8..7326448 100644 --- a/src/server/routes/market.js +++ b/src/server/routes/market.js @@ -8,13 +8,17 @@ import { ButtonStyle } from "discord.js"; import { getMarketOfferById, getMarketOffers, + getMarketOffersBySkin, getOfferBids, getSkin, getUser, insertBid, insertLog, + insertMarketOffer, updateUserCoins, } from "../../database/index.js"; +import { emitMarketUpdate } from "../socket.js"; +import { handleNewMarketOffer, handleNewMarketOfferBid } from "../../utils/marketNotifs.js"; // Create a new router instance const router = express.Router(); @@ -67,12 +71,43 @@ export function marketRoutes(client, io) { }); router.post("/place-offer", async (req, res) => { + const { seller_id, skin_uuid, starting_price, delay, duration, timestamp } = req.body; + const now = Date.now(); try { - // Placeholder for placing an offer logic - // Extract data from req.body and process accordingly - res.status(200).send({ message: "Offer placed successfully" }); + const skin = getSkin.get(skin_uuid); + if (!skin) return res.status(404).send({ error: "Skin not found" }); + const seller = getUser.get(seller_id); + if (!seller) return res.status(404).send({ error: "Seller not found" }); + if (skin.user_id !== seller.id) return res.status(403).send({ error: "You do not own this skin" }); + + const existingOffers = getMarketOffersBySkin.all(skin.uuid); + if ( + existingOffers.length > 0 && + existingOffers.some((offer) => offer.status === "open" || offer.status === "pending") + ) { + return res.status(403).send({ error: "This skin already has an open or pending offer." }); + } + + const opening_at = now + delay; + const closing_at = opening_at + duration; + + const offerId = Date.now() + "-" + seller.id + "-" + skin.uuid; + insertMarketOffer.run({ + id: offerId, + skin_uuid: skin.uuid, + seller_id: seller.id, + starting_price: starting_price, + buyout_price: null, + status: delay > 0 ? "pending" : "open", + opening_at: opening_at, + closing_at: closing_at, + }); + await emitMarketUpdate(); + await handleNewMarketOffer(offerId, client); + res.status(200).send({ message: "Offre créée avec succès" }); } catch (e) { - res.status(500).send({ error: e }); + console.log(e); + return res.status(500).send({ error: e }); } }); @@ -92,11 +127,11 @@ export function marketRoutes(client, io) { if (lastBid?.bidder_id === buyer_id) return res.status(403).send({ error: "You are already the highest bidder" }); if (bid_amount < lastBid?.offer_amount + 10) { - return res.status(403).send({ message: "Bid amount is below minimum" }); + return res.status(403).send({ error: "Bid amount is below minimum" }); } } else { if (bid_amount < offer.starting_price + 10) { - return res.status(403).send({ message: "Bid amount is below minimum" }); + return res.status(403).send({ error: "Bid amount is below minimum" }); } } @@ -105,16 +140,15 @@ export function marketRoutes(client, io) { if (bidder.coins < bid_amount) return res.status(403).send({ error: "You do not have enough coins to place this bid" }); - // TODO: - // buyer must refunded on outbid - + const bidId = Date.now() + "-" + buyer_id + "-" + offer.id; insertBid.run({ + id: bidId, bidder_id: buyer_id, market_offer_id: offer.id, offer_amount: bid_amount, }); const newCoinsAmount = bidder.coins - bid_amount; - updateUserCoins.run({ buyer_id, coins: newCoinsAmount }); + updateUserCoins.run({ id: buyer_id, coins: newCoinsAmount }); insertLog.run({ id: `${buyer_id}-bid-${offer.id}-${Date.now()}`, user_id: buyer_id, @@ -124,7 +158,24 @@ export function marketRoutes(client, io) { user_new_amount: newCoinsAmount, }); - res.status(200).send({ message: "Bid placed successfully" }); + // Refund the previous highest bidder + if (lastBid) { + const previousBidder = getUser.get(lastBid.bidder_id); + const refundedCoinsAmount = previousBidder.coins + lastBid.offer_amount; + updateUserCoins.run({ id: previousBidder.id, coins: refundedCoinsAmount }); + insertLog.run({ + id: `${previousBidder.id}-bid-refund-${offer.id}-${Date.now()}`, + user_id: previousBidder.id, + action: "BID_REFUNDED", + target_user_id: null, + coins_amount: lastBid.offer_amount, + user_new_amount: refundedCoinsAmount, + }); + } + + await handleNewMarketOfferBid(offer.id, bidId, client); + await emitMarketUpdate(); + res.status(200).send({ error: "Bid placed successfully" }); } catch (e) { console.log(`[${Date.now()}]`, e); res.status(500).send({ error: e }); diff --git a/src/server/socket.js b/src/server/socket.js index 5d3d476..1e3b6ae 100644 --- a/src/server/socket.js +++ b/src/server/socket.js @@ -459,3 +459,5 @@ export const emitUpdate = (type, room) => io.emit("blackjack:update", { type, ro export const emitToast = (payload) => io.emit("blackjack:toast", payload); export const emitSolitaireUpdate = (userId, moves) => io.emit("solitaire:update", { userId, moves }); + +export const emitMarketUpdate = () => io.emit("market:update"); diff --git a/src/utils/index.js b/src/utils/index.js index 429fa35..24841c8 100644 --- a/src/utils/index.js +++ b/src/utils/index.js @@ -6,14 +6,26 @@ import { getSkinTiers, getValorantSkins } from "../api/valorant.js"; import { DiscordRequest } from "../api/discord.js"; import { initTodaysSOTD } from "../game/points.js"; import { + deleteBid, + deleteMarketOffer, getAllAkhys, getAllUsers, + getMarketOffers, + getOfferBids, + getSkin, + getUser, insertManySkins, insertUser, resetDailyReward, + updateMarketOffer, + updateSkin, updateUserAvatar, + updateUserCoins, } from "../database/index.js"; -import { activeInventories, activeSearchs, skins } from "../game/state.js"; +import { activeInventories, activePredis, activeSearchs, pokerRooms, skins } from "../game/state.js"; +import { emitMarketUpdate } from "../server/socket.js"; +import { handleMarketOfferClosing, handleMarketOfferOpening } from "./marketNotifs.js"; +import { client } from "../bot/client.js"; export async function InstallGlobalCommands(appId, commands) { // API endpoint to overwrite global commands @@ -111,6 +123,11 @@ export async function getAkhys(client) { * @param {object} io - The Socket.IO server instance. */ export function setupCronJobs(client, io) { + // Every 5 minutes: Update market offers + cron.schedule("* * * * *", () => { + handleMarketOffersUpdate(); + }); + // Every 10 minutes: Clean up expired interactive sessions cron.schedule("*/10 * * * *", () => { const now = Date.now(); @@ -130,9 +147,28 @@ export function setupCronJobs(client, io) { cleanup(activeInventories, "inventory"); cleanup(activeSearchs, "search"); + for (const id in pokerRooms) { + if (pokerRooms[id].last_move_at !== null) { + if (now >= pokerRooms[id].last_move_at + FIVE_MINUTES * 3) { + delete pokerRooms[id]; + console.log(`[Cron] Cleaned up expired poker room ID: ${id}`); + } + } else { + if (now >= pokerRooms[id].created_at + FIVE_MINUTES * 6) { + delete pokerRooms[id]; + console.log(`[Cron] Cleaned up expired poker room ID: ${id}`); + } + } + } - // TODO: Cleanup for predis and poker rooms... - // ... + let cleanedCount = 0; + for (const id in activePredis) { + if (now >= (activePredis[id].endTime || 0)) { + delete activePredis[id]; + cleanedCount++; + } + } + if (cleanedCount > 0) console.log(`[Cron] Cleaned up ${cleanedCount} expired predictions.`); }); // Daily at midnight: Reset daily rewards and init SOTD @@ -147,6 +183,23 @@ export function setupCronJobs(client, io) { } catch (e) { console.error("[Cron] Error during daily reset:", e); } + try { + const offers = getMarketOffers.all(); + const now = Date.now(); + const TWO_DAYS = 2 * 24 * 60 * 60 * 1000; + for (const offer of offers) { + if (now >= offer.closing_at + TWO_DAYS) { + const offerBids = getOfferBids.all(offer.id); + for (const bid of offerBids) { + deleteBid.run(bid.id); + } + deleteMarketOffer.run(offer.id); + console.log(`[Cron] Deleted expired market offer ID: ${offer.id}`); + } + } + } catch (e) { + console.error("[Cron] Error during Market Offers clean up:", e); + } }); // Daily at 7 AM: Re-sync users and skins @@ -223,6 +276,61 @@ export async function postAPOBuy(userId, amount) { // --- Miscellaneous Helpers --- +function handleMarketOffersUpdate() { + const now = Date.now(); + const offers = getMarketOffers.all(); + offers.forEach(async (offer) => { + if (now >= offer.opening_at && offer.status === "pending") { + updateMarketOffer.run({ id: offer.id, final_price: null, buyer_id: null, status: "open" }); + await handleMarketOfferOpening(offer.id, client); + await emitMarketUpdate(); + } + if (now >= offer.closing_at && offer.status !== "closed") { + const bids = getOfferBids.all(offer.id); + + if (bids.length === 0) { + // No bids placed, mark as closed without a sale + updateMarketOffer.run({ + id: offer.id, + buyer_id: null, + final_price: null, + status: "closed", + }); + await emitMarketUpdate(); + } else { + const lastBid = bids[0]; + const seller = getUser.get(offer.seller_id); + const buyer = getUser.get(lastBid.bidder_id); + + try { + // Change skin ownership + const skin = getSkin.get(offer.skin_uuid); + if (!skin) throw new Error(`Skin not found for offer ID: ${offer.id}`); + updateSkin.run({ + user_id: buyer.id, + currentLvl: skin.currentLvl, + currentChroma: skin.currentChroma, + currentPrice: skin.currentPrice, + uuid: skin.uuid, + }); + updateMarketOffer.run({ + id: offer.id, + buyer_id: buyer.id, + final_price: lastBid.offer_amount, + status: "closed", + }); + const newUserCoins = seller.coins + lastBid.offer_amount; + updateUserCoins.run({ id: seller.id, coins: newUserCoins }); + await emitMarketUpdate(); + } catch (e) { + console.error(`[Market Cron] Error processing offer ID: ${offer.id}`, e); + } + } + await handleMarketOfferClosing(offer.id, client); + } + }); +} + export async function getOnlineUsersWithRole(guild, roleId) { if (!guild || !roleId) return new Map(); try { diff --git a/src/utils/marketNotifs.js b/src/utils/marketNotifs.js new file mode 100644 index 0000000..6f9baec --- /dev/null +++ b/src/utils/marketNotifs.js @@ -0,0 +1,401 @@ +import { getMarketOfferById, getOfferBids, getSkin, getUser } from "../database/index.js"; +import { EmbedBuilder } from "discord.js"; + +export async function handleNewMarketOffer(offerId, client) { + const offer = getMarketOfferById.get(offerId); + if (!offer) return; + const skin = getSkin.get(offer.skin_uuid); + + const discordUserSeller = await client.users.fetch(offer.seller_id); + try { + const userSeller = getUser.get(offer.seller_id); + if (discordUserSeller && userSeller?.isAkhy) { + const embed = new EmbedBuilder() + .setTitle("🔔 Offre créée") + .setDescription(`Ton offre pour le skin **${skin ? skin.displayName : offer.skin_uuid}** a bien été créée !`) + .setThumbnail(skin.displayIcon) + .setColor(0x5865f2) // Discord blurple + .addFields( + { + name: "📌 Statut", + value: `\`${offer.status}\``, + inline: true, + }, + { + name: "💰 Prix de départ", + value: `\`${offer.starting_price} coins\``, + inline: true, + }, + { + name: "⏰ Ouverture", + value: ``, + }, + { + name: "⏰ Fermeture", + value: ``, + }, + { + name: "🆔 ID de l’offre", + value: `\`${offer.id}\``, + inline: false, + }, + ) + .setTimestamp(); + + discordUserSeller.send({ embeds: [embed] }).catch(console.error); + } + } catch (e) { + console.error(e); + } + // Send notification in guild channel + + try { + const guildChannel = await client.channels.fetch(process.env.BOT_CHANNEL_ID); + const embed = new EmbedBuilder() + .setTitle("🔔 Nouvelle offre") + .setDescription(`Une offre pour le skin **${skin ? skin.displayName : offer.skin_uuid}** a été créée !`) + .setThumbnail(skin.displayIcon) + .setColor(0x5865f2) // Discord blurple + .addFields( + { + name: "💰 Prix de départ", + value: `\`${offer.starting_price} coins\``, + inline: true, + }, + { + name: "⏰ Ouverture", + value: ``, + }, + { + name: "⏰ Fermeture", + value: ``, + }, + { + name: "Créée par", + value: `<@${offer.seller_id}> ${discordUserSeller ? "(" + discordUserSeller.username + ")" : ""}`, + }, + ) + .setTimestamp(); + guildChannel.send({ embeds: [embed] }).catch(console.error); + } catch (e) { + console.error(e); + } +} + +export async function handleMarketOfferOpening(offerId, client) { + const offer = getMarketOfferById.get(offerId); + if (!offer) return; + const skin = getSkin.get(offer.skin_uuid); + + try { + const discordUserSeller = await client.users.fetch(offer.seller_id); + const userSeller = getUser.get(offer.seller_id); + if (discordUserSeller && userSeller?.isAkhy) { + const embed = new EmbedBuilder() + .setTitle("🔔 Début des enchères") + .setDescription( + `Les enchères sur ton offre pour le skin **${skin ? skin.displayName : offer.skin_uuid}** viennent de commencer !`, + ) + .setThumbnail(skin.displayIcon) + .setColor(0x5865f2) // Discord blurple + .addFields( + { + name: "📌 Statut", + value: `\`${offer.status}\``, + inline: true, + }, + { + name: "💰 Prix de départ", + value: `\`${offer.starting_price} coins\``, + inline: true, + }, + { + name: "⏰ Fermeture", + value: ``, + }, + { + name: "🆔 ID de l’offre", + value: `\`${offer.id}\``, + inline: false, + }, + ) + .setTimestamp(); + + discordUserSeller.send({ embeds: [embed] }).catch(console.error); + } + } catch (e) { + console.error(e); + } + // Send notification in guild channel + + try { + const guildChannel = await client.channels.fetch(process.env.BOT_CHANNEL_ID); + const embed = new EmbedBuilder() + .setTitle("🔔 Début des enchères") + .setDescription( + `Les enchères sur l'offre pour le skin **${skin ? skin.displayName : offer.skin_uuid}** viennent de commencer !`, + ) + .setThumbnail(skin.displayIcon) + .setColor(0x5865f2) // Discord blurple + .addFields( + { + name: "💰 Prix de départ", + value: `\`${offer.starting_price} coins\``, + inline: true, + }, + { + name: "⏰ Fermeture", + value: ``, + }, + ) + .setTimestamp(); + guildChannel.send({ embeds: [embed] }).catch(console.error); + } catch (e) { + console.error(e); + } +} + +export async function handleMarketOfferClosing(offerId, client) { + const offer = getMarketOfferById.get(offerId); + if (!offer) return; + const skin = getSkin.get(offer.skin_uuid); + const bids = getOfferBids.all(offer.id); + + const discordUserSeller = await client.users.fetch(offer.seller_id); + try { + const userSeller = getUser.get(offer.seller_id); + if (discordUserSeller && userSeller?.isAkhy) { + const embed = new EmbedBuilder() + .setTitle("🔔 Fin des enchères") + .setDescription( + `Les enchères sur ton offre pour le skin **${skin ? skin.displayName : offer.skin_uuid}** viennent de se terminer !`, + ) + .setThumbnail(skin.displayIcon) + .setColor(0x5865f2) // Discord blurple + .setTimestamp(); + + if (bids.length === 0) { + embed.addFields( + { + name: "❌ Aucune enchère n'a été placée sur cette offre.", + value: "Tu conserves ce skin dans ton inventaire.", + }, + { + name: "🆔 ID de l’offre", + value: `\`${offer.id}\``, + inline: false, + }, + ); + } else { + const highestBid = bids[0]; + const highestBidderUser = await client.users.fetch(highestBid.bidder_id); + embed.addFields( + { + name: "✅ Enchères terminées avec succès !", + value: `Ton skin a été vendu pour \`${highestBid.offer_amount} coins\` à <@${highestBid.bidder_id}> ${highestBidderUser ? "(" + highestBidderUser.username + ")" : ""}.`, + }, + { + name: "🆔 ID de l’offre", + value: `\`${offer.id}\``, + inline: false, + }, + ); + } + + discordUserSeller.send({ embeds: [embed] }).catch(console.error); + } + } catch (e) { + console.error(e); + } + + // Send notification in guild channel + + try { + const guildChannel = await client.channels.fetch(process.env.BOT_CHANNEL_ID); + const embed = new EmbedBuilder() + .setTitle("🔔 Fin des enchères") + .setDescription( + `Les enchères sur l'offre pour le skin **${skin ? skin.displayName : offer.skin_uuid}** viennent de se terminer !`, + ) + .setThumbnail(skin.displayIcon) + .setColor(0x5865f2) // Discord blurple + .setTimestamp(); + + if (bids.length === 0) { + embed.addFields({ + name: "❌ Aucune enchère n'a été placée sur cette offre.", + value: "", + }); + } else { + const highestBid = bids[0]; + const highestBidderUser = await client.users.fetch(highestBid.bidder_id); + embed.addFields({ + name: "✅ Enchères terminées avec succès !", + value: `Le skin de <@${offer.seller_id}> ${discordUserSeller ? "(" + discordUserSeller.username + ")" : ""} a été vendu pour \`${highestBid.offer_amount} coins\` à <@${highestBid.bidder_id}> ${highestBidderUser ? "(" + highestBidderUser.username + ")" : ""}.`, + }); + const discordUserBidder = await client.users.fetch(highestBid.bidder_id); + const userBidder = getUser.get(highestBid.bidder_id); + if (discordUserBidder && userBidder?.isAkhy) { + const embed = new EmbedBuilder() + .setTitle("🔔 Fin des enchères") + .setDescription( + `Les enchères sur l'offre pour le skin **${skin ? skin.displayName : offer.skin_uuid}** viennent de se terminer !`, + ) + .setThumbnail(skin.displayIcon) + .setColor(0x5865f2) // Discord blurple + .setTimestamp(); + const highestBid = bids[0]; + embed.addFields({ + name: "✅ Enchères terminées avec succès !", + value: `Tu as acheté ce skin pour \`${highestBid.offer_amount} coins\` à <@${offer.seller_id}> ${discordUserSeller ? "(" + discordUserSeller.username + ")" : ""}. Il a été ajouté à ton inventaire.`, + }); + + discordUserBidder.send({ embeds: [embed] }).catch(console.error); + } + } + guildChannel.send({ embeds: [embed] }).catch(console.error); + } catch (e) { + console.error(e); + } +} + +export async function handleNewMarketOfferBid(offerId, bidId, client) { + // Notify Seller and Bidder + const offer = getMarketOfferById.get(offerId); + if (!offer) return; + const bid = getOfferBids.get(offerId); + if (!bid) return; + const skin = getSkin.get(offer.skin_uuid); + + const bidderUser = client.users.fetch(bid.bidder_id); + try { + const discordUserSeller = await client.users.fetch(offer.seller_id); + const userSeller = getUser.get(offer.seller_id); + + if (discordUserSeller && userSeller?.isAkhy) { + const embed = new EmbedBuilder() + .setTitle("🔔 Nouvelle enchère") + .setDescription( + `Il y a eu une nouvelle enchère sur ton offre pour le skin **${skin ? skin.displayName : offer.skin_uuid}**.`, + ) + .setThumbnail(skin.displayIcon) + .setColor(0x5865f2) // Discord blurple + .addFields( + { + name: "👤 Enchérisseur", + value: `<@${bid.bidder_id}> ${bidderUser ? "(" + bidderUser.username + ")" : ""}`, + inline: true, + }, + { + name: "💰 Montant de l’enchère", + value: `\`${bid.offer_amount} coins\``, + inline: true, + }, + { + name: "⏰ Fermeture", + value: ``, + }, + { + name: "🆔 ID de l’offre", + value: `\`${offer.id}\``, + inline: false, + }, + ) + .setTimestamp(); + + discordUserSeller.send({ embeds: [embed] }).catch(console.error); + } + } catch (e) { + console.error(`Erreur lors de la notification du vendeur : ${e}`); + } + + try { + const discordUserNewBidder = await client.users.fetch(bid.bidder_id); + const userNewBidder = getUser.get(bid.bidder_id); + if (discordUserNewBidder && userNewBidder?.isAkhy) { + const embed = new EmbedBuilder() + .setTitle("🔔 Nouvelle enchère") + .setDescription( + `Ton enchère sur l'offre pour le skin **${skin ? skin.displayName : offer.skin_uuid}** a bien été placée!`, + ) + .setThumbnail(skin.displayIcon) + .setColor(0x5865f2) // Discord blurple + .addFields({ + name: "💰 Montant de l’enchère", + value: `\`${bid.offer_amount} coins\``, + inline: true, + }) + .setTimestamp(); + + discordUserNewBidder.send({ embeds: [embed] }).catch(console.error); + } + } catch (e) { + console.error(`Erreur lors de la notification de l'enchérriseur : ${e}`); + } + + try { + const offerBids = getOfferBids.all(offer.id); + if (offerBids.length < 2) return; // No previous bidder to notify + + const discordUserPreviousBidder = await client.users.fetch(offerBids[1].bidder_id); + const userPreviousBidder = getUser.get(offerBids[1].bidder_id); + if (discordUserPreviousBidder && userPreviousBidder?.isAkhy) { + const embed = new EmbedBuilder() + .setTitle("🔔 Nouvelle enchère") + .setDescription( + `Quelqu'un a surenchéri sur l'offre pour le skin **${skin ? skin.displayName : offer.skin_uuid}**, tu n'es plus le meilleur enchérisseur !`, + ) + .setThumbnail(skin.displayIcon) + .setColor(0x5865f2) // Discord blurple + .addFields( + { + name: "👤 Enchérisseur", + value: `<@${bid.bidder_id}> ${bidderUser ? "(" + bidderUser.username + ")" : ""}`, + inline: true, + }, + { + name: "💰 Montant de l’enchère", + value: `\`${bid.offer_amount} coins\``, + inline: true, + }, + ) + .setTimestamp(); + + discordUserPreviousBidder.send({ embeds: [embed] }).catch(console.error); + } + } catch (e) { + console.error(e); + } + + // Notify previous highest bidder +} + +export async function handleCaseOpening(caseType, userId, skinUuid, client) { + const discordUser = await client.users.fetch(userId); + const skin = getSkin.get(skinUuid); + try { + const guildChannel = await client.channels.fetch(process.env.BOT_CHANNEL_ID); + const embed = new EmbedBuilder() + .setTitle("🔔 Ouverture de caisse") + .setDescription( + `${discordUser ? discordUser.username : "Un utilisateur"} vient d'ouvrir une caisse **${caseType}** et a obtenu le skin **${skin.displayName}** !`, + ) + .setThumbnail(skin.displayIcon) + .setColor(skin.tierColor) // Discord blurple + .addFields( + { + name: "💰 Valeur estimée", + value: `\`${skin.currentPrice} coins\``, + inline: true, + }, + { + name: "Level", + value: `${skin.currentLvl}`, + }, + ) + .setTimestamp(); + guildChannel.send({ embeds: [embed] }).catch(console.error); + } catch (e) { + console.error(e); + } +}