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"); }