Files
flopobot_v2/src/bot/handlers/messageCreate.js
2026-02-27 17:20:23 +01:00

481 lines
17 KiB
JavaScript

import { sleep } from "openai/core";
import { AttachmentBuilder } from "discord.js";
import {
buildAiMessages,
buildParticipantsMap,
buildTranscript,
CONTEXT_LIMIT,
gork,
INCLUDE_ATTACHMENT_URLS,
MAX_ATTS_PER_MESSAGE,
stripMentionsOfBot,
} from "../../utils/ai.js";
import { calculateBasePrice, calculateMaxPrice, formatTime, getAkhys } from "../../utils/index.js";
import { channelPointsHandler, initTodaysSOTD, randomSkinPrice, slowmodesHandler } from "../../game/points.js";
import { activePolls, activeSlowmodes, requestTimestamps, skins } from "../../game/state.js";
import prisma from "../../prisma/client.js";
import * as userService from "../../services/user.service.js";
import * as skinService from "../../services/skin.service.js";
import * as logService from "../../services/log.service.js";
import { client } from "../client.js";
import { drawCaseContent, drawCaseSkin, getDummySkinUpgradeProbs } from "../../utils/caseOpening.js";
import { fetchSuggestedPrices, fetchSkinsData } from "../../api/cs.js";
import { csSkinsData, csSkinsPrices } from "../../utils/cs.state.js";
import { getRandomSkinWithRandomSpecs } from "../../utils/cs.utils.js";
// Constants for the AI rate limiter
const MAX_REQUESTS_PER_INTERVAL = parseInt(process.env.MAX_REQUESTS || "5");
const SPAM_INTERVAL = parseInt(process.env.SPAM_INTERVAL || "60000"); // 60 seconds default
/**
* Handles all logic for when a message is created.
* @param {object} message - The Discord.js message object.
* @param {object} client - The Discord.js client instance.
* @param {object} io - The Socket.IO server instance.
*/
export async function handleMessageCreate(message, client, io) {
// Ignore all messages from bots to prevent loops
if (message.author.bot) return;
// --- Specific User Gags ---
if (message.author.id === process.env.PATA_ID) {
if (message.content.toLowerCase().startsWith("feur") || message.content.toLowerCase().startsWith("rati")) {
await sleep(1000);
await message.delete().catch(console.error);
}
}
// --- Main Guild Features (Points & Slowmode) ---
if (message.guildId === process.env.GUILD_ID) {
// Award points for activity
// const pointsAwarded = channelPointsHandler(message);
// if (pointsAwarded) {
// io.emit("data-updated", { table: "users", action: "update" });
// }
// Enforce active slowmodes
const wasSlowmoded = await slowmodesHandler(message, activeSlowmodes);
if (wasSlowmoded.deleted) {
io.emit("slowmode-update");
}
}
// --- AI Mention Handler ---
if (message.mentions.has(client.user) || message.mentions.repliedUser?.id === client.user.id) {
await handleAiMention(message, client, io);
return; // Stop further processing after AI interaction
}
// --- "Quoi/Feur" Gag ---
if (message.content.toLowerCase().includes("quoi")) {
const prob = Math.random();
if (prob < (parseFloat(process.env.FEUR_PROB) || 0.05)) {
message.channel.send("feur").catch(console.error);
}
return;
}
// --- Admin/Dev Guild Commands ---
if (message.guildId === process.env.DEV_GUILD_ID && message.author.id === process.env.DEV_ID) {
await handleAdminCommands(message);
}
}
// --- Sub-handler for AI Logic ---
async function handleAiMention(message, client, io) {
const authorId = message.author.id;
let authorDB = await userService.getUser(authorId);
if (!authorDB) return; // Should not happen if user is in DB, but good practice
// --- Rate Limiting ---
const now = Date.now();
const timestamps = (requestTimestamps.get(authorId) || []).filter((ts) => now - ts < SPAM_INTERVAL);
if (timestamps.length >= MAX_REQUESTS_PER_INTERVAL) {
console.log(`Rate limit exceeded for ${authorDB.username}`);
if (!authorDB.warned) {
await message.reply(`T'abuses fréro, attends un peu ⏳`).catch(console.error);
}
// Update user's warn status
authorDB.warned = 1;
authorDB.warns += 1;
authorDB.allTimeWarns += 1;
await userService.updateManyUsers([authorDB]);
// Apply timeout if warn count is too high
if (authorDB.warns > (parseInt(process.env.MAX_WARNS) || 10)) {
try {
const member = await message.guild.members.fetch(authorId);
const time = parseInt(process.env.SPAM_TIMEOUT_TIME);
await member.timeout(time, "Spam excessif du bot AI.");
message.channel
.send(
`Ce bouffon de <@${authorId}> a été timeout pendant ${formatTime(time / 1000)}, il me cassait les couilles 🤫`,
)
.catch(console.error);
} catch (e) {
console.error("Failed to apply timeout for AI spam:", e);
message.channel
.send(`<@${authorId}>, tu as de la chance que je ne puisse pas te timeout...`)
.catch(console.error);
}
}
return;
}
timestamps.push(now);
requestTimestamps.set(authorId, timestamps);
// Reset warns if user is behaving, and increment their request count
authorDB.warned = 0;
authorDB.warns = 0;
authorDB.totalRequests += 1;
await userService.updateManyUsers([authorDB]);
// --- AI Processing ---
try {
await message.channel.sendTyping();
// 1) Récup contexte
const fetched = await message.channel.messages.fetch({
limit: Math.min(CONTEXT_LIMIT, 100),
});
const messagesArray = Array.from(fetched.values()).reverse(); // oldest -> newest
const requestText = stripMentionsOfBot(message.content, client.user.id);
const invokerId = message.author.id;
const invokerName = message.member?.nickname || message.author.globalName || message.author.username;
const repliedUserId = message.mentions?.repliedUser?.id || null;
// 2) Compact transcript & participants
const participants = buildParticipantsMap(messagesArray);
const transcript = buildTranscript(messagesArray, client.user.id);
const invokerAttachments = Array.from(message.attachments?.values?.() || [])
.slice(0, MAX_ATTS_PER_MESSAGE)
.map((a) => ({
id: a.id,
name: a.name,
type: a.contentType || "application/octet-stream",
size: a.size,
isImage: !!(a.contentType && a.contentType.startsWith("image/")),
url: INCLUDE_ATTACHMENT_URLS ? a.url : undefined,
}));
// 3) Construire prompts
const messageHistory = buildAiMessages({
botId: client.user.id,
botName: "FlopoBot",
invokerId,
invokerName,
requestText,
transcript,
participants,
repliedUserId,
invokerAttachments,
});
// 4) Appel modèle
const reply = await gork(messageHistory);
// 5) Réponse
await message.reply(reply);
} catch (err) {
console.error("Error processing AI mention:", err);
await message.reply("Oups, mon cerveau a grillé. Réessaie plus tard.").catch(console.error);
}
}
// --- Sub-handler for Admin Commands ---
async function handleAdminCommands(message) {
const prefix = process.env.DEV_SITE === "true" ? "dev" : "flopo";
const [command, ...args] = message.content.split(" ");
switch (command) {
case "?sp":
let msgText = "";
for (let skinTierRank = 1; skinTierRank <= 4; skinTierRank++) {
msgText += `\n--- Tier Rank: ${skinTierRank} ---\n`;
let skinMaxLevels = 4;
let skinMaxChromas = 4;
for (let skinLevel = 1; skinLevel < skinMaxLevels; skinLevel++) {
msgText += `Levels: ${skinLevel}/${skinMaxLevels}, MaxChromas: ${1}/${skinMaxChromas} - `;
msgText += `${getDummySkinUpgradeProbs(skinLevel, 1, skinTierRank, skinMaxLevels, skinMaxChromas, 15).successProb.toFixed(4)}, `;
msgText += `${getDummySkinUpgradeProbs(skinLevel, 1, skinTierRank, skinMaxLevels, skinMaxChromas, 15).destructionProb.toFixed(4)}, `;
msgText += `${getDummySkinUpgradeProbs(skinLevel, 1, skinTierRank, skinMaxLevels, skinMaxChromas, 15).upgradePrice}\n`;
}
for (let skinChroma = 1; skinChroma < skinMaxChromas; skinChroma++) {
msgText += `Levels: ${skinMaxLevels}/${skinMaxLevels}, MaxChromas: ${skinChroma}/${skinMaxChromas} - `;
msgText += `${getDummySkinUpgradeProbs(skinMaxLevels, skinChroma, skinTierRank, skinMaxLevels, skinMaxChromas, 15).successProb.toFixed(4)}, `;
msgText += `${getDummySkinUpgradeProbs(skinMaxLevels, skinChroma, skinTierRank, skinMaxLevels, skinMaxChromas, 15).destructionProb.toFixed(4)}, `;
msgText += `${getDummySkinUpgradeProbs(skinMaxLevels, skinChroma, skinTierRank, skinMaxLevels, skinMaxChromas, 15).upgradePrice}\n`;
}
message.reply(msgText);
msgText = "";
}
break;
case "?v":
console.log("Active Polls:", activePolls);
break;
case "?sv":
const amount = parseInt(args[0], 10);
if (isNaN(amount)) return message.reply("Invalid amount.");
let sum = 0;
const start_at = Date.now();
for (let i = 0; i < amount; i++) {
sum += parseFloat(randomSkinPrice());
}
console.log(
`Result for ${amount} skins: Avg: ~${(sum / amount).toFixed(0)} Flopos | Total: ${sum.toFixed(0)} Flopos | Elapsed: ${Date.now() - start_at}ms`,
);
break;
case `${prefix}:sotd`:
initTodaysSOTD();
message.reply("New Solitaire of the Day initialized.");
break;
case `${prefix}:users`:
console.log(await userService.getAllUsers());
break;
case `${prefix}:sql`:
const sqlCommand = args.join(" ");
try {
const result = sqlCommand.trim().toUpperCase().startsWith("SELECT")
? await prisma.$queryRawUnsafe(sqlCommand)
: await prisma.$executeRawUnsafe(sqlCommand);
const jsonString = JSON.stringify(result, null, 2);
const buffer = Buffer.from(jsonString, "utf-8");
const attachment = new AttachmentBuilder(buffer, { name: "sql-result.json" });
message.reply({ content: "SQL query executed successfully:", files: [attachment] });
} catch (e) {
message.reply(`SQL Error: ${e.message}`);
}
break;
case `${prefix}:fetch-data`:
await getAkhys(client);
break;
case `${prefix}:avatars`:
const guild = await client.guilds.fetch(process.env.GUILD_ID);
const members = await guild.members.fetch();
const akhys = members.filter((m) => !m.user.bot && m.roles.cache.has(process.env.AKHY_ROLE_ID));
const usersToUpdate = akhys.map((akhy) => ({
id: akhy.user.id,
avatarUrl: akhy.user.displayAvatarURL({ dynamic: true, size: 256 }),
}));
for (const user of usersToUpdate) {
try {
await userService.updateUserAvatar(user.id, user.avatarUrl);
} catch (err) {}
}
break;
case `${prefix}:rework-skins`:
console.log("Reworking all skin prices...");
const dbSkins = await skinService.getAllSkins();
for (const skin of dbSkins) {
const fetchedSkin = skins.find((s) => s.uuid === skin.uuid);
const basePrice = calculateBasePrice(fetchedSkin, skin.tierRank)?.toFixed(0);
const calculatePrice = () => {
if (!skin.basePrice) return null;
let result = parseFloat(basePrice);
result *= 1 + skin.currentLvl / Math.max(fetchedSkin.levels.length, 2);
result *= 1 + skin.currentChroma / 4;
return parseFloat(result.toFixed(0));
};
const maxPrice = calculateMaxPrice(basePrice, fetchedSkin).toFixed(0);
await skinService.hardUpdateSkin({
uuid: skin.uuid,
displayName: skin.displayName,
contentTierUuid: skin.contentTierUuid,
displayIcon: skin.displayIcon,
userId: skin.userId,
tierRank: skin.tierRank,
tierColor: skin.tierColor,
tierText: skin.tierText,
basePrice: basePrice,
currentLvl: skin.currentLvl || null,
currentChroma: skin.currentChroma || null,
currentPrice: skin.currentPrice ? calculatePrice() : null,
maxPrice: maxPrice,
});
}
console.log("Reworked", dbSkins.length, "skins.");
break;
case `${prefix}:cases-test`:
try {
const caseType = args[0] ?? "standard";
const caseCount = args[1] ?? 1;
let totalResValue = 0;
let highestSkinPrice = 0;
let priceTiers = {
0: 0,
100: 0,
200: 0,
300: 0,
400: 0,
500: 0,
600: 0,
700: 0,
800: 0,
900: 0,
1000: 0,
};
for (let i = 0; i < caseCount; i++) {
const skins = await drawCaseContent(caseType);
const result = await drawCaseSkin(skins);
totalResValue += result.finalPrice;
if (result.finalPrice > highestSkinPrice) highestSkinPrice = result.finalPrice;
if (result.finalPrice > 0 && result.finalPrice < 100) priceTiers["0"] += 1;
if (result.finalPrice >= 100 && result.finalPrice < 200) priceTiers["100"] += 1;
if (result.finalPrice >= 200 && result.finalPrice < 300) priceTiers["200"] += 1;
if (result.finalPrice >= 300 && result.finalPrice < 400) priceTiers["300"] += 1;
if (result.finalPrice >= 400 && result.finalPrice < 500) priceTiers["400"] += 1;
if (result.finalPrice >= 500 && result.finalPrice < 600) priceTiers["500"] += 1;
if (result.finalPrice >= 600 && result.finalPrice < 700) priceTiers["600"] += 1;
if (result.finalPrice >= 700 && result.finalPrice < 800) priceTiers["700"] += 1;
if (result.finalPrice >= 800 && result.finalPrice < 900) priceTiers["800"] += 1;
if (result.finalPrice >= 900 && result.finalPrice < 1000) priceTiers["900"] += 1;
if (result.finalPrice >= 1000) priceTiers["1000"] += 1;
console.log(
`Case ${i + 1}: Won a skin worth ${result.finalPrice} Flopos, ${caseType}, ${result.updatedSkin.tierRank}`,
);
}
console.log(totalResValue / caseCount);
message.reply(
`${totalResValue / caseCount} average skin price over ${caseCount} ${caseType} cases.\nHighest skin price: ${highestSkinPrice}\nPrice tier distribution: ${JSON.stringify(priceTiers)}`,
);
} catch (e) {
console.log(e);
message.reply(`Error during case test: ${e.message}`);
}
break;
case `${prefix}:refund-skins`:
try {
const DBskins = await skinService.getAllSkins();
for (const skin of DBskins) {
const owner = await userService.getUser(skin.userId);
if (owner) {
await userService.updateUserCoins(owner.id, owner.coins + skin.currentPrice);
await logService.insertLog({
id: `${skin.uuid}-skin-refund-${Date.now()}`,
userId: owner.id,
targetUserId: null,
action: "SKIN_REFUND",
coinsAmount: skin.currentPrice,
userNewAmount: owner.coins + skin.currentPrice,
});
}
await skinService.updateSkin({
uuid: skin.uuid,
userId: null,
currentPrice: null,
currentLvl: null,
currentChroma: null,
});
}
message.reply("All skins refunded.");
} catch (e) {
console.log(e);
message.reply(`Error during refund skins ${e.message}`);
}
break;
case `${prefix}:cs-search`:
try {
const searchTerm = args.join(" ");
if (!searchTerm) {
message.reply("Please provide a search term.");
return;
}
const filteredData = csSkinsData
? Object.values(csSkinsData).filter((skin) => {
const name = skin.market_hash_name.toLowerCase();
return args.every((word) => name.includes(word.toLowerCase()));
})
: [];
if (filteredData.length === 0) {
message.reply(`No skins found matching "${searchTerm}".`);
return;
} else if (filteredData.length <= 10) {
const skinList = filteredData
.map(
(skin) =>
`${skin.market_hash_name} - ${
csSkinsPrices[skin.market_hash_name]
? "Sug " +
csSkinsPrices[skin.market_hash_name].suggested_price +
" | Min " +
csSkinsPrices[skin.market_hash_name].min_price +
" | Max " +
csSkinsPrices[skin.market_hash_name].max_price +
" | Avg " +
csSkinsPrices[skin.market_hash_name].mean_price +
" | Med " +
csSkinsPrices[skin.market_hash_name].median_price
: "N/A"
}`,
)
.join("\n");
message.reply(`Skins matching "${searchTerm}":\n${skinList}`);
} else {
message.reply(`Found ${filteredData.length} skins matching "${searchTerm}".`);
}
} catch (e) {
console.log(e);
message.reply(`Error searching CS:GO skins: ${e.message}`);
}
break;
case `${prefix}:open-cs`:
try {
const randomSkin = getRandomSkinWithRandomSpecs(args[0] ? parseFloat(args[0]) : null);
message.reply(
`You opened a CS:GO case and got: ${randomSkin.name} (${randomSkin.data.rarity.name}, ${
randomSkin.isStattrak ? "StatTrak, " : ""
}${randomSkin.isSouvenir ? "Souvenir, " : ""}${randomSkin.wearState} - float ${randomSkin.float})\nBase Price: ${
randomSkin.price ?? "N/A"
} Flopos\nImage url: [url](${randomSkin.data.image || "N/A"})`,
);
} catch (e) {
console.log(e);
message.reply(`Error opening CS:GO case: ${e.message}`);
}
break;
case `${prefix}:simulate-cs`:
try {
const caseCount = parseInt(args[0]) || 100;
const caseType = args[1] || "default";
let totalResValue = 0;
let highestSkinPrice = 0;
const priceTiers = {
"Consumer Grade": 0,
"Industrial Grade": 0,
"Mil-Spec Grade": 0,
"Restricted": 0,
"Classified": 0,
"Covert": 0,
"Extraordinary": 0,
};
for (let i = 0; i < caseCount; i++) {
const result = getRandomSkinWithRandomSpecs();
totalResValue += parseInt(result.price);
if (parseInt(result.price) > highestSkinPrice) {
highestSkinPrice = parseInt(result.price);
}
priceTiers[result.data.rarity.name]++;
}
console.log(totalResValue / caseCount);
message.reply(
`${totalResValue / caseCount} average skin price over ${caseCount} ${caseType} cases.\nHighest skin price: ${highestSkinPrice}\nPrice tier distribution: ${JSON.stringify(priceTiers)}`,
);
} catch (e) {
console.log(e);
message.reply(`Error during case simulation: ${e.message}`);
}
break;
}
}