prisma refactor

This commit is contained in:
Milo
2026-02-06 20:21:15 +01:00
parent c4c8eaf5d6
commit bb7f0047bb
35 changed files with 1750 additions and 2138 deletions

1
.gitignore vendored
View File

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

757
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -11,15 +11,17 @@
"scripts": {
"start": "node index.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",
"license": "MIT",
"dependencies": {
"@google/genai": "^1.30.0",
"@mistralai/mistralai": "^1.6.0",
"@prisma/client": "^6.19.2",
"axios": "^1.9.0",
"better-sqlite3": "^11.9.1",
"discord-interactions": "^4.0.0",
"discord.js": "^14.18.0",
"dotenv": "^16.0.3",
@@ -27,6 +29,7 @@
"node-cron": "^3.0.3",
"openai": "^4.104.0",
"pokersolver": "^2.1.4",
"prisma": "^6.19.2",
"socket.io": "^4.8.1",
"stripe": "^20.3.0",
"unique-names-generator": "^4.7.1",

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,
} from "discord-interactions";
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.
@@ -33,7 +33,7 @@ export async function handleInventoryCommand(req, res, client, interactionId) {
// --- 1. Fetch Data ---
const guild = await client.guilds.fetch(guild_id);
const targetMember = await guild.members.fetch(targetUserId);
const inventorySkins = getUserInventory.all({ user_id: targetUserId });
const inventorySkins = await skinService.getUserInventory(targetUserId);
// --- 2. Handle Empty Inventory ---
if (inventorySkins.length === 0) {

View File

@@ -5,7 +5,7 @@ import {
ButtonStyleTypes,
} from "discord-interactions";
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.
@@ -23,7 +23,7 @@ export async function handleSearchCommand(req, res, client, interactionId) {
try {
// --- 1. Fetch and Filter Data ---
const allDbSkins = getAllSkins.all();
const allDbSkins = await skinService.getAllSkins();
const resultSkins = allDbSkins.filter(
(skin) =>
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
let ownerText = "";
if (currentSkin.user_id) {
if (currentSkin.userId) {
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}** ✅`;
} 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";
}
}

View File

@@ -1,5 +1,5 @@
import { InteractionResponseType } from "discord-interactions";
import { getTopSkins } from "../../database/index.js";
import * as skinService from "../../services/skin.service.js";
/**
* Handles the /skins slash command.
@@ -13,7 +13,7 @@ export async function handleSkinsCommand(req, res, client) {
try {
// --- 1. Fetch Data ---
const topSkins = getTopSkins.all();
const topSkins = await skinService.getTopSkins();
const guild = await client.guilds.fetch(guild_id);
const fields = [];
@@ -23,14 +23,14 @@ export async function handleSkinsCommand(req, res, client) {
let ownerText = "Libre"; // Default text if the skin has no owner
// If the skin has an owner, fetch their details
if (skin.user_id) {
if (skin.userId) {
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
ownerText = `**@${owner.user.globalName || owner.user.username}** ✅`;
} catch (e) {
// 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";
}
}

View File

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

View File

@@ -1,7 +1,9 @@
import { InteractionResponseFlags, InteractionResponseType } from "discord-interactions";
import { ActionRowBuilder, ButtonBuilder, ButtonStyle, EmbedBuilder } from "discord.js";
import { DiscordRequest } from "../../api/discord.js";
import { getAllAvailableSkins, getUser, insertLog, updateSkin, updateUserCoins } from "../../database/index.js";
import * 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";
/**
@@ -27,7 +29,7 @@ export async function handleValorantCommand(req, res, client) {
try {
// --- 1. Verify and process payment ---
const commandUser = getUser.get(userId);
const commandUser = await userService.getUser(userId);
if (!commandUser) {
return res.send({
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()}`,
user_id: userId,
userId: userId,
action: "VALO_CASE_OPEN",
target_user_id: null,
coins_amount: -valoPrice,
user_new_amount: commandUser.coins - valoPrice,
});
updateUserCoins.run({
id: userId,
coins: commandUser.coins - valoPrice,
targetUserId: null,
coinsAmount: -valoPrice,
userNewAmount: commandUser.coins - valoPrice,
});
await userService.updateUserCoins(userId, commandUser.coins - valoPrice);
// --- 2. Send Initial "Opening" Response ---
// 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`;
try {
// --- Skin Selection ---
const availableSkins = getAllAvailableSkins.all();
const availableSkins = await skinService.getAllAvailableSkins();
if (availableSkins.length === 0) {
throw new Error("No available skins to award.");
}
@@ -105,9 +104,9 @@ export async function handleValorantCommand(req, res, client) {
const finalPrice = calculatePrice();
// --- Update Database ---
await updateSkin.run({
await skinService.updateSkin({
uuid: randomSkinData.uuid,
user_id: userId,
userId: userId,
currentLvl: randomLevel,
currentChroma: randomChroma,
currentPrice: finalPrice,

View File

@@ -2,7 +2,7 @@ import { InteractionResponseType, InteractionResponseFlags } from "discord-inter
import { DiscordRequest } from "../../api/discord.js";
import { activePolls } from "../../game/state.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.
@@ -75,7 +75,10 @@ export async function handlePollVote(req, res) {
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 ---
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
let ownerText = "";
if (currentSkin.user_id) {
if (currentSkin.userId) {
try {
const owner = await client.users.fetch(currentSkin.user_id);
const owner = await client.users.fetch(currentSkin.userId);
ownerText = `| **@${owner.globalName || owner.username}** ✅`;
} 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";
}
}

View File

@@ -9,7 +9,9 @@ import { EmbedBuilder, ActionRowBuilder, ButtonBuilder, ButtonStyle } from "disc
import { DiscordRequest } from "../../api/discord.js";
import { postAPOBuy } from "../../utils/index.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.
@@ -65,7 +67,7 @@ export async function handleUpgradeSkin(req, res) {
// --- 2. Handle Payment ---
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) {
return res.send({
@@ -86,18 +88,15 @@ export async function handleUpgradeSkin(req, res) {
});
}
insertLog.run({
await logService.insertLog({
id: `${userId}-${Date.now()}`,
user_id: userId,
userId: userId,
action: "VALO_SKIN_UPGRADE",
target_user_id: null,
coins_amount: -upgradePrice.toFixed(0),
user_new_amount: commandUser.coins - upgradePrice.toFixed(0),
});
updateUserCoins.run({
id: userId,
coins: commandUser.coins - upgradePrice.toFixed(0),
targetUserId: null,
coinsAmount: -upgradePrice.toFixed(0),
userNewAmount: commandUser.coins - upgradePrice.toFixed(0),
});
await userService.updateUserCoins(userId, commandUser.coins - upgradePrice.toFixed(0));
// --- 3. Show Loading Animation ---
// 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();
await updateSkin.run({
await skinService.updateSkin({
uuid: skinToUpgrade.uuid,
user_id: skinToUpgrade.user_id,
userId: skinToUpgrade.userId,
currentLvl: skinToUpgrade.currentLvl,
currentChroma: skinToUpgrade.currentChroma,
currentPrice: skinToUpgrade.currentPrice,
@@ -165,7 +164,7 @@ export async function handleUpgradeSkin(req, res) {
// --- 6. Send Final Result ---
setTimeout(async () => {
// 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 finalComponents = buildFinalComponents(succeeded, skinData, finalSkinState, interactionId);

View File

@@ -13,18 +13,10 @@ import {
import { calculateBasePrice, calculateMaxPrice, formatTime, getAkhys } from "../../utils/index.js";
import { channelPointsHandler, initTodaysSOTD, randomSkinPrice, slowmodesHandler } from "../../game/points.js";
import { activePolls, activeSlowmodes, requestTimestamps, skins } from "../../game/state.js";
import {
flopoDB,
getAllSkins,
getAllUsers,
getUser,
hardUpdateSkin,
insertLog,
updateManyUsers,
updateSkin,
updateUserAvatar,
updateUserCoins,
} from "../../database/index.js";
import prisma from "../../prisma/client.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 { client } from "../client.js";
import { drawCaseContent, drawCaseSkin, getDummySkinUpgradeProbs } from "../../utils/caseOpening.js";
@@ -89,7 +81,7 @@ export async function handleMessageCreate(message, client, io) {
// --- Sub-handler for AI Logic ---
async function handleAiMention(message, client, io) {
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
// --- Rate Limiting ---
@@ -105,7 +97,7 @@ async function handleAiMention(message, client, io) {
authorDB.warned = 1;
authorDB.warns += 1;
authorDB.allTimeWarns += 1;
updateManyUsers([authorDB]);
await userService.updateManyUsers([authorDB]);
// Apply timeout if warn count is too high
if (authorDB.warns > (parseInt(process.env.MAX_WARNS) || 10)) {
@@ -135,7 +127,7 @@ async function handleAiMention(message, client, io) {
authorDB.warned = 0;
authorDB.warns = 0;
authorDB.totalRequests += 1;
updateManyUsers([authorDB]);
await userService.updateManyUsers([authorDB]);
// --- AI Processing ---
try {
@@ -239,13 +231,14 @@ async function handleAdminCommands(message) {
message.reply("New Solitaire of the Day initialized.");
break;
case `${prefix}:users`:
console.log(getAllUsers.all());
console.log(await userService.getAllUsers());
break;
case `${prefix}:sql`:
const sqlCommand = args.join(" ");
try {
const stmt = flopoDB.prepare(sqlCommand);
const result = sqlCommand.trim().toUpperCase().startsWith("SELECT") ? stmt.all() : stmt.run();
const result = sqlCommand.trim().toUpperCase().startsWith("SELECT")
? await prisma.$queryRawUnsafe(sqlCommand)
: 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" });
@@ -267,16 +260,16 @@ async function handleAdminCommands(message) {
avatarUrl: akhy.user.displayAvatarURL({ dynamic: true, size: 256 }),
}));
usersToUpdate.forEach((user) => {
for (const user of usersToUpdate) {
try {
updateUserAvatar.run(user);
await userService.updateUserAvatar(user.id, user.avatarUrl);
} catch (err) {}
});
}
break;
case `${prefix}:rework-skins`:
console.log("Reworking all skin prices...");
const dbSkins = getAllSkins.all();
dbSkins.forEach((skin) => {
const dbSkins = await skinService.getAllSkins();
for (const skin of dbSkins) {
const fetchedSkin = skins.find((s) => s.uuid === skin.uuid);
const basePrice = calculateBasePrice(fetchedSkin, skin.tierRank)?.toFixed(0);
const calculatePrice = () => {
@@ -287,12 +280,12 @@ async function handleAdminCommands(message) {
return parseFloat(result.toFixed(0));
};
const maxPrice = calculateMaxPrice(basePrice, fetchedSkin).toFixed(0);
hardUpdateSkin.run({
await skinService.hardUpdateSkin({
uuid: skin.uuid,
displayName: skin.displayName,
contentTierUuid: skin.contentTierUuid,
displayIcon: skin.displayIcon,
user_id: skin.user_id,
userId: skin.userId,
tierRank: skin.tierRank,
tierColor: skin.tierColor,
tierText: skin.tierText,
@@ -302,7 +295,7 @@ async function handleAdminCommands(message) {
currentPrice: skin.currentPrice ? calculatePrice() : null,
maxPrice: maxPrice,
});
});
}
console.log("Reworked", dbSkins.length, "skins.");
break;
case `${prefix}:cases-test`:
@@ -328,7 +321,7 @@ async function handleAdminCommands(message) {
for (let i = 0; i < caseCount; i++) {
const skins = await drawCaseContent(caseType);
const result = drawCaseSkin(skins);
const result = await drawCaseSkin(skins);
totalResValue += result.finalPrice;
if (result.finalPrice > highestSkinPrice) highestSkinPrice = result.finalPrice;
if (result.finalPrice > 0 && result.finalPrice < 100) priceTiers["0"] += 1;
@@ -358,26 +351,23 @@ async function handleAdminCommands(message) {
break;
case `${prefix}:refund-skins`:
try {
const DBskins = getAllSkins.all();
const DBskins = await skinService.getAllSkins();
for (const skin of DBskins) {
const owner = getUser.get(skin.user_id);
const owner = await userService.getUser(skin.userId);
if (owner) {
updateUserCoins.run({
id: owner.id,
coins: owner.coins + skin.currentPrice,
});
insertLog.run({
await userService.updateUserCoins(owner.id, owner.coins + skin.currentPrice);
await logService.insertLog({
id: `${skin.uuid}-skin-refund-${Date.now()}`,
user_id: owner.id,
target_user_id: null,
userId: owner.id,
targetUserId: null,
action: "SKIN_REFUND",
coins_amount: skin.currentPrice,
user_new_amount: owner.coins + skin.currentPrice,
coinsAmount: skin.currentPrice,
userNewAmount: owner.coins + skin.currentPrice,
});
}
updateSkin.run({
await skinService.updateSkin({
uuid: skin.uuid,
user_id: null,
userId: null,
currentPrice: null,
currentLvl: null,
currentChroma: null,

View File

@@ -1,972 +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
);
CREATE TABLE IF NOT EXISTS transactions
(
id
TEXT
PRIMARY
KEY,
session_id
TEXT
UNIQUE
NOT
NULL,
user_id
TEXT
REFERENCES
users
NOT
NULL,
coins_amount
INTEGER
NOT
NULL,
amount_cents
INTEGER
NOT
NULL,
currency
TEXT
DEFAULT
'eur',
customer_email
TEXT,
customer_name
TEXT,
payment_status
TEXT
NOT
NULL,
created_at
DATETIME
DEFAULT
CURRENT_TIMESTAMP
);
`);
/* -----------------------------------------------------
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();
export const stmtTransactions = flopoDB.prepare(`
CREATE TABLE IF NOT EXISTS transactions
(
id
TEXT
PRIMARY
KEY,
session_id
TEXT
UNIQUE
NOT
NULL,
user_id
TEXT
REFERENCES
users
NOT
NULL,
coins_amount
INTEGER
NOT
NULL,
amount_cents
INTEGER
NOT
NULL,
currency
TEXT
DEFAULT
'eur',
customer_email
TEXT,
customer_name
TEXT,
payment_status
TEXT
NOT
NULL,
created_at
DATETIME
DEFAULT
CURRENT_TIMESTAMP
)
`);
stmtTransactions.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();
}
/* -------------------------
TRANSACTION statements
----------------------------*/
export const insertTransaction = flopoDB.prepare(
`INSERT INTO transactions (id, session_id, user_id, coins_amount, amount_cents, currency, customer_email, customer_name, payment_status)
VALUES (@id, @session_id, @user_id, @coins_amount, @amount_cents, @currency, @customer_email, @customer_name, @payment_status)`,
);
export const getTransactionBySessionId = flopoDB.prepare(
`SELECT * FROM transactions WHERE session_id = ?`,
);
export const getAllTransactions = flopoDB.prepare(
`SELECT * FROM transactions ORDER BY created_at DESC`,
);
export const getUserTransactions = flopoDB.prepare(
`SELECT * FROM transactions WHERE user_id = ? ORDER BY created_at DESC`,
);

View File

@@ -3,7 +3,8 @@
// Inspired by your poker helpers API style.
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 { EmbedBuilder } from "discord.js";
@@ -299,21 +300,18 @@ export async function settleAll(room) {
p.totalDelta += res.delta;
p.totalBets++;
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) {
const coins = userDB.coins;
try {
updateUserCoins.run({
id: p.id,
coins: coins + hand.bet + res.delta,
});
insertLog.run({
await userService.updateUserCoins(p.id, coins + hand.bet + res.delta);
await logService.insertLog({
id: `${p.id}-blackjack-${Date.now()}`,
user_id: p.id,
target_user_id: null,
userId: p.id,
targetUserId: null,
action: "BLACKJACK_PAYOUT",
coins_amount: res.delta + hand.bet,
user_new_amount: coins + hand.bet + res.delta,
coinsAmount: res.delta + hand.bet,
userNewAmount: coins + hand.bet + res.delta,
});
p.bank = coins + hand.bet + res.delta;
} 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 { 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) {
// --- 1. Fetch Player Data ---
const p1DB = getUser.get(p1Id);
const p2DB = getUser.get(p2Id);
const p1DB = await userService.getUser(p1Id);
const p2DB = await userService.getUser(p2Id);
if (!p1DB || !p2DB) {
console.error(`Elo Handler: Could not find user data for ${p1Id} or ${p2Id}.`);
return;
}
let p1EloData = getUserElo.get({ id: p1Id });
let p2EloData = getUserElo.get({ id: p2Id });
let p1EloData = await gameService.getUserElo(p1Id);
let p2EloData = await gameService.getUserElo(p2Id);
// --- 2. Initialize Elo if it doesn't exist ---
if (!p1EloData) {
await insertElos.run({ id: p1Id, elo: 1000 });
await gameService.insertElo(p1Id, 1000);
p1EloData = { id: p1Id, elo: 1000 };
}
if (!p2EloData) {
await insertElos.run({ id: p2Id, elo: 1000 });
await gameService.insertElo(p2Id, 1000);
p2EloData = { id: p2Id, elo: 1000 };
}
@@ -91,34 +92,34 @@ export async function eloHandler(p1Id, p2Id, p1Score, p2Score, type, scores = nu
}
// --- 4. Update Database ---
updateElo.run({ id: p1Id, elo: finalP1Elo });
updateElo.run({ id: p2Id, elo: finalP2Elo });
await gameService.updateElo(p1Id, finalP1Elo);
await gameService.updateElo(p2Id, finalP2Elo);
if (scores) {
insertGame.run({
await gameService.insertGame({
id: `${p1Id}-${p2Id}-${Date.now()}`,
p1: p1Id,
p2: p2Id,
p1_score: scores.p1,
p2_score: scores.p2,
p1_elo: p1CurrentElo,
p2_elo: p2CurrentElo,
p1_new_elo: finalP1Elo,
p2_new_elo: finalP2Elo,
p1Score: scores.p1,
p2Score: scores.p2,
p1Elo: p1CurrentElo,
p2Elo: p2CurrentElo,
p1NewElo: finalP1Elo,
p2NewElo: finalP2Elo,
type: type,
timestamp: Date.now(),
});
} else {
insertGame.run({
await gameService.insertGame({
id: `${p1Id}-${p2Id}-${Date.now()}`,
p1: p1Id,
p2: p2Id,
p1_score: p1Score,
p2_score: p2Score,
p1_elo: p1CurrentElo,
p2_elo: p2CurrentElo,
p1_new_elo: finalP1Elo,
p2_new_elo: finalP2Elo,
p1Score: p1Score,
p2Score: p2Score,
p1Elo: p1CurrentElo,
p2Elo: p2CurrentElo,
p1NewElo: finalP1Elo,
p2NewElo: finalP2Elo,
type: type,
timestamp: Date.now(),
});
@@ -141,11 +142,12 @@ export async function pokerEloHandler(room) {
if (playerIds.length < 2) return; // Not enough players to calculate Elo
// Fetch all players' Elo data at once
const dbPlayers = playerIds.map((id) => {
const user = getUser.get(id);
const elo = getUserElo.get({ id })?.elo || 1000;
const dbPlayers = await Promise.all(playerIds.map(async (id) => {
const user = await userService.getUser(id);
const eloData = await gameService.getUserElo(id);
const elo = eloData?.elo || 1000;
return { ...user, elo };
});
}));
const winnerIds = new Set(room.winners);
const playerCount = dbPlayers.length;
@@ -153,7 +155,7 @@ export async function pokerEloHandler(room) {
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
const expectedScore = 1 / (1 + Math.pow(10, (averageElo - player.elo) / 400));
@@ -175,23 +177,23 @@ export async function pokerEloHandler(room) {
console.log(
`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()}`,
p1: player.id,
p2: null, // No single opponent
p1_score: actualScore,
p2_score: null,
p1_elo: player.elo,
p2_elo: Math.round(averageElo), // Log the average opponent Elo for context
p1_new_elo: newElo,
p2_new_elo: null,
p1Score: actualScore,
p2Score: null,
p1Elo: player.elo,
p2Elo: Math.round(averageElo), // Log the average opponent Elo for context
p1NewElo: newElo,
p2NewElo: null,
type: "POKER_ROUND",
timestamp: Date.now(),
});
} else {
console.error(`Error calculating new Elo for ${player.globalName}.`);
}
});
}
}

View File

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

@@ -2,31 +2,13 @@ import express from "express";
import { sleep } from "openai/core";
import Stripe from "stripe";
// --- Database Imports ---
import {
getAllAkhys,
getAllUsers,
getLogs,
getMarketOffersBySkin,
getOfferBids,
getSkin,
getUser,
getUserElo,
getUserGames,
getUserInventory,
getUserLogs,
getUsersByElo,
insertLog,
insertUser,
pruneOldLogs,
queryDailyReward,
updateSkin,
updateUserCoins,
insertTransaction,
getTransactionBySessionId,
getAllTransactions,
getUserTransactions,
} from "../../database/index.js";
// --- Service Imports ---
import * as userService from "../../services/user.service.js";
import * as gameService from "../../services/game.service.js";
import * as skinService from "../../services/skin.service.js";
import * as logService from "../../services/log.service.js";
import * as transactionService from "../../services/transaction.service.js";
import * as marketService from "../../services/market.service.js";
// --- Game State Imports ---
import { activePolls, activePredis, activeSlowmodes, skins, activeSnakeGames } from "../../game/state.js";
@@ -57,9 +39,9 @@ export function apiRoutes(client, io) {
res.status(200).json({ status: "OK", message: "FlopoBot API is running." });
});
router.get("/users", (req, res) => {
router.get("/users", async (req, res) => {
try {
const users = getAllUsers.all();
const users = await userService.getAllUsers();
res.json(users);
} catch (error) {
console.error("Error fetching users:", error);
@@ -67,9 +49,9 @@ export function apiRoutes(client, io) {
}
});
router.get("/akhys", (req, res) => {
router.get("/akhys", async (req, res) => {
try {
const akhys = getAllAkhys.all();
const akhys = await userService.getAllAkhys();
res.json(akhys);
} catch (error) {
console.error("Error fetching akhys:", error);
@@ -82,7 +64,7 @@ export function apiRoutes(client, io) {
const discordUser = await client.users.fetch(discordUserId);
try {
insertUser.run({
await userService.insertUser({
id: discordUser.id,
username: discordUser.username,
globalName: discordUser.globalName,
@@ -94,14 +76,14 @@ export function apiRoutes(client, io) {
isAkhy: 0,
});
updateUserCoins.run({ id: discordUser.id, coins: 5000 });
insertLog.run({
await userService.updateUserCoins(discordUser.id, 5000);
await logService.insertLog({
id: `${discordUser.id}-welcome-${Date.now()}`,
user_id: discordUser.id,
userId: discordUser.id,
action: "WELCOME_BONUS",
target_user_id: null,
coins_amount: 5000,
user_new_amount: 5000,
targetUserId: null,
coinsAmount: 5000,
userNewAmount: 5000,
});
console.log(`New registered user: ${discordUser.username} (${discordUser.id})`);
@@ -142,7 +124,7 @@ export function apiRoutes(client, io) {
default:
return res.status(400).json({ error: "Invalid case type." });
}
const commandUser = getUser.get(userId);
const commandUser = await userService.getUser(userId);
if (!commandUser) return res.status(404).json({ error: "User not found." });
const valoPrice = caseTypeVal;
if (commandUser.coins < valoPrice) return res.status(403).json({ error: "Not enough FlopoCoins." });
@@ -150,24 +132,21 @@ export function apiRoutes(client, io) {
try {
const selectedSkins = await drawCaseContent(caseType);
const result = drawCaseSkin(selectedSkins);
const result = await drawCaseSkin(selectedSkins);
// --- Update Database ---
insertLog.run({
await logService.insertLog({
id: `${userId}-${Date.now()}`,
user_id: userId,
userId: userId,
action: "VALO_CASE_OPEN",
target_user_id: null,
coins_amount: -valoPrice,
user_new_amount: commandUser.coins - valoPrice,
targetUserId: null,
coinsAmount: -valoPrice,
userNewAmount: commandUser.coins - valoPrice,
});
updateUserCoins.run({
id: userId,
coins: commandUser.coins - valoPrice,
});
updateSkin.run({
await userService.updateUserCoins(userId, commandUser.coins - valoPrice);
await skinService.updateSkin({
uuid: result.randomSkinData.uuid,
user_id: userId,
userId: userId,
currentLvl: result.randomLevel,
currentChroma: result.randomChroma,
currentPrice: result.finalPrice,
@@ -176,7 +155,7 @@ export function apiRoutes(client, io) {
console.log(
`${commandUser.username} opened a ${caseType} Valorant case and received skin ${result.randomSelectedSkinUuid}`,
);
const updatedSkin = getSkin.get(result.randomSkinData.uuid);
const updatedSkin = await skinService.getSkin(result.randomSkinData.uuid);
await handleCaseOpening(caseType, userId, result.randomSelectedSkinUuid, client);
const contentSkins = selectedSkins.map((item) => {
@@ -204,14 +183,15 @@ export function apiRoutes(client, io) {
const { type } = req.params;
try {
const selectedSkins = await drawCaseContent(type, -1);
selectedSkins.forEach((item) => {
for (const item of selectedSkins) {
item.isMelee = isMeleeSkin(item.displayName);
item.isVCT = isVCTSkin(item.displayName);
item.isChampions = isChampionsSkin(item.displayName);
item.vctRegion = getVCTRegion(item.displayName);
item.basePrice = getSkin.get(item.uuid).basePrice;
item.maxPrice = getSkin.get(item.uuid).maxPrice;
});
const skinData = await skinService.getSkin(item.uuid);
item.basePrice = skinData.basePrice;
item.maxPrice = skinData.maxPrice;
}
res.json({ skins: selectedSkins.sort((a, b) => b.maxPrice - a.maxPrice) });
} catch (error) {
console.error("Error fetching case content:", error);
@@ -251,47 +231,44 @@ export function apiRoutes(client, io) {
}
});
router.post("/skin/:uuid/instant-sell", (req, res) => {
router.post("/skin/:uuid/instant-sell", async (req, res) => {
const { userId } = req.body;
try {
const skin = getSkin.get(req.params.uuid);
const skin = await skinService.getSkin(req.params.uuid);
const skinData = skins.find((s) => s.uuid === skin.uuid);
if (
!skinData
) {
return res.status(403).json({ error: "Invalid skin." });
}
if (skin.user_id !== userId) {
if (skin.userId !== userId) {
return res.status(403).json({ error: "User does not own this skin." });
}
const marketOffers = getMarketOffersBySkin.all(skin.uuid);
const marketOffers = await marketService.getMarketOffersBySkin(skin.uuid);
const activeOffers = marketOffers.filter((offer) => offer.status === "pending" || offer.status === "open");
if (activeOffers.length > 0) {
return res.status(403).json({ error: "Impossible de vendre ce skin, une offre FlopoMarket est déjà en cours." });
}
const commandUser = getUser.get(userId);
const commandUser = await userService.getUser(userId);
if (!commandUser) {
return res.status(404).json({ error: "User not found." });
}
const sellPrice = skin.currentPrice;
insertLog.run({
await logService.insertLog({
id: `${userId}-${Date.now()}`,
user_id: userId,
userId: userId,
action: "VALO_SKIN_INSTANT_SELL",
target_user_id: null,
coins_amount: sellPrice,
user_new_amount: commandUser.coins + sellPrice,
targetUserId: null,
coinsAmount: sellPrice,
userNewAmount: commandUser.coins + sellPrice,
});
updateUserCoins.run({
id: userId,
coins: commandUser.coins + sellPrice,
});
updateSkin.run({
await userService.updateUserCoins(userId, commandUser.coins + sellPrice);
await skinService.updateSkin({
uuid: skin.uuid,
user_id: null,
userId: null,
currentLvl: null,
currentChroma: null,
currentPrice: null,
@@ -304,9 +281,9 @@ export function apiRoutes(client, io) {
}
});
router.get("/skin-upgrade/:uuid/fetch", (req, res) => {
router.get("/skin-upgrade/:uuid/fetch", async (req, res) => {
try {
const skin = getSkin.get(req.params.uuid);
const skin = await skinService.getSkin(req.params.uuid);
const skinData = skins.find((s) => s.uuid === skin.uuid);
const { successProb, destructionProb, upgradePrice } = getSkinUpgradeProbs(skin, skinData);
@@ -326,7 +303,7 @@ export function apiRoutes(client, io) {
router.post("/skin-upgrade/:uuid", async (req, res) => {
const { userId } = req.body;
try {
const skin = getSkin.get(req.params.uuid);
const skin = await skinService.getSkin(req.params.uuid);
const skinData = skins.find((s) => s.uuid === skin.uuid);
if (
!skinData ||
@@ -334,17 +311,17 @@ export function apiRoutes(client, io) {
) {
return res.status(403).json({ error: "Skin is already maxed out or invalid skin." });
}
if (skin.user_id !== userId) {
if (skin.userId !== userId) {
return res.status(403).json({ error: "User does not own this skin." });
}
const marketOffers = getMarketOffersBySkin.all(skin.uuid);
const marketOffers = await marketService.getMarketOffersBySkin(skin.uuid);
const activeOffers = marketOffers.filter((offer) => offer.status === "pending" || offer.status === "open");
if (activeOffers.length > 0) {
return res.status(403).json({ error: "Impossible d'améliorer ce skin, une offre FlopoMarket est en cours." });
}
const { successProb, destructionProb, upgradePrice } = getSkinUpgradeProbs(skin, skinData);
const commandUser = getUser.get(userId);
const commandUser = await userService.getUser(userId);
if (!commandUser) {
return res.status(404).json({ error: "User not found." });
}
@@ -352,18 +329,15 @@ export function apiRoutes(client, io) {
return res.status(403).json({ error: `Pas assez de FlopoCoins (${upgradePrice} requis).` });
}
insertLog.run({
await logService.insertLog({
id: `${userId}-${Date.now()}`,
user_id: userId,
userId: userId,
action: "VALO_SKIN_UPGRADE",
target_user_id: null,
coins_amount: -upgradePrice,
user_new_amount: commandUser.coins - upgradePrice,
});
updateUserCoins.run({
id: userId,
coins: commandUser.coins - upgradePrice,
targetUserId: null,
coinsAmount: -upgradePrice,
userNewAmount: commandUser.coins - upgradePrice,
});
await userService.updateUserCoins(userId, commandUser.coins - upgradePrice);
let succeeded = false;
let destructed = false;
@@ -390,17 +364,17 @@ export function apiRoutes(client, io) {
};
skin.currentPrice = calculatePrice();
updateSkin.run({
await skinService.updateSkin({
uuid: skin.uuid,
user_id: skin.user_id,
userId: skin.userId,
currentLvl: skin.currentLvl,
currentChroma: skin.currentChroma,
currentPrice: skin.currentPrice,
});
} else if (destructed) {
updateSkin.run({
await skinService.updateSkin({
uuid: skin.uuid,
user_id: null,
userId: null,
currentLvl: null,
currentChroma: null,
currentPrice: null,
@@ -415,9 +389,9 @@ export function apiRoutes(client, io) {
}
});
router.get("/users/by-elo", (req, res) => {
router.get("/users/by-elo", async (req, res) => {
try {
const users = getUsersByElo.all();
const users = await gameService.getUsersByElo();
res.json(users);
} catch (error) {
console.error("Error fetching users by Elo:", error);
@@ -427,8 +401,8 @@ export function apiRoutes(client, io) {
router.get("/logs", async (req, res) => {
try {
await pruneOldLogs();
const logs = getLogs.all();
await logService.pruneOldLogs();
const logs = await logService.getLogs();
res.status(200).json(logs);
} catch (error) {
console.error("Error fetching logs:", error);
@@ -439,7 +413,7 @@ export function apiRoutes(client, io) {
// --- User-Specific Routes ---
router.get("/user/:id", async (req, res) => {
try {
const user = getUser.get(req.params.id);
const user = await userService.getUser(req.params.id);
res.json({ user });
} catch (error) {
res.status(404).json({ error: "User not found." });
@@ -467,74 +441,75 @@ export function apiRoutes(client, io) {
router.get("/user/:id/coins", async (req, res) => {
try {
const user = getUser.get(req.params.id);
const user = await userService.getUser(req.params.id);
res.json({ coins: user.coins });
} catch (error) {
res.status(404).json({ error: "User not found." });
}
});
router.get("/user/:id/sparkline", (req, res) => {
router.get("/user/:id/sparkline", async (req, res) => {
try {
const logs = getUserLogs.all({ user_id: req.params.id });
const logs = await logService.getUserLogs(req.params.id);
res.json({ sparkline: logs });
} catch (error) {
res.status(500).json({ error: "Failed to fetch logs for sparkline." });
}
});
router.get("/user/:id/elo", (req, res) => {
router.get("/user/:id/elo", async (req, res) => {
try {
const eloData = getUserElo.get({ id: req.params.id });
const eloData = await gameService.getUserElo(req.params.id);
res.json({ elo: eloData?.elo || null });
} catch (e) {
res.status(500).json({ error: "Failed to fetch Elo data." });
}
});
router.get("/user/:id/elo-graph", (req, res) => {
router.get("/user/:id/elo-graph", async (req, res) => {
try {
const games = getUserGames.all({ user_id: req.params.id });
const games = await gameService.getUserGames(req.params.id);
const eloHistory = games
.filter((g) => g.type !== 'POKER_ROUND' && g.type !== 'SOTD')
.filter((game) => game.p2 !== null)
.map((game) => (game.p1 === req.params.id ? game.p1_new_elo : game.p2_new_elo));
.map((game) => (game.p1 === req.params.id ? game.p1NewElo : game.p2NewElo));
eloHistory.splice(0, 0, 1000);
res.json({ elo_graph: eloHistory });
res.json({ eloGraph: eloHistory });
} catch (e) {
res.status(500).json({ error: "Failed to generate Elo graph." });
}
});
router.get("/user/:id/inventory", (req, res) => {
router.get("/user/:id/inventory", async (req, res) => {
try {
const inventory = getUserInventory.all({ user_id: req.params.id });
inventory.forEach((skin) => {
const marketOffers = getMarketOffersBySkin.all(skin.uuid);
marketOffers.forEach((offer) => {
offer.skin = getSkin.get(offer.skin_uuid);
offer.seller = getUser.get(offer.seller_id);
offer.buyer = getUser.get(offer.buyer_id) || null;
offer.bids = getOfferBids.all(offer.id) || {};
offer.bids.forEach((bid) => {
bid.bidder = getUser.get(bid.bidder_id);
});
});
const inventory = await skinService.getUserInventory(req.params.id);
for (const skin of inventory) {
const marketOffers = await marketService.getMarketOffersBySkin(skin.uuid);
for (const offer of marketOffers) {
offer.skin = await skinService.getSkin(offer.skinUuid);
offer.seller = await userService.getUser(offer.sellerId);
offer.buyer = offer.buyerId ? await userService.getUser(offer.buyerId) : null;
offer.bids = await marketService.getOfferBids(offer.id) || {};
for (const bid of offer.bids) {
bid.bidder = await userService.getUser(bid.bidderId);
}
}
skin.offers = marketOffers || {};
skin.isMelee = isMeleeSkin(skin.displayName);
skin.isVCT = isVCTSkin(skin.displayName);
skin.isChampions = isChampionsSkin(skin.displayName);
skin.vctRegion = getVCTRegion(skin.displayName);
});
}
res.json({ inventory });
} catch (error) {
console.log(error);
res.status(500).json({ error: "Failed to fetch inventory." });
}
});
router.get("/user/:id/games-history", async (req, res) => {
try {
const games = getUserGames.all({ user_id: req.params.id }).filter((g) => g.type !== 'POKER_ROUND' && g.type !== 'SOTD').reverse().slice(0, 50);
const games = (await gameService.getUserGames(req.params.id)).filter((g) => g.type !== 'POKER_ROUND' && g.type !== 'SOTD').reverse().slice(0, 50);
res.json({ games });
} catch (err) {
res.status(500).json({ error: "Failed to fetch games history." });
@@ -544,21 +519,21 @@ export function apiRoutes(client, io) {
router.get("/user/:id/daily", async (req, res) => {
const { id } = req.params;
try {
const akhy = getUser.get(id);
const akhy = await userService.getUser(id);
if (!akhy) return res.status(404).json({ message: "Utilisateur introuvable" });
if (akhy.dailyQueried) return res.status(403).json({ message: "Récompense journalière déjà récupérée." });
const amount = 500;
const newCoins = akhy.coins + amount;
queryDailyReward.run(id);
updateUserCoins.run({ id, coins: newCoins });
insertLog.run({
await userService.queryDailyReward(id);
await userService.updateUserCoins(id, newCoins);
await logService.insertLog({
id: `${id}-daily-${Date.now()}`,
user_id: id,
userId: id,
action: "DAILY_REWARD",
target_user_id: null,
coins_amount: amount,
user_new_amount: newCoins,
targetUserId: null,
coinsAmount: amount,
userNewAmount: newCoins,
});
await socketEmit("daily-queried", { userId: id });
@@ -589,7 +564,7 @@ export function apiRoutes(client, io) {
router.post("/change-nickname", async (req, res) => {
const { userId, nickname, commandUserId } = req.body;
const commandUser = getUser.get(commandUserId);
const commandUser = await userService.getUser(commandUserId);
if (!commandUser) return res.status(404).json({ message: "Command user not found." });
if (commandUser.coins < 1000) return res.status(403).json({ message: "Pas assez de FlopoCoins (1000 requis)." });
@@ -600,14 +575,14 @@ export function apiRoutes(client, io) {
await member.setNickname(nickname);
const newCoins = commandUser.coins - 1000;
updateUserCoins.run({ id: commandUserId, coins: newCoins });
insertLog.run({
await userService.updateUserCoins(commandUserId, newCoins);
await logService.insertLog({
id: `${commandUserId}-changenick-${Date.now()}`,
user_id: commandUserId,
userId: commandUserId,
action: "CHANGE_NICKNAME",
target_user_id: userId,
coins_amount: -1000,
user_new_amount: newCoins,
targetUserId: userId,
coinsAmount: -1000,
userNewAmount: newCoins,
});
console.log(`${commandUserId} change nickname of ${userId}: ${old_nickname} -> ${nickname}`);
@@ -640,8 +615,8 @@ export function apiRoutes(client, io) {
router.post("/spam-ping", async (req, res) => {
const { userId, commandUserId } = req.body;
const user = getUser.get(userId);
const commandUser = getUser.get(commandUserId);
const user = await userService.getUser(userId);
const commandUser = await userService.getUser(commandUserId);
if (!commandUser || !user) return res.status(404).json({ message: "Oups petit soucis" });
@@ -654,17 +629,14 @@ export function apiRoutes(client, io) {
res.status(200).json({ message: "C'est parti ehehe" });
updateUserCoins.run({
id: commandUserId,
coins: commandUser.coins - 5000,
});
insertLog.run({
await userService.updateUserCoins(commandUserId, commandUser.coins - 5000);
await logService.insertLog({
id: commandUserId + "-" + Date.now(),
user_id: commandUserId,
userId: commandUserId,
action: "SPAM_PING",
target_user_id: userId,
coins_amount: -5000,
user_new_amount: commandUser.coins - 5000,
targetUserId: userId,
coinsAmount: -5000,
userNewAmount: commandUser.coins - 5000,
});
await emitDataUpdated({ table: "users", action: "update" });
@@ -700,8 +672,8 @@ export function apiRoutes(client, io) {
router.post("/slowmode", async (req, res) => {
let { userId, commandUserId } = req.body;
const user = getUser.get(userId);
const commandUser = getUser.get(commandUserId);
const user = await userService.getUser(userId);
const commandUser = await userService.getUser(commandUserId);
if (!commandUser || !user) return res.status(404).json({ message: "Oups petit soucis" });
@@ -714,17 +686,14 @@ export function apiRoutes(client, io) {
delete activeSlowmodes[userId];
await socketEmit("new-slowmode", { action: "new slowmode" });
updateUserCoins.run({
id: commandUserId,
coins: commandUser.coins - 10000,
});
insertLog.run({
await userService.updateUserCoins(commandUserId, commandUser.coins - 10000);
await logService.insertLog({
id: commandUserId + "-" + Date.now(),
user_id: commandUserId,
userId: commandUserId,
action: "SLOWMODE",
target_user_id: userId,
coins_amount: -10000,
user_new_amount: commandUser.coins - 10000,
targetUserId: userId,
coinsAmount: -10000,
userNewAmount: commandUser.coins - 10000,
});
try {
@@ -759,17 +728,14 @@ export function apiRoutes(client, io) {
};
await socketEmit("new-slowmode", { action: "new slowmode" });
updateUserCoins.run({
id: commandUserId,
coins: commandUser.coins - 10000,
});
insertLog.run({
await userService.updateUserCoins(commandUserId, commandUser.coins - 10000);
await logService.insertLog({
id: commandUserId + "-" + Date.now(),
user_id: commandUserId,
userId: commandUserId,
action: "SLOWMODE",
target_user_id: userId,
coins_amount: -10000,
user_new_amount: commandUser.coins - 10000,
targetUserId: userId,
coinsAmount: -10000,
userNewAmount: commandUser.coins - 10000,
});
await emitDataUpdated({ table: "users", action: "update" });
@@ -796,8 +762,8 @@ export function apiRoutes(client, io) {
router.post("/timeout", async (req, res) => {
let { userId, commandUserId } = req.body;
const user = getUser.get(userId);
const commandUser = getUser.get(commandUserId);
const user = await userService.getUser(userId);
const commandUser = await userService.getUser(commandUserId);
if (!commandUser || !user) return res.status(404).json({ message: "Oups petit soucis" });
@@ -830,17 +796,14 @@ export function apiRoutes(client, io) {
return res.status(403).send({ message: `Impossible de time-out ${user.globalName}` });
}
updateUserCoins.run({
id: commandUserId,
coins: commandUser.coins - 10000,
});
insertLog.run({
await userService.updateUserCoins(commandUserId, commandUser.coins - 10000);
await logService.insertLog({
id: commandUserId + "-" + Date.now(),
user_id: commandUserId,
userId: commandUserId,
action: "TIMEOUT",
target_user_id: userId,
coins_amount: -10000,
user_new_amount: commandUser.coins - 10000,
targetUserId: userId,
coinsAmount: -10000,
userNewAmount: commandUser.coins - 10000,
});
try {
@@ -879,17 +842,14 @@ export function apiRoutes(client, io) {
await socketEmit("new-timeout", { action: "new timeout" });
updateUserCoins.run({
id: commandUserId,
coins: commandUser.coins - 100000,
});
insertLog.run({
await userService.updateUserCoins(commandUserId, commandUser.coins - 100000);
await logService.insertLog({
id: commandUserId + "-" + Date.now(),
user_id: commandUserId,
userId: commandUserId,
action: "TIMEOUT",
target_user_id: userId,
coins_amount: -100000,
user_new_amount: commandUser.coins - 100000,
targetUserId: userId,
coinsAmount: -100000,
userNewAmount: commandUser.coins - 100000,
});
await emitDataUpdated({ table: "users", action: "update" });
@@ -918,7 +878,7 @@ export function apiRoutes(client, io) {
router.post("/start-predi", async (req, res) => {
let { commandUserId, label, options, closingTime, payoutTime } = req.body;
const commandUser = getUser.get(commandUserId);
const commandUser = await userService.getUser(commandUserId);
if (!commandUser) return res.status(403).send({ message: "Oups petit problème" });
if (commandUser.coins < 100) return res.status(403).send({ message: "Tu n'as pas assez de FlopoCoins" });
@@ -997,17 +957,14 @@ export function apiRoutes(client, io) {
};
await socketEmit("new-predi", { action: "new predi" });
updateUserCoins.run({
id: commandUserId,
coins: commandUser.coins - 100,
});
insertLog.run({
await userService.updateUserCoins(commandUserId, commandUser.coins - 100);
await logService.insertLog({
id: commandUserId + "-" + Date.now(),
user_id: commandUserId,
userId: commandUserId,
action: "START_PREDI",
target_user_id: null,
coins_amount: -100,
user_new_amount: commandUser.coins - 100,
targetUserId: null,
coinsAmount: -100,
userNewAmount: commandUser.coins - 100,
});
await emitDataUpdated({ table: "users", action: "update" });
@@ -1022,7 +979,7 @@ export function apiRoutes(client, io) {
let intAmount = parseInt(amount);
if (intAmount < 10 || intAmount > 250000) return res.status(403).send({ message: "Montant invalide" });
const commandUser = getUser.get(commandUserId);
const commandUser = await userService.getUser(commandUserId);
if (!commandUser) return res.status(403).send({ message: "Oups, je ne te connais pas" });
if (commandUser.coins < intAmount) return res.status(403).send({ message: "Tu n'as pas assez de FlopoCoins" });
@@ -1068,17 +1025,14 @@ export function apiRoutes(client, io) {
await socketEmit("new-predi", { action: "new vote" });
updateUserCoins.run({
id: commandUserId,
coins: commandUser.coins - intAmount,
});
insertLog.run({
await userService.updateUserCoins(commandUserId, commandUser.coins - intAmount);
await logService.insertLog({
id: commandUserId + "-" + Date.now(),
user_id: commandUserId,
userId: commandUserId,
action: "PREDI_VOTE",
target_user_id: null,
coins_amount: -intAmount,
user_new_amount: commandUser.coins - intAmount,
targetUserId: null,
coinsAmount: -intAmount,
userNewAmount: commandUser.coins - intAmount,
});
await emitDataUpdated({ table: "users", action: "update" });
@@ -1088,7 +1042,7 @@ export function apiRoutes(client, io) {
router.post("/end-predi", async (req, res) => {
const { commandUserId, predi, confirm, winningOption } = req.body;
const commandUser = getUser.get(commandUserId);
const commandUser = await userService.getUser(commandUserId);
if (!commandUser) return res.status(403).send({ message: "Oups, je ne te connais pas" });
if (commandUserId !== process.env.DEV_ID)
return res.status(403).send({ message: "Tu n'as pas les permissions requises" });
@@ -1099,70 +1053,61 @@ export function apiRoutes(client, io) {
if (!confirm) {
activePredis[predi].cancelledTime = new Date();
activePredis[predi].options[0].votes.forEach((v) => {
const tempUser = getUser.get(v.id);
for (const v of activePredis[predi].options[0].votes) {
const tempUser = await userService.getUser(v.id);
try {
updateUserCoins.run({
id: v.id,
coins: tempUser.coins + v.amount,
});
insertLog.run({
await userService.updateUserCoins(v.id, tempUser.coins + v.amount);
await logService.insertLog({
id: v.id + "-" + Date.now(),
user_id: v.id,
userId: v.id,
action: "PREDI_REFUND",
target_user_id: v.id,
coins_amount: v.amount,
user_new_amount: tempUser.coins + v.amount,
targetUserId: v.id,
coinsAmount: v.amount,
userNewAmount: tempUser.coins + v.amount,
});
} catch (e) {
console.log(`Impossible de rembourser ${v.id} (${v.amount} coins)`);
}
});
activePredis[predi].options[1].votes.forEach((v) => {
const tempUser = getUser.get(v.id);
}
for (const v of activePredis[predi].options[1].votes) {
const tempUser = await userService.getUser(v.id);
try {
updateUserCoins.run({
id: v.id,
coins: tempUser.coins + v.amount,
});
insertLog.run({
await userService.updateUserCoins(v.id, tempUser.coins + v.amount);
await logService.insertLog({
id: v.id + "-" + Date.now(),
user_id: v.id,
userId: v.id,
action: "PREDI_REFUND",
target_user_id: v.id,
coins_amount: v.amount,
user_new_amount: tempUser.coins + v.amount,
targetUserId: v.id,
coinsAmount: v.amount,
userNewAmount: tempUser.coins + v.amount,
});
} catch (e) {
console.log(`Impossible de rembourser ${v.id} (${v.amount} coins)`);
}
});
}
activePredis[predi].closed = true;
} else {
const losingOption = winningOption === 0 ? 1 : 0;
activePredis[predi].options[winningOption].votes.forEach((v) => {
const tempUser = getUser.get(v.id);
for (const v of activePredis[predi].options[winningOption].votes) {
const tempUser = await userService.getUser(v.id);
const ratio =
activePredis[predi].options[winningOption].total === 0
? 0
: activePredis[predi].options[losingOption].total / activePredis[predi].options[winningOption].total;
try {
updateUserCoins.run({
id: v.id,
coins: tempUser.coins + v.amount * (1 + ratio),
});
insertLog.run({
await userService.updateUserCoins(v.id, tempUser.coins + v.amount * (1 + ratio));
await logService.insertLog({
id: v.id + "-" + Date.now(),
user_id: v.id,
userId: v.id,
action: "PREDI_RESULT",
target_user_id: v.id,
coins_amount: v.amount * (1 + ratio),
user_new_amount: tempUser.coins + v.amount * (1 + ratio),
targetUserId: v.id,
coinsAmount: v.amount * (1 + ratio),
userNewAmount: tempUser.coins + v.amount * (1 + ratio),
});
} catch (e) {
console.log(`Impossible de créditer ${v.id} (${v.amount} coins pariés, *${1 + ratio})`);
}
});
}
activePredis[predi].paidTime = new Date();
activePredis[predi].closed = true;
activePredis[predi].winning = winningOption;
@@ -1213,18 +1158,18 @@ export function apiRoutes(client, io) {
const { discordId, score, isWin } = req.body;
console.log(`[SNAKE][SOLO]${discordId}: score=${score}, isWin=${isWin}`);
try {
const user = getUser.get(discordId);
const user = await userService.getUser(discordId);
if (!user) return res.status(404).json({ message: "Utilisateur introuvable" });
const reward = isWin ? score * 2 : score;
const newCoins = user.coins + reward;
updateUserCoins.run({ id: discordId, coins: newCoins });
insertLog.run({
await userService.updateUserCoins(discordId, newCoins);
await logService.insertLog({
id: `${discordId}-snake-reward-${Date.now()}`,
user_id: discordId,
userId: discordId,
action: "SNAKE_GAME_REWARD",
coins_amount: reward,
user_new_amount: newCoins,
target_user_id: null,
coinsAmount: reward,
userNewAmount: newCoins,
targetUserId: null,
});
await emitDataUpdated({ table: "users", action: "update" });
return res.status(200).json({ message: `Récompense de ${reward} FlopoCoins attribuée !` });
@@ -1306,7 +1251,7 @@ export function apiRoutes(client, io) {
return res.status(400).json({ error: "Invalid offer" });
}
const user = getUser.get(userId);
const user = await userService.getUser(userId);
if (!user) {
return res.status(404).json({ error: "User not found" });
}
@@ -1394,14 +1339,14 @@ export function apiRoutes(client, io) {
}
// Check for duplicate processing (idempotency)
const existingTransaction = getTransactionBySessionId.get(session.id);
const existingTransaction = await transactionService.getTransactionBySessionId(session.id);
if (existingTransaction) {
console.log(`Payment already processed: ${session.id}`);
return res.status(200).json({ message: "Already processed" });
}
// Get user
const user = getUser.get(commandUserId);
const user = await userService.getUser(commandUserId);
if (!user) {
console.error(`User not found: ${commandUserId}`);
return res.status(404).json({ error: "User not found" });
@@ -1409,30 +1354,30 @@ export function apiRoutes(client, io) {
// Update coins
const newCoins = user.coins + expectedCoins;
updateUserCoins.run({ id: commandUserId, coins: newCoins });
await userService.updateUserCoins(commandUserId, newCoins);
// Insert transaction record
const transactionId = `${commandUserId}-transaction-${Date.now()}`;
insertTransaction.run({
await transactionService.insertTransaction({
id: transactionId,
session_id: session.id,
user_id: commandUserId,
coins_amount: expectedCoins,
amount_cents: amountPaid,
sessionId: session.id,
userId: commandUserId,
coinsAmount: expectedCoins,
amountCents: amountPaid,
currency: currency,
customer_email: customerEmail,
customer_name: customerName,
payment_status: session.payment_status,
customerEmail: customerEmail,
customerName: customerName,
paymentStatus: session.payment_status,
});
// Insert log entry
insertLog.run({
await logService.insertLog({
id: `${commandUserId}-buycoins-${Date.now()}`,
user_id: commandUserId,
userId: commandUserId,
action: "BUY_COINS",
target_user_id: null,
coins_amount: expectedCoins,
user_new_amount: newCoins,
targetUserId: null,
coinsAmount: expectedCoins,
userNewAmount: newCoins,
});
console.log(`Payment processed: ${commandUserId} purchased ${expectedCoins} coins for ${amountPaid/100} ${currency}`);

View File

@@ -15,7 +15,8 @@ import {
} from "../../game/blackjack.js";
// 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 { emitToast, emitUpdate, emitPlayerUpdate } from "../socket.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" });
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] = {
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 p = room.players[userId];
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 (!room.settings.fakeMoney) {
const userDB = getUser.get(userId);
const userDB = await userService.getUser(userId);
const coins = userDB?.coins ?? 0;
if (coins < bet) return res.status(403).json({ message: "insufficient-funds" });
updateUserCoins.run({ id: userId, coins: coins - bet });
insertLog.run({
await userService.updateUserCoins(userId, coins - bet);
await logService.insertLog({
id: `${userId}-blackjack-${Date.now()}`,
user_id: userId,
target_user_id: null,
userId: userId,
targetUserId: null,
action: "BLACKJACK_BET",
coins_amount: -bet,
user_new_amount: coins - bet,
coinsAmount: -bet,
userNewAmount: coins - bet,
});
p.bank = coins - bet;
}
@@ -261,7 +262,7 @@ export function blackjackRoutes(io) {
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 action = req.params.action;
const p = room.players[userId];
@@ -270,36 +271,36 @@ export function blackjackRoutes(io) {
// Handle extra coin lock for double
if (action === "double" && !room.settings.fakeMoney) {
const userDB = getUser.get(userId);
const userDB = await userService.getUser(userId);
const coins = userDB?.coins ?? 0;
const hand = p.hands[p.activeHand];
if (coins < hand.bet) return res.status(403).json({ message: "insufficient-funds-for-double" });
updateUserCoins.run({ id: userId, coins: coins - hand.bet });
insertLog.run({
await userService.updateUserCoins(userId, coins - hand.bet);
await logService.insertLog({
id: `${userId}-blackjack-${Date.now()}`,
user_id: userId,
target_user_id: null,
userId: userId,
targetUserId: null,
action: "BLACKJACK_DOUBLE",
coins_amount: -hand.bet,
user_new_amount: coins - hand.bet,
coinsAmount: -hand.bet,
userNewAmount: coins - hand.bet,
});
p.bank = coins - hand.bet;
// effective bet size is handled in settlement via hand.doubled flag
}
if (action === "split" && !room.settings.fakeMoney) {
const userDB = getUser.get(userId);
const userDB = await userService.getUser(userId);
const coins = userDB?.coins ?? 0;
const hand = p.hands[p.activeHand];
if (coins < hand.bet) return res.status(403).json({ message: "insufficient-funds-for-split" });
updateUserCoins.run({ id: userId, coins: coins - hand.bet });
insertLog.run({
await userService.updateUserCoins(userId, coins - hand.bet);
await logService.insertLog({
id: `${userId}-blackjack-${Date.now()}`,
user_id: userId,
target_user_id: null,
userId: userId,
targetUserId: null,
action: "BLACKJACK_SPLIT",
coins_amount: -hand.bet,
user_new_amount: coins - hand.bet,
coinsAmount: -hand.bet,
userNewAmount: coins - hand.bet,
});
p.bank = coins - hand.bet;
// 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 ---
// --- Discord.js Builder Imports ---
import { ButtonStyle } from "discord.js";
import {
getMarketOfferById,
getMarketOffers,
getMarketOffersBySkin,
getOfferBids,
getSkin,
getUser,
insertBid,
insertLog,
insertMarketOffer,
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 * as marketService from "../../services/market.service.js";
import { emitMarketUpdate } from "../socket.js";
import { handleNewMarketOffer, handleNewMarketOfferBid } from "../../utils/marketNotifs.js";
@@ -32,25 +24,26 @@ const router = express.Router();
export function marketRoutes(client, io) {
router.get("/offers", async (req, res) => {
try {
const offers = getMarketOffers.all();
offers.forEach((offer) => {
offer.skin = getSkin.get(offer.skin_uuid);
offer.seller = getUser.get(offer.seller_id);
offer.buyer = getUser.get(offer.buyer_id) || null;
offer.bids = getOfferBids.all(offer.id) || {};
offer.bids.forEach((bid) => {
bid.bidder = getUser.get(bid.bidder_id);
});
});
const offers = await marketService.getMarketOffers();
for (const offer of offers) {
offer.skin = await skinService.getSkin(offer.skinUuid);
offer.seller = await userService.getUser(offer.sellerId);
offer.buyer = offer.buyerId ? await userService.getUser(offer.buyerId) : null;
offer.bids = (await marketService.getOfferBids(offer.id)) || {};
for (const bid of offer.bids) {
bid.bidder = await userService.getUser(bid.bidderId);
}
}
res.status(200).send({ offers });
} catch (e) {
console.log(e);
res.status(500).send({ error: e });
}
});
router.get("/offers/:id", async (req, res) => {
try {
const offer = getMarketOfferById.get(req.params.id);
const offer = await marketService.getMarketOfferById(req.params.id);
if (offer) {
res.status(200).send({ offer });
} else {
@@ -63,7 +56,7 @@ export function marketRoutes(client, io) {
router.get("/offers/:id/bids", async (req, res) => {
try {
const bids = getOfferBids.get(req.params.id);
const bids = await marketService.getOfferBids(req.params.id);
res.status(200).send({ bids });
} catch (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 now = Date.now();
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" });
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 (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 (
existingOffers.length > 0 &&
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 offerId = Date.now() + "-" + seller.id + "-" + skin.uuid;
insertMarketOffer.run({
await marketService.insertMarketOffer({
id: offerId,
skin_uuid: skin.uuid,
seller_id: seller.id,
starting_price: starting_price,
buyout_price: null,
skinUuid: skin.uuid,
sellerId: seller.id,
startingPrice: starting_price,
buyoutPrice: null,
status: delay > 0 ? "pending" : "open",
opening_at: opening_at,
closing_at: closing_at,
openingAt: opening_at,
closingAt: closing_at,
});
await emitMarketUpdate();
await handleNewMarketOffer(offerId, client);
@@ -114,62 +107,62 @@ export function marketRoutes(client, io) {
router.post("/offers/:id/place-bid", async (req, res) => {
const { buyer_id, bid_amount, timestamp } = req.body;
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.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];
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" });
if (bid_amount < lastBid?.offer_amount + 10) {
if (bid_amount < lastBid?.offerAmount + 10) {
return res.status(403).send({ error: "Bid amount is below minimum" });
}
} 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" });
}
}
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.coins < bid_amount)
return res.status(403).send({ error: "You do not have enough coins to place this bid" });
const bidId = Date.now() + "-" + buyer_id + "-" + offer.id;
insertBid.run({
await marketService.insertBid({
id: bidId,
bidder_id: buyer_id,
market_offer_id: offer.id,
offer_amount: bid_amount,
bidderId: buyer_id,
marketOfferId: offer.id,
offerAmount: bid_amount,
});
const newCoinsAmount = bidder.coins - bid_amount;
updateUserCoins.run({ id: buyer_id, coins: newCoinsAmount });
insertLog.run({
await userService.updateUserCoins(buyer_id, newCoinsAmount);
await logService.insertLog({
id: `${buyer_id}-bid-${offer.id}-${Date.now()}`,
user_id: buyer_id,
userId: buyer_id,
action: "BID_PLACED",
target_user_id: null,
coins_amount: bid_amount,
user_new_amount: newCoinsAmount,
targetUserId: null,
coinsAmount: bid_amount,
userNewAmount: newCoinsAmount,
});
// Refund the previous highest bidder
if (lastBid) {
const previousBidder = getUser.get(lastBid.bidder_id);
const refundedCoinsAmount = previousBidder.coins + lastBid.offer_amount;
updateUserCoins.run({ id: previousBidder.id, coins: refundedCoinsAmount });
insertLog.run({
const previousBidder = await userService.getUser(lastBid.bidderId);
const refundedCoinsAmount = previousBidder.coins + lastBid.offerAmount;
await userService.updateUserCoins(previousBidder.id, refundedCoinsAmount);
await logService.insertLog({
id: `${previousBidder.id}-bid-refund-${offer.id}-${Date.now()}`,
user_id: previousBidder.id,
userId: previousBidder.id,
action: "BID_REFUNDED",
target_user_id: null,
coins_amount: lastBid.offer_amount,
user_new_amount: refundedCoinsAmount,
targetUserId: null,
coinsAmount: lastBid.offerAmount,
userNewAmount: refundedCoinsAmount,
});
}

View File

@@ -2,7 +2,8 @@ import express from "express";
import { v4 as uuidv4 } from "uuid";
import { monkePaths } from "../../game/state.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";
const router = express.Router();
@@ -16,11 +17,11 @@ const router = express.Router();
export function monkeRoutes(client, io) {
// --- Router Management Endpoints
router.get("/:userId", (req, res) => {
router.get("/:userId", async (req, res) => {
const { userId } = req.params;
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" });
const userGamePath = monkePaths[userId] || null;
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 });
});
router.post("/:userId/start", (req, res) => {
router.post("/:userId/start", async (req, res) => {
const { userId } = req.params;
const { initialBet } = req.body;
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 (!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" });
try {
const newCoins = user.coins - initialBet;
updateUserCoins.run({
id: userId,
coins: newCoins,
});
insertLog.run({
await userService.updateUserCoins(userId, newCoins);
await logService.insertLog({
id: `${userId}-monke-bet-${Date.now()}`,
user_id: userId,
target_user_id: null,
userId: userId,
targetUserId: null,
action: "MONKE_BET",
coins_amount: -initialBet,
user_new_amount: newCoins,
coinsAmount: -initialBet,
userNewAmount: newCoins,
});
} catch (error) {
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] });
});
router.post("/:userId/play", (req, res) => {
router.post("/:userId/play", async (req, res) => {
const { userId } = req.params;
const { choice, step } = req.body;
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 (!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;
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 (!monkePaths[userId]) return res.status(400).json({ error: "No active game found for this user" });
const userGamePath = monkePaths[userId];
@@ -112,17 +110,14 @@ export function monkeRoutes(client, io) {
const newCoins = coins + extractValue;
try {
updateUserCoins.run({
id: userId,
coins: newCoins,
});
insertLog.run({
await userService.updateUserCoins(userId, newCoins);
await logService.insertLog({
id: `${userId}-monke-withdraw-${Date.now()}`,
user_id: userId,
target_user_id: null,
userId: userId,
targetUserId: null,
action: "MONKE_WITHDRAW",
coins_amount: extractValue,
user_new_amount: newCoins,
coinsAmount: extractValue,
userNewAmount: newCoins,
});
return res.status(200).json({ message: "Game stopped", userGamePath });

View File

@@ -10,7 +10,8 @@ import {
getNextActivePlayer,
initialShuffledCards,
} 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 { client } from "../../bot/client.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])) {
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." });
}
@@ -147,19 +148,16 @@ export function pokerRoutes(client, io) {
}
if (!room.fakeMoney) {
const userDB = getUser.get(playerId);
const userDB = await userService.getUser(playerId);
if (userDB) {
updateUserCoins.run({
id: playerId,
coins: userDB.coins - room.minBet,
});
insertLog.run({
await userService.updateUserCoins(playerId, userDB.coins - room.minBet);
await logService.insertLog({
id: `${playerId}-poker-${Date.now()}`,
user_id: playerId,
target_user_id: null,
userId: playerId,
targetUserId: null,
action: "POKER_JOIN",
coins_amount: -room.minBet,
user_new_amount: userDB.coins - room.minBet,
coinsAmount: -room.minBet,
userNewAmount: userDB.coins - room.minBet,
});
}
}
@@ -199,7 +197,7 @@ export function pokerRoutes(client, io) {
}
try {
updatePlayerCoins(
await updatePlayerCoins(
pokerRooms[roomId].players[userId],
pokerRooms[roomId].players[userId].bank,
pokerRooms[roomId].fakeMoney,
@@ -239,7 +237,7 @@ export function pokerRoutes(client, io) {
}
try {
updatePlayerCoins(
await updatePlayerCoins(
pokerRooms[roomId].players[userId],
pokerRooms[roomId].players[userId].bank,
pokerRooms[roomId].fakeMoney,
@@ -359,7 +357,7 @@ export function pokerRoutes(client, io) {
async function joinRoom(roomId, userId, io) {
const user = await client.users.fetch(userId);
const userDB = getUser.get(userId);
const userDB = await userService.getUser(userId);
const room = pokerRooms[roomId];
const playerObject = {
@@ -380,14 +378,14 @@ async function joinRoom(roomId, userId, io) {
} else {
room.players[userId] = playerObject;
if (!room.fakeMoney) {
updateUserCoins.run({ id: userId, coins: userDB.coins - room.minBet });
insertLog.run({
await userService.updateUserCoins(userId, userDB.coins - room.minBet);
await logService.insertLog({
id: `${userId}-poker-${Date.now()}`,
user_id: userId,
target_user_id: null,
userId: userId,
targetUserId: null,
action: "POKER_JOIN",
coins_amount: -room.minBet,
user_new_amount: userDB.coins - room.minBet,
coinsAmount: -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;
const user = getUser.get(player.id);
const user = await userService.getUser(player.id);
if (!user) return;
const userDB = getUser.get(player.id);
updateUserCoins.run({ id: player.id, coins: userDB.coins + amount });
insertLog.run({
const userDB = await userService.getUser(player.id);
await userService.updateUserCoins(player.id, userDB.coins + amount);
await logService.insertLog({
id: `${player.id}-poker-${Date.now()}`,
user_id: player.id,
target_user_id: null,
userId: player.id,
targetUserId: null,
action: `POKER_${amount > 0 ? "WIN" : "LOSE"}`,
coins_amount: amount,
user_new_amount: userDB.coins + amount,
coinsAmount: amount,
userNewAmount: userDB.coins + amount,
});
}
async function clearAfkPlayers(room) {
Object.keys(room.afk).forEach((playerId) => {
for (const playerId of Object.keys(room.afk)) {
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];
}
});
}
room.afk = {};
}

View File

@@ -19,16 +19,9 @@ import {
// --- Game State & Database Imports ---
import { activeSolitaireGames } from "../../game/state.js";
import {
getSOTD,
getUser,
insertSOTDStats,
deleteUserSOTDStats,
getUserSOTDStats,
updateUserCoins,
insertLog,
getAllSOTDStats,
} from "../../database/index.js";
import * as userService from "../../services/user.service.js";
import * as logService from "../../services/log.service.js";
import * as solitaireService from "../../services/solitaire.service.js";
import { socketEmit } from "../socket.js";
// Create a new router instance
@@ -85,7 +78,7 @@ export function solitaireRoutes(client, io) {
res.json({ success: true, gameState });
});
router.post("/start/sotd", (req, res) => {
router.post("/start/sotd", async (req, res) => {
const { userId } = req.body;
/*if (!userId || !getUser.get(userId)) {
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) {
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 ---
router.get("/sotd/rankings", (req, res) => {
router.get("/sotd/rankings", async (req, res) => {
try {
const rankings = getAllSOTDStats.all();
const rankings = await solitaireService.getAllSOTDStats();
res.json({ rankings });
} catch (e) {
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. */
async function handleWin(userId, gameState, io) {
const currentUser = getUser.get(userId);
const currentUser = await userService.getUser(userId);
if (!currentUser) return;
if (gameState.hardMode) {
const bonus = 100;
const newCoins = currentUser.coins + bonus;
updateUserCoins.run({ id: userId, coins: newCoins });
insertLog.run({
await userService.updateUserCoins(userId, newCoins);
await logService.insertLog({
id: `${userId}-hardmode-solitaire-${Date.now()}`,
user_id: userId,
userId: userId,
action: "HARDMODE_SOLITAIRE_WIN",
target_user_id: null,
coins_amount: bonus,
user_new_amount: newCoins,
targetUserId: null,
coinsAmount: bonus,
userNewAmount: newCoins,
});
await socketEmit("data-updated", { table: "users" });
}
@@ -260,20 +253,20 @@ async function handleWin(userId, gameState, io) {
gameState.endTime = Date.now();
const timeTaken = gameState.endTime - gameState.startTime;
const existingStats = getUserSOTDStats.get(userId);
const existingStats = await solitaireService.getUserSOTDStats(userId);
if (!existingStats) {
// First time completing the SOTD, grant bonus coins
const bonus = 1000;
const newCoins = currentUser.coins + bonus;
updateUserCoins.run({ id: userId, coins: newCoins });
insertLog.run({
await userService.updateUserCoins(userId, newCoins);
await logService.insertLog({
id: `${userId}-sotd-complete-${Date.now()}`,
user_id: userId,
userId: userId,
action: "SOTD_WIN",
target_user_id: null,
coins_amount: bonus,
user_new_amount: newCoins,
targetUserId: null,
coinsAmount: bonus,
userNewAmount: newCoins,
});
await socketEmit("data-updated", { table: "users" });
}
@@ -288,10 +281,10 @@ async function handleWin(userId, gameState, io) {
timeTaken < existingStats.time);
if (isNewBest) {
deleteUserSOTDStats.run(userId);
insertSOTDStats.run({
await solitaireService.deleteUserSOTDStats(userId);
await solitaireService.insertSOTDStats({
id: userId,
user_id: userId,
userId: userId,
time: timeTaken,
moves: gameState.moves,
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 { isChampionsSkin } from "./index.js";
@@ -6,16 +6,15 @@ export async function drawCaseContent(caseType = "standard", poolSize = 100) {
if (caseType === "esport") {
// Esport case: return all esport skins
try {
const dbSkins = getAllAvailableSkins.all();
const esportSkins = skins
.filter((s) => dbSkins.find((dbSkin) => dbSkin.displayName.includes("Classic (VCT") && dbSkin.uuid === s.uuid))
.map((s) => {
const dbSkin = getSkin.get(s.uuid);
return {
...s, // Shallow copy to avoid mutating the imported 'skins' object
const dbSkins = await skinService.getAllAvailableSkins();
const esportSkins = [];
for (const s of skins.filter((s) => dbSkins.find((dbSkin) => dbSkin.displayName.includes("Classic (VCT") && dbSkin.uuid === s.uuid))) {
const dbSkin = await skinService.getSkin(s.uuid);
esportSkins.push({
...s,
tierColor: dbSkin?.tierColor,
};
});
}
return esportSkins;
} catch (e) {
console.log(e);
@@ -55,8 +54,8 @@ export async function drawCaseContent(caseType = "standard", poolSize = 100) {
}
try {
const dbSkins = getAllAvailableSkins.all();
const weightedPool = skins
const dbSkins = await skinService.getAllAvailableSkins();
const filtered = skins
.filter((s) => dbSkins.find((dbSkin) => dbSkin.uuid === s.uuid))
.filter((s) => {
if (caseType === "ultra") {
@@ -71,16 +70,19 @@ export async function drawCaseContent(caseType = "standard", poolSize = 100) {
} else {
return isChampionsSkin(s.displayName) === false;
}
})
.map((s) => {
const dbSkin = getSkin.get(s.uuid);
return {
...s, // Shallow copy to avoid mutating the imported 'skins' object
});
const weightedPool = [];
for (const s of filtered) {
const dbSkin = await skinService.getSkin(s.uuid);
const weight = tierWeights[s.contentTierUuid] ?? 0;
if (weight > 0) { // <--- CRITICAL: Remove 0 weight skins
weightedPool.push({
...s,
tierColor: dbSkin?.tierColor,
weight: tierWeights[s.contentTierUuid] ?? 0,
};
})
.filter((s) => s.weight > 0); // <--- CRITICAL: Remove 0 weight skins
weight,
});
}
}
function weightedSample(arr, count) {
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 randomSelectedSkinUuid;
try {
@@ -134,7 +136,7 @@ export function drawCaseSkin(caseContent) {
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);
if (!randomSkinData) {
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 { DiscordRequest } from "../api/discord.js";
import { initTodaysSOTD } from "../game/points.js";
import {
deleteBid,
deleteMarketOffer,
getAllAkhys,
getAllUsers,
getMarketOffers,
getOfferBids,
getSkin,
getUser,
insertManySkins,
insertUser,
resetDailyReward,
updateMarketOffer,
updateSkin,
updateUserAvatar,
updateUserCoins,
} from "../database/index.js";
import * as userService from "../services/user.service.js";
import * as skinService from "../services/skin.service.js";
import * as marketService from "../services/market.service.js";
import { activeInventories, activePredis, activeSearchs, pokerRooms, skins } from "../game/state.js";
import { emitMarketUpdate } from "../server/socket.js";
import { handleMarketOfferClosing, handleMarketOfferOpening } from "./marketNotifs.js";
@@ -49,7 +35,7 @@ export async function InstallGlobalCommands(appId, commands) {
export async function getAkhys(client) {
try {
// 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 members = await guild.members.fetch();
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) {
usersToInsert.forEach((user) => {
for (const user of usersToInsert) {
try {
insertUser.run(user);
await userService.insertUser(user);
} catch (err) {}
});
}
}
const new_akhys = getAllUsers.all().length;
const new_akhys = (await userService.getAllUsers()).length;
const diff = new_akhys - initial_akhys;
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})`,
@@ -97,17 +83,17 @@ export async function getAkhys(client) {
displayName: skin.displayName,
contentTierUuid: skin.contentTierUuid,
displayIcon: skin.displayIcon,
user_id: null,
tierRank: tier.rank,
userId: null,
tierRank: tier.rank != null ? String(tier.rank) : null,
tierColor: tier.highlightColor?.slice(0, 6) || "F2F3F3",
tierText: formatTierText(tier.rank, skin.displayName),
basePrice: basePrice.toFixed(0),
maxPrice: calculateMaxPrice(basePrice, skin).toFixed(0),
maxPrice: parseInt(calculateMaxPrice(basePrice, skin).toFixed(0)),
};
});
if (skinsToInsert.length > 0) {
insertManySkins(skinsToInsert);
await skinService.insertManySkins(skinsToInsert);
}
console.log(`[Sync] Fetched and synced ${skinsToInsert.length} Valorant skins.`);
} catch (err) {
@@ -175,7 +161,7 @@ export function setupCronJobs(client, io) {
cron.schedule(process.env.CRON_EXPR, async () => {
console.log("[Cron] Running daily midnight tasks...");
try {
resetDailyReward.run();
await userService.resetDailyReward();
console.log("[Cron] Daily rewards have been reset for all users.");
//if (!getSOTD.get()) {
initTodaysSOTD();
@@ -184,16 +170,16 @@ export function setupCronJobs(client, io) {
console.error("[Cron] Error during daily reset:", e);
}
try {
const offers = getMarketOffers.all();
const offers = await marketService.getMarketOffers();
const now = Date.now();
const TWO_DAYS = 2 * 24 * 60 * 60 * 1000;
for (const offer of offers) {
if (now >= offer.closing_at + TWO_DAYS) {
const offerBids = getOfferBids.all(offer.id);
if (now >= offer.closingAt + TWO_DAYS) {
const offerBids = await marketService.getOfferBids(offer.id);
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}`);
}
}
@@ -207,14 +193,11 @@ export function setupCronJobs(client, io) {
console.log("[Cron] Running daily 7 AM data sync...");
await getAkhys(client);
try {
const akhys = getAllAkhys.all();
const akhys = await userService.getAllAkhys();
for (const akhy of akhys) {
const user = await client.users.cache.get(akhy.id);
try {
updateUserAvatar.run({
id: akhy.id,
avatarUrl: user.displayAvatarURL({ dynamic: true, size: 256 }),
});
await userService.updateUserAvatar(akhy.id, user.displayAvatarURL({ dynamic: true, size: 256 }));
} catch (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 ---
function handleMarketOffersUpdate() {
async function handleMarketOffersUpdate() {
const now = Date.now();
const offers = getMarketOffers.all();
const offers = await marketService.getMarketOffers();
offers.forEach(async (offer) => {
if (now >= offer.opening_at && offer.status === "pending") {
updateMarketOffer.run({ id: offer.id, final_price: null, buyer_id: null, status: "open" });
if (now >= offer.openingAt && offer.status === "pending") {
await marketService.updateMarketOffer({ id: offer.id, finalPrice: null, buyerId: null, status: "open" });
await handleMarketOfferOpening(offer.id, client);
await emitMarketUpdate();
}
if (now >= offer.closing_at && offer.status !== "closed") {
const bids = getOfferBids.all(offer.id);
if (now >= offer.closingAt && offer.status !== "closed") {
const bids = await marketService.getOfferBids(offer.id);
if (bids.length === 0) {
// No bids placed, mark as closed without a sale
updateMarketOffer.run({
await marketService.updateMarketOffer({
id: offer.id,
buyer_id: null,
final_price: null,
buyerId: null,
finalPrice: null,
status: "closed",
});
await emitMarketUpdate();
} else {
const lastBid = bids[0];
const seller = getUser.get(offer.seller_id);
const buyer = getUser.get(lastBid.bidder_id);
const seller = await userService.getUser(offer.sellerId);
const buyer = await userService.getUser(lastBid.bidderId);
try {
// 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}`);
updateSkin.run({
user_id: buyer.id,
await skinService.updateSkin({
userId: buyer.id,
currentLvl: skin.currentLvl,
currentChroma: skin.currentChroma,
currentPrice: skin.currentPrice,
uuid: skin.uuid,
});
updateMarketOffer.run({
await marketService.updateMarketOffer({
id: offer.id,
buyer_id: buyer.id,
final_price: lastBid.offer_amount,
buyerId: buyer.id,
finalPrice: lastBid.offerAmount,
status: "closed",
});
const newUserCoins = seller.coins + lastBid.offer_amount;
updateUserCoins.run({ id: seller.id, coins: newUserCoins });
const newUserCoins = seller.coins + lastBid.offerAmount;
await userService.updateUserCoins(seller.id, newUserCoins);
await emitMarketUpdate();
} catch (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";
export async function handleNewMarketOffer(offerId, client) {
const offer = getMarketOfferById.get(offerId);
const offer = await marketService.getMarketOfferById(offerId);
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 {
const userSeller = getUser.get(offer.seller_id);
const userSeller = await userService.getUser(offer.sellerId);
if (discordUserSeller && userSeller?.isAkhy) {
const embed = new EmbedBuilder()
.setTitle("🔔 Offre créée")
.setDescription(`Ton offre pour le skin **${skin ? skin.displayName : offer.skin_uuid}** a bien été créée !`)
.setDescription(`Ton offre pour le skin **${skin ? skin.displayName : offer.skinUuid}** a bien été créée !`)
.setThumbnail(skin.displayIcon)
.setColor(0x5865f2) // Discord blurple
.addFields(
@@ -23,16 +25,16 @@ export async function handleNewMarketOffer(offerId, client) {
},
{
name: "💰 Prix de départ",
value: `\`${offer.starting_price} coins\``,
value: `\`${offer.startingPrice} coins\``,
inline: true,
},
{
name: "⏰ Ouverture",
value: `<t:${Math.floor(offer.opening_at / 1000)}:F>`,
value: `<t:${Math.floor(offer.openingAt / 1000)}:F>`,
},
{
name: "⏰ Fermeture",
value: `<t:${Math.floor(offer.closing_at / 1000)}:F>`,
value: `<t:${Math.floor(offer.closingAt / 1000)}:F>`,
},
{
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 embed = new EmbedBuilder()
.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)
.setColor(0x5865f2) // Discord blurple
.addFields(
{
name: "💰 Prix de départ",
value: `\`${offer.starting_price} coins\``,
value: `\`${offer.startingPrice} coins\``,
inline: true,
},
{
name: "⏰ Ouverture",
value: `<t:${Math.floor(offer.opening_at / 1000)}:F>`,
value: `<t:${Math.floor(offer.openingAt / 1000)}:F>`,
},
{
name: "⏰ Fermeture",
value: `<t:${Math.floor(offer.closing_at / 1000)}:F>`,
value: `<t:${Math.floor(offer.closingAt / 1000)}:F>`,
},
{
name: "Créée par",
value: `<@${offer.seller_id}> ${discordUserSeller ? "(" + discordUserSeller.username + ")" : ""}`,
value: `<@${offer.sellerId}> ${discordUserSeller ? "(" + discordUserSeller.username + ")" : ""}`,
},
)
.setTimestamp();
@@ -83,18 +85,18 @@ export async function handleNewMarketOffer(offerId, client) {
}
export async function handleMarketOfferOpening(offerId, client) {
const offer = getMarketOfferById.get(offerId);
const offer = await marketService.getMarketOfferById(offerId);
if (!offer) return;
const skin = getSkin.get(offer.skin_uuid);
const skin = await skinService.getSkin(offer.skinUuid);
try {
const discordUserSeller = await client.users.fetch(offer.seller_id);
const userSeller = getUser.get(offer.seller_id);
const discordUserSeller = await client.users.fetch(offer.sellerId);
const userSeller = await userService.getUser(offer.sellerId);
if (discordUserSeller && userSeller?.isAkhy) {
const embed = new EmbedBuilder()
.setTitle("🔔 Début des enchères")
.setDescription(
`Les enchères sur ton offre pour le skin **${skin ? skin.displayName : offer.skin_uuid}** viennent de commencer !`,
`Les enchères sur ton offre pour le skin **${skin ? skin.displayName : offer.skinUuid}** viennent de commencer !`,
)
.setThumbnail(skin.displayIcon)
.setColor(0x5865f2) // Discord blurple
@@ -106,12 +108,12 @@ export async function handleMarketOfferOpening(offerId, client) {
},
{
name: "💰 Prix de départ",
value: `\`${offer.starting_price} coins\``,
value: `\`${offer.startingPrice} coins\``,
inline: true,
},
{
name: "⏰ Fermeture",
value: `<t:${Math.floor(offer.closing_at / 1000)}:F>`,
value: `<t:${Math.floor(offer.closingAt / 1000)}:F>`,
},
{
name: "🆔 ID de loffre",
@@ -133,19 +135,19 @@ export async function handleMarketOfferOpening(offerId, client) {
const embed = new EmbedBuilder()
.setTitle("🔔 Début des enchères")
.setDescription(
`Les enchères sur l'offre pour le skin **${skin ? skin.displayName : offer.skin_uuid}** viennent de commencer !`,
`Les enchères sur l'offre pour le skin **${skin ? skin.displayName : offer.skinUuid}** viennent de commencer !`,
)
.setThumbnail(skin.displayIcon)
.setColor(0x5865f2) // Discord blurple
.addFields(
{
name: "💰 Prix de départ",
value: `\`${offer.starting_price} coins\``,
value: `\`${offer.startingPrice} coins\``,
inline: true,
},
{
name: "⏰ Fermeture",
value: `<t:${Math.floor(offer.closing_at / 1000)}:F>`,
value: `<t:${Math.floor(offer.closingAt / 1000)}:F>`,
},
)
.setTimestamp();
@@ -156,19 +158,19 @@ export async function handleMarketOfferOpening(offerId, client) {
}
export async function handleMarketOfferClosing(offerId, client) {
const offer = getMarketOfferById.get(offerId);
const offer = await marketService.getMarketOfferById(offerId);
if (!offer) return;
const skin = getSkin.get(offer.skin_uuid);
const bids = getOfferBids.all(offer.id);
const skin = await skinService.getSkin(offer.skinUuid);
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 {
const userSeller = getUser.get(offer.seller_id);
const userSeller = await userService.getUser(offer.sellerId);
if (discordUserSeller && userSeller?.isAkhy) {
const embed = new EmbedBuilder()
.setTitle("🔔 Fin des enchères")
.setDescription(
`Les enchères sur ton offre pour le skin **${skin ? skin.displayName : offer.skin_uuid}** viennent de se terminer !`,
`Les enchères sur ton offre pour le skin **${skin ? skin.displayName : offer.skinUuid}** viennent de se terminer !`,
)
.setThumbnail(skin.displayIcon)
.setColor(0x5865f2) // Discord blurple
@@ -188,11 +190,11 @@ export async function handleMarketOfferClosing(offerId, client) {
);
} else {
const highestBid = bids[0];
const highestBidderUser = await client.users.fetch(highestBid.bidder_id);
const highestBidderUser = await client.users.fetch(highestBid.bidderId);
embed.addFields(
{
name: "✅ Enchères terminées avec succès !",
value: `Ton skin a été vendu pour \`${highestBid.offer_amount} coins\` à <@${highestBid.bidder_id}> ${highestBidderUser ? "(" + highestBidderUser.username + ")" : ""}.`,
value: `Ton skin a été vendu pour \`${highestBid.offerAmount} coins\` à <@${highestBid.bidderId}> ${highestBidderUser ? "(" + highestBidderUser.username + ")" : ""}.`,
},
{
name: "🆔 ID de loffre",
@@ -216,7 +218,7 @@ export async function handleMarketOfferClosing(offerId, client) {
const embed = new EmbedBuilder()
.setTitle("🔔 Fin des enchères")
.setDescription(
`Les enchères sur l'offre pour le skin **${skin ? skin.displayName : offer.skin_uuid}** viennent de se terminer !`,
`Les enchères sur l'offre pour le skin **${skin ? skin.displayName : offer.skinUuid}** viennent de se terminer !`,
)
.setThumbnail(skin.displayIcon)
.setColor(0x5865f2) // Discord blurple
@@ -229,18 +231,18 @@ export async function handleMarketOfferClosing(offerId, client) {
});
} else {
const highestBid = bids[0];
const highestBidderUser = await client.users.fetch(highestBid.bidder_id);
const highestBidderUser = await client.users.fetch(highestBid.bidderId);
embed.addFields({
name: "✅ Enchères terminées avec succès !",
value: `Le skin de <@${offer.seller_id}> ${discordUserSeller ? "(" + discordUserSeller.username + ")" : ""} a été vendu pour \`${highestBid.offer_amount} coins\` à <@${highestBid.bidder_id}> ${highestBidderUser ? "(" + highestBidderUser.username + ")" : ""}.`,
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 userBidder = getUser.get(highestBid.bidder_id);
const discordUserBidder = await client.users.fetch(highestBid.bidderId);
const userBidder = await userService.getUser(highestBid.bidderId);
if (discordUserBidder && userBidder?.isAkhy) {
const embed = new EmbedBuilder()
.setTitle("🔔 Fin des enchères")
.setDescription(
`Les enchères sur l'offre pour le skin **${skin ? skin.displayName : offer.skin_uuid}** viennent de se terminer !`,
`Les enchères sur l'offre pour le skin **${skin ? skin.displayName : offer.skinUuid}** viennent de se terminer !`,
)
.setThumbnail(skin.displayIcon)
.setColor(0x5865f2) // Discord blurple
@@ -248,7 +250,7 @@ export async function handleMarketOfferClosing(offerId, client) {
const highestBid = bids[0];
embed.addFields({
name: "✅ Enchères terminées avec succès !",
value: `Tu as acheté ce skin pour \`${highestBid.offer_amount} coins\` à <@${offer.seller_id}> ${discordUserSeller ? "(" + discordUserSeller.username + ")" : ""}. Il a été ajouté à ton inventaire.`,
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);
@@ -262,39 +264,39 @@ export async function handleMarketOfferClosing(offerId, client) {
export async function handleNewMarketOfferBid(offerId, bidId, client) {
// Notify Seller and Bidder
const offer = getMarketOfferById.get(offerId);
const offer = await marketService.getMarketOfferById(offerId);
if (!offer) return;
const bid = getOfferBids.get(offerId);
const bid = (await marketService.getOfferBids(offerId))[0];
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 {
const discordUserSeller = await client.users.fetch(offer.seller_id);
const userSeller = getUser.get(offer.seller_id);
const discordUserSeller = await client.users.fetch(offer.sellerId);
const userSeller = await userService.getUser(offer.sellerId);
if (discordUserSeller && userSeller?.isAkhy) {
const embed = new EmbedBuilder()
.setTitle("🔔 Nouvelle enchère")
.setDescription(
`Il y a eu une nouvelle enchère sur ton offre pour le skin **${skin ? skin.displayName : offer.skin_uuid}**.`,
`Il y a eu une nouvelle enchère sur ton offre pour le skin **${skin ? skin.displayName : offer.skinUuid}**.`,
)
.setThumbnail(skin.displayIcon)
.setColor(0x5865f2) // Discord blurple
.addFields(
{
name: "👤 Enchérisseur",
value: `<@${bid.bidder_id}> ${bidderUser ? "(" + bidderUser.username + ")" : ""}`,
value: `<@${bid.bidderId}> ${bidderUser ? "(" + bidderUser.username + ")" : ""}`,
inline: true,
},
{
name: "💰 Montant de lenchère",
value: `\`${bid.offer_amount} coins\``,
value: `\`${bid.offerAmount} coins\``,
inline: true,
},
{
name: "⏰ Fermeture",
value: `<t:${Math.floor(offer.closing_at / 1000)}:F>`,
value: `<t:${Math.floor(offer.closingAt / 1000)}:F>`,
},
{
name: "🆔 ID de loffre",
@@ -311,19 +313,19 @@ export async function handleNewMarketOfferBid(offerId, bidId, client) {
}
try {
const discordUserNewBidder = await client.users.fetch(bid.bidder_id);
const userNewBidder = getUser.get(bid.bidder_id);
const discordUserNewBidder = await client.users.fetch(bid.bidderId);
const userNewBidder = await userService.getUser(bid.bidderId);
if (discordUserNewBidder && userNewBidder?.isAkhy) {
const embed = new EmbedBuilder()
.setTitle("🔔 Nouvelle enchère")
.setDescription(
`Ton enchère sur l'offre pour le skin **${skin ? skin.displayName : offer.skin_uuid}** a bien été placée!`,
`Ton enchère sur l'offre pour le skin **${skin ? skin.displayName : offer.skinUuid}** a bien été placée!`,
)
.setThumbnail(skin.displayIcon)
.setColor(0x5865f2) // Discord blurple
.addFields({
name: "💰 Montant de lenchère",
value: `\`${bid.offer_amount} coins\``,
value: `\`${bid.offerAmount} coins\``,
inline: true,
})
.setTimestamp();
@@ -335,28 +337,28 @@ export async function handleNewMarketOfferBid(offerId, bidId, client) {
}
try {
const offerBids = getOfferBids.all(offer.id);
const offerBids = await marketService.getOfferBids(offer.id);
if (offerBids.length < 2) return; // No previous bidder to notify
const discordUserPreviousBidder = await client.users.fetch(offerBids[1].bidder_id);
const userPreviousBidder = getUser.get(offerBids[1].bidder_id);
const discordUserPreviousBidder = await client.users.fetch(offerBids[1].bidderId);
const userPreviousBidder = await userService.getUser(offerBids[1].bidderId);
if (discordUserPreviousBidder && userPreviousBidder?.isAkhy) {
const embed = new EmbedBuilder()
.setTitle("🔔 Nouvelle enchère")
.setDescription(
`Quelqu'un a surenchéri sur l'offre pour le skin **${skin ? skin.displayName : offer.skin_uuid}**, tu n'es plus le meilleur enchérisseur !`,
`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)
.setColor(0x5865f2) // Discord blurple
.addFields(
{
name: "👤 Enchérisseur",
value: `<@${bid.bidder_id}> ${bidderUser ? "(" + bidderUser.username + ")" : ""}`,
value: `<@${bid.bidderId}> ${bidderUser ? "(" + bidderUser.username + ")" : ""}`,
inline: true,
},
{
name: "💰 Montant de lenchère",
value: `\`${bid.offer_amount} coins\``,
value: `\`${bid.offerAmount} coins\``,
inline: true,
},
)
@@ -373,7 +375,7 @@ export async function handleNewMarketOfferBid(offerId, bidId, client) {
export async function handleCaseOpening(caseType, userId, skinUuid, client) {
const discordUser = await client.users.fetch(userId);
const skin = getSkin.get(skinUuid);
const skin = await skinService.getSkin(skinUuid);
try {
const guildChannel = await client.channels.fetch(process.env.BOT_CHANNEL_ID);
const embed = new EmbedBuilder()