Merge pull request #74 from cassoule/feat/db-changes

Feat/db changes
This commit is contained in:
Milo Gourvest
2026-02-06 20:22:00 +01:00
committed by GitHub
36 changed files with 2051 additions and 2088 deletions

3
.gitignore vendored
View File

@@ -4,4 +4,5 @@ flopobot.db
flopobot.db-shm flopobot.db-shm
flopobot.db-wal flopobot.db-wal
.idea .idea
*.db *.db
.claude

960
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -11,15 +11,17 @@
"scripts": { "scripts": {
"start": "node index.js", "start": "node index.js",
"register": "node commands.js", "register": "node commands.js",
"dev": "nodemon index.js" "dev": "nodemon index.js",
"prisma:generate": "prisma generate",
"prisma:migrate": "prisma migrate dev"
}, },
"author": "Milo Gourvest", "author": "Milo Gourvest",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@google/genai": "^1.30.0", "@google/genai": "^1.30.0",
"@mistralai/mistralai": "^1.6.0", "@mistralai/mistralai": "^1.6.0",
"@prisma/client": "^6.19.2",
"axios": "^1.9.0", "axios": "^1.9.0",
"better-sqlite3": "^11.9.1",
"discord-interactions": "^4.0.0", "discord-interactions": "^4.0.0",
"discord.js": "^14.18.0", "discord.js": "^14.18.0",
"dotenv": "^16.0.3", "dotenv": "^16.0.3",
@@ -27,7 +29,9 @@
"node-cron": "^3.0.3", "node-cron": "^3.0.3",
"openai": "^4.104.0", "openai": "^4.104.0",
"pokersolver": "^2.1.4", "pokersolver": "^2.1.4",
"prisma": "^6.19.2",
"socket.io": "^4.8.1", "socket.io": "^4.8.1",
"stripe": "^20.3.0",
"unique-names-generator": "^4.7.1", "unique-names-generator": "^4.7.1",
"uuid": "^11.1.0" "uuid": "^11.1.0"
}, },

View File

@@ -0,0 +1,138 @@
-- CreateTable
CREATE TABLE "users" (
"id" TEXT NOT NULL PRIMARY KEY,
"username" TEXT NOT NULL,
"globalName" TEXT,
"warned" INTEGER NOT NULL DEFAULT 0,
"warns" INTEGER NOT NULL DEFAULT 0,
"allTimeWarns" INTEGER NOT NULL DEFAULT 0,
"totalRequests" INTEGER NOT NULL DEFAULT 0,
"coins" INTEGER NOT NULL DEFAULT 0,
"dailyQueried" INTEGER NOT NULL DEFAULT 0,
"avatarUrl" TEXT,
"isAkhy" INTEGER NOT NULL DEFAULT 0
);
-- CreateTable
CREATE TABLE "skins" (
"uuid" TEXT NOT NULL PRIMARY KEY,
"displayName" TEXT,
"contentTierUuid" TEXT,
"displayIcon" TEXT,
"user_id" TEXT,
"tierRank" TEXT,
"tierColor" TEXT,
"tierText" TEXT,
"basePrice" TEXT,
"currentLvl" INTEGER,
"currentChroma" INTEGER,
"currentPrice" INTEGER,
"maxPrice" INTEGER,
CONSTRAINT "skins_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "users" ("id") ON DELETE SET NULL ON UPDATE CASCADE
);
-- CreateTable
CREATE TABLE "market_offers" (
"id" TEXT NOT NULL PRIMARY KEY,
"skin_uuid" TEXT NOT NULL,
"seller_id" TEXT NOT NULL,
"starting_price" INTEGER NOT NULL,
"buyout_price" INTEGER,
"final_price" INTEGER,
"status" TEXT NOT NULL,
"posted_at" TEXT DEFAULT '',
"opening_at" TEXT NOT NULL,
"closing_at" TEXT NOT NULL,
"buyer_id" TEXT,
CONSTRAINT "market_offers_skin_uuid_fkey" FOREIGN KEY ("skin_uuid") REFERENCES "skins" ("uuid") ON DELETE RESTRICT ON UPDATE CASCADE,
CONSTRAINT "market_offers_seller_id_fkey" FOREIGN KEY ("seller_id") REFERENCES "users" ("id") ON DELETE RESTRICT ON UPDATE CASCADE,
CONSTRAINT "market_offers_buyer_id_fkey" FOREIGN KEY ("buyer_id") REFERENCES "users" ("id") ON DELETE SET NULL ON UPDATE CASCADE
);
-- CreateTable
CREATE TABLE "bids" (
"id" TEXT NOT NULL PRIMARY KEY,
"bidder_id" TEXT NOT NULL,
"market_offer_id" TEXT NOT NULL,
"offer_amount" INTEGER NOT NULL,
"offered_at" TEXT DEFAULT '',
CONSTRAINT "bids_bidder_id_fkey" FOREIGN KEY ("bidder_id") REFERENCES "users" ("id") ON DELETE RESTRICT ON UPDATE CASCADE,
CONSTRAINT "bids_market_offer_id_fkey" FOREIGN KEY ("market_offer_id") REFERENCES "market_offers" ("id") ON DELETE RESTRICT ON UPDATE CASCADE
);
-- CreateTable
CREATE TABLE "logs" (
"id" TEXT NOT NULL PRIMARY KEY,
"user_id" TEXT NOT NULL,
"action" TEXT,
"target_user_id" TEXT,
"coins_amount" INTEGER,
"user_new_amount" INTEGER,
"created_at" TEXT DEFAULT '',
CONSTRAINT "logs_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "users" ("id") ON DELETE RESTRICT ON UPDATE CASCADE,
CONSTRAINT "logs_target_user_id_fkey" FOREIGN KEY ("target_user_id") REFERENCES "users" ("id") ON DELETE SET NULL ON UPDATE CASCADE
);
-- CreateTable
CREATE TABLE "games" (
"id" TEXT NOT NULL PRIMARY KEY,
"p1" TEXT NOT NULL,
"p2" TEXT,
"p1_score" INTEGER,
"p2_score" INTEGER,
"p1_elo" INTEGER,
"p2_elo" INTEGER,
"p1_new_elo" INTEGER,
"p2_new_elo" INTEGER,
"type" TEXT,
"timestamp" TEXT,
CONSTRAINT "games_p1_fkey" FOREIGN KEY ("p1") REFERENCES "users" ("id") ON DELETE RESTRICT ON UPDATE CASCADE,
CONSTRAINT "games_p2_fkey" FOREIGN KEY ("p2") REFERENCES "users" ("id") ON DELETE SET NULL ON UPDATE CASCADE
);
-- CreateTable
CREATE TABLE "elos" (
"id" TEXT NOT NULL PRIMARY KEY,
"elo" INTEGER NOT NULL,
CONSTRAINT "elos_id_fkey" FOREIGN KEY ("id") REFERENCES "users" ("id") ON DELETE RESTRICT ON UPDATE CASCADE
);
-- CreateTable
CREATE TABLE "sotd" (
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
"tableauPiles" TEXT,
"foundationPiles" TEXT,
"stockPile" TEXT,
"wastePile" TEXT,
"isDone" INTEGER NOT NULL DEFAULT 0,
"seed" TEXT
);
-- CreateTable
CREATE TABLE "sotd_stats" (
"id" TEXT NOT NULL PRIMARY KEY,
"user_id" TEXT NOT NULL,
"time" INTEGER,
"moves" INTEGER,
"score" INTEGER,
CONSTRAINT "sotd_stats_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "users" ("id") ON DELETE RESTRICT ON UPDATE CASCADE
);
-- CreateTable
CREATE TABLE "transactions" (
"id" TEXT NOT NULL PRIMARY KEY,
"session_id" TEXT NOT NULL,
"user_id" TEXT NOT NULL,
"coins_amount" INTEGER NOT NULL,
"amount_cents" INTEGER NOT NULL,
"currency" TEXT NOT NULL DEFAULT 'eur',
"customer_email" TEXT,
"customer_name" TEXT,
"payment_status" TEXT NOT NULL,
"created_at" TEXT DEFAULT '',
CONSTRAINT "transactions_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "users" ("id") ON DELETE RESTRICT ON UPDATE CASCADE
);
-- CreateIndex
CREATE UNIQUE INDEX "transactions_session_id_key" ON "transactions"("session_id");

175
prisma/schema.prisma Normal file
View File

@@ -0,0 +1,175 @@
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "sqlite"
url = env("DATABASE_URL")
}
model User {
id String @id
username String
globalName String?
warned Int @default(0)
warns Int @default(0)
allTimeWarns Int @default(0)
totalRequests Int @default(0)
coins Int @default(0)
dailyQueried Int @default(0)
avatarUrl String?
isAkhy Int @default(0)
elo Elo?
skins Skin[]
sellerOffers MarketOffer[] @relation("Seller")
buyerOffers MarketOffer[] @relation("Buyer")
bids Bid[]
logs Log[] @relation("UserLogs")
targetLogs Log[] @relation("TargetUserLogs")
gamesAsP1 Game[] @relation("Player1")
gamesAsP2 Game[] @relation("Player2")
sotdStats SotdStat[]
transactions Transaction[]
@@map("users")
}
model Skin {
uuid String @id
displayName String?
contentTierUuid String?
displayIcon String?
userId String? @map("user_id")
tierRank String?
tierColor String?
tierText String?
basePrice String?
currentLvl Int?
currentChroma Int?
currentPrice Int?
maxPrice Int?
owner User? @relation(fields: [userId], references: [id])
marketOffers MarketOffer[]
@@map("skins")
}
model MarketOffer {
id String @id
skinUuid String @map("skin_uuid")
sellerId String @map("seller_id")
startingPrice Int @map("starting_price")
buyoutPrice Int? @map("buyout_price")
finalPrice Int? @map("final_price")
status String
postedAt DateTime? @default(now()) @map("posted_at")
openingAt DateTime @map("opening_at")
closingAt DateTime @map("closing_at")
buyerId String? @map("buyer_id")
skin Skin @relation(fields: [skinUuid], references: [uuid])
seller User @relation("Seller", fields: [sellerId], references: [id])
buyer User? @relation("Buyer", fields: [buyerId], references: [id])
bids Bid[]
@@map("market_offers")
}
model Bid {
id String @id
bidderId String @map("bidder_id")
marketOfferId String @map("market_offer_id")
offerAmount Int @map("offer_amount")
offeredAt DateTime? @default(now()) @map("offered_at")
bidder User @relation(fields: [bidderId], references: [id])
marketOffer MarketOffer @relation(fields: [marketOfferId], references: [id])
@@map("bids")
}
model Log {
id String @id
userId String @map("user_id")
action String?
targetUserId String? @map("target_user_id")
coinsAmount Int? @map("coins_amount")
userNewAmount Int? @map("user_new_amount")
createdAt DateTime? @default(now()) @map("created_at")
user User @relation("UserLogs", fields: [userId], references: [id])
targetUser User? @relation("TargetUserLogs", fields: [targetUserId], references: [id])
@@map("logs")
}
model Game {
id String @id
p1 String
p2 String?
p1Score Int? @map("p1_score")
p2Score Int? @map("p2_score")
p1Elo Int? @map("p1_elo")
p2Elo Int? @map("p2_elo")
p1NewElo Int? @map("p1_new_elo")
p2NewElo Int? @map("p2_new_elo")
type String?
timestamp DateTime?
player1 User @relation("Player1", fields: [p1], references: [id])
player2 User? @relation("Player2", fields: [p2], references: [id])
@@map("games")
}
model Elo {
id String @id
elo Int
user User @relation(fields: [id], references: [id])
@@map("elos")
}
model Sotd {
id Int @id
tableauPiles String?
foundationPiles String?
stockPile String?
wastePile String?
isDone Int @default(0)
seed String?
@@map("sotd")
}
model SotdStat {
id String @id
userId String @map("user_id")
time Int?
moves Int?
score Int?
user User @relation(fields: [userId], references: [id])
@@map("sotd_stats")
}
model Transaction {
id String @id
sessionId String @unique @map("session_id")
userId String @map("user_id")
coinsAmount Int @map("coins_amount")
amountCents Int @map("amount_cents")
currency String @default("eur")
customerEmail String? @map("customer_email")
customerName String? @map("customer_name")
paymentStatus String @map("payment_status")
createdAt DateTime? @default(now()) @map("created_at")
user User @relation(fields: [userId], references: [id])
@@map("transactions")
}

View File

