Files
flopobot_v2/src/utils/index.js
2025-12-31 19:24:21 +01:00

496 lines
15 KiB
JavaScript

import "dotenv/config";
import cron from "node-cron";
// --- Local Imports ---
import { getSkinTiers, getValorantSkins } from "../api/valorant.js";
import { DiscordRequest } from "../api/discord.js";
import { initTodaysSOTD } from "../game/points.js";
import {
deleteBid,
deleteMarketOffer,
getAllAkhys,
getAllUsers,
getMarketOffers,
getOfferBids,
getSkin,
getUser,
insertManySkins,
insertUser,
resetDailyReward,
updateMarketOffer,
updateSkin,
updateUserAvatar,
updateUserCoins,
} from "../database/index.js";
import { activeInventories, activePredis, activeSearchs, pokerRooms, skins } from "../game/state.js";
import { emitMarketUpdate } from "../server/socket.js";
import { handleMarketOfferClosing, handleMarketOfferOpening } from "./marketNotifs.js";
import { client } from "../bot/client.js";
export async function InstallGlobalCommands(appId, commands) {
// API endpoint to overwrite global commands
const endpoint = `applications/${appId}/commands`;
try {
// This is calling the bulk overwrite endpoint: https://discord.com/developers/docs/interactions/application-commands#bulk-overwrite-global-application-commands
await DiscordRequest(endpoint, { method: "PUT", body: commands });
} catch (err) {
console.error(err);
}
}
// --- Data Fetching & Initialization ---
/**
* Fetches all members with the 'Akhy' role and all Valorant skins,
* then syncs them with the database.
* @param {object} client - The Discord.js client instance.
*/
export async function getAkhys(client) {
try {
// 1. Fetch Discord Members
const initial_akhys = getAllUsers.all().length;
const guild = await client.guilds.fetch(process.env.GUILD_ID);
const members = await guild.members.fetch();
const akhys = members.filter((m) => !m.user.bot && m.roles.cache.has(process.env.AKHY_ROLE_ID));
const usersToInsert = akhys.map((akhy) => ({
id: akhy.user.id,
username: akhy.user.username,
globalName: akhy.user.globalName,
warned: 0,
warns: 0,
allTimeWarns: 0,
totalRequests: 0,
avatarUrl: akhy.user.displayAvatarURL({ dynamic: true, size: 256 }),
isAkhy: 1,
}));
if (usersToInsert.length > 0) {
usersToInsert.forEach((user) => {
try {
insertUser.run(user);
} catch (err) {}
});
}
const new_akhys = getAllUsers.all().length;
const diff = new_akhys - initial_akhys;
console.log(
`[Sync] Found and synced ${usersToInsert.length} ${diff !== 0 ? "(" + (diff > 0 ? "+" + diff : diff) + ") " : ""}users with the 'Akhy' role. (ID:${process.env.AKHY_ROLE_ID})`,
);
// 2. Fetch Valorant Skins
const [fetchedSkins, fetchedTiers] = await Promise.all([getValorantSkins(), getSkinTiers()]);
// Clear and rebuild the in-memory skin cache
skins.length = 0;
fetchedSkins.forEach((skin) => skins.push(skin));
const skinsToInsert = fetchedSkins
.filter((skin) => skin.contentTierUuid)
.map((skin) => {
const tier = fetchedTiers.find((t) => t.uuid === skin.contentTierUuid) || {};
const basePrice = calculateBasePrice(skin, tier.rank);
return {
uuid: skin.uuid,
displayName: skin.displayName,
contentTierUuid: skin.contentTierUuid,
displayIcon: skin.displayIcon,
user_id: null,
tierRank: tier.rank,
tierColor: tier.highlightColor?.slice(0, 6) || "F2F3F3",
tierText: formatTierText(tier.rank, skin.displayName),
basePrice: basePrice.toFixed(0),
maxPrice: calculateMaxPrice(basePrice, skin).toFixed(0),
};
});
if (skinsToInsert.length > 0) {
insertManySkins(skinsToInsert);
}
console.log(`[Sync] Fetched and synced ${skinsToInsert.length} Valorant skins.`);
} catch (err) {
console.error("Error during initial data sync (getAkhys):", err);
}
}
// --- Cron Jobs / Scheduled Tasks ---
/**
* Sets up all recurring tasks for the application.
* @param {object} client - The Discord.js client instance.
* @param {object} io - The Socket.IO server instance.
*/
export function setupCronJobs(client, io) {
// Every 5 minutes: Update market offers
cron.schedule("* * * * *", () => {
handleMarketOffersUpdate();
});
// Every 10 minutes: Clean up expired interactive sessions
cron.schedule("*/10 * * * *", () => {
const now = Date.now();
const FIVE_MINUTES = 5 * 60 * 1000;
const ONE_DAY = 24 * 60 * 60 * 1000;
const cleanup = (sessions, name) => {
let cleanedCount = 0;
for (const id in sessions) {
if (now >= (sessions[id].timestamp || 0) + FIVE_MINUTES) {
delete sessions[id];
cleanedCount++;
}
}
if (cleanedCount > 0) console.log(`[Cron] Cleaned up ${cleanedCount} expired ${name} sessions.`);
};
cleanup(activeInventories, "inventory");
cleanup(activeSearchs, "search");
for (const id in pokerRooms) {
if (pokerRooms[id].last_move_at !== null) {
if (now >= pokerRooms[id].last_move_at + FIVE_MINUTES * 3) {
delete pokerRooms[id];
console.log(`[Cron] Cleaned up expired poker room ID: ${id}`);
}
} else {
if (now >= pokerRooms[id].created_at + FIVE_MINUTES * 6) {
delete pokerRooms[id];
console.log(`[Cron] Cleaned up expired poker room ID: ${id}`);
}
}
}
let cleanedCount = 0;
for (const id in activePredis) {
if (now >= (activePredis[id].endTime || 0)) {
delete activePredis[id];
cleanedCount++;
}
}
if (cleanedCount > 0) console.log(`[Cron] Cleaned up ${cleanedCount} expired predictions.`);
});
// Daily at midnight: Reset daily rewards and init SOTD
cron.schedule(process.env.CRON_EXPR, async () => {
console.log("[Cron] Running daily midnight tasks...");
try {
resetDailyReward.run();
console.log("[Cron] Daily rewards have been reset for all users.");
//if (!getSOTD.get()) {
initTodaysSOTD();
//}
} catch (e) {
console.error("[Cron] Error during daily reset:", e);
}
try {
const offers = getMarketOffers.all();
const now = Date.now();
const TWO_DAYS = 2 * 24 * 60 * 60 * 1000;
for (const offer of offers) {
if (now >= offer.closing_at + TWO_DAYS) {
const offerBids = getOfferBids.all(offer.id);
for (const bid of offerBids) {
deleteBid.run(bid.id);
}
deleteMarketOffer.run(offer.id);
console.log(`[Cron] Deleted expired market offer ID: ${offer.id}`);
}
}
} catch (e) {
console.error("[Cron] Error during Market Offers clean up:", e);
}
});
// Daily at 7 AM: Re-sync users and skins
cron.schedule("0 7 * * *", async () => {
console.log("[Cron] Running daily 7 AM data sync...");
await getAkhys(client);
try {
const akhys = getAllAkhys.all();
for (const akhy of akhys) {
const user = await client.users.cache.get(akhy.id);
try {
updateUserAvatar.run({
id: akhy.id,
avatarUrl: user.displayAvatarURL({ dynamic: true, size: 256 }),
});
} catch (err) {
console.error(`[Cron] Error updating avatar for user ID: ${akhy.id}`, err);
}
}
} catch (e) {
console.error("[Cron] Error during daily avatar update:", e);
}
});
}
// --- Formatting Helpers ---
export function capitalize(str) {
if (typeof str !== "string" || str.length === 0) return "";
return str.charAt(0).toUpperCase() + str.slice(1);
}
export function formatTime(seconds) {
const d = Math.floor(seconds / (3600 * 24));
const h = Math.floor((seconds % (3600 * 24)) / 3600);
const m = Math.floor((seconds % 3600) / 60);
const s = Math.floor(seconds % 60);
const parts = [];
if (d > 0) parts.push(`**${d}** jour${d > 1 ? "s" : ""}`);
if (h > 0) parts.push(`**${h}** heure${h > 1 ? "s" : ""}`);
if (m > 0) parts.push(`**${m}** minute${m > 1 ? "s" : ""}`);
if (s > 0 || parts.length === 0) parts.push(`**${s}** seconde${s > 1 ? "s" : ""}`);
return parts.join(", ").replace(/,([^,]*)$/, " et$1");
}
// --- External API Helpers ---
/**
* Fetches user data from the "APO" service.
*/
export async function getAPOUsers() {
const fetchUrl = `${process.env.APO_BASE_URL}/users?serverId=${process.env.GUILD_ID}`;
try {
const response = await fetch(fetchUrl);
if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`);
return await response.json();
} catch (error) {
console.error("Error fetching APO users:", error);
return null;
}
}
/**
* Sends a "buy" request to the "APO" service.
* @param {string} userId - The Discord user ID.
* @param {number} amount - The amount to "buy".
*/
export async function postAPOBuy(userId, amount) {
const fetchUrl = `${process.env.APO_BASE_URL}/buy?serverId=${process.env.GUILD_ID}&userId=${userId}&amount=${amount}`;
return fetch(fetchUrl, { method: "POST" });
}
// --- Miscellaneous Helpers ---
function handleMarketOffersUpdate() {
const now = Date.now();
const offers = getMarketOffers.all();
offers.forEach(async (offer) => {
if (now >= offer.opening_at && offer.status === "pending") {
updateMarketOffer.run({ id: offer.id, final_price: null, buyer_id: null, status: "open" });
await handleMarketOfferOpening(offer.id, client);
await emitMarketUpdate();
}
if (now >= offer.closing_at && offer.status !== "closed") {
const bids = getOfferBids.all(offer.id);
if (bids.length === 0) {
// No bids placed, mark as closed without a sale
updateMarketOffer.run({
id: offer.id,
buyer_id: null,
final_price: null,
status: "closed",
});
await emitMarketUpdate();
} else {
const lastBid = bids[0];
const seller = getUser.get(offer.seller_id);
const buyer = getUser.get(lastBid.bidder_id);
try {
// Change skin ownership
const skin = getSkin.get(offer.skin_uuid);
if (!skin) throw new Error(`Skin not found for offer ID: ${offer.id}`);
updateSkin.run({
user_id: buyer.id,
currentLvl: skin.currentLvl,
currentChroma: skin.currentChroma,
currentPrice: skin.currentPrice,
uuid: skin.uuid,
});
updateMarketOffer.run({
id: offer.id,
buyer_id: buyer.id,
final_price: lastBid.offer_amount,
status: "closed",
});
const newUserCoins = seller.coins + lastBid.offer_amount;
updateUserCoins.run({ id: seller.id, coins: newUserCoins });
await emitMarketUpdate();
} catch (e) {
console.error(`[Market Cron] Error processing offer ID: ${offer.id}`, e);
}
}
await handleMarketOfferClosing(offer.id, client);
}
});
}
export async function getOnlineUsersWithRole(guild, roleId) {
if (!guild || !roleId) return new Map();
try {
const members = await guild.members.fetch();
return members.filter(
(m) =>
!m.user.bot &&
m.presence?.status !== "offline" &&
m.presence?.status !== undefined &&
m.roles.cache.has(roleId),
);
} catch (err) {
console.error("Error fetching online members with role:", err);
return new Map();
}
}
export function getRandomEmoji(list = 0) {
const emojiLists = [
["😭", "😄", "😌", "🤓", "😎", "😤", "🤖", "😶‍🌫️", "🌏", "📸", "💿", "👋", "🌊", "✨"],
[
"<:CAUGHT:1323810730155446322>",
"<:hinhinhin:1072510144933531758>",
"<:o7:1290773422451986533>",
"<:zhok:1115221772623683686>",
"<:nice:1154049521110765759>",
"<:nerd:1087658195603951666>",
"<:peepSelfie:1072508131839594597>",
],
];
const selectedList = emojiLists[list] || [""];
return selectedList[Math.floor(Math.random() * selectedList.length)];
}
export function formatAmount(amount) {
if (amount >= 1000000000) {
amount /= 1000000000;
return (
amount
.toFixed(2)
.toString()
.replace(/\B(?=(\d{3})+(?!\d))/g, " ") + "Md"
);
}
if (amount >= 1000000) {
amount /= 1000000;
return (
amount
.toFixed(2)
.toString()
.replace(/\B(?=(\d{3})+(?!\d))/g, " ") + "M"
);
}
if (amount >= 10000) {
amount /= 1000;
return (
amount
.toFixed(2)
.toString()
.replace(/\B(?=(\d{3})+(?!\d))/g, " ") + "K"
);
}
return amount.toString().replace(/\B(?=(\d{3})+(?!\d))/g, " ");
}
// --- Private Helpers ---
export function calculateBasePrice(skin, tierRank) {
const name = skin.displayName.toLowerCase();
let price = 6000; // Default for melee
if (name.includes("classic")) price = 150;
else if (name.includes("shorty")) price = 300;
else if (name.includes("frenzy")) price = 450;
else if (name.includes("ghost")) price = 500;
else if (name.includes("sheriff")) price = 800;
else if (name.includes("stinger")) price = 1000;
else if (name.includes("spectre")) price = 1600;
else if (name.includes("bucky")) price = 900;
else if (name.includes("judge")) price = 1500;
else if (name.includes("bulldog")) price = 2100;
else if (name.includes("guardian")) price = 2700;
else if (name.includes("vandal") || name.includes("phantom")) price = 2900;
else if (name.includes("marshal")) price = 950;
else if (name.includes("outlaw")) price = 2400;
else if (name.includes("operator")) price = 4500;
else if (name.includes("ares")) price = 1700;
else if (name.includes("odin")) price = 3200;
price *= 1 + (tierRank || 0);
if (name.includes("vct")) price *= 2.75;
if (name.includes("champions")) price *= 2;
return price / 124;
}
export function calculateMaxPrice(basePrice, skin) {
let res = basePrice;
res *= 1 + skin.levels.length / Math.max(skin.levels.length, 2);
res *= 1 + skin.chromas.length / 4;
return res;
}
function formatTierText(rank, displayName) {
const tiers = {
0: "**<:select:1362964319498670222> Select**",
1: "**<:deluxe:1362964308094488797> Deluxe**",
2: "**<:premium:1362964330349330703> Premium**",
3: "**<:exclusive:1362964427556651098> Exclusive**",
4: "**<:ultra:1362964339685986314> Ultra**",
};
let res = tiers[rank] || "Pas de tier";
if (displayName.includes("VCT")) res += " | Esports";
if (displayName.toLowerCase().includes("champions")) res += " | Champions";
return res;
}
export function isMeleeSkin(skinName) {
const name = skinName.toLowerCase();
return !(name.includes("classic") || name.includes("shorty") || name.includes("frenzy") || name.includes("ghost") || name.includes("sheriff") || name.includes("stinger") || name.includes("spectre") ||
name.includes("bucky") || name.includes("judge") || name.includes("bulldog") || name.includes("guardian") ||
name.includes("vandal") || name.includes("phantom") || name.includes("marshal") || name.includes("outlaw") ||
name.includes("operator") || name.includes("ares") || name.includes("odin"));
}
export function isVCTSkin(skinName) {
const name = skinName.toLowerCase();
return name.includes("vct");
}
const VCT_TEAMS = {
"vct-am": [
/x 100t\)$/g, /x c9\)$/g, /x eg\)$/g, /x fur\)$/g, /x krü\)$/g, /x lev\)$/g, /x loud\)$/g,
/x mibr\)$/g, /x sen\)$/g, /x nrg\)$/g, /x g2\)$/g, /x nv\)$/g, /x 2g\)$/g
],
"vct-emea": [
/x bbl\)$/g, /x fnc\)$/g, /x fut\)$/g, /x m8\)$/g, /x gx\)$/g, /x kc\)$/g, /x navi\)$/g,
/x th\)$/g, /x tl\)$/g, /x vit\)$/g, /x ulf\)$/g, /x pcf\)$/g, /x koi\)$/g, /x apk\)$/g
],
"vct-pcf": [
/x dfm\)$/g, /x drx\)$/g, /x fs\)$/g, /x gen\)$/g, /x ge\)$/g, /x prx\)$/g, /x rrq\)$/g,
/x t1\)$/g, /x ts\)$/g, /x zeta\)$/g, /x vl\)$/g, /x ns\)$/g, /x tln\)$/g, /x boom\)$/g, /x bld\)$/g
],
"vct-cn": [
/x ag\)$/g, /x blg\)$/g, /x edg\)$/g, /x fpx\)$/g, /x jdg\)$/g, /x nova\)$/g, /x tec\)$/g,
/x te\)$/g, /x tyl\)$/g, /x wol\)$/g, /x xlg\)$/g, /x xlg\)$/g, /x drg\)$/g, /x drg\)$/g
]
};
export function getVCTRegion(skinName) {
if (!isVCTSkin(skinName)) return null;
const name = skinName.toLowerCase().trim();
for (const [region, regexes] of Object.entries(VCT_TEAMS)) {
if (regexes.some(regex => regex.test(name))) {
return region;
}
}
return null;
}
export function isChampionsSkin(skinName) {
const name = skinName.toLowerCase();
return name.includes("champions");
}