mirror of
https://github.com/cassoule/flopobot_v2.git
synced 2026-03-18 21:40:27 +01:00
Compare commits
55 Commits
ery-260115
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
19f9a11e4b | ||
|
|
78ecf9698c | ||
|
|
82c9c8ddad | ||
|
|
622522afa7 | ||
|
|
bd4375b2cc | ||
|
|
e4beb7f5be | ||
|
|
ede1489016 | ||
|
|
ef6022ae35 | ||
|
|
30233e5239 | ||
|
|
ee231b517e | ||
|
|
1523d0f696 | ||
|
|
4cc1b17984 | ||
|
|
e18aabadb6 | ||
|
|
4d1de5d48c | ||
|
|
885f7d9b44 | ||
|
|
7f043a7c93 | ||
|
|
72e67be565 | ||
|
|
2d2e2d71a8 | ||
|
|
d7eb194db6 | ||
|
|
45f90dc207 | ||
|
|
6af86b9032 | ||
|
|
aeec76e457 | ||
|
|
c635252758 | ||
|
|
639e7a9c3c | ||
|
|
d53e43f9c4 | ||
|
|
6072d19642 | ||
|
|
a8fac1cb19 | ||
|
|
6c2f9df2f0 | ||
|
|
3115df3cdd | ||
|
|
db7d9aec8f | ||
|
|
373a4c6edc | ||
|
|
1c432e68bd | ||
|
|
9e12065f0d | ||
|
|
2cabd43769 | ||
|
|
bb7f0047bb | ||
|
|
adcd4cac1f | ||
|
|
c4c8eaf5d6 | ||
|
|
1371200041 | ||
|
|
ff8ffd7503 | ||
|
|
3b63b17124 | ||
|
|
ad7bddd1dd | ||
|
|
29798e40c7 | ||
|
|
2a81ab578c | ||
|
|
b47def3a9f | ||
|
|
54059b7133 | ||
|
|
4dd5be7e2f | ||
|
|
12eac37226 | ||
|
|
26c03ed62a | ||
|
|
beabace9eb | ||
|
|
a08e9ed626 | ||
|
|
1514bb08c2 | ||
|
|
41e8f01b25 | ||
|
|
aac8aeb348 | ||
|
|
1412ed7ea2 | ||
|
|
f8e3d994da |
18
.github/workflows/deploy.yml
vendored
Normal file
18
.github/workflows/deploy.yml
vendored
Normal file
@@ -0,0 +1,18 @@
|
||||
name: Deploy to Hetzner
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Deploy via SSH
|
||||
uses: appleboy/ssh-action@v1
|
||||
with:
|
||||
host: ${{ secrets.SERVER_IP }}
|
||||
username: root
|
||||
key: ${{ secrets.SSH_PRIVATE_KEY }}
|
||||
script: ~/deploy.sh
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -4,4 +4,5 @@ flopobot.db
|
||||
flopobot.db-shm
|
||||
flopobot.db-wal
|
||||
.idea
|
||||
*.db
|
||||
*.db
|
||||
.claude
|
||||
|
||||
17
README.md
17
README.md
@@ -7,9 +7,9 @@
|
||||
```
|
||||
├── public/
|
||||
│ └── images/ # Static assets
|
||||
├── src/
|
||||
├── src/
|
||||
│ ├── api/ # External API integrations
|
||||
│ ├── bot/
|
||||
│ ├── bot/
|
||||
│ │ ├── commands/ # Slash command implementations
|
||||
│ │ ├── components/ # Discord message components
|
||||
│ │ ├── handlers/ # Event handlers
|
||||
@@ -17,10 +17,10 @@
|
||||
│ │ └── events.js # Event registration
|
||||
│ ├── config/
|
||||
│ │ └── commands.js # Slash command definitions
|
||||
│ ├── database/
|
||||
│ ├── database/
|
||||
│ │ └── index.js # Database connection and models
|
||||
│ ├── game/ # Game logic and data
|
||||
│ ├── server/
|
||||
│ ├── server/
|
||||
│ │ ├── routes/ # Express routes
|
||||
│ │ ├── app.js # Express app setup
|
||||
│ │ └── socket.js # Socket.io setup
|
||||
@@ -30,6 +30,7 @@
|
||||
```
|
||||
|
||||
## Features
|
||||
|
||||
- **Moderation Tools** : Includes commands for managing server members.
|
||||
- **AI Integration** : Utilizes AI APIs for enhanced interactions.
|
||||
- **Game Mechanics** : Implements game features and logic.
|
||||
@@ -38,12 +39,14 @@
|
||||
- **Web Integration** : Designed to work alongside a [FlopoSite](https://floposite.com) (see [FlopoSite's repo)](https://github.com/cassoule/floposite)).
|
||||
|
||||
## Additional Information
|
||||
Note that FlopoBot is a work in progress, and new features and improvements are continually being added. Contributions and feedback are welcome !
|
||||
|
||||
FlopoBot was orriginally created to be integrated in a specific Discord server, so adding it to other servers won't provide the full experience (for now).
|
||||
Note that FlopoBot is a work in progress, and new features and improvements are continually being added. Contributions and feedback are welcome !
|
||||
|
||||
FlopoBot was orriginally created to be integrated in a specific Discord server, so adding it to other servers won't provide the full experience (for now).
|
||||
|
||||
FlopoSite though is public and can be used by anyone :)
|
||||
|
||||
## Related Links
|
||||
|
||||
- [FlopoSite Website](https://floposite.com)
|
||||
- [FlopoSite Repository](https://github.com/cassoule/floposite)
|
||||
- [FlopoSite Repository](https://github.com/cassoule/floposite)
|
||||
|
||||
6
index.js
6
index.js
@@ -26,9 +26,15 @@ initializeSocket(io, client);
|
||||
|
||||
// --- BOT INITIALIZATION ---
|
||||
initializeEvents(client, io);
|
||||
client.rest.on("rateLimited", (info) => {
|
||||
console.log("Rate limited:", info);
|
||||
});
|
||||
console.log("Logging in...");
|
||||
client.login(process.env.BOT_TOKEN).then(() => {
|
||||
console.log(`Logged in as ${client.user.tag}`);
|
||||
console.log("[Discord Bot Events Initialized]");
|
||||
}).catch((error) => {
|
||||
console.error("Error logging in to Discord:", error);
|
||||
});
|
||||
|
||||
// --- APP STARTUP ---
|
||||
|
||||
1086
package-lock.json
generated
1086
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
15
package.json
15
package.json
@@ -11,23 +11,32 @@
|
||||
"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",
|
||||
"format": "prettier --write .",
|
||||
"lint": "eslint . --ext .js,.jsx,.ts,.tsx",
|
||||
"stripe:whook": "stripe listen --forward-to localhost:3000/api/buy-coins"
|
||||
},
|
||||
"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",
|
||||
"discord.js": "^14.25.1",
|
||||
"dotenv": "^16.0.3",
|
||||
"express": "^4.18.2",
|
||||
"jsonwebtoken": "^9.0.3",
|
||||
"node-cron": "^3.0.3",
|
||||
"openai": "^4.104.0",
|
||||
"pnpm": "^10.29.2",
|
||||
"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"
|
||||
},
|
||||
|
||||
3241
pnpm-lock.yaml
generated
Normal file
3241
pnpm-lock.yaml
generated
Normal file
File diff suppressed because it is too large
Load Diff
5
pnpm-workspace.yaml
Normal file
5
pnpm-workspace.yaml
Normal file
@@ -0,0 +1,5 @@
|
||||
onlyBuiltDependencies:
|
||||
- "@prisma/client"
|
||||
- "@prisma/engines"
|
||||
- prisma
|
||||
- protobufjs
|
||||
137
prisma/migrations/20260210172326_init/migration.sql
Normal file
137
prisma/migrations/20260210172326_init/migration.sql
Normal file
@@ -0,0 +1,137 @@
|
||||
-- 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" DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
"opening_at" DATETIME NOT NULL,
|
||||
"closing_at" DATETIME 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" DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
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" DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
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" DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
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" DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
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");
|
||||
44
prisma/migrations/20260301140605_add_cs_skins/migration.sql
Normal file
44
prisma/migrations/20260301140605_add_cs_skins/migration.sql
Normal file
@@ -0,0 +1,44 @@
|
||||
-- CreateTable
|
||||
CREATE TABLE "cs_skins" (
|
||||
"id" TEXT NOT NULL PRIMARY KEY,
|
||||
"market_hash_name" TEXT NOT NULL,
|
||||
"displayName" TEXT,
|
||||
"image_url" TEXT,
|
||||
"rarity" TEXT,
|
||||
"rarity_color" TEXT,
|
||||
"weapon_type" TEXT,
|
||||
"float" REAL,
|
||||
"wear_state" TEXT,
|
||||
"is_stattrak" BOOLEAN NOT NULL DEFAULT false,
|
||||
"is_souvenir" BOOLEAN NOT NULL DEFAULT false,
|
||||
"price" INTEGER,
|
||||
"user_id" TEXT,
|
||||
CONSTRAINT "cs_skins_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "users" ("id") ON DELETE SET NULL ON UPDATE CASCADE
|
||||
);
|
||||
|
||||
-- RedefineTables
|
||||
PRAGMA defer_foreign_keys=ON;
|
||||
PRAGMA foreign_keys=OFF;
|
||||
CREATE TABLE "new_market_offers" (
|
||||
"id" TEXT NOT NULL PRIMARY KEY,
|
||||
"skin_uuid" TEXT,
|
||||
"cs_skin_id" TEXT,
|
||||
"seller_id" TEXT NOT NULL,
|
||||
"starting_price" INTEGER NOT NULL,
|
||||
"buyout_price" INTEGER,
|
||||
"final_price" INTEGER,
|
||||
"status" TEXT NOT NULL,
|
||||
"posted_at" DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
"opening_at" DATETIME NOT NULL,
|
||||
"closing_at" DATETIME NOT NULL,
|
||||
"buyer_id" TEXT,
|
||||
CONSTRAINT "market_offers_skin_uuid_fkey" FOREIGN KEY ("skin_uuid") REFERENCES "skins" ("uuid") ON DELETE SET NULL ON UPDATE CASCADE,
|
||||
CONSTRAINT "market_offers_cs_skin_id_fkey" FOREIGN KEY ("cs_skin_id") REFERENCES "cs_skins" ("id") ON DELETE SET NULL 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
|
||||
);
|
||||
INSERT INTO "new_market_offers" ("buyer_id", "buyout_price", "closing_at", "final_price", "id", "opening_at", "posted_at", "seller_id", "skin_uuid", "starting_price", "status") SELECT "buyer_id", "buyout_price", "closing_at", "final_price", "id", "opening_at", "posted_at", "seller_id", "skin_uuid", "starting_price", "status" FROM "market_offers";
|
||||
DROP TABLE "market_offers";
|
||||
ALTER TABLE "new_market_offers" RENAME TO "market_offers";
|
||||
PRAGMA foreign_keys=ON;
|
||||
PRAGMA defer_foreign_keys=OFF;
|
||||
3
prisma/migrations/migration_lock.toml
Normal file
3
prisma/migrations/migration_lock.toml
Normal file
@@ -0,0 +1,3 @@
|
||||
# Please do not edit this file manually
|
||||
# It should be added in your version-control system (e.g., Git)
|
||||
provider = "sqlite"
|
||||
199
prisma/schema.prisma
Normal file
199
prisma/schema.prisma
Normal file
@@ -0,0 +1,199 @@
|
||||
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[]
|
||||
csSkins CsSkin[] @relation("CsSkins")
|
||||
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 CsSkin {
|
||||
id String @id @default(uuid())
|
||||
marketHashName String @map("market_hash_name")
|
||||
displayName String?
|
||||
imageUrl String? @map("image_url")
|
||||
rarity String?
|
||||
rarityColor String? @map("rarity_color")
|
||||
weaponType String? @map("weapon_type")
|
||||
float Float?
|
||||
wearState String? @map("wear_state")
|
||||
isStattrak Boolean @default(false) @map("is_stattrak")
|
||||
isSouvenir Boolean @default(false) @map("is_souvenir")
|
||||
price Int?
|
||||
userId String? @map("user_id")
|
||||
|
||||
owner User? @relation("CsSkins", fields: [userId], references: [id])
|
||||
marketOffers MarketOffer[] @relation("CsSkinOffers")
|
||||
|
||||
@@map("cs_skins")
|
||||
}
|
||||
|
||||
model MarketOffer {
|
||||
id String @id
|
||||
skinUuid String? @map("skin_uuid")
|
||||
csSkinId String? @map("cs_skin_id")
|
||||
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])
|
||||
csSkin CsSkin? @relation("CsSkinOffers", fields: [csSkinId], references: [id])
|
||||
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? @default(now()) @map("timestamp")
|
||||
|
||||
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")
|
||||
}
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 296 KiB |
51
src/api/cs.js
Normal file
51
src/api/cs.js
Normal file
@@ -0,0 +1,51 @@
|
||||
import { csSkinsData, csSkinsPrices } from "../utils/cs.state.js";
|
||||
|
||||
const params = new URLSearchParams({
|
||||
app_id: 730,
|
||||
currency: "EUR",
|
||||
});
|
||||
|
||||
export const fetchSuggestedPrices = async () => {
|
||||
try {
|
||||
const response = await fetch(`https://api.skinport.com/v1/items?${params}`, {
|
||||
method: "GET",
|
||||
headers: { "Accept-Encoding": "br" },
|
||||
});
|
||||
const data = await response.json();
|
||||
data.forEach((skin) => {
|
||||
if (skin.market_hash_name) {
|
||||
csSkinsPrices[skin.market_hash_name] = {
|
||||
suggested_price: skin.suggested_price,
|
||||
min_price: skin.min_price,
|
||||
max_price: skin.max_price,
|
||||
mean_price: skin.mean_price,
|
||||
median_price: skin.median_price,
|
||||
};
|
||||
}
|
||||
});
|
||||
return data;
|
||||
} catch (error) {
|
||||
console.error("Error parsing JSON:", error);
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
export const fetchSkinsData = async () => {
|
||||
try {
|
||||
const response = await fetch(
|
||||
`https://raw.githubusercontent.com/ByMykel/CSGO-API/main/public/api/en/skins.json`,
|
||||
);
|
||||
const data = await response.json();
|
||||
data.forEach((skin) => {
|
||||
if (skin.market_hash_name) {
|
||||
csSkinsData[skin.market_hash_name] = skin;
|
||||
} else if (skin.name) {
|
||||
csSkinsData[skin.name] = skin;
|
||||
}
|
||||
});
|
||||
return data;
|
||||
} catch (error) {
|
||||
console.error("Error fetching skins data:", error);
|
||||
return null;
|
||||
}
|
||||
};
|
||||
@@ -12,7 +12,7 @@ export async function handleInfoCommand(req, res, client) {
|
||||
|
||||
try {
|
||||
// Fetch the guild object from the client
|
||||
const guild = await client.guilds.fetch(guild_id);
|
||||
const guild = client.guilds.cache.get(guild_id);
|
||||
|
||||
// Fetch all members to ensure the cache is up to date
|
||||
await guild.members.fetch();
|
||||
|
||||
@@ -5,11 +5,14 @@ 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";
|
||||
import * as csSkinService from "../../services/csSkin.service.js";
|
||||
import { RarityToColor } from "../../utils/cs.utils.js";
|
||||
import { resolveMember } from "../../utils/index.js";
|
||||
|
||||
/**
|
||||
* Handles the /inventory slash command.
|
||||
* Displays a paginated, interactive embed of a user's Valorant skin inventory.
|
||||
* Displays a paginated, interactive embed of a user's skin inventory.
|
||||
*
|
||||
* @param {object} req - The Express request object.
|
||||
* @param {object} res - The Express response object.
|
||||
@@ -26,16 +29,22 @@ export async function handleInventoryCommand(req, res, client, interactionId) {
|
||||
});
|
||||
const { member, guild_id, token, data } = req.body;
|
||||
const commandUserId = member.user.id;
|
||||
// User can specify another member, otherwise it defaults to themself
|
||||
const targetUserId = data.options && data.options.length > 0 ? data.options[0].value : commandUserId;
|
||||
|
||||
try {
|
||||
// --- 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 guild = client.guilds.cache.get(guild_id);
|
||||
const targetMember = await resolveMember(guild, targetUserId);
|
||||
|
||||
// Fetch both Valorant and CS2 inventories
|
||||
const valoSkins = await skinService.getUserInventory(targetUserId);
|
||||
const csSkins = await csSkinService.getUserCsInventory(targetUserId);
|
||||
|
||||
// Combine into a unified list with a type marker
|
||||
const inventorySkins = [
|
||||
...csSkins.map((s) => ({ ...s, _type: "cs" })),
|
||||
...valoSkins.map((s) => ({ ...s, _type: "valo" })),
|
||||
];
|
||||
|
||||
// --- 2. Handle Empty Inventory ---
|
||||
if (inventorySkins.length === 0) {
|
||||
return res.send({
|
||||
type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE,
|
||||
@@ -44,64 +53,30 @@ export async function handleInventoryCommand(req, res, client, interactionId) {
|
||||
{
|
||||
title: `Inventaire de ${targetMember.user.globalName || targetMember.user.username}`,
|
||||
description: "Cet inventaire est vide.",
|
||||
color: 0x4f545c, // Discord Gray
|
||||
color: 0x4f545c,
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// --- 3. Store Interactive Session State ---
|
||||
// This state is crucial for the component handlers to know which inventory to update.
|
||||
activeInventories[interactionId] = {
|
||||
akhyId: targetUserId, // The inventory owner
|
||||
userId: commandUserId, // The user who ran the command
|
||||
akhyId: targetUserId,
|
||||
userId: commandUserId,
|
||||
page: 0,
|
||||
amount: inventorySkins.length,
|
||||
endpoint: `webhooks/${process.env.APP_ID}/${token}/messages/@original`,
|
||||
timestamp: Date.now(),
|
||||
inventorySkins: inventorySkins, // Cache the skins to avoid re-querying the DB on each page turn
|
||||
inventorySkins: inventorySkins,
|
||||
};
|
||||
|
||||
// --- 4. Prepare Embed Content ---
|
||||
const currentSkin = inventorySkins[0];
|
||||
const skinData = skins.find((s) => s.uuid === currentSkin.uuid);
|
||||
if (!skinData) {
|
||||
throw new Error(`Skin data not found for UUID: ${currentSkin.uuid}`);
|
||||
}
|
||||
const totalPrice = inventorySkins.reduce((sum, skin) => sum + (skin.currentPrice || 0), 0);
|
||||
const totalPrice = inventorySkins.reduce((sum, skin) => {
|
||||
return sum + (skin._type === "cs" ? skin.price || 0 : skin.currentPrice || 0);
|
||||
}, 0);
|
||||
|
||||
// --- Helper functions for formatting ---
|
||||
const getChromaText = (skin, skinInfo) => {
|
||||
let result = "";
|
||||
for (let i = 1; i <= skinInfo.chromas.length; i++) {
|
||||
result += skin.currentChroma === i ? "💠 " : "◾ ";
|
||||
}
|
||||
return result || "N/A";
|
||||
};
|
||||
const embed = buildSkinEmbed(currentSkin, targetMember, 1, inventorySkins.length, totalPrice);
|
||||
|
||||
const getChromaName = (skin, skinInfo) => {
|
||||
if (skin.currentChroma > 1) {
|
||||
const name = skinInfo.chromas[skin.currentChroma - 1]?.displayName
|
||||
.replace(/[\r\n]+/g, " ")
|
||||
.replace(skinInfo.displayName, "")
|
||||
.trim();
|
||||
const match = name.match(/Variante\s*[0-9\s]*-\s*([^)]+)/i);
|
||||
return match ? match[1].trim() : name;
|
||||
}
|
||||
return "Base";
|
||||
};
|
||||
|
||||
const getImageUrl = (skin, skinInfo) => {
|
||||
if (skin.currentLvl === skinInfo.levels.length) {
|
||||
const chroma = skinInfo.chromas[skin.currentChroma - 1];
|
||||
return chroma?.fullRender || chroma?.displayIcon || skinInfo.displayIcon;
|
||||
}
|
||||
const level = skinInfo.levels[skin.currentLvl - 1];
|
||||
return level?.displayIcon || skinInfo.displayIcon || skinInfo.chromas[0].fullRender;
|
||||
};
|
||||
|
||||
// --- 5. Build Initial Components (Buttons) ---
|
||||
const components = [
|
||||
{
|
||||
type: MessageComponentTypes.BUTTON,
|
||||
@@ -117,38 +92,10 @@ export async function handleInventoryCommand(req, res, client, interactionId) {
|
||||
},
|
||||
];
|
||||
|
||||
const isUpgradable =
|
||||
currentSkin.currentLvl < skinData.levels.length || currentSkin.currentChroma < skinData.chromas.length;
|
||||
// Only show upgrade button if the skin is upgradable AND the command user owns the inventory
|
||||
if (isUpgradable && targetUserId === commandUserId) {
|
||||
components.push({
|
||||
type: MessageComponentTypes.BUTTON,
|
||||
custom_id: `upgrade_${interactionId}`,
|
||||
label: `Upgrade ⏫ (${process.env.VALO_UPGRADE_PRICE || (currentSkin.maxPrice / 10).toFixed(0)} Flopos)`,
|
||||
style: ButtonStyleTypes.PRIMARY,
|
||||
});
|
||||
}
|
||||
|
||||
// --- 6. Send Final Response ---
|
||||
return res.send({
|
||||
type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE,
|
||||
data: {
|
||||
embeds: [
|
||||
{
|
||||
title: `Inventaire de ${targetMember.user.globalName || targetMember.user.username}`,
|
||||
color: parseInt(currentSkin.tierColor, 16) || 0xf2f3f3,
|
||||
footer: {
|
||||
text: `Page 1/${inventorySkins.length} | Valeur Totale : ${totalPrice.toFixed(0)} Flopos`,
|
||||
},
|
||||
fields: [
|
||||
{
|
||||
name: `${currentSkin.displayName} | ${currentSkin.currentPrice.toFixed(0)} Flopos`,
|
||||
value: `${currentSkin.tierText}\nChroma : ${getChromaText(currentSkin, skinData)} | ${getChromaName(currentSkin, skinData)}\nLvl : **${currentSkin.currentLvl}**/${skinData.levels.length}`,
|
||||
},
|
||||
],
|
||||
image: { url: getImageUrl(currentSkin, skinData) },
|
||||
},
|
||||
],
|
||||
embeds: [embed],
|
||||
components: [
|
||||
{ type: MessageComponentTypes.ACTION_ROW, components: components },
|
||||
{
|
||||
@@ -170,3 +117,47 @@ export async function handleInventoryCommand(req, res, client, interactionId) {
|
||||
return res.status(500).json({ error: "Failed to generate inventory." });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds an embed for a single skin (CS2 or Valorant).
|
||||
*/
|
||||
export function buildSkinEmbed(skin, targetMember, page, total, totalPrice) {
|
||||
if (skin._type === "cs") {
|
||||
const badges = [
|
||||
skin.isStattrak ? "StatTrak™" : null,
|
||||
skin.isSouvenir ? "Souvenir" : null,
|
||||
].filter(Boolean).join(" | ");
|
||||
|
||||
return {
|
||||
title: `Inventaire de ${targetMember.user.globalName || targetMember.user.username}`,
|
||||
color: RarityToColor[skin.rarity] || 0xf2f3f3,
|
||||
footer: {
|
||||
text: `Page ${page}/${total} | Valeur Totale : ${totalPrice} Flopos`,
|
||||
},
|
||||
fields: [
|
||||
{
|
||||
name: `${skin.displayName} | ${skin.price} Flopos`,
|
||||
value: `${skin.rarity}${badges ? ` | ${badges}` : ""}\n${skin.wearState} (float: ${skin.float?.toFixed(8)})\n${skin.weaponType || ""}`,
|
||||
},
|
||||
],
|
||||
image: skin.imageUrl ? { url: skin.imageUrl } : undefined,
|
||||
};
|
||||
}
|
||||
|
||||
// Valorant skin fallback
|
||||
const skinData = skins.find((s) => s.uuid === skin.uuid);
|
||||
return {
|
||||
title: `Inventaire de ${targetMember.user.globalName || targetMember.user.username}`,
|
||||
color: parseInt(skin.tierColor, 16) || 0xf2f3f3,
|
||||
footer: {
|
||||
text: `Page ${page}/${total} | Valeur Totale : ${totalPrice} Flopos`,
|
||||
},
|
||||
fields: [
|
||||
{
|
||||
name: `${skin.displayName} | ${(skin.currentPrice || 0).toFixed(0)} Flopos`,
|
||||
value: `${skin.tierText || "Valorant"}\nLvl : **${skin.currentLvl}**/${skinData?.levels?.length || "?"}`,
|
||||
},
|
||||
],
|
||||
image: skinData ? { url: skinData.displayIcon } : undefined,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -5,7 +5,8 @@ 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";
|
||||
import { resolveMember } from "../../utils/index.js";
|
||||
|
||||
/**
|
||||
* Handles the /search slash command.
|
||||
@@ -23,7 +24,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),
|
||||
@@ -52,7 +53,7 @@ export async function handleSearchCommand(req, res, client, interactionId) {
|
||||
};
|
||||
|
||||
// --- 4. Prepare Initial Embed Content ---
|
||||
const guild = await client.guilds.fetch(guild_id);
|
||||
const guild = client.guilds.cache.get(guild_id);
|
||||
const currentSkin = resultSkins[0];
|
||||
const skinData = skins.find((s) => s.uuid === currentSkin.uuid);
|
||||
if (!skinData) {
|
||||
@@ -61,12 +62,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 resolveMember(guild, 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,6 @@
|
||||
import { InteractionResponseType } from "discord-interactions";
|
||||
import { getTopSkins } from "../../database/index.js";
|
||||
import * as skinService from "../../services/skin.service.js";
|
||||
import { resolveMember } from "../../utils/index.js";
|
||||
|
||||
/**
|
||||
* Handles the /skins slash command.
|
||||
@@ -13,8 +14,8 @@ export async function handleSkinsCommand(req, res, client) {
|
||||
|
||||
try {
|
||||
// --- 1. Fetch Data ---
|
||||
const topSkins = getTopSkins.all();
|
||||
const guild = await client.guilds.fetch(guild_id);
|
||||
const topSkins = await skinService.getTopSkins();
|
||||
const guild = client.guilds.cache.get(guild_id);
|
||||
const fields = [];
|
||||
|
||||
// --- 2. Build Embed Fields Asynchronously ---
|
||||
@@ -23,14 +24,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 resolveMember(guild, 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";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,11 +5,11 @@ import {
|
||||
ButtonStyleTypes,
|
||||
} from "discord-interactions";
|
||||
|
||||
import { formatTime, getOnlineUsersWithRole } from "../../utils/index.js";
|
||||
import { formatTime, getOnlineUsersWithRole, resolveMember } 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.
|
||||
@@ -28,9 +28,9 @@ export async function handleTimeoutCommand(req, res, client) {
|
||||
const time = options[1].value;
|
||||
|
||||
// Fetch member objects from Discord
|
||||
const guild = await client.guilds.fetch(guild_id);
|
||||
const fromMember = await guild.members.fetch(userId);
|
||||
const toMember = await guild.members.fetch(targetUserId);
|
||||
const guild = client.guilds.cache.get(guild_id);
|
||||
const fromMember = await resolveMember(guild, userId);
|
||||
const toMember = await resolveMember(guild, targetUserId);
|
||||
|
||||
// --- Validation Checks ---
|
||||
// 1. Check if a poll is already running for the target user
|
||||
@@ -102,12 +102,14 @@ export async function handleTimeoutCommand(req, res, client) {
|
||||
if (remaining === 0) {
|
||||
clearInterval(countdownInterval);
|
||||
|
||||
const votersList = poll.voters
|
||||
.map((voterId) => {
|
||||
const user = getUser.get(voterId);
|
||||
return `- ${user?.globalName || "Utilisateur Inconnu"}`;
|
||||
})
|
||||
.join("\n");
|
||||
const votersList = (
|
||||
await Promise.all(
|
||||
poll.voters.map(async (voterId) => {
|
||||
const user = await userService.getUser(voterId);
|
||||
return `- ${user?.globalName || "Utilisateur Inconnu"}`;
|
||||
}),
|
||||
)
|
||||
).join("\n");
|
||||
|
||||
try {
|
||||
await DiscordRequest(poll.endpoint, {
|
||||
@@ -143,12 +145,14 @@ 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);
|
||||
return `- ${user?.globalName || "Utilisateur Inconnu"}`;
|
||||
})
|
||||
.join("\n");
|
||||
const votersList = (
|
||||
await Promise.all(
|
||||
poll.voters.map(async (voterId) => {
|
||||
const user = await userService.getUser(voterId);
|
||||
return `- ${user?.globalName || "Utilisateur Inconnu"}`;
|
||||
}),
|
||||
)
|
||||
).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,
|
||||
|
||||
@@ -6,7 +6,9 @@ import {
|
||||
} from "discord-interactions";
|
||||
|
||||
import { DiscordRequest } from "../../api/discord.js";
|
||||
import { activeInventories, skins } from "../../game/state.js";
|
||||
import { activeInventories } from "../../game/state.js";
|
||||
import { buildSkinEmbed } from "../commands/inventory.js";
|
||||
import { resolveMember } from "../../utils/index.js";
|
||||
|
||||
/**
|
||||
* Handles navigation button clicks (Previous/Next) for the inventory embed.
|
||||
@@ -18,13 +20,10 @@ export async function handleInventoryNav(req, res, client) {
|
||||
const { member, data, guild_id } = req.body;
|
||||
const { custom_id } = data;
|
||||
|
||||
// Extract direction ('prev' or 'next') and the original interaction ID from the custom_id
|
||||
const [direction, page, interactionId] = custom_id.split("_");
|
||||
|
||||
// --- 1. Retrieve the interactive session ---
|
||||
const inventorySession = activeInventories[interactionId];
|
||||
|
||||
// --- 2. Validation Checks ---
|
||||
if (!inventorySession) {
|
||||
return res.send({
|
||||
type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE,
|
||||
@@ -35,7 +34,6 @@ export async function handleInventoryNav(req, res, client) {
|
||||
});
|
||||
}
|
||||
|
||||
// Ensure the user clicking the button is the one who initiated the command
|
||||
if (inventorySession.userId !== member.user.id) {
|
||||
return res.send({
|
||||
type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE,
|
||||
@@ -46,7 +44,6 @@ export async function handleInventoryNav(req, res, client) {
|
||||
});
|
||||
}
|
||||
|
||||
// --- 3. Update Page Number ---
|
||||
const { amount } = inventorySession;
|
||||
if (direction === "next") {
|
||||
inventorySession.page = (inventorySession.page + 1) % amount;
|
||||
@@ -55,49 +52,18 @@ export async function handleInventoryNav(req, res, client) {
|
||||
}
|
||||
|
||||
try {
|
||||
// --- 4. Rebuild Embed with New Page Content ---
|
||||
const { page, inventorySkins } = inventorySession;
|
||||
const currentSkin = inventorySkins[page];
|
||||
const skinData = skins.find((s) => s.uuid === currentSkin.uuid);
|
||||
if (!skinData) {
|
||||
throw new Error(`Skin data not found for UUID: ${currentSkin.uuid}`);
|
||||
}
|
||||
const { inventorySkins } = inventorySession;
|
||||
const currentPage = inventorySession.page;
|
||||
const currentSkin = inventorySkins[currentPage];
|
||||
|
||||
const guild = await client.guilds.fetch(guild_id);
|
||||
const targetMember = await guild.members.fetch(inventorySession.akhyId);
|
||||
const totalPrice = inventorySkins.reduce((sum, skin) => sum + (skin.currentPrice || 0), 0);
|
||||
const guild = client.guilds.cache.get(guild_id);
|
||||
const targetMember = await resolveMember(guild, inventorySession.akhyId);
|
||||
const totalPrice = inventorySkins.reduce((sum, skin) => {
|
||||
return sum + (skin._type === "cs" ? skin.price || 0 : skin.currentPrice || 0);
|
||||
}, 0);
|
||||
|
||||
// --- Helper functions for formatting ---
|
||||
const getChromaText = (skin, skinInfo) => {
|
||||
let result = "";
|
||||
for (let i = 1; i <= skinInfo.chromas.length; i++) {
|
||||
result += skin.currentChroma === i ? "💠 " : "◾ ";
|
||||
}
|
||||
return result || "N/A";
|
||||
};
|
||||
const embed = buildSkinEmbed(currentSkin, targetMember, currentPage + 1, amount, totalPrice);
|
||||
|
||||
const getChromaName = (skin, skinInfo) => {
|
||||
if (skin.currentChroma > 1) {
|
||||
const name = skinInfo.chromas[skin.currentChroma - 1]?.displayName
|
||||
.replace(/[\r\n]+/g, " ")
|
||||
.replace(skinInfo.displayName, "")
|
||||
.trim();
|
||||
const match = name.match(/Variante\s*[0-9\s]*-\s*([^)]+)/i);
|
||||
return match ? match[1].trim() : name;
|
||||
}
|
||||
return "Base";
|
||||
};
|
||||
|
||||
const getImageUrl = (skin, skinInfo) => {
|
||||
if (skin.currentLvl === skinInfo.levels.length) {
|
||||
const chroma = skinInfo.chromas[skin.currentChroma - 1];
|
||||
return chroma?.fullRender || chroma?.displayIcon || skinInfo.displayIcon;
|
||||
}
|
||||
const level = skinInfo.levels[skin.currentLvl - 1];
|
||||
return level?.displayIcon || skinInfo.displayIcon || skinInfo.chromas[0].fullRender;
|
||||
};
|
||||
|
||||
// --- 5. Rebuild Components (Buttons) ---
|
||||
let components = [
|
||||
{
|
||||
type: MessageComponentTypes.BUTTON,
|
||||
@@ -113,38 +79,10 @@ export async function handleInventoryNav(req, res, client) {
|
||||
},
|
||||
];
|
||||
|
||||
const isUpgradable =
|
||||
currentSkin.currentLvl < skinData.levels.length || currentSkin.currentChroma < skinData.chromas.length;
|
||||
// Conditionally add the upgrade button
|
||||
if (isUpgradable && inventorySession.akhyId === inventorySession.userId) {
|
||||
components.push({
|
||||
type: MessageComponentTypes.BUTTON,
|
||||
custom_id: `upgrade_${interactionId}`,
|
||||
label: `Upgrade ⏫ (${process.env.VALO_UPGRADE_PRICE || (currentSkin.maxPrice / 10).toFixed(0)} Flopos)`,
|
||||
style: ButtonStyleTypes.PRIMARY,
|
||||
});
|
||||
}
|
||||
|
||||
// --- 6. Send PATCH Request to Update the Message ---
|
||||
await DiscordRequest(inventorySession.endpoint, {
|
||||
method: "PATCH",
|
||||
body: {
|
||||
embeds: [
|
||||
{
|
||||
title: `Inventaire de ${targetMember.user.globalName || targetMember.user.username}`,
|
||||
color: parseInt(currentSkin.tierColor, 16) || 0xf2f3f3,
|
||||
footer: {
|
||||
text: `Page ${page + 1}/${amount} | Valeur Totale : ${totalPrice.toFixed(0)} Flopos`,
|
||||
},
|
||||
fields: [
|
||||
{
|
||||
name: `${currentSkin.displayName} | ${currentSkin.currentPrice.toFixed(0)} Flopos`,
|
||||
value: `${currentSkin.tierText}\nChroma : ${getChromaText(currentSkin, skinData)} | ${getChromaName(currentSkin, skinData)}\nLvl : **${currentSkin.currentLvl}**/${skinData.levels.length}`,
|
||||
},
|
||||
],
|
||||
image: { url: getImageUrl(currentSkin, skinData) },
|
||||
},
|
||||
],
|
||||
embeds: [embed],
|
||||
components: [
|
||||
{ type: MessageComponentTypes.ACTION_ROW, components: components },
|
||||
{
|
||||
@@ -162,14 +100,9 @@ export async function handleInventoryNav(req, res, client) {
|
||||
},
|
||||
});
|
||||
|
||||
// --- 7. Acknowledge the Interaction ---
|
||||
// This tells Discord the interaction was received, and since the message is already updated,
|
||||
// no further action is needed.
|
||||
return res.send({ type: InteractionResponseType.DEFERRED_UPDATE_MESSAGE });
|
||||
} catch (error) {
|
||||
console.error("Error handling inventory navigation:", error);
|
||||
// In case of an error, we should still acknowledge the interaction to prevent it from failing.
|
||||
// We can send a silent, ephemeral error message.
|
||||
return res.send({
|
||||
type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE,
|
||||
data: {
|
||||
|
||||
@@ -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,14 @@ 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) {
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
|
||||
import { DiscordRequest } from "../../api/discord.js";
|
||||
import { activeSearchs, skins } from "../../game/state.js";
|
||||
import { resolveUser } from "../../utils/index.js";
|
||||
|
||||
/**
|
||||
* Handles navigation button clicks (Previous/Next) for the search results embed.
|
||||
@@ -65,12 +66,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 resolveUser(client, 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,5 +1,7 @@
|
||||
import { handleMessageCreate } from "./handlers/messageCreate.js";
|
||||
import { getAkhys } from "../utils/index.js";
|
||||
import { fetchSuggestedPrices, fetchSkinsData } from "../api/cs.js";
|
||||
import { buildPriceIndex, buildWeaponRarityPriceMap } from "../utils/cs.state.js";
|
||||
|
||||
/**
|
||||
* Initializes and attaches all necessary event listeners to the Discord client.
|
||||
@@ -18,6 +20,10 @@ export function initializeEvents(client, io) {
|
||||
await getAkhys(client);
|
||||
console.log("[Startup] Setting up scheduled tasks...");
|
||||
//setupCronJobs(client, io);
|
||||
await fetchSuggestedPrices();
|
||||
await fetchSkinsData();
|
||||
buildPriceIndex();
|
||||
buildWeaponRarityPriceMap();
|
||||
console.log("--- FlopoBOT is fully operational ---");
|
||||
});
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { sleep } from "openai/core";
|
||||
import { AttachmentBuilder } from "discord.js";
|
||||
import {
|
||||
buildAiMessages,
|
||||
buildParticipantsMap,
|
||||
@@ -9,23 +10,19 @@ import {
|
||||
MAX_ATTS_PER_MESSAGE,
|
||||
stripMentionsOfBot,
|
||||
} from "../../utils/ai.js";
|
||||
import { calculateBasePrice, calculateMaxPrice, formatTime, getAkhys } from "../../utils/index.js";
|
||||
import { calculateBasePrice, calculateMaxPrice, formatTime, getAkhys, resolveMember } 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";
|
||||
import { fetchSuggestedPrices, fetchSkinsData } from "../../api/cs.js";
|
||||
import { csSkinsData, csSkinsPrices } from "../../utils/cs.state.js";
|
||||
import { getRandomSkinWithRandomSpecs, RarityToColor } from "../../utils/cs.utils.js";
|
||||
import * as csSkinService from "../../services/csSkin.service.js";
|
||||
|
||||
// Constants for the AI rate limiter
|
||||
const MAX_REQUESTS_PER_INTERVAL = parseInt(process.env.MAX_REQUESTS || "5");
|
||||
@@ -52,10 +49,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 +85,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,12 +101,12 @@ 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)) {
|
||||
try {
|
||||
const member = await message.guild.members.fetch(authorId);
|
||||
const member = await resolveMember(message.guild, authorId);
|
||||
const time = parseInt(process.env.SPAM_TIMEOUT_TIME);
|
||||
await member.timeout(time, "Spam excessif du bot AI.");
|
||||
message.channel
|
||||
@@ -134,7 +131,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 {
|
||||
@@ -197,22 +194,22 @@ async function handleAdminCommands(message) {
|
||||
|
||||
switch (command) {
|
||||
case "?sp":
|
||||
let msgText = ""
|
||||
let msgText = "";
|
||||
for (let skinTierRank = 1; skinTierRank <= 4; skinTierRank++) {
|
||||
msgText += `\n--- Tier Rank: ${skinTierRank} ---\n`;
|
||||
let skinMaxLevels = 4;
|
||||
let skinMaxChromas = 4;
|
||||
for (let skinLevel = 1; skinLevel < skinMaxLevels; skinLevel++) {
|
||||
msgText += (`Levels: ${skinLevel}/${skinMaxLevels}, MaxChromas: ${1}/${skinMaxChromas} - `);
|
||||
msgText += (`${getDummySkinUpgradeProbs(skinLevel, 1, skinTierRank, skinMaxLevels, skinMaxChromas, 15).successProb.toFixed(4)}, `);
|
||||
msgText += (`${getDummySkinUpgradeProbs(skinLevel, 1, skinTierRank, skinMaxLevels, skinMaxChromas, 15).destructionProb.toFixed(4)}, `);
|
||||
msgText += (`${getDummySkinUpgradeProbs(skinLevel, 1, skinTierRank, skinMaxLevels, skinMaxChromas, 15).upgradePrice}\n`);
|
||||
msgText += `Levels: ${skinLevel}/${skinMaxLevels}, MaxChromas: ${1}/${skinMaxChromas} - `;
|
||||
msgText += `${getDummySkinUpgradeProbs(skinLevel, 1, skinTierRank, skinMaxLevels, skinMaxChromas, 15).successProb.toFixed(4)}, `;
|
||||
msgText += `${getDummySkinUpgradeProbs(skinLevel, 1, skinTierRank, skinMaxLevels, skinMaxChromas, 15).destructionProb.toFixed(4)}, `;
|
||||
msgText += `${getDummySkinUpgradeProbs(skinLevel, 1, skinTierRank, skinMaxLevels, skinMaxChromas, 15).upgradePrice}\n`;
|
||||
}
|
||||
for (let skinChroma = 1; skinChroma < skinMaxChromas; skinChroma++) {
|
||||
msgText += (`Levels: ${skinMaxLevels}/${skinMaxLevels}, MaxChromas: ${skinChroma}/${skinMaxChromas} - `);
|
||||
msgText += (`${getDummySkinUpgradeProbs(skinMaxLevels, skinChroma, skinTierRank, skinMaxLevels, skinMaxChromas, 15).successProb.toFixed(4)}, `);
|
||||
msgText += (`${getDummySkinUpgradeProbs(skinMaxLevels, skinChroma, skinTierRank, skinMaxLevels, skinMaxChromas, 15).destructionProb.toFixed(4)}, `);
|
||||
msgText += (`${getDummySkinUpgradeProbs(skinMaxLevels, skinChroma, skinTierRank, skinMaxLevels, skinMaxChromas, 15).upgradePrice}\n`);
|
||||
msgText += `Levels: ${skinMaxLevels}/${skinMaxLevels}, MaxChromas: ${skinChroma}/${skinMaxChromas} - `;
|
||||
msgText += `${getDummySkinUpgradeProbs(skinMaxLevels, skinChroma, skinTierRank, skinMaxLevels, skinMaxChromas, 15).successProb.toFixed(4)}, `;
|
||||
msgText += `${getDummySkinUpgradeProbs(skinMaxLevels, skinChroma, skinTierRank, skinMaxLevels, skinMaxChromas, 15).destructionProb.toFixed(4)}, `;
|
||||
msgText += `${getDummySkinUpgradeProbs(skinMaxLevels, skinChroma, skinTierRank, skinMaxLevels, skinMaxChromas, 15).upgradePrice}\n`;
|
||||
}
|
||||
message.reply(msgText);
|
||||
msgText = "";
|
||||
@@ -238,17 +235,19 @@ 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();
|
||||
console.log(result);
|
||||
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) {
|
||||
console.error(e);
|
||||
message.reply(`SQL Error: ${e.message}`);
|
||||
}
|
||||
break;
|
||||
@@ -256,7 +255,7 @@ async function handleAdminCommands(message) {
|
||||
await getAkhys(client);
|
||||
break;
|
||||
case `${prefix}:avatars`:
|
||||
const guild = await client.guilds.fetch(process.env.GUILD_ID);
|
||||
const guild = client.guilds.cache.get(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));
|
||||
|
||||
@@ -265,16 +264,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 = () => {
|
||||
@@ -285,12 +284,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,
|
||||
@@ -300,7 +299,7 @@ async function handleAdminCommands(message) {
|
||||
currentPrice: skin.currentPrice ? calculatePrice() : null,
|
||||
maxPrice: maxPrice,
|
||||
});
|
||||
});
|
||||
}
|
||||
console.log("Reworked", dbSkins.length, "skins.");
|
||||
break;
|
||||
case `${prefix}:cases-test`:
|
||||
@@ -326,7 +325,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;
|
||||
@@ -356,37 +355,145 @@ async function handleAdminCommands(message) {
|
||||
break;
|
||||
case `${prefix}:refund-skins`:
|
||||
try {
|
||||
const DBskins = getAllSkins.all();
|
||||
for (const skin of DBskins) {
|
||||
const owner = getUser.get(skin.user_id);
|
||||
if (owner) {
|
||||
updateUserCoins.run({
|
||||
id: owner.id,
|
||||
coins: owner.coins + skin.currentPrice,
|
||||
});
|
||||
insertLog.run({
|
||||
id: `${skin.uuid}-skin-refund-${Date.now()}`,
|
||||
user_id: owner.id,
|
||||
target_user_id: null,
|
||||
action: "SKIN_REFUND",
|
||||
coins_amount: skin.currentPrice,
|
||||
user_new_amount: owner.coins + skin.currentPrice,
|
||||
});
|
||||
const allCsSkins = await csSkinService.getAllOwnedCsSkins();
|
||||
let refundedCount = 0;
|
||||
let totalRefunded = 0;
|
||||
for (const skin of allCsSkins) {
|
||||
const price = skin.price || 0;
|
||||
let owner = null;
|
||||
try {
|
||||
owner = await userService.getUser(skin.userId);
|
||||
} catch {
|
||||
//
|
||||
}
|
||||
updateSkin.run({
|
||||
uuid: skin.uuid,
|
||||
user_id: null,
|
||||
currentPrice: null,
|
||||
currentLvl: null,
|
||||
currentChroma: null,
|
||||
});
|
||||
if (owner) {
|
||||
await userService.updateUserCoins(owner.id, owner.coins + price);
|
||||
await logService.insertLog({
|
||||
id: `${skin.id}-cs-skin-refund-${Date.now()}`,
|
||||
userId: owner.id,
|
||||
targetUserId: null,
|
||||
action: "CS_SKIN_REFUND",
|
||||
coinsAmount: price,
|
||||
userNewAmount: owner.coins + price,
|
||||
});
|
||||
totalRefunded += price;
|
||||
refundedCount++;
|
||||
}
|
||||
await csSkinService.deleteCsSkin(skin.id);
|
||||
}
|
||||
message.reply("All skins refunded.");
|
||||
message.reply(`Refunded ${refundedCount} CS skins (${totalRefunded} FlopoCoins total).`);
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
message.reply(`Error during refund skins ${e.message}`);
|
||||
}
|
||||
|
||||
break;
|
||||
case `${prefix}:cs-search`:
|
||||
try {
|
||||
const searchTerm = args.join(" ");
|
||||
if (!searchTerm) {
|
||||
message.reply("Please provide a search term.");
|
||||
return;
|
||||
}
|
||||
const filteredData = csSkinsData
|
||||
? Object.values(csSkinsData).filter((skin) => {
|
||||
const name = skin.market_hash_name.toLowerCase();
|
||||
return args.every((word) => name.includes(word.toLowerCase()));
|
||||
})
|
||||
: [];
|
||||
if (filteredData.length === 0) {
|
||||
message.reply(`No skins found matching "${searchTerm}".`);
|
||||
return;
|
||||
} else if (filteredData.length <= 10) {
|
||||
const skinList = filteredData
|
||||
.map(
|
||||
(skin) =>
|
||||
`${skin.market_hash_name} - ${
|
||||
csSkinsPrices[skin.market_hash_name]
|
||||
? "Sug " +
|
||||
csSkinsPrices[skin.market_hash_name].suggested_price +
|
||||
" | Min " +
|
||||
csSkinsPrices[skin.market_hash_name].min_price +
|
||||
" | Max " +
|
||||
csSkinsPrices[skin.market_hash_name].max_price +
|
||||
" | Avg " +
|
||||
csSkinsPrices[skin.market_hash_name].mean_price +
|
||||
" | Med " +
|
||||
csSkinsPrices[skin.market_hash_name].median_price
|
||||
: "N/A"
|
||||
}`,
|
||||
)
|
||||
.join("\n");
|
||||
message.reply(`Skins matching "${searchTerm}":\n${skinList}`);
|
||||
} else {
|
||||
message.reply(`Found ${filteredData.length} skins matching "${searchTerm}".`);
|
||||
}
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
message.reply(`Error searching CS:GO skins: ${e.message}`);
|
||||
}
|
||||
break;
|
||||
case `${prefix}:open-cs`:
|
||||
try {
|
||||
const randomSkin = await getRandomSkinWithRandomSpecs(args[0] ? parseFloat(args[0]) : null);
|
||||
const created = await csSkinService.insertCsSkin({
|
||||
marketHashName: randomSkin.name,
|
||||
displayName: randomSkin.data.name || randomSkin.name,
|
||||
imageUrl: randomSkin.data.image || null,
|
||||
rarity: randomSkin.data.rarity.name,
|
||||
rarityColor: RarityToColor[randomSkin.data.rarity.name]?.toString(16) || null,
|
||||
weaponType: randomSkin.data.weapon?.name || null,
|
||||
float: randomSkin.float,
|
||||
wearState: randomSkin.wearState,
|
||||
isStattrak: randomSkin.isStattrak,
|
||||
isSouvenir: randomSkin.isSouvenir,
|
||||
price: parseInt(randomSkin.price),
|
||||
userId: message.author.id,
|
||||
});
|
||||
message.reply(
|
||||
`You opened a CS:GO case and got: ${randomSkin.name} (${randomSkin.data.rarity.name}, ${
|
||||
randomSkin.isStattrak ? "StatTrak, " : ""
|
||||
}${randomSkin.isSouvenir ? "Souvenir, " : ""}${randomSkin.wearState} - float ${randomSkin.float})\nBase Price: ${
|
||||
randomSkin.price ?? "N/A"
|
||||
} Flopos\nSkin ID: ${created.id}\nImage url: [url](${randomSkin.data.image || "N/A"})`,
|
||||
);
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
message.reply(`Error opening CS:GO case: ${e.message}`);
|
||||
}
|
||||
break;
|
||||
case `${prefix}:simulate-cs`:
|
||||
try {
|
||||
const caseCount = parseInt(args[0]) || 100;
|
||||
const caseType = args[1] || "default";
|
||||
let totalResValue = 0;
|
||||
let highestSkinPrice = 0;
|
||||
const priceTiers = {
|
||||
"Consumer Grade": 0,
|
||||
"Industrial Grade": 0,
|
||||
"Mil-Spec Grade": 0,
|
||||
"Restricted": 0,
|
||||
"Classified": 0,
|
||||
"Covert": 0,
|
||||
"Extraordinary": 0,
|
||||
};
|
||||
|
||||
for (let i = 0; i < caseCount; i++) {
|
||||
const result = await getRandomSkinWithRandomSpecs();
|
||||
totalResValue += parseInt(result.price);
|
||||
if (parseInt(result.price) > highestSkinPrice) {
|
||||
highestSkinPrice = parseInt(result.price);
|
||||
}
|
||||
priceTiers[result.data.rarity.name]++;
|
||||
}
|
||||
console.log(totalResValue / caseCount);
|
||||
message.reply(
|
||||
`${totalResValue / caseCount} average skin price over ${caseCount} ${caseType} cases.\nHighest skin price: ${highestSkinPrice}\nPrice tier distribution: ${JSON.stringify(priceTiers)}`,
|
||||
);
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
message.reply(`Error during case simulation: ${e.message}`);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,873 +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 = ?`);
|
||||
|
||||
/* -------------------------
|
||||
Market queries already declared above (kept for completeness)
|
||||
----------------------------*/
|
||||
|
||||
/* -------------------------
|
||||
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) {
|
||||
@@ -325,8 +323,8 @@ export async function settleAll(room) {
|
||||
hand.result = res.result;
|
||||
hand.delta = res.delta;
|
||||
try {
|
||||
const guild = await client.guilds.fetch(process.env.GUILD_ID);
|
||||
const generalChannel = await guild.channels.fetch(process.env.BOT_CHANNEL_ID);
|
||||
const guild = client.guilds.cache.get(process.env.GUILD_ID);
|
||||
const generalChannel = guild.channels.cache.get(process.env.BOT_CHANNEL_ID);
|
||||
const msg = await generalChannel.messages.fetch(p.msgId);
|
||||
const updatedEmbed = new EmbedBuilder()
|
||||
.setDescription(`<@${p.id}> joue au Blackjack.`)
|
||||
@@ -363,7 +361,10 @@ export function applyAction(room, playerId, action) {
|
||||
if (hand.stood || hand.busted) throw new Error("Already ended");
|
||||
hand.hasActed = true;
|
||||
hand.cards.push(draw(room.shoe));
|
||||
if (isBust(hand.cards)) hand.busted = true;
|
||||
if (isBust(hand.cards)) {
|
||||
hand.busted = true;
|
||||
p.activeHand++;
|
||||
}
|
||||
return "hit";
|
||||
}
|
||||
case "stand": {
|
||||
|
||||
124
src/game/elo.js
124
src/game/elo.js
@@ -1,6 +1,8 @@
|
||||
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";
|
||||
import { resolveUser } from "../utils/index.js";
|
||||
|
||||
/**
|
||||
* Handles Elo calculation for a standard 1v1 game.
|
||||
@@ -10,25 +12,25 @@ import { client } from "../bot/client.js";
|
||||
* @param {number} p2Score - The score for player 2.
|
||||
* @param {string} type - The type of game being played (e.g., 'TICTACTOE', 'CONNECT4').
|
||||
*/
|
||||
export async function eloHandler(p1Id, p2Id, p1Score, p2Score, type) {
|
||||
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 };
|
||||
}
|
||||
|
||||
@@ -43,9 +45,26 @@ export async function eloHandler(p1Id, p2Id, p1Score, p2Score, type) {
|
||||
const expectedP1 = 1 / (1 + Math.pow(10, (p2CurrentElo - p1CurrentElo) / 400));
|
||||
const expectedP2 = 1 / (1 + Math.pow(10, (p1CurrentElo - p2CurrentElo) / 400));
|
||||
|
||||
// Calculate raw Elo changes
|
||||
const p1Change = K_FACTOR * (p1Score - expectedP1);
|
||||
const p2Change = K_FACTOR * (p2Score - expectedP2);
|
||||
|
||||
// Make losing friendlier: loser loses 70% of what winner gains
|
||||
let finalP1Change = p1Change;
|
||||
let finalP2Change = p2Change;
|
||||
|
||||
if (p1Score > p2Score) {
|
||||
// P1 won, P2 lost
|
||||
finalP2Change = p2Change * 0.7;
|
||||
} else if (p2Score > p1Score) {
|
||||
// P2 won, P1 lost
|
||||
finalP1Change = p1Change * 0.7;
|
||||
}
|
||||
// If it's a draw (p1Score === p2Score), keep the original changes
|
||||
|
||||
// Calculate new Elo ratings
|
||||
const p1NewElo = Math.round(p1CurrentElo + K_FACTOR * (p1Score - expectedP1));
|
||||
const p2NewElo = Math.round(p2CurrentElo + K_FACTOR * (p2Score - expectedP2));
|
||||
const p1NewElo = Math.round(p1CurrentElo + finalP1Change);
|
||||
const p2NewElo = Math.round(p2CurrentElo + finalP2Change);
|
||||
|
||||
// Ensure Elo doesn't drop below a certain threshold (e.g., 100)
|
||||
const finalP1Elo = Math.max(0, p1NewElo);
|
||||
@@ -54,9 +73,9 @@ export async function eloHandler(p1Id, p2Id, p1Score, p2Score, type) {
|
||||
console.log(`Elo Update (${type}) for ${p1DB.globalName}: ${p1CurrentElo} -> ${finalP1Elo}`);
|
||||
console.log(`Elo Update (${type}) for ${p2DB.globalName}: ${p2CurrentElo} -> ${finalP2Elo}`);
|
||||
try {
|
||||
const generalChannel = await client.channels.fetch(process.env.BOT_CHANNEL_ID);
|
||||
const user1 = await client.users.fetch(p1Id);
|
||||
const user2 = await client.users.fetch(p2Id);
|
||||
const generalChannel = client.channels.cache.get(process.env.BOT_CHANNEL_ID);
|
||||
const user1 = await resolveUser(client, p1Id);
|
||||
const user2 = await resolveUser(client, p2Id);
|
||||
const diff1 = finalP1Elo - p1CurrentElo;
|
||||
const diff2 = finalP2Elo - p2CurrentElo;
|
||||
const embed = new EmbedBuilder()
|
||||
@@ -74,22 +93,38 @@ export async function eloHandler(p1Id, p2Id, p1Score, p2Score, type) {
|
||||
}
|
||||
|
||||
// --- 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);
|
||||
|
||||
insertGame.run({
|
||||
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,
|
||||
type: type,
|
||||
timestamp: Date.now(),
|
||||
});
|
||||
if (scores) {
|
||||
await gameService.insertGame({
|
||||
id: `${p1Id}-${p2Id}-${Date.now()}`,
|
||||
p1: p1Id,
|
||||
p2: p2Id,
|
||||
p1Score: scores.p1,
|
||||
p2Score: scores.p2,
|
||||
p1Elo: p1CurrentElo,
|
||||
p2Elo: p2CurrentElo,
|
||||
p1NewElo: finalP1Elo,
|
||||
p2NewElo: finalP2Elo,
|
||||
type: type,
|
||||
timestamp: Date.now(),
|
||||
});
|
||||
} else {
|
||||
await gameService.insertGame({
|
||||
id: `${p1Id}-${p2Id}-${Date.now()}`,
|
||||
p1: p1Id,
|
||||
p2: p2Id,
|
||||
p1Score: p1Score,
|
||||
p2Score: p2Score,
|
||||
p1Elo: p1CurrentElo,
|
||||
p2Elo: p2CurrentElo,
|
||||
p1NewElo: finalP1Elo,
|
||||
p2NewElo: finalP2Elo,
|
||||
type: type,
|
||||
timestamp: Date.now(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -106,11 +141,14 @@ 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;
|
||||
return { ...user, elo };
|
||||
});
|
||||
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;
|
||||
@@ -118,7 +156,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));
|
||||
|
||||
@@ -140,23 +178,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),
|
||||
|
||||
@@ -11,6 +11,9 @@ export let activeConnect4Games = {};
|
||||
// Stores active Tic-Tac-Toe games, keyed by a unique game ID.
|
||||
export let activeTicTacToeGames = {};
|
||||
|
||||
// Stores active Snake games, keyed by a unique game ID.
|
||||
export let activeSnakeGames = {};
|
||||
|
||||
// Stores active Solitaire games, keyed by user ID.
|
||||
export let activeSolitaireGames = {};
|
||||
|
||||
@@ -20,6 +23,8 @@ export let pokerRooms = {};
|
||||
// Stores active erinyes rooms, keyed by a unique room ID (uuidv4).
|
||||
export let erinyesRooms = {};
|
||||
|
||||
export let monkePaths = {};
|
||||
|
||||
// --- User and Session State ---
|
||||
|
||||
// Stores active user inventories for paginated embeds, keyed by the interaction ID.
|
||||
@@ -52,6 +57,9 @@ export let tictactoeQueue = [];
|
||||
// Stores user IDs waiting to play Connect 4.
|
||||
export let connect4Queue = [];
|
||||
|
||||
// Stores user IDs waiting to play Snake 1v1.
|
||||
export let snakeQueue = [];
|
||||
|
||||
export let queueMessagesEndpoints = [];
|
||||
|
||||
// --- Rate Limiting and Caching ---
|
||||
|
||||
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;
|
||||
@@ -11,6 +11,8 @@ import { solitaireRoutes } from "./routes/solitaire.js";
|
||||
import { getSocketIo } from "./socket.js";
|
||||
import { blackjackRoutes } from "./routes/blackjack.js";
|
||||
import { marketRoutes } from "./routes/market.js";
|
||||
import { monkeRoutes } from "./routes/monke.js";
|
||||
import { authRoutes } from "./routes/auth.js";
|
||||
|
||||
// --- EXPRESS APP INITIALIZATION ---
|
||||
const app = express();
|
||||
@@ -24,7 +26,7 @@ app.use((req, res, next) => {
|
||||
res.header("Access-Control-Allow-Origin", FLAPI_URL);
|
||||
res.header(
|
||||
"Access-Control-Allow-Headers",
|
||||
"Content-Type, X-API-Key, ngrok-skip-browser-warning, Cache-Control, Pragma, Expires",
|
||||
"Content-Type, Authorization, X-API-Key, ngrok-skip-browser-warning, Cache-Control, Pragma, Expires",
|
||||
);
|
||||
next();
|
||||
});
|
||||
@@ -36,6 +38,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());
|
||||
|
||||
@@ -44,6 +49,9 @@ app.use("/public", express.static("public"));
|
||||
|
||||
// --- API ROUTES ---
|
||||
|
||||
// Auth routes (Discord OAuth2, no client/io needed)
|
||||
app.use("/api/auth", authRoutes());
|
||||
|
||||
// General API routes (users, polls, etc.)
|
||||
app.use("/api", apiRoutes(client, io));
|
||||
|
||||
@@ -62,4 +70,7 @@ app.use("/api/market-place", marketRoutes(client, io));
|
||||
// erinyes-specific routes
|
||||
// app.use("/api/erinyes", erinyesRoutes(client, io));
|
||||
|
||||
// monke-specific routes
|
||||
app.use("/api/monke-game", monkeRoutes(client, io));
|
||||
|
||||
export { app };
|
||||
|
||||
63
src/server/middleware/auth.js
Normal file
63
src/server/middleware/auth.js
Normal file
@@ -0,0 +1,63 @@
|
||||
import jwt from "jsonwebtoken";
|
||||
|
||||
const JWT_SECRET = process.env.JWT_SECRET;
|
||||
|
||||
/**
|
||||
* Middleware that requires a valid JWT token in the Authorization header.
|
||||
* Sets req.userId to the authenticated Discord user ID.
|
||||
*/
|
||||
export function requireAuth(req, res, next) {
|
||||
const authHeader = req.headers.authorization;
|
||||
if (!authHeader || !authHeader.startsWith("Bearer ")) {
|
||||
return res.status(401).json({ error: "Authentication required." });
|
||||
}
|
||||
|
||||
const token = authHeader.split("Bearer ")[1];
|
||||
try {
|
||||
const payload = jwt.verify(token, JWT_SECRET);
|
||||
req.userId = payload.discordId;
|
||||
next();
|
||||
} catch (err) {
|
||||
return res.status(401).json({ error: "Invalid or expired token." });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Optional auth middleware - attaches userId if token is present, but doesn't block.
|
||||
* Useful for routes that work for both authenticated and unauthenticated users.
|
||||
*/
|
||||
export function optionalAuth(req, res, next) {
|
||||
const authHeader = req.headers.authorization;
|
||||
if (authHeader && authHeader.startsWith("Bearer ")) {
|
||||
const token = authHeader.split("Bearer ")[1];
|
||||
try {
|
||||
const payload = jwt.verify(token, JWT_SECRET);
|
||||
req.userId = payload.discordId;
|
||||
} catch {
|
||||
// Token invalid, continue without userId
|
||||
}
|
||||
}
|
||||
next();
|
||||
}
|
||||
|
||||
/**
|
||||
* Signs a JWT token for a given Discord user ID.
|
||||
* @param {string} discordId - The Discord user ID.
|
||||
* @returns {string} The signed JWT token.
|
||||
*/
|
||||
export function signToken(discordId) {
|
||||
return jwt.sign({ discordId }, JWT_SECRET, { expiresIn: "7d" });
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies a JWT token and returns the payload.
|
||||
* @param {string} token - The JWT token to verify.
|
||||
* @returns {object|null} The decoded payload or null if invalid.
|
||||
*/
|
||||
export function verifyToken(token) {
|
||||
try {
|
||||
return jwt.verify(token, JWT_SECRET);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
118
src/server/routes/auth.js
Normal file
118
src/server/routes/auth.js
Normal file
@@ -0,0 +1,118 @@
|
||||
import express from "express";
|
||||
import axios from "axios";
|
||||
import { signToken } from "../middleware/auth.js";
|
||||
import * as userService from "../../services/user.service.js";
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
const DISCORD_API = "https://discord.com/api/v10";
|
||||
const DISCORD_CLIENT_ID = process.env.APP_ID;
|
||||
const DISCORD_CLIENT_SECRET = process.env.DISCORD_CLIENT_SECRET;
|
||||
const API_URL = process.env.DEV_SITE === "true" ? process.env.API_URL_DEV : process.env.API_URL;
|
||||
const FLAPI_URL = process.env.DEV_SITE === "true" ? process.env.FLAPI_URL_DEV : process.env.FLAPI_URL;
|
||||
const REDIRECT_URI = `${API_URL}/api/auth/discord/callback`;
|
||||
|
||||
/**
|
||||
* GET /api/auth/discord
|
||||
* Redirects the user to Discord's OAuth2 authorization page.
|
||||
*/
|
||||
router.get("/discord", (req, res) => {
|
||||
const params = new URLSearchParams({
|
||||
client_id: DISCORD_CLIENT_ID,
|
||||
redirect_uri: REDIRECT_URI,
|
||||
response_type: "code",
|
||||
scope: "identify",
|
||||
});
|
||||
|
||||
res.redirect(`${DISCORD_API}/oauth2/authorize?${params.toString()}`);
|
||||
});
|
||||
|
||||
/**
|
||||
* GET /api/auth/discord/callback
|
||||
* Handles the OAuth2 callback from Discord.
|
||||
* Exchanges the authorization code for tokens, fetches user info,
|
||||
* creates a JWT, and redirects the user back to the frontend.
|
||||
*/
|
||||
router.get("/discord/callback", async (req, res) => {
|
||||
const { code } = req.query;
|
||||
if (!code) {
|
||||
return res.status(400).json({ error: "Missing authorization code." });
|
||||
}
|
||||
|
||||
try {
|
||||
// Exchange the authorization code for an access token
|
||||
const tokenResponse = await axios.post(
|
||||
`${DISCORD_API}/oauth2/token`,
|
||||
new URLSearchParams({
|
||||
client_id: DISCORD_CLIENT_ID,
|
||||
client_secret: DISCORD_CLIENT_SECRET,
|
||||
grant_type: "authorization_code",
|
||||
code,
|
||||
redirect_uri: REDIRECT_URI,
|
||||
}),
|
||||
{ headers: { "Content-Type": "application/x-www-form-urlencoded" } },
|
||||
);
|
||||
|
||||
const { access_token } = tokenResponse.data;
|
||||
|
||||
// Fetch the user's Discord profile
|
||||
const userResponse = await axios.get(`${DISCORD_API}/users/@me`, {
|
||||
headers: { Authorization: `Bearer ${access_token}` },
|
||||
});
|
||||
|
||||
const discordUser = userResponse.data;
|
||||
|
||||
// Ensure the user exists in our database
|
||||
const existingUser = await userService.getUser(discordUser.id);
|
||||
if (existingUser) {
|
||||
// Update avatar if it changed
|
||||
const avatarUrl = discordUser.avatar
|
||||
? `https://cdn.discordapp.com/avatars/${discordUser.id}/${discordUser.avatar}.png?size=256`
|
||||
: null;
|
||||
if (avatarUrl) {
|
||||
await userService.updateUserAvatar(discordUser.id, avatarUrl);
|
||||
}
|
||||
}
|
||||
|
||||
// Sign a JWT with the verified Discord ID
|
||||
const token = signToken(discordUser.id);
|
||||
|
||||
// Redirect back to the frontend with the token
|
||||
res.redirect(`${FLAPI_URL}/auth/callback?token=${token}&discordId=${discordUser.id}`);
|
||||
} catch (error) {
|
||||
console.error("Discord OAuth2 error:", error.response?.data || error.message);
|
||||
console.log("Status:", error.response?.status);
|
||||
console.log("Headers:", JSON.stringify(error.response?.headers));
|
||||
res.redirect(`${FLAPI_URL}/auth/callback?error=auth_failed`);
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* GET /api/auth/me
|
||||
* Returns the authenticated user's info. Requires a valid JWT.
|
||||
*/
|
||||
router.get("/me", async (req, res) => {
|
||||
const authHeader = req.headers.authorization;
|
||||
if (!authHeader || !authHeader.startsWith("Bearer ")) {
|
||||
return res.status(401).json({ error: "Authentication required." });
|
||||
}
|
||||
|
||||
const token = authHeader.split("Bearer ")[1];
|
||||
const { verifyToken } = await import("../middleware/auth.js");
|
||||
const payload = verifyToken(token);
|
||||
if (!payload) {
|
||||
return res.status(401).json({ error: "Invalid or expired token." });
|
||||
}
|
||||
|
||||
const user = await userService.getUser(payload.discordId);
|
||||
if (!user) {
|
||||
console.warn("User not found for Discord ID in token:", payload.discordId);
|
||||
return res.json({ discordId: payload.discordId });
|
||||
}
|
||||
|
||||
res.json({ user, discordId: user.id });
|
||||
});
|
||||
|
||||
export function authRoutes() {
|
||||
return router;
|
||||
}
|
||||
@@ -15,10 +15,13 @@ 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 } from "../socket.js";
|
||||
import { EmbedBuilder } from "discord.js";
|
||||
import { emitToast, emitUpdate, emitPlayerUpdate } from "../socket.js";
|
||||
import { EmbedBuilder, time } from "discord.js";
|
||||
import { requireAuth } from "../middleware/auth.js";
|
||||
import { resolveUser } from "../../utils/index.js";
|
||||
|
||||
export function blackjackRoutes(io) {
|
||||
const router = express.Router();
|
||||
@@ -77,23 +80,25 @@ export function blackjackRoutes(io) {
|
||||
|
||||
let changed = false;
|
||||
for (const p of Object.values(room.players)) {
|
||||
if (!p.inRound) continue;
|
||||
const h = p.hands[p.activeHand];
|
||||
if (!h.hasActed && !h.busted && !h.stood && !h.surrendered) {
|
||||
h.surrendered = true;
|
||||
h.stood = true;
|
||||
h.hasActed = true;
|
||||
//room.leavingAfterRound[p.id] = true; // kick at end of round
|
||||
emitToast({ type: "player-timeout", userId: p.id });
|
||||
changed = true;
|
||||
} else if (h.hasActed && !h.stood) {
|
||||
h.stood = true;
|
||||
//room.leavingAfterRound[p.id] = true; // kick at end of round
|
||||
emitToast({ type: "player-auto-stand", userId: p.id });
|
||||
changed = true;
|
||||
try {
|
||||
if (!p.inRound) continue;
|
||||
// Handle all remaining hands (important after splits)
|
||||
for (let i = p.activeHand; i < p.hands.length; i++) {
|
||||
const h = p.hands[i];
|
||||
if (!h || h.busted || h.stood || h.surrendered) continue;
|
||||
h.stood = true;
|
||||
h.hasActed = true;
|
||||
changed = true;
|
||||
}
|
||||
if (changed) {
|
||||
p.activeHand = p.hands.length;
|
||||
emitToast({ type: "player-auto-stand", userId: p.id });
|
||||
}
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
}
|
||||
}
|
||||
if (changed) emitUpdate("auto-surrender", snapshot(room));
|
||||
//if (changed) emitUpdate("auto-surrender", snapshot(room));
|
||||
return changed;
|
||||
}
|
||||
|
||||
@@ -118,13 +123,12 @@ export function blackjackRoutes(io) {
|
||||
// --- Public endpoints ---
|
||||
router.get("/", (req, res) => res.status(200).json({ room: snapshot(room) }));
|
||||
|
||||
router.post("/join", async (req, res) => {
|
||||
const { userId } = req.body;
|
||||
if (!userId) return res.status(400).json({ message: "userId required" });
|
||||
router.post("/join", requireAuth, async (req, res) => {
|
||||
const userId = req.userId;
|
||||
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 user = await resolveUser(client, userId);
|
||||
const bank = (await userService.getUser(userId))?.coins ?? 0;
|
||||
|
||||
room.players[userId] = {
|
||||
id: userId,
|
||||
@@ -152,8 +156,8 @@ export function blackjackRoutes(io) {
|
||||
};
|
||||
|
||||
try {
|
||||
const guild = await client.guilds.fetch(process.env.GUILD_ID);
|
||||
const generalChannel = await guild.channels.fetch(process.env.BOT_CHANNEL_ID);
|
||||
const guild = client.guilds.cache.get(process.env.GUILD_ID);
|
||||
const generalChannel = guild.channels.cache.get(process.env.BOT_CHANNEL_ID);
|
||||
const embed = new EmbedBuilder()
|
||||
.setDescription(`<@${userId}> joue au Blackjack`)
|
||||
.addFields(
|
||||
@@ -178,16 +182,21 @@ export function blackjackRoutes(io) {
|
||||
}
|
||||
|
||||
emitUpdate("player-joined", snapshot(room));
|
||||
emitPlayerUpdate({
|
||||
id: userId,
|
||||
msg: `${user?.globalName || user?.username} a rejoint la table de Blackjack.`,
|
||||
timestamp: Date.now(),
|
||||
});
|
||||
return res.status(200).json({ message: "joined" });
|
||||
});
|
||||
|
||||
router.post("/leave", async (req, res) => {
|
||||
const { userId } = req.body;
|
||||
if (!userId || !room.players[userId]) return res.status(403).json({ message: "not in room" });
|
||||
router.post("/leave", requireAuth, async (req, res) => {
|
||||
const userId = req.userId;
|
||||
if (!room.players[userId]) return res.status(403).json({ message: "not in room" });
|
||||
|
||||
try {
|
||||
const guild = await client.guilds.fetch(process.env.GUILD_ID);
|
||||
const generalChannel = await guild.channels.fetch(process.env.BOT_CHANNEL_ID);
|
||||
const guild = client.guilds.cache.get(process.env.GUILD_ID);
|
||||
const generalChannel = guild.channels.cache.get(process.env.BOT_CHANNEL_ID);
|
||||
const msg = await generalChannel.messages.fetch(room.players[userId].msgId);
|
||||
const updatedEmbed = new EmbedBuilder()
|
||||
.setDescription(`<@${userId}> a quitté la table de Blackjack.`)
|
||||
@@ -218,12 +227,19 @@ export function blackjackRoutes(io) {
|
||||
} else {
|
||||
delete room.players[userId];
|
||||
emitUpdate("player-left", snapshot(room));
|
||||
const user = await resolveUser(client, userId);
|
||||
emitPlayerUpdate({
|
||||
id: userId,
|
||||
msg: `${user?.globalName || user?.username} a quitté la table de Blackjack.`,
|
||||
timestamp: Date.now(),
|
||||
});
|
||||
return res.status(200).json({ message: "left" });
|
||||
}
|
||||
});
|
||||
|
||||
router.post("/bet", (req, res) => {
|
||||
const { userId, amount } = req.body;
|
||||
router.post("/bet", requireAuth, async (req, res) => {
|
||||
const userId = req.userId;
|
||||
const { amount } = req.body;
|
||||
const p = room.players[userId];
|
||||
if (!p) return res.status(404).json({ message: "not in room" });
|
||||
if (room.status !== "betting") return res.status(403).json({ message: "betting-closed" });
|
||||
@@ -232,17 +248,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;
|
||||
}
|
||||
@@ -254,8 +270,8 @@ export function blackjackRoutes(io) {
|
||||
return res.status(200).json({ message: "bet-accepted" });
|
||||
});
|
||||
|
||||
router.post("/action/:action", (req, res) => {
|
||||
const { userId } = req.body;
|
||||
router.post("/action/:action", requireAuth, async (req, res) => {
|
||||
const userId = req.userId;
|
||||
const action = req.params.action;
|
||||
const p = room.players[userId];
|
||||
if (!p) return res.status(404).json({ message: "not in room" });
|
||||
@@ -263,36 +279,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
|
||||
@@ -352,6 +368,12 @@ export function blackjackRoutes(io) {
|
||||
// Remove leavers
|
||||
for (const userId of Object.keys(room.leavingAfterRound)) {
|
||||
delete room.players[userId];
|
||||
const user = await resolveUser(client, userId);
|
||||
emitPlayerUpdate({
|
||||
id: userId,
|
||||
msg: `${user?.globalName || user?.username} a quitté la table de Blackjack.`,
|
||||
timestamp: Date.now(),
|
||||
});
|
||||
}
|
||||
// Prepare next round
|
||||
startBetting(room, now);
|
||||
|
||||
@@ -2,6 +2,7 @@ import express from "express";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
import { erinyesRooms } from "../../game/state.js";
|
||||
import { socketEmit } from "../socket.js";
|
||||
import { resolveUser } from "../../utils/index.js";
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
@@ -35,7 +36,7 @@ export function erinyesRoutes(client, io) {
|
||||
res.status(404).json({ message: "You are already in a room." });
|
||||
}
|
||||
|
||||
const creator = await client.users.fetch(creatorId);
|
||||
const creator = await resolveUser(client, creatorId);
|
||||
const id = uuidv4();
|
||||
|
||||
createRoom({
|
||||
|
||||
@@ -5,20 +5,14 @@ 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 * as csSkinService from "../../services/csSkin.service.js";
|
||||
import { emitMarketUpdate } from "../socket.js";
|
||||
import { handleNewMarketOffer, handleNewMarketOfferBid } from "../../utils/marketNotifs.js";
|
||||
import { requireAuth } from "../middleware/auth.js";
|
||||
|
||||
// Create a new router instance
|
||||
const router = express.Router();
|
||||
@@ -32,25 +26,30 @@ 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) {
|
||||
if (offer.csSkinId) {
|
||||
offer.csSkin = await csSkinService.getCsSkin(offer.csSkinId);
|
||||
} else if (offer.skinUuid) {
|
||||
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,24 +62,39 @@ 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 });
|
||||
}
|
||||
});
|
||||
|
||||
router.post("/place-offer", async (req, res) => {
|
||||
const { seller_id, skin_uuid, starting_price, delay, duration, timestamp } = req.body;
|
||||
router.post("/place-offer", requireAuth, async (req, res) => {
|
||||
const seller_id = req.userId;
|
||||
const { skin_uuid, cs_skin_id, starting_price, delay, duration, timestamp } = req.body;
|
||||
const now = Date.now();
|
||||
try {
|
||||
const skin = getSkin.get(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" });
|
||||
|
||||
const existingOffers = getMarketOffersBySkin.all(skin.uuid);
|
||||
let skinRef; // { skinUuid, csSkinId } - one or the other
|
||||
if (cs_skin_id) {
|
||||
const csSkin = await csSkinService.getCsSkin(cs_skin_id);
|
||||
if (!csSkin) return res.status(404).send({ error: "CS skin not found" });
|
||||
if (csSkin.userId !== seller.id) return res.status(403).send({ error: "You do not own this skin" });
|
||||
skinRef = { csSkinId: csSkin.id };
|
||||
} else if (skin_uuid) {
|
||||
const skin = await skinService.getSkin(skin_uuid);
|
||||
if (!skin) return res.status(404).send({ error: "Skin not found" });
|
||||
if (skin.userId !== seller.id) return res.status(403).send({ error: "You do not own this skin" });
|
||||
skinRef = { skinUuid: skin.uuid };
|
||||
} else {
|
||||
return res.status(400).send({ error: "Must provide skin_uuid or cs_skin_id" });
|
||||
}
|
||||
|
||||
const existingOffers = skinRef.skinUuid
|
||||
? await marketService.getMarketOffersBySkin(skinRef.skinUuid)
|
||||
: await marketService.getMarketOffersByCsSkin(skinRef.csSkinId);
|
||||
if (
|
||||
existingOffers.length > 0 &&
|
||||
existingOffers.some((offer) => offer.status === "open" || offer.status === "pending")
|
||||
@@ -91,16 +105,17 @@ export function marketRoutes(client, io) {
|
||||
const opening_at = now + delay;
|
||||
const closing_at = opening_at + duration;
|
||||
|
||||
const offerId = Date.now() + "-" + seller.id + "-" + skin.uuid;
|
||||
insertMarketOffer.run({
|
||||
const offerId = Date.now() + "-" + seller.id + "-" + (skinRef.skinUuid || skinRef.csSkinId);
|
||||
await marketService.insertMarketOffer({
|
||||
id: offerId,
|
||||
skin_uuid: skin.uuid,
|
||||
seller_id: seller.id,
|
||||
starting_price: starting_price,
|
||||
buyout_price: null,
|
||||
skinUuid: skinRef.skinUuid || null,
|
||||
csSkinId: skinRef.csSkinId || null,
|
||||
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);
|
||||
@@ -111,65 +126,66 @@ export function marketRoutes(client, io) {
|
||||
}
|
||||
});
|
||||
|
||||
router.post("/offers/:id/place-bid", async (req, res) => {
|
||||
const { buyer_id, bid_amount, timestamp } = req.body;
|
||||
router.post("/offers/:id/place-bid", requireAuth, async (req, res) => {
|
||||
const buyer_id = req.userId;
|
||||
const { 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,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
136
src/server/routes/monke.js
Normal file
136
src/server/routes/monke.js
Normal file
@@ -0,0 +1,136 @@
|
||||
import express from "express";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
import { monkePaths } from "../../game/state.js";
|
||||
import { socketEmit } from "../socket.js";
|
||||
import * as userService from "../../services/user.service.js";
|
||||
import * as logService from "../../services/log.service.js";
|
||||
import { init } from "openai/_shims/index.mjs";
|
||||
import { requireAuth } from "../middleware/auth.js";
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
/**
|
||||
* Factory function to create and configure the monke API routes.
|
||||
* @param {object} client - The Discord.js client instance.
|
||||
* @param {object} io - The Socket.IO server instance.
|
||||
* @returns {object} The configured Express router.
|
||||
*/
|
||||
export function monkeRoutes(client, io) {
|
||||
// --- Router Management Endpoints
|
||||
|
||||
router.get("/:userId", async (req, res) => {
|
||||
const { userId } = req.params;
|
||||
|
||||
if (!userId) return res.status(400).json({ error: "User ID is required" });
|
||||
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" });
|
||||
|
||||
return res.status(200).json({ userGamePath });
|
||||
});
|
||||
|
||||
router.post("/:userId/start", requireAuth, async (req, res) => {
|
||||
const userId = req.userId;
|
||||
const { initialBet } = req.body;
|
||||
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;
|
||||
await userService.updateUserCoins(userId, newCoins);
|
||||
await logService.insertLog({
|
||||
id: `${userId}-monke-bet-${Date.now()}`,
|
||||
userId: userId,
|
||||
targetUserId: null,
|
||||
action: "MONKE_BET",
|
||||
coinsAmount: -initialBet,
|
||||
userNewAmount: newCoins,
|
||||
});
|
||||
} catch (error) {
|
||||
return res.status(500).json({ error: "Failed to update user coins" });
|
||||
}
|
||||
|
||||
monkePaths[userId] = [
|
||||
{ round: 0, choice: null, result: null, bet: initialBet, extractValue: null, timestamp: Date.now() },
|
||||
];
|
||||
|
||||
return res.status(200).json({ message: "Monke game started", userGamePath: monkePaths[userId] });
|
||||
});
|
||||
|
||||
router.post("/:userId/play", requireAuth, async (req, res) => {
|
||||
const userId = req.userId;
|
||||
const { choice, step } = req.body;
|
||||
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 currentRound = monkePaths[userId].length - 1;
|
||||
if (step !== currentRound) return res.status(400).json({ error: "Invalid step for the current round" });
|
||||
if (monkePaths[userId][currentRound].choice !== null)
|
||||
return res.status(400).json({ error: "This round has already been played" });
|
||||
const randomLoseChoice = Math.floor(Math.random() * 3); // 0, 1, or 2
|
||||
|
||||
if (choice !== randomLoseChoice) {
|
||||
monkePaths[userId][currentRound].choice = choice;
|
||||
monkePaths[userId][currentRound].result = randomLoseChoice;
|
||||
monkePaths[userId][currentRound].extractValue = Math.round(monkePaths[userId][currentRound].bet * 1.33);
|
||||
monkePaths[userId][currentRound].timestamp = Date.now();
|
||||
|
||||
monkePaths[userId].push({
|
||||
round: currentRound + 1,
|
||||
choice: null,
|
||||
result: null,
|
||||
bet: monkePaths[userId][currentRound].extractValue,
|
||||
extractValue: null,
|
||||
timestamp: Date.now(),
|
||||
});
|
||||
|
||||
return res.status(200).json({ message: "Round won", userGamePath: monkePaths[userId], lost: false });
|
||||
} else {
|
||||
monkePaths[userId][currentRound].choice = choice;
|
||||
monkePaths[userId][currentRound].result = randomLoseChoice;
|
||||
monkePaths[userId][currentRound].extractValue = 0;
|
||||
monkePaths[userId][currentRound].timestamp = Date.now();
|
||||
|
||||
const userGamePath = monkePaths[userId];
|
||||
delete monkePaths[userId];
|
||||
|
||||
return res.status(200).json({ message: "Round lost", userGamePath, lost: true });
|
||||
}
|
||||
});
|
||||
|
||||
router.post("/:userId/stop", requireAuth, async (req, res) => {
|
||||
const userId = req.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];
|
||||
delete monkePaths[userId];
|
||||
|
||||
const extractValue = userGamePath[userGamePath.length - 1].bet;
|
||||
const coins = user.coins || 0;
|
||||
|
||||
const newCoins = coins + extractValue;
|
||||
|
||||
try {
|
||||
await userService.updateUserCoins(userId, newCoins);
|
||||
await logService.insertLog({
|
||||
id: `${userId}-monke-withdraw-${Date.now()}`,
|
||||
userId: userId,
|
||||
targetUserId: null,
|
||||
action: "MONKE_WITHDRAW",
|
||||
coinsAmount: extractValue,
|
||||
userNewAmount: newCoins,
|
||||
});
|
||||
|
||||
return res.status(200).json({ message: "Game stopped", userGamePath });
|
||||
} catch (error) {
|
||||
return res.status(500).json({ error: "Failed to update user coins" });
|
||||
}
|
||||
});
|
||||
|
||||
return router;
|
||||
}
|
||||
@@ -10,12 +10,14 @@ 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";
|
||||
import { ActionRowBuilder, ButtonBuilder, ButtonStyle, EmbedBuilder } from "discord.js";
|
||||
import { formatAmount } from "../../utils/index.js";
|
||||
import { formatAmount, resolveUser } from "../../utils/index.js";
|
||||
import { requireAuth } from "../middleware/auth.js";
|
||||
|
||||
const { Hand } = pkg;
|
||||
|
||||
@@ -43,16 +45,16 @@ export function pokerRoutes(client, io) {
|
||||
}
|
||||
});
|
||||
|
||||
router.post("/create", async (req, res) => {
|
||||
const { creatorId, minBet, fakeMoney } = req.body;
|
||||
if (!creatorId) return res.status(400).json({ message: "Creator ID is required." });
|
||||
router.post("/create", requireAuth, async (req, res) => {
|
||||
const creatorId = req.userId;
|
||||
const { minBet, fakeMoney } = req.body;
|
||||
|
||||
if (Object.values(pokerRooms).some((room) => room.host_id === creatorId || room.players[creatorId])) {
|
||||
return res.status(403).json({ message: "You are already in a poker room." });
|
||||
}
|
||||
|
||||
const guild = await client.guilds.fetch(process.env.GUILD_ID);
|
||||
const creator = await client.users.fetch(creatorId);
|
||||
const guild = client.guilds.cache.get(process.env.GUILD_ID);
|
||||
const creator = await resolveUser(client, creatorId);
|
||||
const id = uuidv4();
|
||||
const name = uniqueNamesGenerator({
|
||||
dictionaries: [adjectives, ["Poker"]],
|
||||
@@ -89,7 +91,7 @@ export function pokerRoutes(client, io) {
|
||||
await emitPokerUpdate({ room: pokerRooms[id], type: "room-created" });
|
||||
|
||||
try {
|
||||
const generalChannel = await guild.channels.fetch(process.env.BOT_CHANNEL_ID);
|
||||
const generalChannel = guild.channels.cache.get(process.env.BOT_CHANNEL_ID);
|
||||
const embed = new EmbedBuilder()
|
||||
.setTitle("Flopoker 🃏")
|
||||
.setDescription(`<@${creatorId}> a créé une table de poker`)
|
||||
@@ -124,14 +126,18 @@ export function pokerRoutes(client, io) {
|
||||
res.status(201).json({ roomId: id });
|
||||
});
|
||||
|
||||
router.post("/join", async (req, res) => {
|
||||
const { userId, roomId } = req.body;
|
||||
if (!userId || !roomId) return res.status(400).json({ message: "User ID and Room ID are required." });
|
||||
router.post("/join", requireAuth, async (req, res) => {
|
||||
const userId = req.userId;
|
||||
const { roomId } = req.body;
|
||||
if (!roomId) return res.status(400).json({ message: "Room ID is required." });
|
||||
if (!pokerRooms[roomId]) return res.status(404).json({ message: "Room not found." });
|
||||
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." });
|
||||
}
|
||||
|
||||
@@ -139,27 +145,25 @@ export function pokerRoutes(client, io) {
|
||||
res.status(200).json({ message: "Successfully joined." });
|
||||
});
|
||||
|
||||
router.post("/accept", async (req, res) => {
|
||||
const { hostId, playerId, roomId } = req.body;
|
||||
router.post("/accept", requireAuth, async (req, res) => {
|
||||
const hostId = req.userId;
|
||||
const { playerId, roomId } = req.body;
|
||||
const room = pokerRooms[roomId];
|
||||
if (!room || room.host_id !== hostId || !room.queue[playerId]) {
|
||||
return res.status(403).json({ message: "Unauthorized or player not in queue." });
|
||||
}
|
||||
|
||||
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,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -171,8 +175,9 @@ export function pokerRoutes(client, io) {
|
||||
res.status(200).json({ message: "Player accepted." });
|
||||
});
|
||||
|
||||
router.post("/leave", async (req, res) => {
|
||||
const { userId, roomId } = req.body;
|
||||
router.post("/leave", requireAuth, async (req, res) => {
|
||||
const userId = req.userId;
|
||||
const { roomId } = req.body;
|
||||
|
||||
if (!pokerRooms[roomId]) return res.status(404).send({ message: "Table introuvable" });
|
||||
if (!pokerRooms[roomId].players[userId]) return res.status(404).send({ message: "Joueur introuvable" });
|
||||
@@ -199,7 +204,7 @@ export function pokerRoutes(client, io) {
|
||||
}
|
||||
|
||||
try {
|
||||
updatePlayerCoins(
|
||||
await updatePlayerCoins(
|
||||
pokerRooms[roomId].players[userId],
|
||||
pokerRooms[roomId].players[userId].bank,
|
||||
pokerRooms[roomId].fakeMoney,
|
||||
@@ -222,8 +227,9 @@ export function pokerRoutes(client, io) {
|
||||
return res.status(200);
|
||||
});
|
||||
|
||||
router.post("/kick", async (req, res) => {
|
||||
const { commandUserId, userId, roomId } = req.body;
|
||||
router.post("/kick", requireAuth, async (req, res) => {
|
||||
const commandUserId = req.userId;
|
||||
const { userId, roomId } = req.body;
|
||||
|
||||
if (!pokerRooms[roomId]) return res.status(404).send({ message: "Table introuvable" });
|
||||
if (!pokerRooms[roomId].players[commandUserId]) return res.status(404).send({ message: "Joueur introuvable" });
|
||||
@@ -239,7 +245,7 @@ export function pokerRoutes(client, io) {
|
||||
}
|
||||
|
||||
try {
|
||||
updatePlayerCoins(
|
||||
await updatePlayerCoins(
|
||||
pokerRooms[roomId].players[userId],
|
||||
pokerRooms[roomId].players[userId].bank,
|
||||
pokerRooms[roomId].fakeMoney,
|
||||
@@ -264,7 +270,7 @@ export function pokerRoutes(client, io) {
|
||||
|
||||
// --- Game Action Endpoints ---
|
||||
|
||||
router.post("/start", async (req, res) => {
|
||||
router.post("/start", requireAuth, async (req, res) => {
|
||||
const { roomId } = req.body;
|
||||
const room = pokerRooms[roomId];
|
||||
if (!room) return res.status(404).json({ message: "Room not found." });
|
||||
@@ -275,7 +281,7 @@ export function pokerRoutes(client, io) {
|
||||
});
|
||||
|
||||
// NEW: Endpoint to start the next hand
|
||||
router.post("/next-hand", async (req, res) => {
|
||||
router.post("/next-hand", requireAuth, async (req, res) => {
|
||||
const { roomId } = req.body;
|
||||
const room = pokerRooms[roomId];
|
||||
if (!room || !room.waiting_for_restart) {
|
||||
@@ -285,8 +291,9 @@ export function pokerRoutes(client, io) {
|
||||
res.status(200).json({ message: "Next hand started." });
|
||||
});
|
||||
|
||||
router.post("/action/:action", async (req, res) => {
|
||||
const { playerId, amount, roomId } = req.body;
|
||||
router.post("/action/:action", requireAuth, async (req, res) => {
|
||||
const playerId = req.userId;
|
||||
const { amount, roomId } = req.body;
|
||||
const { action } = req.params;
|
||||
const room = pokerRooms[roomId];
|
||||
|
||||
@@ -358,8 +365,8 @@ export function pokerRoutes(client, io) {
|
||||
// --- Helper Functions ---
|
||||
|
||||
async function joinRoom(roomId, userId, io) {
|
||||
const user = await client.users.fetch(userId);
|
||||
const userDB = getUser.get(userId);
|
||||
const user = await resolveUser(client, userId);
|
||||
const userDB = await userService.getUser(userId);
|
||||
const room = pokerRooms[roomId];
|
||||
|
||||
const playerObject = {
|
||||
@@ -380,14 +387,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 +546,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,17 +19,11 @@ 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";
|
||||
import { requireAuth } from "../middleware/auth.js";
|
||||
|
||||
// Create a new router instance
|
||||
const router = express.Router();
|
||||
@@ -43,9 +37,9 @@ const router = express.Router();
|
||||
export function solitaireRoutes(client, io) {
|
||||
// --- Game Initialization Endpoints ---
|
||||
|
||||
router.post("/start", (req, res) => {
|
||||
const { userId, userSeed, hardMode } = req.body;
|
||||
if (!userId) return res.status(400).json({ error: "User ID is required." });
|
||||
router.post("/start", requireAuth, (req, res) => {
|
||||
const userId = req.userId;
|
||||
const { userSeed, hardMode } = req.body;
|
||||
|
||||
// If a game already exists for the user, return it instead of creating a new one.
|
||||
if (activeSolitaireGames[userId] && !activeSolitaireGames[userId].isSOTD) {
|
||||
@@ -85,11 +79,11 @@ export function solitaireRoutes(client, io) {
|
||||
res.json({ success: true, gameState });
|
||||
});
|
||||
|
||||
router.post("/start/sotd", (req, res) => {
|
||||
const { userId } = req.body;
|
||||
router.post("/start/sotd", requireAuth, async (req, res) => {
|
||||
const userId = req.userId;
|
||||
/*if (!userId || !getUser.get(userId)) {
|
||||
return res.status(404).json({ error: 'User not found.' });
|
||||
}*/
|
||||
return res.status(404).json({ error: 'User not found.' });
|
||||
}*/
|
||||
|
||||
if (activeSolitaireGames[userId]?.isSOTD) {
|
||||
return res.json({
|
||||
@@ -98,7 +92,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 +120,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." });
|
||||
@@ -145,16 +139,17 @@ export function solitaireRoutes(client, io) {
|
||||
}
|
||||
});
|
||||
|
||||
router.post("/reset", (req, res) => {
|
||||
const { userId } = req.body;
|
||||
router.post("/reset", requireAuth, (req, res) => {
|
||||
const userId = req.userId;
|
||||
if (activeSolitaireGames[userId]) {
|
||||
delete activeSolitaireGames[userId];
|
||||
}
|
||||
res.json({ success: true, message: "Game reset." });
|
||||
});
|
||||
|
||||
router.post("/move", async (req, res) => {
|
||||
const { userId, ...moveData } = req.body;
|
||||
router.post("/move", requireAuth, async (req, res) => {
|
||||
const userId = req.userId;
|
||||
const { ...moveData } = req.body;
|
||||
const gameState = activeSolitaireGames[userId];
|
||||
|
||||
if (!gameState) return res.status(404).json({ error: "Game not found." });
|
||||
@@ -183,8 +178,8 @@ export function solitaireRoutes(client, io) {
|
||||
}
|
||||
});
|
||||
|
||||
router.post("/draw", (req, res) => {
|
||||
const { userId } = req.body;
|
||||
router.post("/draw", requireAuth, (req, res) => {
|
||||
const userId = req.userId;
|
||||
const gameState = activeSolitaireGames[userId];
|
||||
|
||||
if (!gameState) return res.status(404).json({ error: "Game not found." });
|
||||
@@ -199,8 +194,8 @@ export function solitaireRoutes(client, io) {
|
||||
res.json({ success: true, gameState });
|
||||
});
|
||||
|
||||
router.post("/undo", (req, res) => {
|
||||
const { userId } = req.body;
|
||||
router.post("/undo", requireAuth, (req, res) => {
|
||||
const userId = req.userId;
|
||||
const gameState = activeSolitaireGames[userId];
|
||||
|
||||
if (!gameState) return res.status(404).json({ error: "Game not found." });
|
||||
@@ -237,20 +232,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 +255,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 +283,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,
|
||||
|
||||
@@ -2,9 +2,11 @@ import { ActionRowBuilder, ButtonBuilder, ButtonStyle, EmbedBuilder } from "disc
|
||||
import {
|
||||
activeConnect4Games,
|
||||
activeTicTacToeGames,
|
||||
activeSnakeGames,
|
||||
connect4Queue,
|
||||
queueMessagesEndpoints,
|
||||
tictactoeQueue,
|
||||
snakeQueue,
|
||||
} from "../game/state.js";
|
||||
import {
|
||||
C4_ROWS,
|
||||
@@ -14,6 +16,8 @@ import {
|
||||
formatConnect4BoardForDiscord,
|
||||
} from "../game/various.js";
|
||||
import { eloHandler } from "../game/elo.js";
|
||||
import { verifyToken } from "./middleware/auth.js";
|
||||
import { resolveUser } from "../utils/index.js";
|
||||
|
||||
// --- Module-level State ---
|
||||
let io;
|
||||
@@ -23,21 +27,40 @@ let io;
|
||||
export function initializeSocket(server, client) {
|
||||
io = server;
|
||||
|
||||
// Authenticate socket connections via JWT (optional - allows unauthenticated connections for health checks)
|
||||
io.use((socket, next) => {
|
||||
const token = socket.handshake.auth?.token;
|
||||
if (token) {
|
||||
const payload = verifyToken(token);
|
||||
if (!payload) {
|
||||
return next(new Error("Invalid or expired token"));
|
||||
}
|
||||
socket.userId = payload.discordId;
|
||||
}
|
||||
next();
|
||||
});
|
||||
|
||||
io.on("connection", (socket) => {
|
||||
socket.on("user-connected", async (userId) => {
|
||||
if (!userId) return;
|
||||
await refreshQueuesForUser(userId, client);
|
||||
socket.on("user-connected", async () => {
|
||||
if (!socket.userId) return;
|
||||
await refreshQueuesForUser(socket.userId, client);
|
||||
});
|
||||
|
||||
registerTicTacToeEvents(socket, client);
|
||||
registerConnect4Events(socket, client);
|
||||
registerSnakeEvents(socket, client);
|
||||
|
||||
socket.on("tictactoe:queue:leave", async ({ discordId }) => await refreshQueuesForUser(discordId, client));
|
||||
socket.on("blackjack:chat", (data) => {
|
||||
io.emit("blackjack:chat", data);
|
||||
});
|
||||
|
||||
socket.on("tictactoe:queue:leave", async () => await refreshQueuesForUser(socket.userId, client));
|
||||
socket.on("connect4:queue:leave", async () => await refreshQueuesForUser(socket.userId, client));
|
||||
socket.on("snake:queue:leave", async () => await refreshQueuesForUser(socket.userId, client));
|
||||
|
||||
// catch tab kills / network drops
|
||||
socket.on("disconnecting", async () => {
|
||||
const discordId = socket.handshake.auth?.discordId; // or your mapping
|
||||
await refreshQueuesForUser(discordId, client);
|
||||
await refreshQueuesForUser(socket.userId, client);
|
||||
});
|
||||
|
||||
socket.on("disconnect", () => {
|
||||
@@ -55,17 +78,24 @@ export function getSocketIo() {
|
||||
// --- Event Registration ---
|
||||
|
||||
function registerTicTacToeEvents(socket, client) {
|
||||
socket.on("tictactoeconnection", (e) => refreshQueuesForUser(e.id, client));
|
||||
socket.on("tictactoequeue", (e) => onQueueJoin(client, "tictactoe", e.playerId));
|
||||
socket.on("tictactoeplaying", (e) => onTicTacToeMove(client, e));
|
||||
socket.on("tictactoegameOver", (e) => onGameOver(client, "tictactoe", e.playerId, e.winner));
|
||||
socket.on("tictactoeconnection", () => refreshQueuesForUser(socket.userId, client));
|
||||
socket.on("tictactoequeue", () => onQueueJoin(client, "tictactoe", socket.userId));
|
||||
socket.on("tictactoeplaying", (e) => onTicTacToeMove(client, { ...e, playerId: socket.userId }));
|
||||
socket.on("tictactoegameOver", (e) => onGameOver(client, "tictactoe", socket.userId, e.winner));
|
||||
}
|
||||
|
||||
function registerConnect4Events(socket, client) {
|
||||
socket.on("connect4connection", (e) => refreshQueuesForUser(e.id, client));
|
||||
socket.on("connect4queue", (e) => onQueueJoin(client, "connect4", e.playerId));
|
||||
socket.on("connect4playing", (e) => onConnect4Move(client, e));
|
||||
socket.on("connect4NoTime", (e) => onGameOver(client, "connect4", e.playerId, e.winner, "(temps écoulé)"));
|
||||
socket.on("connect4connection", () => refreshQueuesForUser(socket.userId, client));
|
||||
socket.on("connect4queue", () => onQueueJoin(client, "connect4", socket.userId));
|
||||
socket.on("connect4playing", (e) => onConnect4Move(client, { ...e, playerId: socket.userId }));
|
||||
socket.on("connect4NoTime", (e) => onGameOver(client, "connect4", socket.userId, e.winner, "(temps écoulé)"));
|
||||
}
|
||||
|
||||
function registerSnakeEvents(socket, client) {
|
||||
socket.on("snakeconnection", () => refreshQueuesForUser(socket.userId, client));
|
||||
socket.on("snakequeue", () => onQueueJoin(client, "snake", socket.userId));
|
||||
socket.on("snakegamestate", (e) => onSnakeGameStateUpdate(client, { ...e, playerId: socket.userId }));
|
||||
socket.on("snakegameOver", (e) => onGameOver(client, "snake", socket.userId, e.winner));
|
||||
}
|
||||
|
||||
// --- Core Handlers (Preserving Original Logic) ---
|
||||
@@ -74,18 +104,25 @@ async function onQueueJoin(client, gameType, playerId) {
|
||||
if (!playerId) return;
|
||||
const { queue, activeGames, title, url } = getGameAssets(gameType);
|
||||
|
||||
if (
|
||||
queue.includes(playerId) ||
|
||||
Object.values(activeGames).some((g) => g.p1.id === playerId || g.p2.id === playerId)
|
||||
) {
|
||||
// Check if player is already in queue or in an active game
|
||||
if (queue.includes(playerId)) {
|
||||
console.log(`[${title}] Player ${playerId} already in queue, ignoring duplicate join.`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (Object.values(activeGames).some((g) => g.p1.id === playerId || g.p2.id === playerId)) {
|
||||
console.log(`[${title}] Player ${playerId} already in active game, ignoring queue join.`);
|
||||
return;
|
||||
}
|
||||
|
||||
queue.push(playerId);
|
||||
console.log(`[${title}] Player ${playerId} joined the queue.`);
|
||||
console.log(`[${title}] Player ${playerId} joined the queue. Queue size: ${queue.length}`);
|
||||
|
||||
if (queue.length === 1) await postQueueToDiscord(client, playerId, title, url);
|
||||
if (queue.length >= 2) await createGame(client, gameType);
|
||||
if (queue.length >= 2) {
|
||||
// Process matchmaking immediately to avoid race conditions
|
||||
await createGame(client, gameType);
|
||||
}
|
||||
|
||||
await emitQueueUpdate(client, gameType);
|
||||
}
|
||||
@@ -189,7 +226,57 @@ async function onConnect4Move(client, eventData) {
|
||||
await onGameOver(client, "connect4", playerId, winnerId);
|
||||
}
|
||||
|
||||
async function onGameOver(client, gameType, playerId, winnerId, reason = "") {
|
||||
async function onSnakeGameStateUpdate(client, eventData) {
|
||||
const { playerId, snake, food, score, gameOver, win } = eventData;
|
||||
const lobby = Object.values(activeSnakeGames).find(
|
||||
(l) => (l.p1.id === playerId || l.p2.id === playerId) && !l.gameOver,
|
||||
);
|
||||
if (!lobby) return;
|
||||
|
||||
const player = lobby.p1.id === playerId ? lobby.p1 : lobby.p2;
|
||||
player.snake = snake;
|
||||
player.food = food;
|
||||
player.score = score;
|
||||
player.gameOver = gameOver === true ? true : false;
|
||||
player.win = win;
|
||||
|
||||
lobby.lastmove = Date.now();
|
||||
|
||||
// Broadcast the updated state to both players in this specific game
|
||||
io.emit("snakegamestate", {
|
||||
gameKey: lobby.gameKey,
|
||||
lobby: {
|
||||
p1: lobby.p1,
|
||||
p2: lobby.p2,
|
||||
},
|
||||
});
|
||||
|
||||
// Check if game should end
|
||||
if (lobby.p1.gameOver && lobby.p2.gameOver) {
|
||||
// Both players finished - determine winner
|
||||
let winnerId = null;
|
||||
if (lobby.p1.win && !lobby.p2.win) {
|
||||
winnerId = lobby.p1.id;
|
||||
} else if (lobby.p2.win && !lobby.p1.win) {
|
||||
winnerId = lobby.p2.id;
|
||||
} else if (lobby.p1.score > lobby.p2.score) {
|
||||
winnerId = lobby.p1.id;
|
||||
} else if (lobby.p2.score > lobby.p1.score) {
|
||||
winnerId = lobby.p2.id;
|
||||
}
|
||||
// If scores are equal, winnerId remains null (draw)
|
||||
io.emit("snakegameOver", { gameKey: lobby.gameKey, game: lobby, winner: winnerId });
|
||||
await onGameOver(client, "snake", playerId, winnerId, "", { p1: lobby.p1.score, p2: lobby.p2.score });
|
||||
} else if (lobby.p1.win || lobby.p2.win) {
|
||||
// One player won by filling the grid
|
||||
const winnerId = lobby.p1.win ? lobby.p1.id : lobby.p2.id;
|
||||
io.emit("snakegameOver", { gameKey: lobby.gameKey, game: lobby, winner: winnerId });
|
||||
await onGameOver(client, "snake", playerId, winnerId, "", { p1: lobby.p1.score, p2: lobby.p2.score });
|
||||
}
|
||||
delete activeSnakeGames[lobby.gameKey];
|
||||
}
|
||||
|
||||
export async function onGameOver(client, gameType, playerId, winnerId, reason = "", scores = null) {
|
||||
const { activeGames, title } = getGameAssets(gameType);
|
||||
const gameKey = Object.keys(activeGames).find((key) => key.includes(playerId));
|
||||
const game = gameKey ? activeGames[gameKey] : undefined;
|
||||
@@ -198,24 +285,29 @@ async function onGameOver(client, gameType, playerId, winnerId, reason = "") {
|
||||
game.gameOver = true;
|
||||
let resultText;
|
||||
if (winnerId === null) {
|
||||
await eloHandler(game.p1.id, game.p2.id, 0.5, 0.5, title.toUpperCase());
|
||||
await eloHandler(game.p1.id, game.p2.id, 0.5, 0.5, title.toUpperCase(), scores);
|
||||
resultText = "Égalité";
|
||||
} else {
|
||||
await eloHandler(
|
||||
game.p1.id,
|
||||
game.p2.id,
|
||||
game.p1.id === winnerId ? 1 : 0,
|
||||
game.p2.id === winnerId ? 1 : 0,
|
||||
title.toUpperCase(),
|
||||
);
|
||||
// Temp fix: Don't update ELO for Snake since it's not in a stable state yet.
|
||||
if (gameType !== "snake") {
|
||||
await eloHandler(
|
||||
game.p1.id,
|
||||
game.p2.id,
|
||||
game.p1.id === winnerId ? 1 : 0,
|
||||
game.p2.id === winnerId ? 1 : 0,
|
||||
title.toUpperCase(),
|
||||
scores,
|
||||
);
|
||||
}
|
||||
const winnerName = game.p1.id === winnerId ? game.p1.name : game.p2.name;
|
||||
resultText = `Victoire de ${winnerName}`;
|
||||
}
|
||||
|
||||
await updateDiscordMessage(client, game, title, `${resultText} ${reason}`);
|
||||
|
||||
if (gameType === "tictactoe") io.emit("tictactoegameOver", { game, winner: winnerId });
|
||||
if (gameType === "connect4") io.emit("connect4gameOver", { game, winner: winnerId });
|
||||
if (gameType === "snake") io.emit("snakegameOver", { gameKey: game.gameKey, game, winner: winnerId });
|
||||
|
||||
await updateDiscordMessage(client, game, title, `${resultText} ${reason}`);
|
||||
|
||||
if (gameKey) {
|
||||
setTimeout(() => delete activeGames[gameKey], 1000);
|
||||
@@ -228,7 +320,7 @@ async function createGame(client, gameType) {
|
||||
const { queue, activeGames, title } = getGameAssets(gameType);
|
||||
const p1Id = queue.shift();
|
||||
const p2Id = queue.shift();
|
||||
const [p1, p2] = await Promise.all([client.users.fetch(p1Id), client.users.fetch(p2Id)]);
|
||||
const [p1, p2] = await Promise.all([resolveUser(client, p1Id), resolveUser(client, p2Id)]);
|
||||
|
||||
let lobby;
|
||||
if (gameType === "tictactoe") {
|
||||
@@ -251,8 +343,7 @@ async function createGame(client, gameType) {
|
||||
gameOver: false,
|
||||
lastmove: Date.now(),
|
||||
};
|
||||
} else {
|
||||
// connect4
|
||||
} else if (gameType === "connect4") {
|
||||
lobby = {
|
||||
p1: {
|
||||
id: p1Id,
|
||||
@@ -272,15 +363,59 @@ async function createGame(client, gameType) {
|
||||
lastmove: Date.now(),
|
||||
winningPieces: [],
|
||||
};
|
||||
} else if (gameType === "snake") {
|
||||
const gameKey = `${p1Id}-${p2Id}-${Date.now()}`;
|
||||
lobby = {
|
||||
gameKey: gameKey,
|
||||
p1: {
|
||||
id: p1Id,
|
||||
name: p1.globalName,
|
||||
avatar: p1.displayAvatarURL({ dynamic: true, size: 256 }),
|
||||
snake: [],
|
||||
food: null,
|
||||
score: 0,
|
||||
gameOver: false,
|
||||
win: false,
|
||||
},
|
||||
p2: {
|
||||
id: p2Id,
|
||||
name: p2.globalName,
|
||||
avatar: p2.displayAvatarURL({ dynamic: true, size: 256 }),
|
||||
snake: [],
|
||||
food: null,
|
||||
score: 0,
|
||||
gameOver: false,
|
||||
win: false,
|
||||
},
|
||||
gameOver: false,
|
||||
lastmove: Date.now(),
|
||||
};
|
||||
activeGames[gameKey] = lobby;
|
||||
}
|
||||
|
||||
const msgId = await updateDiscordMessage(client, lobby, title);
|
||||
lobby.msgId = msgId;
|
||||
|
||||
const gameKey = `${p1Id}-${p2Id}`;
|
||||
activeGames[gameKey] = lobby;
|
||||
if (gameType !== "snake") {
|
||||
const gameKey = `${p1Id}-${p2Id}`;
|
||||
activeGames[gameKey] = lobby;
|
||||
}
|
||||
|
||||
io.emit(`${gameType}playing`, { allPlayers: Object.values(activeGames) });
|
||||
|
||||
// For Snake, also emit a specific match notification to the two players
|
||||
if (gameType === "snake") {
|
||||
io.emit("snakematch", {
|
||||
gameKey: lobby.gameKey,
|
||||
p1Id: p1Id,
|
||||
p2Id: p2Id,
|
||||
lobby: {
|
||||
p1: lobby.p1,
|
||||
p2: lobby.p2,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
await emitQueueUpdate(client, gameType);
|
||||
}
|
||||
|
||||
@@ -292,8 +427,9 @@ async function refreshQueuesForUser(userId, client) {
|
||||
if (index > -1) {
|
||||
tictactoeQueue.splice(index, 1);
|
||||
try {
|
||||
const generalChannel = await client.channels.fetch(process.env.BOT_CHANNEL_ID);
|
||||
const user = await client.users.fetch(userId);
|
||||
const guild = client.guilds.cache.get(process.env.GUILD_ID);
|
||||
const generalChannel = guild.channels.cache.get(process.env.BOT_CHANNEL_ID);
|
||||
const user = await resolveUser(client, userId);
|
||||
const queueMsg = await generalChannel.messages.fetch(queueMessagesEndpoints[userId]);
|
||||
const updatedEmbed = new EmbedBuilder()
|
||||
.setTitle("Tic Tac Toe")
|
||||
@@ -311,8 +447,9 @@ async function refreshQueuesForUser(userId, client) {
|
||||
if (index > -1) {
|
||||
connect4Queue.splice(index, 1);
|
||||
try {
|
||||
const generalChannel = await client.channels.fetch(process.env.BOT_CHANNEL_ID);
|
||||
const user = await client.users.fetch(userId);
|
||||
const guild = client.guilds.cache.get(process.env.GUILD_ID);
|
||||
const generalChannel = guild.channels.cache.get(process.env.BOT_CHANNEL_ID);
|
||||
const user = await resolveUser(client, userId);
|
||||
const queueMsg = await generalChannel.messages.fetch(queueMessagesEndpoints[userId]);
|
||||
const updatedEmbed = new EmbedBuilder()
|
||||
.setTitle("Puissance 4")
|
||||
@@ -326,15 +463,36 @@ async function refreshQueuesForUser(userId, client) {
|
||||
}
|
||||
}
|
||||
|
||||
index = snakeQueue.indexOf(userId);
|
||||
if (index > -1) {
|
||||
snakeQueue.splice(index, 1);
|
||||
try {
|
||||
const guild = client.guilds.cache.get(process.env.GUILD_ID);
|
||||
const generalChannel = guild.channels.cache.get(process.env.BOT_CHANNEL_ID);
|
||||
const user = await resolveUser(client, userId);
|
||||
const queueMsg = await generalChannel.messages.fetch(queueMessagesEndpoints[userId]);
|
||||
const updatedEmbed = new EmbedBuilder()
|
||||
.setTitle("Snake 1v1")
|
||||
.setDescription(`**${user.globalName || user.username}** a quitté la file d'attente.`)
|
||||
.setColor(0xed4245)
|
||||
.setTimestamp(new Date());
|
||||
await queueMsg.edit({ embeds: [updatedEmbed], components: [] });
|
||||
delete queueMessagesEndpoints[userId];
|
||||
} catch (e) {
|
||||
console.error("Error updating queue message : ", e);
|
||||
}
|
||||
}
|
||||
|
||||
await emitQueueUpdate(client, "tictactoe");
|
||||
await emitQueueUpdate(client, "connect4");
|
||||
await emitQueueUpdate(client, "snake");
|
||||
}
|
||||
|
||||
async function emitQueueUpdate(client, gameType) {
|
||||
const { queue, activeGames } = getGameAssets(gameType);
|
||||
const names = await Promise.all(
|
||||
queue.map(async (id) => {
|
||||
const user = await client.users.fetch(id).catch(() => null);
|
||||
const user = await resolveUser(client, id).catch(() => null);
|
||||
return user?.globalName || user?.username;
|
||||
}),
|
||||
);
|
||||
@@ -359,13 +517,20 @@ function getGameAssets(gameType) {
|
||||
title: "Puissance 4",
|
||||
url: "/connect-4",
|
||||
};
|
||||
if (gameType === "snake")
|
||||
return {
|
||||
queue: snakeQueue,
|
||||
activeGames: activeSnakeGames,
|
||||
title: "Snake 1v1",
|
||||
url: "/snake",
|
||||
};
|
||||
return { queue: [], activeGames: {} };
|
||||
}
|
||||
|
||||
async function postQueueToDiscord(client, playerId, title, url) {
|
||||
try {
|
||||
const generalChannel = await client.channels.fetch(process.env.BOT_CHANNEL_ID);
|
||||
const user = await client.users.fetch(playerId);
|
||||
const generalChannel = client.channels.cache.get(process.env.BOT_CHANNEL_ID);
|
||||
const user = await resolveUser(client, playerId);
|
||||
const embed = new EmbedBuilder()
|
||||
.setTitle(title)
|
||||
.setDescription(`**${user.globalName || user.username}** est dans la file d'attente.`)
|
||||
@@ -388,7 +553,7 @@ async function postQueueToDiscord(client, playerId, title, url) {
|
||||
}
|
||||
|
||||
async function updateDiscordMessage(client, game, title, resultText = "") {
|
||||
const channel = await client.channels.fetch(process.env.BOT_CHANNEL_ID).catch(() => null);
|
||||
const channel = client.channels.cache.get(process.env.BOT_CHANNEL_ID);
|
||||
if (!channel) return null;
|
||||
|
||||
let description;
|
||||
@@ -399,8 +564,10 @@ async function updateDiscordMessage(client, game, title, resultText = "") {
|
||||
if (i % 3 === 0) gridText += "\n";
|
||||
}
|
||||
description = `### **❌ ${game.p1.name}** vs **${game.p2.name} ⭕**\n${gridText}`;
|
||||
} else {
|
||||
} else if (title === "Puissance 4") {
|
||||
description = `**🔴 ${game.p1.name}** vs **${game.p2.name} 🟡**\n\n${formatConnect4BoardForDiscord(game.board)}`;
|
||||
} else if (title === "Snake 1v1") {
|
||||
description = `**🐍 ${game.p1.name}** (${game.p1.score}) vs (${game.p2.score}) **${game.p2.name}** `;
|
||||
}
|
||||
if (resultText) description += `\n### ${resultText}`;
|
||||
|
||||
@@ -436,6 +603,7 @@ function cleanupStaleGames() {
|
||||
};
|
||||
cleanup(activeTicTacToeGames, "TicTacToe");
|
||||
cleanup(activeConnect4Games, "Connect4");
|
||||
cleanup(activeSnakeGames, "Snake");
|
||||
}
|
||||
|
||||
/* EMITS */
|
||||
@@ -456,6 +624,7 @@ export async function emitPokerToast(data) {
|
||||
}
|
||||
|
||||
export const emitUpdate = (type, room) => io.emit("blackjack:update", { type, room });
|
||||
export const emitPlayerUpdate = (data) => io.emit("blackjack:chat", data);
|
||||
export const emitToast = (payload) => io.emit("blackjack:toast", payload);
|
||||
|
||||
export const emitSolitaireUpdate = (userId, moves) => io.emit("solitaire:update", { userId, moves });
|
||||
|
||||
49
src/services/csSkin.service.js
Normal file
49
src/services/csSkin.service.js
Normal file
@@ -0,0 +1,49 @@
|
||||
import prisma from "../prisma/client.js";
|
||||
|
||||
export async function getCsSkin(id) {
|
||||
return prisma.csSkin.findUnique({ where: { id } });
|
||||
}
|
||||
|
||||
export async function getUserCsInventory(userId) {
|
||||
return prisma.csSkin.findMany({
|
||||
where: { userId },
|
||||
orderBy: { price: "desc" },
|
||||
});
|
||||
}
|
||||
|
||||
export async function getUserCsSkinsByRarity(userId, rarity) {
|
||||
return prisma.csSkin.findMany({
|
||||
where: { userId, rarity },
|
||||
orderBy: { price: "desc" },
|
||||
});
|
||||
}
|
||||
|
||||
export async function getAllOwnedCsSkins() {
|
||||
return prisma.csSkin.findMany({
|
||||
where: { userId: { not: null } },
|
||||
});
|
||||
}
|
||||
|
||||
export async function insertCsSkin(data) {
|
||||
return prisma.csSkin.create({ data });
|
||||
}
|
||||
|
||||
export async function updateCsSkin(data) {
|
||||
const { id, ...rest } = data;
|
||||
return prisma.csSkin.update({ where: { id }, data: rest });
|
||||
}
|
||||
|
||||
export async function findReferenceSkin(marketHashName, isStattrak, isSouvenir) {
|
||||
return prisma.csSkin.findFirst({
|
||||
where: { marketHashName, isStattrak, isSouvenir, price: { not: null }, float: { not: null } },
|
||||
orderBy: { price: "desc" },
|
||||
});
|
||||
}
|
||||
|
||||
export async function deleteCsSkin(id) {
|
||||
return prisma.csSkin.delete({ where: { id } });
|
||||
}
|
||||
|
||||
export async function deleteManyCsSkins(ids) {
|
||||
return prisma.csSkin.deleteMany({ where: { id: { in: ids } } });
|
||||
}
|
||||
47
src/services/game.service.js
Normal file
47
src/services/game.service.js
Normal file
@@ -0,0 +1,47 @@
|
||||
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,
|
||||
);
|
||||
}
|
||||
}
|
||||
132
src/services/market.service.js
Normal file
132
src/services/market.service.js
Normal file
@@ -0,0 +1,132 @@
|
||||
import prisma from "../prisma/client.js";
|
||||
|
||||
function toOffer(offer) {
|
||||
return { ...offer, openingAt: Number(offer.openingAt), closingAt: Number(offer.closingAt) };
|
||||
}
|
||||
|
||||
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 } },
|
||||
csSkin: { select: { displayName: true, imageUrl: true, rarity: true, wearState: true, float: true, isStattrak: true, isSouvenir: true } },
|
||||
seller: { select: { username: true, globalName: true } },
|
||||
buyer: { select: { username: true, globalName: true } },
|
||||
},
|
||||
});
|
||||
if (!offer) return null;
|
||||
const skinData = offer.csSkin || offer.skin;
|
||||
return toOffer({
|
||||
...offer,
|
||||
skinName: skinData?.displayName,
|
||||
skinIcon: offer.skin?.displayIcon || offer.csSkin?.imageUrl,
|
||||
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 getMarketOffersByCsSkin(csSkinId) {
|
||||
const offers = await prisma.marketOffer.findMany({
|
||||
where: { csSkinId },
|
||||
include: {
|
||||
csSkin: { select: { displayName: true, imageUrl: true } },
|
||||
seller: { select: { username: true, globalName: true } },
|
||||
buyer: { select: { username: true, globalName: true } },
|
||||
},
|
||||
});
|
||||
return offers.map((offer) =>
|
||||
toOffer({
|
||||
...offer,
|
||||
skinName: offer.csSkin?.displayName,
|
||||
skinIcon: offer.csSkin?.imageUrl,
|
||||
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 });
|
||||
}),
|
||||
);
|
||||
}
|
||||
39
src/services/solitaire.service.js
Normal file
39
src/services/solitaire.service.js
Normal file
@@ -0,0 +1,39 @@
|
||||
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: { username: true, globalName: true, avatarUrl: true } } },
|
||||
orderBy: [{ score: "desc" }, { moves: "asc" }, { time: "asc" }],
|
||||
});
|
||||
return stats.map((s) => ({
|
||||
...s,
|
||||
}));
|
||||
}
|
||||
|
||||
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" },
|
||||
});
|
||||
}
|
||||
75
src/services/user.service.js
Normal file
75
src/services/user.service.js
Normal file
@@ -0,0 +1,75 @@
|
||||
import prisma from "../prisma/client.js";
|
||||
import { socketEmit } from "../server/socket.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) {
|
||||
await socketEmit("data-updated", { table: "users", action: "update", userId: id, newCoins: 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,21 +1,22 @@
|
||||
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";
|
||||
|
||||
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,
|
||||
};
|
||||
try {
|
||||
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,32 +56,36 @@ 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") {
|
||||
return !(s.displayName.toLowerCase().includes("vct") && s.displayName.toLowerCase().includes("classic"))
|
||||
return !(s.displayName.toLowerCase().includes("vct") && s.displayName.toLowerCase().includes("classic"));
|
||||
} else {
|
||||
return !s.displayName.toLowerCase().includes("vct");
|
||||
}
|
||||
})
|
||||
.filter((s) => {
|
||||
if (caseType === "ultra") {
|
||||
return true
|
||||
return true;
|
||||
} 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);
|
||||
@@ -88,7 +93,7 @@ export async function drawCaseContent(caseType = "standard", poolSize = 100) {
|
||||
const result = [];
|
||||
|
||||
// 2. Adjust count if the pool is smaller than requested
|
||||
const actualCount = Math.min(count, list.length) ;
|
||||
const actualCount = Math.min(count, list.length);
|
||||
|
||||
for (let i = 0; i < actualCount; i++) {
|
||||
let r = Math.random() * totalWeight;
|
||||
@@ -123,7 +128,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 +139,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}`);
|
||||
@@ -170,10 +175,19 @@ export function drawCaseSkin(caseContent) {
|
||||
|
||||
export function getSkinUpgradeProbs(skin, skinData) {
|
||||
const successProb =
|
||||
(1 - (((skin.currentChroma + skin.currentLvl + skinData.chromas.length + skinData.levels.length) / 18) * (parseInt(skin.tierRank) / 4)))/1.5;
|
||||
const destructionProb = ((skin.currentChroma + skinData.levels.length) / (skinData.chromas.length + skinData.levels.length)) * (parseInt(skin.tierRank) / 5) * 0.075;
|
||||
(1 -
|
||||
((skin.currentChroma + skin.currentLvl + skinData.chromas.length + skinData.levels.length) / 18) *
|
||||
(parseInt(skin.tierRank) / 4)) /
|
||||
1.5;
|
||||
const destructionProb =
|
||||
((skin.currentChroma + skinData.levels.length) / (skinData.chromas.length + skinData.levels.length)) *
|
||||
(parseInt(skin.tierRank) / 5) *
|
||||
0.075;
|
||||
const nextLvl = skin.currentLvl < skinData.levels.length ? skin.currentLvl + 1 : skin.currentLvl;
|
||||
const nextChroma = skin.currentLvl === skinData.levels.length && skin.currentChroma < skinData.chromas.length ? skin.currentChroma + 1 : skin.currentChroma;
|
||||
const nextChroma =
|
||||
skin.currentLvl === skinData.levels.length && skin.currentChroma < skinData.chromas.length
|
||||
? skin.currentChroma + 1
|
||||
: skin.currentChroma;
|
||||
const calculateNextPrice = () => {
|
||||
let result = parseFloat(skin.basePrice);
|
||||
result *= 1 + nextLvl / Math.max(skinData.levels.length, 2);
|
||||
@@ -185,10 +199,18 @@ export function getSkinUpgradeProbs(skin, skinData) {
|
||||
return { successProb, destructionProb, upgradePrice };
|
||||
}
|
||||
|
||||
export function getDummySkinUpgradeProbs(skinLevel, skinChroma, skinTierRank, skinMaxLevels, skinMaxChromas, skinMaxPrice) {
|
||||
export function getDummySkinUpgradeProbs(
|
||||
skinLevel,
|
||||
skinChroma,
|
||||
skinTierRank,
|
||||
skinMaxLevels,
|
||||
skinMaxChromas,
|
||||
skinMaxPrice,
|
||||
) {
|
||||
const successProb =
|
||||
1 - (((skinChroma + skinLevel + (skinMaxChromas + skinMaxLevels)) / 18) * (parseInt(skinTierRank) / 4));
|
||||
const destructionProb = ((skinChroma + skinMaxLevels) / (skinMaxChromas + skinMaxLevels)) * (parseInt(skinTierRank) / 5) * 0.1;
|
||||
const upgradePrice = Math.max(Math.floor((parseFloat(skinMaxPrice) * (1 - successProb))), 1);
|
||||
1 - ((skinChroma + skinLevel + (skinMaxChromas + skinMaxLevels)) / 18) * (parseInt(skinTierRank) / 4);
|
||||
const destructionProb =
|
||||
((skinChroma + skinMaxLevels) / (skinMaxChromas + skinMaxLevels)) * (parseInt(skinTierRank) / 5) * 0.1;
|
||||
const upgradePrice = Math.max(Math.floor(parseFloat(skinMaxPrice) * (1 - successProb)), 1);
|
||||
return { successProb, destructionProb, upgradePrice };
|
||||
}
|
||||
|
||||
80
src/utils/cs.state.js
Normal file
80
src/utils/cs.state.js
Normal file
@@ -0,0 +1,80 @@
|
||||
export let csSkinsData = {};
|
||||
|
||||
export let csSkinsPrices = {};
|
||||
|
||||
// Structured index: baseSkinName -> { base, stattrak, souvenir } -> wearState -> priceData
|
||||
export let csSkinsPriceIndex = {};
|
||||
|
||||
// weaponType -> rarity -> [baseSkinName, ...] (only skins that have Skinport prices)
|
||||
export let weaponRarityPriceMap = {};
|
||||
|
||||
const wearRegex = /\s*\((Factory New|Minimal Wear|Field-Tested|Well-Worn|Battle-Scarred)\)\s*$/;
|
||||
|
||||
function parseSkinportKey(key) {
|
||||
const wearMatch = key.match(wearRegex);
|
||||
if (!wearMatch) return null;
|
||||
|
||||
const wearState = wearMatch[1];
|
||||
let baseName = key.slice(0, wearMatch.index);
|
||||
let variant = "base";
|
||||
|
||||
if (baseName.startsWith("★ StatTrak™ ")) {
|
||||
variant = "stattrak";
|
||||
baseName = "★ " + baseName.slice("★ StatTrak™ ".length);
|
||||
} else if (baseName.startsWith("StatTrak™ ")) {
|
||||
variant = "stattrak";
|
||||
baseName = baseName.slice("StatTrak™ ".length);
|
||||
} else if (baseName.startsWith("Souvenir ")) {
|
||||
variant = "souvenir";
|
||||
baseName = baseName.slice("Souvenir ".length);
|
||||
}
|
||||
|
||||
return { baseName, variant, wearState };
|
||||
}
|
||||
|
||||
export function buildPriceIndex() {
|
||||
csSkinsPriceIndex = {};
|
||||
|
||||
for (const [key, priceData] of Object.entries(csSkinsPrices)) {
|
||||
const parsed = parseSkinportKey(key);
|
||||
if (!parsed) continue;
|
||||
|
||||
const { baseName, variant, wearState } = parsed;
|
||||
|
||||
if (!csSkinsPriceIndex[baseName]) {
|
||||
csSkinsPriceIndex[baseName] = {};
|
||||
}
|
||||
if (!csSkinsPriceIndex[baseName][variant]) {
|
||||
csSkinsPriceIndex[baseName][variant] = {};
|
||||
}
|
||||
csSkinsPriceIndex[baseName][variant][wearState] = priceData;
|
||||
}
|
||||
|
||||
const indexedCount = Object.keys(csSkinsPriceIndex).length;
|
||||
const totalSkins = Object.keys(csSkinsData).length;
|
||||
const coverage = totalSkins > 0 ? ((indexedCount / totalSkins) * 100).toFixed(1) : 0;
|
||||
console.log(`[Skinport] Price index built: ${indexedCount} skins indexed, ${totalSkins} total skins (${coverage}% coverage)`);
|
||||
}
|
||||
|
||||
export function buildWeaponRarityPriceMap() {
|
||||
weaponRarityPriceMap = {};
|
||||
|
||||
for (const [skinName, skinData] of Object.entries(csSkinsData)) {
|
||||
// Only include skins that have at least one Skinport price entry
|
||||
if (!csSkinsPriceIndex[skinName]) continue;
|
||||
|
||||
const weapon = skinData.weapon?.name;
|
||||
const rarity = skinData.rarity?.name;
|
||||
if (!weapon || !rarity) continue;
|
||||
|
||||
if (!weaponRarityPriceMap[weapon]) {
|
||||
weaponRarityPriceMap[weapon] = {};
|
||||
}
|
||||
if (!weaponRarityPriceMap[weapon][rarity]) {
|
||||
weaponRarityPriceMap[weapon][rarity] = [];
|
||||
}
|
||||
weaponRarityPriceMap[weapon][rarity].push(skinName);
|
||||
}
|
||||
|
||||
console.log(`[Skinport] Weapon/rarity price map built: ${Object.keys(weaponRarityPriceMap).length} weapon types`);
|
||||
}
|
||||
231
src/utils/cs.utils.js
Normal file
231
src/utils/cs.utils.js
Normal file
@@ -0,0 +1,231 @@
|
||||
import { csSkinsData, csSkinsPriceIndex, weaponRarityPriceMap } from "./cs.state.js";
|
||||
|
||||
const StateFactoryNew = "Factory New";
|
||||
const StateMinimalWear = "Minimal Wear";
|
||||
const StateFieldTested = "Field-Tested";
|
||||
const StateWellWorn = "Well-Worn";
|
||||
const StateBattleScarred = "Battle-Scarred";
|
||||
|
||||
const EUR_TO_FLOPOS = parseInt(process.env.EUR_TO_FLOPOS) || 6;
|
||||
const FLOAT_MODIFIER_MAX = 0.05;
|
||||
const STATTRAK_FALLBACK_MULTIPLIER = 3.5;
|
||||
const SOUVENIR_FALLBACK_MULTIPLIER = 6;
|
||||
|
||||
const WEAR_STATE_ORDER = [StateFactoryNew, StateMinimalWear, StateFieldTested, StateWellWorn, StateBattleScarred];
|
||||
const WEAR_STATE_RANGES = {
|
||||
[StateFactoryNew]: { min: 0.00, max: 0.07 },
|
||||
[StateMinimalWear]: { min: 0.07, max: 0.15 },
|
||||
[StateFieldTested]: { min: 0.15, max: 0.38 },
|
||||
[StateWellWorn]: { min: 0.38, max: 0.45 },
|
||||
[StateBattleScarred]: { min: 0.45, max: 1.00 },
|
||||
};
|
||||
|
||||
export const RarityToColor = {
|
||||
Gold: 0xffd700, // Standard Gold
|
||||
Extraordinary: 0xffae00, // Orange
|
||||
Covert: 0xeb4b4b, // Red
|
||||
Classified: 0xd32ce6, // Pink/Magenta
|
||||
Restricted: 0x8847ff, // Purple
|
||||
"Mil-Spec Grade": 0x4b69ff, // Dark Blue
|
||||
"Industrial Grade": 0x5e98d9, // Light Blue
|
||||
"Consumer Grade": 0xb0c3d9, // Light Grey/White
|
||||
};
|
||||
|
||||
// Last-resort fallback price ranges in EUR (used only when Skinport has no data)
|
||||
const basePriceRanges = {
|
||||
"Consumer Grade": { min: 0.03, max: 0.10 },
|
||||
"Industrial Grade": { min: 0.05, max: 0.30 },
|
||||
"Mil-Spec Grade": { min: 0.10, max: 1.50 },
|
||||
"Restricted": { min: 1.00, max: 10.00 },
|
||||
"Classified": { min: 5.00, max: 40.00 },
|
||||
"Covert": { min: 25.00, max: 150.00 },
|
||||
"Extraordinary": { min: 100.00, max: 800.00 },
|
||||
};
|
||||
|
||||
export const TRADE_UP_MAP = {
|
||||
"Consumer Grade": "Industrial Grade",
|
||||
"Industrial Grade": "Mil-Spec Grade",
|
||||
"Mil-Spec Grade": "Restricted",
|
||||
"Restricted": "Classified",
|
||||
"Classified": "Covert",
|
||||
};
|
||||
|
||||
export function randomSkinRarity() {
|
||||
const roll = Math.random();
|
||||
|
||||
const goldLimit = 0.003;
|
||||
const extraLimit = goldLimit + 0.014;
|
||||
const classifiedLimit = extraLimit + 0.04;
|
||||
const restrictedLimit = classifiedLimit + 0.2;
|
||||
const milSpecLimit = restrictedLimit + 0.5;
|
||||
const industrialLimit = milSpecLimit + 0.2;
|
||||
|
||||
if (roll < goldLimit) return "Covert";
|
||||
if (roll < extraLimit) return "Extraordinary";
|
||||
if (roll < classifiedLimit) return "Classified";
|
||||
if (roll < restrictedLimit) return "Restricted";
|
||||
if (roll < milSpecLimit) return "Mil-Spec Grade";
|
||||
if (roll < industrialLimit) return "Industrial Grade";
|
||||
return "Consumer Grade";
|
||||
}
|
||||
|
||||
function getSkinportPrice(priceData) {
|
||||
if (!priceData) return null;
|
||||
return priceData.suggested_price ?? priceData.median_price ?? priceData.mean_price ?? priceData.min_price ?? null;
|
||||
}
|
||||
|
||||
function applyFloatModifier(basePrice, float, wearState) {
|
||||
const range = WEAR_STATE_RANGES[wearState];
|
||||
if (!range) return basePrice;
|
||||
const span = range.max - range.min;
|
||||
if (span <= 0) return basePrice;
|
||||
// 0 = best float in range, 1 = worst
|
||||
const positionInRange = (float - range.min) / span;
|
||||
const modifier = 1 + FLOAT_MODIFIER_MAX * (1 - 2 * positionInRange);
|
||||
return basePrice * modifier;
|
||||
}
|
||||
|
||||
function getAdjacentWearStates(wearState) {
|
||||
const idx = WEAR_STATE_ORDER.indexOf(wearState);
|
||||
if (idx === -1) return [];
|
||||
// Return wear states ordered by proximity
|
||||
const adjacent = [];
|
||||
for (let dist = 1; dist < WEAR_STATE_ORDER.length; dist++) {
|
||||
if (idx - dist >= 0) adjacent.push(WEAR_STATE_ORDER[idx - dist]);
|
||||
if (idx + dist < WEAR_STATE_ORDER.length) adjacent.push(WEAR_STATE_ORDER[idx + dist]);
|
||||
}
|
||||
return adjacent;
|
||||
}
|
||||
|
||||
function lookupSkinportEurPrice(skinName, wearState, isStattrak, isSouvenir) {
|
||||
const skinEntry = csSkinsPriceIndex[skinName];
|
||||
if (!skinEntry) return null;
|
||||
|
||||
const variant = isSouvenir ? "souvenir" : isStattrak ? "stattrak" : "base";
|
||||
|
||||
// 1. Exact match: correct variant + wear state
|
||||
let price = getSkinportPrice(skinEntry[variant]?.[wearState]);
|
||||
if (price !== null) return price;
|
||||
|
||||
// 2. Drop variant: use base price × multiplier
|
||||
if (variant !== "base") {
|
||||
const basePrice = getSkinportPrice(skinEntry["base"]?.[wearState]);
|
||||
if (basePrice !== null) {
|
||||
const multiplier = isSouvenir ? SOUVENIR_FALLBACK_MULTIPLIER : STATTRAK_FALLBACK_MULTIPLIER;
|
||||
return basePrice * multiplier;
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Adjacent wear state (same variant, then base with multiplier)
|
||||
for (const adjWear of getAdjacentWearStates(wearState)) {
|
||||
const adjPrice = getSkinportPrice(skinEntry[variant]?.[adjWear]);
|
||||
if (adjPrice !== null) return adjPrice;
|
||||
|
||||
if (variant !== "base") {
|
||||
const adjBase = getSkinportPrice(skinEntry["base"]?.[adjWear]);
|
||||
if (adjBase !== null) {
|
||||
const multiplier = isSouvenir ? SOUVENIR_FALLBACK_MULTIPLIER : STATTRAK_FALLBACK_MULTIPLIER;
|
||||
return adjBase * multiplier;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function findSimilarSkinPrice(skinName, rarity, wearState) {
|
||||
const skinData = csSkinsData[skinName];
|
||||
const weapon = skinData?.weapon?.name;
|
||||
if (!weapon) return null;
|
||||
|
||||
const candidates = weaponRarityPriceMap[weapon]?.[rarity];
|
||||
if (!candidates || candidates.length === 0) return null;
|
||||
|
||||
// Pick a random candidate that has a price for this wear state
|
||||
const shuffled = [...candidates].sort(() => Math.random() - 0.5);
|
||||
for (const candidate of shuffled) {
|
||||
if (candidate === skinName) continue;
|
||||
const entry = csSkinsPriceIndex[candidate];
|
||||
if (!entry) continue;
|
||||
// Try base variant first
|
||||
const price = getSkinportPrice(entry["base"]?.[wearState]);
|
||||
if (price !== null) return price;
|
||||
// Try any wear state
|
||||
for (const ws of WEAR_STATE_ORDER) {
|
||||
const wsPrice = getSkinportPrice(entry["base"]?.[ws]);
|
||||
if (wsPrice !== null) return wsPrice;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
export function generatePrice(skinName, rarity, float, isStattrak, isSouvenir) {
|
||||
const wearState = getWearState(float);
|
||||
let eurPrice = lookupSkinportEurPrice(skinName, wearState, isStattrak, isSouvenir);
|
||||
|
||||
if (eurPrice === null) {
|
||||
// 4. Similar skin: same weapon + same rarity
|
||||
eurPrice = findSimilarSkinPrice(skinName, rarity, wearState);
|
||||
}
|
||||
|
||||
if (eurPrice === null) {
|
||||
// 5. Last resort: rarity-based random range (already in EUR-ish scale)
|
||||
const ranges = basePriceRanges[rarity] || basePriceRanges["Industrial Grade"];
|
||||
eurPrice = ranges.min + Math.random() * (ranges.max - ranges.min);
|
||||
}
|
||||
|
||||
let finalPrice = Math.round(eurPrice * EUR_TO_FLOPOS);
|
||||
finalPrice = applyFloatModifier(finalPrice, float, wearState);
|
||||
finalPrice = Math.max(Math.round(finalPrice), 1);
|
||||
|
||||
return finalPrice.toFixed(0);
|
||||
}
|
||||
|
||||
export function rollStattrak(canBeStattrak) {
|
||||
if (!canBeStattrak) return false;
|
||||
return Math.random() < 0.15;
|
||||
}
|
||||
|
||||
export function rollSouvenir(canBeSouvenir) {
|
||||
if (!canBeSouvenir) return false;
|
||||
return Math.random() < 0.15;
|
||||
}
|
||||
|
||||
export function getRandomFloatInRange(min, max) {
|
||||
return min + Math.random() * (max - min);
|
||||
}
|
||||
|
||||
export function getWearState(wear) {
|
||||
const clamped = Math.max(0.0, Math.min(1.0, wear));
|
||||
|
||||
if (clamped < 0.07) return StateFactoryNew;
|
||||
if (clamped < 0.15) return StateMinimalWear;
|
||||
if (clamped < 0.38) return StateFieldTested;
|
||||
if (clamped < 0.45) return StateWellWorn;
|
||||
return StateBattleScarred;
|
||||
}
|
||||
|
||||
export async function getRandomSkinWithRandomSpecs(u_float, forcedRarity) {
|
||||
const skinNames = Object.keys(csSkinsData);
|
||||
const selectedRarity = forcedRarity || randomSkinRarity();
|
||||
const filteredSkinNames = skinNames.filter(name => csSkinsData[name].rarity.name === selectedRarity);
|
||||
const randomIndex = Math.floor(Math.random() * filteredSkinNames.length);
|
||||
|
||||
const skinName = filteredSkinNames[randomIndex];
|
||||
const skinData = csSkinsData[skinName];
|
||||
const float = (u_float !== null && u_float !== undefined) ? u_float : getRandomFloatInRange(skinData.min_float, skinData.max_float);
|
||||
const wearState = getWearState(float);
|
||||
const skinIsStattrak = rollStattrak(skinData.stattrak);
|
||||
const skinIsSouvenir = rollSouvenir(skinData.souvenir);
|
||||
|
||||
return {
|
||||
name: skinName,
|
||||
data: skinData,
|
||||
isStattrak: skinIsStattrak,
|
||||
isSouvenir: skinIsSouvenir,
|
||||
wearState,
|
||||
float,
|
||||
price: generatePrice(skinName, skinData.rarity.name, float, skinIsStattrak, skinIsSouvenir),
|
||||
};
|
||||
}
|
||||
@@ -5,23 +5,10 @@ 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 * as csSkinService from "../services/csSkin.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,8 +36,8 @@ export async function InstallGlobalCommands(appId, commands) {
|
||||
export async function getAkhys(client) {
|
||||
try {
|
||||
// 1. Fetch Discord Members
|
||||
const initial_akhys = getAllUsers.all().length;
|
||||
const guild = await client.guilds.fetch(process.env.GUILD_ID);
|
||||
const initial_akhys = (await userService.getAllUsers()).length;
|
||||
const guild = client.guilds.cache.get(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 +54,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 +84,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 +162,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 +171,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 +194,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 +260,57 @@ 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);
|
||||
if (!skin) throw new Error(`Skin not found for offer ID: ${offer.id}`);
|
||||
updateSkin.run({
|
||||
user_id: buyer.id,
|
||||
currentLvl: skin.currentLvl,
|
||||
currentChroma: skin.currentChroma,
|
||||
currentPrice: skin.currentPrice,
|
||||
uuid: skin.uuid,
|
||||
});
|
||||
updateMarketOffer.run({
|
||||
// Change skin ownership (supports both Valorant and CS2 skins)
|
||||
if (offer.csSkinId) {
|
||||
const csSkin = await csSkinService.getCsSkin(offer.csSkinId);
|
||||
if (!csSkin) throw new Error(`CS skin not found for offer ID: ${offer.id}`);
|
||||
await csSkinService.updateCsSkin({ id: csSkin.id, userId: buyer.id });
|
||||
} else if (offer.skinUuid) {
|
||||
const skin = await skinService.getSkin(offer.skinUuid);
|
||||
if (!skin) throw new Error(`Skin not found for offer ID: ${offer.id}`);
|
||||
await skinService.updateSkin({
|
||||
userId: buyer.id,
|
||||
currentLvl: skin.currentLvl,
|
||||
currentChroma: skin.currentChroma,
|
||||
currentPrice: skin.currentPrice,
|
||||
uuid: skin.uuid,
|
||||
});
|
||||
}
|
||||
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);
|
||||
@@ -449,10 +439,26 @@ function formatTierText(rank, displayName) {
|
||||
|
||||
export function isMeleeSkin(skinName) {
|
||||
const name = skinName.toLowerCase();
|
||||
return !(name.includes("classic") || name.includes("shorty") || name.includes("frenzy") || name.includes("ghost") || name.includes("sheriff") || name.includes("stinger") || name.includes("spectre") ||
|
||||
name.includes("bucky") || name.includes("judge") || name.includes("bulldog") || name.includes("guardian") ||
|
||||
name.includes("vandal") || name.includes("phantom") || name.includes("marshal") || name.includes("outlaw") ||
|
||||
name.includes("operator") || name.includes("ares") || name.includes("odin"));
|
||||
return !(
|
||||
name.includes("classic") ||
|
||||
name.includes("shorty") ||
|
||||
name.includes("frenzy") ||
|
||||
name.includes("ghost") ||
|
||||
name.includes("sheriff") ||
|
||||
name.includes("stinger") ||
|
||||
name.includes("spectre") ||
|
||||
name.includes("bucky") ||
|
||||
name.includes("judge") ||
|
||||
name.includes("bulldog") ||
|
||||
name.includes("guardian") ||
|
||||
name.includes("vandal") ||
|
||||
name.includes("phantom") ||
|
||||
name.includes("marshal") ||
|
||||
name.includes("outlaw") ||
|
||||
name.includes("operator") ||
|
||||
name.includes("ares") ||
|
||||
name.includes("odin")
|
||||
);
|
||||
}
|
||||
|
||||
export function isVCTSkin(skinName) {
|
||||
@@ -461,31 +467,78 @@ export function isVCTSkin(skinName) {
|
||||
}
|
||||
|
||||
const VCT_TEAMS = {
|
||||
"vct-am": [
|
||||
/x 100t\)$/g, /x c9\)$/g, /x eg\)$/g, /x fur\)$/g, /x krü\)$/g, /x lev\)$/g, /x loud\)$/g,
|
||||
/x mibr\)$/g, /x sen\)$/g, /x nrg\)$/g, /x g2\)$/g, /x nv\)$/g, /x 2g\)$/g
|
||||
],
|
||||
"vct-emea": [
|
||||
/x bbl\)$/g, /x fnc\)$/g, /x fut\)$/g, /x m8\)$/g, /x gx\)$/g, /x kc\)$/g, /x navi\)$/g,
|
||||
/x th\)$/g, /x tl\)$/g, /x vit\)$/g, /x ulf\)$/g, /x pcf\)$/g, /x koi\)$/g, /x apk\)$/g
|
||||
],
|
||||
"vct-pcf": [
|
||||
/x dfm\)$/g, /x drx\)$/g, /x fs\)$/g, /x gen\)$/g, /x ge\)$/g, /x prx\)$/g, /x rrq\)$/g,
|
||||
/x t1\)$/g, /x ts\)$/g, /x zeta\)$/g, /x vl\)$/g, /x ns\)$/g, /x tln\)$/g, /x boom\)$/g, /x bld\)$/g
|
||||
],
|
||||
"vct-cn": [
|
||||
/x ag\)$/g, /x blg\)$/g, /x edg\)$/g, /x fpx\)$/g, /x jdg\)$/g, /x nova\)$/g, /x tec\)$/g,
|
||||
/x te\)$/g, /x tyl\)$/g, /x wol\)$/g, /x xlg\)$/g, /x xlg\)$/g, /x drg\)$/g, /x drg\)$/g
|
||||
]
|
||||
"vct-am": [
|
||||
/x 100t\)$/g,
|
||||
/x c9\)$/g,
|
||||
/x eg\)$/g,
|
||||
/x fur\)$/g,
|
||||
/x krü\)$/g,
|
||||
/x lev\)$/g,
|
||||
/x loud\)$/g,
|
||||
/x mibr\)$/g,
|
||||
/x sen\)$/g,
|
||||
/x nrg\)$/g,
|
||||
/x g2\)$/g,
|
||||
/x nv\)$/g,
|
||||
/x 2g\)$/g,
|
||||
],
|
||||
"vct-emea": [
|
||||
/x bbl\)$/g,
|
||||
/x fnc\)$/g,
|
||||
/x fut\)$/g,
|
||||
/x m8\)$/g,
|
||||
/x gx\)$/g,
|
||||
/x kc\)$/g,
|
||||
/x navi\)$/g,
|
||||
/x th\)$/g,
|
||||
/x tl\)$/g,
|
||||
/x vit\)$/g,
|
||||
/x ulf\)$/g,
|
||||
/x pcf\)$/g,
|
||||
/x koi\)$/g,
|
||||
/x apk\)$/g,
|
||||
],
|
||||
"vct-pcf": [
|
||||
/x dfm\)$/g,
|
||||
/x drx\)$/g,
|
||||
/x fs\)$/g,
|
||||
/x gen\)$/g,
|
||||
/x ge\)$/g,
|
||||
/x prx\)$/g,
|
||||
/x rrq\)$/g,
|
||||
/x t1\)$/g,
|
||||
/x ts\)$/g,
|
||||
/x zeta\)$/g,
|
||||
/x vl\)$/g,
|
||||
/x ns\)$/g,
|
||||
/x tln\)$/g,
|
||||
/x boom\)$/g,
|
||||
/x bld\)$/g,
|
||||
],
|
||||
"vct-cn": [
|
||||
/x ag\)$/g,
|
||||
/x blg\)$/g,
|
||||
/x edg\)$/g,
|
||||
/x fpx\)$/g,
|
||||
/x jdg\)$/g,
|
||||
/x nova\)$/g,
|
||||
/x tec\)$/g,
|
||||
/x te\)$/g,
|
||||
/x tyl\)$/g,
|
||||
/x wol\)$/g,
|
||||
/x xlg\)$/g,
|
||||
/x xlg\)$/g,
|
||||
/x drg\)$/g,
|
||||
],
|
||||
};
|
||||
|
||||
export function getVCTRegion(skinName) {
|
||||
if (!isVCTSkin(skinName)) return null;
|
||||
const name = skinName.toLowerCase().trim();
|
||||
for (const [region, regexes] of Object.entries(VCT_TEAMS)) {
|
||||
if (regexes.some(regex => regex.test(name))) {
|
||||
return region;
|
||||
}
|
||||
if (regexes.some((regex) => regex.test(name))) {
|
||||
return region;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
@@ -493,4 +546,12 @@ export function getVCTRegion(skinName) {
|
||||
export function isChampionsSkin(skinName) {
|
||||
const name = skinName.toLowerCase();
|
||||
return name.includes("champions");
|
||||
}
|
||||
}
|
||||
|
||||
export async function resolveUser(client, userId) {
|
||||
return client.users.cache.get(userId) || await client.users.fetch(userId);
|
||||
}
|
||||
|
||||
export async function resolveMember(guild, userId) {
|
||||
return guild.members.cache.get(userId) || await guild.members.fetch(userId);
|
||||
}
|
||||
|
||||
@@ -1,20 +1,38 @@
|
||||
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 csSkinService from "../services/csSkin.service.js";
|
||||
import * as marketService from "../services/market.service.js";
|
||||
import { EmbedBuilder } from "discord.js";
|
||||
import { resolveUser } from "./index.js";
|
||||
|
||||
/**
|
||||
* Gets the skin display name and icon from an offer, supporting both Valorant and CS2 skins.
|
||||
*/
|
||||
async function getOfferSkinInfo(offer) {
|
||||
if (offer.csSkinId) {
|
||||
const csSkin = await csSkinService.getCsSkin(offer.csSkinId);
|
||||
return { name: csSkin?.displayName || offer.csSkinId, icon: csSkin?.imageUrl || null };
|
||||
}
|
||||
if (offer.skinUuid) {
|
||||
const skin = await skinService.getSkin(offer.skinUuid);
|
||||
return { name: skin?.displayName || offer.skinUuid, icon: skin?.displayIcon || null };
|
||||
}
|
||||
return { name: "Unknown", icon: null };
|
||||
}
|
||||
|
||||
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 { name: skinName, icon: skinIcon } = await getOfferSkinInfo(offer);
|
||||
|
||||
const discordUserSeller = await client.users.fetch(offer.seller_id);
|
||||
const discordUserSeller = await resolveUser(client, 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 !`)
|
||||
.setThumbnail(skin.displayIcon)
|
||||
.setColor(0x5865f2) // Discord blurple
|
||||
.setDescription(`Ton offre pour le skin **${skinName}** a bien été créée !`)
|
||||
.setColor(0x5865f2)
|
||||
.addFields(
|
||||
{
|
||||
name: "📌 Statut",
|
||||
@@ -23,59 +41,59 @@ 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",
|
||||
name: "🆔 ID de l'offre",
|
||||
value: `\`${offer.id}\``,
|
||||
inline: false,
|
||||
},
|
||||
)
|
||||
.setTimestamp();
|
||||
if (skinIcon) embed.setThumbnail(skinIcon);
|
||||
|
||||
discordUserSeller.send({ embeds: [embed] }).catch(console.error);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
// Send notification in guild channel
|
||||
|
||||
try {
|
||||
const guildChannel = await client.channels.fetch(process.env.BOT_CHANNEL_ID);
|
||||
const guildChannel = client.channels.cache.get(process.env.BOT_CHANNEL_ID);
|
||||
const embed = new EmbedBuilder()
|
||||
.setTitle("🔔 Nouvelle offre")
|
||||
.setDescription(`Une offre pour le skin **${skin ? skin.displayName : offer.skin_uuid}** a été créée !`)
|
||||
.setThumbnail(skin.displayIcon)
|
||||
.setColor(0x5865f2) // Discord blurple
|
||||
.setDescription(`Une offre pour le skin **${skinName}** a été créée !`)
|
||||
.setColor(0x5865f2)
|
||||
.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();
|
||||
if (skinIcon) embed.setThumbnail(skinIcon);
|
||||
guildChannel.send({ embeds: [embed] }).catch(console.error);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
@@ -83,21 +101,20 @@ 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 { name: skinName, icon: skinIcon } = await getOfferSkinInfo(offer);
|
||||
|
||||
try {
|
||||
const discordUserSeller = await client.users.fetch(offer.seller_id);
|
||||
const userSeller = getUser.get(offer.seller_id);
|
||||
const discordUserSeller = await resolveUser(client, 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 **${skinName}** viennent de commencer !`,
|
||||
)
|
||||
.setThumbnail(skin.displayIcon)
|
||||
.setColor(0x5865f2) // Discord blurple
|
||||
.setColor(0x5865f2)
|
||||
.addFields(
|
||||
{
|
||||
name: "📌 Statut",
|
||||
@@ -106,49 +123,49 @@ 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",
|
||||
name: "🆔 ID de l'offre",
|
||||
value: `\`${offer.id}\``,
|
||||
inline: false,
|
||||
},
|
||||
)
|
||||
.setTimestamp();
|
||||
if (skinIcon) embed.setThumbnail(skinIcon);
|
||||
|
||||
discordUserSeller.send({ embeds: [embed] }).catch(console.error);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
// Send notification in guild channel
|
||||
|
||||
try {
|
||||
const guildChannel = await client.channels.fetch(process.env.BOT_CHANNEL_ID);
|
||||
const guildChannel = client.channels.cache.get(process.env.BOT_CHANNEL_ID);
|
||||
const embed = new EmbedBuilder()
|
||||
.setTitle("🔔 Début des enchères")
|
||||
.setDescription(
|
||||
`Les enchères sur l'offre pour le skin **${skin ? skin.displayName : offer.skin_uuid}** viennent de commencer !`,
|
||||
`Les enchères sur l'offre pour le skin **${skinName}** viennent de commencer !`,
|
||||
)
|
||||
.setThumbnail(skin.displayIcon)
|
||||
.setColor(0x5865f2) // Discord blurple
|
||||
.setColor(0x5865f2)
|
||||
.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();
|
||||
if (skinIcon) embed.setThumbnail(skinIcon);
|
||||
guildChannel.send({ embeds: [embed] }).catch(console.error);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
@@ -156,23 +173,23 @@ 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 { name: skinName, icon: skinIcon } = await getOfferSkinInfo(offer);
|
||||
const bids = await marketService.getOfferBids(offer.id);
|
||||
|
||||
const discordUserSeller = await client.users.fetch(offer.seller_id);
|
||||
const discordUserSeller = await resolveUser(client, 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 **${skinName}** viennent de se terminer !`,
|
||||
)
|
||||
.setThumbnail(skin.displayIcon)
|
||||
.setColor(0x5865f2) // Discord blurple
|
||||
.setColor(0x5865f2)
|
||||
.setTimestamp();
|
||||
if (skinIcon) embed.setThumbnail(skinIcon);
|
||||
|
||||
if (bids.length === 0) {
|
||||
embed.addFields(
|
||||
@@ -181,21 +198,21 @@ export async function handleMarketOfferClosing(offerId, client) {
|
||||
value: "Tu conserves ce skin dans ton inventaire.",
|
||||
},
|
||||
{
|
||||
name: "🆔 ID de l’offre",
|
||||
name: "🆔 ID de l'offre",
|
||||
value: `\`${offer.id}\``,
|
||||
inline: false,
|
||||
},
|
||||
);
|
||||
} else {
|
||||
const highestBid = bids[0];
|
||||
const highestBidderUser = await client.users.fetch(highestBid.bidder_id);
|
||||
const highestBidderUser = await resolveUser(client, 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",
|
||||
name: "🆔 ID de l'offre",
|
||||
value: `\`${offer.id}\``,
|
||||
inline: false,
|
||||
},
|
||||
@@ -208,18 +225,17 @@ export async function handleMarketOfferClosing(offerId, client) {
|
||||
console.error(e);
|
||||
}
|
||||
|
||||
// Send notification in guild channel
|
||||
|
||||
try {
|
||||
const guildChannel = await client.channels.fetch(process.env.BOT_CHANNEL_ID);
|
||||
const guild = client.guilds.cache.get(process.env.GUILD_ID);
|
||||
const guildChannel = guild.channels.cache.get(process.env.BOT_CHANNEL_ID);
|
||||
const embed = new EmbedBuilder()
|
||||
.setTitle("🔔 Fin des enchères")
|
||||
.setDescription(
|
||||
`Les enchères sur l'offre pour le skin **${skin ? skin.displayName : offer.skin_uuid}** viennent de se terminer !`,
|
||||
`Les enchères sur l'offre pour le skin **${skinName}** viennent de se terminer !`,
|
||||
)
|
||||
.setThumbnail(skin.displayIcon)
|
||||
.setColor(0x5865f2) // Discord blurple
|
||||
.setColor(0x5865f2)
|
||||
.setTimestamp();
|
||||
if (skinIcon) embed.setThumbnail(skinIcon);
|
||||
|
||||
if (bids.length === 0) {
|
||||
embed.addFields({
|
||||
@@ -228,29 +244,28 @@ export async function handleMarketOfferClosing(offerId, client) {
|
||||
});
|
||||
} else {
|
||||
const highestBid = bids[0];
|
||||
const highestBidderUser = await client.users.fetch(highestBid.bidder_id);
|
||||
const highestBidderUser = await resolveUser(client, 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 resolveUser(client, highestBid.bidderId);
|
||||
const userBidder = await userService.getUser(highestBid.bidderId);
|
||||
if (discordUserBidder && userBidder?.isAkhy) {
|
||||
const embed = new EmbedBuilder()
|
||||
const bidderEmbed = 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 **${skinName}** viennent de se terminer !`,
|
||||
)
|
||||
.setThumbnail(skin.displayIcon)
|
||||
.setColor(0x5865f2) // Discord blurple
|
||||
.setColor(0x5865f2)
|
||||
.setTimestamp();
|
||||
const highestBid = bids[0];
|
||||
embed.addFields({
|
||||
if (skinIcon) bidderEmbed.setThumbnail(skinIcon);
|
||||
bidderEmbed.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);
|
||||
discordUserBidder.send({ embeds: [bidderEmbed] }).catch(console.error);
|
||||
}
|
||||
}
|
||||
guildChannel.send({ embeds: [embed] }).catch(console.error);
|
||||
@@ -260,48 +275,47 @@ 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 { name: skinName, icon: skinIcon } = await getOfferSkinInfo(offer);
|
||||
|
||||
const bidderUser = client.users.fetch(bid.bidder_id);
|
||||
const bidderUser = await resolveUser(client, bid.bidderId);
|
||||
try {
|
||||
const discordUserSeller = await client.users.fetch(offer.seller_id);
|
||||
const userSeller = getUser.get(offer.seller_id);
|
||||
const discordUserSeller = await resolveUser(client, 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 **${skinName}**.`,
|
||||
)
|
||||
.setThumbnail(skin.displayIcon)
|
||||
.setColor(0x5865f2) // Discord blurple
|
||||
.setColor(0x5865f2)
|
||||
.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\``,
|
||||
name: "💰 Montant de l'enchère",
|
||||
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",
|
||||
name: "🆔 ID de l'offre",
|
||||
value: `\`${offer.id}\``,
|
||||
inline: false,
|
||||
},
|
||||
)
|
||||
.setTimestamp();
|
||||
if (skinIcon) embed.setThumbnail(skinIcon);
|
||||
|
||||
discordUserSeller.send({ embeds: [embed] }).catch(console.error);
|
||||
}
|
||||
@@ -310,22 +324,22 @@ 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 resolveUser(client, 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 **${skinName}** a bien été placée!`,
|
||||
)
|
||||
.setThumbnail(skin.displayIcon)
|
||||
.setColor(0x5865f2) // Discord blurple
|
||||
.setColor(0x5865f2)
|
||||
.addFields({
|
||||
name: "💰 Montant de l’enchère",
|
||||
value: `\`${bid.offer_amount} coins\``,
|
||||
name: "💰 Montant de l'enchère",
|
||||
value: `\`${bid.offerAmount} coins\``,
|
||||
inline: true,
|
||||
})
|
||||
.setTimestamp();
|
||||
if (skinIcon) embed.setThumbnail(skinIcon);
|
||||
|
||||
discordUserNewBidder.send({ embeds: [embed] }).catch(console.error);
|
||||
}
|
||||
@@ -334,54 +348,52 @@ export async function handleNewMarketOfferBid(offerId, bidId, client) {
|
||||
}
|
||||
|
||||
try {
|
||||
const offerBids = getOfferBids.all(offer.id);
|
||||
if (offerBids.length < 2) return; // No previous bidder to notify
|
||||
const offerBids = await marketService.getOfferBids(offer.id);
|
||||
if (offerBids.length < 2) return;
|
||||
|
||||
const discordUserPreviousBidder = await client.users.fetch(offerBids[1].bidder_id);
|
||||
const userPreviousBidder = getUser.get(offerBids[1].bidder_id);
|
||||
const discordUserPreviousBidder = await resolveUser(client, 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 **${skinName}**, tu n'es plus le meilleur enchérisseur !`,
|
||||
)
|
||||
.setThumbnail(skin.displayIcon)
|
||||
.setColor(0x5865f2) // Discord blurple
|
||||
.setColor(0x5865f2)
|
||||
.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\``,
|
||||
name: "💰 Montant de l'enchère",
|
||||
value: `\`${bid.offerAmount} coins\``,
|
||||
inline: true,
|
||||
},
|
||||
)
|
||||
.setTimestamp();
|
||||
if (skinIcon) embed.setThumbnail(skinIcon);
|
||||
|
||||
discordUserPreviousBidder.send({ embeds: [embed] }).catch(console.error);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
|
||||
// Notify previous highest bidder
|
||||
}
|
||||
|
||||
export async function handleCaseOpening(caseType, userId, skinUuid, client) {
|
||||
const discordUser = await client.users.fetch(userId);
|
||||
const skin = getSkin.get(skinUuid);
|
||||
const discordUser = await resolveUser(client, userId);
|
||||
const skin = await skinService.getSkin(skinUuid);
|
||||
try {
|
||||
const guildChannel = await client.channels.fetch(process.env.BOT_CHANNEL_ID);
|
||||
const guildChannel = client.channels.cache.get(process.env.BOT_CHANNEL_ID);
|
||||
const embed = new EmbedBuilder()
|
||||
.setTitle("🔔 Ouverture de caisse")
|
||||
.setDescription(
|
||||
`${discordUser ? discordUser.username : "Un utilisateur"} vient d'ouvrir une caisse **${caseType}** et a obtenu le skin **${skin.displayName}** !`,
|
||||
)
|
||||
.setThumbnail(skin.displayIcon)
|
||||
.setColor(skin.tierColor) // Discord blurple
|
||||
.setColor(skin.tierColor)
|
||||
.addFields(
|
||||
{
|
||||
name: "💰 Valeur estimée",
|
||||
|
||||
Reference in New Issue
Block a user