Files
flopobot_v2/index.js
2025-06-07 18:04:40 +02:00

3749 lines
129 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import 'dotenv/config';
import express from 'express';
import {
ButtonStyleTypes,
InteractionResponseFlags,
InteractionResponseType,
InteractionType,
MessageComponentTypes,
verifyKeyMiddleware,
} from 'discord-interactions';
import {
getRandomEmoji,
DiscordRequest,
//getOnlineUsersWithRole,
formatTime,
gork,
getRandomHydrateText,
getAPOUsers,
postAPOBuy
} from './utils.js';
import {channelPointsHandler, eloHandler, pokerTest, slowmodesHandler} from './game.js';
import { Client, GatewayIntentBits, EmbedBuilder, ActionRowBuilder, ButtonBuilder, ButtonStyle } from 'discord.js';
import cron from 'node-cron';
import Database from "better-sqlite3";
import {
flopoDB,
insertUser,
insertManyUsers,
updateUser,
updateManyUsers,
getUser,
getAllUsers,
stmtUsers,
stmtSkins,
updateManySkins,
insertSkin,
updateSkin,
insertManySkins,
getAllSkins,
getSkin,
getAllAvailableSkins,
getUserInventory,
getTopSkins, updateUserCoins,
insertLog, stmtLogs,
getLogs, getUserLogs, getUserElo, getUserGames, getUsersByElo,
} from './init_database.js';
import { getValorantSkins, getSkinTiers } from './valo.js';
import {sleep} from "openai/core";
import { v4 as uuidv4 } from 'uuid';
import { uniqueNamesGenerator, adjectives, languages, animals } from 'unique-names-generator';
// Create an express app
const app = express();
// Get port, or default to 25578
const PORT = process.env.PORT || 25578;
const FLAPI_URL = process.env.DEV_SITE === 'true' ? process.env.FLAPI_URL_DEV : process.env.FLAPI_URL
app.use(express.json());
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');
next();
});
// To keep track of our active games
const activeGames = {};
const activePolls = {};
const activeInventories = {};
const activeSearchs = {};
const activeSlowmodes = {};
const activePredis = {};
let todaysHydrateCron = ''
const SPAM_INTERVAL = process.env.SPAM_INTERVAL
const client = new Client({
intents: [
GatewayIntentBits.Guilds, // For guild events
GatewayIntentBits.GuildMessages, // For messages in guilds
GatewayIntentBits.MessageContent, // For reading message content (privileged intent)
GatewayIntentBits.GuildMembers,
GatewayIntentBits.GuildPresences,
]
});
const requestTimestamps = new Map(); // userId => [timestamp1, timestamp2, ...]
const MAX_REQUESTS_PER_INTERVAL = parseInt(process.env.MAX_REQUESTS || "5");
const akhysData= new Map()
const skins = []
async function getAkhys() {
try {
stmtUsers.run();
const guild = await client.guilds.fetch(process.env.GUILD_ID);
const members = await guild.members.fetch(); // Fetch all members
const akhys = members.filter(m => !m.user.bot && m.roles.cache.has(process.env.VOTING_ROLE_ID));
akhys.forEach(akhy => {
akhysData.set(akhy.user.id, {
id: akhy.user.id,
username: akhy.user.username,
globalName: akhy.user.globalName,
warned: false,
warns: 0,
allTimeWarns: 0,
totalRequests: 0,
});
insertManyUsers([
{
id: akhy.user.id,
username: akhy.user.username,
globalName: akhy.user.globalName,
warned: 0,
warns: 0,
allTimeWarns: 0,
totalRequests: 0
},
]);
});
} catch (err) {
console.error('Error while counting akhys:', err);
}
try {
stmtSkins.run();
const fetchedSkins = await getValorantSkins()
const fetchedTiers = await getSkinTiers()
fetchedSkins.forEach((skin) => {
const chromas = []
const levels = []
skin.chromas.forEach((chroma) => {
chromas.push({
uuid: chroma.uuid,
displayName: chroma.displayName,
displayIcon: chroma.displayIcon,
fullRender: chroma.fullRender,
swatch: chroma.swatch,
streamedVideo: chroma.streamedVideo,
})
})
skin.levels.forEach((level) => {
levels.push({
uuid: level.uuid,
displayName: level.displayName,
displayIcon: level.displayIcon,
streamedVideo: level.streamedVideo,
})
})
skins.push({
uuid: skin.uuid,
displayName: skin.displayName,
contentTierUuid: skin.contentTierUuid,
displayIcon: skin.displayIcon,
chromas: chromas,
levels: levels,
})
})
let newSkinCount = 0;
let newSkinText = '';
for (const skin of skins) {
try {
if (skin.contentTierUuid !== null) {
const tierRank = () => {
const tier = fetchedTiers.filter((tier) => { return tier.uuid === skin.contentTierUuid})[0]
const rank = tier ? tier['rank'] : null;
return rank ? rank + 1 : 0;
}
const tierColor = () => {
const tier = fetchedTiers.filter((tier) => { return tier.uuid === skin.contentTierUuid})[0]
return tier ? tier['highlightColor']?.slice(0, 6) : 'F2F3F3'
}
const tierText = () => {
const tier = fetchedTiers.filter((tier) => { return tier.uuid === skin.contentTierUuid})[0]
const rank = tier ? tier['rank'] : null;
let res;
if (rank === null) return 'Pas de tier';
switch(rank) {
case 0:
res = '**<:select:1362964319498670222> Select**'
break
case 1:
res = '**<:deluxe:1362964308094488797> Deluxe**'
break
case 2:
res = '**<:premium:1362964330349330703> Premium**'
break
case 3:
res = '**<:exclusive:1362964427556651098> Exclusive**'
break
case 4:
res = '**<:ultra:1362964339685986314> Ultra**'
break
default:
return 'Pas de tier'
}
res += skin.displayName.includes('VCT') ? ' | Esports Edition' : ''
res += skin.displayName.toLowerCase().includes('champions') ? ' | Champions' : ''
res += skin.displayName.toLowerCase().includes('arcane') ? ' | Arcane' : ''
return res
}
const basePrice = () => {
let res;
if (skin.displayName.toLowerCase().includes('classic')){
res = 150;
} else if (skin.displayName.toLowerCase().includes('shorty')) {
res = 300;
} else if (skin.displayName.toLowerCase().includes('frenzy')) {
res = 450;
} else if (skin.displayName.toLowerCase().includes('ghost')) {
res = 500;
} else if (skin.displayName.toLowerCase().includes('sheriff')) {
res = 800;
} else if (skin.displayName.toLowerCase().includes('stinger')) {
res = 1100;
} else if (skin.displayName.toLowerCase().includes('spectre')) {
res = 1600;
} else if (skin.displayName.toLowerCase().includes('bucky')) {
res = 850;
} else if (skin.displayName.toLowerCase().includes('judge')) {
res = 1850;
} else if (skin.displayName.toLowerCase().includes('bulldog')) {
res = 2050;
} else if (skin.displayName.toLowerCase().includes('guardian')) {
res = 2250;
} else if (skin.displayName.toLowerCase().includes('phantom')) {
res = 2900;
} else if (skin.displayName.toLowerCase().includes('vandal')) {
res = 2900;
} else if (skin.displayName.toLowerCase().includes('marshal')) {
res = 950;
} else if (skin.displayName.toLowerCase().includes('outlaw')) {
res = 2400;
} else if (skin.displayName.toLowerCase().includes('operator')) {
res = 4700;
} else if (skin.displayName.toLowerCase().includes('ares')) {
res = 1600;
} else if (skin.displayName.toLowerCase().includes('odin')) {
res = 3200;
} else {
res = 6000;
}
res *= (1 + (tierRank()))
res *= skin.displayName.includes('VCT') ? 1.25 : 1;
res *= skin.displayName.toLowerCase().includes('champions') ? 2 : 1;
res *= skin.displayName.toLowerCase().includes('arcane') ? 1.5 : 1;
res *= 1+(Math.random()/100) // [1 to 1.01]
return (res/1111).toFixed(2);
}
const skinBasePrice = basePrice();
const maxPrice = (price) => {
let res = price
res *= (1 + (skin.levels.length / Math.max(skin.levels.length, 2)))
res *= (1 + (skin.chromas.length / 4))
return res.toFixed(2);
}
await insertSkin.run(
{
uuid: skin.uuid,
displayName: skin.displayName,
contentTierUuid: skin.contentTierUuid,
displayIcon: skin.displayIcon,
user_id: null,
tierRank: tierRank(),
tierColor: tierColor(),
tierText: tierText(),
basePrice: skinBasePrice,
currentLvl: null,
currentChroma: null,
currentPrice: null,
maxPrice: maxPrice(skinBasePrice),
});
newSkinCount++;
newSkinText += skin.displayName + ' | ';
}
} catch (e) {
//
}
}
console.log(`New skins : ${newSkinCount}`);
if (newSkinCount <= 30 && newSkinCount > 0) console.log(newSkinText);
} catch (e) {
console.error('Error while fetching skins:', e);
}
try {
stmtLogs.run()
} catch (e) {
console.log('Logs table init error')
}
}
async function getOnlineUsersWithRole(guild_id=process.env.GUILD_ID, role_id=process.env.VOTING_ROLE_ID) {
try {
const guild = await client.guilds.fetch(guild_id);
const members = await guild.members.fetch(); // Fetch all members
const online = members.filter(m => !m.user.bot && m.presence?.status && m.roles.cache.has(role_id));
return online
} catch (err) {
console.error('Error while counting online members:', err);
}
}
// Login to Discord using bot token (optional)
client.login(process.env.BOT_TOKEN);
// Listen for message events
client.on('messageCreate', async (message) => {
// Ignore messages from bots to avoid feedback loops
if (message.author.bot) return;
// hihihiha
if (message.author.id === process.env.PATA_ID) {
if (message.content.startsWith('feur')
|| message.content.startsWith('rati')) {
await sleep(1000)
await message.delete()
}
}
// coins mechanic and slowmodes check
if (message.guildId === process.env.GUILD_ID) {
channelPointsHandler(message)
io.emit('data-updated', { table: 'users', action: 'update' });
const deletedSlowmode = await slowmodesHandler(message, activeSlowmodes)
if (deletedSlowmode) io.emit('new-slowmode', { action: 'deleted slowmode' });
}
if (message.content.toLowerCase().startsWith(`<@${process.env.APP_ID}>`) || message.mentions.repliedUser?.id === process.env.APP_ID) {
let startTime = Date.now()
let akhyAuthor = await getUser.get(message.author.id)
const now = Date.now();
const timestamps = requestTimestamps.get(message.author.id) || [];
// Remove timestamps older than SPAM_INTERVAL seconds
const updatedTimestamps = timestamps.filter(ts => now - ts < SPAM_INTERVAL);
if (updatedTimestamps.length >= MAX_REQUESTS_PER_INTERVAL) {
console.log(akhyAuthor.warned ? `${message.author.username} is restricted : ${updatedTimestamps}` : `Rate limit exceeded for ${message.author.username}`);
if (!akhyAuthor.warned) {
await message.reply(`T'abuses fréro, attends un peu ⏳`)
} else if (akhyAuthor.warns === Math.max(1, process.env.MAX_WARNS - 3)) {
await message.author.send("Attention si tu continues de spam tu vas te faire timeout 🤯")
}
await updateManyUsers([
{
id: akhyAuthor.id,
username: akhyAuthor.username,
globalName: akhyAuthor.globalName,
warned: 1, // true
warns: akhyAuthor.warns + 1,
allTimeWarns: akhyAuthor.allTimeWarns + 1,
totalRequests: akhyAuthor.totalRequests
},
])
akhyAuthor = await getUser.get(akhyAuthor.id)
if (akhyAuthor.warns > process.env.MAX_WARNS ?? 10) {
const guild = await client.guilds.fetch(process.env.GUILD_ID);
const time = parseInt(process.env.SPAM_TIMEOUT_TIME)
try {
await guild.members.edit(akhyAuthor.id, {
communication_disabled_until: new Date(Date.now() + time).toISOString(),
reason: 'Dose le spam fdp',
});
} catch (e) {
console.log('Tried timeout for AI spam : ', e)
message.channel.send(`<@${akhyAuthor.id}> tu me fais chier !! T'as de la chance que je puisse pas te timeout 🔪`)
.catch(console.error);
return
}
message.channel.send(`Ce bouffon de <@${akhyAuthor.id}> a été timeout pendant ${formatTime(time/1000)}, il me cassait les couilles 🤫`)
.catch(console.error);
return
}
return;
}
// Track this new usage
updatedTimestamps.push(now);
requestTimestamps.set(akhyAuthor.id, updatedTimestamps);
await updateManyUsers([
{
id: akhyAuthor.id,
username: akhyAuthor.username,
globalName: akhyAuthor.globalName,
warned: 0, // false
warns: 0, // reset
allTimeWarns: akhyAuthor.allTimeWarns,
totalRequests: akhyAuthor.totalRequests + 1
},
])
akhyAuthor = await getUser.get(akhyAuthor.id)
try {
// Fetch last messages from the channel
const fetched = await message.channel.messages.fetch({ limit: 100 });
const messagesArray = Array.from(fetched.values()).reverse(); // oldest to newest
const requestMessage = message.content.replace(`<@${process.env.APP_ID}>`, '')
// Map to OpenAI/Gemini format
console.log('AI fetch', process.env.MODEL)
const allAkhys = await getAllUsers.all()
let allAkhysText = ''
allAkhys.forEach(akhy => {
allAkhysText += `<@${akhy.id}> alias ${akhy.globalName}, `
})
let convo = 'Voici les derniers messages de la conversation pour contexte (du plus vieux au plus récent) :\n'
messagesArray.forEach(msg => {
convo += `<@${msg.author.id}> a dit : ${msg.content}.\n`
})
let formatted = [];
if (process.env.MODEL === 'OpenAI' || process.env.MODEL === 'Gemini') {
formatted.push({
role: 'developer',
content: `${convo}`,
});
formatted.push({
role: 'developer',
content: `Voici la liste des différents utilisateurs présents : ${allAkhysText}`,
})
formatted.push({
role: 'developer',
content: `Voici une liste de quelques emojis que tu peux utiliser sur le serveur: <:CAUGHT:1323810730155446322> quand tu te fais prendre la main dans le sac ou que tu a un avis divergent ou risqué, <:hinhinhin:1072510144933531758> pour le rire ou quand tu es moqueur, <:o7:1290773422451986533> pour payer respect ou remercier ou dire au revoir, <:zhok:1115221772623683686> pour quand quelquechose manque de sens, <:nice:1154049521110765759> pour quelquechose de bien, <:nerd:1087658195603951666> pour une explication technique ou une attitude nerd, <:peepSelfie:1072508131839594597> pour à peu près n\'importe quelle situation quand tu es blazé`
})
formatted.push(
{
role: "developer",
content: "Adopte une attitude détendue et répond comme si tu participais à la conversation, pas trop long, pas de retour à la ligne, simple et utilise les emojis du serveur. N'hésites pas à utiliser des abréviations mais sans en abuser."
},
{
role: 'developer',
content: message.mentions.repliedUser?.id ? `La phrase de l'utilisateur répond à un message de ${message.mentions.repliedUser?.id === process.env.APP_ID ? 'toi-même' : message.mentions.repliedUser?.id}` : '',
},
{
role: "developer",
content: `Ton id est : <@${process.env.APP_ID}>, évite de l'utiliser. Ton username et global_name sont : ${process.env.APP_NAME}`
},
{
role: "developer",
content: `L'utilisateur qui s'adresse a toi est : <@${akhyAuthor.id}>`
},
{
role: "user",
content: requestMessage.length > 1 ? requestMessage : 'Salut',
});
}
else if (process.env.MODEL === 'Mistral') {
// Map to Mistral format
formatted.push({
role: 'system',
content: `${convo}`,
});
formatted.push({
role: 'system',
content: `Voici la liste des différents utilisateurs présents : ${allAkhysText}`,
});
formatted.push(
{
role: "system",
content: "Adopte une attitude détendue et répond comme si tu participais à la conversation, pas trop long, pas de retour à la ligne, simple. N'hésites pas à utiliser des abréviations mais sans en abuser."
},
{
role: 'system',
content: message.mentions.repliedUser?.id ? `La phrase de l'utilisateur répond à un message de ${message.mentions.repliedUser?.id === process.env.APP_ID ? 'toi-même' : message.mentions.repliedUser?.id}` : '',
},
{
role: "system",
content: `Ton id est : <@${process.env.APP_ID}>, évite de l'utiliser. Ton username et global_name sont : ${process.env.APP_NAME}`
},
{
role: "system",
content: `L'utilisateur qui s'adresse a toi est : <@${akhyAuthor.id}>`
},
{
role: "user",
content: requestMessage.length > 1 ? requestMessage : 'Salut',
});
}
// await gork(formatted); IA en marche
const reply = await gork(formatted);
// Send response to the channel
await message.reply(reply);
} catch (err) {
console.error("Error fetching or sending messages:", err);
await message.reply("Oups, y'a eu un problème!");
}
}
else if (message.content.toLowerCase().includes("quoi")) {
let prob = Math.random()
console.log(`feur ${prob}`)
if (prob < process.env.FEUR_PROB) {
// Send a message "feur" to the same channel
message.channel.send(`feur`)
.catch(console.error);
}
}
else if (message.guildId === process.env.DEV_GUILD_ID) {
// ADMIN COMMANDS
if (message.content.toLowerCase().startsWith('?u')) {
console.log(await getAPOUsers())
}
else if (message.content.toLowerCase().startsWith('?b')) {
const amount = message.content.replace('?b ', '')
console.log(amount)
console.log(await postAPOBuy('650338922874011648', amount))
}
else if (message.content.toLowerCase().startsWith('?v')) {
console.log('active polls :')
console.log(activePolls)
}
else if (message.author.id === process.env.DEV_ID) {
const prefix = process.env.DEV_SITE === 'true' ? 'test' : 'flopo'
if (message.content === prefix + ':add-coins-to-users') {
console.log(message.author.id)
try {
const stmtUpdateUsers = flopoDB.prepare(`
ALTER TABLE users
ADD coins INTEGER DEFAULT 0
`);
stmtUpdateUsers.run()
} catch (e) {
console.log(e)
}
}
else if (message.content === prefix + ':users') {
const allAkhys = getAllUsers.all()
console.log(allAkhys)
}
else if (message.content === prefix + ':cancel') {
await message.delete()
}
else if (message.content.startsWith(prefix + ':reset-user-coins')) {
const userId = message.content.replace(prefix + ':reset-user-coins ', '')
const authorDB = getUser.get(userId)
if (authorDB) {
updateUserCoins.run({
id: userId,
coins: 0,
})
console.log(`${authorDB.username}'s coins were reset to 0`)
} else {
console.log('invalid user')
}
}
else if (message.content.startsWith(prefix + ':send-message')) {
const msg = message.content.replace(prefix + ':send-message ', '')
await fetch(process.env.BASE_URL + '/send-message', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
channelId: '1368908514545631262',
message: msg,
})
});
}
else if (message.content.startsWith(prefix + ':sql')) {
let sqlCommand = message.content.replace(prefix + ':sql ', '')
console.log(sqlCommand)
try {
if (sqlCommand.startsWith('SELECT')) {
const stmt = flopoDB.prepare(`${sqlCommand}`).all();
console.log(stmt)
} else {
const stmt = flopoDB.prepare(`${sqlCommand}`).run();
console.log(stmt)
}
} catch (e) {
console.log(e)
}
}
else if (message.content.startsWith(prefix + ':poker')) {
io.emit('message', message.content);
}
}
}
});
// Once bot is ready
client.once('ready', async () => {
console.log(`Logged in as ${client.user.tag}`);
console.log(`[Connected with ${FLAPI_URL}]`)
const randomMinute = Math.floor(Math.random() * 60);
const randomHour = Math.floor(Math.random() * (18 - 8 + 1)) + 8;
todaysHydrateCron = `${randomMinute} ${randomHour} * * *`
console.log(todaysHydrateCron)
await getAkhys();
console.log('FlopoBOT marked as ready')
// every 10 minutes
cron.schedule('*/10 * * * *', async () => {
const FIVE_MINUTES = 5 * 60 * 1000;
// clean 5 minutes old inventories
for (const id in activeInventories) {
const inventory = activeInventories[id];
if (Date.now() >= inventory.timestamp + FIVE_MINUTES) {
console.log(`Removing expired inventory : ${id}`);
delete activeInventories[id];
}
}
for (const id in activeSearchs) {
const search = activeSearchs[id];
if (Date.now() >= search.timestamp + FIVE_MINUTES) {
console.log(`Removing expired search : ${id}`);
delete activeSearchs[id];
}
}
for (const id in activePredis) {
const predi = activePredis[id];
if (predi.closed) {
if (predi.paidTime && Date.now() >= predi.paidTime + (24 * 60 * 60 * 1000)) {
console.log(`Removing expired paid predi : ${id}`);
delete activePredis[id];
} else if (Date.now() >= predi.cancelledTime + (24 * 60 * 60 * 1000)) {
console.log(`Removing expired cancelled predi : ${id}`);
delete activePredis[id];
}
}
}
});
// ─── 💀 Midnight Chaos Timer ──────────────────────
cron.schedule(process.env.CRON_EXPR, async () => {
const randomMinute = Math.floor(Math.random() * 60);
const randomHour = Math.floor(Math.random() * (18 - 8 + 1)) + 8;
todaysHydrateCron = `${randomMinute} ${randomHour} * * *`
console.log(todaysHydrateCron)
const guild = await client.guilds.fetch(process.env.GUILD_ID);
const roleId = process.env.VOTING_ROLE_ID; // Set this in your .env file
const members = await getOnlineUsersWithRole(process.env.GUILD_ID, roleId);
const prob = Math.random();
if (members.size === 0 || prob > process.env.CHAOS_PROB) {
console.log(`No roulette tonight ${prob}`)
return
}
const randomMember = members[Math.floor(Math.random() * members.size)];
const timeoutUntil = new Date(Date.now() + 12 * 60 * 60 * 1000).toISOString();
try {
await guild.members.edit(randomMember.user.id, {
communication_disabled_until: timeoutUntil,
reason: 'Roulette Russe 🔔',
});
const generalChannel = guild.channels.cache.find(
ch => ch.name === 'général' || ch.name === 'general'
);
if (generalChannel && generalChannel.isTextBased()) {
generalChannel.send(
`🎯 <@${randomMember.user.id}> ça dégage, à mimir ! (jusqu'à 12h00)`
);
}
console.log(`${randomMember.user.username} has been timed out until ${timeoutUntil}`);
} catch (err) {
console.error('Failed to timeout random member:', err);
}
});
cron.schedule(todaysHydrateCron, async () => {
const guild = await client.guilds.fetch(process.env.GUILD_ID);
try {
const generalChannel = guild.channels.cache.find(
ch => ch.name === 'général' || ch.name === 'general'
);
if (generalChannel && generalChannel.isTextBased()) {
generalChannel.send(
`${getRandomHydrateText()} ${getRandomEmoji(1)}`
);
}
console.log(`Message hydratation`);
} catch (err) {
console.error('Message hydratation:', err);
}
});
// users/skins dayly fetch at 7am
cron.schedule('0 7 * * *', async() => {
// fetch eventual new users/skins
await getAkhys();
console.log('Users and skins fetched')
})
});
/**
* Interactions endpoint URL where Discord will send HTTP requests
* Parse request body and verifies incoming requests using discord-interactions package
*/
app.post('/interactions', verifyKeyMiddleware(process.env.PUBLIC_KEY), async function (req, res) {
// Interaction id, type and data
const { id, type, data } = req.body;
/**
* Handle verification requests
*/
if (type === InteractionType.PING) {
return res.send({ type: InteractionResponseType.PONG });
}
/**
* Handle slash command requests
* See https://discord.com/developers/docs/interactions/application-commands#slash-commands
*/
if (type === InteractionType.APPLICATION_COMMAND) {
const { name } = data;
// 'timeout' command
if (name === 'timeout') {
// Interaction context
const context = req.body.context;
// User ID is in user field for (G)DMs, and member for servers
const userId = context === 0 ? req.body.member.user.id : req.body.user.id;
// User's choices
const akhy = req.body.data.options[0].value;
const time = req.body.data.options[1].value;
const guild = await client.guilds.fetch(req.body.guild_id);
const fromMember = await guild.members.fetch(userId);
const toMember = await guild.members.fetch(akhy);
const already = Object.values(activePolls).find(poll => poll.toUsername === toMember.user);
if (already) {
return res.send({
type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE,
data: {
content: `Impossible de timeout **${toMember.user}** car un vote est déjà en cours`,
flags: InteractionResponseFlags.EPHEMERAL,
}
});
}
if (toMember.communicationDisabledUntilTimestamp > Date.now()) {
return res.send({
type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE,
data: {
content: `**${toMember.user}** est déjà timeout`,
flags: InteractionResponseFlags.EPHEMERAL,
}
});
}
// Save the poll information along with channel ID so we can notify later
activePolls[id] = {
id: userId,
username: fromMember.user,
toUserId: akhy,
toUsername: toMember.user,
time: time,
time_display: formatTime(time),
for: 0,
against: 0,
voters: [],
channelId: req.body.channel_id, // Capture channel for follow-up notification
endpoint: `webhooks/${process.env.APP_ID}/${req.body.token}/messages/@original`,
};
const guildId = req.body.guild_id;
const roleId = process.env.VOTING_ROLE_ID; // Set this in your .env file
const onlineEligibleUsers = await getOnlineUsersWithRole(guildId, roleId);
const requiredMajority = Math.max(parseInt(process.env.MIN_VOTES), Math.floor(onlineEligibleUsers.size / 2) + 1);
const votesNeeded = Math.max(0, requiredMajority - activePolls[id].for);
activePolls[id].endTime = Date.now() + process.env.POLL_TIME * 1000;
activePolls[id].requiredMajority = requiredMajority;
// Set an interval to update the countdown every 10 seconds (or more often if you want)
const countdownInterval = setInterval(async () => {
const poll = activePolls[id];
if (!poll) {
clearInterval(countdownInterval);
io.emit('new-poll', { action: 'timeout cleared' });
return;
}
const remaining = Math.max(0, Math.floor((poll?.endTime - Date.now()) / 1000));
const minutes = Math.floor(remaining / 60);
const seconds = remaining % 60;
const countdownText = `**${minutes}m ${seconds}s** restantes`;
const votesNeeded = Math.max(0, activePolls[id].requiredMajority - activePolls[id].for);
if (!poll || remaining === 0) {
try {
await DiscordRequest(
poll.endpoint,
{
method: 'PATCH',
body: {
embeds: [
{
title: `Le vote pour timeout ${poll.toUsername.username} pendant ${poll.time_display} a échoué 😔`,
description: `Il manquait **${votesNeeded}** vote(s)`,
fields: [
{
name: 'Pour',
value: '✅ ' + poll.for,
inline: true,
},
{
name: 'Temps restant',
value: '⏳ ' + countdownText,
inline: false,
},
],
color: 0xF2F3F3, // You can set the color of the embed
},
],
components: [],
},
}
);
} catch (err) {
console.error('Error sending message', err);
}
console.log('clear poll')
clearInterval(countdownInterval);
delete activePolls[id];
io.emit('new-poll', { action: 'timeout cleared' });
return;
}
try {
await DiscordRequest(
poll.endpoint,
{
method: 'PATCH',
body: {
embeds: [
{
title: `Timeout`,
description: `**${poll.username}** propose de timeout **${poll.toUsername}** pendant ${poll.time_display}\nIl manque **${votesNeeded}** vote(s)`,
fields: [
{
name: 'Pour',
value: '✅ ' + poll.for,
inline: true,
},
{
name: 'Temps restant',
value: '⏳ ' + countdownText,
inline: false,
},
],
color: 0xF2F3F3, // You can set the color of the embed
},
],
components: [
{
type: MessageComponentTypes.ACTION_ROW,
components: [
{
type: MessageComponentTypes.BUTTON,
custom_id: `vote_for_${req.body.id}`,
label: 'Oui ✅',
style: ButtonStyleTypes.SECONDARY,
},
{
type: MessageComponentTypes.BUTTON,
custom_id: `vote_against_${req.body.id}`,
label: 'Non ❌',
style: ButtonStyleTypes.SECONDARY,
},
],
},
],
},
}
);
} catch (err) {
console.error('Error updating countdown:', err);
}
}, 1000); // every second
const remaining = Math.max(0, Math.floor((activePolls[id].endTime - Date.now()) / 1000));
const minutes = Math.floor(remaining / 60);
const seconds = remaining % 60;
const countdownText = `**${minutes}m ${seconds}s** restantes`;
// web site update
io.emit('new-poll', { action: 'timeout command' });
return res.send({
type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE,
data: {
embeds: [
{
title: `Timeout`,
description: `**${activePolls[id].username}** propose de timeout **${activePolls[id].toUsername}** pendant ${activePolls[id].time_display}\nIl manque **${votesNeeded}** vote(s)`,
fields: [
{
name: 'Pour',
value: '✅ ' + activePolls[id].for,
inline: true,
},
{
name: 'Temps restant',
value: '⏳ ' + countdownText,
inline: false,
},
],
color: 0xF2F3F3, // You can set the color of the embed
},
],
components: [
{
type: MessageComponentTypes.ACTION_ROW,
components: [
{
type: MessageComponentTypes.BUTTON,
custom_id: `vote_for_${req.body.id}`,
label: 'Oui ✅',
style: ButtonStyleTypes.SECONDARY,
},
{
type: MessageComponentTypes.BUTTON,
custom_id: `vote_against_${req.body.id}`,
label: 'Non ❌',
style: ButtonStyleTypes.SECONDARY,
},
],
},
],
},
});
}
if (name === 'inventory') {
// Interaction context
const context = req.body.context;
// User ID is in user field for (G)DMs, and member for servers
const userId = context === 0 ? req.body.member.user.id : req.body.user.id;
// User's choices
const akhy = req.body.data.options ? req.body.data.options[0].value : userId;
const guild = await client.guilds.fetch(req.body.guild_id);
const completeAkhy = await guild.members.fetch(akhy);
const invSkins = getUserInventory.all({user_id: akhy});
const chromaText = (skin) => {
let res = ""
for (let i = 1; i <= skins.find((s) => s.uuid === skin.uuid).chromas.length; i++) {
res += skin.currentChroma === i ? '💠 ' : '◾ '
}
return res
}
const chromaName = (skin) => {
if (skin.currentChroma >= 2) {
const name = skins.find((s) => s.uuid === skin.uuid).chromas[skin.currentChroma-1].displayName.replace(/[\r\n]+/g, '').replace(skin.displayName, '')
const match = name.match(/variante\s+[1-4]\s+([^)]+)/)
const result = match ? match[2] : null;
if (match) {
return match[1].trim()
} else {
return name
}
}
if (skin.currentChroma === 1) {
return 'Base'
}
return ''
};
let content = '';
let totalPrice = 0;
let fields = [];
invSkins.forEach(skin => {
content += `- ${skin.displayName} | ${skin.currentPrice.toFixed()}\n`;
totalPrice += skin.currentPrice;
fields.push({
name: `${skin.displayName} | ${skin.currentPrice.toFixed(2)}`,
value: `${skin.tierText}\nChroma : ${chromaText(skin)} | ${chromaName(skin)}\nLvl : **${skin.currentLvl}**/${skins.find((s) => s.uuid === skin.uuid).levels.length}\n`,
inline: false,
})
})
activeInventories[id] = {
akhyId: akhy,
userId: userId,
page: 0,
amount: invSkins.length,
reqBodyId: req.body.id,
endpoint: `webhooks/${process.env.APP_ID}/${req.body.token}/messages/@original`,
timestamp: Date.now(),
};
if (invSkins.length === 0) {
return res.send({
type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE,
data: {
embeds: [
{
title: `Inventaire de ${completeAkhy.user.username}`,
description: "Aucun skin dans l'inventaire",
color: 0xF2F3F3,
footer: {text: `Total : ${totalPrice.toFixed(2)}`},
},
],
},
});
}
const trueSkin = skins.find((s) => s.uuid === invSkins[0].uuid);
const imageUrl = () => {
let res;
if (invSkins[0].currentLvl === trueSkin.levels.length) {
if (invSkins[0].currentChroma === 1) {
res = trueSkin.chromas[0].displayIcon
} else {
res = trueSkin.chromas[invSkins[0].currentChroma-1].fullRender ?? trueSkin.chromas[invSkins[0].currentChroma-1].displayIcon
}
} else if (invSkins[0].currentLvl === 1) {
res = trueSkin.levels[0].displayIcon ?? trueSkin.chromas[0].fullRender
} else if (invSkins[0].currentLvl === 2 || invSkins[0].currentLvl === 3) {
res = trueSkin.displayIcon
}
if (res) return res;
return trueSkin.displayIcon
};
let components = [
{
type: MessageComponentTypes.BUTTON,
custom_id: `prev_page_${req.body.id}`,
label: '⏮️ Préc.',
style: ButtonStyleTypes.SECONDARY,
},
{
type: MessageComponentTypes.BUTTON,
custom_id: `next_page_${req.body.id}`,
label: 'Suiv. ⏭️',
style: ButtonStyleTypes.SECONDARY,
},
]
if ((invSkins[0].currentLvl < trueSkin.levels.length || invSkins[0].currentChroma < trueSkin.chromas.length) && akhy === userId) {
components.push({
type: MessageComponentTypes.BUTTON,
custom_id: `upgrade_${req.body.id}`,
label: `Upgrade ⏫`,
style: ButtonStyleTypes.PRIMARY,
})
}
return res.send({
type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE,
data: {
embeds: [
{
title: `Inventaire de ${completeAkhy.user.username}`,
description: `${invSkins?.length > 0 ? '' : "Aucun skin dans l'inventaire"}`,
color: 0xF2F3F3,
footer: {text: `${activeInventories[id].page+1}/${invSkins?.length} | Total : ${totalPrice.toFixed(2)}`},
fields: [fields[activeInventories[id].page]],
image: {
url: invSkins?.length > 0 ? imageUrl() : '',
}
},
],
components: [
{
type: MessageComponentTypes.ACTION_ROW,
components: components,
},
],
},
});
}
if (name === 'valorant') {
const buyResponse = await postAPOBuy(req.body.member.user.id, process.env.VALO_PRICE ?? 150)
if (buyResponse.status === 500 || buyResponse.ok === false) {
return res.send({
type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE,
data: {
content: `Tu n'as pas assez d'argent...`,
flags: InteractionResponseFlags.EPHEMERAL,
}
});
}
// First, send the initial response immediately
const initialEmbed = new EmbedBuilder()
.setTitle(`\t`)
.setImage('https://media.tenor.com/gIWab6ojBnYAAAAd/weapon-line-up-valorant.gif')
.setColor(`#F2F3F3`);
// Send the initial response and store the reply object
await res.send({
type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE,
data: { embeds: [initialEmbed] }
});
// Get a random skin
const dbSkins = getAllAvailableSkins.all();
const randomIndex = Math.floor(Math.random() * dbSkins.length);
let randomSkin;
try {
randomSkin = skins.find((skin) => skin.uuid === dbSkins[randomIndex].uuid);
if (!randomSkin) throw new Error("Skin not found");
} catch (e) {
// Edit the original message if there's an error
await DiscordRequest(
`webhooks/${process.env.APP_ID}/${req.body.token}/messages/@original`,
{
method: 'PATCH',
body: {
content: "Oups, ya eu un ptit problème",
embeds: []
}
}
);
return;
}
// Generate random level and chroma
const randomLevel = Math.floor(Math.random() * randomSkin.levels.length + 1);
let randomChroma = randomLevel === randomSkin.levels.length
? Math.floor(Math.random() * randomSkin.chromas.length + 1)
: 1;
if (randomChroma === randomSkin.chromas.length && randomSkin.chromas.length >= 2) randomChroma--
const selectedLevel = randomSkin.levels[randomLevel - 1]
const selectedChroma = randomSkin.chromas[randomChroma - 1]
// Set timeout for the reveal
setTimeout(async () => {
// Prepare the final embed
const selectedLevel = randomSkin.levels[randomLevel - 1];
const selectedChroma = randomSkin.chromas[randomChroma - 1];
// Helper functions (unchanged from your original code)
const videoUrl = () => {
let res;
if (randomLevel === randomSkin.levels.length) {
if (randomChroma === 1) {
res = randomSkin.levels[randomSkin.levels.length - 1].streamedVideo ?? randomSkin.chromas[0].streamedVideo
} else {
res = randomSkin.chromas[randomChroma-1].streamedVideo
}
} else {
res = randomSkin.levels[randomLevel-1].streamedVideo
}
return res;
};
const imageUrl = () => {
let res;
if (randomLevel === randomSkin.levels.length) {
if (randomChroma === 1) {
res = randomSkin.chromas[0].displayIcon
} else {
res = randomSkin.chromas[randomChroma-1].fullRender ?? randomSkin.chromas[randomChroma-1].displayIcon
}
} else if (randomLevel === 1) {
res = randomSkin.levels[0].displayIcon ?? randomSkin.chromas[0].fullRender
} else if (randomLevel === 2 || randomLevel === 3) {
res = randomSkin.displayIcon
}
if (res) return res;
return randomSkin.displayIcon
};
const chromaName = () => {
if (randomChroma >= 2) {
const name = selectedChroma.displayName.replace(/[\r\n]+/g, '').replace(randomSkin.displayName, '')
const match = name.match(/variante\s+[1-4]\s+([^)]+)/)
const result = match ? match[2] : null;
if (match) {
return match[1].trim()
} else {
return name
}
}
if (randomChroma === 1) {
return 'Base'
}
return ''
};
const lvlText = () => {
let res = ""
if (randomLevel >= 1) {
res += '1⃣ '
}
if (randomLevel >= 2) {
res += '2⃣ '
}
if (randomLevel >= 3) {
res += '3⃣ '
}
if (randomLevel >= 4) {
res += '4⃣ '
}
if (randomLevel >= 5) {
res += '5⃣ '
}
for (let i = 0; i < randomSkin.levels.length - randomLevel; i++) {
res += '◾ '
}
return res
}
const chromaText = () => {
let res = ""
for (let i = 1; i <= randomSkin.chromas.length; i++) {
res += randomChroma === i ? '💠 ' : '◾ '
}
return res
}
const price = () => {
let res = dbSkins[randomIndex].basePrice;
res *= (1 + (randomLevel / Math.max(randomSkin.levels.length, 2)))
res *= (1 + (randomChroma / 4))
return res.toFixed(2);
}
// Update the database
try {
await updateSkin.run({
uuid: randomSkin.uuid,
user_id: req.body.member.user.id,
currentLvl: randomLevel,
currentChroma: randomChroma,
currentPrice: price()
});
} catch (e) {
console.log('Database error', e);
}
// Build the final embed
const finalEmbed = new EmbedBuilder()
.setTitle(`${randomSkin.displayName} | ${chromaName()}`)
.setFields([
{ name: '', value: `**Lvl** | ${lvlText()}`, inline: true },
{ name: '', value: `**Chroma** | ${chromaText()}`, inline: true },
{ name: '', value: `**Prix** | ${price()} <:vp:1362964205808128122>`, inline: true },
])
.setDescription(dbSkins[randomIndex].tierText)
.setImage(imageUrl())
.setFooter({ text: 'Ajouté à ton inventaire' })
.setColor(`#${dbSkins[randomIndex].tierColor}`);
// Prepare components if video exists
const video = videoUrl();
const components = [];
if (video) {
components.push(
new ActionRowBuilder().addComponents(
new ButtonBuilder()
.setLabel('🎬 Aperçu vidéo')
.setStyle(ButtonStyle.Link)
.setURL(video)
)
);
}
// Edit the original message
try {
await DiscordRequest(
`webhooks/${process.env.APP_ID}/${req.body.token}/messages/@original`,
{
method: 'PATCH',
body: {
embeds: [finalEmbed],
components: components
}
}
);
} catch (err) {
console.error('Error editing message:', err);
}
}, 5000);
return;
}
if (name === 'info') {
const guild = await client.guilds.fetch(req.body.guild_id);
await guild.members.fetch()
const timedOutMembers = guild.members.cache.filter(
(member) =>
member.communicationDisabledUntil &&
member.communicationDisabledUntil > new Date()
);
if (timedOutMembers.size === 0) {
return res.send({
type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE,
data: {
embeds: [
{
title: `Membres timeout`,
description: "Aucun membre n'est actuellement timeout.",
color: 0xF2F3F3,
},
],
},
});
}
const list = timedOutMembers.map(
(member) =>
`**${member.user.tag}** (jusqu'à ${member.communicationDisabledUntil.toLocaleString()})`
).join("\n");
return res.send({
type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE,
data: {
embeds: [
{
title: `Membres timeout`,
description: `${list}`,
color: 0xF2F3F3,
},
],
},
});
}
if (name === 'skins') {
const topSkins = getTopSkins.all()
const guild = await client.guilds.fetch(req.body.guild_id)
let fields = []
for (const skin of topSkins) {
const index = topSkins.indexOf(skin);
const owner = skin.user_id ? await guild.members.fetch(skin.user_id) : null;
fields.push({
name: `#${index+1} - **${skin.displayName}**`,
value: `${skin.maxPrice}${skin.user_id ? '| **@'+ owner.user.username+'** ✅' : ''}\n`,
inline: false
});
}
return res.send({
type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE,
data: {
embeds: [
{
fields: fields,
color: 0xF2F3F3,
},
],
},
});
}
if (name === 'search') {
const context = req.body.context;
// User ID is in user field for (G)DMs, and member for servers
const userId = context === 0 ? req.body.member.user.id : req.body.user.id;
const searchValue = req.body.data.options[0].value.toLowerCase();
const guild = await client.guilds.fetch(req.body.guild_id);
let dbSkins = getAllSkins.all()
let resultSkins = dbSkins.filter((skin) => {
return skin.displayName.toLowerCase().includes(searchValue) || skin.tierText.toLowerCase().includes(searchValue);
})
if (resultSkins.length === 0) {
return res.send({
type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE,
data: {
content: 'Aucun résultat ne correspond à ta recherche',
flags: InteractionResponseFlags.EPHEMERAL,
}
})
}
const owner = await guild.members.fetch(resultSkins[0].user_id)
let fields = [
{
name: `**${resultSkins[0].displayName}** | ${resultSkins[0].tierText}`,
value: `${resultSkins[0].maxPrice}${resultSkins[0].user_id ? '| **@'+ owner.user.username +'** ✅' : ''}`,
inline: false,
}
]
activeSearchs[id] = {
userId: userId,
page: 0,
amount: resultSkins.length,
resultSkins: resultSkins,
endpoint: `webhooks/${process.env.APP_ID}/${req.body.token}/messages/@original`,
timestamp: Date.now(),
searchValue: searchValue,
};
const trueSkin = skins.find((s) => s.uuid === resultSkins[0].uuid);
const imageUrl = () => {
let res;
if (trueSkin.chromas[trueSkin.chromas.length-1].displayIcon) {
res = trueSkin.chromas[trueSkin.chromas.length-1].displayIcon
} else if (trueSkin.levels[trueSkin.levels.length-1].displayIcon) {
res = trueSkin.levels[trueSkin.levels.length-1].displayIcon
} else {
res = trueSkin.displayIcon
}
return res
};
const videoUrl = () => {
let res;
if (trueSkin.chromas[trueSkin.chromas.length-1].streamedVideo) {
res = trueSkin.chromas[trueSkin.chromas.length-1].streamedVideo
} else if (trueSkin.levels[trueSkin.levels.length-1].streamedVideo) {
res = trueSkin.levels[trueSkin.levels.length-1].streamedVideo
} else {
res = null
}
return res
};
const originalComponents = [
{
type: MessageComponentTypes.BUTTON,
custom_id: `prev_search_page_${req.body.id}`,
label: '⏮️ Préc.',
style: ButtonStyleTypes.SECONDARY,
},
{
type: MessageComponentTypes.BUTTON,
custom_id: `next_search_page_${req.body.id}`,
label: 'Suiv. ⏭️',
style: ButtonStyleTypes.SECONDARY,
},
];
return res.send({
type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE,
data: {
embeds: [
{
title: `Résultat de recherche`,
description: `🔎 ${searchValue}`,
fields: fields,
color: parseInt(resultSkins[0].tierColor, 16),
image: { url: imageUrl() },
footer: { text: `1/${resultSkins.length} résultat(s)` },
},
],
components: [
{
type: MessageComponentTypes.ACTION_ROW,
components: originalComponents,
},
],
},
});
}
console.error(`unknown command: ${name}`);
return res.status(400).json({ error: 'unknown command' });
}
if (type === InteractionType.MESSAGE_COMPONENT) {
// custom_id set in payload when sending message component
const componentId = data.custom_id;
if (componentId.startsWith('accept_button_')) {
// get the associated game ID
const gameId = componentId.replace('accept_button_', '');
// Delete message with token in request body
const endpoint = `webhooks/${process.env.APP_ID}/${req.body.token}/messages/${req.body.message.id}`;
try {
await res.send({
type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE,
data: {
content: 'What is your object of choice?',
// Indicates it'll be an ephemeral message
flags: InteractionResponseFlags.EPHEMERAL,
components: [
{
type: MessageComponentTypes.ACTION_ROW,
components: [
{
type: MessageComponentTypes.STRING_SELECT,
// Append game ID
custom_id: `select_choice_${gameId}`,
options: getShuffledOptions(),
},
],
},
],
},
});
// Delete previous message
await DiscordRequest(endpoint, { method: 'DELETE' });
} catch (err) {
console.error('Error sending message:', err);
}
}
else if (componentId.startsWith('vote_')) {
let gameId, isVotingFor;
if (componentId.startsWith('vote_for_')) {
gameId = componentId.replace('vote_for_', '');
isVotingFor = true;
} else {
gameId = componentId.replace('vote_against_', '');
isVotingFor = false;
}
if (activePolls[gameId]) {
const poll = activePolls[gameId];
poll.voters = poll.voters || [];
const voterId = req.body.member.user.id;
// Check if the voter has the required voting role
const voterRoles = req.body.member.roles || [];
if (!voterRoles.includes(process.env.VOTING_ROLE_ID)) {
return res.send({
type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE,
data: {
content: "Tu n'as pas le rôle requis pour voter.",
flags: InteractionResponseFlags.EPHEMERAL,
},
});
}
// Enforce one vote per eligible user
if (poll.voters.find(u => u === voterId)) {
return res.send({
type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE,
data: {
content: "Tu as déjà voté !",
flags: InteractionResponseFlags.EPHEMERAL,
},
});
}
// Record the vote
poll.voters.push(voterId);
if (isVotingFor) {
poll.for++;
} else {
poll.against++;
}
io.emit('new-poll', { action: 'new vote' });
// Retrieve online eligible users (ensure your bot has the necessary intents)
const guildId = req.body.guild_id;
const roleId = process.env.VOTING_ROLE_ID; // Set this in your .env file
const onlineEligibleUsers = await getOnlineUsersWithRole(guildId, roleId);
const votesNeeded = Math.max(0, poll.requiredMajority - poll.for);
// Check if the majority is reached
if (poll.for >= poll.requiredMajority) {
try {
// Build the updated poll message content
await DiscordRequest(
poll.endpoint,
{
method: 'PATCH',
body: {
embeds: [
{
title: `Timeout`,
description: `Proposition de timeout **${poll.toUsername}** pendant ${poll.time_display}`,
fields: [
{
name: 'Votes totaux',
value: '✅ ' + poll.for,
inline: true,
},
],
color: 0xF2F3F3, // You can set the color of the embed
},
],
components: [], // remove buttons
},
}
);
} catch (err) {
console.error('Error updating poll message:', err);
}
// Clear the poll so the setTimeout callback doesn't fire later
delete activePolls[gameId];
// **Actual Timeout Action**
try {
// Calculate the ISO8601 timestamp to disable communications until now + poll.time seconds
const timeoutUntil = new Date(Date.now() + poll.time * 1000).toISOString();
const endpointTimeout = `guilds/${req.body.guild_id}/members/${poll.toUserId}`;
await DiscordRequest(endpointTimeout, {
method: 'PATCH',
body: { communication_disabled_until: timeoutUntil },
});
return res.send({
type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE,
data: {
content: `<@${poll.toUserId}> a été timeout pendant ${poll.time_display} par décision démocratique 👊`,
},
});
} catch (err) {
console.error('Error timing out user:', err);
return res.send({
type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE,
data: {
content: `Impossible de timeout <@${poll.toUserId}>, désolé... 😔`,
},
});
}
}
// If the vote is "for", update the original poll message to reflect the new vote count.
if (isVotingFor) {
const remaining = Math.max(0, Math.floor((poll.endTime - Date.now()) / 1000));
const minutes = Math.floor(remaining / 60);
const seconds = remaining % 60;
const countdownText = `**${minutes}m ${seconds}s** restantes`;
try {
// Build the updated poll message content
await DiscordRequest(
poll.endpoint,
{
method: 'PATCH',
body: {
embeds: [
{
title: `Timeout`,
description: `**${poll.username}** propose de timeout **${poll.toUsername}** pendant ${poll.time_display}\nIl manque **${votesNeeded}** vote(s)`,
fields: [
{
name: 'Pour',
value: '✅ ' + poll.for,
inline: true,
},
{
name: 'Temps restant',
value: '⏳ ' + countdownText,
inline: false,
},
],
color: 0xF2F3F3, // You can set the color of the embed
},
],
components: req.body.message.components, // preserve the buttons
},
}
);
} catch (err) {
console.error('Error updating poll message:', err);
}
}
// Send an ephemeral acknowledgement to the voter
return res.send({
type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE,
data: {
content: `Vote enregistré ! ✅`,
flags: InteractionResponseFlags.EPHEMERAL,
},
});
}
}
else if (componentId.startsWith('prev_page')) {
let invId = componentId.replace('prev_page_', '');
const context = req.body.context;
// User ID is in user field for (G)DMs, and member for servers
const userId = context === 0 ? req.body.member.user.id : req.body.user.id;
const guild = await client.guilds.fetch(req.body.guild_id);
if (!activeInventories[invId]) {
return res.send({
type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE,
data: {
content: `Oups, cet affichage n'est plus actif.\nRelance la commande pour avoir un nouvel élément intéractif`,
flags: InteractionResponseFlags.EPHEMERAL,
},
});
}
const completeAkhy = await guild.members.fetch(activeInventories[invId].akhyId);
const invSkins = getUserInventory.all({user_id: activeInventories[invId].akhyId});
const chromaText = (skin) => {
let res = ""
for (let i = 1; i <= skins.find((s) => s.uuid === skin.uuid).chromas.length; i++) {
res += skin.currentChroma === i ? '💠 ' : '◾ '
}
return res
}
const chromaName = (skin) => {
if (skin.currentChroma >= 2) {
const name = skins.find((s) => s.uuid === skin.uuid).chromas[skin.currentChroma-1].displayName.replace(/[\r\n]+/g, '').replace(skin.displayName, '')
const match = name.match(/variante\s+[1-4]\s+([^)]+)/)
const result = match ? match[2] : null;
if (match) {
return match[1].trim()
} else {
return name
}
}
if (skin.currentChroma === 1) {
return 'Base'
}
return ''
};
let content = '';
let totalPrice = 0;
let fields = [];
invSkins.forEach(skin => {
content += `- ${skin.displayName} | ${skin.currentPrice.toFixed()}\n`;
totalPrice += skin.currentPrice;
fields.push({
name: `${skin.displayName} | ${skin.currentPrice.toFixed(2)}`,
value: `${skin.tierText}\nChroma : ${chromaText(skin)} | ${chromaName(skin)}\nLvl : **${skin.currentLvl}**/${skins.find((s) => s.uuid === skin.uuid).levels.length}\n`,
inline: false,
})
})
if (activeInventories[invId] && activeInventories[invId].userId === req.body.member.user.id) {
if (activeInventories[invId].page === 0) {
activeInventories[invId].page = activeInventories[invId].amount-1
} else {
activeInventories[invId].page--
}
} else {
return res.send({
type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE,
data: {
content: `Tu n'est pas à l'origine de cette commande /inventory`,
flags: InteractionResponseFlags.EPHEMERAL,
},
});
}
const trueSkin = skins.find((s) => s.uuid === invSkins[activeInventories[invId].page].uuid);
const imageUrl = () => {
let res;
if (invSkins[activeInventories[invId].page].currentLvl === trueSkin.levels.length) {
if (invSkins[activeInventories[invId].page].currentChroma === 1) {
res = trueSkin.chromas[0].displayIcon
} else {
res = trueSkin.chromas[invSkins[activeInventories[invId].page].currentChroma-1].fullRender ?? trueSkin.chromas[invSkins[activeInventories[invId].page].currentChroma-1].displayIcon
}
} else if (invSkins[activeInventories[invId].page].currentLvl === 1) {
res = trueSkin.levels[0].displayIcon ?? trueSkin.chromas[0].fullRender
} else if (invSkins[activeInventories[invId].page].currentLvl === 2 || invSkins[activeInventories[invId].page].currentLvl === 3) {
res = trueSkin.displayIcon
}
if (res) return res;
return trueSkin.displayIcon
};
let components = req.body.message.components;
if ((invSkins[activeInventories[invId].page].currentLvl < trueSkin.levels.length || invSkins[activeInventories[invId].page].currentChroma < trueSkin.chromas.length) && activeInventories[invId].akhyId === activeInventories[invId].userId) {
if (components[0].components.length === 2) {
components[0].components.push({
type: MessageComponentTypes.BUTTON,
custom_id: `upgrade_${activeInventories[invId].reqBodyId}`,
label: `Upgrade ⏫`,
style: ButtonStyleTypes.PRIMARY,
})
}
} else {
if (components[0].components.length === 3) {
components[0].components.pop()
}
}
try {
await DiscordRequest(
activeInventories[invId].endpoint,
{
method: 'PATCH',
body: {
embeds: [
{
title: `Inventaire de ${completeAkhy.user.username}`,
description: `${invSkins?.length > 0 ? '' : "Aucun skin dans l'inventaire"}`,
color: 0xF2F3F3,
footer: {text: `${activeInventories[invId].page+1}/${invSkins?.length} | Total : ${totalPrice.toFixed(2)}`},
fields: [fields[activeInventories[invId].page]],
image: {
url: invSkins?.length > 0 ? imageUrl() : '',
}
},
],
components: components,
},
}
);
} catch (err) {
console.log('Pas trouvé : ', err)
}
return res.send({
type: InteractionResponseType.DEFERRED_UPDATE_MESSAGE,
});
}
else if (componentId.startsWith('next_page')) {
let invId = componentId.replace('next_page_', '');
const context = req.body.context;
// User ID is in user field for (G)DMs, and member for servers
const userId = context === 0 ? req.body.member.user.id : req.body.user.id;
const guild = await client.guilds.fetch(req.body.guild_id);
if (!activeInventories[invId]) {
return res.send({
type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE,
data: {
content: `Oups, cet inventaire n'est plus actif.\nRelance la commande pour avoir un nouvel inventaire interactif`,
flags: InteractionResponseFlags.EPHEMERAL,
},
});
}
const completeAkhy = await guild.members.fetch(activeInventories[invId].akhyId);
const invSkins = getUserInventory.all({user_id: activeInventories[invId].akhyId});
const chromaText = (skin) => {
let res = ""
for (let i = 1; i <= skins.find((s) => s.uuid === skin.uuid).chromas.length; i++) {
res += skin.currentChroma === i ? '💠 ' : '◾ '
}
return res
}
const chromaName = (skin) => {
if (skin.currentChroma >= 2) {
const name = skins.find((s) => s.uuid === skin.uuid).chromas[skin.currentChroma-1].displayName.replace(/[\r\n]+/g, '').replace(skin.displayName, '')
const match = name.match(/variante\s+[1-4]\s+([^)]+)/)
const result = match ? match[2] : null;
if (match) {
return match[1].trim()
} else {
return name
}
}
if (skin.currentChroma === 1) {
return 'Base'
}
return ''
};
let content = '';
let totalPrice = 0;
let fields = [];
invSkins.forEach(skin => {
content += `- ${skin.displayName} | ${skin.currentPrice.toFixed()}\n`;
totalPrice += skin.currentPrice;
fields.push({
name: `${skin.displayName} | ${skin.currentPrice.toFixed(2)}`,
value: `${skin.tierText}\nChroma : ${chromaText(skin)} | ${chromaName(skin)}\nLvl : **${skin.currentLvl}**/${skins.find((s) => s.uuid === skin.uuid).levels.length}\n`,
inline: false,
})
})
if (activeInventories[invId] && activeInventories[invId].userId === req.body.member.user.id) {
if (activeInventories[invId].page === activeInventories[invId].amount-1) {
activeInventories[invId].page = 0
} else {
activeInventories[invId].page++
}
} else {
return res.send({
type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE,
data: {
content: `Tu n'est pas à l'origine de cette commande /inventory`,
flags: InteractionResponseFlags.EPHEMERAL,
},
});
}
const trueSkin = skins.find((s) => s.uuid === invSkins[activeInventories[invId].page].uuid);
const imageUrl = () => {
let res;
if (invSkins[activeInventories[invId].page].currentLvl === trueSkin.levels.length) {
if (invSkins[activeInventories[invId].page].currentChroma === 1) {
res = trueSkin.chromas[0].displayIcon
} else {
res = trueSkin.chromas[invSkins[activeInventories[invId].page].currentChroma-1].fullRender ?? trueSkin.chromas[invSkins[activeInventories[invId].page].currentChroma-1].displayIcon
}
} else if (invSkins[activeInventories[invId].page].currentLvl === 1) {
res = trueSkin.levels[0].displayIcon ?? trueSkin.chromas[0].fullRender
} else if (invSkins[activeInventories[invId].page].currentLvl === 2 || invSkins[activeInventories[invId].page].currentLvl === 3) {
res = trueSkin.displayIcon
}
if (res) return res;
return trueSkin.displayIcon
};
let components = req.body.message.components;
if ((invSkins[activeInventories[invId].page].currentLvl < trueSkin.levels.length || invSkins[activeInventories[invId].page].currentChroma < trueSkin.chromas.length) && activeInventories[invId].akhyId === activeInventories[invId].userId) {
if (components[0].components.length === 2) {
components[0].components.push({
type: MessageComponentTypes.BUTTON,
custom_id: `upgrade_${activeInventories[invId].reqBodyId}`,
label: `Upgrade ⏫`,
style: ButtonStyleTypes.PRIMARY,
})
}
} else {
if (components[0].components.length === 3) {
components[0].components.pop()
}
}
try {
await DiscordRequest(
activeInventories[invId].endpoint,
{
method: 'PATCH',
body: {
embeds: [
{
title: `Inventaire de ${completeAkhy.user.username}`,
description: `${invSkins?.length > 0 ? '' : "Aucun skin dans l'inventaire"}`,
color: 0xF2F3F3,
footer: {text: `${activeInventories[invId].page+1}/${invSkins?.length} | Total : ${totalPrice.toFixed(2)}`},
fields: [fields[activeInventories[invId].page]],
image: {
url: invSkins?.length > 0 ? imageUrl() : '',
}
},
],
components: components,
},
}
);
} catch (err) {
console.log('Pas trouvé : ', err)
}
return res.send({
type: InteractionResponseType.DEFERRED_UPDATE_MESSAGE,
});
}
else if (componentId.startsWith('upgrade_')) {
let invId = componentId.replace('upgrade_', '')
const context = req.body.context
const userId = context === 0 ? req.body.member.user.id : req.body.user.id
const guild = await client.guilds.fetch(req.body.guild.id)
if (!activeInventories[invId]) {
return res.send({
type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE,
data: {
content: `Oups, cet inventaire n'est plus actif.\nRelance la commande pour avoir un nouvel inventaire interactif`,
flags: InteractionResponseFlags.EPHEMERAL,
},
});
}
const completeAkhy = await guild.members.fetch(activeInventories[invId].akhyId)
const invSkins = getUserInventory.all({user_id: activeInventories[invId].akhyId})
if (!activeInventories[invId] || activeInventories[invId].userId !== req.body.member.user.id) {
return res.send({
type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE,
data: {
content: `Tu n'est pas à l'origine de cette commande /inventory`,
flags: InteractionResponseFlags.EPHEMERAL,
},
});
}
const upgradePrice = process.env.VALO_UPGRADE_PRICE ?? invSkins[activeInventories[invId].page].maxPrice/10
const buyResponse = await postAPOBuy(req.body.member.user.id, upgradePrice)
if (buyResponse.status === 500 || buyResponse.ok === false) {
return res.send({
type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE,
data: {
content: `Tu n'as pas assez d'argent, cette amélioration coûte ${upgradePrice}`,
flags: InteractionResponseFlags.EPHEMERAL,
}
});
}
const skin = invSkins[activeInventories[invId].page];
const trueSkin = skins.find((s) => s.uuid === invSkins[activeInventories[invId].page].uuid);
const lvlNb = trueSkin.levels.length
const chromaNb = trueSkin.chromas.length
const tierRank = trueSkin.tierRank
const currentLvl = skin.currentLvl
const currentChroma = skin.currentChroma
let succeeded = false
if (currentLvl < lvlNb) {
let prob = (currentLvl/lvlNb)
if (tierRank) prob *= ((tierRank+1)/4)+.1
let rand = Math.random()
console.log(`lvl upgrade prob : ${prob} | ${rand}`)
succeeded = rand >= prob
//amélioration du lvl
if (succeeded) {
let newLvl = skin.currentLvl + 1
const price = () => {
let res = skin.basePrice;
res *= (1 + (newLvl / Math.max(trueSkin.levels.length, 2)))
res *= (1 + (skin.currentChroma / 4))
return res.toFixed(2);
}
try {
await updateSkin.run({
uuid: skin.uuid,
user_id: skin.user_id,
currentLvl: newLvl,
currentChroma: skin.currentChroma,
currentPrice: price()
});
} catch (e) {
console.log('Database error', e);
}
}
}
else if (currentChroma < chromaNb) {
let prob = (currentChroma/chromaNb)
if (tierRank) prob *= ((tierRank+1)/4)+.1
let rand = Math.random()
console.log(`lvl upgrade prob : ${prob} | ${rand}`)
succeeded = rand >= prob
//amélioration du chroma
if (succeeded) {
let newChroma = skin.currentChroma + 1
const price = () => {
let res = skin.basePrice;
res *= (1 + (skin.currentLvl / Math.max(trueSkin.levels.length, 2)))
res *= (1 + (newChroma / 4))
return res.toFixed(2);
}
try {
await updateSkin.run({
uuid: skin.uuid,
user_id: skin.user_id,
currentLvl: skin.currentLvl,
currentChroma: newChroma,
currentPrice: price()
});
} catch (e) {
console.log('Database error', e);
}
}
} else {
return res.send({
type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE,
data: {
content: `Ce skin n'est pas améliorable`,
flags: InteractionResponseFlags.EPHEMERAL,
},
});
}
// gif
const initialEmbed = new EmbedBuilder()
.setTitle(`Amélioration en cours...`)
.setImage('https://media.tenor.com/HD8nVN2QP9MAAAAC/thoughts-think.gif')
.setColor(0xF2F3F3);
// Send the initial response and store the reply object
await res.send({
type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE,
data: { embeds: [initialEmbed] }
});
// then result
setTimeout(async () => {
// Prepare the final embed
let updatedSkin = await getSkin.get(trueSkin.uuid)
const randomLevel = updatedSkin.currentLvl
const randomChroma = updatedSkin.currentChroma
const selectedChroma = trueSkin.chromas[randomChroma-1]
// Helper functions (unchanged from your original code)
const videoUrl = () => {
let res;
if (randomLevel === trueSkin.levels.length) {
if (randomChroma === 1) {
res = trueSkin.levels[trueSkin.levels.length - 1].streamedVideo ?? trueSkin.chromas[0].streamedVideo
} else {
res = trueSkin.chromas[randomChroma-1].streamedVideo
}
} else {
res = trueSkin.levels[randomLevel-1].streamedVideo
}
return res;
};
const imageUrl = () => {
let res;
if (randomLevel === trueSkin.levels.length) {
if (randomChroma === 1) {
res = trueSkin.chromas[0].displayIcon
} else {
res = trueSkin.chromas[randomChroma-1].fullRender ?? trueSkin.chromas[randomChroma-1].displayIcon
}
} else if (randomLevel === 1) {
res = trueSkin.levels[0].displayIcon ?? trueSkin.chromas[0].fullRender
} else if (randomLevel === 2 || randomLevel === 3) {
res = trueSkin.displayIcon
}
if (res) return res;
return trueSkin.displayIcon
};
const chromaName = () => {
if (randomChroma >= 2) {
const name = selectedChroma.displayName.replace(/[\r\n]+/g, '').replace(trueSkin.displayName, '')
const match = name.match(/variante\s+[1-4]\s+([^)]+)/)
const result = match ? match[2] : null;
if (match) {
return match[1].trim()
} else {
return name
}
}
if (randomChroma === 1) {
return 'Base'
}
return ''
};
const lvlText = () => {
let res = ""
if (randomLevel >= 1) {
res += '1⃣ '
}
if (randomLevel >= 2) {
res += '2⃣ '
}
if (randomLevel >= 3) {
res += '3⃣ '
}
if (randomLevel >= 4) {
res += '4⃣ '
}
if (randomLevel >= 5) {
res += '5⃣ '
}
for (let i = 0; i < trueSkin.levels.length - randomLevel; i++) {
res += '◾ '
}
return res
}
const chromaText = () => {
let res = ""
for (let i = 1; i <= trueSkin.chromas.length; i++) {
res += randomChroma === i ? '💠 ' : '◾ '
}
return res
}
// Build the final embed
let finalEmbed;
if (succeeded) {
finalEmbed = new EmbedBuilder()
.setTitle(`L'amélioration a réussi ! 🎉`)
.setFields([
{ name: '', value: `${updatedSkin.displayName} | ${chromaName()}`, inline: false },
{ name: '', value: `**Lvl** | ${lvlText()}`, inline: true },
{ name: '', value: `**Chroma** | ${chromaText()}`, inline: true },
{ name: '', value: `**Prix** | ${updatedSkin.currentPrice} <:vp:1362964205808128122>`, inline: true },
])
.setDescription(updatedSkin.tierText)
.setImage(imageUrl())
.setColor(0x00FF00);
}
else {
finalEmbed = new EmbedBuilder()
.setTitle(`L'amélioration a râté... ❌`)
.setFields([
{ name: '', value: `${updatedSkin.displayName} | ${chromaName()}`, inline: false },
])
.setDescription(updatedSkin.tierText)
.setImage(imageUrl())
.setColor(0xFF0000);
}
// Prepare components if video exists
const video = videoUrl();
const components = [];
if (!succeeded) {
components.push(new ActionRowBuilder().addComponents(
new ButtonBuilder()
.setLabel('Réessayer 🔄️')
.setStyle(ButtonStyle.Primary)
.setCustomId(`upgrade_${activeInventories[invId].reqBodyId}`)
))
} else if (video) {
components.push(
new ActionRowBuilder().addComponents(
new ButtonBuilder()
.setLabel('🎬 Aperçu vidéo')
.setStyle(ButtonStyle.Link)
.setURL(video)
)
);
}
// Edit the original message
try {
await DiscordRequest(
`webhooks/${process.env.APP_ID}/${req.body.token}/messages/@original`,
{
method: 'PATCH',
body: {
embeds: [finalEmbed],
components: components
}
}
);
} catch (err) {
console.error('Error editing message:', err);
}
}, 500);
}
else if (componentId.startsWith('prev_search_page')) {
let searchId = componentId.replace('prev_search_page_', '');
const context = req.body.context;
// User ID is in user field for (G)DMs, and member for servers
const userId = context === 0 ? req.body.member.user.id : req.body.user.id;
const guild = await client.guilds.fetch(req.body.guild_id);
if (!activeSearchs[searchId]) {
return res.send({
type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE,
data: {
content: `Oups, cet affichage n'est plus actif.\nRelance la commande pour avoir un nouvel élément intéractif`,
flags: InteractionResponseFlags.EPHEMERAL,
},
});
}
const chromaText = (skin) => {
let res = ""
for (let i = 1; i <= skins.find((s) => s.uuid === skin.uuid).chromas.length; i++) {
res += skin.currentChroma === i ? '💠 ' : '◾ '
}
return res
}
const chromaName = (skin) => {
if (skin.currentChroma >= 2) {
const name = skins.find((s) => s.uuid === skin.uuid).chromas[skin.currentChroma-1].displayName.replace(/[\r\n]+/g, '').replace(skin.displayName, '')
const match = name.match(/variante\s+[1-4]\s+([^)]+)/)
const result = match ? match[2] : null;
if (match) {
return match[1].trim()
} else {
return name
}
}
if (skin.currentChroma === 1) {
return 'Base'
}
return ''
};
if (activeSearchs[searchId] && activeSearchs[searchId].userId === req.body.member.user.id) {
if (activeSearchs[searchId].page === 0) {
activeSearchs[searchId].page = activeSearchs[searchId].amount-1
} else {
activeSearchs[searchId].page--
}
} else {
return res.send({
type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE,
data: {
content: `Tu n'est pas à l'origine de cette commande /search`,
flags: InteractionResponseFlags.EPHEMERAL,
},
});
}
const trueSkin = skins.find((s) => s.uuid === activeSearchs[searchId].resultSkins[activeSearchs[searchId].page].uuid);
const imageUrl = () => {
let res;
if (trueSkin.chromas[trueSkin.chromas.length-1].displayIcon) {
res = trueSkin.chromas[trueSkin.chromas.length-1].displayIcon
} else if (trueSkin.levels[trueSkin.levels.length-1].displayIcon) {
res = trueSkin.levels[trueSkin.levels.length-1].displayIcon
} else {
res = trueSkin.displayIcon
}
return res
};
const videoUrl = () => {
let res;
if (trueSkin.chromas[trueSkin.chromas.length-1].streamedVideo) {
res = trueSkin.chromas[trueSkin.chromas.length-1].streamedVideo
} else if (trueSkin.levels[trueSkin.levels.length-1].streamedVideo) {
res = trueSkin.levels[trueSkin.levels.length-1].streamedVideo
} else {
res = null
}
return res
};
const owner = await guild.members.fetch(activeSearchs[searchId].resultSkins[activeSearchs[searchId].page].user_id)
let fields = [
{
name: `**${activeSearchs[searchId].resultSkins[activeSearchs[searchId].page].displayName}** | ${activeSearchs[searchId].resultSkins[activeSearchs[searchId].page].tierText}`,
value: `${activeSearchs[searchId].resultSkins[activeSearchs[searchId].page].maxPrice}${activeSearchs[searchId].resultSkins[activeSearchs[searchId].page].user_id ? '| **@'+ owner.user.username +'** ✅' : ''}`,
inline: false,
}
]
try {
const originalComponents = req.body.message.components || [];
await DiscordRequest(
activeSearchs[searchId].endpoint,
{
method: 'PATCH',
body: {
embeds: [
{
title: `Résultat de recherche`,
description: `🔎 ${activeSearchs[searchId].searchValue}`,
fields: fields,
color: parseInt(activeSearchs[searchId].resultSkins[activeSearchs[searchId].page].tierColor, 16),
image: { url: imageUrl() },
footer: { text: `${activeSearchs[searchId].page+1}/${activeSearchs[searchId].resultSkins.length} résultat(s)` },
},
],
components: originalComponents,
},
}
);
} catch (err) {
console.log('Pas trouvé : ', err)
}
return res.send({
type: InteractionResponseType.DEFERRED_UPDATE_MESSAGE,
});
}
else if (componentId.startsWith('next_search_page')) {
let searchId = componentId.replace('next_search_page_', '');
const context = req.body.context;
// User ID is in user field for (G)DMs, and member for servers
const userId = context === 0 ? req.body.member.user.id : req.body.user.id;
const guild = await client.guilds.fetch(req.body.guild_id);
if (!activeSearchs[searchId]) {
return res.send({
type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE,
data: {
content: `Oups, cet affichage n'est plus actif.\nRelance la commande pour avoir un nouvel élément intéractif`,
flags: InteractionResponseFlags.EPHEMERAL,
},
});
}
const chromaText = (skin) => {
let res = ""
for (let i = 1; i <= skins.find((s) => s.uuid === skin.uuid).chromas.length; i++) {
res += skin.currentChroma === i ? '💠 ' : '◾ '
}
return res
}
const chromaName = (skin) => {
if (skin.currentChroma >= 2) {
const name = skins.find((s) => s.uuid === skin.uuid).chromas[skin.currentChroma-1].displayName.replace(/[\r\n]+/g, '').replace(skin.displayName, '')
const match = name.match(/variante\s+[1-4]\s+([^)]+)/)
const result = match ? match[2] : null;
if (match) {
return match[1].trim()
} else {
return name
}
}
if (skin.currentChroma === 1) {
return 'Base'
}
return ''
};
if (activeSearchs[searchId] && activeSearchs[searchId].userId === req.body.member.user.id) {
if (activeSearchs[searchId].page === activeSearchs[searchId].amount-1) {
activeSearchs[searchId].page = 0
} else {
activeSearchs[searchId].page++
}
} else {
return res.send({
type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE,
data: {
content: `Tu n'est pas à l'origine de cette commande /search`,
flags: InteractionResponseFlags.EPHEMERAL,
},
});
}
const trueSkin = skins.find((s) => s.uuid === activeSearchs[searchId].resultSkins[activeSearchs[searchId].page].uuid);
const imageUrl = () => {
let res;
if (trueSkin.chromas[trueSkin.chromas.length-1].displayIcon) {
res = trueSkin.chromas[trueSkin.chromas.length-1].displayIcon
} else if (trueSkin.levels[trueSkin.levels.length-1].displayIcon) {
res = trueSkin.levels[trueSkin.levels.length-1].displayIcon
} else {
res = trueSkin.displayIcon
}
return res
};
const videoUrl = () => {
let res;
if (trueSkin.chromas[trueSkin.chromas.length-1].streamedVideo) {
res = trueSkin.chromas[trueSkin.chromas.length-1].streamedVideo
} else if (trueSkin.levels[trueSkin.levels.length-1].streamedVideo) {
res = trueSkin.levels[trueSkin.levels.length-1].streamedVideo
} else {
res = null
}
return res
};
const owner = await guild.members.fetch(activeSearchs[searchId].resultSkins[activeSearchs[searchId].page].user_id)
let fields = [
{
name: `**${activeSearchs[searchId].resultSkins[activeSearchs[searchId].page].displayName}** | ${activeSearchs[searchId].resultSkins[activeSearchs[searchId].page].tierText}`,
value: `${activeSearchs[searchId].resultSkins[activeSearchs[searchId].page].maxPrice}${activeSearchs[searchId].resultSkins[activeSearchs[searchId].page].user_id ? '| **@'+ owner.user.username +'** ✅' : ''}`,
inline: false,
}
]
try {
const originalComponents = req.body.message.components || [];
await DiscordRequest(
activeSearchs[searchId].endpoint,
{
method: 'PATCH',
body: {
embeds: [
{
title: `Résultat de recherche`,
description: `🔎 ${activeSearchs[searchId].searchValue}`,
fields: fields,
color: parseInt(activeSearchs[searchId].resultSkins[activeSearchs[searchId].page].tierColor, 16),
image: { url: imageUrl() },
footer: { text: `${activeSearchs[searchId].page+1}/${activeSearchs[searchId].resultSkins.length} résultat(s)` },
},
],
components: originalComponents,
},
}
);
} catch (err) {
console.log('Pas trouvé : ', err)
}
return res.send({
type: InteractionResponseType.DEFERRED_UPDATE_MESSAGE,
});
}
else if (componentId.startsWith('option_')) {
const optionId = parseInt(componentId.replace('option_', '')[0]);
const prediId = componentId.replace(`option_${optionId}_`, '');
let intAmount = 10;
const commandUserId = req.body.member.user.id
const commandUser = getUser.get(commandUserId);
if (!commandUser) return res.status(403).send({ message: 'Oups, je ne te connais pas'})
if (commandUser.coins < intAmount) {
return res.send({
type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE,
data: {
content: 'Tu n\'as pas assez de FlopoCoins',
flags: InteractionResponseFlags.EPHEMERAL,
},
});
}
const prediObject = activePredis[prediId]
if (!prediObject) {
return res.send({
type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE,
data: {
content: 'Prédiction introuvable',
flags: InteractionResponseFlags.EPHEMERAL,
},
});
}
if (prediObject.endTime < Date.now()) {
return res.send({
type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE,
data: {
content: 'Les votes de cette prédiction sont clos',
flags: InteractionResponseFlags.EPHEMERAL,
},
});
}
const otherOption = optionId === 0 ? 1 : 0;
if (prediObject.options[otherOption].votes.find(v => v.id === commandUserId) && commandUserId !== process.env.DEV_ID) {
return res.send({
type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE,
data: {
content: 'Tu ne peux pas voter pour les 2 deux options',
flags: InteractionResponseFlags.EPHEMERAL,
},
});
}
let stopMaxCoins = false
if (prediObject.options[optionId].votes.find(v => v.id === commandUserId)) {
activePredis[prediId].options[optionId].votes.forEach(v => {
if (v.id === commandUserId) {
if (v.amount >= 250000) {
stopMaxCoins = true
return
}
if (v.amount + intAmount > 250000) {
intAmount = 250000-v.amount
}
v.amount += intAmount
}
})
} else {
activePredis[prediId].options[optionId].votes.push({
id: commandUserId,
amount: intAmount,
})
}
if (stopMaxCoins) {
return res.send({
type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE,
data: {
content: 'Tu as déjà parié le max (250K)',
flags: InteractionResponseFlags.EPHEMERAL,
},
});
}
activePredis[prediId].options[optionId].total += intAmount
activePredis[prediId].options[optionId].percent = (activePredis[prediId].options[optionId].total / (activePredis[prediId].options[otherOption].total + activePredis[prediId].options[optionId].total)) * 100
activePredis[prediId].options[otherOption].percent = 100 - activePredis[prediId].options[optionId].percent
io.emit('new-predi', { action: 'new vote' });
updateUserCoins.run({
id: commandUserId,
coins: commandUser.coins - intAmount,
})
insertLog.run({
id: commandUserId + '-' + Date.now(),
user_id: commandUserId,
action: 'PREDI_VOTE',
target_user_id: null,
coins_amount: -intAmount,
user_new_amount: commandUser.coins - intAmount,
})
io.emit('data-updated', { table: 'users', action: 'update' });
try {
const totalAmount = activePredis[prediId].options[optionId].votes.find(v => v.id === commandUserId)?.amount;
const optionLabel = activePredis[prediId].options[optionId].label;
return res.send({
type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE,
data: {
content: `Vote enregistré, **${intAmount}** Flopocoins sur **"${optionLabel}"** (**${totalAmount}** au total)`,
flags: InteractionResponseFlags.EPHEMERAL,
},
});
} catch (err) {
console.log('Pas trouvé : ', err)
return res.send({
type: InteractionResponseType.DEFERRED_UPDATE_MESSAGE,
});
}
}
return;
}
console.error('unknown interaction type', type);
return res.status(400).json({ error: 'unknown interaction type' });
});
// Check flAPI
app.get('/check', (req, res) => {
res.status(200).json({ check: true, status: 'OK' });
});
// Get all users ordered by coins
app.get('/users', (req, res) => {
const users = getAllUsers.all();
res.json(users);
});
app.get('/users/by-elo', (req, res) => {
const users = getUsersByElo.all()
res.json(users);
})
app.get('/logs', (req, res) => {
return res.status(200).json(getLogs.all())
})
app.post('/timedout', async (req, res) => {
const { userId } = req.body
const guild = await client.guilds.fetch(process.env.GUILD_ID);
let member;
try {
member = await guild.members.fetch(userId);
} catch (e) {
return res.status(404).send({ message: 'Unknown member' })
}
return res.status(200).json({ isTimedOut: member?.communicationDisabledUntilTimestamp > Date.now()})
})
// Get user's avatar
app.get('/user/:id/avatar', async (req, res) => {
try {
const userId = req.params.id; // Get the ID from route parameters
const user = await client.users.fetch(userId);
if (!user) {
return res.status(404).json({ error: 'User not found' });
}
const avatarUrl = user.displayAvatarURL({ format: 'png', size: 256 });
res.json({ avatarUrl });
} catch (error) {
console.error('Error fetching user avatar');
res.status(500).json({ error: 'Failed to fetch avatar' });
}
})
app.get('/user/:id/sparkline', async (req, res) => {
try {
const userId = req.params.id
const user = getUser.get(userId)
if (!user) return res.status(404).send({ message: 'Utilisateur introuvable'})
return res.status(200).json({ sparkline: getUserLogs.all({user_id: userId}) })
} catch (e) {
return res.status(500).send({ message: 'erreur'})
}
})
app.get('/user/:id/elo', async (req, res) => {
try {
const userId = req.params.id
const user = getUser.get(userId)
if (!user) return res.status(404).send({ message: 'Utilisateur introuvable'})
const userElo = getUserElo.get({ id: userId })
if (!userElo) return res.status(200).json({ elo: null })
return res.status(200).json({ elo: userElo.elo })
} catch (e) {
return res.status(500).send({ message: 'erreur'})
}
})
app.get('/user/:id/elo-graph', async (req, res) => {
try {
const userId = req.params.id
const user = getUser.get(userId)
if (!user) return res.status(404).send({ message: 'Utilisateur introuvable'})
const games = getUserGames.all({ user_id: userId });
if (!games) return res.status(404).send({ message: 'Aucune partie'})
let array = []
games.forEach((game, index) => {
if (game.p1 === userId) {
array.push(game.p1_elo)
if (index === games.length - 1) array.push(game.p1_new_elo)
} else if (game.p2 === userId) {
array.push(game.p2_elo)
if (index === games.length - 1) array.push(game.p2_new_elo)
}
})
return res.status(200).json({ elo_graph: array })
} catch (e) {
return res.status(500).send({ message: 'erreur'})
}
})
// Get user's inventory
app.get('/user/:id/inventory', async (req, res) => {
try {
const userId = req.params.id; // Get the ID from route parameters
const user = await client.users.fetch(userId);
if (!user) {
return res.status(404).json({ error: 'User not found' });
}
const inventory = getUserInventory.all({user_id: userId});
res.json({ inventory });
} catch (error) {
console.error('Error fetching user avatar');
res.status(500).json({ error: 'Failed to fetch inventory' });
}
})
// Get active polls
app.get('/polls', async (req, res) => {
try {
res.json({ activePolls });
} catch (error) {
console.error('Error fetching active polls');
res.status(500).json({ error: 'Failed to fetch active polls' });
}
})
// Send a custom message in the admin command channel
app.post('/send-message', (req, res) => {
const { userId, channelId, message } = req.body;
const channel = client.channels.cache.get(channelId);
const user = getUser.get(userId);
if (!user) return res.status(404).json({ error: 'User not found' });
if (!channel) return res.status(404).json({ error: 'Channel not found' });
if (user.coins < 10) return res.status(403).json({ error: 'Pas assez de coins' });
updateUserCoins.run({
id: userId,
coins: user.coins - 10,
})
insertLog.run({
id: userId + '-' + Date.now(),
user_id: userId,
action: 'SEND_MESSAGE',
target_user_id: null,
coins_amount: -10,
user_new_amount: user.coins - 10,
})
io.emit('data-updated', { table: 'users', action: 'update' });
channel.send(message)
.then(() => res.json({ success: true }))
.catch(err => res.status(500).json({ error: err.message }));
});
// Change user's server specific username
app.post('/change-nickname', async (req, res) => {
const { userId, nickname, commandUserId } = req.body;
const commandUser = getUser.get(commandUserId);
if (!commandUser) return res.status(404).json({ message: 'Oups petit soucis' });
if (commandUser.coins < 1000) return res.status(403).json({ message: 'Pas assez de coins' });
try {
const guild = await client.guilds.fetch(process.env.GUILD_ID);
const member = await guild.members.fetch(userId);
await member.setNickname(nickname);
let message = nickname ? `Le pseudo de '${member.user.tag}' a été changé en '${nickname}'` : `Le pseudo de '${member.user.tag}' a été remis par défaut`
res.status(200).json({ message : message });
updateUserCoins.run({
id: commandUserId,
coins: commandUser.coins - 1000,
})
insertLog.run({
id: commandUserId + '-' + Date.now(),
user_id: commandUserId,
action: 'CHANGE_NICKNAME',
target_user_id: userId,
coins_amount: -1000,
user_new_amount: commandUser.coins - 1000,
})
io.emit('data-updated', { table: 'users', action: 'update' });
} catch (error) {
res.status(500).json({ message : `J'ai pas réussi à changer le pseudo` });
}
})
app.post('/spam-ping', async (req, res) => {
const { userId, commandUserId } = req.body;
const user = getUser.get(userId);
const commandUser = getUser.get(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' });
try {
const discordUser = await client.users.fetch(userId);
await discordUser.send(`<@${userId}>`)
res.status(200).json({ message : 'C\'est parti ehehe' });
updateUserCoins.run({
id: commandUserId,
coins: commandUser.coins - 10000,
})
insertLog.run({
id: commandUserId + '-' + Date.now(),
user_id: commandUserId,
action: 'SPAM_PING',
target_user_id: userId,
coins_amount: -10000,
user_new_amount: commandUser.coins - 10000,
})
io.emit('data-updated', { table: 'users', action: 'update' });
for (let i = 0; i < 29; i++) {
await discordUser.send(`<@${userId}>`)
await sleep(1000);
}
} catch (err) {
console.log(err)
res.status(500).json({ message : "Oups ça n'a pas marché" });
}
})
app.post('/timeout/vote', async (req, res) => {
const { commandUserId, voteKey, voteFor } = req.body;
const commandUser = getUser.get(commandUserId);
const poll = activePolls[voteKey];
const isVotingFor = voteFor;
if (!commandUser) return res.status(404).json({ message: 'Oups petit soucis' });
if (!poll) return res.status(404).json({ message: 'Vote de timeout introuvable' });
if (activePolls[voteKey]) {
const poll = activePolls[voteKey];
poll.voters = poll.voters || [];
const voterId = commandUserId;
const guild = await client.guilds.fetch(process.env.GUILD_ID)
const commandMember = await guild.members.fetch(commandUserId);
// Check if the voter has the required voting role
const voterRoles = commandMember.roles.cache.map(role => role.id) || [];
if (!voterRoles.includes(process.env.VOTING_ROLE_ID)) {
return res.status(403).json({ message: 'Tu n\'as pas le rôle requis pour voter'})
}
// Enforce one vote per eligible user
if (poll.voters.find(u => u === voterId)) {
return res.status(403).json({ message: 'Tu as déjà voté'})
}
// Record the vote
poll.voters.push(voterId);
if (isVotingFor) {
poll.for++;
} else {
poll.against++;
}
io.emit('new-poll', { action: 'new vote' });
// Retrieve online eligible users (ensure your bot has the necessary intents)
const guildId = process.env.GUILD_ID;
const roleId = process.env.VOTING_ROLE_ID; // Set this in your .env file
const onlineEligibleUsers = await getOnlineUsersWithRole(guildId, roleId);
const votesNeeded = Math.max(0, poll.requiredMajority - poll.for);
// Check if the majority is reached
if (poll.for >= poll.requiredMajority) {
try {
// Build the updated poll message content
await DiscordRequest(
poll.endpoint,
{
method: 'PATCH',
body: {
embeds: [
{
title: `Timeout`,
description: `Proposition de timeout **${poll.toUsername}** pendant ${poll.time_display}`,
fields: [
{
name: 'Votes totaux',
value: '✅ ' + poll.for,
inline: true,
},
],
color: 0xF2F3F3, // You can set the color of the embed
},
],
components: [], // remove buttons
},
}
);
} catch (err) {
console.error('Error updating poll message:', err);
}
// Clear the poll so the setTimeout callback doesn't fire later
delete activePolls[voteKey];
// **Actual Timeout Action**
try {
// Calculate the ISO8601 timestamp to disable communications until now + poll.time seconds
const timeoutUntil = new Date(Date.now() + poll.time * 1000).toISOString();
const endpointTimeout = `guilds/${process.env.GUILD_ID}/members/${poll.toUserId}`;
await DiscordRequest(endpointTimeout, {
method: 'PATCH',
body: { communication_disabled_until: timeoutUntil },
});
return res.send({
type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE,
data: {
content: `<@${poll.toUserId}> a été timeout pendant ${poll.time_display} par décision démocratique 👊`,
},
});
} catch (err) {
console.error('Error timing out user:', err);
return res.send({
type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE,
data: {
content: `Impossible de timeout <@${poll.toUserId}>, désolé... 😔`,
},
});
}
}
// If the vote is "for", update the original poll message to reflect the new vote count.
if (isVotingFor) {
const remaining = Math.max(0, Math.floor((poll.endTime - Date.now()) / 1000));
const minutes = Math.floor(remaining / 60);
const seconds = remaining % 60;
const countdownText = `**${minutes}m ${seconds}s** restantes`;
try {
// Build the updated poll message content
await DiscordRequest(
poll.endpoint,
{
method: 'PATCH',
body: {
embeds: [
{
title: `Timeout`,
description: `**${poll.username}** propose de timeout **${poll.toUsername}** pendant ${poll.time_display}\nIl manque **${votesNeeded}** vote(s)`,
fields: [
{
name: 'Pour',
value: '✅ ' + poll.for,
inline: true,
},
{
name: 'Temps restant',
value: '⏳ ' + countdownText,
inline: false,
},
],
color: 0xF2F3F3, // You can set the color of the embed
},
],
components: req.body.message.components, // preserve the buttons
},
}
);
} catch (err) {
console.error('Error updating poll message:', err);
}
}
return res.status(200).json({ message: 'Vote enregistré !'})
}
})
app.post('/slowmode', async (req, res) => {
let { userId, commandUserId} = req.body
const user = getUser.get(userId)
const commandUser = getUser.get(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];
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,
};
io.emit('new-slowmode', { action: 'new slowmode' });
updateUserCoins.run({
id: commandUserId,
coins: commandUser.coins - 10000,
})
insertLog.run({
id: commandUserId + '-' + Date.now(),
user_id: commandUserId,
action: 'SLOWMODE',
target_user_id: userId,
coins_amount: -10000,
user_new_amount: commandUser.coins - 10000,
})
io.emit('data-updated', { table: 'users', action: 'update' });
return res.status(200).json({ message: `${user.globalName} est maintenant en slowmode pour 1h`})
})
app.get('/slowmodes', async (req, res) => {
res.status(200).json({ slowmodes: activeSlowmodes });
})
app.post('/start-predi', async (req, res) => {
let { commandUserId, label, options, closingTime, payoutTime } = req.body
const commandUser = getUser.get(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 = guild.channels.cache.find(
ch => ch.name === 'général' || ch.name === 'general'
);
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) {
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,
};
io.emit('new-predi', { action: 'new predi' });
updateUserCoins.run({
id: commandUserId,
coins: commandUser.coins - 100,
})
insertLog.run({
id: commandUserId + '-' + Date.now(),
user_id: commandUserId,
action: 'START_PREDI',
target_user_id: null,
coins_amount: -100,
user_new_amount: commandUser.coins - 100,
})
io.emit('data-updated', { table: 'users', action: 'update' });
return res.status(200).json({ message: `Ta prédi '${label}' a commencée !`})
})
app.get('/predis', async (req, res) => {
const reversedPredis = Object.entries(activePredis).reverse();
const openEntries = [];
const closedEntries = [];
for (const [key, value] of reversedPredis) {
if (value.closed === true) {
closedEntries.push([key, value]);
} else {
openEntries.push([key, value]);
}
}
const reorderedPredis = Object.fromEntries([...openEntries, ...closedEntries]);
res.status(200).json({ predis: reorderedPredis });
});
app.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 = getUser.get(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
io.emit('new-predi', { action: 'new vote' });
updateUserCoins.run({
id: commandUserId,
coins: commandUser.coins - intAmount,
})
insertLog.run({
id: commandUserId + '-' + Date.now(),
user_id: commandUserId,
action: 'PREDI_VOTE',
target_user_id: null,
coins_amount: -intAmount,
user_new_amount: commandUser.coins - intAmount,
})
io.emit('data-updated', { table: 'users', action: 'update' });
return res.status(200).send({ message : `Vote enregistré!` });
})
app.post('/end-predi', async (req, res) => {
const { commandUserId, predi, confirm, winningOption } = req.body
const commandUser = getUser.get(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();
activePredis[predi].options[0].votes.forEach((v) => {
const tempUser = getUser.get(v.id)
try {
updateUserCoins.run({
id: v.id,
coins: tempUser.coins + v.amount
})
insertLog.run({
id: v.id + '-' + Date.now(),
user_id: v.id,
action: 'PREDI_REFUND',
target_user_id: v.id,
coins_amount: v.amount,
user_new_amount: tempUser.coins + v.amount,
})
} catch (e) {
console.log(`Impossible de rembourser ${v.id} (${v.amount} coins)`)
}
})
activePredis[predi].options[1].votes.forEach((v) => {
const tempUser = getUser.get(v.id)
try {
updateUserCoins.run({
id: v.id,
coins: tempUser.coins + v.amount
})
insertLog.run({
id: v.id + '-' + Date.now(),
user_id: v.id,
action: 'PREDI_REFUND',
target_user_id: v.id,
coins_amount: v.amount,
user_new_amount: tempUser.coins + v.amount,
})
} catch (e) {
console.log(`Impossible de rembourser ${v.id} (${v.amount} coins)`)
}
})
activePredis[predi].closed = true;
}
else {
const losingOption = winningOption === 0 ? 1 : 0;
activePredis[predi].options[winningOption].votes.forEach((v) => {
const tempUser = getUser.get(v.id)
const ratio = activePredis[predi].options[winningOption].total === 0 ? 0 : activePredis[predi].options[losingOption].total / activePredis[predi].options[winningOption].total
try {
updateUserCoins.run({
id: v.id,
coins: tempUser.coins + (v.amount * (1 + ratio))
})
insertLog.run({
id: v.id + '-' + Date.now(),
user_id: v.id,
action: 'PREDI_RESULT',
target_user_id: v.id,
coins_amount: v.amount * (1 + ratio),
user_new_amount: tempUser.coins + (v.amount * (1 + ratio)),
})
} 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 = guild.channels.cache.find(
ch => ch.name === 'général' || ch.name === 'general'
);
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);
}
io.emit('new-predi', { action: 'closed predi' });
io.emit('data-updated', { table: 'users', action: 'fin predi' });
return res.status(200).json({ message: 'Prédi close' });
})
// ADMIN Add coins
app.post('/add-coins', (req, res) => {
const { commandUserId } = req.body;
const commandUser = getUser.get(commandUserId);
if (!commandUser) return res.status(404).json({ error: 'User not found' });
if (commandUserId !== process.env.DEV_ID) return res.status(404).json({ error: 'Not admin' });
updateUserCoins.run({
id: commandUserId,
coins: commandUser.coins + 1000,
})
insertLog.run({
id: commandUserId + '-' + Date.now(),
user_id: commandUserId,
action: 'ADD_COINS',
target_user_id: commandUserId,
coins_amount: 1000,
user_new_amount: commandUser.coins + 1000,
})
io.emit('data-updated', { table: 'users', action: 'update' });
res.status(200).json({ message : `+1000` });
});
app.post('/buy-coins', (req, res) => {
const { commandUserId, coins } = req.body;
const commandUser = getUser.get(commandUserId);
if (!commandUser) return res.status(404).json({ error: 'User not found' });
updateUserCoins.run({
id: commandUserId,
coins: commandUser.coins + coins,
})
insertLog.run({
id: commandUserId + '-' + Date.now(),
user_id: commandUserId,
action: 'ADD_COINS',
target_user_id: commandUserId,
coins_amount: coins,
user_new_amount: commandUser.coins + coins,
})
io.emit('data-updated', { table: 'users', action: 'update' });
res.status(200).json({ message : `+${coins}` });
});
const pokerRooms = {}
app.post('/create-poker-room', async (req, res) => {
const { creatorId } = req.body
const id = uuidv4()
const t12names = [
'cassoule',
'passoule',
'kiwiko',
'piwiko',
'wata',
'pata',
'apologize',
'apologay',
'daspoon',
'esteban',
'edorima',
'momozhok',
'popozhok',
'dodozhok',
'flopozhok',
'thomas',
'poma'
]
const name = uniqueNamesGenerator({ dictionaries: [adjectives, t12names], separator: ' ', style: 'capital' });
const creator = await client.users.fetch(creatorId)
if (!creator) {
return res.status(404).send({message: 'Utilisateur introuvable'})
}
if (Object.values(pokerRooms).find(room => room.host_id === creatorId)) {
return res.status(403).send({message: 'Tu ne peux créer qu\'une seule table à la fois'})
}
pokerRooms[id] = {
id: id,
host_id: creatorId,
host_name: creator.globalName,
name: name,
created_at: Date.now(),
last_move_at: Date.now(),
players: {}
}
io.emit('new-poker-room')
return res.status(200).send({ roomId: id })
});
app.get('/poker-rooms', (req, res) => {
return res.status(200).send({ rooms: pokerRooms })
})
app.get('/poker-rooms/:id', (req, res) => {
return res.status(200).send({ room: pokerRooms[req.params.id] })
})
app.post('/poker-room/join', async (req, res) => {
const { userId, roomId } = req.body
const user = await client.users.fetch(userId)
try {
pokerRooms[roomId].players[userId] = user
} catch (e) {
//
}
io.emit('player-joined')
});
import http from 'http';
import { Server } from 'socket.io';
const server = http.createServer(app);
const io = new Server(server, {
cors: {
Origin: FLAPI_URL,
methods: ['GET', 'POST', 'PUT', 'OPTIONS'],
}
});
let queue = []
let playingArray = []
io.on('connection', (socket) => {
socket.on('user-connected', async (user) => {
const username = getUser.get(user)
console.log(`user connected: ${username?.username ?? '-'}`);
queue = queue.filter(obj => obj !== user)
let names = [];
for (const n of queue) {
let name = await client.users.fetch(n)
names.push(name?.username)
}
io.emit('tictactoequeue', { allPlayers: playingArray, queue: names })
})
socket.on('tictactoeconnection', async (e) => {
queue = queue.filter(obj => obj !== e.id)
let names = [];
for (const n of queue) {
let name = await client.users.fetch(n)
names.push(name?.username)
}
io.emit('tictactoequeue', { allPlayers: playingArray, queue: names })
})
socket.on('tictactoequeue', async (e) => {
console.log(`${e.playerId} in tic tac toe queue`);
let msgId;
if (!queue.find(obj => obj === e.playerId)) {
queue.push(e.playerId)
if (queue.length === 1) {
try {
const guild = await client.guilds.fetch(process.env.GUILD_ID);
const generalChannel = guild.channels.cache.find(
ch => ch.name === 'général' || ch.name === 'general'
);
const user = await client.users.fetch(e.playerId)
const embed = new EmbedBuilder()
.setTitle(`Tic Tac Toe`)
.setDescription(`**${user.username}** est dans la file d'attente`)
.setColor('#5865f2')
.setTimestamp(new Date());
const row = new ActionRowBuilder()
.addComponents(
new ButtonBuilder()
.setLabel(`Jouer contre ${user.username}`)
.setURL(`${process.env.DEV_SITE === 'true' ? process.env.FLAPI_URL_DEV : process.env.FLAPI_URL}/tic-tac-toe`)
.setStyle(ButtonStyle.Link)
)
await generalChannel.send({ embeds: [embed], components: [row] });
} catch (e) {
console.log(e)
}
}
}
if (queue.length >= 2) {
let p1 = await client.users.fetch(queue[0])
let p2 = await client.users.fetch(queue[1])
let msgId
try {
const guild = await client.guilds.fetch(process.env.GUILD_ID);
const generalChannel = guild.channels.cache.find(
ch => ch.name === 'général' || ch.name === 'general'
);
const embed = new EmbedBuilder()
.setTitle(`Tic Tac Toe`)
.setDescription(`### **❌ ${p1.globalName}** vs **${p2.globalName} ⭕**\n` +
`🟦🟦🟦\n` +
`🟦🟦🟦\n` +
`🟦🟦🟦\n`)
.setColor('#5865f2')
.setTimestamp(new Date());
const msg = await generalChannel.send({ embeds: [embed] });
msgId = msg.id
} catch (e) {
console.log(e)
}
let p1obj = {
id: queue[0],
name: p1.globalName,
val: 'X',
move: "",
}
let p2obj = {
id: queue[1],
name: p2.globalName,
val: 'O',
move: "",
}
let lobby = {
p1: p1obj,
p2: p2obj,
sum: 1,
xs: [],
os: [],
lastmove: Date.now(),
msgId: msgId,
}
playingArray.push(lobby)
queue.splice(0, 2)
}
let names = [];
for (const n of queue) {
let name = await client.users.fetch(n)
names.push(name?.globalName)
}
io.emit('tictactoequeue', { allPlayers: playingArray, queue: names })
})
socket.on('tictactoeplaying', async (e) => {
let lobbyToChange;
if (e.value === 'X') {
lobbyToChange = playingArray.find(obj => obj.p1.id === e.playerId)
lobbyToChange.p2.move = ''
lobbyToChange.p1.move = e.boxId
lobbyToChange.sum++
lobbyToChange.xs.push(e.boxId)
lobbyToChange.lastmove = Date.now()
}
else if (e.value === 'O') {
lobbyToChange = playingArray.find(obj => obj.p2.id === e.playerId)
lobbyToChange.p1.move = ''
lobbyToChange.p2.move = e.boxId
lobbyToChange.sum++
lobbyToChange.os.push(e.boxId)
lobbyToChange.lastmove = Date.now()
}
let gridText = ''
for (let i = 1; i <= 9; i++) {
if (lobbyToChange.os.includes(i)) {
gridText += '⭕'
} else if (lobbyToChange.xs.includes(i)) {
gridText += '❌'
} else {
gridText += '🟦'
}
if (i%3 === 0) {
gridText += '\n'
}
}
try {
const guild = await client.guilds.fetch(process.env.GUILD_ID);
const generalChannel = await guild.channels.cache.find(
ch => ch.name === 'général' || ch.name === 'general'
);
const message = await generalChannel.messages.fetch(lobbyToChange.msgId)
const embed = new EmbedBuilder()
.setTitle(`Tic Tac Toe`)
.setDescription(`### **❌ ${lobbyToChange.p1.name}** vs **${lobbyToChange.p2.name} ⭕**\n` + gridText)
.setColor('#5865f2')
.setTimestamp(new Date());
await message.edit({ embeds: [embed] });
} catch (e) {
console.log(e)
}
io.emit('tictactoeplaying', { allPlayers: playingArray })
})
socket.on('tictactoegameOver', async (e) => {
const winner = e.winner
const game = playingArray.find(obj => obj.p1.id === e.playerId)
if (game) {
let gridText = ''
for (let i = 1; i <= 9; i++) {
if (game.os.includes(i)) {
gridText += '⭕'
} else if (game.xs.includes(i)) {
gridText += '❌'
} else {
gridText += '🟦'
}
if (i%3 === 0) {
gridText += '\n'
}
}
if (winner === null) {
await eloHandler(game.p1.id, game.p2.id, 0, 0, 'TICTACTOE')
try {
const guild = await client.guilds.fetch(process.env.GUILD_ID);
const generalChannel = await guild.channels.cache.find(
ch => ch.name === 'général' || ch.name === 'general'
);
const message = await generalChannel.messages.fetch(game.msgId)
const embed = new EmbedBuilder()
.setTitle(`Tic Tac Toe`)
.setDescription(`### **❌ ${game.p1.name}** vs **${game.p2.name} ⭕**\n` + gridText + `\n### Égalité`)
.setColor('#5865f2')
.setTimestamp(new Date());
await message.edit({ embeds: [embed] });
} catch (e) {
console.log(e)
}
} else {
await eloHandler(game.p1.id, game.p2.id, game.p1.id === winner ? 1 : 0, game.p2.id === winner ? 1 : 0, 'TICTACTOE')
try {
const guild = await client.guilds.fetch(process.env.GUILD_ID);
const generalChannel = await guild.channels.cache.find(
ch => ch.name === 'général' || ch.name === 'general'
);
const message = await generalChannel.messages.fetch(game.msgId)
const embed = new EmbedBuilder()
.setTitle(`Tic Tac Toe`)
.setDescription(`### **❌ ${game.p1.name}** vs **${game.p2.name} ⭕**\n` + gridText + `\n### Victoire de ${game.p1.id === winner ? game.p1.name : game.p2.name}`)
.setColor('#5865f2')
.setTimestamp(new Date());
await message.edit({ embeds: [embed] });
} catch (e) {
console.log(e)
}
}
}
playingArray = playingArray.filter(obj => obj.p1.id !== e.playerId)
})
});
server.listen(PORT, () => {
console.log(`Express+Socket.IO listening on port ${PORT}`);
});