mirror of
https://github.com/cassoule/flopobot_v2.git
synced 2026-03-18 13:30:36 +01:00
1409 lines
47 KiB
JavaScript
1409 lines
47 KiB
JavaScript
import express from "express";
|
|
import { sleep } from "openai/core";
|
|
import Stripe from "stripe";
|
|
|
|
// --- Service Imports ---
|
|
import * as userService from "../../services/user.service.js";
|
|
import * as gameService from "../../services/game.service.js";
|
|
import * as skinService from "../../services/skin.service.js";
|
|
import * as logService from "../../services/log.service.js";
|
|
import * as transactionService from "../../services/transaction.service.js";
|
|
import * as marketService from "../../services/market.service.js";
|
|
|
|
// --- Game State Imports ---
|
|
import { activePolls, activePredis, activeSlowmodes, skins, activeSnakeGames } from "../../game/state.js";
|
|
|
|
// --- Utility and API Imports ---
|
|
import { formatTime, isMeleeSkin, isVCTSkin, isChampionsSkin, getVCTRegion } from "../../utils/index.js";
|
|
import { DiscordRequest } from "../../api/discord.js";
|
|
|
|
// --- Discord.js Builder Imports ---
|
|
import { ActionRowBuilder, ButtonBuilder, ButtonStyle, EmbedBuilder } from "discord.js";
|
|
import { emitDataUpdated, socketEmit, onGameOver } from "../socket.js";
|
|
import { handleCaseOpening } from "../../utils/marketNotifs.js";
|
|
import { drawCaseContent, drawCaseSkin, getSkinUpgradeProbs } from "../../utils/caseOpening.js";
|
|
|
|
// Create a new router instance
|
|
const router = express.Router();
|
|
|
|
/**
|
|
* Factory function to create and configure the main 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 apiRoutes(client, io) {
|
|
// --- Server Health & Basic Data ---
|
|
|
|
router.get("/check", (req, res) => {
|
|
res.status(200).json({ status: "OK", message: "FlopoBot API is running." });
|
|
});
|
|
|
|
router.get("/users", async (req, res) => {
|
|
try {
|
|
const users = await userService.getAllUsers();
|
|
res.json(users);
|
|
} catch (error) {
|
|
console.error("Error fetching users:", error);
|
|
res.status(500).json({ error: "Failed to fetch users." });
|
|
}
|
|
});
|
|
|
|
router.get("/akhys", async (req, res) => {
|
|
try {
|
|
const akhys = await userService.getAllAkhys();
|
|
res.json(akhys);
|
|
} catch (error) {
|
|
console.error("Error fetching akhys:", error);
|
|
res.status(500).json({ error: "Failed to fetch akhys" });
|
|
}
|
|
});
|
|
|
|
router.post("/register-user", async (req, res) => {
|
|
const { discordUserId } = req.body;
|
|
const discordUser = await client.users.fetch(discordUserId);
|
|
|
|
try {
|
|
await userService.insertUser({
|
|
id: discordUser.id,
|
|
username: discordUser.username,
|
|
globalName: discordUser.globalName,
|
|
warned: 0,
|
|
warns: 0,
|
|
allTimeWarns: 0,
|
|
totalRequests: 0,
|
|
avatarUrl: discordUser.displayAvatarURL({ dynamic: true, size: 256 }),
|
|
isAkhy: 0,
|
|
});
|
|
|
|
await userService.updateUserCoins(discordUser.id, 5000);
|
|
await logService.insertLog({
|
|
id: `${discordUser.id}-welcome-${Date.now()}`,
|
|
userId: discordUser.id,
|
|
action: "WELCOME_BONUS",
|
|
targetUserId: null,
|
|
coinsAmount: 5000,
|
|
userNewAmount: 5000,
|
|
});
|
|
|
|
console.log(`New registered user: ${discordUser.username} (${discordUser.id})`);
|
|
|
|
res.status(200).json({ message: `Bienvenue ${discordUser.username} !` });
|
|
} catch (e) {
|
|
console.log(`Failed to register user ${discordUser.username} (${discordUser.id})`, e);
|
|
res.status(500).json({ error: "Erreur lors de la création du nouvel utilisateur." });
|
|
}
|
|
});
|
|
|
|
router.get("/skins", (req, res) => {
|
|
try {
|
|
res.json(skins);
|
|
} catch (error) {
|
|
console.error("Error fetching skins:", error);
|
|
res.status(500).json({ error: "Failed to fetch skins." });
|
|
}
|
|
});
|
|
|
|
router.post("/open-case", async (req, res) => {
|
|
const { userId, caseType } = req.body;
|
|
|
|
let caseTypeVal;
|
|
switch (caseType) {
|
|
case "standard":
|
|
caseTypeVal = 500;
|
|
break;
|
|
case "premium":
|
|
caseTypeVal = 750;
|
|
break;
|
|
case "ultra":
|
|
caseTypeVal = 1500;
|
|
break;
|
|
case "esport":
|
|
caseTypeVal = 100;
|
|
break;
|
|
default:
|
|
return res.status(400).json({ error: "Invalid case type." });
|
|
}
|
|
const commandUser = await userService.getUser(userId);
|
|
if (!commandUser) return res.status(404).json({ error: "User not found." });
|
|
const valoPrice = caseTypeVal;
|
|
if (commandUser.coins < valoPrice) return res.status(403).json({ error: "Not enough FlopoCoins." });
|
|
|
|
try {
|
|
const selectedSkins = await drawCaseContent(caseType);
|
|
|
|
const result = await drawCaseSkin(selectedSkins);
|
|
|
|
// --- Update Database ---
|
|
await logService.insertLog({
|
|
id: `${userId}-${Date.now()}`,
|
|
userId: userId,
|
|
action: "VALO_CASE_OPEN",
|
|
targetUserId: null,
|
|
coinsAmount: -valoPrice,
|
|
userNewAmount: commandUser.coins - valoPrice,
|
|
});
|
|
await userService.updateUserCoins(userId, commandUser.coins - valoPrice);
|
|
await skinService.updateSkin({
|
|
uuid: result.randomSkinData.uuid,
|
|
userId: userId,
|
|
currentLvl: result.randomLevel,
|
|
currentChroma: result.randomChroma,
|
|
currentPrice: result.finalPrice,
|
|
});
|
|
|
|
console.log(
|
|
`${commandUser.username} opened a ${caseType} Valorant case and received skin ${result.randomSelectedSkinUuid}`,
|
|
);
|
|
const updatedSkin = await skinService.getSkin(result.randomSkinData.uuid);
|
|
await handleCaseOpening(caseType, userId, result.randomSelectedSkinUuid, client);
|
|
|
|
const contentSkins = selectedSkins.map((item) => {
|
|
return {
|
|
...item,
|
|
isMelee: isMeleeSkin(item.displayName),
|
|
isVCT: isVCTSkin(item.displayName),
|
|
isChampions: isChampionsSkin(item.displayName),
|
|
vctRegion: getVCTRegion(item.displayName),
|
|
};
|
|
});
|
|
res.json({
|
|
selectedSkins: contentSkins,
|
|
randomSelectedSkinUuid: result.randomSelectedSkinUuid,
|
|
randomSelectedSkinIndex: result.randomSelectedSkinIndex,
|
|
updatedSkin,
|
|
});
|
|
} catch (error) {
|
|
console.error("Error fetching skins:", error);
|
|
res.status(500).json({ error: "Failed to fetch skins." });
|
|
}
|
|
});
|
|
|
|
router.get("/case-content/:type", async (req, res) => {
|
|
const { type } = req.params;
|
|
try {
|
|
const selectedSkins = await drawCaseContent(type, -1);
|
|
for (const item of selectedSkins) {
|
|
item.isMelee = isMeleeSkin(item.displayName);
|
|
item.isVCT = isVCTSkin(item.displayName);
|
|
item.isChampions = isChampionsSkin(item.displayName);
|
|
item.vctRegion = getVCTRegion(item.displayName);
|
|
const skinData = await skinService.getSkin(item.uuid);
|
|
item.basePrice = skinData.basePrice;
|
|
item.maxPrice = skinData.maxPrice;
|
|
}
|
|
res.json({ skins: selectedSkins.sort((a, b) => b.maxPrice - a.maxPrice) });
|
|
} catch (error) {
|
|
console.error("Error fetching case content:", error);
|
|
res.status(500).json({ error: "Failed to fetch case content." });
|
|
}
|
|
});
|
|
|
|
router.get("/skin/:id", (req, res) => {
|
|
try {
|
|
const skinData = skins.find((s) => s.uuid === req.params.id);
|
|
res.json(skinData);
|
|
} catch (error) {
|
|
console.error("Error fetching skin:", error);
|
|
res.status(500).json({ error: "Failed to fetch skin." });
|
|
}
|
|
});
|
|
|
|
router.post("/skin/:id", (req, res) => {
|
|
const { level, chroma } = req.body;
|
|
try {
|
|
const skinData = skins.find((s) => s.uuid === req.params.id);
|
|
if (!skinData) res.status(404).json({ error: "Invalid skin." });
|
|
|
|
const levelData = skinData.levels[level - 1] || {};
|
|
const chromaData = skinData.chromas[chroma - 1] || {};
|
|
|
|
let videoUrl = null;
|
|
if (level === skinData.levels.length) {
|
|
videoUrl = chromaData.streamedVideo;
|
|
}
|
|
videoUrl = videoUrl || levelData.streamedVideo;
|
|
|
|
res.json({ url: videoUrl });
|
|
} catch (error) {
|
|
console.error("Error fetching skins:", error);
|
|
res.status(500).json({ error: "Failed to fetch skins." });
|
|
}
|
|
});
|
|
|
|
router.post("/skin/:uuid/instant-sell", async (req, res) => {
|
|
const { userId } = req.body;
|
|
try {
|
|
const skin = await skinService.getSkin(req.params.uuid);
|
|
const skinData = skins.find((s) => s.uuid === skin.uuid);
|
|
if (!skinData) {
|
|
return res.status(403).json({ error: "Invalid skin." });
|
|
}
|
|
if (skin.userId !== userId) {
|
|
return res.status(403).json({ error: "User does not own this skin." });
|
|
}
|
|
|
|
const marketOffers = await marketService.getMarketOffersBySkin(skin.uuid);
|
|
const activeOffers = marketOffers.filter((offer) => offer.status === "pending" || offer.status === "open");
|
|
if (activeOffers.length > 0) {
|
|
return res
|
|
.status(403)
|
|
.json({ error: "Impossible de vendre ce skin, une offre FlopoMarket est déjà en cours." });
|
|
}
|
|
|
|
const commandUser = await userService.getUser(userId);
|
|
if (!commandUser) {
|
|
return res.status(404).json({ error: "User not found." });
|
|
}
|
|
const sellPrice = skin.currentPrice;
|
|
|
|
await logService.insertLog({
|
|
id: `${userId}-${Date.now()}`,
|
|
userId: userId,
|
|
action: "VALO_SKIN_INSTANT_SELL",
|
|
targetUserId: null,
|
|
coinsAmount: sellPrice,
|
|
userNewAmount: commandUser.coins + sellPrice,
|
|
});
|
|
await userService.updateUserCoins(userId, commandUser.coins + sellPrice);
|
|
await skinService.updateSkin({
|
|
uuid: skin.uuid,
|
|
userId: null,
|
|
currentLvl: null,
|
|
currentChroma: null,
|
|
currentPrice: null,
|
|
});
|
|
console.log(`${commandUser.username} instantly sold skin ${skin.uuid} for ${sellPrice} FlopoCoins`);
|
|
res.status(200).json({ sellPrice });
|
|
} catch (error) {
|
|
console.error("Error fetching skin upgrade:", error);
|
|
res.status(500).json({ error: "Failed to fetch skin upgrade." });
|
|
}
|
|
});
|
|
|
|
router.get("/skin-upgrade/:uuid/fetch", async (req, res) => {
|
|
try {
|
|
const skin = await skinService.getSkin(req.params.uuid);
|
|
const skinData = skins.find((s) => s.uuid === skin.uuid);
|
|
const { successProb, destructionProb, upgradePrice } = getSkinUpgradeProbs(skin, skinData);
|
|
|
|
const segments = [
|
|
{ id: "SUCCEEDED", color: "5865f2", percent: successProb, label: "Réussie" },
|
|
{ id: "DESTRUCTED", color: "f26558", percent: destructionProb, label: "Détruit" },
|
|
{ id: "NONE", color: "18181818", percent: 1 - successProb - destructionProb, label: "Échec" },
|
|
];
|
|
|
|
res.json({ segments, upgradePrice });
|
|
} catch (error) {
|
|
console.log(error);
|
|
res.status(500).json({ error: "Failed to fetch skin upgrade." });
|
|
}
|
|
});
|
|
|
|
router.post("/skin-upgrade/:uuid", async (req, res) => {
|
|
const { userId } = req.body;
|
|
try {
|
|
const skin = await skinService.getSkin(req.params.uuid);
|
|
const skinData = skins.find((s) => s.uuid === skin.uuid);
|
|
if (!skinData || (skin.currentLvl >= skinData.levels.length && skin.currentChroma >= skinData.chromas.length)) {
|
|
return res.status(403).json({ error: "Skin is already maxed out or invalid skin." });
|
|
}
|
|
if (skin.userId !== userId) {
|
|
return res.status(403).json({ error: "User does not own this skin." });
|
|
}
|
|
const marketOffers = await marketService.getMarketOffersBySkin(skin.uuid);
|
|
const activeOffers = marketOffers.filter((offer) => offer.status === "pending" || offer.status === "open");
|
|
if (activeOffers.length > 0) {
|
|
return res.status(403).json({ error: "Impossible d'améliorer ce skin, une offre FlopoMarket est en cours." });
|
|
}
|
|
const { successProb, destructionProb, upgradePrice } = getSkinUpgradeProbs(skin, skinData);
|
|
|
|
const commandUser = await userService.getUser(userId);
|
|
if (!commandUser) {
|
|
return res.status(404).json({ error: "User not found." });
|
|
}
|
|
if (commandUser.coins < upgradePrice) {
|
|
return res.status(403).json({ error: `Pas assez de FlopoCoins (${upgradePrice} requis).` });
|
|
}
|
|
|
|
await logService.insertLog({
|
|
id: `${userId}-${Date.now()}`,
|
|
userId: userId,
|
|
action: "VALO_SKIN_UPGRADE",
|
|
targetUserId: null,
|
|
coinsAmount: -upgradePrice,
|
|
userNewAmount: commandUser.coins - upgradePrice,
|
|
});
|
|
await userService.updateUserCoins(userId, commandUser.coins - upgradePrice);
|
|
|
|
let succeeded = false;
|
|
let destructed = false;
|
|
|
|
const roll = Math.random();
|
|
if (roll < destructionProb) {
|
|
destructed = true;
|
|
} else if (roll < successProb + destructionProb) {
|
|
succeeded = true;
|
|
}
|
|
|
|
if (succeeded) {
|
|
const isLevelUpgrade = skin.currentLvl < skinData.levels.length;
|
|
if (isLevelUpgrade) {
|
|
skin.currentLvl++;
|
|
} else {
|
|
skin.currentChroma++;
|
|
}
|
|
const calculatePrice = () => {
|
|
let result = parseFloat(skin.basePrice);
|
|
result *= 1 + skin.currentLvl / Math.max(skinData.levels.length, 2);
|
|
result *= 1 + skin.currentChroma / 4;
|
|
return parseFloat(result.toFixed(0));
|
|
};
|
|
skin.currentPrice = calculatePrice();
|
|
|
|
await skinService.updateSkin({
|
|
uuid: skin.uuid,
|
|
userId: skin.userId,
|
|
currentLvl: skin.currentLvl,
|
|
currentChroma: skin.currentChroma,
|
|
currentPrice: skin.currentPrice,
|
|
});
|
|
} else if (destructed) {
|
|
await skinService.updateSkin({
|
|
uuid: skin.uuid,
|
|
userId: null,
|
|
currentLvl: null,
|
|
currentChroma: null,
|
|
currentPrice: null,
|
|
});
|
|
}
|
|
|
|
console.log(
|
|
`${commandUser.username} attempted to upgrade skin ${skin.uuid} - ${succeeded ? "SUCCEEDED" : destructed ? "DESTRUCTED" : "FAILED"}`,
|
|
);
|
|
res.json({ wonId: succeeded ? "SUCCEEDED" : destructed ? "DESTRUCTED" : "NONE" });
|
|
} catch (error) {
|
|
console.error("Error fetching skin upgrade:", error);
|
|
res.status(500).json({ error: "Failed to fetch skin upgrade." });
|
|
}
|
|
});
|
|
|
|
router.get("/users/by-elo", async (req, res) => {
|
|
try {
|
|
const users = await gameService.getUsersByElo();
|
|
res.json(users);
|
|
} catch (error) {
|
|
console.error("Error fetching users by Elo:", error);
|
|
res.status(500).json({ error: "Failed to fetch users by Elo." });
|
|
}
|
|
});
|
|
|
|
router.get("/logs", async (req, res) => {
|
|
try {
|
|
await logService.pruneOldLogs();
|
|
const logs = await logService.getLogs();
|
|
res.status(200).json(logs);
|
|
} catch (error) {
|
|
console.error("Error fetching logs:", error);
|
|
res.status(500).json({ error: "Failed to fetch logs." });
|
|
}
|
|
});
|
|
|
|
// --- User-Specific Routes ---
|
|
router.get("/user/:id", async (req, res) => {
|
|
try {
|
|
const user = await userService.getUser(req.params.id);
|
|
res.json({ user });
|
|
} catch (error) {
|
|
res.status(404).json({ error: "User not found." });
|
|
}
|
|
});
|
|
|
|
router.get("/user/:id/avatar", async (req, res) => {
|
|
try {
|
|
const user = await client.users.fetch(req.params.id);
|
|
const avatarUrl = user.displayAvatarURL({ format: "png", size: 256 });
|
|
res.json({ avatarUrl });
|
|
} catch (error) {
|
|
res.status(404).json({ error: "User not found or failed to fetch avatar." });
|
|
}
|
|
});
|
|
|
|
router.get("/user/:id/username", async (req, res) => {
|
|
try {
|
|
const user = await client.users.fetch(req.params.id);
|
|
res.json({ user });
|
|
} catch (error) {
|
|
res.status(404).json({ error: "User not found." });
|
|
}
|
|
});
|
|
|
|
router.get("/user/:id/coins", async (req, res) => {
|
|
try {
|
|
const user = await userService.getUser(req.params.id);
|
|
res.json({ coins: user.coins });
|
|
} catch (error) {
|
|
res.status(404).json({ error: "User not found." });
|
|
}
|
|
});
|
|
|
|
router.get("/user/:id/sparkline", async (req, res) => {
|
|
try {
|
|
const logs = await logService.getUserLogs(req.params.id);
|
|
res.json({ sparkline: logs });
|
|
} catch (error) {
|
|
res.status(500).json({ error: "Failed to fetch logs for sparkline." });
|
|
}
|
|
});
|
|
|
|
router.get("/user/:id/elo", async (req, res) => {
|
|
try {
|
|
const eloData = await gameService.getUserElo(req.params.id);
|
|
res.json({ elo: eloData?.elo || null });
|
|
} catch (e) {
|
|
res.status(500).json({ error: "Failed to fetch Elo data." });
|
|
}
|
|
});
|
|
|
|
router.get("/user/:id/elo-graph", async (req, res) => {
|
|
try {
|
|
const games = await gameService.getUserGames(req.params.id);
|
|
const eloHistory = games
|
|
.filter((g) => g.type !== "POKER_ROUND" && g.type !== "SOTD")
|
|
.filter((game) => game.p2 !== null)
|
|
.map((game) => (game.p1 === req.params.id ? game.p1NewElo : game.p2NewElo));
|
|
eloHistory.splice(0, 0, 1000);
|
|
res.json({ eloGraph: eloHistory });
|
|
} catch (e) {
|
|
res.status(500).json({ error: "Failed to generate Elo graph." });
|
|
}
|
|
});
|
|
|
|
router.get("/user/:id/inventory", async (req, res) => {
|
|
try {
|
|
const inventory = await skinService.getUserInventory(req.params.id);
|
|
for (const skin of inventory) {
|
|
const marketOffers = await marketService.getMarketOffersBySkin(skin.uuid);
|
|
for (const offer of marketOffers) {
|
|
offer.skin = await skinService.getSkin(offer.skinUuid);
|
|
offer.seller = await userService.getUser(offer.sellerId);
|
|
offer.buyer = offer.buyerId ? await userService.getUser(offer.buyerId) : null;
|
|
offer.bids = (await marketService.getOfferBids(offer.id)) || {};
|
|
for (const bid of offer.bids) {
|
|
bid.bidder = await userService.getUser(bid.bidderId);
|
|
}
|
|
}
|
|
skin.offers = marketOffers || {};
|
|
skin.isMelee = isMeleeSkin(skin.displayName);
|
|
skin.isVCT = isVCTSkin(skin.displayName);
|
|
skin.isChampions = isChampionsSkin(skin.displayName);
|
|
skin.vctRegion = getVCTRegion(skin.displayName);
|
|
}
|
|
res.json({ inventory });
|
|
} catch (error) {
|
|
console.log(error);
|
|
res.status(500).json({ error: "Failed to fetch inventory." });
|
|
}
|
|
});
|
|
|
|
router.get("/user/:id/games-history", async (req, res) => {
|
|
try {
|
|
const games = (await gameService.getUserGames(req.params.id))
|
|
.filter((g) => g.type !== "POKER_ROUND" && g.type !== "SOTD")
|
|
.reverse()
|
|
.slice(0, 50);
|
|
res.json({ games });
|
|
} catch (err) {
|
|
res.status(500).json({ error: "Failed to fetch games history." });
|
|
}
|
|
});
|
|
|
|
router.get("/user/:id/daily", async (req, res) => {
|
|
const { id } = req.params;
|
|
try {
|
|
const akhy = await userService.getUser(id);
|
|
if (!akhy) return res.status(404).json({ message: "Utilisateur introuvable" });
|
|
if (akhy.dailyQueried) return res.status(403).json({ message: "Récompense journalière déjà récupérée." });
|
|
|
|
const amount = 500;
|
|
const newCoins = akhy.coins + amount;
|
|
await userService.queryDailyReward(id);
|
|
await userService.updateUserCoins(id, newCoins);
|
|
await logService.insertLog({
|
|
id: `${id}-daily-${Date.now()}`,
|
|
userId: id,
|
|
action: "DAILY_REWARD",
|
|
targetUserId: null,
|
|
coinsAmount: amount,
|
|
userNewAmount: newCoins,
|
|
});
|
|
|
|
await socketEmit("daily-queried", { userId: id });
|
|
res.status(200).json({ message: `+${amount} FlopoCoins! Récompense récupérée !` });
|
|
} catch (error) {
|
|
res.status(500).json({ error: "Failed to process daily reward." });
|
|
}
|
|
});
|
|
|
|
// --- Poll & Timeout Routes ---
|
|
|
|
router.get("/polls", (req, res) => {
|
|
res.json({ activePolls });
|
|
});
|
|
|
|
router.post("/timedout", async (req, res) => {
|
|
try {
|
|
const { userId } = req.body;
|
|
const guild = await client.guilds.fetch(process.env.GUILD_ID);
|
|
const member = await guild.members.fetch(userId);
|
|
res.status(200).json({ isTimedOut: member?.isCommunicationDisabled() || false });
|
|
} catch (e) {
|
|
res.status(404).send({ message: "Member not found or guild unavailable." });
|
|
}
|
|
});
|
|
|
|
// --- Shop & Interaction Routes ---
|
|
|
|
router.post("/change-nickname", async (req, res) => {
|
|
const { userId, nickname, commandUserId } = req.body;
|
|
const commandUser = await userService.getUser(commandUserId);
|
|
if (!commandUser) return res.status(404).json({ message: "Command user not found." });
|
|
if (commandUser.coins < 1000) return res.status(403).json({ message: "Pas assez de FlopoCoins (1000 requis)." });
|
|
|
|
try {
|
|
const guild = await client.guilds.fetch(process.env.GUILD_ID);
|
|
const member = await guild.members.fetch(userId);
|
|
const old_nickname = member.nickname;
|
|
await member.setNickname(nickname);
|
|
|
|
const newCoins = commandUser.coins - 1000;
|
|
await userService.updateUserCoins(commandUserId, newCoins);
|
|
await logService.insertLog({
|
|
id: `${commandUserId}-changenick-${Date.now()}`,
|
|
userId: commandUserId,
|
|
action: "CHANGE_NICKNAME",
|
|
targetUserId: userId,
|
|
coinsAmount: -1000,
|
|
userNewAmount: newCoins,
|
|
});
|
|
|
|
console.log(`${commandUserId} change nickname of ${userId}: ${old_nickname} -> ${nickname}`);
|
|
|
|
try {
|
|
const generalChannel = await guild.channels.fetch(process.env.GENERAL_CHANNEL_ID);
|
|
const embed = new EmbedBuilder()
|
|
.setDescription(`<@${commandUserId}> a modifié le pseudo de <@${userId}>`)
|
|
.addFields(
|
|
{ name: `${old_nickname}`, value: ``, inline: true },
|
|
{ name: `➡️`, value: ``, inline: true },
|
|
{ name: `${nickname}`, value: ``, inline: true },
|
|
)
|
|
.setColor("#5865f2")
|
|
.setTimestamp(new Date());
|
|
|
|
await generalChannel.send({ embeds: [embed] });
|
|
} catch (e) {
|
|
console.log(`[${Date.now()}]`, e);
|
|
}
|
|
|
|
res.status(200).json({
|
|
message: `Le pseudo de ${member.user.username} a été changé.`,
|
|
});
|
|
} catch (error) {
|
|
res.status(500).json({ message: `Erreur: Impossible de changer le pseudo.` });
|
|
}
|
|
});
|
|
|
|
router.post("/spam-ping", async (req, res) => {
|
|
const { userId, commandUserId } = req.body;
|
|
|
|
const user = await userService.getUser(userId);
|
|
const commandUser = await userService.getUser(commandUserId);
|
|
|
|
if (!commandUser || !user) return res.status(404).json({ message: "Oups petit soucis" });
|
|
|
|
if (commandUser.coins < 5000) return res.status(403).json({ message: "Pas assez de coins" });
|
|
|
|
try {
|
|
const discordUser = await client.users.fetch(userId);
|
|
|
|
await discordUser.send(`<@${userId}>`);
|
|
|
|
res.status(200).json({ message: "C'est parti ehehe" });
|
|
|
|
await userService.updateUserCoins(commandUserId, commandUser.coins - 5000);
|
|
await logService.insertLog({
|
|
id: commandUserId + "-" + Date.now(),
|
|
userId: commandUserId,
|
|
action: "SPAM_PING",
|
|
targetUserId: userId,
|
|
coinsAmount: -5000,
|
|
userNewAmount: commandUser.coins - 5000,
|
|
});
|
|
await emitDataUpdated({ table: "users", action: "update" });
|
|
|
|
try {
|
|
const guild = await client.guilds.fetch(process.env.GUILD_ID);
|
|
const generalChannel = await guild.channels.fetch(process.env.GENERAL_CHANNEL_ID);
|
|
const embed = new EmbedBuilder()
|
|
.setDescription(`<@${commandUserId}> a envoyé un spam ping à <@${userId}>`)
|
|
.setColor("#5865f2")
|
|
.setTimestamp(new Date());
|
|
|
|
await generalChannel.send({ embeds: [embed] });
|
|
} catch (e) {
|
|
console.log(`[${Date.now()}]`, e);
|
|
}
|
|
|
|
for (let i = 1; i < 120; i++) {
|
|
await discordUser.send(`<@${userId}>`);
|
|
await sleep(250);
|
|
}
|
|
} catch (e) {
|
|
console.log(`[${Date.now()}]`, e);
|
|
res.status(500).json({ message: "Oups ça n'a pas marché" });
|
|
}
|
|
});
|
|
|
|
// --- Slowmode Routes ---
|
|
|
|
router.get("/slowmodes", (req, res) => {
|
|
res.status(200).json({ slowmodes: activeSlowmodes });
|
|
});
|
|
|
|
router.post("/slowmode", async (req, res) => {
|
|
let { userId, commandUserId } = req.body;
|
|
|
|
const user = await userService.getUser(userId);
|
|
const commandUser = await userService.getUser(commandUserId);
|
|
|
|
if (!commandUser || !user) return res.status(404).json({ message: "Oups petit soucis" });
|
|
|
|
if (commandUser.coins < 10000) return res.status(403).json({ message: "Pas assez de coins" });
|
|
|
|
if (!user) return res.status(403).send({ message: "Oups petit problème" });
|
|
|
|
if (activeSlowmodes[userId]) {
|
|
if (userId === commandUserId) {
|
|
delete activeSlowmodes[userId];
|
|
await socketEmit("new-slowmode", { action: "new slowmode" });
|
|
|
|
await userService.updateUserCoins(commandUserId, commandUser.coins - 10000);
|
|
await logService.insertLog({
|
|
id: commandUserId + "-" + Date.now(),
|
|
userId: commandUserId,
|
|
action: "SLOWMODE",
|
|
targetUserId: userId,
|
|
coinsAmount: -10000,
|
|
userNewAmount: commandUser.coins - 10000,
|
|
});
|
|
|
|
try {
|
|
const guild = await client.guilds.fetch(process.env.GUILD_ID);
|
|
const generalChannel = await guild.channels.fetch(process.env.GENERAL_CHANNEL_ID);
|
|
const embed = new EmbedBuilder()
|
|
.setDescription(`<@${commandUserId}> a retiré son slowmode`)
|
|
.setColor("#5865f2")
|
|
.setTimestamp(new Date());
|
|
|
|
await generalChannel.send({ embeds: [embed] });
|
|
} catch (e) {
|
|
console.log(`[${Date.now()}]`, e);
|
|
}
|
|
return res.status(200).json({ message: "Slowmode retiré" });
|
|
} else {
|
|
let timeLeft = (activeSlowmodes[userId].endAt - Date.now()) / 1000;
|
|
timeLeft =
|
|
timeLeft > 60 ? (timeLeft / 60).toFixed()?.toString() + "min" : timeLeft.toFixed()?.toString() + "sec";
|
|
return res.status(403).json({
|
|
message: `${user.globalName} est déjà en slowmode (${timeLeft})`,
|
|
});
|
|
}
|
|
} else if (userId === commandUserId) {
|
|
return res.status(403).json({ message: "Impossible de te mettre toi-même en slowmode" });
|
|
}
|
|
|
|
activeSlowmodes[userId] = {
|
|
userId: userId,
|
|
endAt: Date.now() + 60 * 60 * 1000, // 1 heure
|
|
lastMessage: null,
|
|
};
|
|
await socketEmit("new-slowmode", { action: "new slowmode" });
|
|
|
|
await userService.updateUserCoins(commandUserId, commandUser.coins - 10000);
|
|
await logService.insertLog({
|
|
id: commandUserId + "-" + Date.now(),
|
|
userId: commandUserId,
|
|
action: "SLOWMODE",
|
|
targetUserId: userId,
|
|
coinsAmount: -10000,
|
|
userNewAmount: commandUser.coins - 10000,
|
|
});
|
|
await emitDataUpdated({ table: "users", action: "update" });
|
|
|
|
try {
|
|
const guild = await client.guilds.fetch(process.env.GUILD_ID);
|
|
const generalChannel = await guild.channels.fetch(process.env.GENERAL_CHANNEL_ID);
|
|
const embed = new EmbedBuilder()
|
|
.setDescription(`<@${commandUserId}> a mis <@${userId}> en slowmode pendant 1h`)
|
|
.setColor("#5865f2")
|
|
.setTimestamp(new Date());
|
|
|
|
await generalChannel.send({ embeds: [embed] });
|
|
} catch (e) {
|
|
console.log(`[${Date.now()}]`, e);
|
|
}
|
|
|
|
return res.status(200).json({
|
|
message: `${user.globalName} est maintenant en slowmode pour 1h`,
|
|
});
|
|
});
|
|
|
|
// --- Time-Out Route ---
|
|
|
|
router.post("/timeout", async (req, res) => {
|
|
let { userId, commandUserId } = req.body;
|
|
|
|
const user = await userService.getUser(userId);
|
|
const commandUser = await userService.getUser(commandUserId);
|
|
|
|
if (!commandUser || !user) return res.status(404).json({ message: "Oups petit soucis" });
|
|
|
|
if (commandUser.coins < 100000) return res.status(403).json({ message: "Pas assez de coins" });
|
|
|
|
if (!user) return res.status(403).send({ message: "Oups petit problème" });
|
|
|
|
const guild = await client.guilds.fetch(process.env.GUILD_ID);
|
|
const member = await guild.members.fetch(userId);
|
|
|
|
if (userId === commandUserId) {
|
|
if (
|
|
member &&
|
|
(!member.communicationDisabledUntilTimestamp || member.communicationDisabledUntilTimestamp < Date.now())
|
|
) {
|
|
return res.status(403).json({ message: `Impossible de t'auto time-out` });
|
|
}
|
|
await socketEmit("new-timeout", { action: "new slowmode" });
|
|
|
|
try {
|
|
const endpointTimeout = `guilds/${process.env.GUILD_ID}/members/${userId}`;
|
|
await DiscordRequest(endpointTimeout, {
|
|
method: "PATCH",
|
|
body: {
|
|
communication_disabled_until: new Date(Date.now()).toISOString(),
|
|
},
|
|
});
|
|
} catch (e) {
|
|
console.log(`[${Date.now()}]`, e);
|
|
return res.status(403).send({ message: `Impossible de time-out ${user.globalName}` });
|
|
}
|
|
|
|
await userService.updateUserCoins(commandUserId, commandUser.coins - 10000);
|
|
await logService.insertLog({
|
|
id: commandUserId + "-" + Date.now(),
|
|
userId: commandUserId,
|
|
action: "TIMEOUT",
|
|
targetUserId: userId,
|
|
coinsAmount: -10000,
|
|
userNewAmount: commandUser.coins - 10000,
|
|
});
|
|
|
|
try {
|
|
const generalChannel = await guild.channels.fetch(process.env.GENERAL_CHANNEL_ID);
|
|
const embed = new EmbedBuilder()
|
|
.setDescription(`<@${commandUserId}> a retiré son time-out`)
|
|
.setColor("#5865f2")
|
|
.setTimestamp(new Date());
|
|
|
|
await generalChannel.send({ embeds: [embed] });
|
|
} catch (e) {
|
|
console.log(`[${Date.now()}]`, e);
|
|
}
|
|
return res.status(200).json({ message: "Time-out retiré" });
|
|
}
|
|
|
|
if (
|
|
member &&
|
|
member.communicationDisabledUntilTimestamp &&
|
|
member.communicationDisabledUntilTimestamp > Date.now()
|
|
) {
|
|
return res.status(403).json({ message: `${user.globalName} est déjà time-out` });
|
|
}
|
|
|
|
try {
|
|
const timeoutUntil = new Date(Date.now() + 43200 * 1000).toISOString();
|
|
const endpointTimeout = `guilds/${process.env.GUILD_ID}/members/${userId}`;
|
|
await DiscordRequest(endpointTimeout, {
|
|
method: "PATCH",
|
|
body: { communication_disabled_until: timeoutUntil },
|
|
});
|
|
} catch (e) {
|
|
console.log(`[${Date.now()}]`, e);
|
|
return res.status(403).send({ message: `Impossible de time-out ${user.globalName}` });
|
|
}
|
|
|
|
await socketEmit("new-timeout", { action: "new timeout" });
|
|
|
|
await userService.updateUserCoins(commandUserId, commandUser.coins - 100000);
|
|
await logService.insertLog({
|
|
id: commandUserId + "-" + Date.now(),
|
|
userId: commandUserId,
|
|
action: "TIMEOUT",
|
|
targetUserId: userId,
|
|
coinsAmount: -100000,
|
|
userNewAmount: commandUser.coins - 100000,
|
|
});
|
|
await emitDataUpdated({ table: "users", action: "update" });
|
|
|
|
try {
|
|
const generalChannel = await guild.channels.fetch(process.env.GENERAL_CHANNEL_ID);
|
|
const embed = new EmbedBuilder()
|
|
.setDescription(`<@${commandUserId}> a time-out <@${userId}> pour 12h`)
|
|
.setColor("#5865f2")
|
|
.setTimestamp(new Date());
|
|
|
|
await generalChannel.send({ embeds: [embed] });
|
|
} catch (e) {
|
|
console.log(`[${Date.now()}]`, e);
|
|
}
|
|
|
|
return res.status(200).json({ message: `${user.globalName} est maintenant time-out pour 12h` });
|
|
});
|
|
|
|
// --- Prediction Routes ---
|
|
|
|
router.get("/predis", (req, res) => {
|
|
const reversedPredis = Object.fromEntries(Object.entries(activePredis).reverse());
|
|
res.status(200).json({ predis: reversedPredis });
|
|
});
|
|
|
|
router.post("/start-predi", async (req, res) => {
|
|
let { commandUserId, label, options, closingTime, payoutTime } = req.body;
|
|
|
|
const commandUser = await userService.getUser(commandUserId);
|
|
|
|
if (!commandUser) return res.status(403).send({ message: "Oups petit problème" });
|
|
if (commandUser.coins < 100) return res.status(403).send({ message: "Tu n'as pas assez de FlopoCoins" });
|
|
|
|
if (Object.values(activePredis).find((p) => p.creatorId === commandUserId && p.endTime > Date.now() && !p.closed)) {
|
|
return res.status(403).json({
|
|
message: `Tu ne peux pas lancer plus d'une prédi à la fois !`,
|
|
});
|
|
}
|
|
|
|
const startTime = Date.now();
|
|
const newPrediId = commandUserId?.toString() + "-" + startTime?.toString();
|
|
|
|
let msgId;
|
|
try {
|
|
const guild = await client.guilds.fetch(process.env.GUILD_ID);
|
|
const generalChannel = await guild.channels.fetch(process.env.GENERAL_CHANNEL_ID);
|
|
const embed = new EmbedBuilder()
|
|
.setTitle(`Prédiction de ${commandUser.username}`)
|
|
.setDescription(`**${label}**`)
|
|
.addFields(
|
|
{ name: `${options[0]}`, value: ``, inline: true },
|
|
{ name: ``, value: `ou`, inline: true },
|
|
{ name: `${options[1]}`, value: ``, inline: true },
|
|
)
|
|
.setFooter({
|
|
text: `${formatTime(closingTime).replaceAll("*", "")} pour voter`,
|
|
})
|
|
.setColor("#5865f2")
|
|
.setTimestamp(new Date());
|
|
|
|
const row = new ActionRowBuilder().addComponents(
|
|
new ButtonBuilder()
|
|
.setCustomId(`option_0_${newPrediId}`)
|
|
.setLabel(`+10 sur '${options[0]}'`)
|
|
.setStyle(ButtonStyle.Primary),
|
|
new ButtonBuilder()
|
|
.setCustomId(`option_1_${newPrediId}`)
|
|
.setLabel(`+10 sur '${options[1]}'`)
|
|
.setStyle(ButtonStyle.Primary),
|
|
);
|
|
|
|
const row2 = new ActionRowBuilder().addComponents(
|
|
new ButtonBuilder()
|
|
.setLabel("Voter sur FlopoSite")
|
|
.setURL(`${process.env.DEV_SITE === "true" ? process.env.FLAPI_URL_DEV : process.env.FLAPI_URL}/dashboard`)
|
|
.setStyle(ButtonStyle.Link),
|
|
);
|
|
|
|
const msg = await generalChannel.send({
|
|
embeds: [embed],
|
|
components: [/*row,*/ row2],
|
|
});
|
|
msgId = msg.id;
|
|
} catch (e) {
|
|
console.log(`[${Date.now()}]`, e);
|
|
return res.status(500).send({ message: "Erreur lors de l'envoi du message" });
|
|
}
|
|
|
|
const formattedOptions = [
|
|
{ label: options[0], votes: [], total: 0, percent: 0 },
|
|
{ label: options[1], votes: [], total: 0, percent: 0 },
|
|
];
|
|
activePredis[newPrediId] = {
|
|
creatorId: commandUserId,
|
|
label: label,
|
|
options: formattedOptions,
|
|
startTime: startTime,
|
|
closingTime: startTime + closingTime * 1000,
|
|
endTime: startTime + closingTime * 1000 + payoutTime * 1000,
|
|
closed: false,
|
|
winning: null,
|
|
cancelledTime: null,
|
|
paidTime: null,
|
|
msgId: msgId,
|
|
};
|
|
await socketEmit("new-predi", { action: "new predi" });
|
|
|
|
await userService.updateUserCoins(commandUserId, commandUser.coins - 100);
|
|
await logService.insertLog({
|
|
id: commandUserId + "-" + Date.now(),
|
|
userId: commandUserId,
|
|
action: "START_PREDI",
|
|
targetUserId: null,
|
|
coinsAmount: -100,
|
|
userNewAmount: commandUser.coins - 100,
|
|
});
|
|
await emitDataUpdated({ table: "users", action: "update" });
|
|
|
|
return res.status(200).json({ message: `Ta prédi '${label}' a commencée !` });
|
|
});
|
|
|
|
router.post("/vote-predi", async (req, res) => {
|
|
const { commandUserId, predi, amount, option } = req.body;
|
|
|
|
let warning = false;
|
|
|
|
let intAmount = parseInt(amount);
|
|
if (intAmount < 10 || intAmount > 250000) return res.status(403).send({ message: "Montant invalide" });
|
|
|
|
const commandUser = await userService.getUser(commandUserId);
|
|
if (!commandUser) return res.status(403).send({ message: "Oups, je ne te connais pas" });
|
|
if (commandUser.coins < intAmount) return res.status(403).send({ message: "Tu n'as pas assez de FlopoCoins" });
|
|
|
|
const prediObject = activePredis[predi];
|
|
if (!prediObject) return res.status(403).send({ message: "Prédiction introuvable" });
|
|
|
|
if (prediObject.endTime < Date.now())
|
|
return res.status(403).send({ message: "Les votes de cette prédiction sont clos" });
|
|
|
|
const otherOption = option === 0 ? 1 : 0;
|
|
if (
|
|
prediObject.options[otherOption].votes.find((v) => v.id === commandUserId) &&
|
|
commandUserId !== process.env.DEV_ID
|
|
)
|
|
return res.status(403).send({ message: "Tu ne peux pas voter pour les 2 deux options" });
|
|
|
|
if (prediObject.options[option].votes.find((v) => v.id === commandUserId)) {
|
|
activePredis[predi].options[option].votes.forEach((v) => {
|
|
if (v.id === commandUserId) {
|
|
if (v.amount === 250000) {
|
|
return res.status(403).send({ message: "Tu as déjà parié le max (250K)" });
|
|
}
|
|
if (v.amount + intAmount > 250000) {
|
|
intAmount = 250000 - v.amount;
|
|
warning = true;
|
|
}
|
|
v.amount += intAmount;
|
|
}
|
|
});
|
|
} else {
|
|
activePredis[predi].options[option].votes.push({
|
|
id: commandUserId,
|
|
amount: intAmount,
|
|
});
|
|
}
|
|
activePredis[predi].options[option].total += intAmount;
|
|
|
|
activePredis[predi].options[option].percent =
|
|
(activePredis[predi].options[option].total /
|
|
(activePredis[predi].options[otherOption].total + activePredis[predi].options[option].total)) *
|
|
100;
|
|
activePredis[predi].options[otherOption].percent = 100 - activePredis[predi].options[option].percent;
|
|
|
|
await socketEmit("new-predi", { action: "new vote" });
|
|
|
|
await userService.updateUserCoins(commandUserId, commandUser.coins - intAmount);
|
|
await logService.insertLog({
|
|
id: commandUserId + "-" + Date.now(),
|
|
userId: commandUserId,
|
|
action: "PREDI_VOTE",
|
|
targetUserId: null,
|
|
coinsAmount: -intAmount,
|
|
userNewAmount: commandUser.coins - intAmount,
|
|
});
|
|
await emitDataUpdated({ table: "users", action: "update" });
|
|
|
|
return res.status(200).send({ message: `Vote enregistré!` });
|
|
});
|
|
|
|
router.post("/end-predi", async (req, res) => {
|
|
const { commandUserId, predi, confirm, winningOption } = req.body;
|
|
|
|
const commandUser = await userService.getUser(commandUserId);
|
|
if (!commandUser) return res.status(403).send({ message: "Oups, je ne te connais pas" });
|
|
if (commandUserId !== process.env.DEV_ID)
|
|
return res.status(403).send({ message: "Tu n'as pas les permissions requises" });
|
|
|
|
const prediObject = activePredis[predi];
|
|
if (!prediObject) return res.status(403).send({ message: "Prédiction introuvable" });
|
|
if (prediObject.closed) return res.status(403).send({ message: "Prédiction déjà close" });
|
|
|
|
if (!confirm) {
|
|
activePredis[predi].cancelledTime = new Date();
|
|
for (const v of activePredis[predi].options[0].votes) {
|
|
const tempUser = await userService.getUser(v.id);
|
|
try {
|
|
await userService.updateUserCoins(v.id, tempUser.coins + v.amount);
|
|
await logService.insertLog({
|
|
id: v.id + "-" + Date.now(),
|
|
userId: v.id,
|
|
action: "PREDI_REFUND",
|
|
targetUserId: v.id,
|
|
coinsAmount: v.amount,
|
|
userNewAmount: tempUser.coins + v.amount,
|
|
});
|
|
} catch (e) {
|
|
console.log(`Impossible de rembourser ${v.id} (${v.amount} coins)`);
|
|
}
|
|
}
|
|
for (const v of activePredis[predi].options[1].votes) {
|
|
const tempUser = await userService.getUser(v.id);
|
|
try {
|
|
await userService.updateUserCoins(v.id, tempUser.coins + v.amount);
|
|
await logService.insertLog({
|
|
id: v.id + "-" + Date.now(),
|
|
userId: v.id,
|
|
action: "PREDI_REFUND",
|
|
targetUserId: v.id,
|
|
coinsAmount: v.amount,
|
|
userNewAmount: tempUser.coins + v.amount,
|
|
});
|
|
} catch (e) {
|
|
console.log(`Impossible de rembourser ${v.id} (${v.amount} coins)`);
|
|
}
|
|
}
|
|
activePredis[predi].closed = true;
|
|
} else {
|
|
const losingOption = winningOption === 0 ? 1 : 0;
|
|
for (const v of activePredis[predi].options[winningOption].votes) {
|
|
const tempUser = await userService.getUser(v.id);
|
|
const ratio =
|
|
activePredis[predi].options[winningOption].total === 0
|
|
? 0
|
|
: activePredis[predi].options[losingOption].total / activePredis[predi].options[winningOption].total;
|
|
try {
|
|
await userService.updateUserCoins(v.id, tempUser.coins + v.amount * (1 + ratio));
|
|
await logService.insertLog({
|
|
id: v.id + "-" + Date.now(),
|
|
userId: v.id,
|
|
action: "PREDI_RESULT",
|
|
targetUserId: v.id,
|
|
coinsAmount: v.amount * (1 + ratio),
|
|
userNewAmount: tempUser.coins + v.amount * (1 + ratio),
|
|
});
|
|
} catch (e) {
|
|
console.log(`Impossible de créditer ${v.id} (${v.amount} coins pariés, *${1 + ratio})`);
|
|
}
|
|
}
|
|
activePredis[predi].paidTime = new Date();
|
|
activePredis[predi].closed = true;
|
|
activePredis[predi].winning = winningOption;
|
|
}
|
|
|
|
try {
|
|
const guild = await client.guilds.fetch(process.env.GUILD_ID);
|
|
const generalChannel = await guild.channels.fetch(process.env.GENERAL_CHANNEL_ID);
|
|
const message = await generalChannel.messages.fetch(activePredis[predi].msgId);
|
|
const updatedEmbed = new EmbedBuilder()
|
|
.setTitle(`Prédiction de ${commandUser.username}`)
|
|
.setDescription(`**${activePredis[predi].label}**`)
|
|
.setFields(
|
|
{
|
|
name: `${activePredis[predi].options[0].label}`,
|
|
value: ``,
|
|
inline: true,
|
|
},
|
|
{ name: ``, value: `ou`, inline: true },
|
|
{
|
|
name: `${activePredis[predi].options[1].label}`,
|
|
value: ``,
|
|
inline: true,
|
|
},
|
|
)
|
|
.setFooter({
|
|
text: `${activePredis[predi].cancelledTime !== null ? "Prédi annulée" : "Prédi confirmée !"}`,
|
|
})
|
|
.setTimestamp(new Date());
|
|
const row = new ActionRowBuilder().addComponents(
|
|
new ButtonBuilder()
|
|
.setLabel("Voir")
|
|
.setURL(`${process.env.DEV_SITE === "true" ? process.env.FLAPI_URL_DEV : process.env.FLAPI_URL}/dashboard`)
|
|
.setStyle(ButtonStyle.Link),
|
|
);
|
|
await message.edit({ embeds: [updatedEmbed], components: [row] });
|
|
} catch (err) {
|
|
console.error("Error updating prédi message:", err);
|
|
}
|
|
|
|
await socketEmit("new-predi", { action: "closed predi" });
|
|
await emitDataUpdated({ table: "users", action: "fin predi" });
|
|
|
|
return res.status(200).json({ message: "Prédi close" });
|
|
});
|
|
|
|
router.post("/snake/reward", async (req, res) => {
|
|
const { discordId, score, isWin } = req.body;
|
|
console.log(`[SNAKE][SOLO]${discordId}: score=${score}, isWin=${isWin}`);
|
|
try {
|
|
const user = await userService.getUser(discordId);
|
|
if (!user) return res.status(404).json({ message: "Utilisateur introuvable" });
|
|
const reward = isWin ? score * 2 : score;
|
|
const newCoins = user.coins + reward;
|
|
await userService.updateUserCoins(discordId, newCoins);
|
|
await logService.insertLog({
|
|
id: `${discordId}-snake-reward-${Date.now()}`,
|
|
userId: discordId,
|
|
action: "SNAKE_GAME_REWARD",
|
|
coinsAmount: reward,
|
|
userNewAmount: newCoins,
|
|
targetUserId: null,
|
|
});
|
|
await emitDataUpdated({ table: "users", action: "update" });
|
|
return res.status(200).json({ message: `Récompense de ${reward} FlopoCoins attribuée !` });
|
|
} catch (e) {
|
|
console.error("Error rewarding snake game:", e);
|
|
return res.status(500).json({ message: "Erreur lors de l'attribution de la récompense" });
|
|
}
|
|
});
|
|
|
|
router.post("/queue/leave", async (req, res) => {
|
|
const { discordId, game, reason } = req.body;
|
|
if (game === "snake" && (reason === "beforeunload" || reason === "route-leave")) {
|
|
const lobby = Object.values(activeSnakeGames).find(
|
|
(l) => (l.p1.id === discordId || l.p2.id === discordId) && !l.gameOver,
|
|
);
|
|
if (!lobby) return;
|
|
|
|
const player = lobby.p1.id === discordId ? lobby.p1 : lobby.p2;
|
|
const otherPlayer = lobby.p1.id === discordId ? lobby.p2 : lobby.p1;
|
|
if (player.gameOver === true) return res.status(200).json({ message: "Déjà quitté" });
|
|
player.gameOver = true;
|
|
otherPlayer.win = true;
|
|
|
|
lobby.lastmove = Date.now();
|
|
|
|
// Broadcast the updated state to both players
|
|
await socketEmit("snakegamestate", {
|
|
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)
|
|
await onGameOver(client, "snake", discordId, 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;
|
|
await onGameOver(client, "snake", discordId, winnerId, "", { p1: lobby.p1.score, p2: lobby.p2.score });
|
|
}
|
|
}
|
|
});
|
|
|
|
// Fixed coin offers - server-side source of truth
|
|
const COIN_OFFERS = [
|
|
{ id: "offer_5000", coins: 5000, amount_cents: 99, label: "5 000 FlopoCoins" },
|
|
{ id: "offer_20000", coins: 20000, amount_cents: 299, label: "20 000 FlopoCoins" },
|
|
{ id: "offer_40000", coins: 40000, amount_cents: 499, label: "40 000 FlopoCoins" },
|
|
{ id: "offer_100000", coins: 100000, amount_cents: 999, label: "100 000 FlopoCoins" },
|
|
];
|
|
|
|
router.get("/coin-offers", (req, res) => {
|
|
res.json({ offers: COIN_OFFERS });
|
|
});
|
|
|
|
router.post("/create-checkout-session", async (req, res) => {
|
|
const { userId, offerId } = req.body;
|
|
|
|
if (!userId || !offerId) {
|
|
return res.status(400).json({ error: "Missing required fields: userId, offerId" });
|
|
}
|
|
|
|
const offer = COIN_OFFERS.find((o) => o.id === offerId);
|
|
if (!offer) {
|
|
return res.status(400).json({ error: "Invalid offer" });
|
|
}
|
|
|
|
const user = await userService.getUser(userId);
|
|
if (!user) {
|
|
return res.status(404).json({ error: "User not found" });
|
|
}
|
|
|
|
try {
|
|
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY);
|
|
const FLAPI_URL = process.env.DEV_SITE === "true" ? process.env.FLAPI_URL_DEV : process.env.FLAPI_URL;
|
|
|
|
const session = await stripe.checkout.sessions.create({
|
|
payment_method_types: ["card"],
|
|
line_items: [
|
|
{
|
|
price_data: {
|
|
currency: "eur",
|
|
product_data: {
|
|
name: offer.label,
|
|
description: `Achat de ${offer.label} pour FlopoBot`,
|
|
},
|
|
unit_amount: offer.amount_cents,
|
|
},
|
|
quantity: 1,
|
|
},
|
|
],
|
|
mode: "payment",
|
|
success_url: `${FLAPI_URL}/payment-success?session_id={CHECKOUT_SESSION_ID}`,
|
|
cancel_url: `${FLAPI_URL}/dashboard`,
|
|
metadata: {
|
|
userId: userId,
|
|
coins: offer.coins.toString(),
|
|
},
|
|
});
|
|
|
|
console.log(
|
|
`[CHECKOUT] New session for user ${userId}: ${session.id}, offer: ${offer.id} (${offer.coins} coins for ${offer.amount_cents} cents)`,
|
|
);
|
|
|
|
res.json({ sessionId: session.id, url: session.url });
|
|
} catch (error) {
|
|
console.error("Error creating checkout session:", error);
|
|
res.status(500).json({ error: "Failed to create checkout session" });
|
|
}
|
|
});
|
|
|
|
router.post("/buy-coins", async (req, res) => {
|
|
const sig = req.headers["stripe-signature"];
|
|
const endpointSecret = process.env.STRIPE_WEBHOOK_SECRET;
|
|
|
|
if (!endpointSecret) {
|
|
console.error("STRIPE_WEBHOOK_SECRET not configured");
|
|
return res.status(500).json({ error: "Webhook not configured" });
|
|
}
|
|
|
|
let event;
|
|
|
|
try {
|
|
// Verify webhook signature - requires raw body
|
|
// Note: You need to configure Express to preserve raw body for this route
|
|
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY);
|
|
event = stripe.webhooks.constructEvent(req.body, sig, endpointSecret);
|
|
} catch (err) {
|
|
console.error(`Webhook signature verification failed: ${err.message}`);
|
|
return res.status(400).json({ error: `Webhook Error: ${err.message}` });
|
|
}
|
|
|
|
// Handle the event
|
|
if (event.type === "checkout.session.completed") {
|
|
const session = event.data.object;
|
|
|
|
// Extract metadata from the checkout session
|
|
const commandUserId = session.metadata?.userId;
|
|
const expectedCoins = parseInt(session.metadata?.coins);
|
|
const amountPaid = session.amount_total; // in cents
|
|
const currency = session.currency;
|
|
const customerEmail = session.customer_details?.email;
|
|
const customerName = session.customer_details?.name;
|
|
|
|
// Validate metadata exists
|
|
if (!commandUserId || !expectedCoins) {
|
|
console.error("Missing userId or coins in session metadata");
|
|
return res.status(400).json({ error: "Invalid session metadata" });
|
|
}
|
|
|
|
// Verify payment was successful
|
|
if (session.payment_status !== "paid") {
|
|
console.error(`Payment not completed for session ${session.id}`);
|
|
return res.status(400).json({ error: "Payment not completed" });
|
|
}
|
|
|
|
// Check for duplicate processing (idempotency)
|
|
const existingTransaction = await transactionService.getTransactionBySessionId(session.id);
|
|
if (existingTransaction) {
|
|
console.log(`Payment already processed: ${session.id}`);
|
|
return res.status(200).json({ message: "Already processed" });
|
|
}
|
|
|
|
// Get user
|
|
const user = await userService.getUser(commandUserId);
|
|
if (!user) {
|
|
console.error(`User not found: ${commandUserId}`);
|
|
return res.status(404).json({ error: "User not found" });
|
|
}
|
|
|
|
// Update coins
|
|
const newCoins = user.coins + expectedCoins;
|
|
await userService.updateUserCoins(commandUserId, newCoins);
|
|
|
|
// Insert transaction record
|
|
const transactionId = `${commandUserId}-transaction-${Date.now()}`;
|
|
await transactionService.insertTransaction({
|
|
id: transactionId,
|
|
sessionId: session.id,
|
|
userId: commandUserId,
|
|
coinsAmount: expectedCoins,
|
|
amountCents: amountPaid,
|
|
currency: currency,
|
|
customerEmail: customerEmail,
|
|
customerName: customerName,
|
|
paymentStatus: session.payment_status,
|
|
});
|
|
|
|
// Insert log entry
|
|
await logService.insertLog({
|
|
id: `${commandUserId}-buycoins-${Date.now()}`,
|
|
userId: commandUserId,
|
|
action: "BUY_COINS",
|
|
targetUserId: null,
|
|
coinsAmount: expectedCoins,
|
|
userNewAmount: newCoins,
|
|
});
|
|
|
|
console.log(
|
|
`Payment processed: ${commandUserId} purchased ${expectedCoins} coins for ${amountPaid / 100} ${currency}`,
|
|
);
|
|
|
|
// Notify user via Discord if possible
|
|
try {
|
|
const discordUser = await client.users.fetch(commandUserId);
|
|
await discordUser.send(
|
|
`✅ Votre achat de ${expectedCoins} FlopoCoins a été confirmé ! Merci pour votre soutien !`,
|
|
);
|
|
} catch (e) {
|
|
console.log(`Could not DM user ${commandUserId}:`, e.message);
|
|
}
|
|
|
|
return res.status(200).json({ message: `Added ${expectedCoins} coins.` });
|
|
}
|
|
|
|
// Return 200 for unhandled event types (Stripe requires this)
|
|
res.status(200).json({ received: true });
|
|
});
|
|
|
|
return router;
|
|
}
|