mirror of
https://github.com/cassoule/flopobot_v2.git
synced 2026-03-18 13:30:36 +01:00
3
.gitignore
vendored
3
.gitignore
vendored
@@ -4,4 +4,5 @@ flopobot.db
|
||||
flopobot.db-shm
|
||||
flopobot.db-wal
|
||||
.idea
|
||||
*.db
|
||||
*.db
|
||||
.claude
|
||||
|
||||
960
package-lock.json
generated
960
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -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,7 +29,9 @@
|
||||
"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",
|
||||
"uuid": "^11.1.0"
|
||||
},
|
||||
|
||||
138
prisma/migrations/0_init/migration.sql
Normal file
138
prisma/migrations/0_init/migration.sql
Normal 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
175
prisma/schema.prisma
Normal 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")
|
||||
}
|
||||
@@ -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) {
|
||||
|
||||
@@ -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";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { sleep } from "openai/core";
|
||||
import { AttachmentBuilder } from "discord.js";
|
||||
import {
|
||||
buildAiMessages,
|
||||
buildParticipantsMap,
|
||||
@@ -12,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";
|
||||
|
||||
@@ -52,10 +45,10 @@ export async function handleMessageCreate(message, client, io) {
|
||||
// --- Main Guild Features (Points & Slowmode) ---
|
||||
if (message.guildId === process.env.GUILD_ID) {
|
||||
// Award points for activity
|
||||
const pointsAwarded = channelPointsHandler(message);
|
||||
if (pointsAwarded) {
|
||||
io.emit("data-updated", { table: "users", action: "update" });
|
||||
}
|
||||
// const pointsAwarded = channelPointsHandler(message);
|
||||
// if (pointsAwarded) {
|
||||
// io.emit("data-updated", { table: "users", action: "update" });
|
||||
// }
|
||||
|
||||
// Enforce active slowmodes
|
||||
const wasSlowmoded = await slowmodesHandler(message, activeSlowmodes);
|
||||
@@ -88,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 ---
|
||||
@@ -104,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)) {
|
||||
@@ -134,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 {
|
||||
@@ -238,14 +231,18 @@ 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();
|
||||
message.reply("```json\n" + JSON.stringify(result, null, 2).substring(0, 1900) + "\n```");
|
||||
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" });
|
||||
message.reply({ content: "SQL query executed successfully:", files: [attachment] });
|
||||
} catch (e) {
|
||||
message.reply(`SQL Error: ${e.message}`);
|
||||
}
|
||||
@@ -263,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 = () => {
|
||||
@@ -283,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,
|
||||
@@ -298,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`:
|
||||
@@ -324,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;
|
||||
@@ -354,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,
|
||||
|
||||
@@ -1,863 +0,0 @@
|
||||
import Database from "better-sqlite3";
|
||||
|
||||
export const flopoDB = new Database(process.env.DB_PATH || "flopobot.db");
|
||||
|
||||
/* -------------------------
|
||||
CREATE ALL TABLES FIRST
|
||||
----------------------------*/
|
||||
flopoDB.exec(`
|
||||
CREATE TABLE IF NOT EXISTS users
|
||||
(
|
||||
id
|
||||
TEXT
|
||||
PRIMARY
|
||||
KEY,
|
||||
username
|
||||
TEXT
|
||||
NOT
|
||||
NULL,
|
||||
globalName
|
||||
TEXT,
|
||||
warned
|
||||
BOOLEAN
|
||||
DEFAULT
|
||||
0,
|
||||
warns
|
||||
INTEGER
|
||||
DEFAULT
|
||||
0,
|
||||
allTimeWarns
|
||||
INTEGER
|
||||
DEFAULT
|
||||
0,
|
||||
totalRequests
|
||||
INTEGER
|
||||
DEFAULT
|
||||
0,
|
||||
coins
|
||||
INTEGER
|
||||
DEFAULT
|
||||
0,
|
||||
dailyQueried
|
||||
BOOLEAN
|
||||
DEFAULT
|
||||
0,
|
||||
avatarUrl
|
||||
TEXT
|
||||
DEFAULT
|
||||
NULL,
|
||||
isAkhy
|
||||
BOOLEAN
|
||||
DEFAULT
|
||||
0
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS skins
|
||||
(
|
||||
uuid
|
||||
TEXT
|
||||
PRIMARY
|
||||
KEY,
|
||||
displayName
|
||||
TEXT,
|
||||
contentTierUuid
|
||||
TEXT,
|
||||
displayIcon
|
||||
TEXT,
|
||||
user_id
|
||||
TEXT
|
||||
REFERENCES
|
||||
users,
|
||||
tierRank
|
||||
TEXT,
|
||||
tierColor
|
||||
TEXT,
|
||||
tierText
|
||||
TEXT,
|
||||
basePrice
|
||||
TEXT,
|
||||
currentLvl
|
||||
INTEGER
|
||||
DEFAULT
|
||||
NULL,
|
||||
currentChroma
|
||||
INTEGER
|
||||
DEFAULT
|
||||
NULL,
|
||||
currentPrice
|
||||
INTEGER
|
||||
DEFAULT
|
||||
NULL,
|
||||
maxPrice
|
||||
INTEGER
|
||||
DEFAULT
|
||||
NULL
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS market_offers
|
||||
(
|
||||
id
|
||||
PRIMARY
|
||||
KEY,
|
||||
skin_uuid
|
||||
TEXT
|
||||
REFERENCES
|
||||
skins,
|
||||
seller_id
|
||||
TEXT
|
||||
REFERENCES
|
||||
users,
|
||||
starting_price
|
||||
INTEGER
|
||||
NOT
|
||||
NULL,
|
||||
buyout_price
|
||||
INTEGER
|
||||
DEFAULT
|
||||
NULL,
|
||||
final_price
|
||||
INTEGER
|
||||
DEFAULT
|
||||
NULL,
|
||||
status
|
||||
TEXT
|
||||
NOT
|
||||
NULL,
|
||||
posted_at
|
||||
TIMESTAMP
|
||||
DEFAULT
|
||||
CURRENT_TIMESTAMP,
|
||||
opening_at
|
||||
TIMESTAMP
|
||||
NOT
|
||||
NULL,
|
||||
closing_at
|
||||
TIMESTAMP
|
||||
NOT
|
||||
NULL,
|
||||
buyer_id
|
||||
TEXT
|
||||
REFERENCES
|
||||
users
|
||||
DEFAULT
|
||||
NULL
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS bids
|
||||
(
|
||||
id
|
||||
PRIMARY
|
||||
KEY,
|
||||
bidder_id
|
||||
TEXT
|
||||
REFERENCES
|
||||
users,
|
||||
market_offer_id
|
||||
REFERENCES
|
||||
market_offers,
|
||||
offer_amount
|
||||
INTEGER,
|
||||
offered_at
|
||||
TIMESTAMP
|
||||
DEFAULT
|
||||
CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS logs
|
||||
(
|
||||
id
|
||||
PRIMARY
|
||||
KEY,
|
||||
user_id
|
||||
TEXT
|
||||
REFERENCES
|
||||
users,
|
||||
action
|
||||
TEXT,
|
||||
target_user_id
|
||||
TEXT
|
||||
REFERENCES
|
||||
users,
|
||||
coins_amount
|
||||
INTEGER,
|
||||
user_new_amount
|
||||
INTEGER,
|
||||
created_at
|
||||
TIMESTAMP
|
||||
DEFAULT
|
||||
CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS games
|
||||
(
|
||||
id
|
||||
PRIMARY
|
||||
KEY,
|
||||
p1
|
||||
TEXT
|
||||
REFERENCES
|
||||
users,
|
||||
p2
|
||||
TEXT
|
||||
REFERENCES
|
||||
users,
|
||||
p1_score
|
||||
INTEGER,
|
||||
p2_score
|
||||
INTEGER,
|
||||
p1_elo
|
||||
INTEGER,
|
||||
p2_elo
|
||||
INTEGER,
|
||||
p1_new_elo
|
||||
INTEGER,
|
||||
p2_new_elo
|
||||
INTEGER,
|
||||
type
|
||||
TEXT,
|
||||
timestamp
|
||||
TIMESTAMP
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS elos
|
||||
(
|
||||
id
|
||||
PRIMARY
|
||||
KEY
|
||||
REFERENCES
|
||||
users,
|
||||
elo
|
||||
INTEGER
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS sotd
|
||||
(
|
||||
id
|
||||
INT
|
||||
PRIMARY
|
||||
KEY,
|
||||
tableauPiles
|
||||
TEXT,
|
||||
foundationPiles
|
||||
TEXT,
|
||||
stockPile
|
||||
TEXT,
|
||||
wastePile
|
||||
TEXT,
|
||||
isDone
|
||||
BOOLEAN
|
||||
DEFAULT
|
||||
false,
|
||||
seed
|
||||
TEXT
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS sotd_stats
|
||||
(
|
||||
id
|
||||
TEXT
|
||||
PRIMARY
|
||||
KEY,
|
||||
user_id
|
||||
TEXT
|
||||
REFERENCES
|
||||
users,
|
||||
time
|
||||
INTEGER,
|
||||
moves
|
||||
INTEGER,
|
||||
score
|
||||
INTEGER
|
||||
);
|
||||
`);
|
||||
|
||||
/* -----------------------------------------------------
|
||||
PREPARE ANY CREATE TABLE STATEMENT OBJECTS (kept for parity)
|
||||
------------------------------------------------------*/
|
||||
|
||||
export const stmtUsers = flopoDB.prepare(`
|
||||
CREATE TABLE IF NOT EXISTS users
|
||||
(
|
||||
id
|
||||
TEXT
|
||||
PRIMARY
|
||||
KEY,
|
||||
username
|
||||
TEXT
|
||||
NOT
|
||||
NULL,
|
||||
globalName
|
||||
TEXT,
|
||||
warned
|
||||
BOOLEAN
|
||||
DEFAULT
|
||||
0,
|
||||
warns
|
||||
INTEGER
|
||||
DEFAULT
|
||||
0,
|
||||
allTimeWarns
|
||||
INTEGER
|
||||
DEFAULT
|
||||
0,
|
||||
totalRequests
|
||||
INTEGER
|
||||
DEFAULT
|
||||
0,
|
||||
coins
|
||||
INTEGER
|
||||
DEFAULT
|
||||
0,
|
||||
dailyQueried
|
||||
BOOLEAN
|
||||
DEFAULT
|
||||
0,
|
||||
avatarUrl
|
||||
TEXT
|
||||
DEFAULT
|
||||
NULL,
|
||||
isAkhy
|
||||
BOOLEAN
|
||||
DEFAULT
|
||||
0
|
||||
)
|
||||
`);
|
||||
stmtUsers.run();
|
||||
|
||||
export const stmtSkins = flopoDB.prepare(`
|
||||
CREATE TABLE IF NOT EXISTS skins
|
||||
(
|
||||
uuid
|
||||
TEXT
|
||||
PRIMARY
|
||||
KEY,
|
||||
displayName
|
||||
TEXT,
|
||||
contentTierUuid
|
||||
TEXT,
|
||||
displayIcon
|
||||
TEXT,
|
||||
user_id
|
||||
TEXT
|
||||
REFERENCES
|
||||
users,
|
||||
tierRank
|
||||
TEXT,
|
||||
tierColor
|
||||
TEXT,
|
||||
tierText
|
||||
TEXT,
|
||||
basePrice
|
||||
TEXT,
|
||||
currentLvl
|
||||
INTEGER
|
||||
DEFAULT
|
||||
NULL,
|
||||
currentChroma
|
||||
INTEGER
|
||||
DEFAULT
|
||||
NULL,
|
||||
currentPrice
|
||||
INTEGER
|
||||
DEFAULT
|
||||
NULL,
|
||||
maxPrice
|
||||
INTEGER
|
||||
DEFAULT
|
||||
NULL
|
||||
)
|
||||
`);
|
||||
stmtSkins.run();
|
||||
|
||||
export const stmtMarketOffers = flopoDB.prepare(`
|
||||
CREATE TABLE IF NOT EXISTS market_offers
|
||||
(
|
||||
id
|
||||
PRIMARY
|
||||
KEY,
|
||||
skin_uuid
|
||||
TEXT
|
||||
REFERENCES
|
||||
skins,
|
||||
seller_id
|
||||
TEXT
|
||||
REFERENCES
|
||||
users,
|
||||
starting_price
|
||||
INTEGER
|
||||
NOT
|
||||
NULL,
|
||||
buyout_price
|
||||
INTEGER
|
||||
DEFAULT
|
||||
NULL,
|
||||
final_price
|
||||
INTEGER
|
||||
DEFAULT
|
||||
NULL,
|
||||
status
|
||||
TEXT
|
||||
NOT
|
||||
NULL,
|
||||
posted_at
|
||||
TIMESTAMP
|
||||
DEFAULT
|
||||
CURRENT_TIMESTAMP,
|
||||
opening_at
|
||||
TIMESTAMP
|
||||
NOT
|
||||
NULL,
|
||||
closing_at
|
||||
TIMESTAMP
|
||||
NOT
|
||||
NULL,
|
||||
buyer_id
|
||||
TEXT
|
||||
REFERENCES
|
||||
users
|
||||
DEFAULT
|
||||
NULL
|
||||
)
|
||||
`);
|
||||
stmtMarketOffers.run();
|
||||
|
||||
export const stmtBids = flopoDB.prepare(`
|
||||
CREATE TABLE IF NOT EXISTS bids
|
||||
(
|
||||
id
|
||||
PRIMARY
|
||||
KEY,
|
||||
bidder_id
|
||||
TEXT
|
||||
REFERENCES
|
||||
users,
|
||||
market_offer_id
|
||||
REFERENCES
|
||||
market_offers,
|
||||
offer_amount
|
||||
INTEGER,
|
||||
offered_at
|
||||
TIMESTAMP
|
||||
DEFAULT
|
||||
CURRENT_TIMESTAMP
|
||||
)
|
||||
`);
|
||||
stmtBids.run();
|
||||
|
||||
export const stmtLogs = flopoDB.prepare(`
|
||||
CREATE TABLE IF NOT EXISTS logs
|
||||
(
|
||||
id
|
||||
PRIMARY
|
||||
KEY,
|
||||
user_id
|
||||
TEXT
|
||||
REFERENCES
|
||||
users,
|
||||
action
|
||||
TEXT,
|
||||
target_user_id
|
||||
TEXT
|
||||
REFERENCES
|
||||
users,
|
||||
coins_amount
|
||||
INTEGER,
|
||||
user_new_amount
|
||||
INTEGER,
|
||||
created_at
|
||||
TIMESTAMP
|
||||
DEFAULT
|
||||
CURRENT_TIMESTAMP
|
||||
)
|
||||
`);
|
||||
stmtLogs.run();
|
||||
|
||||
export const stmtGames = flopoDB.prepare(`
|
||||
CREATE TABLE IF NOT EXISTS games
|
||||
(
|
||||
id
|
||||
PRIMARY
|
||||
KEY,
|
||||
p1
|
||||
TEXT
|
||||
REFERENCES
|
||||
users,
|
||||
p2
|
||||
TEXT
|
||||
REFERENCES
|
||||
users,
|
||||
p1_score
|
||||
INTEGER,
|
||||
p2_score
|
||||
INTEGER,
|
||||
p1_elo
|
||||
INTEGER,
|
||||
p2_elo
|
||||
INTEGER,
|
||||
p1_new_elo
|
||||
INTEGER,
|
||||
p2_new_elo
|
||||
INTEGER,
|
||||
type
|
||||
TEXT,
|
||||
timestamp
|
||||
TIMESTAMP
|
||||
)
|
||||
`);
|
||||
stmtGames.run();
|
||||
|
||||
export const stmtElos = flopoDB.prepare(`
|
||||
CREATE TABLE IF NOT EXISTS elos
|
||||
(
|
||||
id
|
||||
PRIMARY
|
||||
KEY
|
||||
REFERENCES
|
||||
users,
|
||||
elo
|
||||
INTEGER
|
||||
)
|
||||
`);
|
||||
stmtElos.run();
|
||||
|
||||
export const stmtSOTD = flopoDB.prepare(`
|
||||
CREATE TABLE IF NOT EXISTS sotd
|
||||
(
|
||||
id
|
||||
INT
|
||||
PRIMARY
|
||||
KEY,
|
||||
tableauPiles
|
||||
TEXT,
|
||||
foundationPiles
|
||||
TEXT,
|
||||
stockPile
|
||||
TEXT,
|
||||
wastePile
|
||||
TEXT,
|
||||
isDone
|
||||
BOOLEAN
|
||||
DEFAULT
|
||||
false,
|
||||
seed
|
||||
TEXT
|
||||
)
|
||||
`);
|
||||
stmtSOTD.run();
|
||||
|
||||
export const stmtSOTDStats = flopoDB.prepare(`
|
||||
CREATE TABLE IF NOT EXISTS sotd_stats
|
||||
(
|
||||
id
|
||||
TEXT
|
||||
PRIMARY
|
||||
KEY,
|
||||
user_id
|
||||
TEXT
|
||||
REFERENCES
|
||||
users,
|
||||
time
|
||||
INTEGER,
|
||||
moves
|
||||
INTEGER,
|
||||
score
|
||||
INTEGER
|
||||
)
|
||||
`);
|
||||
stmtSOTDStats.run();
|
||||
|
||||
/* -------------------------
|
||||
USER statements
|
||||
----------------------------*/
|
||||
export const insertUser = flopoDB.prepare(
|
||||
`INSERT INTO users (id, username, globalName, warned, warns, allTimeWarns, totalRequests, avatarUrl, isAkhy)
|
||||
VALUES (@id, @username, @globalName, @warned, @warns, @allTimeWarns, @totalRequests, @avatarUrl, @isAkhy)`,
|
||||
);
|
||||
export const updateUser = flopoDB.prepare(
|
||||
`UPDATE users
|
||||
SET warned = @warned,
|
||||
warns = @warns,
|
||||
allTimeWarns = @allTimeWarns,
|
||||
totalRequests = @totalRequests
|
||||
WHERE id = @id`,
|
||||
);
|
||||
export const updateUserAvatar = flopoDB.prepare("UPDATE users SET avatarUrl = @avatarUrl WHERE id = @id");
|
||||
export const queryDailyReward = flopoDB.prepare(`UPDATE users
|
||||
SET dailyQueried = 1
|
||||
WHERE id = ?`
|
||||
);
|
||||
export const resetDailyReward = flopoDB.prepare(`UPDATE users
|
||||
SET dailyQueried = 0`
|
||||
);
|
||||
export const updateUserCoins = flopoDB.prepare("UPDATE users SET coins = @coins WHERE id = @id");
|
||||
export const getUser = flopoDB.prepare(
|
||||
"SELECT users.*,elos.elo FROM users LEFT JOIN elos ON elos.id = users.id WHERE users.id = ?",
|
||||
);
|
||||
export const getAllUsers = flopoDB.prepare(
|
||||
"SELECT users.*,elos.elo FROM users LEFT JOIN elos ON elos.id = users.id ORDER BY coins DESC",
|
||||
);
|
||||
export const getAllAkhys = flopoDB.prepare(
|
||||
"SELECT users.*,elos.elo FROM users LEFT JOIN elos ON elos.id = users.id WHERE isAkhy = 1 ORDER BY coins DESC",
|
||||
);
|
||||
|
||||
/* -------------------------
|
||||
SKINS statements
|
||||
----------------------------*/
|
||||
export const insertSkin = flopoDB.prepare(
|
||||
`INSERT INTO skins (uuid, displayName, contentTierUuid, displayIcon, user_id, tierRank, tierColor, tierText,
|
||||
basePrice, maxPrice)
|
||||
VALUES (@uuid, @displayName, @contentTierUuid, @displayIcon, @user_id, @tierRank, @tierColor, @tierText,
|
||||
@basePrice, @maxPrice)`,
|
||||
);
|
||||
export const updateSkin = flopoDB.prepare(
|
||||
`UPDATE skins
|
||||
SET user_id = @user_id,
|
||||
currentLvl = @currentLvl,
|
||||
currentChroma = @currentChroma,
|
||||
currentPrice = @currentPrice
|
||||
WHERE uuid = @uuid`,
|
||||
);
|
||||
export const hardUpdateSkin = flopoDB.prepare(
|
||||
`UPDATE skins
|
||||
SET displayName = @displayName,
|
||||
contentTierUuid = @contentTierUuid,
|
||||
displayIcon = @displayIcon,
|
||||
tierRank = @tierRank,
|
||||
tierColor = @tierColor,
|
||||
tierText = @tierText,
|
||||
basePrice = @basePrice,
|
||||
user_id = @user_id,
|
||||
currentLvl = @currentLvl,
|
||||
currentChroma = @currentChroma,
|
||||
currentPrice = @currentPrice,
|
||||
maxPrice = @maxPrice
|
||||
WHERE uuid = @uuid`,
|
||||
);
|
||||
export const getSkin = flopoDB.prepare("SELECT * FROM skins WHERE uuid = ?");
|
||||
export const getAllSkins = flopoDB.prepare("SELECT * FROM skins ORDER BY maxPrice DESC");
|
||||
export const getAllAvailableSkins = flopoDB.prepare("SELECT * FROM skins WHERE user_id IS NULL");
|
||||
export const getUserInventory = flopoDB.prepare(
|
||||
"SELECT * FROM skins WHERE user_id = @user_id ORDER BY currentPrice DESC",
|
||||
);
|
||||
export const getTopSkins = flopoDB.prepare("SELECT * FROM skins ORDER BY maxPrice DESC LIMIT 10");
|
||||
|
||||
/* -------------------------
|
||||
MARKET / BIDS / OFFERS
|
||||
----------------------------*/
|
||||
export const getMarketOffers = flopoDB.prepare(`
|
||||
SELECT *
|
||||
FROM market_offers
|
||||
ORDER BY market_offers.posted_at DESC
|
||||
`);
|
||||
export const getMarketOfferById = flopoDB.prepare(`
|
||||
SELECT market_offers.*,
|
||||
skins.displayName AS skinName,
|
||||
skins.displayIcon AS skinIcon,
|
||||
seller.username AS sellerName,
|
||||
seller.globalName AS sellerGlobalName,
|
||||
buyer.username AS buyerName,
|
||||
buyer.globalName AS buyerGlobalName
|
||||
FROM market_offers
|
||||
JOIN skins ON skins.uuid = market_offers.skin_uuid
|
||||
JOIN users AS seller ON seller.id = market_offers.seller_id
|
||||
LEFT JOIN users AS buyer ON buyer.id = market_offers.buyer_id
|
||||
WHERE market_offers.id = ?
|
||||
`);
|
||||
export const getMarketOffersBySkin = flopoDB.prepare(`
|
||||
SELECT market_offers.*,
|
||||
skins.displayName AS skinName,
|
||||
skins.displayIcon AS skinIcon,
|
||||
seller.username AS sellerName,
|
||||
seller.globalName AS sellerGlobalName,
|
||||
buyer.username AS buyerName,
|
||||
buyer.globalName AS buyerGlobalName
|
||||
FROM market_offers
|
||||
JOIN skins ON skins.uuid = market_offers.skin_uuid
|
||||
JOIN users AS seller ON seller.id = market_offers.seller_id
|
||||
LEFT JOIN users AS buyer ON buyer.id = market_offers.buyer_id
|
||||
WHERE market_offers.skin_uuid = ?
|
||||
`);
|
||||
export const insertMarketOffer = flopoDB.prepare(`
|
||||
INSERT INTO market_offers (id, skin_uuid, seller_id, starting_price, buyout_price, status, opening_at, closing_at)
|
||||
VALUES (@id, @skin_uuid, @seller_id, @starting_price, @buyout_price, @status, @opening_at, @closing_at)
|
||||
`);
|
||||
export const updateMarketOffer = flopoDB.prepare(`
|
||||
UPDATE market_offers
|
||||
SET final_price = @final_price,
|
||||
status = @status,
|
||||
buyer_id = @buyer_id
|
||||
WHERE id = @id
|
||||
`);
|
||||
export const deleteMarketOffer = flopoDB.prepare(`
|
||||
DELETE
|
||||
FROM market_offers
|
||||
WHERE id = ?
|
||||
`);
|
||||
|
||||
/* -------------------------
|
||||
BIDS
|
||||
----------------------------*/
|
||||
export const getBids = flopoDB.prepare(`
|
||||
SELECT bids.*,
|
||||
bidder.username AS bidderName,
|
||||
bidder.globalName AS bidderGlobalName
|
||||
FROM bids
|
||||
JOIN users AS bidder ON bidder.id = bids.bidder_id
|
||||
ORDER BY bids.offer_amount DESC, bids.offered_at ASC
|
||||
`);
|
||||
export const getBidById = flopoDB.prepare(`
|
||||
SELECT bids.*
|
||||
FROM bids
|
||||
WHERE bids.id = ?
|
||||
`);
|
||||
export const getOfferBids = flopoDB.prepare(`
|
||||
SELECT bids.*
|
||||
FROM bids
|
||||
WHERE bids.market_offer_id = ?
|
||||
ORDER BY bids.offer_amount DESC, bids.offered_at ASC
|
||||
`);
|
||||
export const insertBid = flopoDB.prepare(`
|
||||
INSERT INTO bids (id, bidder_id, market_offer_id, offer_amount)
|
||||
VALUES (@id, @bidder_id, @market_offer_id, @offer_amount)
|
||||
`);
|
||||
export const deleteBid = flopoDB.prepare(`
|
||||
DELETE
|
||||
FROM bids
|
||||
WHERE id = ?
|
||||
`);
|
||||
|
||||
/* -------------------------
|
||||
BULK TRANSACTIONS (synchronous)
|
||||
----------------------------*/
|
||||
export const insertManyUsers = flopoDB.transaction((users) => {
|
||||
for (const user of users)
|
||||
try {
|
||||
insertUser.run(user);
|
||||
} catch (e) {}
|
||||
});
|
||||
export const updateManyUsers = flopoDB.transaction((users) => {
|
||||
for (const user of users)
|
||||
try {
|
||||
updateUser.run(user);
|
||||
} catch (e) {
|
||||
console.log(`User update failed`);
|
||||
}
|
||||
});
|
||||
export const insertManySkins = flopoDB.transaction((skins) => {
|
||||
for (const skin of skins)
|
||||
try {
|
||||
insertSkin.run(skin);
|
||||
} catch (e) {}
|
||||
});
|
||||
export const updateManySkins = flopoDB.transaction((skins) => {
|
||||
for (const skin of skins)
|
||||
try {
|
||||
updateSkin.run(skin);
|
||||
} catch (e) {}
|
||||
});
|
||||
|
||||
/* -------------------------
|
||||
LOGS
|
||||
----------------------------*/
|
||||
export const insertLog = flopoDB.prepare(
|
||||
`INSERT INTO logs (id, user_id, action, target_user_id, coins_amount, user_new_amount)
|
||||
VALUES (@id, @user_id, @action, @target_user_id, @coins_amount, @user_new_amount)`,
|
||||
);
|
||||
export const getLogs = flopoDB.prepare("SELECT * FROM logs");
|
||||
export const getUserLogs = flopoDB.prepare("SELECT * FROM logs WHERE user_id = @user_id");
|
||||
|
||||
/* -------------------------
|
||||
GAMES
|
||||
----------------------------*/
|
||||
export const insertGame = flopoDB.prepare(
|
||||
`INSERT INTO games (id, p1, p2, p1_score, p2_score, p1_elo, p2_elo, p1_new_elo, p2_new_elo, type, timestamp)
|
||||
VALUES (@id, @p1, @p2, @p1_score, @p2_score, @p1_elo, @p2_elo, @p1_new_elo, @p2_new_elo, @type, @timestamp)`,
|
||||
);
|
||||
export const getGames = flopoDB.prepare("SELECT * FROM games");
|
||||
export const getUserGames = flopoDB.prepare(
|
||||
"SELECT * FROM games WHERE p1 = @user_id OR p2 = @user_id ORDER BY timestamp",
|
||||
);
|
||||
|
||||
/* -------------------------
|
||||
ELOS
|
||||
----------------------------*/
|
||||
export const insertElos = flopoDB.prepare(`INSERT INTO elos (id, elo)
|
||||
VALUES (@id, @elo)`
|
||||
);
|
||||
export const getElos = flopoDB.prepare(`SELECT *
|
||||
FROM elos`
|
||||
);
|
||||
export const getUserElo = flopoDB.prepare(`SELECT *
|
||||
FROM elos
|
||||
WHERE id = @id`
|
||||
);
|
||||
export const updateElo = flopoDB.prepare("UPDATE elos SET elo = @elo WHERE id = @id");
|
||||
export const getUsersByElo = flopoDB.prepare(
|
||||
"SELECT * FROM users JOIN elos ON elos.id = users.id ORDER BY elos.elo DESC",
|
||||
);
|
||||
|
||||
/* -------------------------
|
||||
SOTD
|
||||
----------------------------*/
|
||||
export const getSOTD = flopoDB.prepare(`SELECT *
|
||||
FROM sotd
|
||||
WHERE id = '0'`
|
||||
);
|
||||
export const insertSOTD =
|
||||
flopoDB.prepare(`INSERT INTO sotd (id, tableauPiles, foundationPiles, stockPile, wastePile, seed)
|
||||
VALUES (0, @tableauPiles, @foundationPiles, @stockPile, @wastePile, @seed)`);
|
||||
export const deleteSOTD = flopoDB.prepare(`DELETE
|
||||
FROM sotd
|
||||
WHERE id = '0'`
|
||||
);
|
||||
export const getAllSOTDStats = flopoDB.prepare(`SELECT sotd_stats.*, users.globalName
|
||||
FROM sotd_stats
|
||||
JOIN users ON users.id = sotd_stats.user_id
|
||||
ORDER BY score DESC, moves ASC, time ASC`);
|
||||
export const getUserSOTDStats = flopoDB.prepare(`SELECT *
|
||||
FROM sotd_stats
|
||||
WHERE user_id = ?`);
|
||||
export const insertSOTDStats = flopoDB.prepare(`INSERT INTO sotd_stats (id, user_id, time, moves, score)
|
||||
VALUES (@id, @user_id, @time, @moves, @score)`);
|
||||
export const clearSOTDStats = flopoDB.prepare(`DELETE
|
||||
FROM sotd_stats`);
|
||||
export const deleteUserSOTDStats = flopoDB.prepare(`DELETE
|
||||
FROM sotd_stats
|
||||
WHERE user_id = ?`);
|
||||
|
||||
/* -------------------------
|
||||
pruneOldLogs
|
||||
----------------------------*/
|
||||
export async function pruneOldLogs() {
|
||||
const users = flopoDB
|
||||
.prepare(
|
||||
`
|
||||
SELECT user_id
|
||||
FROM logs
|
||||
GROUP BY user_id
|
||||
HAVING COUNT(*) > ${process.env.LOGS_BY_USER}
|
||||
`,
|
||||
)
|
||||
.all();
|
||||
|
||||
const transaction = flopoDB.transaction(() => {
|
||||
for (const { user_id } of users) {
|
||||
flopoDB
|
||||
.prepare(
|
||||
`
|
||||
DELETE
|
||||
FROM logs
|
||||
WHERE id IN (SELECT id
|
||||
FROM (SELECT id,
|
||||
ROW_NUMBER() OVER (ORDER BY created_at DESC) AS rn
|
||||
FROM logs
|
||||
WHERE user_id = ?)
|
||||
WHERE rn > ${process.env.LOGS_BY_USER})
|
||||
`,
|
||||
)
|
||||
.run(user_id);
|
||||
}
|
||||
});
|
||||
|
||||
transaction();
|
||||
}
|
||||
@@ -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) {
|
||||
|
||||
@@ -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}.`);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
5
src/prisma/client.js
Normal file
@@ -0,0 +1,5 @@
|
||||
import { PrismaClient } from "@prisma/client";
|
||||
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
export default prisma;
|
||||
@@ -37,6 +37,9 @@ app.post("/interactions", verifyKeyMiddleware(process.env.PUBLIC_KEY), async (re
|
||||
await handleInteraction(req, res, client);
|
||||
});
|
||||
|
||||
// Stripe webhook endpoint needs raw body for signature verification
|
||||
app.use("/api/buy-coins", express.raw({ type: "application/json" }));
|
||||
|
||||
// JSON Body Parser Middleware
|
||||
app.use(express.json());
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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 });
|
||||
|
||||
@@ -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 = {};
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
49
src/services/game.service.js
Normal file
49
src/services/game.service.js
Normal 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);
|
||||
}
|
||||
33
src/services/log.service.js
Normal file
33
src/services/log.service.js
Normal 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,
|
||||
);
|
||||
}
|
||||
}
|
||||
107
src/services/market.service.js
Normal file
107
src/services/market.service.js
Normal 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 } });
|
||||
}
|
||||
59
src/services/skin.service.js
Normal file
59
src/services/skin.service.js
Normal 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 });
|
||||
}),
|
||||
);
|
||||
}
|
||||
40
src/services/solitaire.service.js
Normal file
40
src/services/solitaire.service.js
Normal 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 } });
|
||||
}
|
||||
20
src/services/transaction.service.js
Normal file
20
src/services/transaction.service.js
Normal 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" },
|
||||
});
|
||||
}
|
||||
73
src/services/user.service.js
Normal file
73
src/services/user.service.js
Normal 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 });
|
||||
}),
|
||||
);
|
||||
}
|
||||
@@ -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
|
||||
tierColor: dbSkin?.tierColor,
|
||||
};
|
||||
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}`);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 l’offre",
|
||||
@@ -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 l’offre",
|
||||
@@ -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 l’offre",
|
||||
@@ -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 l’enchè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 l’offre",
|
||||
@@ -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 l’enchè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 l’enchè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()
|
||||
|
||||
Reference in New Issue
Block a user