@@ -5,7 +5,7 @@ import {
InteractionResponseFlags, InteractionResponseFlags,
} from "discord-interactions"; } from "discord-interactions";
import { activeInventories, skins } from "../../game/state.js"; import { activeInventories, skins } from "../../game/state.js";
import { getUserInventory } from "../../database/index.js"; import * as skinService from "../../services/skin.service.js";
/** /**
* Handles the /inventory slash command. * Handles the /inventory slash command.
@@ -33,7 +33,7 @@ export async function handleInventoryCommand(req, res, client, interactionId) {
// --- 1. Fetch Data --- // --- 1. Fetch Data ---
const guild = await client.guilds.fetch(guild_id); const guild = await client.guilds.fetch(guild_id);
const targetMember = await guild.members.fetch(targetUserId); const targetMember = await guild.members.fetch(targetUserId);
const inventorySkins = getUserInventory.all({ user_id: targetUserId }); const inventorySkins = await skinService.getUserInventory(targetUserId);
// --- 2. Handle Empty Inventory --- // --- 2. Handle Empty Inventory ---
if (inventorySkins.length === 0) { if (inventorySkins.length === 0) {

View File

@@ -5,7 +5,7 @@ import {
ButtonStyleTypes, ButtonStyleTypes,
} from "discord-interactions"; } from "discord-interactions";
import { activeSearchs, skins } from "../../game/state.js"; import { activeSearchs, skins } from "../../game/state.js";
import { getAllSkins } from "../../database/index.js"; import * as skinService from "../../services/skin.service.js";
/** /**
* Handles the /search slash command. * Handles the /search slash command.
@@ -23,7 +23,7 @@ export async function handleSearchCommand(req, res, client, interactionId) {
try { try {
// --- 1. Fetch and Filter Data --- // --- 1. Fetch and Filter Data ---
const allDbSkins = getAllSkins.all(); const allDbSkins = await skinService.getAllSkins();
const resultSkins = allDbSkins.filter( const resultSkins = allDbSkins.filter(
(skin) => (skin) =>
skin.displayName.toLowerCase().includes(searchValue) || skin.tierText.toLowerCase().includes(searchValue), skin.displayName.toLowerCase().includes(searchValue) || skin.tierText.toLowerCase().includes(searchValue),
@@ -61,12 +61,12 @@ export async function handleSearchCommand(req, res, client, interactionId) {
// Fetch owner details if the skin is owned // Fetch owner details if the skin is owned
let ownerText = ""; let ownerText = "";
if (currentSkin.user_id) { if (currentSkin.userId) {
try { try {
const owner = await guild.members.fetch(currentSkin.user_id); const owner = await guild.members.fetch(currentSkin.userId);
ownerText = `| **@${owner.user.globalName || owner.user.username}** ✅`; ownerText = `| **@${owner.user.globalName || owner.user.username}** ✅`;
} catch (e) { } catch (e) {
console.warn(`Could not fetch owner for user ID: ${currentSkin.user_id}`); console.warn(`Could not fetch owner for user ID: ${currentSkin.userId}`);
ownerText = "| Appartenant à un utilisateur inconnu"; ownerText = "| Appartenant à un utilisateur inconnu";
} }
} }

View File

@@ -1,5 +1,5 @@
import { InteractionResponseType } from "discord-interactions"; import { InteractionResponseType } from "discord-interactions";
import { getTopSkins } from "../../database/index.js"; import * as skinService from "../../services/skin.service.js";
/** /**
* Handles the /skins slash command. * Handles the /skins slash command.
@@ -13,7 +13,7 @@ export async function handleSkinsCommand(req, res, client) {
try { try {
// --- 1. Fetch Data --- // --- 1. Fetch Data ---
const topSkins = getTopSkins.all(); const topSkins = await skinService.getTopSkins();
const guild = await client.guilds.fetch(guild_id); const guild = await client.guilds.fetch(guild_id);
const fields = []; const fields = [];
@@ -23,14 +23,14 @@ export async function handleSkinsCommand(req, res, client) {
let ownerText = "Libre"; // Default text if the skin has no owner let ownerText = "Libre"; // Default text if the skin has no owner
// If the skin has an owner, fetch their details // If the skin has an owner, fetch their details
if (skin.user_id) { if (skin.userId) {
try { try {
const owner = await guild.members.fetch(skin.user_id); const owner = await guild.members.fetch(skin.userId);
// Use globalName if available, otherwise fallback to username // Use globalName if available, otherwise fallback to username
ownerText = `**@${owner.user.globalName || owner.user.username}** ✅`; ownerText = `**@${owner.user.globalName || owner.user.username}** ✅`;
} catch (e) { } catch (e) {
// This can happen if the user has left the server // This can happen if the user has left the server
console.warn(`Could not fetch owner for user ID: ${skin.user_id}`); console.warn(`Could not fetch owner for user ID: ${skin.userId}`);
ownerText = "Appartient à un utilisateur inconnu"; ownerText = "Appartient à un utilisateur inconnu";
} }
} }

View File

@@ -9,7 +9,7 @@ import { formatTime, getOnlineUsersWithRole } from "../../utils/index.js";
import { DiscordRequest } from "../../api/discord.js"; import { DiscordRequest } from "../../api/discord.js";
import { activePolls } from "../../game/state.js"; import { activePolls } from "../../game/state.js";
import { getSocketIo } from "../../server/socket.js"; import { getSocketIo } from "../../server/socket.js";
import { getUser } from "../../database/index.js"; import * as userService from "../../services/user.service.js";
/** /**
* Handles the /timeout slash command. * Handles the /timeout slash command.
@@ -102,12 +102,12 @@ export async function handleTimeoutCommand(req, res, client) {
if (remaining === 0) { if (remaining === 0) {
clearInterval(countdownInterval); clearInterval(countdownInterval);
const votersList = poll.voters const votersList = (await Promise.all(poll.voters
.map((voterId) => { .map(async (voterId) => {
const user = getUser.get(voterId); const user = await userService.getUser(voterId);
return `- ${user?.globalName || "Utilisateur Inconnu"}`; return `- ${user?.globalName || "Utilisateur Inconnu"}`;
}) })
.join("\n"); )).join("\n");
try { try {
await DiscordRequest(poll.endpoint, { await DiscordRequest(poll.endpoint, {
@@ -143,12 +143,12 @@ export async function handleTimeoutCommand(req, res, client) {
// --- Periodic Update Logic --- // --- Periodic Update Logic ---
// Update the message every second with the new countdown // Update the message every second with the new countdown
try { try {
const votersList = poll.voters const votersList = (await Promise.all(poll.voters
.map((voterId) => { .map(async (voterId) => {
const user = getUser.get(voterId); const user = await userService.getUser(voterId);
return `- ${user?.globalName || "Utilisateur Inconnu"}`; return `- ${user?.globalName || "Utilisateur Inconnu"}`;
}) })
.join("\n"); )).join("\n");
await DiscordRequest(poll.endpoint, { await DiscordRequest(poll.endpoint, {
method: "PATCH", method: "PATCH",

View File

@@ -1,7 +1,9 @@
import { InteractionResponseFlags, InteractionResponseType } from "discord-interactions"; import { InteractionResponseFlags, InteractionResponseType } from "discord-interactions";
import { ActionRowBuilder, ButtonBuilder, ButtonStyle, EmbedBuilder } from "discord.js"; import { ActionRowBuilder, ButtonBuilder, ButtonStyle, EmbedBuilder } from "discord.js";
import { DiscordRequest } from "../../api/discord.js"; import { DiscordRequest } from "../../api/discord.js";
import { getAllAvailableSkins, getUser, insertLog, updateSkin, updateUserCoins } from "../../database/index.js"; import * as userService from "../../services/user.service.js";
import * as skinService from "../../services/skin.service.js";
import * as logService from "../../services/log.service.js";
import { skins } from "../../game/state.js"; import { skins } from "../../game/state.js";
/** /**
@@ -27,7 +29,7 @@ export async function handleValorantCommand(req, res, client) {
try { try {
// --- 1. Verify and process payment --- // --- 1. Verify and process payment ---
const commandUser = getUser.get(userId); const commandUser = await userService.getUser(userId);
if (!commandUser) { if (!commandUser) {
return res.send({ return res.send({
type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE, type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE,
@@ -47,18 +49,15 @@ export async function handleValorantCommand(req, res, client) {
}); });
} }
insertLog.run({ await logService.insertLog({
id: `${userId}-${Date.now()}`, id: `${userId}-${Date.now()}`,
user_id: userId, userId: userId,
action: "VALO_CASE_OPEN", action: "VALO_CASE_OPEN",
target_user_id: null, targetUserId: null,
coins_amount: -valoPrice, coinsAmount: -valoPrice,
user_new_amount: commandUser.coins - valoPrice, userNewAmount: commandUser.coins - valoPrice,
});
updateUserCoins.run({
id: userId,
coins: commandUser.coins - valoPrice,
}); });
await userService.updateUserCoins(userId, commandUser.coins - valoPrice);
// --- 2. Send Initial "Opening" Response --- // --- 2. Send Initial "Opening" Response ---
// Acknowledge the interaction immediately with a loading message. // Acknowledge the interaction immediately with a loading message.
@@ -77,7 +76,7 @@ export async function handleValorantCommand(req, res, client) {
const webhookEndpoint = `webhooks/${process.env.APP_ID}/${token}/messages/@original`; const webhookEndpoint = `webhooks/${process.env.APP_ID}/${token}/messages/@original`;
try { try {
// --- Skin Selection --- // --- Skin Selection ---
const availableSkins = getAllAvailableSkins.all(); const availableSkins = await skinService.getAllAvailableSkins();
if (availableSkins.length === 0) { if (availableSkins.length === 0) {
throw new Error("No available skins to award."); throw new Error("No available skins to award.");
} }
@@ -105,9 +104,9 @@ export async function handleValorantCommand(req, res, client) {
const finalPrice = calculatePrice(); const finalPrice = calculatePrice();
// --- Update Database --- // --- Update Database ---
await updateSkin.run({ await skinService.updateSkin({
uuid: randomSkinData.uuid, uuid: randomSkinData.uuid,
user_id: userId, userId: userId,
currentLvl: randomLevel, currentLvl: randomLevel,
currentChroma: randomChroma, currentChroma: randomChroma,
currentPrice: finalPrice, currentPrice: finalPrice,

View File

@@ -2,7 +2,7 @@ import { InteractionResponseType, InteractionResponseFlags } from "discord-inter
import { DiscordRequest } from "../../api/discord.js"; import { DiscordRequest } from "../../api/discord.js";
import { activePolls } from "../../game/state.js"; import { activePolls } from "../../game/state.js";
import { getSocketIo } from "../../server/socket.js"; import { getSocketIo } from "../../server/socket.js";
import { getUser } from "../../database/index.js"; import * as userService from "../../services/user.service.js";
/** /**
* Handles clicks on the 'Yes' or 'No' buttons of a timeout poll. * Handles clicks on the 'Yes' or 'No' buttons of a timeout poll.
@@ -75,7 +75,10 @@ export async function handlePollVote(req, res) {
io.emit("poll-update"); // Notify frontend clients of the change io.emit("poll-update"); // Notify frontend clients of the change
const votersList = poll.voters.map((vId) => `- ${getUser.get(vId)?.globalName || "Utilisateur Inconnu"}`).join("\n"); const votersList = (await Promise.all(poll.voters.map(async (vId) => {
const user = await userService.getUser(vId);
return `- ${user?.globalName || "Utilisateur Inconnu"}`;
}))).join("\n");
// --- 4. Check for Majority --- // --- 4. Check for Majority ---
if (isVotingFor && poll.for >= poll.requiredMajority) { if (isVotingFor && poll.for >= poll.requiredMajority) {

View File

@@ -65,12 +65,12 @@ export async function handleSearchNav(req, res, client) {
// Fetch owner details if the skin is owned // Fetch owner details if the skin is owned
let ownerText = ""; let ownerText = "";
if (currentSkin.user_id) { if (currentSkin.userId) {
try { try {
const owner = await client.users.fetch(currentSkin.user_id); const owner = await client.users.fetch(currentSkin.userId);
ownerText = `| **@${owner.globalName || owner.username}** ✅`; ownerText = `| **@${owner.globalName || owner.username}** ✅`;
} catch (e) { } catch (e) {
console.warn(`Could not fetch owner for user ID: ${currentSkin.user_id}`); console.warn(`Could not fetch owner for user ID: ${currentSkin.userId}`);
ownerText = "| Appartenant à un utilisateur inconnu"; ownerText = "| Appartenant à un utilisateur inconnu";
} }
} }

View File

@@ -9,7 +9,9 @@ import { EmbedBuilder, ActionRowBuilder, ButtonBuilder, ButtonStyle } from "disc
import { DiscordRequest } from "../../api/discord.js"; import { DiscordRequest } from "../../api/discord.js";
import { postAPOBuy } from "../../utils/index.js"; import { postAPOBuy } from "../../utils/index.js";
import { activeInventories, skins } from "../../game/state.js"; import { activeInventories, skins } from "../../game/state.js";
import { getSkin, getUser, insertLog, updateSkin, updateUserCoins } from "../../database/index.js"; import * as userService from "../../services/user.service.js";
import * as skinService from "../../services/skin.service.js";
import * as logService from "../../services/log.service.js";
/** /**
* Handles the click of the 'Upgrade' button on a skin in the inventory. * Handles the click of the 'Upgrade' button on a skin in the inventory.
@@ -65,7 +67,7 @@ export async function handleUpgradeSkin(req, res) {
// --- 2. Handle Payment --- // --- 2. Handle Payment ---
const upgradePrice = parseFloat(process.env.VALO_UPGRADE_PRICE) || parseFloat(skinToUpgrade.maxPrice) / 10; const upgradePrice = parseFloat(process.env.VALO_UPGRADE_PRICE) || parseFloat(skinToUpgrade.maxPrice) / 10;
const commandUser = getUser.get(userId); const commandUser = await userService.getUser(userId);
if (!commandUser) { if (!commandUser) {
return res.send({ return res.send({
@@ -86,18 +88,15 @@ export async function handleUpgradeSkin(req, res) {
}); });
} }
insertLog.run({ await logService.insertLog({
id: `${userId}-${Date.now()}`, id: `${userId}-${Date.now()}`,
user_id: userId, userId: userId,
action: "VALO_SKIN_UPGRADE", action: "VALO_SKIN_UPGRADE",
target_user_id: null, targetUserId: null,
coins_amount: -upgradePrice.toFixed(0), coinsAmount: -upgradePrice.toFixed(0),
user_new_amount: commandUser.coins - upgradePrice.toFixed(0), userNewAmount: commandUser.coins - upgradePrice.toFixed(0),
});
updateUserCoins.run({
id: userId,
coins: commandUser.coins - upgradePrice.toFixed(0),
}); });
await userService.updateUserCoins(userId, commandUser.coins - upgradePrice.toFixed(0));
// --- 3. Show Loading Animation --- // --- 3. Show Loading Animation ---
// Acknowledge the click immediately and then edit the message to show a loading state. // Acknowledge the click immediately and then edit the message to show a loading state.
@@ -151,9 +150,9 @@ export async function handleUpgradeSkin(req, res) {
}; };
skinToUpgrade.currentPrice = calculatePrice(); skinToUpgrade.currentPrice = calculatePrice();
await updateSkin.run({ await skinService.updateSkin({
uuid: skinToUpgrade.uuid, uuid: skinToUpgrade.uuid,
user_id: skinToUpgrade.user_id, userId: skinToUpgrade.userId,
currentLvl: skinToUpgrade.currentLvl, currentLvl: skinToUpgrade.currentLvl,
currentChroma: skinToUpgrade.currentChroma, currentChroma: skinToUpgrade.currentChroma,
currentPrice: skinToUpgrade.currentPrice, currentPrice: skinToUpgrade.currentPrice,
@@ -165,7 +164,7 @@ export async function handleUpgradeSkin(req, res) {
// --- 6. Send Final Result --- // --- 6. Send Final Result ---
setTimeout(async () => { setTimeout(async () => {
// Fetch the latest state of the skin from the database // Fetch the latest state of the skin from the database
const finalSkinState = getSkin.get(skinToUpgrade.uuid); const finalSkinState = await skinService.getSkin(skinToUpgrade.uuid);
const finalEmbed = buildFinalEmbed(succeeded, finalSkinState, skinData); const finalEmbed = buildFinalEmbed(succeeded, finalSkinState, skinData);
const finalComponents = buildFinalComponents(succeeded, skinData, finalSkinState, interactionId); const finalComponents = buildFinalComponents(succeeded, skinData, finalSkinState, interactionId);

View File

@@ -1,4 +1,5 @@
import { sleep } from "openai/core"; import { sleep } from "openai/core";
import { AttachmentBuilder } from "discord.js";
import { import {
buildAiMessages, buildAiMessages,
buildParticipantsMap, buildParticipantsMap,
@@ -12,18 +13,10 @@ import {
import { calculateBasePrice, calculateMaxPrice, formatTime, getAkhys } from "../../utils/index.js"; import { calculateBasePrice, calculateMaxPrice, formatTime, getAkhys } from "../../utils/index.js";
import { channelPointsHandler, initTodaysSOTD, randomSkinPrice, slowmodesHandler } from "../../game/points.js"; import { channelPointsHandler, initTodaysSOTD, randomSkinPrice, slowmodesHandler } from "../../game/points.js";
import { activePolls, activeSlowmodes, requestTimestamps, skins } from "../../game/state.js"; import { activePolls, activeSlowmodes, requestTimestamps, skins } from "../../game/state.js";
import { import prisma from "../../prisma/client.js";
flopoDB, import * as userService from "../../services/user.service.js";
getAllSkins, import * as skinService from "../../services/skin.service.js";
getAllUsers, import * as logService from "../../services/log.service.js";
getUser,
hardUpdateSkin,
insertLog,
updateManyUsers,
updateSkin,
updateUserAvatar,
updateUserCoins,
} from "../../database/index.js";
import { client } from "../client.js"; import { client } from "../client.js";
import { drawCaseContent, drawCaseSkin, getDummySkinUpgradeProbs } from "../../utils/caseOpening.js"; import { drawCaseContent, drawCaseSkin, getDummySkinUpgradeProbs } from "../../utils/caseOpening.js";
@@ -52,10 +45,10 @@ export async function handleMessageCreate(message, client, io) {
// --- Main Guild Features (Points & Slowmode) --- // --- Main Guild Features (Points & Slowmode) ---
if (message.guildId === process.env.GUILD_ID) { if (message.guildId === process.env.GUILD_ID) {
// Award points for activity // Award points for activity
const pointsAwarded = channelPointsHandler(message); // const pointsAwarded = channelPointsHandler(message);
if (pointsAwarded) { // if (pointsAwarded) {
io.emit("data-updated", { table: "users", action: "update" }); // io.emit("data-updated", { table: "users", action: "update" });
} // }
// Enforce active slowmodes // Enforce active slowmodes
const wasSlowmoded = await slowmodesHandler(message, activeSlowmodes); const wasSlowmoded = await slowmodesHandler(message, activeSlowmodes);
@@ -88,7 +81,7 @@ export async function handleMessageCreate(message, client, io) {
// --- Sub-handler for AI Logic --- // --- Sub-handler for AI Logic ---
async function handleAiMention(message, client, io) { async function handleAiMention(message, client, io) {
const authorId = message.author.id; const authorId = message.author.id;
let authorDB = getUser.get(authorId); let authorDB = await userService.getUser(authorId);
if (!authorDB) return; // Should not happen if user is in DB, but good practice if (!authorDB) return; // Should not happen if user is in DB, but good practice
// --- Rate Limiting --- // --- Rate Limiting ---
@@ -104,7 +97,7 @@ async function handleAiMention(message, client, io) {
authorDB.warned = 1; authorDB.warned = 1;
authorDB.warns += 1; authorDB.warns += 1;
authorDB.allTimeWarns += 1; authorDB.allTimeWarns += 1;
updateManyUsers([authorDB]); await userService.updateManyUsers([authorDB]);
// Apply timeout if warn count is too high // Apply timeout if warn count is too high
if (authorDB.warns > (parseInt(process.env.MAX_WARNS) || 10)) { if (authorDB.warns > (parseInt(process.env.MAX_WARNS) || 10)) {
@@ -134,7 +127,7 @@ async function handleAiMention(message, client, io) {
authorDB.warned = 0; authorDB.warned = 0;
authorDB.warns = 0; authorDB.warns = 0;
authorDB.totalRequests += 1; authorDB.totalRequests += 1;
updateManyUsers([authorDB]); await userService.updateManyUsers([authorDB]);
// --- AI Processing --- // --- AI Processing ---
try { try {
@@ -238,14 +231,18 @@ async function handleAdminCommands(message) {
message.reply("New Solitaire of the Day initialized."); message.reply("New Solitaire of the Day initialized.");
break; break;
case `${prefix}:users`: case `${prefix}:users`:
console.log(getAllUsers.all()); console.log(await userService.getAllUsers());
break; break;
case `${prefix}:sql`: case `${prefix}:sql`:
const sqlCommand = args.join(" "); const sqlCommand = args.join(" ");
try { try {
const stmt = flopoDB.prepare(sqlCommand); const result = sqlCommand.trim().toUpperCase().startsWith("SELECT")
const result = sqlCommand.trim().toUpperCase().startsWith("SELECT") ? stmt.all() : stmt.run(); ? await prisma.$queryRawUnsafe(sqlCommand)
message.reply("```json\n" + JSON.stringify(result, null, 2).substring(0, 1900) + "\n```"); : await prisma.$executeRawUnsafe(sqlCommand);
const jsonString = JSON.stringify(result, null, 2);
const buffer = Buffer.from(jsonString, "utf-8");
const attachment = new AttachmentBuilder(buffer, { name: "sql-result.json" });
message.reply({ content: "SQL query executed successfully:", files: [attachment] });
} catch (e) { } catch (e) {
message.reply(`SQL Error: ${e.message}`); message.reply(`SQL Error: ${e.message}`);
} }
@@ -263,16 +260,16 @@ async function handleAdminCommands(message) {
avatarUrl: akhy.user.displayAvatarURL({ dynamic: true, size: 256 }), avatarUrl: akhy.user.displayAvatarURL({ dynamic: true, size: 256 }),
})); }));
usersToUpdate.forEach((user) => { for (const user of usersToUpdate) {
try { try {
updateUserAvatar.run(user); await userService.updateUserAvatar(user.id, user.avatarUrl);
} catch (err) {} } catch (err) {}
}); }
break; break;
case `${prefix}:rework-skins`: case `${prefix}:rework-skins`:
console.log("Reworking all skin prices..."); console.log("Reworking all skin prices...");
const dbSkins = getAllSkins.all(); const dbSkins = await skinService.getAllSkins();
dbSkins.forEach((skin) => { for (const skin of dbSkins) {
const fetchedSkin = skins.find((s) => s.uuid === skin.uuid); const fetchedSkin = skins.find((s) => s.uuid === skin.uuid);
const basePrice = calculateBasePrice(fetchedSkin, skin.tierRank)?.toFixed(0); const basePrice = calculateBasePrice(fetchedSkin, skin.tierRank)?.toFixed(0);
const calculatePrice = () => { const calculatePrice = () => {
@@ -283,12 +280,12 @@ async function handleAdminCommands(message) {
return parseFloat(result.toFixed(0)); return parseFloat(result.toFixed(0));
}; };
const maxPrice = calculateMaxPrice(basePrice, fetchedSkin).toFixed(0); const maxPrice = calculateMaxPrice(basePrice, fetchedSkin).toFixed(0);
hardUpdateSkin.run({ await skinService.hardUpdateSkin({
uuid: skin.uuid, uuid: skin.uuid,
displayName: skin.displayName, displayName: skin.displayName,
contentTierUuid: skin.contentTierUuid, contentTierUuid: skin.contentTierUuid,
displayIcon: skin.displayIcon, displayIcon: skin.displayIcon,
user_id: skin.user_id, userId: skin.userId,
tierRank: skin.tierRank, tierRank: skin.tierRank,
tierColor: skin.tierColor, tierColor: skin.tierColor,
tierText: skin.tierText, tierText: skin.tierText,
@@ -298,7 +295,7 @@ async function handleAdminCommands(message) {
currentPrice: skin.currentPrice ? calculatePrice() : null, currentPrice: skin.currentPrice ? calculatePrice() : null,
maxPrice: maxPrice, maxPrice: maxPrice,
}); });
}); }
console.log("Reworked", dbSkins.length, "skins."); console.log("Reworked", dbSkins.length, "skins.");
break; break;
case `${prefix}:cases-test`: case `${prefix}:cases-test`:
@@ -324,7 +321,7 @@ async function handleAdminCommands(message) {
for (let i = 0; i < caseCount; i++) { for (let i = 0; i < caseCount; i++) {
const skins = await drawCaseContent(caseType); const skins = await drawCaseContent(caseType);
const result = drawCaseSkin(skins); const result = await drawCaseSkin(skins);
totalResValue += result.finalPrice; totalResValue += result.finalPrice;
if (result.finalPrice > highestSkinPrice) highestSkinPrice = result.finalPrice; if (result.finalPrice > highestSkinPrice) highestSkinPrice = result.finalPrice;
if (result.finalPrice > 0 && result.finalPrice < 100) priceTiers["0"] += 1; if (result.finalPrice > 0 && result.finalPrice < 100) priceTiers["0"] += 1;
@@ -354,26 +351,23 @@ async function handleAdminCommands(message) {
break; break;
case `${prefix}:refund-skins`: case `${prefix}:refund-skins`:
try { try {
const DBskins = getAllSkins.all(); const DBskins = await skinService.getAllSkins();
for (const skin of DBskins) { for (const skin of DBskins) {
const owner = getUser.get(skin.user_id); const owner = await userService.getUser(skin.userId);
if (owner) { if (owner) {
updateUserCoins.run({ await userService.updateUserCoins(owner.id, owner.coins + skin.currentPrice);
id: owner.id, await logService.insertLog({
coins: owner.coins + skin.currentPrice,
});
insertLog.run({
id: `${skin.uuid}-skin-refund-${Date.now()}`, id: `${skin.uuid}-skin-refund-${Date.now()}`,
user_id: owner.id, userId: owner.id,
target_user_id: null, targetUserId: null,
action: "SKIN_REFUND", action: "SKIN_REFUND",
coins_amount: skin.currentPrice, coinsAmount: skin.currentPrice,
user_new_amount: owner.coins + skin.currentPrice, userNewAmount: owner.coins + skin.currentPrice,
}); });
} }
updateSkin.run({ await skinService.updateSkin({
uuid: skin.uuid, uuid: skin.uuid,
user_id: null, userId: null,
currentPrice: null, currentPrice: null,
currentLvl: null, currentLvl: null,
currentChroma: null, currentChroma: null,

View File

@@ -1,863 +0,0 @@
import Database from "better-sqlite3";
export const flopoDB = new Database(process.env.DB_PATH || "flopobot.db");
/* -------------------------
CREATE ALL TABLES FIRST
----------------------------*/
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
);
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
);
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
);
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
);
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
);
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
);
CREATE TABLE IF NOT EXISTS elos
(
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
);
CREATE TABLE IF NOT EXISTS sotd_stats
(
id
TEXT
PRIMARY
KEY,
user_id
TEXT
REFERENCES
users,
time
INTEGER,
moves
INTEGER,
score
INTEGER
);
`);
/* -----------------------------------------------------
PREPARE ANY CREATE TABLE STATEMENT OBJECTS (kept for parity)
------------------------------------------------------*/
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
)
`);
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
)
`);
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
)
`);
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
)
`);
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
)
`);
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
)
`);
stmtGames.run();
export const stmtElos = flopoDB.prepare(`
CREATE TABLE IF NOT EXISTS elos
(
id
PRIMARY
KEY
REFERENCES
users,
elo
INTEGER
)
`);
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
)
`);
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
)
`);
stmtSOTDStats.run();
/* -------------------------
USER statements
----------------------------*/
export const insertUser = flopoDB.prepare(
`INSERT INTO users (id, username, globalName, warned, warns, allTimeWarns, totalRequests, avatarUrl, isAkhy)
VALUES (@id, @username, @globalName, @warned, @warns, @allTimeWarns, @totalRequests, @avatarUrl, @isAkhy)`,
);
export const updateUser = flopoDB.prepare(
`UPDATE users
SET warned = @warned,
warns = @warns,
allTimeWarns = @allTimeWarns,
totalRequests = @totalRequests
WHERE id = @id`,
);
export const updateUserAvatar = flopoDB.prepare("UPDATE users SET avatarUrl = @avatarUrl WHERE id = @id");
export const queryDailyReward = flopoDB.prepare(`UPDATE users
SET dailyQueried = 1
WHERE id = ?`
);
export const resetDailyReward = flopoDB.prepare(`UPDATE users
SET dailyQueried = 0`
);
export const updateUserCoins = flopoDB.prepare("UPDATE users SET coins = @coins WHERE id = @id");
export const getUser = flopoDB.prepare(
"SELECT users.*,elos.elo FROM users LEFT JOIN elos ON elos.id = users.id WHERE users.id = ?",
);
export const getAllUsers = flopoDB.prepare(
"SELECT users.*,elos.elo FROM users LEFT JOIN elos ON elos.id = users.id ORDER BY coins DESC",
);
export const getAllAkhys = flopoDB.prepare(
"SELECT users.*,elos.elo FROM users LEFT JOIN elos ON elos.id = users.id WHERE isAkhy = 1 ORDER BY coins DESC",
);
/* -------------------------
SKINS statements
----------------------------*/
export const insertSkin = flopoDB.prepare(
`INSERT INTO skins (uuid, displayName, contentTierUuid, displayIcon, user_id, tierRank, tierColor, tierText,
basePrice, maxPrice)
VALUES (@uuid, @displayName, @contentTierUuid, @displayIcon, @user_id, @tierRank, @tierColor, @tierText,
@basePrice, @maxPrice)`,
);
export const updateSkin = flopoDB.prepare(
`UPDATE skins
SET user_id = @user_id,
currentLvl = @currentLvl,
currentChroma = @currentChroma,
currentPrice = @currentPrice
WHERE uuid = @uuid`,
);
export const hardUpdateSkin = flopoDB.prepare(
`UPDATE skins
SET displayName = @displayName,
contentTierUuid = @contentTierUuid,
displayIcon = @displayIcon,
tierRank = @tierRank,
tierColor = @tierColor,
tierText = @tierText,
basePrice = @basePrice,
user_id = @user_id,
currentLvl = @currentLvl,
currentChroma = @currentChroma,
currentPrice = @currentPrice,
maxPrice = @maxPrice
WHERE uuid = @uuid`,
);
export const getSkin = flopoDB.prepare("SELECT * FROM skins WHERE uuid = ?");
export const getAllSkins = flopoDB.prepare("SELECT * FROM skins ORDER BY maxPrice DESC");
export const getAllAvailableSkins = flopoDB.prepare("SELECT * FROM skins WHERE user_id IS NULL");
export const getUserInventory = flopoDB.prepare(
"SELECT * FROM skins WHERE user_id = @user_id ORDER BY currentPrice DESC",
);
export const getTopSkins = flopoDB.prepare("SELECT * FROM skins ORDER BY maxPrice DESC LIMIT 10");
/* -------------------------
MARKET / BIDS / OFFERS
----------------------------*/
export const getMarketOffers = flopoDB.prepare(`
SELECT *
FROM market_offers
ORDER BY market_offers.posted_at DESC
`);
export const getMarketOfferById = flopoDB.prepare(`
SELECT market_offers.*,
skins.displayName AS skinName,
skins.displayIcon AS skinIcon,
seller.username AS sellerName,
seller.globalName AS sellerGlobalName,
buyer.username AS buyerName,
buyer.globalName AS buyerGlobalName
FROM market_offers
JOIN skins ON skins.uuid = market_offers.skin_uuid
JOIN users AS seller ON seller.id = market_offers.seller_id
LEFT JOIN users AS buyer ON buyer.id = market_offers.buyer_id
WHERE market_offers.id = ?
`);
export const getMarketOffersBySkin = flopoDB.prepare(`
SELECT market_offers.*,
skins.displayName AS skinName,
skins.displayIcon AS skinIcon,
seller.username AS sellerName,
seller.globalName AS sellerGlobalName,
buyer.username AS buyerName,
buyer.globalName AS buyerGlobalName
FROM market_offers
JOIN skins ON skins.uuid = market_offers.skin_uuid
JOIN users AS seller ON seller.id = market_offers.seller_id
LEFT JOIN users AS buyer ON buyer.id = market_offers.buyer_id
WHERE market_offers.skin_uuid = ?
`);
export const insertMarketOffer = flopoDB.prepare(`
INSERT INTO market_offers (id, skin_uuid, seller_id, starting_price, buyout_price, status, opening_at, closing_at)
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
----------------------------*/
export const getBids = flopoDB.prepare(`
SELECT bids.*,
bidder.username AS bidderName,
bidder.globalName AS bidderGlobalName
FROM bids
JOIN users AS bidder ON bidder.id = bids.bidder_id
ORDER BY bids.offer_amount DESC, bids.offered_at ASC
`);
export const getBidById = flopoDB.prepare(`
SELECT bids.*
FROM bids
WHERE bids.id = ?
`);
export const getOfferBids = flopoDB.prepare(`
SELECT bids.*
FROM bids
WHERE bids.market_offer_id = ?
ORDER BY bids.offer_amount DESC, bids.offered_at ASC
`);
export const insertBid = flopoDB.prepare(`
INSERT INTO bids (id, bidder_id, market_offer_id, offer_amount)
VALUES (@id, @bidder_id, @market_offer_id, @offer_amount)
`);
export const deleteBid = flopoDB.prepare(`
DELETE
FROM bids
WHERE id = ?
`);
/* -------------------------
BULK TRANSACTIONS (synchronous)
----------------------------*/
export const insertManyUsers = flopoDB.transaction((users) => {
for (const user of users)
try {
insertUser.run(user);
} catch (e) {}
});
export const updateManyUsers = flopoDB.transaction((users) => {
for (const user of users)
try {
updateUser.run(user);
} catch (e) {
console.log(`User update failed`);
}
});
export const insertManySkins = flopoDB.transaction((skins) => {
for (const skin of skins)
try {
insertSkin.run(skin);
} catch (e) {}
});
export const updateManySkins = flopoDB.transaction((skins) => {
for (const skin of skins)
try {
updateSkin.run(skin);
} catch (e) {}
});
/* -------------------------
LOGS
----------------------------*/
export const insertLog = flopoDB.prepare(
`INSERT INTO logs (id, user_id, action, target_user_id, coins_amount, user_new_amount)
VALUES (@id, @user_id, @action, @target_user_id, @coins_amount, @user_new_amount)`,
);
export const getLogs = flopoDB.prepare("SELECT * FROM logs");
export const getUserLogs = flopoDB.prepare("SELECT * FROM logs WHERE user_id = @user_id");
/* -------------------------
GAMES
----------------------------*/
export const insertGame = flopoDB.prepare(
`INSERT INTO games (id, p1, p2, p1_score, p2_score, p1_elo, p2_elo, p1_new_elo, p2_new_elo, type, timestamp)
VALUES (@id, @p1, @p2, @p1_score, @p2_score, @p1_elo, @p2_elo, @p1_new_elo, @p2_new_elo, @type, @timestamp)`,
);
export const getGames = flopoDB.prepare("SELECT * FROM games");
export const getUserGames = flopoDB.prepare(
"SELECT * FROM games WHERE p1 = @user_id OR p2 = @user_id ORDER BY timestamp",
);
/* -------------------------
ELOS
----------------------------*/
export const insertElos = flopoDB.prepare(`INSERT INTO elos (id, elo)
VALUES (@id, @elo)`
);
export const getElos = flopoDB.prepare(`SELECT *
FROM elos`
);
export const getUserElo = flopoDB.prepare(`SELECT *
FROM elos
WHERE id = @id`
);
export const updateElo = flopoDB.prepare("UPDATE elos SET elo = @elo WHERE id = @id");
export const getUsersByElo = flopoDB.prepare(
"SELECT * FROM users JOIN elos ON elos.id = users.id ORDER BY elos.elo DESC",
);
/* -------------------------
SOTD
----------------------------*/
export const getSOTD = flopoDB.prepare(`SELECT *
FROM sotd
WHERE id = '0'`
);
export const insertSOTD =
flopoDB.prepare(`INSERT INTO sotd (id, tableauPiles, foundationPiles, stockPile, wastePile, seed)
VALUES (0, @tableauPiles, @foundationPiles, @stockPile, @wastePile, @seed)`);
export const deleteSOTD = flopoDB.prepare(`DELETE
FROM sotd
WHERE id = '0'`
);
export const getAllSOTDStats = flopoDB.prepare(`SELECT sotd_stats.*, users.globalName
FROM sotd_stats
JOIN users ON users.id = sotd_stats.user_id
ORDER BY score DESC, moves ASC, time ASC`);
export const getUserSOTDStats = flopoDB.prepare(`SELECT *
FROM sotd_stats
WHERE user_id = ?`);
export const insertSOTDStats = flopoDB.prepare(`INSERT INTO sotd_stats (id, user_id, time, moves, score)
VALUES (@id, @user_id, @time, @moves, @score)`);
export const clearSOTDStats = flopoDB.prepare(`DELETE
FROM sotd_stats`);
export const deleteUserSOTDStats = flopoDB.prepare(`DELETE
FROM sotd_stats
WHERE user_id = ?`);
/* -------------------------
pruneOldLogs
----------------------------*/
export async function pruneOldLogs() {
const users = flopoDB
.prepare(
`
SELECT user_id
FROM logs
GROUP BY user_id
HAVING COUNT(*) > ${process.env.LOGS_BY_USER}
`,
)
.all();
const transaction = flopoDB.transaction(() => {
for (const { user_id } of users) {
flopoDB
.prepare(
`
DELETE
FROM logs
WHERE id IN (SELECT id
FROM (SELECT id,
ROW_NUMBER() OVER (ORDER BY created_at DESC) AS rn
FROM logs
WHERE user_id = ?)
WHERE rn > ${process.env.LOGS_BY_USER})
`,
)
.run(user_id);
}
});
transaction();
}

View File

@@ -3,7 +3,8 @@
// Inspired by your poker helpers API style. // Inspired by your poker helpers API style.
import { emitToast } from "../server/socket.js"; import { emitToast } from "../server/socket.js";
import { getUser, insertLog, updateUserCoins } from "../database/index.js"; import * as userService from "../services/user.service.js";
import * as logService from "../services/log.service.js";
import { client } from "../bot/client.js"; import { client } from "../bot/client.js";
import { EmbedBuilder } from "discord.js"; import { EmbedBuilder } from "discord.js";
@@ -299,21 +300,18 @@ export async function settleAll(room) {
p.totalDelta += res.delta; p.totalDelta += res.delta;
p.totalBets++; p.totalBets++;
if (res.result === "win" || res.result === "push" || res.result === "blackjack") { if (res.result === "win" || res.result === "push" || res.result === "blackjack") {
const userDB = getUser.get(p.id); const userDB = await userService.getUser(p.id);
if (userDB) { if (userDB) {
const coins = userDB.coins; const coins = userDB.coins;
try { try {
updateUserCoins.run({ await userService.updateUserCoins(p.id, coins + hand.bet + res.delta);
id: p.id, await logService.insertLog({
coins: coins + hand.bet + res.delta,
});
insertLog.run({
id: `${p.id}-blackjack-${Date.now()}`, id: `${p.id}-blackjack-${Date.now()}`,
user_id: p.id, userId: p.id,
target_user_id: null, targetUserId: null,
action: "BLACKJACK_PAYOUT", action: "BLACKJACK_PAYOUT",
coins_amount: res.delta + hand.bet, coinsAmount: res.delta + hand.bet,
user_new_amount: coins + hand.bet + res.delta, userNewAmount: coins + hand.bet + res.delta,
}); });
p.bank = coins + hand.bet + res.delta; p.bank = coins + hand.bet + res.delta;
} catch (e) { } catch (e) {

View File

@@ -1,4 +1,5 @@
import { getUser, getUserElo, insertElos, insertGame, updateElo } from "../database/index.js"; import * as userService from "../services/user.service.js";
import * as gameService from "../services/game.service.js";
import { ButtonStyle, EmbedBuilder } from "discord.js"; import { ButtonStyle, EmbedBuilder } from "discord.js";
import { client } from "../bot/client.js"; import { client } from "../bot/client.js";
@@ -12,23 +13,23 @@ import { client } from "../bot/client.js";
*/ */
export async function eloHandler(p1Id, p2Id, p1Score, p2Score, type, scores = null) { export async function eloHandler(p1Id, p2Id, p1Score, p2Score, type, scores = null) {
// --- 1. Fetch Player Data --- // --- 1. Fetch Player Data ---
const p1DB = getUser.get(p1Id); const p1DB = await userService.getUser(p1Id);
const p2DB = getUser.get(p2Id); const p2DB = await userService.getUser(p2Id);
if (!p1DB || !p2DB) { if (!p1DB || !p2DB) {
console.error(`Elo Handler: Could not find user data for ${p1Id} or ${p2Id}.`); console.error(`Elo Handler: Could not find user data for ${p1Id} or ${p2Id}.`);
return; return;
} }
let p1EloData = getUserElo.get({ id: p1Id }); let p1EloData = await gameService.getUserElo(p1Id);
let p2EloData = getUserElo.get({ id: p2Id }); let p2EloData = await gameService.getUserElo(p2Id);
// --- 2. Initialize Elo if it doesn't exist --- // --- 2. Initialize Elo if it doesn't exist ---
if (!p1EloData) { if (!p1EloData) {
await insertElos.run({ id: p1Id, elo: 1000 }); await gameService.insertElo(p1Id, 1000);
p1EloData = { id: p1Id, elo: 1000 }; p1EloData = { id: p1Id, elo: 1000 };
} }
if (!p2EloData) { if (!p2EloData) {
await insertElos.run({ id: p2Id, elo: 1000 }); await gameService.insertElo(p2Id, 1000);
p2EloData = { id: p2Id, elo: 1000 }; p2EloData = { id: p2Id, elo: 1000 };
} }
@@ -91,34 +92,34 @@ export async function eloHandler(p1Id, p2Id, p1Score, p2Score, type, scores = nu
} }
// --- 4. Update Database --- // --- 4. Update Database ---
updateElo.run({ id: p1Id, elo: finalP1Elo }); await gameService.updateElo(p1Id, finalP1Elo);
updateElo.run({ id: p2Id, elo: finalP2Elo }); await gameService.updateElo(p2Id, finalP2Elo);
if (scores) { if (scores) {
insertGame.run({ await gameService.insertGame({
id: `${p1Id}-${p2Id}-${Date.now()}`, id: `${p1Id}-${p2Id}-${Date.now()}`,
p1: p1Id, p1: p1Id,
p2: p2Id, p2: p2Id,
p1_score: scores.p1, p1Score: scores.p1,
p2_score: scores.p2, p2Score: scores.p2,
p1_elo: p1CurrentElo, p1Elo: p1CurrentElo,
p2_elo: p2CurrentElo, p2Elo: p2CurrentElo,
p1_new_elo: finalP1Elo, p1NewElo: finalP1Elo,
p2_new_elo: finalP2Elo, p2NewElo: finalP2Elo,
type: type, type: type,
timestamp: Date.now(), timestamp: Date.now(),
}); });
} else { } else {
insertGame.run({ await gameService.insertGame({
id: `${p1Id}-${p2Id}-${Date.now()}`, id: `${p1Id}-${p2Id}-${Date.now()}`,
p1: p1Id, p1: p1Id,
p2: p2Id, p2: p2Id,
p1_score: p1Score, p1Score: p1Score,
p2_score: p2Score, p2Score: p2Score,
p1_elo: p1CurrentElo, p1Elo: p1CurrentElo,
p2_elo: p2CurrentElo, p2Elo: p2CurrentElo,
p1_new_elo: finalP1Elo, p1NewElo: finalP1Elo,
p2_new_elo: finalP2Elo, p2NewElo: finalP2Elo,
type: type, type: type,
timestamp: Date.now(), timestamp: Date.now(),
}); });
@@ -141,11 +142,12 @@ export async function pokerEloHandler(room) {
if (playerIds.length < 2) return; // Not enough players to calculate Elo if (playerIds.length < 2) return; // Not enough players to calculate Elo
// Fetch all players' Elo data at once // Fetch all players' Elo data at once
const dbPlayers = playerIds.map((id) => { const dbPlayers = await Promise.all(playerIds.map(async (id) => {
const user = getUser.get(id); const user = await userService.getUser(id);
const elo = getUserElo.get({ id })?.elo || 1000; const eloData = await gameService.getUserElo(id);
const elo = eloData?.elo || 1000;
return { ...user, elo }; return { ...user, elo };
}); }));
const winnerIds = new Set(room.winners); const winnerIds = new Set(room.winners);
const playerCount = dbPlayers.length; const playerCount = dbPlayers.length;
@@ -153,7 +155,7 @@ export async function pokerEloHandler(room) {
const averageElo = dbPlayers.reduce((sum, p) => sum + p.elo, 0) / playerCount; const averageElo = dbPlayers.reduce((sum, p) => sum + p.elo, 0) / playerCount;
dbPlayers.forEach((player) => { for (const player of dbPlayers) {
// Expected score is the chance of winning against an "average" player from the field // Expected score is the chance of winning against an "average" player from the field
const expectedScore = 1 / (1 + Math.pow(10, (averageElo - player.elo) / 400)); const expectedScore = 1 / (1 + Math.pow(10, (averageElo - player.elo) / 400));
@@ -175,23 +177,23 @@ export async function pokerEloHandler(room) {
console.log( console.log(
`Elo Update (POKER) for ${player.globalName}: ${player.elo} -> ${newElo} (Δ: ${eloChange.toFixed(2)})`, `Elo Update (POKER) for ${player.globalName}: ${player.elo} -> ${newElo} (Δ: ${eloChange.toFixed(2)})`,
); );
updateElo.run({ id: player.id, elo: newElo }); await gameService.updateElo(player.id, newElo);
insertGame.run({ await gameService.insertGame({
id: `${player.id}-poker-${Date.now()}`, id: `${player.id}-poker-${Date.now()}`,
p1: player.id, p1: player.id,
p2: null, // No single opponent p2: null, // No single opponent
p1_score: actualScore, p1Score: actualScore,
p2_score: null, p2Score: null,
p1_elo: player.elo, p1Elo: player.elo,
p2_elo: Math.round(averageElo), // Log the average opponent Elo for context p2Elo: Math.round(averageElo), // Log the average opponent Elo for context
p1_new_elo: newElo, p1NewElo: newElo,
p2_new_elo: null, p2NewElo: null,
type: "POKER_ROUND", type: "POKER_ROUND",
timestamp: Date.now(), timestamp: Date.now(),
}); });
} else { } else {
console.error(`Error calculating new Elo for ${player.globalName}.`); console.error(`Error calculating new Elo for ${player.globalName}.`);
} }
}); }
} }

View File

@@ -1,15 +1,7 @@
import { import * as userService from "../services/user.service.js";
clearSOTDStats, import * as skinService from "../services/skin.service.js";
deleteSOTD, import * as logService from "../services/log.service.js";
getAllSkins, import * as solitaireService from "../services/solitaire.service.js";
getAllSOTDStats,
getUser,
insertGame,
insertLog,
insertSOTD,
pruneOldLogs,
updateUserCoins
} from "../database/index.js";
import { activeSlowmodes, activeSolitaireGames, messagesTimestamps, skins } from "./state.js"; import { activeSlowmodes, activeSolitaireGames, messagesTimestamps, skins } from "./state.js";
import { createDeck, createSeededRNG, deal, seededShuffle } from "./solitaire.js"; import { createDeck, createSeededRNG, deal, seededShuffle } from "./solitaire.js";
import { emitSolitaireUpdate } from "../server/socket.js"; import { emitSolitaireUpdate } from "../server/socket.js";
@@ -22,7 +14,7 @@ import { emitSolitaireUpdate } from "../server/socket.js";
*/ */
export async function channelPointsHandler(message) { export async function channelPointsHandler(message) {
const author = message.author; const author = message.author;
const authorDB = getUser.get(author.id); const authorDB = await userService.getUser(author.id);
if (!authorDB) { if (!authorDB) {
// User not in our database, do nothing. // User not in our database, do nothing.
@@ -53,21 +45,18 @@ export async function channelPointsHandler(message) {
const coinsToAdd = recentTimestamps.length === 10 ? 50 : 10; const coinsToAdd = recentTimestamps.length === 10 ? 50 : 10;
const newCoinTotal = authorDB.coins + coinsToAdd; const newCoinTotal = authorDB.coins + coinsToAdd;
updateUserCoins.run({ await userService.updateUserCoins(author.id, newCoinTotal);
id: author.id,
coins: newCoinTotal,
});
insertLog.run({ await logService.insertLog({
id: `${author.id}-${now}`, id: `${author.id}-${now}`,
user_id: author.id, userId: author.id,
action: "AUTO_COINS", action: "AUTO_COINS",
target_user_id: null, targetUserId: null,
coins_amount: coinsToAdd, coinsAmount: coinsToAdd,
user_new_amount: newCoinTotal, userNewAmount: newCoinTotal,
}); });
await pruneOldLogs(); await logService.pruneOldLogs();
return true; // Indicate that points were awarded return true; // Indicate that points were awarded
} }
@@ -116,8 +105,8 @@ export async function slowmodesHandler(message) {
* Used for testing and simulations. * Used for testing and simulations.
* @returns {string} The calculated random price as a string. * @returns {string} The calculated random price as a string.
*/ */
export function randomSkinPrice() { export async function randomSkinPrice() {
const dbSkins = getAllSkins.all(); const dbSkins = await skinService.getAllSkins();
if (dbSkins.length === 0) return "0.00"; if (dbSkins.length === 0) return "0.00";
const randomDbSkin = dbSkins[Math.floor(Math.random() * dbSkins.length)]; const randomDbSkin = dbSkins[Math.floor(Math.random() * dbSkins.length)];
@@ -144,30 +133,30 @@ export function randomSkinPrice() {
* Initializes the Solitaire of the Day. * Initializes the Solitaire of the Day.
* This function clears previous stats, awards the winner, and generates a new daily seed. * This function clears previous stats, awards the winner, and generates a new daily seed.
*/ */
export function initTodaysSOTD() { export async function initTodaysSOTD() {
console.log(`Initializing new Solitaire of the Day...`); console.log(`Initializing new Solitaire of the Day...`);
// 1. Award previous day's winner // 1. Award previous day's winner
const rankings = getAllSOTDStats.all(); const rankings = await solitaireService.getAllSOTDStats();
if (rankings.length > 0) { if (rankings.length > 0) {
const winnerId = rankings[0].user_id; const winnerId = rankings[0].userId;
const secondPlaceId = rankings[1] ? rankings[1].user_id : null; const secondPlaceId = rankings[1] ? rankings[1].userId : null;
const thirdPlaceId = rankings[2] ? rankings[2].user_id : null; const thirdPlaceId = rankings[2] ? rankings[2].userId : null;
const winnerUser = getUser.get(winnerId); const winnerUser = await userService.getUser(winnerId);
const secondPlaceUser = secondPlaceId ? getUser.get(secondPlaceId) : null; const secondPlaceUser = secondPlaceId ? await userService.getUser(secondPlaceId) : null;
const thirdPlaceUser = thirdPlaceId ? getUser.get(thirdPlaceId) : null; const thirdPlaceUser = thirdPlaceId ? await userService.getUser(thirdPlaceId) : null;
if (winnerUser) { if (winnerUser) {
const reward = 2500; const reward = 2500;
const newCoinTotal = winnerUser.coins + reward; const newCoinTotal = winnerUser.coins + reward;
updateUserCoins.run({ id: winnerId, coins: newCoinTotal }); await userService.updateUserCoins(winnerId, newCoinTotal);
insertLog.run({ await logService.insertLog({
id: `${winnerId}-sotd-win-${Date.now()}`, id: `${winnerId}-sotd-win-${Date.now()}`,
target_user_id: null, targetUserId: null,
user_id: winnerId, userId: winnerId,
action: "SOTD_FIRST_PLACE", action: "SOTD_FIRST_PLACE",
coins_amount: reward, coinsAmount: reward,
user_new_amount: newCoinTotal, userNewAmount: newCoinTotal,
}); });
console.log( console.log(
`${winnerUser.globalName || winnerUser.username} won the previous SOTD and received ${reward} coins.`, `${winnerUser.globalName || winnerUser.username} won the previous SOTD and received ${reward} coins.`,
@@ -176,14 +165,14 @@ export function initTodaysSOTD() {
if (secondPlaceUser) { if (secondPlaceUser) {
const reward = 1500; const reward = 1500;
const newCoinTotal = secondPlaceUser.coins + reward; const newCoinTotal = secondPlaceUser.coins + reward;
updateUserCoins.run({ id: secondPlaceId, coins: newCoinTotal }); await userService.updateUserCoins(secondPlaceId, newCoinTotal);
insertLog.run({ await logService.insertLog({
id: `${secondPlaceId}-sotd-second-${Date.now()}`, id: `${secondPlaceId}-sotd-second-${Date.now()}`,
target_user_id: null, targetUserId: null,
user_id: secondPlaceId, userId: secondPlaceId,
action: "SOTD_SECOND_PLACE", action: "SOTD_SECOND_PLACE",
coins_amount: reward, coinsAmount: reward,
user_new_amount: newCoinTotal, userNewAmount: newCoinTotal,
}); });
console.log( console.log(
`${secondPlaceUser.globalName || secondPlaceUser.username} got second place in the previous SOTD and received ${reward} coins.`, `${secondPlaceUser.globalName || secondPlaceUser.username} got second place in the previous SOTD and received ${reward} coins.`,
@@ -192,14 +181,14 @@ export function initTodaysSOTD() {
if (thirdPlaceUser) { if (thirdPlaceUser) {
const reward = 750; const reward = 750;
const newCoinTotal = thirdPlaceUser.coins + reward; const newCoinTotal = thirdPlaceUser.coins + reward;
updateUserCoins.run({ id: thirdPlaceId, coins: newCoinTotal }); await userService.updateUserCoins(thirdPlaceId, newCoinTotal);
insertLog.run({ await logService.insertLog({
id: `${thirdPlaceId}-sotd-third-${Date.now()}`, id: `${thirdPlaceId}-sotd-third-${Date.now()}`,
target_user_id: null, targetUserId: null,
user_id: thirdPlaceId, userId: thirdPlaceId,
action: "SOTD_THIRD_PLACE", action: "SOTD_THIRD_PLACE",
coins_amount: reward, coinsAmount: reward,
user_new_amount: newCoinTotal, userNewAmount: newCoinTotal,
}); });
console.log( console.log(
`${thirdPlaceUser.globalName || thirdPlaceUser.username} got third place in the previous SOTD and received ${reward} coins.`, `${thirdPlaceUser.globalName || thirdPlaceUser.username} got third place in the previous SOTD and received ${reward} coins.`,
@@ -221,9 +210,9 @@ export function initTodaysSOTD() {
// 3. Clear old stats and save the new game state to the database // 3. Clear old stats and save the new game state to the database
try { try {
clearSOTDStats.run(); await solitaireService.clearSOTDStats();
deleteSOTD.run(); await solitaireService.deleteSOTD();
insertSOTD.run({ await solitaireService.insertSOTD({
tableauPiles: JSON.stringify(todaysSOTD.tableauPiles), tableauPiles: JSON.stringify(todaysSOTD.tableauPiles),
foundationPiles: JSON.stringify(todaysSOTD.foundationPiles), foundationPiles: JSON.stringify(todaysSOTD.foundationPiles),
stockPile: JSON.stringify(todaysSOTD.stockPile), stockPile: JSON.stringify(todaysSOTD.stockPile),

5
src/prisma/client.js Normal file
View File

@@ -0,0 +1,5 @@
import { PrismaClient } from "@prisma/client";
const prisma = new PrismaClient();
export default prisma;

View File

@@ -37,6 +37,9 @@ app.post("/interactions", verifyKeyMiddleware(process.env.PUBLIC_KEY), async (re
await handleInteraction(req, res, client); await handleInteraction(req, res, client);
}); });
// Stripe webhook endpoint needs raw body for signature verification
app.use("/api/buy-coins", express.raw({ type: "application/json" }));
// JSON Body Parser Middleware // JSON Body Parser Middleware
app.use(express.json()); app.use(express.json());

File diff suppressed because it is too large Load Diff

View File

@@ -15,7 +15,8 @@ import {
} from "../../game/blackjack.js"; } from "../../game/blackjack.js";
// Optional: hook into your DB & Discord systems if available // Optional: hook into your DB & Discord systems if available
import { getUser, insertLog, updateUserCoins } from "../../database/index.js"; import * as userService from "../../services/user.service.js";
import * as logService from "../../services/log.service.js";
import { client } from "../../bot/client.js"; import { client } from "../../bot/client.js";
import { emitToast, emitUpdate, emitPlayerUpdate } from "../socket.js"; import { emitToast, emitUpdate, emitPlayerUpdate } from "../socket.js";
import { EmbedBuilder, time } from "discord.js"; import { EmbedBuilder, time } from "discord.js";
@@ -128,7 +129,7 @@ export function blackjackRoutes(io) {
if (room.players[userId]) return res.status(200).json({ message: "Already here" }); if (room.players[userId]) return res.status(200).json({ message: "Already here" });
const user = await client.users.fetch(userId); const user = await client.users.fetch(userId);
const bank = getUser.get(userId)?.coins ?? 0; const bank = (await userService.getUser(userId))?.coins ?? 0;
room.players[userId] = { room.players[userId] = {
id: userId, id: userId,
@@ -229,7 +230,7 @@ export function blackjackRoutes(io) {
} }
}); });
router.post("/bet", (req, res) => { router.post("/bet", async (req, res) => {
const { userId, amount } = req.body; const { userId, amount } = req.body;
const p = room.players[userId]; const p = room.players[userId];
if (!p) return res.status(404).json({ message: "not in room" }); if (!p) return res.status(404).json({ message: "not in room" });
@@ -239,17 +240,17 @@ export function blackjackRoutes(io) {
if (bet < room.minBet || bet > room.maxBet) return res.status(400).json({ message: "invalid-bet" }); if (bet < room.minBet || bet > room.maxBet) return res.status(400).json({ message: "invalid-bet" });
if (!room.settings.fakeMoney) { if (!room.settings.fakeMoney) {
const userDB = getUser.get(userId); const userDB = await userService.getUser(userId);
const coins = userDB?.coins ?? 0; const coins = userDB?.coins ?? 0;
if (coins < bet) return res.status(403).json({ message: "insufficient-funds" }); if (coins < bet) return res.status(403).json({ message: "insufficient-funds" });
updateUserCoins.run({ id: userId, coins: coins - bet }); await userService.updateUserCoins(userId, coins - bet);
insertLog.run({ await logService.insertLog({
id: `${userId}-blackjack-${Date.now()}`, id: `${userId}-blackjack-${Date.now()}`,
user_id: userId, userId: userId,
target_user_id: null, targetUserId: null,
action: "BLACKJACK_BET", action: "BLACKJACK_BET",
coins_amount: -bet, coinsAmount: -bet,
user_new_amount: coins - bet, userNewAmount: coins - bet,
}); });
p.bank = coins - bet; p.bank = coins - bet;
} }
@@ -261,7 +262,7 @@ export function blackjackRoutes(io) {
return res.status(200).json({ message: "bet-accepted" }); return res.status(200).json({ message: "bet-accepted" });
}); });
router.post("/action/:action", (req, res) => { router.post("/action/:action", async (req, res) => {
const { userId } = req.body; const { userId } = req.body;
const action = req.params.action; const action = req.params.action;
const p = room.players[userId]; const p = room.players[userId];
@@ -270,36 +271,36 @@ export function blackjackRoutes(io) {
// Handle extra coin lock for double // Handle extra coin lock for double
if (action === "double" && !room.settings.fakeMoney) { if (action === "double" && !room.settings.fakeMoney) {
const userDB = getUser.get(userId); const userDB = await userService.getUser(userId);
const coins = userDB?.coins ?? 0; const coins = userDB?.coins ?? 0;
const hand = p.hands[p.activeHand]; const hand = p.hands[p.activeHand];
if (coins < hand.bet) return res.status(403).json({ message: "insufficient-funds-for-double" }); if (coins < hand.bet) return res.status(403).json({ message: "insufficient-funds-for-double" });
updateUserCoins.run({ id: userId, coins: coins - hand.bet }); await userService.updateUserCoins(userId, coins - hand.bet);
insertLog.run({ await logService.insertLog({
id: `${userId}-blackjack-${Date.now()}`, id: `${userId}-blackjack-${Date.now()}`,
user_id: userId, userId: userId,
target_user_id: null, targetUserId: null,
action: "BLACKJACK_DOUBLE", action: "BLACKJACK_DOUBLE",
coins_amount: -hand.bet, coinsAmount: -hand.bet,
user_new_amount: coins - hand.bet, userNewAmount: coins - hand.bet,
}); });
p.bank = coins - hand.bet; p.bank = coins - hand.bet;
// effective bet size is handled in settlement via hand.doubled flag // effective bet size is handled in settlement via hand.doubled flag
} }
if (action === "split" && !room.settings.fakeMoney) { if (action === "split" && !room.settings.fakeMoney) {
const userDB = getUser.get(userId); const userDB = await userService.getUser(userId);
const coins = userDB?.coins ?? 0; const coins = userDB?.coins ?? 0;
const hand = p.hands[p.activeHand]; const hand = p.hands[p.activeHand];
if (coins < hand.bet) return res.status(403).json({ message: "insufficient-funds-for-split" }); if (coins < hand.bet) return res.status(403).json({ message: "insufficient-funds-for-split" });
updateUserCoins.run({ id: userId, coins: coins - hand.bet }); await userService.updateUserCoins(userId, coins - hand.bet);
insertLog.run({ await logService.insertLog({
id: `${userId}-blackjack-${Date.now()}`, id: `${userId}-blackjack-${Date.now()}`,
user_id: userId, userId: userId,
target_user_id: null, targetUserId: null,
action: "BLACKJACK_SPLIT", action: "BLACKJACK_SPLIT",
coins_amount: -hand.bet, coinsAmount: -hand.bet,
user_new_amount: coins - hand.bet, userNewAmount: coins - hand.bet,
}); });
p.bank = coins - hand.bet; p.bank = coins - hand.bet;
// effective bet size is handled in settlement via hand.doubled flag // effective bet size is handled in settlement via hand.doubled flag

View File

@@ -5,18 +5,10 @@ import express from "express";
// --- Utility and API Imports --- // --- Utility and API Imports ---
// --- Discord.js Builder Imports --- // --- Discord.js Builder Imports ---
import { ButtonStyle } from "discord.js"; import { ButtonStyle } from "discord.js";
import { import * as userService from "../../services/user.service.js";
getMarketOfferById, import * as skinService from "../../services/skin.service.js";
getMarketOffers, import * as logService from "../../services/log.service.js";
getMarketOffersBySkin, import * as marketService from "../../services/market.service.js";
getOfferBids,
getSkin,
getUser,
insertBid,
insertLog,
insertMarketOffer,
updateUserCoins,
} from "../../database/index.js";
import { emitMarketUpdate } from "../socket.js"; import { emitMarketUpdate } from "../socket.js";
import { handleNewMarketOffer, handleNewMarketOfferBid } from "../../utils/marketNotifs.js"; import { handleNewMarketOffer, handleNewMarketOfferBid } from "../../utils/marketNotifs.js";
@@ -32,25 +24,26 @@ const router = express.Router();
export function marketRoutes(client, io) { export function marketRoutes(client, io) {
router.get("/offers", async (req, res) => { router.get("/offers", async (req, res) => {
try { try {
const offers = getMarketOffers.all(); const offers = await marketService.getMarketOffers();
offers.forEach((offer) => { for (const offer of offers) {
offer.skin = getSkin.get(offer.skin_uuid); offer.skin = await skinService.getSkin(offer.skinUuid);
offer.seller = getUser.get(offer.seller_id); offer.seller = await userService.getUser(offer.sellerId);
offer.buyer = getUser.get(offer.buyer_id) || null; offer.buyer = offer.buyerId ? await userService.getUser(offer.buyerId) : null;
offer.bids = getOfferBids.all(offer.id) || {}; offer.bids = (await marketService.getOfferBids(offer.id)) || {};
offer.bids.forEach((bid) => { for (const bid of offer.bids) {
bid.bidder = getUser.get(bid.bidder_id); bid.bidder = await userService.getUser(bid.bidderId);
}); }
}); }
res.status(200).send({ offers }); res.status(200).send({ offers });
} catch (e) { } catch (e) {
console.log(e);
res.status(500).send({ error: e }); res.status(500).send({ error: e });
} }
}); });
router.get("/offers/:id", async (req, res) => { router.get("/offers/:id", async (req, res) => {
try { try {
const offer = getMarketOfferById.get(req.params.id); const offer = await marketService.getMarketOfferById(req.params.id);
if (offer) { if (offer) {
res.status(200).send({ offer }); res.status(200).send({ offer });
} else { } else {
@@ -63,7 +56,7 @@ export function marketRoutes(client, io) {
router.get("/offers/:id/bids", async (req, res) => { router.get("/offers/:id/bids", async (req, res) => {
try { try {
const bids = getOfferBids.get(req.params.id); const bids = await marketService.getOfferBids(req.params.id);
res.status(200).send({ bids }); res.status(200).send({ bids });
} catch (e) { } catch (e) {
res.status(500).send({ error: e }); res.status(500).send({ error: e });
@@ -74,13 +67,13 @@ export function marketRoutes(client, io) {
const { seller_id, skin_uuid, starting_price, delay, duration, timestamp } = req.body; const { seller_id, skin_uuid, starting_price, delay, duration, timestamp } = req.body;
const now = Date.now(); const now = Date.now();
try { try {
const skin = getSkin.get(skin_uuid); const skin = await skinService.getSkin(skin_uuid);
if (!skin) return res.status(404).send({ error: "Skin not found" }); if (!skin) return res.status(404).send({ error: "Skin not found" });
const seller = getUser.get(seller_id); const seller = await userService.getUser(seller_id);
if (!seller) return res.status(404).send({ error: "Seller not found" }); 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" }); if (skin.userId !== seller.id) return res.status(403).send({ error: "You do not own this skin" });
const existingOffers = getMarketOffersBySkin.all(skin.uuid); const existingOffers = await marketService.getMarketOffersBySkin(skin.uuid);
if ( if (
existingOffers.length > 0 && existingOffers.length > 0 &&
existingOffers.some((offer) => offer.status === "open" || offer.status === "pending") existingOffers.some((offer) => offer.status === "open" || offer.status === "pending")
@@ -92,15 +85,15 @@ export function marketRoutes(client, io) {
const closing_at = opening_at + duration; const closing_at = opening_at + duration;
const offerId = Date.now() + "-" + seller.id + "-" + skin.uuid; const offerId = Date.now() + "-" + seller.id + "-" + skin.uuid;
insertMarketOffer.run({ await marketService.insertMarketOffer({
id: offerId, id: offerId,
skin_uuid: skin.uuid, skinUuid: skin.uuid,
seller_id: seller.id, sellerId: seller.id,
starting_price: starting_price, startingPrice: starting_price,
buyout_price: null, buyoutPrice: null,
status: delay > 0 ? "pending" : "open", status: delay > 0 ? "pending" : "open",
opening_at: opening_at, openingAt: opening_at,
closing_at: closing_at, closingAt: closing_at,
}); });
await emitMarketUpdate(); await emitMarketUpdate();
await handleNewMarketOffer(offerId, client); await handleNewMarketOffer(offerId, client);
@@ -114,62 +107,62 @@ export function marketRoutes(client, io) {
router.post("/offers/:id/place-bid", async (req, res) => { router.post("/offers/:id/place-bid", async (req, res) => {
const { buyer_id, bid_amount, timestamp } = req.body; const { buyer_id, bid_amount, timestamp } = req.body;
try { try {
const offer = getMarketOfferById.get(req.params.id); const offer = await marketService.getMarketOfferById(req.params.id);
if (!offer) return res.status(404).send({ error: "Offer not found" }); if (!offer) return res.status(404).send({ error: "Offer not found" });
if (offer.closing_at < timestamp) return res.status(403).send({ error: "Bidding period has ended" }); if (offer.closingAt < timestamp) return res.status(403).send({ error: "Bidding period has ended" });
if (buyer_id === offer.seller_id) return res.status(403).send({ error: "You can't bid on your own offer" }); if (buyer_id === offer.sellerId) return res.status(403).send({ error: "You can't bid on your own offer" });
const offerBids = getOfferBids.all(offer.id); const offerBids = await marketService.getOfferBids(offer.id);
const lastBid = offerBids[0]; const lastBid = offerBids[0];
if (lastBid) { if (lastBid) {
if (lastBid?.bidder_id === buyer_id) if (lastBid?.bidderId === buyer_id)
return res.status(403).send({ error: "You are already the highest bidder" }); return res.status(403).send({ error: "You are already the highest bidder" });
if (bid_amount < lastBid?.offer_amount + 10) { if (bid_amount < lastBid?.offerAmount + 10) {
return res.status(403).send({ error: "Bid amount is below minimum" }); return res.status(403).send({ error: "Bid amount is below minimum" });
} }
} else { } else {
if (bid_amount < offer.starting_price + 10) { if (bid_amount < offer.startingPrice + 10) {
return res.status(403).send({ error: "Bid amount is below minimum" }); return res.status(403).send({ error: "Bid amount is below minimum" });
} }
} }
const bidder = getUser.get(buyer_id); const bidder = await userService.getUser(buyer_id);
if (!bidder) return res.status(404).send({ error: "Bidder not found" }); if (!bidder) return res.status(404).send({ error: "Bidder not found" });
if (bidder.coins < bid_amount) if (bidder.coins < bid_amount)
return res.status(403).send({ error: "You do not have enough coins to place this bid" }); return res.status(403).send({ error: "You do not have enough coins to place this bid" });
const bidId = Date.now() + "-" + buyer_id + "-" + offer.id; const bidId = Date.now() + "-" + buyer_id + "-" + offer.id;
insertBid.run({ await marketService.insertBid({
id: bidId, id: bidId,
bidder_id: buyer_id, bidderId: buyer_id,
market_offer_id: offer.id, marketOfferId: offer.id,
offer_amount: bid_amount, offerAmount: bid_amount,
}); });
const newCoinsAmount = bidder.coins - bid_amount; const newCoinsAmount = bidder.coins - bid_amount;
updateUserCoins.run({ id: buyer_id, coins: newCoinsAmount }); await userService.updateUserCoins(buyer_id, newCoinsAmount);
insertLog.run({ await logService.insertLog({
id: `${buyer_id}-bid-${offer.id}-${Date.now()}`, id: `${buyer_id}-bid-${offer.id}-${Date.now()}`,
user_id: buyer_id, userId: buyer_id,
action: "BID_PLACED", action: "BID_PLACED",
target_user_id: null, targetUserId: null,
coins_amount: bid_amount, coinsAmount: bid_amount,
user_new_amount: newCoinsAmount, userNewAmount: newCoinsAmount,
}); });
// Refund the previous highest bidder // Refund the previous highest bidder
if (lastBid) { if (lastBid) {
const previousBidder = getUser.get(lastBid.bidder_id); const previousBidder = await userService.getUser(lastBid.bidderId);
const refundedCoinsAmount = previousBidder.coins + lastBid.offer_amount; const refundedCoinsAmount = previousBidder.coins + lastBid.offerAmount;
updateUserCoins.run({ id: previousBidder.id, coins: refundedCoinsAmount }); await userService.updateUserCoins(previousBidder.id, refundedCoinsAmount);
insertLog.run({ await logService.insertLog({
id: `${previousBidder.id}-bid-refund-${offer.id}-${Date.now()}`, id: `${previousBidder.id}-bid-refund-${offer.id}-${Date.now()}`,
user_id: previousBidder.id, userId: previousBidder.id,
action: "BID_REFUNDED", action: "BID_REFUNDED",
target_user_id: null, targetUserId: null,
coins_amount: lastBid.offer_amount, coinsAmount: lastBid.offerAmount,
user_new_amount: refundedCoinsAmount, userNewAmount: refundedCoinsAmount,
}); });
} }

View File

@@ -2,7 +2,8 @@ import express from "express";
import { v4 as uuidv4 } from "uuid"; import { v4 as uuidv4 } from "uuid";
import { monkePaths } from "../../game/state.js"; import { monkePaths } from "../../game/state.js";
import { socketEmit } from "../socket.js"; import { socketEmit } from "../socket.js";
import { getUser, updateUserCoins, insertLog } from "../../database/index.js"; import * as userService from "../../services/user.service.js";
import * as logService from "../../services/log.service.js";
import { init } from "openai/_shims/index.mjs"; import { init } from "openai/_shims/index.mjs";
const router = express.Router(); const router = express.Router();
@@ -16,11 +17,11 @@ const router = express.Router();
export function monkeRoutes(client, io) { export function monkeRoutes(client, io) {
// --- Router Management Endpoints // --- Router Management Endpoints
router.get("/:userId", (req, res) => { router.get("/:userId", async (req, res) => {
const { userId } = req.params; const { userId } = req.params;
if (!userId) return res.status(400).json({ error: "User ID is required" }); if (!userId) return res.status(400).json({ error: "User ID is required" });
const user = getUser.get(userId); const user = await userService.getUser(userId);
if (!user) return res.status(404).json({ error: "User not found" }); if (!user) return res.status(404).json({ error: "User not found" });
const userGamePath = monkePaths[userId] || null; const userGamePath = monkePaths[userId] || null;
if (!userGamePath) return res.status(404).json({ error: "No active game found for this user" }); if (!userGamePath) return res.status(404).json({ error: "No active game found for this user" });
@@ -28,29 +29,26 @@ export function monkeRoutes(client, io) {
return res.status(200).json({ userGamePath }); return res.status(200).json({ userGamePath });
}); });
router.post("/:userId/start", (req, res) => { router.post("/:userId/start", async (req, res) => {
const { userId } = req.params; const { userId } = req.params;
const { initialBet } = req.body; const { initialBet } = req.body;
if (!userId) return res.status(400).json({ error: "User ID is required" }); if (!userId) return res.status(400).json({ error: "User ID is required" });
const user = getUser.get(userId); const user = await userService.getUser(userId);
if (!user) return res.status(404).json({ error: "User not found" }); if (!user) return res.status(404).json({ error: "User not found" });
if (!initialBet) return res.status(400).json({ error: "Initial bet is required" }); if (!initialBet) return res.status(400).json({ error: "Initial bet is required" });
if (initialBet > user.coins) return res.status(400).json({ error: "Insufficient coins for the initial bet" }); if (initialBet > user.coins) return res.status(400).json({ error: "Insufficient coins for the initial bet" });
try { try {
const newCoins = user.coins - initialBet; const newCoins = user.coins - initialBet;
updateUserCoins.run({ await userService.updateUserCoins(userId, newCoins);
id: userId, await logService.insertLog({
coins: newCoins,
});
insertLog.run({
id: `${userId}-monke-bet-${Date.now()}`, id: `${userId}-monke-bet-${Date.now()}`,
user_id: userId, userId: userId,
target_user_id: null, targetUserId: null,
action: "MONKE_BET", action: "MONKE_BET",
coins_amount: -initialBet, coinsAmount: -initialBet,
user_new_amount: newCoins, userNewAmount: newCoins,
}); });
} catch (error) { } catch (error) {
return res.status(500).json({ error: "Failed to update user coins" }); return res.status(500).json({ error: "Failed to update user coins" });
@@ -61,12 +59,12 @@ export function monkeRoutes(client, io) {
return res.status(200).json({ message: "Monke game started", userGamePath: monkePaths[userId] }); return res.status(200).json({ message: "Monke game started", userGamePath: monkePaths[userId] });
}); });
router.post("/:userId/play", (req, res) => { router.post("/:userId/play", async (req, res) => {
const { userId } = req.params; const { userId } = req.params;
const { choice, step } = req.body; const { choice, step } = req.body;
if (!userId) return res.status(400).json({ error: "User ID is required" }); if (!userId) return res.status(400).json({ error: "User ID is required" });
const user = getUser.get(userId); const user = await userService.getUser(userId);
if (!user) return res.status(404).json({ error: "User not found" }); if (!user) return res.status(404).json({ error: "User not found" });
if (!monkePaths[userId]) return res.status(400).json({ error: "No active game found for this user" }); if (!monkePaths[userId]) return res.status(400).json({ error: "No active game found for this user" });
@@ -97,10 +95,10 @@ export function monkeRoutes(client, io) {
} }
}); });
router.post("/:userId/stop", (req, res) => { router.post("/:userId/stop", async (req, res) => {
const { userId } = req.params; const { userId } = req.params;
if (!userId) return res.status(400).json({ error: "User ID is required" }); if (!userId) return res.status(400).json({ error: "User ID is required" });
const user = getUser.get(userId); const user = await userService.getUser(userId);
if (!user) return res.status(404).json({ error: "User not found" }); if (!user) return res.status(404).json({ error: "User not found" });
if (!monkePaths[userId]) return res.status(400).json({ error: "No active game found for this user" }); if (!monkePaths[userId]) return res.status(400).json({ error: "No active game found for this user" });
const userGamePath = monkePaths[userId]; const userGamePath = monkePaths[userId];
@@ -112,17 +110,14 @@ export function monkeRoutes(client, io) {
const newCoins = coins + extractValue; const newCoins = coins + extractValue;
try { try {
updateUserCoins.run({ await userService.updateUserCoins(userId, newCoins);
id: userId, await logService.insertLog({
coins: newCoins,
});
insertLog.run({
id: `${userId}-monke-withdraw-${Date.now()}`, id: `${userId}-monke-withdraw-${Date.now()}`,
user_id: userId, userId: userId,
target_user_id: null, targetUserId: null,
action: "MONKE_WITHDRAW", action: "MONKE_WITHDRAW",
coins_amount: extractValue, coinsAmount: extractValue,
user_new_amount: newCoins, userNewAmount: newCoins,
}); });
return res.status(200).json({ message: "Game stopped", userGamePath }); return res.status(200).json({ message: "Game stopped", userGamePath });

View File

@@ -10,7 +10,8 @@ import {
getNextActivePlayer, getNextActivePlayer,
initialShuffledCards, initialShuffledCards,
} from "../../game/poker.js"; } from "../../game/poker.js";
import { getUser, insertLog, updateUserCoins } from "../../database/index.js"; import * as userService from "../../services/user.service.js";
import * as logService from "../../services/log.service.js";
import { sleep } from "openai/core"; import { sleep } from "openai/core";
import { client } from "../../bot/client.js"; import { client } from "../../bot/client.js";
import { emitPokerToast, emitPokerUpdate } from "../socket.js"; import { emitPokerToast, emitPokerUpdate } from "../socket.js";
@@ -131,7 +132,7 @@ export function pokerRoutes(client, io) {
if (Object.values(pokerRooms).some((r) => r.players[userId] || r.queue[userId])) { if (Object.values(pokerRooms).some((r) => r.players[userId] || r.queue[userId])) {
return res.status(403).json({ message: "You are already in a room or queue." }); return res.status(403).json({ message: "You are already in a room or queue." });
} }
if (!pokerRooms[roomId].fakeMoney && pokerRooms[roomId].minBet > (getUser.get(userId)?.coins ?? 0)) { if (!pokerRooms[roomId].fakeMoney && pokerRooms[roomId].minBet > ((await userService.getUser(userId))?.coins ?? 0)) {
return res.status(403).json({ message: "You do not have enough coins to join this room." }); return res.status(403).json({ message: "You do not have enough coins to join this room." });
} }
@@ -147,19 +148,16 @@ export function pokerRoutes(client, io) {
} }
if (!room.fakeMoney) { if (!room.fakeMoney) {
const userDB = getUser.get(playerId); const userDB = await userService.getUser(playerId);
if (userDB) { if (userDB) {
updateUserCoins.run({ await userService.updateUserCoins(playerId, userDB.coins - room.minBet);
id: playerId, await logService.insertLog({
coins: userDB.coins - room.minBet,
});
insertLog.run({
id: `${playerId}-poker-${Date.now()}`, id: `${playerId}-poker-${Date.now()}`,
user_id: playerId, userId: playerId,
target_user_id: null, targetUserId: null,
action: "POKER_JOIN", action: "POKER_JOIN",
coins_amount: -room.minBet, coinsAmount: -room.minBet,
user_new_amount: userDB.coins - room.minBet, userNewAmount: userDB.coins - room.minBet,
}); });
} }
} }
@@ -199,7 +197,7 @@ export function pokerRoutes(client, io) {
} }
try { try {
updatePlayerCoins( await updatePlayerCoins(
pokerRooms[roomId].players[userId], pokerRooms[roomId].players[userId],
pokerRooms[roomId].players[userId].bank, pokerRooms[roomId].players[userId].bank,
pokerRooms[roomId].fakeMoney, pokerRooms[roomId].fakeMoney,
@@ -239,7 +237,7 @@ export function pokerRoutes(client, io) {
} }
try { try {
updatePlayerCoins( await updatePlayerCoins(
pokerRooms[roomId].players[userId], pokerRooms[roomId].players[userId],
pokerRooms[roomId].players[userId].bank, pokerRooms[roomId].players[userId].bank,
pokerRooms[roomId].fakeMoney, pokerRooms[roomId].fakeMoney,
@@ -359,7 +357,7 @@ export function pokerRoutes(client, io) {
async function joinRoom(roomId, userId, io) { async function joinRoom(roomId, userId, io) {
const user = await client.users.fetch(userId); const user = await client.users.fetch(userId);
const userDB = getUser.get(userId); const userDB = await userService.getUser(userId);
const room = pokerRooms[roomId]; const room = pokerRooms[roomId];
const playerObject = { const playerObject = {
@@ -380,14 +378,14 @@ async function joinRoom(roomId, userId, io) {
} else { } else {
room.players[userId] = playerObject; room.players[userId] = playerObject;
if (!room.fakeMoney) { if (!room.fakeMoney) {
updateUserCoins.run({ id: userId, coins: userDB.coins - room.minBet }); await userService.updateUserCoins(userId, userDB.coins - room.minBet);
insertLog.run({ await logService.insertLog({
id: `${userId}-poker-${Date.now()}`, id: `${userId}-poker-${Date.now()}`,
user_id: userId, userId: userId,
target_user_id: null, targetUserId: null,
action: "POKER_JOIN", action: "POKER_JOIN",
coins_amount: -room.minBet, coinsAmount: -room.minBet,
user_new_amount: userDB.coins - room.minBet, userNewAmount: userDB.coins - room.minBet,
}); });
} }
} }
@@ -539,29 +537,29 @@ function updatePlayerHandSolves(room) {
} }
} }
function updatePlayerCoins(player, amount, isFake) { async function updatePlayerCoins(player, amount, isFake) {
if (isFake) return; if (isFake) return;
const user = getUser.get(player.id); const user = await userService.getUser(player.id);
if (!user) return; if (!user) return;
const userDB = getUser.get(player.id); const userDB = await userService.getUser(player.id);
updateUserCoins.run({ id: player.id, coins: userDB.coins + amount }); await userService.updateUserCoins(player.id, userDB.coins + amount);
insertLog.run({ await logService.insertLog({
id: `${player.id}-poker-${Date.now()}`, id: `${player.id}-poker-${Date.now()}`,
user_id: player.id, userId: player.id,
target_user_id: null, targetUserId: null,
action: `POKER_${amount > 0 ? "WIN" : "LOSE"}`, action: `POKER_${amount > 0 ? "WIN" : "LOSE"}`,
coins_amount: amount, coinsAmount: amount,
user_new_amount: userDB.coins + amount, userNewAmount: userDB.coins + amount,
}); });
} }
async function clearAfkPlayers(room) { async function clearAfkPlayers(room) {
Object.keys(room.afk).forEach((playerId) => { for (const playerId of Object.keys(room.afk)) {
if (room.players[playerId]) { if (room.players[playerId]) {
updatePlayerCoins(room.players[playerId], room.players[playerId].bank, room.fakeMoney); await updatePlayerCoins(room.players[playerId], room.players[playerId].bank, room.fakeMoney);
delete room.players[playerId]; delete room.players[playerId];
} }
}); }
room.afk = {}; room.afk = {};
} }

View File

@@ -19,16 +19,9 @@ import {
// --- Game State & Database Imports --- // --- Game State & Database Imports ---
import { activeSolitaireGames } from "../../game/state.js"; import { activeSolitaireGames } from "../../game/state.js";
import { import * as userService from "../../services/user.service.js";
getSOTD, import * as logService from "../../services/log.service.js";
getUser, import * as solitaireService from "../../services/solitaire.service.js";
insertSOTDStats,
deleteUserSOTDStats,
getUserSOTDStats,
updateUserCoins,
insertLog,
getAllSOTDStats,
} from "../../database/index.js";
import { socketEmit } from "../socket.js"; import { socketEmit } from "../socket.js";
// Create a new router instance // Create a new router instance
@@ -85,7 +78,7 @@ export function solitaireRoutes(client, io) {
res.json({ success: true, gameState }); res.json({ success: true, gameState });
}); });
router.post("/start/sotd", (req, res) => { router.post("/start/sotd", async (req, res) => {
const { userId } = req.body; const { userId } = req.body;
/*if (!userId || !getUser.get(userId)) { /*if (!userId || !getUser.get(userId)) {
return res.status(404).json({ error: 'User not found.' }); return res.status(404).json({ error: 'User not found.' });
@@ -98,7 +91,7 @@ export function solitaireRoutes(client, io) {
}); });
} }
const sotd = getSOTD.get(); const sotd = await solitaireService.getSOTD();
if (!sotd) { if (!sotd) {
return res.status(500).json({ error: "Solitaire of the Day is not configured." }); return res.status(500).json({ error: "Solitaire of the Day is not configured." });
} }
@@ -126,9 +119,9 @@ export function solitaireRoutes(client, io) {
// --- Game State & Action Endpoints --- // --- Game State & Action Endpoints ---
router.get("/sotd/rankings", (req, res) => { router.get("/sotd/rankings", async (req, res) => {
try { try {
const rankings = getAllSOTDStats.all(); const rankings = await solitaireService.getAllSOTDStats();
res.json({ rankings }); res.json({ rankings });
} catch (e) { } catch (e) {
res.status(500).json({ error: "Failed to fetch SOTD rankings." }); res.status(500).json({ error: "Failed to fetch SOTD rankings." });
@@ -237,20 +230,20 @@ function updateGameStats(gameState, actionType, moveData = {}) {
/** Handles the logic when a game is won. */ /** Handles the logic when a game is won. */
async function handleWin(userId, gameState, io) { async function handleWin(userId, gameState, io) {
const currentUser = getUser.get(userId); const currentUser = await userService.getUser(userId);
if (!currentUser) return; if (!currentUser) return;
if (gameState.hardMode) { if (gameState.hardMode) {
const bonus = 100; const bonus = 100;
const newCoins = currentUser.coins + bonus; const newCoins = currentUser.coins + bonus;
updateUserCoins.run({ id: userId, coins: newCoins }); await userService.updateUserCoins(userId, newCoins);
insertLog.run({ await logService.insertLog({
id: `${userId}-hardmode-solitaire-${Date.now()}`, id: `${userId}-hardmode-solitaire-${Date.now()}`,
user_id: userId, userId: userId,
action: "HARDMODE_SOLITAIRE_WIN", action: "HARDMODE_SOLITAIRE_WIN",
target_user_id: null, targetUserId: null,
coins_amount: bonus, coinsAmount: bonus,
user_new_amount: newCoins, userNewAmount: newCoins,
}); });
await socketEmit("data-updated", { table: "users" }); await socketEmit("data-updated", { table: "users" });
} }
@@ -260,20 +253,20 @@ async function handleWin(userId, gameState, io) {
gameState.endTime = Date.now(); gameState.endTime = Date.now();
const timeTaken = gameState.endTime - gameState.startTime; const timeTaken = gameState.endTime - gameState.startTime;
const existingStats = getUserSOTDStats.get(userId); const existingStats = await solitaireService.getUserSOTDStats(userId);
if (!existingStats) { if (!existingStats) {
// First time completing the SOTD, grant bonus coins // First time completing the SOTD, grant bonus coins
const bonus = 1000; const bonus = 1000;
const newCoins = currentUser.coins + bonus; const newCoins = currentUser.coins + bonus;
updateUserCoins.run({ id: userId, coins: newCoins }); await userService.updateUserCoins(userId, newCoins);
insertLog.run({ await logService.insertLog({
id: `${userId}-sotd-complete-${Date.now()}`, id: `${userId}-sotd-complete-${Date.now()}`,
user_id: userId, userId: userId,
action: "SOTD_WIN", action: "SOTD_WIN",
target_user_id: null, targetUserId: null,
coins_amount: bonus, coinsAmount: bonus,
user_new_amount: newCoins, userNewAmount: newCoins,
}); });
await socketEmit("data-updated", { table: "users" }); await socketEmit("data-updated", { table: "users" });
} }
@@ -288,10 +281,10 @@ async function handleWin(userId, gameState, io) {
timeTaken < existingStats.time); timeTaken < existingStats.time);
if (isNewBest) { if (isNewBest) {
deleteUserSOTDStats.run(userId); await solitaireService.deleteUserSOTDStats(userId);
insertSOTDStats.run({ await solitaireService.insertSOTDStats({
id: userId, id: userId,
user_id: userId, userId: userId,
time: timeTaken, time: timeTaken,
moves: gameState.moves, moves: gameState.moves,
score: gameState.score, score: gameState.score,

View File

@@ -0,0 +1,49 @@
import prisma from "../prisma/client.js";
export async function getUserElo(id) {
return prisma.elo.findUnique({ where: { id } });
}
export async function insertElo(id, elo) {
return prisma.elo.create({ data: { id, elo } });
}
export async function updateElo(id, elo) {
return prisma.elo.update({ where: { id }, data: { elo } });
}
export async function getUsersByElo() {
const users = await prisma.user.findMany({
include: { elo: true },
orderBy: { elo: { elo: "desc" } },
});
return users
.filter((u) => u.elo)
.map((u) => ({ ...u, elo: u.elo?.elo ?? null }));
}
function toGame(game) {
return { ...game, timestamp: game.timestamp != null ? game.timestamp.getTime() : null };
}
export async function insertGame(data) {
return prisma.game.create({
data: {
...data,
timestamp: data.timestamp != null ? new Date(data.timestamp) : null,
},
});
}
export async function getGames() {
const games = await prisma.game.findMany();
return games.map(toGame);
}
export async function getUserGames(userId) {
const games = await prisma.game.findMany({
where: { OR: [{ p1: userId }, { p2: userId }] },
orderBy: { timestamp: "asc" },
});
return games.map(toGame);
}

View File

@@ -0,0 +1,33 @@
import prisma from "../prisma/client.js";
export async function insertLog(data) {
return prisma.log.create({ data });
}
export async function getLogs() {
return prisma.log.findMany();
}
export async function getUserLogs(userId) {
return prisma.log.findMany({ where: { userId } });
}
export async function pruneOldLogs() {
const limit = parseInt(process.env.LOGS_BY_USER);
const usersWithExcess = await prisma.$queryRawUnsafe(
`SELECT user_id as userId FROM logs GROUP BY user_id HAVING COUNT(*) > ?`,
limit,
);
for (const { userId } of usersWithExcess) {
await prisma.$executeRawUnsafe(
`DELETE FROM logs WHERE id IN (
SELECT id FROM (
SELECT id, ROW_NUMBER() OVER (ORDER BY created_at DESC) AS rn
FROM logs WHERE user_id = ?
) WHERE rn > ?
)`,
userId,
limit,
);
}
}

View File

@@ -0,0 +1,107 @@
import prisma from "../prisma/client.js";
function toOffer(offer) {
return { ...offer, openingAt: offer.openingAt.getTime(), closingAt: offer.closingAt.getTime() };
}
export async function getMarketOffers() {
const offers = await prisma.marketOffer.findMany({ orderBy: { postedAt: "desc" } });
return offers.map(toOffer);
}
export async function getMarketOfferById(id) {
const offer = await prisma.marketOffer.findUnique({
where: { id },
include: {
skin: { select: { displayName: true, displayIcon: true } },
seller: { select: { username: true, globalName: true } },
buyer: { select: { username: true, globalName: true } },
},
});
if (!offer) return null;
// Flatten to match the old query shape
return toOffer({
...offer,
skinName: offer.skin?.displayName,
skinIcon: offer.skin?.displayIcon,
sellerName: offer.seller?.username,
sellerGlobalName: offer.seller?.globalName,
buyerName: offer.buyer?.username ?? null,
buyerGlobalName: offer.buyer?.globalName ?? null,
});
}
export async function getMarketOffersBySkin(skinUuid) {
const offers = await prisma.marketOffer.findMany({
where: { skinUuid },
include: {
skin: { select: { displayName: true, displayIcon: true } },
seller: { select: { username: true, globalName: true } },
buyer: { select: { username: true, globalName: true } },
},
});
return offers.map((offer) => toOffer({
...offer,
skinName: offer.skin?.displayName,
skinIcon: offer.skin?.displayIcon,
sellerName: offer.seller?.username,
sellerGlobalName: offer.seller?.globalName,
buyerName: offer.buyer?.username ?? null,
buyerGlobalName: offer.buyer?.globalName ?? null,
}));
}
export async function insertMarketOffer(data) {
return prisma.marketOffer.create({
data: {
...data,
openingAt: new Date(data.openingAt),
closingAt: new Date(data.closingAt),
},
});
}
export async function updateMarketOffer(data) {
const { id, ...rest } = data;
return prisma.marketOffer.update({ where: { id }, data: rest });
}
export async function deleteMarketOffer(id) {
return prisma.marketOffer.delete({ where: { id } });
}
// --- Bids ---
export async function getBids() {
const bids = await prisma.bid.findMany({
include: { bidder: { select: { username: true, globalName: true } } },
orderBy: [{ offerAmount: "desc" }, { offeredAt: "asc" }],
});
return bids.map((bid) => ({
...bid,
bidderName: bid.bidder?.username,
bidderGlobalName: bid.bidder?.globalName,
}));
}
export async function getBidById(id) {
return prisma.bid.findUnique({ where: { id } });
}
export async function getOfferBids(marketOfferId) {
const bids = await prisma.bid.findMany({
where: { marketOfferId },
orderBy: [{ offerAmount: "desc" }, { offeredAt: "asc" }],
});
return bids.map((bid) => ({
...bid,
}));
}
export async function insertBid(data) {
return prisma.bid.create({ data });
}
export async function deleteBid(id) {
return prisma.bid.delete({ where: { id } });
}

View File

@@ -0,0 +1,59 @@
import prisma from "../prisma/client.js";
export async function getSkin(uuid) {
return prisma.skin.findUnique({ where: { uuid } });
}
export async function getAllSkins() {
return prisma.skin.findMany({ orderBy: { maxPrice: "desc" } });
}
export async function getAllAvailableSkins() {
return prisma.skin.findMany({ where: { userId: null } });
}
export async function getUserInventory(userId) {
return prisma.skin.findMany({
where: { userId },
orderBy: { currentPrice: "desc" },
});
}
export async function getTopSkins() {
return prisma.skin.findMany({ orderBy: { maxPrice: "desc" }, take: 10 });
}
export async function insertSkin(data) {
return prisma.skin.create({ data });
}
export async function updateSkin(data) {
const { uuid, ...rest } = data;
return prisma.skin.update({ where: { uuid }, data: rest });
}
export async function hardUpdateSkin(data) {
const { uuid, ...rest } = data;
return prisma.skin.update({ where: { uuid }, data: rest });
}
export async function insertManySkins(skins) {
return prisma.$transaction(
skins.map((skin) =>
prisma.skin.upsert({
where: { uuid: skin.uuid },
update: {},
create: skin,
}),
),
);
}
export async function updateManySkins(skins) {
return prisma.$transaction(
skins.map((skin) => {
const { uuid, ...data } = skin;
return prisma.skin.update({ where: { uuid }, data });
}),
);
}

View File

@@ -0,0 +1,40 @@
import prisma from "../prisma/client.js";
export async function getSOTD() {
return prisma.sotd.findUnique({ where: { id: 0 } });
}
export async function insertSOTD(data) {
return prisma.sotd.create({ data: { id: 0, ...data } });
}
export async function deleteSOTD() {
return prisma.sotd.delete({ where: { id: 0 } }).catch(() => {});
}
export async function getAllSOTDStats() {
const stats = await prisma.sotdStat.findMany({
include: { user: { select: { globalName: true } } },
orderBy: [{ score: "desc" }, { moves: "asc" }, { time: "asc" }],
});
return stats.map((s) => ({
...s,
globalName: s.user?.globalName,
}));
}
export async function getUserSOTDStats(userId) {
return prisma.sotdStat.findFirst({ where: { userId } });
}
export async function insertSOTDStats(data) {
return prisma.sotdStat.create({ data });
}
export async function clearSOTDStats() {
return prisma.sotdStat.deleteMany();
}
export async function deleteUserSOTDStats(userId) {
return prisma.sotdStat.deleteMany({ where: { userId } });
}

View File

@@ -0,0 +1,20 @@
import prisma from "../prisma/client.js";
export async function insertTransaction(data) {
return prisma.transaction.create({ data });
}
export async function getTransactionBySessionId(sessionId) {
return prisma.transaction.findUnique({ where: { sessionId } });
}
export async function getAllTransactions() {
return prisma.transaction.findMany({ orderBy: { createdAt: "desc" } });
}
export async function getUserTransactions(userId) {
return prisma.transaction.findMany({
where: { userId },
orderBy: { createdAt: "desc" },
});
}

View File

@@ -0,0 +1,73 @@
import prisma from "../prisma/client.js";
export async function getUser(id) {
const user = await prisma.user.findUnique({
where: { id },
include: { elo: true },
});
if (!user) return null;
return { ...user, elo: user.elo?.elo ?? null };
}
export async function getAllUsers() {
const users = await prisma.user.findMany({
include: { elo: true },
orderBy: { coins: "desc" },
});
return users.map((u) => ({ ...u, elo: u.elo?.elo ?? null }));
}
export async function getAllAkhys() {
const users = await prisma.user.findMany({
where: { isAkhy: 1 },
include: { elo: true },
orderBy: { coins: "desc" },
});
return users.map((u) => ({ ...u, elo: u.elo?.elo ?? null }));
}
export async function insertUser(data) {
return prisma.user.create({ data });
}
export async function updateUser(data) {
const { id, ...rest } = data;
return prisma.user.update({ where: { id }, data: rest });
}
export async function updateUserCoins(id, coins) {
return prisma.user.update({ where: { id }, data: { coins } });
}
export async function updateUserAvatar(id, avatarUrl) {
return prisma.user.update({ where: { id }, data: { avatarUrl } });
}
export async function queryDailyReward(id) {
return prisma.user.update({ where: { id }, data: { dailyQueried: 1 } });
}
export async function resetDailyReward() {
return prisma.user.updateMany({ data: { dailyQueried: 0 } });
}
export async function insertManyUsers(users) {
return prisma.$transaction(
users.map((user) =>
prisma.user.upsert({
where: { id: user.id },
update: {},
create: user,
}),
),
);
}
export async function updateManyUsers(users) {
return prisma.$transaction(
users.map((user) => {
const { id, elo, ...data } = user;
return prisma.user.update({ where: { id }, data });
}),
);
}

View File

@@ -1,4 +1,4 @@
import { getAllAvailableSkins, getSkin } from "../database/index.js"; import * as skinService from "../services/skin.service.js";
import { skins } from "../game/state.js"; import { skins } from "../game/state.js";
import { isChampionsSkin } from "./index.js"; import { isChampionsSkin } from "./index.js";
@@ -6,16 +6,15 @@ export async function drawCaseContent(caseType = "standard", poolSize = 100) {
if (caseType === "esport") { if (caseType === "esport") {
// Esport case: return all esport skins // Esport case: return all esport skins
try { try {
const dbSkins = getAllAvailableSkins.all(); const dbSkins = await skinService.getAllAvailableSkins();
const esportSkins = skins const esportSkins = [];
.filter((s) => dbSkins.find((dbSkin) => dbSkin.displayName.includes("Classic (VCT") && dbSkin.uuid === s.uuid)) for (const s of skins.filter((s) => dbSkins.find((dbSkin) => dbSkin.displayName.includes("Classic (VCT") && dbSkin.uuid === s.uuid))) {
.map((s) => { const dbSkin = await skinService.getSkin(s.uuid);
const dbSkin = getSkin.get(s.uuid); esportSkins.push({
return { ...s,
...s, // Shallow copy to avoid mutating the imported 'skins' object tierColor: dbSkin?.tierColor,
tierColor: dbSkin?.tierColor,
};
}); });
}
return esportSkins; return esportSkins;
} catch (e) { } catch (e) {
console.log(e); console.log(e);
@@ -55,8 +54,8 @@ export async function drawCaseContent(caseType = "standard", poolSize = 100) {
} }
try { try {
const dbSkins = getAllAvailableSkins.all(); const dbSkins = await skinService.getAllAvailableSkins();
const weightedPool = skins const filtered = skins
.filter((s) => dbSkins.find((dbSkin) => dbSkin.uuid === s.uuid)) .filter((s) => dbSkins.find((dbSkin) => dbSkin.uuid === s.uuid))
.filter((s) => { .filter((s) => {
if (caseType === "ultra") { if (caseType === "ultra") {
@@ -71,16 +70,19 @@ export async function drawCaseContent(caseType = "standard", poolSize = 100) {
} else { } else {
return isChampionsSkin(s.displayName) === false; return isChampionsSkin(s.displayName) === false;
} }
}) });
.map((s) => { const weightedPool = [];
const dbSkin = getSkin.get(s.uuid); for (const s of filtered) {
return { const dbSkin = await skinService.getSkin(s.uuid);
...s, // Shallow copy to avoid mutating the imported 'skins' object const weight = tierWeights[s.contentTierUuid] ?? 0;
if (weight > 0) { // <--- CRITICAL: Remove 0 weight skins
weightedPool.push({
...s,
tierColor: dbSkin?.tierColor, tierColor: dbSkin?.tierColor,
weight: tierWeights[s.contentTierUuid] ?? 0, weight,
}; });
}) }
.filter((s) => s.weight > 0); // <--- CRITICAL: Remove 0 weight skins }
function weightedSample(arr, count) { function weightedSample(arr, count) {
let totalWeight = arr.reduce((sum, x) => sum + x.weight, 0); let totalWeight = arr.reduce((sum, x) => sum + x.weight, 0);
@@ -123,7 +125,7 @@ export async function drawCaseContent(caseType = "standard", poolSize = 100) {
} }
} }
export function drawCaseSkin(caseContent) { export async function drawCaseSkin(caseContent) {
let randomSelectedSkinIndex; let randomSelectedSkinIndex;
let randomSelectedSkinUuid; let randomSelectedSkinUuid;
try { try {
@@ -134,7 +136,7 @@ export function drawCaseSkin(caseContent) {
throw new Error("Failed to draw a skin from the case content."); throw new Error("Failed to draw a skin from the case content.");
} }
const dbSkin = getSkin.get(randomSelectedSkinUuid); const dbSkin = await skinService.getSkin(randomSelectedSkinUuid);
const randomSkinData = skins.find((skin) => skin.uuid === dbSkin.uuid); const randomSkinData = skins.find((skin) => skin.uuid === dbSkin.uuid);
if (!randomSkinData) { if (!randomSkinData) {
throw new Error(`Could not find skin data for UUID: ${dbSkin.uuid}`); throw new Error(`Could not find skin data for UUID: ${dbSkin.uuid}`);

View File

@@ -5,23 +5,9 @@ import cron from "node-cron";
import { getSkinTiers, getValorantSkins } from "../api/valorant.js"; import { getSkinTiers, getValorantSkins } from "../api/valorant.js";
import { DiscordRequest } from "../api/discord.js"; import { DiscordRequest } from "../api/discord.js";
import { initTodaysSOTD } from "../game/points.js"; import { initTodaysSOTD } from "../game/points.js";
import { import * as userService from "../services/user.service.js";
deleteBid, import * as skinService from "../services/skin.service.js";
deleteMarketOffer, import * as marketService from "../services/market.service.js";
getAllAkhys,
getAllUsers,
getMarketOffers,
getOfferBids,
getSkin,
getUser,
insertManySkins,
insertUser,
resetDailyReward,
updateMarketOffer,
updateSkin,
updateUserAvatar,
updateUserCoins,
} from "../database/index.js";
import { activeInventories, activePredis, activeSearchs, pokerRooms, skins } from "../game/state.js"; import { activeInventories, activePredis, activeSearchs, pokerRooms, skins } from "../game/state.js";
import { emitMarketUpdate } from "../server/socket.js"; import { emitMarketUpdate } from "../server/socket.js";
import { handleMarketOfferClosing, handleMarketOfferOpening } from "./marketNotifs.js"; import { handleMarketOfferClosing, handleMarketOfferOpening } from "./marketNotifs.js";
@@ -49,7 +35,7 @@ export async function InstallGlobalCommands(appId, commands) {
export async function getAkhys(client) { export async function getAkhys(client) {
try { try {
// 1. Fetch Discord Members // 1. Fetch Discord Members
const initial_akhys = getAllUsers.all().length; const initial_akhys = (await userService.getAllUsers()).length;
const guild = await client.guilds.fetch(process.env.GUILD_ID); const guild = await client.guilds.fetch(process.env.GUILD_ID);
const members = await guild.members.fetch(); const members = await guild.members.fetch();
const akhys = members.filter((m) => !m.user.bot && m.roles.cache.has(process.env.AKHY_ROLE_ID)); const akhys = members.filter((m) => !m.user.bot && m.roles.cache.has(process.env.AKHY_ROLE_ID));
@@ -67,14 +53,14 @@ export async function getAkhys(client) {
})); }));
if (usersToInsert.length > 0) { if (usersToInsert.length > 0) {
usersToInsert.forEach((user) => { for (const user of usersToInsert) {
try { try {
insertUser.run(user); await userService.insertUser(user);
} catch (err) {} } catch (err) {}
}); }
} }
const new_akhys = getAllUsers.all().length; const new_akhys = (await userService.getAllUsers()).length;
const diff = new_akhys - initial_akhys; const diff = new_akhys - initial_akhys;
console.log( console.log(
`[Sync] Found and synced ${usersToInsert.length} ${diff !== 0 ? "(" + (diff > 0 ? "+" + diff : diff) + ") " : ""}users with the 'Akhy' role. (ID:${process.env.AKHY_ROLE_ID})`, `[Sync] Found and synced ${usersToInsert.length} ${diff !== 0 ? "(" + (diff > 0 ? "+" + diff : diff) + ") " : ""}users with the 'Akhy' role. (ID:${process.env.AKHY_ROLE_ID})`,
@@ -97,17 +83,17 @@ export async function getAkhys(client) {
displayName: skin.displayName, displayName: skin.displayName,
contentTierUuid: skin.contentTierUuid, contentTierUuid: skin.contentTierUuid,
displayIcon: skin.displayIcon, displayIcon: skin.displayIcon,
user_id: null, userId: null,
tierRank: tier.rank, tierRank: tier.rank != null ? String(tier.rank) : null,
tierColor: tier.highlightColor?.slice(0, 6) || "F2F3F3", tierColor: tier.highlightColor?.slice(0, 6) || "F2F3F3",
tierText: formatTierText(tier.rank, skin.displayName), tierText: formatTierText(tier.rank, skin.displayName),
basePrice: basePrice.toFixed(0), basePrice: basePrice.toFixed(0),
maxPrice: calculateMaxPrice(basePrice, skin).toFixed(0), maxPrice: parseInt(calculateMaxPrice(basePrice, skin).toFixed(0)),
}; };
}); });
if (skinsToInsert.length > 0) { if (skinsToInsert.length > 0) {
insertManySkins(skinsToInsert); await skinService.insertManySkins(skinsToInsert);
} }
console.log(`[Sync] Fetched and synced ${skinsToInsert.length} Valorant skins.`); console.log(`[Sync] Fetched and synced ${skinsToInsert.length} Valorant skins.`);
} catch (err) { } catch (err) {
@@ -175,7 +161,7 @@ export function setupCronJobs(client, io) {
cron.schedule(process.env.CRON_EXPR, async () => { cron.schedule(process.env.CRON_EXPR, async () => {
console.log("[Cron] Running daily midnight tasks..."); console.log("[Cron] Running daily midnight tasks...");
try { try {
resetDailyReward.run(); await userService.resetDailyReward();
console.log("[Cron] Daily rewards have been reset for all users."); console.log("[Cron] Daily rewards have been reset for all users.");
//if (!getSOTD.get()) { //if (!getSOTD.get()) {
initTodaysSOTD(); initTodaysSOTD();
@@ -184,16 +170,16 @@ export function setupCronJobs(client, io) {
console.error("[Cron] Error during daily reset:", e); console.error("[Cron] Error during daily reset:", e);
} }
try { try {
const offers = getMarketOffers.all(); const offers = await marketService.getMarketOffers();
const now = Date.now(); const now = Date.now();
const TWO_DAYS = 2 * 24 * 60 * 60 * 1000; const TWO_DAYS = 2 * 24 * 60 * 60 * 1000;
for (const offer of offers) { for (const offer of offers) {
if (now >= offer.closing_at + TWO_DAYS) { if (now >= offer.closingAt + TWO_DAYS) {
const offerBids = getOfferBids.all(offer.id); const offerBids = await marketService.getOfferBids(offer.id);
for (const bid of offerBids) { for (const bid of offerBids) {
deleteBid.run(bid.id); await marketService.deleteBid(bid.id);
} }
deleteMarketOffer.run(offer.id); await marketService.deleteMarketOffer(offer.id);
console.log(`[Cron] Deleted expired market offer ID: ${offer.id}`); console.log(`[Cron] Deleted expired market offer ID: ${offer.id}`);
} }
} }
@@ -207,14 +193,11 @@ export function setupCronJobs(client, io) {
console.log("[Cron] Running daily 7 AM data sync..."); console.log("[Cron] Running daily 7 AM data sync...");
await getAkhys(client); await getAkhys(client);
try { try {
const akhys = getAllAkhys.all(); const akhys = await userService.getAllAkhys();
for (const akhy of akhys) { for (const akhy of akhys) {
const user = await client.users.cache.get(akhy.id); const user = await client.users.cache.get(akhy.id);
try { try {
updateUserAvatar.run({ await userService.updateUserAvatar(akhy.id, user.displayAvatarURL({ dynamic: true, size: 256 }));
id: akhy.id,
avatarUrl: user.displayAvatarURL({ dynamic: true, size: 256 }),
});
} catch (err) { } catch (err) {
console.error(`[Cron] Error updating avatar for user ID: ${akhy.id}`, err); console.error(`[Cron] Error updating avatar for user ID: ${akhy.id}`, err);
} }
@@ -276,51 +259,51 @@ export async function postAPOBuy(userId, amount) {
// --- Miscellaneous Helpers --- // --- Miscellaneous Helpers ---
function handleMarketOffersUpdate() { async function handleMarketOffersUpdate() {
const now = Date.now(); const now = Date.now();
const offers = getMarketOffers.all(); const offers = await marketService.getMarketOffers();
offers.forEach(async (offer) => { offers.forEach(async (offer) => {
if (now >= offer.opening_at && offer.status === "pending") { if (now >= offer.openingAt && offer.status === "pending") {
updateMarketOffer.run({ id: offer.id, final_price: null, buyer_id: null, status: "open" }); await marketService.updateMarketOffer({ id: offer.id, finalPrice: null, buyerId: null, status: "open" });
await handleMarketOfferOpening(offer.id, client); await handleMarketOfferOpening(offer.id, client);
await emitMarketUpdate(); await emitMarketUpdate();
} }
if (now >= offer.closing_at && offer.status !== "closed") { if (now >= offer.closingAt && offer.status !== "closed") {
const bids = getOfferBids.all(offer.id); const bids = await marketService.getOfferBids(offer.id);
if (bids.length === 0) { if (bids.length === 0) {
// No bids placed, mark as closed without a sale // No bids placed, mark as closed without a sale
updateMarketOffer.run({ await marketService.updateMarketOffer({
id: offer.id, id: offer.id,
buyer_id: null, buyerId: null,
final_price: null, finalPrice: null,
status: "closed", status: "closed",
}); });
await emitMarketUpdate(); await emitMarketUpdate();
} else { } else {
const lastBid = bids[0]; const lastBid = bids[0];
const seller = getUser.get(offer.seller_id); const seller = await userService.getUser(offer.sellerId);
const buyer = getUser.get(lastBid.bidder_id); const buyer = await userService.getUser(lastBid.bidderId);
try { try {
// Change skin ownership // Change skin ownership
const skin = getSkin.get(offer.skin_uuid); const skin = await skinService.getSkin(offer.skinUuid);
if (!skin) throw new Error(`Skin not found for offer ID: ${offer.id}`); if (!skin) throw new Error(`Skin not found for offer ID: ${offer.id}`);
updateSkin.run({ await skinService.updateSkin({
user_id: buyer.id, userId: buyer.id,
currentLvl: skin.currentLvl, currentLvl: skin.currentLvl,
currentChroma: skin.currentChroma, currentChroma: skin.currentChroma,
currentPrice: skin.currentPrice, currentPrice: skin.currentPrice,
uuid: skin.uuid, uuid: skin.uuid,
}); });
updateMarketOffer.run({ await marketService.updateMarketOffer({
id: offer.id, id: offer.id,
buyer_id: buyer.id, buyerId: buyer.id,
final_price: lastBid.offer_amount, finalPrice: lastBid.offerAmount,
status: "closed", status: "closed",
}); });
const newUserCoins = seller.coins + lastBid.offer_amount; const newUserCoins = seller.coins + lastBid.offerAmount;
updateUserCoins.run({ id: seller.id, coins: newUserCoins }); await userService.updateUserCoins(seller.id, newUserCoins);
await emitMarketUpdate(); await emitMarketUpdate();
} catch (e) { } catch (e) {
console.error(`[Market Cron] Error processing offer ID: ${offer.id}`, e); console.error(`[Market Cron] Error processing offer ID: ${offer.id}`, e);

View File

@@ -1,18 +1,20 @@
import { getMarketOfferById, getOfferBids, getSkin, getUser } from "../database/index.js"; import * as userService from "../services/user.service.js";
import * as skinService from "../services/skin.service.js";
import * as marketService from "../services/market.service.js";
import { EmbedBuilder } from "discord.js"; import { EmbedBuilder } from "discord.js";
export async function handleNewMarketOffer(offerId, client) { export async function handleNewMarketOffer(offerId, client) {
const offer = getMarketOfferById.get(offerId); const offer = await marketService.getMarketOfferById(offerId);
if (!offer) return; if (!offer) return;
const skin = getSkin.get(offer.skin_uuid); const skin = await skinService.getSkin(offer.skinUuid);
const discordUserSeller = await client.users.fetch(offer.seller_id); const discordUserSeller = await client.users.fetch(offer.sellerId);
try { try {
const userSeller = getUser.get(offer.seller_id); const userSeller = await userService.getUser(offer.sellerId);
if (discordUserSeller && userSeller?.isAkhy) { if (discordUserSeller && userSeller?.isAkhy) {
const embed = new EmbedBuilder() const embed = new EmbedBuilder()
.setTitle("🔔 Offre créée") .setTitle("🔔 Offre créée")
.setDescription(`Ton offre pour le skin **${skin ? skin.displayName : offer.skin_uuid}** a bien été créée !`) .setDescription(`Ton offre pour le skin **${skin ? skin.displayName : offer.skinUuid}** a bien été créée !`)
.setThumbnail(skin.displayIcon) .setThumbnail(skin.displayIcon)
.setColor(0x5865f2) // Discord blurple .setColor(0x5865f2) // Discord blurple
.addFields( .addFields(
@@ -23,16 +25,16 @@ export async function handleNewMarketOffer(offerId, client) {
}, },
{ {
name: "💰 Prix de départ", name: "💰 Prix de départ",
value: `\`${offer.starting_price} coins\``, value: `\`${offer.startingPrice} coins\``,
inline: true, inline: true,
}, },
{ {
name: "⏰ Ouverture", name: "⏰ Ouverture",
value: `<t:${Math.floor(offer.opening_at / 1000)}:F>`, value: `<t:${Math.floor(offer.openingAt / 1000)}:F>`,
}, },
{ {
name: "⏰ Fermeture", name: "⏰ Fermeture",
value: `<t:${Math.floor(offer.closing_at / 1000)}:F>`, value: `<t:${Math.floor(offer.closingAt / 1000)}:F>`,
}, },
{ {
name: "🆔 ID de loffre", name: "🆔 ID de loffre",
@@ -53,26 +55,26 @@ export async function handleNewMarketOffer(offerId, client) {
const guildChannel = await client.channels.fetch(process.env.BOT_CHANNEL_ID); const guildChannel = await client.channels.fetch(process.env.BOT_CHANNEL_ID);
const embed = new EmbedBuilder() const embed = new EmbedBuilder()
.setTitle("🔔 Nouvelle offre") .setTitle("🔔 Nouvelle offre")
.setDescription(`Une offre pour le skin **${skin ? skin.displayName : offer.skin_uuid}** a été créée !`) .setDescription(`Une offre pour le skin **${skin ? skin.displayName : offer.skinUuid}** a été créée !`)
.setThumbnail(skin.displayIcon) .setThumbnail(skin.displayIcon)
.setColor(0x5865f2) // Discord blurple .setColor(0x5865f2) // Discord blurple
.addFields( .addFields(
{ {
name: "💰 Prix de départ", name: "💰 Prix de départ",
value: `\`${offer.starting_price} coins\``, value: `\`${offer.startingPrice} coins\``,
inline: true, inline: true,
}, },
{ {
name: "⏰ Ouverture", name: "⏰ Ouverture",
value: `<t:${Math.floor(offer.opening_at / 1000)}:F>`, value: `<t:${Math.floor(offer.openingAt / 1000)}:F>`,
}, },
{ {
name: "⏰ Fermeture", name: "⏰ Fermeture",
value: `<t:${Math.floor(offer.closing_at / 1000)}:F>`, value: `<t:${Math.floor(offer.closingAt / 1000)}:F>`,
}, },
{ {
name: "Créée par", name: "Créée par",
value: `<@${offer.seller_id}> ${discordUserSeller ? "(" + discordUserSeller.username + ")" : ""}`, value: `<@${offer.sellerId}> ${discordUserSeller ? "(" + discordUserSeller.username + ")" : ""}`,
}, },
) )
.setTimestamp(); .setTimestamp();
@@ -83,18 +85,18 @@ export async function handleNewMarketOffer(offerId, client) {
} }
export async function handleMarketOfferOpening(offerId, client) { export async function handleMarketOfferOpening(offerId, client) {
const offer = getMarketOfferById.get(offerId); const offer = await marketService.getMarketOfferById(offerId);
if (!offer) return; if (!offer) return;
const skin = getSkin.get(offer.skin_uuid); const skin = await skinService.getSkin(offer.skinUuid);
try { try {
const discordUserSeller = await client.users.fetch(offer.seller_id); const discordUserSeller = await client.users.fetch(offer.sellerId);
const userSeller = getUser.get(offer.seller_id); const userSeller = await userService.getUser(offer.sellerId);
if (discordUserSeller && userSeller?.isAkhy) { if (discordUserSeller && userSeller?.isAkhy) {
const embed = new EmbedBuilder() const embed = new EmbedBuilder()
.setTitle("🔔 Début des enchères") .setTitle("🔔 Début des enchères")
.setDescription( .setDescription(
`Les enchères sur ton offre pour le skin **${skin ? skin.displayName : offer.skin_uuid}** viennent de commencer !`, `Les enchères sur ton offre pour le skin **${skin ? skin.displayName : offer.skinUuid}** viennent de commencer !`,
) )
.setThumbnail(skin.displayIcon) .setThumbnail(skin.displayIcon)
.setColor(0x5865f2) // Discord blurple .setColor(0x5865f2) // Discord blurple
@@ -106,12 +108,12 @@ export async function handleMarketOfferOpening(offerId, client) {
}, },
{ {
name: "💰 Prix de départ", name: "💰 Prix de départ",
value: `\`${offer.starting_price} coins\``, value: `\`${offer.startingPrice} coins\``,
inline: true, inline: true,
}, },
{ {
name: "⏰ Fermeture", name: "⏰ Fermeture",
value: `<t:${Math.floor(offer.closing_at / 1000)}:F>`, value: `<t:${Math.floor(offer.closingAt / 1000)}:F>`,
}, },
{ {
name: "🆔 ID de loffre", name: "🆔 ID de loffre",
@@ -133,19 +135,19 @@ export async function handleMarketOfferOpening(offerId, client) {
const embed = new EmbedBuilder() const embed = new EmbedBuilder()
.setTitle("🔔 Début des enchères") .setTitle("🔔 Début des enchères")
.setDescription( .setDescription(
`Les enchères sur l'offre pour le skin **${skin ? skin.displayName : offer.skin_uuid}** viennent de commencer !`, `Les enchères sur l'offre pour le skin **${skin ? skin.displayName : offer.skinUuid}** viennent de commencer !`,
) )
.setThumbnail(skin.displayIcon) .setThumbnail(skin.displayIcon)
.setColor(0x5865f2) // Discord blurple .setColor(0x5865f2) // Discord blurple
.addFields( .addFields(
{ {
name: "💰 Prix de départ", name: "💰 Prix de départ",
value: `\`${offer.starting_price} coins\``, value: `\`${offer.startingPrice} coins\``,
inline: true, inline: true,
}, },
{ {
name: "⏰ Fermeture", name: "⏰ Fermeture",
value: `<t:${Math.floor(offer.closing_at / 1000)}:F>`, value: `<t:${Math.floor(offer.closingAt / 1000)}:F>`,
}, },
) )
.setTimestamp(); .setTimestamp();
@@ -156,19 +158,19 @@ export async function handleMarketOfferOpening(offerId, client) {
} }
export async function handleMarketOfferClosing(offerId, client) { export async function handleMarketOfferClosing(offerId, client) {
const offer = getMarketOfferById.get(offerId); const offer = await marketService.getMarketOfferById(offerId);
if (!offer) return; if (!offer) return;
const skin = getSkin.get(offer.skin_uuid); const skin = await skinService.getSkin(offer.skinUuid);
const bids = getOfferBids.all(offer.id); const bids = await marketService.getOfferBids(offer.id);
const discordUserSeller = await client.users.fetch(offer.seller_id); const discordUserSeller = await client.users.fetch(offer.sellerId);
try { try {
const userSeller = getUser.get(offer.seller_id); const userSeller = await userService.getUser(offer.sellerId);
if (discordUserSeller && userSeller?.isAkhy) { if (discordUserSeller && userSeller?.isAkhy) {
const embed = new EmbedBuilder() const embed = new EmbedBuilder()
.setTitle("🔔 Fin des enchères") .setTitle("🔔 Fin des enchères")
.setDescription( .setDescription(
`Les enchères sur ton offre pour le skin **${skin ? skin.displayName : offer.skin_uuid}** viennent de se terminer !`, `Les enchères sur ton offre pour le skin **${skin ? skin.displayName : offer.skinUuid}** viennent de se terminer !`,
) )
.setThumbnail(skin.displayIcon) .setThumbnail(skin.displayIcon)
.setColor(0x5865f2) // Discord blurple .setColor(0x5865f2) // Discord blurple
@@ -188,11 +190,11 @@ export async function handleMarketOfferClosing(offerId, client) {
); );
} else { } else {
const highestBid = bids[0]; const highestBid = bids[0];
const highestBidderUser = await client.users.fetch(highestBid.bidder_id); const highestBidderUser = await client.users.fetch(highestBid.bidderId);
embed.addFields( embed.addFields(
{ {
name: "✅ Enchères terminées avec succès !", 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 + ")" : ""}.`, value: `Ton skin a été vendu pour \`${highestBid.offerAmount} coins\` à <@${highestBid.bidderId}> ${highestBidderUser ? "(" + highestBidderUser.username + ")" : ""}.`,
}, },
{ {
name: "🆔 ID de loffre", name: "🆔 ID de loffre",
@@ -216,7 +218,7 @@ export async function handleMarketOfferClosing(offerId, client) {
const embed = new EmbedBuilder() const embed = new EmbedBuilder()
.setTitle("🔔 Fin des enchères") .setTitle("🔔 Fin des enchères")
.setDescription( .setDescription(
`Les enchères sur l'offre pour le skin **${skin ? skin.displayName : offer.skin_uuid}** viennent de se terminer !`, `Les enchères sur l'offre pour le skin **${skin ? skin.displayName : offer.skinUuid}** viennent de se terminer !`,
) )
.setThumbnail(skin.displayIcon) .setThumbnail(skin.displayIcon)
.setColor(0x5865f2) // Discord blurple .setColor(0x5865f2) // Discord blurple
@@ -229,18 +231,18 @@ export async function handleMarketOfferClosing(offerId, client) {
}); });
} else { } else {
const highestBid = bids[0]; const highestBid = bids[0];
const highestBidderUser = await client.users.fetch(highestBid.bidder_id); const highestBidderUser = await client.users.fetch(highestBid.bidderId);
embed.addFields({ embed.addFields({
name: "✅ Enchères terminées avec succès !", 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 + ")" : ""}.`, value: `Le skin de <@${offer.sellerId}> ${discordUserSeller ? "(" + discordUserSeller.username + ")" : ""} a été vendu pour \`${highestBid.offerAmount} coins\` à <@${highestBid.bidderId}> ${highestBidderUser ? "(" + highestBidderUser.username + ")" : ""}.`,
}); });
const discordUserBidder = await client.users.fetch(highestBid.bidder_id); const discordUserBidder = await client.users.fetch(highestBid.bidderId);
const userBidder = getUser.get(highestBid.bidder_id); const userBidder = await userService.getUser(highestBid.bidderId);
if (discordUserBidder && userBidder?.isAkhy) { if (discordUserBidder && userBidder?.isAkhy) {
const embed = new EmbedBuilder() const embed = new EmbedBuilder()
.setTitle("🔔 Fin des enchères") .setTitle("🔔 Fin des enchères")
.setDescription( .setDescription(
`Les enchères sur l'offre pour le skin **${skin ? skin.displayName : offer.skin_uuid}** viennent de se terminer !`, `Les enchères sur l'offre pour le skin **${skin ? skin.displayName : offer.skinUuid}** viennent de se terminer !`,
) )
.setThumbnail(skin.displayIcon) .setThumbnail(skin.displayIcon)
.setColor(0x5865f2) // Discord blurple .setColor(0x5865f2) // Discord blurple
@@ -248,7 +250,7 @@ export async function handleMarketOfferClosing(offerId, client) {
const highestBid = bids[0]; const highestBid = bids[0];
embed.addFields({ embed.addFields({
name: "✅ Enchères terminées avec succès !", 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.`, value: `Tu as acheté ce skin pour \`${highestBid.offerAmount} coins\` à <@${offer.sellerId}> ${discordUserSeller ? "(" + discordUserSeller.username + ")" : ""}. Il a été ajouté à ton inventaire.`,
}); });
discordUserBidder.send({ embeds: [embed] }).catch(console.error); discordUserBidder.send({ embeds: [embed] }).catch(console.error);
@@ -262,39 +264,39 @@ export async function handleMarketOfferClosing(offerId, client) {
export async function handleNewMarketOfferBid(offerId, bidId, client) { export async function handleNewMarketOfferBid(offerId, bidId, client) {
// Notify Seller and Bidder // Notify Seller and Bidder
const offer = getMarketOfferById.get(offerId); const offer = await marketService.getMarketOfferById(offerId);
if (!offer) return; if (!offer) return;
const bid = getOfferBids.get(offerId); const bid = (await marketService.getOfferBids(offerId))[0];
if (!bid) return; if (!bid) return;
const skin = getSkin.get(offer.skin_uuid); const skin = await skinService.getSkin(offer.skinUuid);
const bidderUser = client.users.fetch(bid.bidder_id); const bidderUser = client.users.fetch(bid.bidderId);
try { try {
const discordUserSeller = await client.users.fetch(offer.seller_id); const discordUserSeller = await client.users.fetch(offer.sellerId);
const userSeller = getUser.get(offer.seller_id); const userSeller = await userService.getUser(offer.sellerId);
if (discordUserSeller && userSeller?.isAkhy) { if (discordUserSeller && userSeller?.isAkhy) {
const embed = new EmbedBuilder() const embed = new EmbedBuilder()
.setTitle("🔔 Nouvelle enchère") .setTitle("🔔 Nouvelle enchère")
.setDescription( .setDescription(
`Il y a eu une nouvelle enchère sur ton offre pour le skin **${skin ? skin.displayName : offer.skin_uuid}**.`, `Il y a eu une nouvelle enchère sur ton offre pour le skin **${skin ? skin.displayName : offer.skinUuid}**.`,
) )
.setThumbnail(skin.displayIcon) .setThumbnail(skin.displayIcon)
.setColor(0x5865f2) // Discord blurple .setColor(0x5865f2) // Discord blurple
.addFields( .addFields(
{ {
name: "👤 Enchérisseur", name: "👤 Enchérisseur",
value: `<@${bid.bidder_id}> ${bidderUser ? "(" + bidderUser.username + ")" : ""}`, value: `<@${bid.bidderId}> ${bidderUser ? "(" + bidderUser.username + ")" : ""}`,
inline: true, inline: true,
}, },
{ {
name: "💰 Montant de lenchère", name: "💰 Montant de lenchère",
value: `\`${bid.offer_amount} coins\``, value: `\`${bid.offerAmount} coins\``,
inline: true, inline: true,
}, },
{ {
name: "⏰ Fermeture", name: "⏰ Fermeture",
value: `<t:${Math.floor(offer.closing_at / 1000)}:F>`, value: `<t:${Math.floor(offer.closingAt / 1000)}:F>`,
}, },
{ {
name: "🆔 ID de loffre", name: "🆔 ID de loffre",
@@ -311,19 +313,19 @@ export async function handleNewMarketOfferBid(offerId, bidId, client) {
} }
try { try {
const discordUserNewBidder = await client.users.fetch(bid.bidder_id); const discordUserNewBidder = await client.users.fetch(bid.bidderId);
const userNewBidder = getUser.get(bid.bidder_id); const userNewBidder = await userService.getUser(bid.bidderId);
if (discordUserNewBidder && userNewBidder?.isAkhy) { if (discordUserNewBidder && userNewBidder?.isAkhy) {
const embed = new EmbedBuilder() const embed = new EmbedBuilder()
.setTitle("🔔 Nouvelle enchère") .setTitle("🔔 Nouvelle enchère")
.setDescription( .setDescription(
`Ton enchère sur l'offre pour le skin **${skin ? skin.displayName : offer.skin_uuid}** a bien été placée!`, `Ton enchère sur l'offre pour le skin **${skin ? skin.displayName : offer.skinUuid}** a bien été placée!`,
) )
.setThumbnail(skin.displayIcon) .setThumbnail(skin.displayIcon)
.setColor(0x5865f2) // Discord blurple .setColor(0x5865f2) // Discord blurple
.addFields({ .addFields({
name: "💰 Montant de lenchère", name: "💰 Montant de lenchère",
value: `\`${bid.offer_amount} coins\``, value: `\`${bid.offerAmount} coins\``,
inline: true, inline: true,
}) })
.setTimestamp(); .setTimestamp();
@@ -335,28 +337,28 @@ export async function handleNewMarketOfferBid(offerId, bidId, client) {
} }
try { try {
const offerBids = getOfferBids.all(offer.id); const offerBids = await marketService.getOfferBids(offer.id);
if (offerBids.length < 2) return; // No previous bidder to notify if (offerBids.length < 2) return; // No previous bidder to notify
const discordUserPreviousBidder = await client.users.fetch(offerBids[1].bidder_id); const discordUserPreviousBidder = await client.users.fetch(offerBids[1].bidderId);
const userPreviousBidder = getUser.get(offerBids[1].bidder_id); const userPreviousBidder = await userService.getUser(offerBids[1].bidderId);
if (discordUserPreviousBidder && userPreviousBidder?.isAkhy) { if (discordUserPreviousBidder && userPreviousBidder?.isAkhy) {
const embed = new EmbedBuilder() const embed = new EmbedBuilder()
.setTitle("🔔 Nouvelle enchère") .setTitle("🔔 Nouvelle enchère")
.setDescription( .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 !`, `Quelqu'un a surenchéri sur l'offre pour le skin **${skin ? skin.displayName : offer.skinUuid}**, tu n'es plus le meilleur enchérisseur !`,
) )
.setThumbnail(skin.displayIcon) .setThumbnail(skin.displayIcon)
.setColor(0x5865f2) // Discord blurple .setColor(0x5865f2) // Discord blurple
.addFields( .addFields(
{ {
name: "👤 Enchérisseur", name: "👤 Enchérisseur",
value: `<@${bid.bidder_id}> ${bidderUser ? "(" + bidderUser.username + ")" : ""}`, value: `<@${bid.bidderId}> ${bidderUser ? "(" + bidderUser.username + ")" : ""}`,
inline: true, inline: true,
}, },
{ {
name: "💰 Montant de lenchère", name: "💰 Montant de lenchère",
value: `\`${bid.offer_amount} coins\``, value: `\`${bid.offerAmount} coins\``,
inline: true, inline: true,
}, },
) )
@@ -373,7 +375,7 @@ export async function handleNewMarketOfferBid(offerId, bidId, client) {
export async function handleCaseOpening(caseType, userId, skinUuid, client) { export async function handleCaseOpening(caseType, userId, skinUuid, client) {
const discordUser = await client.users.fetch(userId); const discordUser = await client.users.fetch(userId);
const skin = getSkin.get(skinUuid); const skin = await skinService.getSkin(skinUuid);
try { try {
const guildChannel = await client.channels.fetch(process.env.BOT_CHANNEL_ID); const guildChannel = await client.channels.fetch(process.env.BOT_CHANNEL_ID);
const embed = new EmbedBuilder() const embed = new EmbedBuilder()