// ============================================= // FORTNITE DISCORD BOT — ALL-IN-ONE // ============================================= // Commands: // !setuproles — create all roles // !addicons — upload skin icons as server emojis // !setroleicons — apply emojis to roles // !removeoldemojis — delete old skin emojis // !verify — verify Fortnite account & auto-assign roles // // Run with: node bot.js // ============================================= require('dotenv').config(); const { Client, GatewayIntentBits, PermissionsBitField, EmbedBuilder, REST, Routes, SlashCommandBuilder, ActionRowBuilder, ButtonBuilder, ButtonStyle, AttachmentBuilder } = require('discord.js'); const sharp = require('sharp'); const client = new Client({ intents: [ GatewayIntentBits.Guilds, GatewayIntentBits.GuildMessages, GatewayIntentBits.MessageContent, GatewayIntentBits.DirectMessages, ], partials: ['CHANNEL'], }); // ───────────────────────────────────────────────────────────────────────────── // ROLES TO CREATE // ───────────────────────────────────────────────────────────────────────────── const roles = [ { name: 'Fully Golden Peely', color: 0xFFD700 }, { name: 'OG Aerial Assault Trooper', color: 0x8A8A8A }, { name: 'OG Ghoul Trooper', color: 0xA89DB8 }, { name: 'OG Renegade Raider', color: 0x7B2D2D }, { name: 'OG Skull Trooper', color: 0x9B59B6 }, { name: 'Barricade', color: 0x9E9E9E }, { name: 'Double Helix', color: 0xD0443C }, { name: 'Honor Guard', color: 0x4A90D9 }, { name: 'Wonder', color: 0x2ECC71 }, { name: 'Surf Strider', color: 0x1ABC9C }, { name: 'Stealth Reflex', color: 0xE8927C }, { name: 'Dark Vertex', color: 0x8E44AD }, { name: 'Royale Bomber', color: 0xE67E22 }, { name: 'Wildcat', color: 0xE8832A }, { name: 'Clyde', color: 0xE8832A }, { name: 'Eon', color: 0x27AE60 }, { name: 'Huntmaster Saber', color: 0xD4AC0D }, { name: 'Dark Skully', color: 0x7D6B8A }, { name: 'Galaxy', color: 0x5B7DD8 }, { name: 'Ikonik', color: 0xD0443C }, { name: 'Black Knight', color: 0xCCCCCC }, { name: 'Glow', color: 0xBB8FCC }, { name: 'Rogue Spider Knight', color: 0x2ECC71 }, { name: 'Thrilldiver', color: 0x4A90D9 }, { name: 'Freediver', color: 0x9E9E9E }, { name: 'Neo Versa', color: 0x9B59B6 }, { name: 'Cobalt Snowfoot', color: 0x1ABC9C }, { name: 'Florin', color: 0x8E44AD }, { name: "What's Wrong, Toona Fish?", color: 0xCCCCCC }, { name: 'Cameo Needs Loot', color: 0x2ECC71 }, { name: 'Victory!', color: 0x9B59B6 }, { name: 'Collegiate items', color: 0x2ECC71 }, { name: 'StreamElements', color: 0xF1C40F }, { name: 'Llamas', color: 0xFF69B4 }, { name: 'Repaz', color: 0xD4AC0D }, { name: 'lollek', color: 0xFFFFFF }, { name: 'Celebration Heart', color: 0x9B59B6 }, { name: 'Funflower', color: 0xD4C227 }, { name: 'Level 100', color: 0xBB44EE }, { name: 'Alpha Banner', color: 0x777777 }, { name: 'YouTube Banner', color: 0x777777 }, { name: 'STW Founders', color: 0xE67E22 }, { name: 'Ultimate STW Founders', color: 0x888888 }, { name: 'Rare Usernames', color: 0xF1C40F }, { name: 'Skin Collector', color: 0x888888 }, { name: 'PS+ Pack Collector', color: 0x3498DB }, { name: 'Spray Collector', color: 0xCCCCCC }, { name: 'Rare Banners', color: 0x888888 }, { name: 'Emote Collector', color: 0xCCCCCC }, // --- Exclusive Pickaxes --- { name: "Raider's Revenge", color: 0x7B2D2D }, { name: 'Axe of Champions', color: 0xF1C40F }, { name: 'Blade of Champions', color: 0xF1C40F }, { name: 'Axecalibur', color: 0xCCCCCC }, { name: 'EVA', color: 0x4A90D9 }, { name: 'AC/DC', color: 0xF1C40F }, { name: 'Resonator', color: 0x27AE60 }, { name: 'Pinpoint', color: 0xD0443C }, { name: 'Stellar Axe', color: 0x5B7DD8 }, { name: 'Stealth Angular Axe', color: 0x9B59B6 }, { name: 'Dark Razor', color: 0x7D6B8A }, { name: 'Electri-claw', color: 0xE8832A }, { name: 'Dark Splitter', color: 0x8E44AD }, { name: 'Merry Mint Axe', color: 0x2ECC71 }, { name: 'Indigo Inverter', color: 0x1ABC9C }, { name: 'Rose Glow', color: 0xFF69B4 }, { name: 'Controller', color: 0x3498DB }, { name: 'Pneumatic Twin', color: 0x4A90D9 }, { name: 'Perfect Point', color: 0x9E9E9E }, { name: 'Petrol Pump', color: 0xE67E22 }, { name: 'Future Relic Scythe', color: 0x8E44AD }, // --- Exclusive Emoticons --- { name: 'Skull Trooper Emoticon', color: 0x9B59B6 }, { name: 'Maevys Emoticon', color: 0xD0443C }, { name: 'ZetFar Emoticon', color: 0x4A90D9 }, // --- PlayStation Exclusives --- { name: 'Blue Team Leader', color: 0x3498DB }, // PS Plus Pack 1 { name: 'Blue Striker', color: 0x3498DB }, // PS Plus Pack 2 { name: 'Prodigy', color: 0x3498DB }, // PS Plus Pack 4 { name: 'Carbon Commando', color: 0x3498DB }, // PS Plus Pack 5 { name: 'Trilogy', color: 0x3498DB }, // PS Plus Pack 8 { name: 'Point Patroller', color: 0x3498DB }, // PS Plus Pack 9 { name: 'Cloud Striker', color: 0x3498DB }, // PS Plus Pack 12 { name: 'Fixer', color: 0x3498DB }, // PS Plus Pack 14 { name: 'Blizzard Bomber', color: 0x3498DB }, // PS Plus Pack 18 { name: 'Jun-Hwan', color: 0x3498DB }, // Blue Phoenix Pack { name: 'Lina Scorch', color: 0x3498DB }, // Blazin Wheels Pack { name: 'Indigo Kuno', color: 0x9B59B6 }, // PS Generations Cup // --- Twitch / Amazon Prime --- { name: 'Sub Commander', color: 0x9146FF }, // Twitch Prime Pack 1 { name: 'Havoc', color: 0x9146FF }, // Twitch Prime Pack 1 { name: 'Trailblazer', color: 0x9146FF }, // Twitch Prime Pack 2 // --- Intel / PC Exclusive --- { name: 'Reflex', color: 0x0071C5 }, // Intel bundle // --- OG Battle Pass (C1S2) --- { name: 'Sparkle Specialist', color: 0xFF69B4 }, { name: 'Royale Knight', color: 0xCCCCCC }, { name: 'Blue Squire', color: 0x4A90D9 }, // --- OG Battle Pass (C1S3) --- { name: 'The Reaper', color: 0x888888 }, { name: 'Elite Agent', color: 0x888888 }, { name: 'Rogue Agent', color: 0x888888 }, // --- OG Battle Pass (C1S4) --- { name: 'Omega', color: 0x8E44AD }, // --- Chapter 2 Season 2 Battle Pass (golden styles) --- { name: 'Skye', color: 0xF1C40F }, { name: 'TNTina', color: 0xF1C40F }, { name: 'Meowscles', color: 0xF1C40F }, { name: 'Midas', color: 0xFFD700 }, // --- Gaming Legends / Collab Exclusives --- { name: 'Travis Scott', color: 0xF1C40F }, // Icon Series, never returned { name: 'Astro Jack', color: 0xF1C40F }, // Icon Series, never returned { name: 'Kratos', color: 0xCCCCCC }, // PlayStation collab { name: 'Master Chief', color: 0x4A90D9 }, // Xbox collab // --- World Cup / Tournament --- { name: 'World Warrior', color: 0xF1C40F }, // Fortnite World Cup 2019 // --- V-Bucks Card Promo --- { name: 'Color Splash Jellie', color: 0xFF69B4 }, // --- Save the World Founders --- { name: 'Rose Team Leader', color: 0xE67E22 }, { name: 'Warpaint', color: 0xE67E22 }, // --- OT Banners --- { name: 'OT1 Banner', color: 0xFFD700 }, { name: 'OT2 Banner', color: 0xFFD700 }, { name: 'OT3 Banner', color: 0xFFD700 }, { name: 'OT4 Banner', color: 0xFFD700 }, { name: 'OT5 Banner', color: 0xFFD700 }, { name: 'OT6 Banner', color: 0xFFD700 }, { name: 'OT7 Banner', color: 0xFFD700 }, { name: 'OT8 Banner', color: 0xFFD700 }, { name: 'OT9 Banner', color: 0xFFD700 }, { name: 'OT10 Banner', color: 0xFFD700 }, { name: 'OT11 Banner', color: 0xFFD700 }, ]; // ───────────────────────────────────────────────────────────────────────────── // EMOJI / ICON MAP // ───────────────────────────────────────────────────────────────────────────── const roleIconMap = [ { name: 'Fully Golden Peely', id: 'CID_701_Athena_Commando_M_BananaAgent', emoji: 'FullyGoldenPeely', overrideUrl: 'https://fortnite-api.com/images/cosmetics/br/cid_701_athena_commando_m_bananaagent/variants/progressive/stage4.png' }, { name: 'OG Aerial Assault Trooper', id: 'CID_017_Athena_Commando_M', emoji: 'AerialAssaultTrooper' }, { name: 'OG Ghoul Trooper', id: 'CID_029_Athena_Commando_F_Halloween', emoji: 'GhoulTrooper', overrideUrl: 'https://fortnite-api.com/images/cosmetics/br/cid_029_athena_commando_f_halloween/variants/material/mat3.png' }, { name: 'OG Renegade Raider', id: 'CID_028_Athena_Commando_F', emoji: 'RenegadeRaider' }, { name: 'OG Skull Trooper', id: 'CID_030_Athena_Commando_M_Halloween', emoji: 'SkullTrooper', overrideUrl: 'https://fortnite-api.com/images/cosmetics/br/cid_030_athena_commando_m_halloween/variants/clothingcolor/mat1.png' }, { name: 'Barricade', id: 'Character_RankedPhantom', emoji: 'Barricade' }, { name: 'Double Helix', id: 'CID_183_Athena_Commando_M_ModernMilitaryRed', emoji: 'DoubleHelix' }, { name: 'Honor Guard', id: 'CID_342_Athena_Commando_M_StreetRacerMetallic', emoji: 'HonorGuard' }, { name: 'Wonder', id: 'CID_434_Athena_Commando_F_StealthHonor', emoji: 'Wonder' }, { name: 'Surf Strider', id: 'CID_783_Athena_Commando_M_AquaJacket', emoji: 'SurfStrider' }, { name: 'Stealth Reflex', id: 'CID_386_Athena_Commando_M_StreetOpsStealth', emoji: 'StealthReflex' }, { name: 'Dark Vertex', id: 'CID_371_Athena_Commando_M_SpeedyMidnight', emoji: 'DarkVertex' }, { name: 'Royale Bomber', id: 'CID_113_Athena_Commando_M_BlueAce', emoji: 'RoyaleBomber' }, { name: 'Wildcat', id: 'CID_757_Athena_Commando_F_WildCat', emoji: 'Wildcat' }, { name: 'Clyde', id: 'Character_UnbrokenTrash', emoji: 'Clyde' }, { name: 'Eon', id: 'CID_174_Athena_Commando_F_CarbideWhite', emoji: 'Eon' }, { name: 'Huntmaster Saber', id: 'Character_MasterKeyOrder', emoji: 'HuntmasterSaber' }, { name: 'Dark Skully', id: 'CID_850_Athena_Commando_F_SkullBriteCube', emoji: 'DarkSkully' }, { name: 'Galaxy', id: 'CID_175_Athena_Commando_M_Celestial', emoji: 'Galaxy' }, { name: 'Ikonik', id: 'CID_313_Athena_Commando_M_KpopFashion', emoji: 'Ikonik' }, // { name: 'Black Knight', id: 'CID_035_Athena_Commando_M_Medieval', emoji: 'BlackKnight' }, // temporarily disabled — upload stalls { name: 'Glow', id: 'CID_479_Athena_Commando_F_Davinci', emoji: 'Glow' }, { name: 'Rogue Spider Knight', id: 'CID_516_Athena_Commando_M_BlackWidowRogue', emoji: 'RogueSpiderKnight' }, { name: 'Thrilldiver', id: 'Character_SummerUnsuitable', emoji: 'Thrilldiver' }, { name: 'Freediver', id: 'Character_CoolSuitable', emoji: 'Freediver' }, { name: 'Neo Versa', id: 'CID_441_Athena_Commando_F_CyberScavengerBlue', emoji: 'NeoVersa' }, { name: 'Cobalt Snowfoot', id: 'Character_SnowNinjaDark', emoji: 'CobaltSnowfoot' }, { name: 'Florin', id: 'Character_AsterOrder', emoji: 'Florin' }, { name: "What's Wrong, Toona Fish?", id: 'CID_A_201_Athena_Commando_M_TeriyakiFishToon', emoji: 'ToonaFish' }, { name: 'Cameo Needs Loot', id: 'Emoji_S20_NeedLoot', emoji: 'Cameo', overrideUrl: 'https://fortnite-api.com/images/cosmetics/br/emoji_s20_needloot/smallicon.png' }, // --- Exclusive Pickaxes --- { name: "Raider's Revenge", id: 'Pickaxe_Lockjaw', emoji: 'RaidersRevenge' }, { name: 'Axe of Champions', id: 'Pickaxe_ID_376_FNCS', emoji: 'AxeOfChampions' }, { name: 'Blade of Champions', id: 'Pickaxe_FNCS_S32', emoji: 'BladeOfChampions' }, { name: 'Axecalibur', id: 'Pickaxe_ID_011_Medieval', emoji: 'Axecalibur' }, { name: 'EVA', id: 'Pickaxe_ID_028_Space', emoji: 'EVA' }, { name: 'AC/DC', id: 'Pickaxe_ID_013_Teslacoil', emoji: 'ACDC' }, { name: 'Resonator', id: 'Pickaxe_ID_077_CarbideWhite', emoji: 'Resonator' }, { name: 'Pinpoint', id: 'Pickaxe_ID_099_ModernMilitaryRed', emoji: 'Pinpoint' }, { name: 'Stellar Axe', id: 'Pickaxe_ID_116_Celestial', emoji: 'StellarAxe' }, { name: 'Stealth Angular Axe', id: 'Pickaxe_ID_189_StreetOpsStealth', emoji: 'StealthAngularAxe' }, { name: 'Dark Razor', id: 'Pickaxe_ID_178_SpeedyMidnight', emoji: 'DarkRazor' }, { name: 'Electri-claw', id: 'Pickaxe_ID_398_WildCatFemale', emoji: 'Electriclaw' }, { name: 'Dark Splitter', id: 'Pickaxe_ID_461_SkullBriteCube', emoji: 'DarkSplitter' }, { name: 'Merry Mint Axe', id: 'Pickaxe_ID_294_CandyCane', emoji: 'MerryMintAxe' }, { name: 'Indigo Inverter', id: 'Pickaxe_SnowNinjaDark', emoji: 'IndigoInverter' }, { name: 'Rose Glow', id: 'Pickaxe_ID_153_RoseLeader', emoji: 'RoseGlow' }, { name: 'Controller', id: 'Pickaxe_ID_088_PSBurnout', emoji: 'Controller' }, { name: 'Pneumatic Twin', id: 'Pickaxe_ID_256_TechOpsBlue', emoji: 'PneumaticTwin' }, { name: 'Perfect Point', id: 'Pickaxe_ID_464_LongShortsMale', emoji: 'PerfectPoint' }, { name: 'Petrol Pump', id: 'Pickaxe_RetroWheels', emoji: 'PetrolPump' }, { name: 'Future Relic Scythe', id: 'Pickaxe_SnowSoldierFashion', emoji: 'FutureRelicScythe' }, // --- OT Banners (OT1–OT11) --- { name: 'OT1 Banner', id: 'OT1Banner', emoji: 'OT1Banner', bannerUrl: 'https://fortnite-api.com/images/banners/ot1banner/icon.png' }, { name: 'OT2 Banner', id: 'OT2Banner', emoji: 'OT2Banner', bannerUrl: 'https://fortnite-api.com/images/banners/ot2banner/icon.png' }, { name: 'OT3 Banner', id: 'OT3Banner', emoji: 'OT3Banner', bannerUrl: 'https://fortnite-api.com/images/banners/ot3banner/icon.png' }, { name: 'OT4 Banner', id: 'OT4Banner', emoji: 'OT4Banner', bannerUrl: 'https://fortnite-api.com/images/banners/ot4banner/icon.png' }, { name: 'OT5 Banner', id: 'OT5Banner', emoji: 'OT5Banner', bannerUrl: 'https://fortnite-api.com/images/banners/ot5banner/icon.png' }, { name: 'OT6 Banner', id: 'OT6Banner', emoji: 'OT6Banner', bannerUrl: 'https://fortnite-api.com/images/banners/ot6banner/icon.png' }, { name: 'OT7 Banner', id: 'OT7Banner', emoji: 'OT7Banner', bannerUrl: 'https://fortnite-api.com/images/banners/ot7banner/icon.png' }, { name: 'OT8 Banner', id: 'OT8Banner', emoji: 'OT8Banner', bannerUrl: 'https://fortnite-api.com/images/banners/ot8banner/icon.png' }, { name: 'OT9 Banner', id: 'OT9Banner', emoji: 'OT9Banner', bannerUrl: 'https://fortnite-api.com/images/banners/ot9banner/icon.png' }, { name: 'OT10 Banner', id: 'OT10Banner', emoji: 'OT10Banner', bannerUrl: 'https://fortnite-api.com/images/banners/ot10banner/icon.png' }, { name: 'OT11 Banner', id: 'OT11Banner', emoji: 'OT11Banner', bannerUrl: 'https://fortnite-api.com/images/banners/ot11banner/icon.png' }, // --- Exclusive Emoticons (in-game emoji cosmetics) --- { name: 'Skull Trooper Emoticon', id: 'Emoji_Skulltrooper', emoji: 'EmoticonSkullTrooper', overrideUrl: 'https://fortnite-api.com/images/cosmetics/br/emoji_skulltrooper/smallicon.png' }, { name: 'Maevys Emoticon', id: 'Emoji_S22_Showdown_Mae', emoji: 'EmoticonMaevys', overrideUrl: 'https://fortnite-api.com/images/cosmetics/br/emoji_s22_showdown_mae/smallicon.png' }, { name: 'ZetFar Emoticon', id: 'Emoji_S22_Showdown_Zet', emoji: 'EmoticonZetFar', overrideUrl: 'https://fortnite-api.com/images/cosmetics/br/emoji_s22_showdown_zet/smallicon.png' }, // --- PlayStation Exclusives --- { name: 'Blue Team Leader', id: 'CID_052_Athena_Commando_F_PSBlue', emoji: 'BlueTeamLeader' }, { name: 'Blue Striker', id: 'CID_138_Athena_Commando_M_PSBurnout', emoji: 'BlueStriker' }, { name: 'Prodigy', id: 'CID_296_Athena_Commando_M_Math', emoji: 'Prodigy' }, { name: 'Carbon Commando', id: 'CID_360_Athena_Commando_M_TechOpsBlue', emoji: 'CarbonCommando' }, { name: 'Trilogy', id: 'CID_657_Athena_Commando_F_TechOpsBlue', emoji: 'Trilogy' }, { name: 'Point Patroller', id: 'CID_711_Athena_Commando_M_LongShorts', emoji: 'PointPatroller' }, { name: 'Cloud Striker', id: 'CID_926_Athena_Commando_F_StreetFashionDiamond', emoji: 'CloudStriker' }, { name: 'Fixer', id: 'CID_A_101_Athena_Commando_M_TacticalWoodlandBlue', emoji: 'Fixer' }, { name: 'Blizzard Bomber', id: 'CID_A_421_Athena_Commando_F_BlizzardBomber', emoji: 'BlizzardBomber' }, { name: 'Jun-Hwan', id: 'Character_BlueJet', emoji: 'JunHwan' }, { name: 'Lina Scorch', id: 'Character_RetroWheels', emoji: 'LinaScorch' }, { name: 'Indigo Kuno', id: 'CID_660_Athena_Commando_F_BandageNinjaBlue', emoji: 'IndigoKuno' }, // --- Twitch / Amazon Prime --- { name: 'Sub Commander', id: 'CID_085_Athena_Commando_M_Twitch', emoji: 'SubCommander' }, { name: 'Havoc', id: 'CID_089_Athena_Commando_M_RetroGrey', emoji: 'Havoc' }, { name: 'Trailblazer', id: 'CID_114_Athena_Commando_F_TacticalWoodland', emoji: 'Trailblazer' }, // --- Intel / PC Exclusive --- { name: 'Reflex', id: 'CID_259_Athena_Commando_M_StreetOps', emoji: 'Reflex' }, // --- OG Battle Pass --- { name: 'Sparkle Specialist', id: 'CID_039_Athena_Commando_F_Disco', emoji: 'SparkleSpecialist' }, { name: 'Royale Knight', id: 'CID_033_Athena_Commando_F_Medieval', emoji: 'RoyaleKnight' }, { name: 'Blue Squire', id: 'CID_032_Athena_Commando_M_Medieval', emoji: 'BlueSquire' }, { name: 'The Reaper', id: 'CID_084_Athena_Commando_M_Assassin', emoji: 'TheReaper' }, { name: 'Elite Agent', id: 'CID_083_Athena_Commando_F_Tactical', emoji: 'EliteAgent' }, { name: 'Rogue Agent', id: 'CID_090_Athena_Commando_M_Tactical', emoji: 'RogueAgent' }, { name: 'Omega', id: 'CID_116_Athena_Commando_M_CarbideBlack', emoji: 'Omega' }, // --- Ch2S2 Battle Pass gold styles --- { name: 'Skye', id: 'CID_690_Athena_Commando_F_Photographer', emoji: 'Skye' }, { name: 'TNTina', id: 'CID_691_Athena_Commando_F_TNTina', emoji: 'TNTina' }, { name: 'Meowscles', id: 'CID_693_Athena_Commando_M_BuffCat', emoji: 'Meowscles' }, { name: 'Midas', id: 'CID_694_Athena_Commando_M_CatBurglar', emoji: 'Midas' }, // --- Gaming Legends / Collab --- { name: 'Travis Scott', id: 'CID_703_Athena_Commando_M_Cyclone', emoji: 'TravisScott' }, { name: 'Astro Jack', id: 'CID_761_Athena_Commando_M_CycloneSpace', emoji: 'AstroJack' }, { name: 'Kratos', id: 'CID_964_Athena_Commando_M_Historian_869BC', emoji: 'Kratos' }, { name: 'Master Chief', id: 'CID_971_Athena_Commando_M_Jupiter_S0Z6M', emoji: 'MasterChief' }, // --- World Cup / Tournament --- { name: 'World Warrior', id: 'CID_478_Athena_Commando_F_WorldCup', emoji: 'WorldWarrior' }, // --- V-Bucks Card Promo --- { name: 'Color Splash Jellie', id: 'Character_TubaGummi', emoji: 'ColorSplashJellie' }, // --- Save the World Founders --- { name: 'Rose Team Leader', id: 'CID_096_Athena_Commando_F_Founder', emoji: 'RoseTeamLeader' }, { name: 'Warpaint', id: 'CID_095_Athena_Commando_M_Founder', emoji: 'Warpaint' }, ]; // ───────────────────────────────────────────────────────────────────────────── // VERIFY ROLE REQUIREMENTS // ───────────────────────────────────────────────────────────────────────────── const ROLE_REQUIREMENTS = [ // OG Season 1 skins (style required) { role: 'OG Renegade Raider', id: 'cid_028_athena_commando_f', styleTag: 'Mat3' }, { role: 'OG Aerial Assault Trooper', id: 'cid_017_athena_commando_m', styleTag: 'Stage2' }, { role: 'OG Ghoul Trooper', id: 'cid_029_athena_commando_f_halloween', styleTag: 'Mat3' }, { role: 'OG Skull Trooper', id: 'cid_030_athena_commando_m_halloween', styleTag: 'Mat1' }, // OG Battle Pass skins { role: 'Black Knight', id: 'cid_035_athena_commando_m_medieval' }, { role: 'Blue Squire', id: 'cid_032_athena_commando_m_medieval' }, { role: 'Royale Knight', id: 'cid_033_athena_commando_f_medieval' }, { role: 'Sparkle Specialist', id: 'cid_039_athena_commando_f_disco' }, { role: 'The Reaper', id: 'cid_084_athena_commando_m_assassin' }, { role: 'Elite Agent', id: 'cid_083_athena_commando_f_tactical' }, { role: 'Omega', id: 'cid_116_athena_commando_m_carbideblack', styleTag: 'Stage5' }, { role: 'Carbide', id: 'cid_115_athena_commando_m_carbideblue', styleTag: 'Stage5' }, // Samsung bundles { role: 'Galaxy', id: 'cid_175_athena_commando_m_celestial' }, { role: 'Stellar Axe', id: 'pickaxe_id_116_celestial' }, { role: 'Ikonik', id: 'cid_313_athena_commando_m_kpopfashion' }, { role: 'Scenario Emote', id: 'eid_kpopdance03' }, { role: 'Glow', id: 'cid_479_athena_commando_f_davinci' }, { role: 'Levitate Emote', id: 'eid_davinci' }, // Device / Platform exclusives { role: 'Honor Guard', id: 'cid_342_athena_commando_m_streetracermetallic' }, { role: 'Wonder', id: 'cid_434_athena_commando_f_stealthhonor' }, { role: 'Eon', id: 'cid_174_athena_commando_f_carbidewhite' }, { role: 'Resonator', id: 'pickaxe_id_077_carbidewhite' }, { role: 'Dark Vertex', id: 'cid_371_athena_commando_m_speedymidnight' }, { role: 'Double Helix', id: 'cid_183_athena_commando_m_modernmilitaryred' }, { role: 'Pinpoint', id: 'pickaxe_id_099_modernmilitaryred' }, { role: 'Neo Versa', id: 'cid_441_athena_commando_f_cyberscavengerblue' }, { role: 'Cobalt Snowfoot', id: 'character_snowninjadark' }, { role: 'Royale Bomber', id: 'cid_113_athena_commando_m_blueace' }, { role: 'Wildcat', id: 'cid_757_athena_commando_f_wildcat' }, { role: 'Stealth Reflex', id: 'cid_386_athena_commando_m_streetopsstealth' }, { role: 'Reflex', id: 'cid_259_athena_commando_m_streetops' }, { role: 'Angular Axe', id: 'pickaxe_id_118_streetops' }, { role: 'Surf Strider', id: 'cid_783_athena_commando_m_aquajacket' }, { role: 'Rogue Spider Knight', id: 'cid_516_athena_commando_m_blackwidowrogue' }, { role: 'Huntmaster Saber', id: 'character_masterkeyorder' }, // PlayStation Plus exclusives { role: 'Blue Team Leader', id: 'cid_052_athena_commando_f_psblue' }, { role: 'Blue Striker', id: 'cid_138_athena_commando_m_psburnout' }, { role: 'Carbon Commando', id: 'cid_360_athena_commando_m_techopsblue' }, { role: 'Prodigy', id: 'cid_296_athena_commando_m_math' }, { role: 'Cloud Striker', id: 'cid_926_athena_commando_f_streetfashiondiamond' }, { role: 'Fixer', id: 'cid_a_101_athena_commando_m_tacticalwoodlandblue' }, { role: 'Point Patroller', id: 'cid_711_athena_commando_m_longshorts' }, // Twitch Prime exclusives { role: 'Sub Commander', id: 'cid_085_athena_commando_m_twitch' }, { role: 'Havoc', id: 'cid_089_athena_commando_m_retrogrey' }, { role: 'Trailblazer', id: 'cid_114_athena_commando_f_tacticalwoodland' }, // Exclusive Pickaxes { role: "Raider's Revenge", id: 'pickaxe_lockjaw', styleTag: 'Stage2' }, { role: 'AC/DC', id: 'pickaxe_id_013_teslacoil' }, { role: 'Merry Mint Axe', id: 'pickaxe_id_294_candycane' }, { role: 'Tenderizer', id: 'pickaxe_id_044_tacticalurbanhammer' }, { role: 'Axe of Champions', id: 'pickaxe_id_376_fncs' }, { role: 'Blade of Champions', id: 'pickaxe_fncs_s32' }, // OG / Exclusive Emotes { role: 'Fresh Emote', id: 'eid_fresh' }, { role: 'Rambunctious Emote', id: 'eid_zippy' }, { role: 'Tidy Emote', id: 'eid_tidy' }, { role: 'Floss Emote', id: 'eid_floss' }, { role: 'Take the L Emote', id: 'eid_takethelathena' }, { role: 'Orange Justice Emote', id: 'eid_goodvibes' }, { role: 'Ride the Pony Emote', id: 'eid_ridethepony_athena' }, { role: 'Pony Up Emote', id: 'eid_ridetheponymultiple' }, { role: 'Evasive Maneuvers Emote', id: 'eid_temple' }, // Exclusive Emoticons { role: 'Cameo Needs Loot', id: 'emoji_s20_needloot' }, // Founder exclusives { role: 'Rose Team Leader', id: 'cid_096_athena_commando_f_founder' }, { role: 'Warpaint', id: 'cid_095_athena_commando_m_founder' }, { role: 'Fully Golden Peely', id: 'cid_701_athena_commando_m_bananaagent', styleTag: 'Stage4' }, // Samsung Galaxy bundle items { role: 'Galactic Disc', id: 'bid_138_celestial' }, { role: 'Discovery Glider', id: 'glider_id_090_celestial' }, // IKONIK bundle - Scenario already added above // Glow bundle - Levitate already added above // EON bundle { role: 'Aurora Glider', id: 'glider_id_056_carbidewhite' }, // Dark Vertex bundle { role: 'Dark Deflector', id: 'bid_234_speedymidnight' }, { role: 'Razor Edge', id: 'pickaxe_id_065_speedyred' }, { role: 'Dark Forerunner Glider', id: 'glider_id_131_speedymidnight' }, // Double Helix bundle { role: 'Telemetry', id: 'bid_114_modernmilitaryred' }, { role: 'Rotor Glider', id: 'glider_id_075_modernmilitaryred' }, // Neo Versa bundle { role: 'Neo Phrenzy', id: 'bid_288_cyberscavengerfemaleblue' }, // Royale Bomber bundle { role: 'Royale Air Glider', id: 'glider_id_105_snowboard' }, // Reflex bundle { role: 'Response Unit', id: 'bid_140_streetopsmale' }, { role: 'Pivot Glider', id: 'glider_id_092_streetops' }, // Wildcat bundle { role: 'Sleek Strike', id: 'bid_521_wildcat' }, // Rogue Spider Knight bundle { role: 'Time Keeper', id: 'bid_395_phantom' }, // Stealth Reflex bundle { role: 'Stealth Response Unit', id: 'bid_249_streetopsstealth' }, // Aerial Assault One glider (S1 exclusive) { role: 'Aerial Assault One', id: 'glider_id_001', styleTag: 'Stage2' }, // Extra exclusive pickaxes from master list { role: 'Trusty No. 2', id: 'pickaxe_id_029_assassin' }, { role: 'Party Animal', id: 'pickaxe_id_020_keg' }, { role: 'Crowbar', id: 'pickaxe_id_092_bling' }, { role: 'Instigator', id: 'pickaxe_id_039_tacticalblack' }, // Missing emotes from master list { role: "Widow's Pirouette Emote", id: 'eid_ashtonboardwalk' }, { role: 'Hot Marat Emote', id: 'eid_wir' }, { role: 'Kiss the Cup Emote', id: 'eid_trophycelebration' }, // Missing skins from master list { role: 'Gildedguy', id: 'cid_a_141_athena_commando_m_dojo' }, { role: 'Tart Tycoon', id: 'cid_851_athena_commando_m_bittenhead' }, // Gliders { role: 'Mako Glider', id: 'glider_warthog' }, // Rare / STW / Creator banners (checked via homebase_banners in common_core) { role: 'Alpha Tester Banner', bannerId: 'OtherBanner51' }, { role: 'World Cup Banner', bannerId: 'BRWorldCup2019' }, { role: 'Creator Banner', bannerId: 'InfluencerBanner1' }, { role: 'Creator Banner', bannerId: 'InfluencerBanner2' }, { role: 'Creator Banner', bannerId: 'InfluencerBanner3' }, { role: 'Creator Banner', bannerId: 'InfluencerBanner4' }, { role: 'Creator Banner', bannerId: 'InfluencerBanner5' }, { role: 'Creator Banner', bannerId: 'InfluencerBanner6' }, { role: 'Creator Banner', bannerId: 'InfluencerBanner7' }, { role: 'Creator Banner', bannerId: 'InfluencerBanner8' }, { role: 'Creator Banner', bannerId: 'InfluencerBanner9' }, // STW Founder banners { role: 'STW Founder Banner', bannerId: 'FounderTier5Banner1' }, { role: 'STW Founder Banner', bannerId: 'FounderTier4Banner1' }, { role: 'STW Founder Banner', bannerId: 'FounderTier3Banner1' }, { role: 'STW Founder Banner', bannerId: 'FounderTier2Banner1' }, { role: 'STW Founder Banner', bannerId: 'FounderTier1Banner1' }, // STW Endurance banners { role: 'STW Endurance Banner', bannerId: 'EnduranceWave30Theater04' }, { role: 'STW Endurance Banner', bannerId: 'EnduranceWave30Theater03' }, { role: 'STW Endurance Banner', bannerId: 'EnduranceWave30Theater02' }, { role: 'STW Endurance Banner', bannerId: 'EnduranceWave30Theater01' }, // STW Survival / Wargames banners { role: 'STW Event Banner', bannerId: 'SurvivalBannerStorm2018Hard' }, { role: 'STW Event Banner', bannerId: 'SurvivalBannerHolidayHard' }, { role: 'STW Event Banner', bannerId: 'WargamesTprifts' }, { role: 'STW Event Banner', bannerId: 'WargamesTiki' }, { role: 'STW Event Banner', bannerId: 'WargamesPods' }, // STW Achievement banners { role: 'STW Achievement Banner', bannerId: 'AchievementUnspeakableHorrors' }, { role: 'STW Achievement Banner', bannerId: 'AchievementWorldExplorer' }, { role: 'STW Achievement Banner', bannerId: 'AchievementGuardianAngel' }, { role: 'STW Achievement Banner', bannerId: 'AchievementPlaysWellWithOthers' }, { role: 'STW Achievement Banner', bannerId: 'AchievementTalentedBuilder' }, { role: 'STW Achievement Banner', bannerId: 'AchievementGoGnome' }, { role: 'STW Achievement Banner', bannerId: 'AchievementLootLegend' }, // FNCS / Skirmish / World Cup banners { role: 'FNCS Banner', bannerId: 'BRS23_FNCS_Drops' }, { role: 'FNCS Banner', bannerId: 'S11FNCS' }, { role: 'Skirmish Banner', bannerId: 'BRSkirmishBushBandits' }, { role: 'Skirmish Banner', bannerId: 'BRSkirmishDustyDogs' }, { role: 'Skirmish Banner', bannerId: 'BRSkirmishFortKnights' }, { role: 'Skirmish Banner', bannerId: 'BRSkirmishLuckyLlamas' }, { role: 'Skirmish Banner', bannerId: 'BRSkirmishRiftRaiders' }, // Season Level 100 banners { role: 'Season Level 100 Banner', bannerId: 'BRS10Level100' }, { role: 'Season Level 100 Banner', bannerId: 'BRS9Lvl100' }, { role: 'Season Level 100 Banner', bannerId: 'BRS8Level100' }, // Creator / Influencer banners 10-58 { role: 'Creator Banner', bannerId: 'InfluencerBanner10' }, { role: 'Creator Banner', bannerId: 'InfluencerBanner11' }, { role: 'Creator Banner', bannerId: 'InfluencerBanner12' }, { role: 'Creator Banner', bannerId: 'InfluencerBanner13' }, { role: 'Creator Banner', bannerId: 'InfluencerBanner14' }, { role: 'Creator Banner', bannerId: 'InfluencerBanner15' }, { role: 'Creator Banner', bannerId: 'InfluencerBanner16' }, { role: 'Creator Banner', bannerId: 'InfluencerBanner17' }, { role: 'Creator Banner', bannerId: 'InfluencerBanner18' }, { role: 'Creator Banner', bannerId: 'InfluencerBanner19' }, { role: 'Creator Banner', bannerId: 'InfluencerBanner20' }, { role: 'Creator Banner', bannerId: 'InfluencerBanner21' }, { role: 'Creator Banner', bannerId: 'InfluencerBanner22' }, { role: 'Creator Banner', bannerId: 'InfluencerBanner23' }, { role: 'Creator Banner', bannerId: 'InfluencerBanner24' }, { role: 'Creator Banner', bannerId: 'InfluencerBanner25' }, { role: 'Creator Banner', bannerId: 'InfluencerBanner26' }, { role: 'Creator Banner', bannerId: 'InfluencerBanner27' }, { role: 'Creator Banner', bannerId: 'InfluencerBanner28' }, { role: 'Creator Banner', bannerId: 'InfluencerBanner29' }, { role: 'Creator Banner', bannerId: 'InfluencerBanner30' }, { role: 'Creator Banner', bannerId: 'InfluencerBanner31' }, { role: 'Creator Banner', bannerId: 'InfluencerBanner32' }, { role: 'Creator Banner', bannerId: 'InfluencerBanner33' }, { role: 'Creator Banner', bannerId: 'InfluencerBanner34' }, { role: 'Creator Banner', bannerId: 'InfluencerBanner35' }, { role: 'Creator Banner', bannerId: 'InfluencerBanner36' }, { role: 'Creator Banner', bannerId: 'InfluencerBanner37' }, { role: 'Creator Banner', bannerId: 'InfluencerBanner38' }, { role: 'Creator Banner', bannerId: 'InfluencerBanner39' }, { role: 'Creator Banner', bannerId: 'InfluencerBanner40' }, { role: 'Creator Banner', bannerId: 'InfluencerBanner41' }, { role: 'Creator Banner', bannerId: 'InfluencerBanner42' }, { role: 'Creator Banner', bannerId: 'InfluencerBanner43' }, { role: 'Creator Banner', bannerId: 'InfluencerBanner44' }, { role: 'Creator Banner', bannerId: 'InfluencerBanner45' }, { role: 'Creator Banner', bannerId: 'InfluencerBanner46' }, { role: 'Creator Banner', bannerId: 'InfluencerBanner47' }, { role: 'Creator Banner', bannerId: 'InfluencerBanner48' }, { role: 'Creator Banner', bannerId: 'InfluencerBanner49' }, { role: 'Creator Banner', bannerId: 'InfluencerBanner50' }, { role: 'Creator Banner', bannerId: 'InfluencerBanner51' }, { role: 'Creator Banner', bannerId: 'InfluencerBanner52' }, { role: 'Creator Banner', bannerId: 'InfluencerBanner53' }, { role: 'Creator Banner', bannerId: 'InfluencerBanner54' }, { role: 'Creator Banner', bannerId: 'InfluencerBanner55' }, { role: 'Creator Banner', bannerId: 'InfluencerBanner56' }, { role: 'Creator Banner', bannerId: 'InfluencerBanner57' }, { role: 'Creator Banner', bannerId: 'InfluencerBanner58' }, // OT Banners { role: 'OT1 Banner', bannerId: 'OT1Banner' }, { role: 'OT2 Banner', bannerId: 'OT2Banner' }, { role: 'OT3 Banner', bannerId: 'OT3Banner' }, { role: 'OT4 Banner', bannerId: 'OT4Banner' }, { role: 'OT5 Banner', bannerId: 'OT5Banner' }, { role: 'OT6 Banner', bannerId: 'OT6Banner' }, { role: 'OT7 Banner', bannerId: 'OT7Banner' }, { role: 'OT8 Banner', bannerId: 'OT8Banner' }, { role: 'OT9 Banner', bannerId: 'OT9Banner' }, { role: 'OT10 Banner', bannerId: 'OT10Banner' }, { role: 'OT11 Banner', bannerId: 'OT11Banner' }, // STW Founders { role: 'STW Founders', id: 'cid_095_athena_commando_m_founder' }, { role: 'Ultimate STW Founders', id: 'tiertoken_collectionbook_ultimate', stw: true }, ] // ───────────────────────────────────────────────────────────────────────────── // EPIC OAUTH CONSTANTS // ───────────────────────────────────────────────────────────────────────────── const SWITCH_TOKEN = 'OThmN2U0MmMyZTNhNGY4NmE3NGViNDNmYmI0MWVkMzk6MGEyNDQ5YTItMDAxYS00NTFlLWFmZWMtM2U4MTI5MDFjNGQ3'; const IOS_TOKEN = 'M2Y2OWU1NmM3NjQ5NDkyYzhjYzI5ZjFhZjA4YThhMTI6YjUxZWU5Y2IxMjIzNGY1MGE2OWVmYTY3ZWY1MzgxMmU='; const EPIC_OAUTH = 'https://account-public-service-prod.ol.epicgames.com/account/api/oauth/token'; const EPIC_OAUTH_PROD03 = 'https://account-public-service-prod03.ol.epicgames.com/account/api/oauth/token'; const EPIC_EXCHANGE = 'https://account-public-service-prod03.ol.epicgames.com/account/api/oauth/exchange'; const EPIC_DEVICE_AUTH = 'https://account-public-service-prod03.ol.epicgames.com/account/api/oauth/deviceAuthorization'; const pending = new Map(); const verifiedUsers = new Map(); // userId -> { guildId, assignedRoles: string[], epicName: string } const LOG_CHANNEL_ID = '1517695849952706642'; // ───────────────────────────────────────────────────────────────────────────── // SHARED HELPERS // ───────────────────────────────────────────────────────────────────────────── async function fetchWithTimeout(url, ms = 12000) { const ctrl = new AbortController(); const timer = setTimeout(() => ctrl.abort(), ms); try { return await fetch(url, { signal: ctrl.signal }); } finally { clearTimeout(timer); } } // ─── get icon URL — build directly from known CDN pattern, no API roundtrip ── async function fetchIconUrl(cosmeticId) { // Direct CDN URL — fortnite-api.com images are always at this pattern const url = `https://fortnite-api.com/images/cosmetics/br/${cosmeticId.toLowerCase()}/icon.png`; const res = await fetchWithTimeout(url, 8000); if (res.ok) return url; // Fallback to API lookup if direct URL 404s const apiRes = await fetchWithTimeout(`https://fortnite-api.com/v2/cosmetics/br/${cosmeticId}`, 8000); if (!apiRes.ok) throw new Error(`API ${apiRes.status} for ${cosmeticId}`); const json = await apiRes.json(); if (!json.data?.images?.icon) throw new Error(`No icon for ${cosmeticId}`); return json.data.images.icon; } async function downloadAndProcess(imageUrl) { const res = await fetchWithTimeout(imageUrl, 15000); if (!res.ok) throw new Error(`Download failed ${res.status}`); const raw = Buffer.from(await res.arrayBuffer()); return sharp(raw) .resize(128, 128, { fit: 'cover', position: 'centre', kernel: 'lanczos3' }) .sharpen({ sigma: 0.8, m1: 1.0, m2: 2.0 }) .png({ compressionLevel: 6 }) .toBuffer(); } async function createEmojiSafe(guild, buffer, name, reason) { for (let attempt = 1; attempt <= 5; attempt++) { try { return await guild.emojis.create({ attachment: buffer, name, reason }); } catch (err) { const isRL = err.status === 429 || err.httpStatus === 429 || (err.message || '').toLowerCase().includes('rate limit'); if (isRL) { const wait = ((err.retryAfter || 5) * 1000) + 1000; console.warn(` ⏳ Rate limited "${name}", waiting ${Math.ceil(wait/1000)}s...`); await new Promise(r => setTimeout(r, wait)); } else { throw err; } } } throw new Error(`Gave up on emoji "${name}"`); } // ─── Epic helpers ───────────────────────────────────────────────────────────── async function getSwitchAccessToken() { const res = await fetch(EPIC_OAUTH, { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded', 'Authorization': `basic ${SWITCH_TOKEN}` }, body: 'grant_type=client_credentials', }); return (await res.json()).access_token; } async function createDeviceCode(token) { const res = await fetch(EPIC_DEVICE_AUTH, { method: 'POST', headers: { 'Authorization': `bearer ${token}`, 'Content-Type': 'application/x-www-form-urlencoded' }, }); return res.json(); } async function waitForLogin(deviceCode, maxMs = 300000) { const start = Date.now(); while (Date.now() - start < maxMs) { await new Promise(r => setTimeout(r, 10000)); const res = await fetch(EPIC_OAUTH_PROD03, { method: 'POST', headers: { 'Authorization': `basic ${SWITCH_TOKEN}`, 'Content-Type': 'application/x-www-form-urlencoded' }, body: `grant_type=device_code&device_code=${deviceCode}`, }); const data = await res.json(); if (res.status === 200 && data.access_token) return data; const err = data.errorCode || ''; if (err.includes('not_found') || err.includes('expired')) return null; } return null; } async function exchangeForIosToken(accessToken) { const exRes = await fetch(EPIC_EXCHANGE, { headers: { 'Authorization': `bearer ${accessToken}` } }); const exData = await exRes.json(); const res = await fetch(EPIC_OAUTH_PROD03, { method: 'POST', headers: { 'Authorization': `basic ${IOS_TOKEN}`, 'Content-Type': 'application/x-www-form-urlencoded' }, body: `grant_type=exchange_code&exchange_code=${exData.code}`, }); return res.json(); } async function getProfile(accountId, accessToken, profileId) { const url = `https://fortnite-public-service-prod11.ol.epicgames.com/fortnite/api/game/v2/profile/${accountId}/client/QueryProfile?profileId=${profileId}&rvn=-1`; const res = await fetch(url, { method: 'POST', headers: { 'Authorization': `bearer ${accessToken}`, 'Content-Type': 'application/json' }, body: '{}', }); return res.json(); } // ───────────────────────────────────────────────────────────────────────────── // SKIN CHECKER IMAGE RENDERER (RiftCheckerBot style) // ───────────────────────────────────────────────────────────────────────────── const { createCanvas, loadImage, GlobalFonts } = require('@napi-rs/canvas'); const path = require('path'); // Register Fortnite font (Burbank Big Condensed Black) + Tahoma GlobalFonts.registerFromPath( path.join(__dirname, 'BurbankBigCondensed-Black.ttf'), 'Burbank' ); GlobalFonts.registerFromPath('C:/Windows/Fonts/tahoma.ttf', 'Tahoma'); GlobalFonts.registerFromPath('C:/Windows/Fonts/tahomabd.ttf', 'TahomaB'); // Custom background URL — set with !setbg , cleared with !clearbg let customBgUrl = null; const ICON = 96; const COLS = 12; // wider rows = shorter images const LABEL = 26; const CELL_H = ICON + LABEL; const PAD = 14; const HDR_H = 72; const FTR_H = 36; const RC = { common: '#c8c8c8', uncommon: '#60aa3a', rare: '#4990e2', epic: '#874bd2', legendary: '#d3782e', mythic: '#ffd700', icon: '#54ceec', marvel: '#e63c3c', dark: '#641464', frozen: '#94dffc', lava: '#ca4d1d', shadow: '#505050', gaming: '#1a8e48', slurp: '#00e9b0', }; const OG_STYLE_ICONS = { 'cid_029_athena_commando_f_halloween': 'https://fortnite-api.com/images/cosmetics/br/cid_029_athena_commando_f_halloween/variants/material/mat3.png', 'cid_030_athena_commando_m_halloween': 'https://fortnite-api.com/images/cosmetics/br/cid_030_athena_commando_m_halloween/variants/clothingcolor/mat1.png', 'cid_028_athena_commando_f': 'https://fortnite-api.com/images/cosmetics/br/cid_028_athena_commando_f/variants/material/mat3.png', 'cid_701_athena_commando_m_bananaagent': 'https://fortnite-api.com/images/cosmetics/br/cid_701_athena_commando_m_bananaagent/variants/progressive/stage4.png', 'cid_116_athena_commando_m_carbideblack': 'https://fortnite-api.com/images/cosmetics/br/cid_116_athena_commando_m_carbideblack/variants/parts/stage1.png', 'cid_694_athena_commando_m_catburglar': 'https://fortnite-api.com/images/cosmetics/br/cid_694_athena_commando_m_catburglar/variants/progressive/stage4.png', 'cid_693_athena_commando_m_buffcat': 'https://fortnite-api.com/images/cosmetics/br/cid_693_athena_commando_m_buffcat/variants/progressive/stage4.png', 'cid_691_athena_commando_f_tntina': 'https://fortnite-api.com/images/cosmetics/br/cid_691_athena_commando_f_tntina/variants/progressive/stage7.png', 'cid_690_athena_commando_f_photographer': 'https://fortnite-api.com/images/cosmetics/br/cid_690_athena_commando_f_photographer/variants/progressive/stage4.png', }; // Cache the logo so we only fetch once per session let _logoBuffer = null; async function getLogoBuffer() { if (_logoBuffer) return _logoBuffer; try { const res = await fetchWithTimeout( 'https://fortnite-api.com/images/cosmetics/br/cid_028_athena_commando_f/variants/material/mat3.png', 8000 ); if (res.ok) _logoBuffer = Buffer.from(await res.arrayBuffer()); } catch { _logoBuffer = null; } return _logoBuffer; } const CACHE_DIR = path.join(__dirname, 'RiftCheckerBot-main', 'cache'); const RARITY_DIR = path.join(__dirname, 'RiftCheckerBot-main', 'styles', 'rift', 'rarity'); const ICON_DIR = path.join(__dirname, 'RiftCheckerBot-main', 'cosmetic_icons'); const fs = require('fs'); // Cosmetics that have special OG variant images in local cache // key = lowercase cosmetic ID, value = local cache filename const LOCAL_OG_ICONS = { 'cid_029_athena_commando_f_halloween': 'pink_ghoul.png', // Pink Ghoul 'cid_030_athena_commando_m_halloween': 'purple_skull.png', // Purple Skull 'cid_028_athena_commando_f': 'og_rene.png', // Black & Gold Renegade Raider 'cid_017_athena_commando_m': 'og_aat.png', // OG Aerial Assault Trooper 'cid_116_athena_commando_m_carbideblack': 'omega_max.png', // Max Omega 'cid_701_athena_commando_m_bananaagent': 'gold_peely.png', // Gold Agent Peely }; // Remote OG variant URLs for cosmetics not in local cache const REMOTE_OG_ICONS = { 'pickaxe_lockjaw': 'https://fortnite-api.com/images/cosmetics/br/pickaxe_lockjaw/variants/particle/particle1.png', 'glider_id_001': 'https://fortnite-api.com/images/cosmetics/br/glider_id_001/variants/particle/particle1.png', 'cid_694_athena_commando_m_catburglar': 'https://fortnite-api.com/images/cosmetics/br/cid_694_athena_commando_m_catburglar/variants/progressive/stage4.png', 'cid_693_athena_commando_m_buffcat': 'https://fortnite-api.com/images/cosmetics/br/cid_693_athena_commando_m_buffcat/variants/progressive/stage4.png', 'cid_691_athena_commando_f_tntina': 'https://fortnite-api.com/images/cosmetics/br/cid_691_athena_commando_f_tntina/variants/progressive/stage7.png', 'cid_690_athena_commando_f_photographer': 'https://fortnite-api.com/images/cosmetics/br/cid_690_athena_commando_f_photographer/variants/progressive/stage4.png', }; // Load a rarity background image from local cache const rarityImageCache = {}; async function getRarityImage(rarity) { const key = (rarity || 'common').toLowerCase(); if (rarityImageCache[key]) return rarityImageCache[key]; const filePath = path.join(RARITY_DIR, `${key}.png`); if (fs.existsSync(filePath)) { try { const img = await loadImage(fs.readFileSync(filePath)); rarityImageCache[key] = img; return img; } catch { /* fall through */ } } // fallback to common const fallback = path.join(RARITY_DIR, 'common.png'); if (fs.existsSync(fallback)) { const img = await loadImage(fs.readFileSync(fallback)); rarityImageCache['common'] = img; return img; } return null; } // Get cosmetic icon — local cache first, then remote async function fetchSkinImageBuffer(cosmeticId, useOgStyle = false) { const id = cosmeticId.toLowerCase(); // 1. Try local OG variant if requested if (useOgStyle && LOCAL_OG_ICONS[id]) { const localPath = path.join(CACHE_DIR, LOCAL_OG_ICONS[id]); if (fs.existsSync(localPath)) return fs.readFileSync(localPath); } // 2. Try remote OG variant if requested if (useOgStyle && REMOTE_OG_ICONS[id]) { try { const res = await fetchWithTimeout(REMOTE_OG_ICONS[id], 8000); if (res.ok) return Buffer.from(await res.arrayBuffer()); } catch { /* fall through */ } } // 3. Check if this is a banner ID — fetch from banner API const bannerPrefixes = ['ot','influencerbanner','foundertier','brs','survival','endurance','wargames','achievement','otherbanner','skirmish','worldcup']; if (bannerPrefixes.some(p => id.startsWith(p))) { // Try local RiftCheckerBot cache first try { const files = fs.readdirSync(CACHE_DIR); const match = files.find(f => f.toLowerCase() === id + '.png' || f.toLowerCase() === cosmeticId + '.png'); if (match) return fs.readFileSync(path.join(CACHE_DIR, match)); } catch { /* fall through */ } // Fetch from banner API try { const bMeta = await getBannerMeta(); const iconUrl = bMeta[id]?.icon; if (iconUrl) { const res = await fetchWithTimeout(iconUrl, 8000); if (res.ok) return Buffer.from(await res.arrayBuffer()); } } catch { /* fall through */ } return null; } // 4. Try local cache with exact cosmetic ID match try { const files = fs.readdirSync(CACHE_DIR); const baseName = cosmeticId + '.png'; const match = files.find(f => f.toLowerCase() === baseName.toLowerCase()); if (match) return fs.readFileSync(path.join(CACHE_DIR, match)); } catch { /* fall through */ } // 5. Fortnite API CDN const iconUrl = `https://fortnite-api.com/images/cosmetics/br/${id}/icon.png`; try { const res = await fetchWithTimeout(iconUrl, 8000); if (res.ok) return Buffer.from(await res.arrayBuffer()); } catch { /* fall through */ } // 6. Fallback: smallIcon const smallUrl = `https://fortnite-api.com/images/cosmetics/br/${id}/smallicon.png`; try { const res = await fetchWithTimeout(smallUrl, 8000); if (res.ok) return Buffer.from(await res.arrayBuffer()); } catch { /* give up */ } return null; } async function renderLockerImage(items, countLabel, subtitle, submittedBy = null, ogStyleOwned = null) { if (!items || items.length === 0) return null; const LOGO_SIZE = HDR_H - 8; const rows = Math.ceil(items.length / COLS); const cols = Math.min(items.length, COLS); const W = Math.max(cols * ICON + PAD * 2, 400); const H = HDR_H + rows * CELL_H + PAD + FTR_H; const canvas = createCanvas(W, H); const ctx = canvas.getContext('2d'); // ── Background ─────────────────────────────────────────────────────────── if (customBgUrl) { try { const res = await fetchWithTimeout(customBgUrl, 10000); if (res.ok) { let rawBuf = Buffer.from(await res.arrayBuffer()); const contentType = res.headers.get('content-type') || ''; const isGif = customBgUrl.toLowerCase().endsWith('.gif') || contentType.includes('gif'); // For GIFs extract first frame; for everything else just resize const processed = await sharp(rawBuf, { animated: isGif }) .resize(W, H, { fit: 'cover', position: 'centre' }) .png() .toBuffer(); ctx.drawImage(await loadImage(processed), 0, 0, W, H); ctx.fillStyle = 'rgba(0,0,0,0.55)'; ctx.fillRect(0, 0, W, H); } else { throw new Error('bg fetch failed'); } } catch { const bg = ctx.createLinearGradient(0, 0, 0, H); bg.addColorStop(0, '#1c1c28'); bg.addColorStop(1, '#0e0e18'); ctx.fillStyle = bg; ctx.fillRect(0, 0, W, H); } } else { const bg = ctx.createLinearGradient(0, 0, 0, H); bg.addColorStop(0, '#1c1c28'); bg.addColorStop(1, '#0e0e18'); ctx.fillStyle = bg; ctx.fillRect(0, 0, W, H); } // Grid lines ctx.strokeStyle = 'rgba(255,255,255,0.05)'; ctx.lineWidth = 1; for (let c = 0; c <= cols; c++) { ctx.beginPath(); ctx.moveTo(PAD + c * ICON, HDR_H); ctx.lineTo(PAD + c * ICON, HDR_H + rows * CELL_H); ctx.stroke(); } for (let r = 0; r <= rows; r++) { ctx.beginPath(); ctx.moveTo(PAD, HDR_H + r * CELL_H); ctx.lineTo(PAD + cols * ICON, HDR_H + r * CELL_H); ctx.stroke(); } // ── Logo ───────────────────────────────────────────────────────────────── const logoBuf = await getLogoBuffer(); let logoEndX = PAD; if (logoBuf) { try { const logoResized = await sharp(logoBuf).resize(LOGO_SIZE, LOGO_SIZE, { fit: 'cover' }).png().toBuffer(); const logoImg = await loadImage(logoResized); const cx = PAD + LOGO_SIZE / 2, cy = 4 + LOGO_SIZE / 2, lr = LOGO_SIZE / 2; ctx.save(); ctx.beginPath(); ctx.arc(cx, cy, lr, 0, Math.PI * 2); ctx.clip(); ctx.drawImage(logoImg, PAD, 4, LOGO_SIZE, LOGO_SIZE); ctx.restore(); ctx.strokeStyle = '#ffd700'; ctx.lineWidth = 2; ctx.beginPath(); ctx.arc(cx, cy, lr, 0, Math.PI * 2); ctx.stroke(); logoEndX = PAD + LOGO_SIZE + 10; } catch { logoEndX = PAD; } } // ── Header text ─────────────────────────────────────────────────────────── ctx.fillStyle = '#ffffff'; ctx.font = 'bold 26px TahomaB'; ctx.fillText(countLabel, logoEndX, 40); ctx.fillStyle = '#aaaaaa'; ctx.font = '13px Tahoma'; ctx.fillText(subtitle.toUpperCase(), logoEndX, 60); // ── Cosmetic grid ───────────────────────────────────────────────────────── let idx = 0; for (const item of items) { const col = idx % COLS; const row = Math.floor(idx / COLS); const x = PAD + col * ICON; const y = HDR_H + row * CELL_H; const id = item.id.toLowerCase(); // Determine if we should show OG style for this item const hasOgRequirement = !!OG_STYLE_REQUIREMENTS[id]; const ownsOgStyle = ogStyleOwned ? ogStyleOwned.has(id) : false; const useOg = hasOgRequirement && ownsOgStyle; // Effective rarity: mythic if OG style owned, otherwise use metaMap rarity const effectiveRarity = useOg ? 'mythic' : (item.rarity || 'common'); // ── Draw rarity background from local cache ─────────────────────────── const rarityFilePath = path.join(RARITY_DIR, `${effectiveRarity}.png`); if (fs.existsSync(rarityFilePath)) { try { const rarBuf = await sharp(fs.readFileSync(rarityFilePath)) .resize(ICON, ICON, { fit: 'fill' }).png().toBuffer(); ctx.drawImage(await loadImage(rarBuf), x, y, ICON, ICON); } catch { /* skip */ } } else { ctx.fillStyle = (RC[effectiveRarity] || RC.common) + '28'; ctx.fillRect(x, y, ICON, ICON); } // ── Gradient border around the cell ────────────────────────────────── const borderColor = RC[effectiveRarity] || RC.common; // Top edge gradient (left to right) const topGrad = ctx.createLinearGradient(x, y, x + ICON, y); topGrad.addColorStop(0, borderColor + '00'); topGrad.addColorStop(0.3, borderColor + 'ff'); topGrad.addColorStop(0.7, borderColor + 'ff'); topGrad.addColorStop(1, borderColor + '00'); ctx.strokeStyle = topGrad; ctx.lineWidth = 2; ctx.beginPath(); ctx.moveTo(x, y); ctx.lineTo(x + ICON, y); ctx.stroke(); // Bottom edge gradient const botGrad = ctx.createLinearGradient(x, y + ICON, x + ICON, y + ICON); botGrad.addColorStop(0, borderColor + '00'); botGrad.addColorStop(0.3, borderColor + 'ff'); botGrad.addColorStop(0.7, borderColor + 'ff'); botGrad.addColorStop(1, borderColor + '00'); ctx.strokeStyle = botGrad; ctx.beginPath(); ctx.moveTo(x, y + ICON); ctx.lineTo(x + ICON, y + ICON); ctx.stroke(); // Left edge gradient (top to bottom) const leftGrad = ctx.createLinearGradient(x, y, x, y + ICON); leftGrad.addColorStop(0, borderColor + '00'); leftGrad.addColorStop(0.3, borderColor + 'ff'); leftGrad.addColorStop(0.7, borderColor + 'ff'); leftGrad.addColorStop(1, borderColor + '00'); ctx.strokeStyle = leftGrad; ctx.beginPath(); ctx.moveTo(x, y); ctx.lineTo(x, y + ICON); ctx.stroke(); // Right edge gradient const rightGrad = ctx.createLinearGradient(x + ICON, y, x + ICON, y + ICON); rightGrad.addColorStop(0, borderColor + '00'); rightGrad.addColorStop(0.3, borderColor + 'ff'); rightGrad.addColorStop(0.7, borderColor + 'ff'); rightGrad.addColorStop(1, borderColor + '00'); ctx.strokeStyle = rightGrad; ctx.beginPath(); ctx.moveTo(x + ICON, y); ctx.lineTo(x + ICON, y + ICON); ctx.stroke(); // ── Draw cosmetic icon ──────────────────────────────────────────────── const buf = await fetchSkinImageBuffer(item.id, useOg); if (buf) { try { const resized = await sharp(buf).resize(ICON - 6, ICON - 6, { fit: 'cover' }).png().toBuffer(); ctx.drawImage(await loadImage(resized), x + 3, y + 3, ICON - 6, ICON - 6); } catch { /* skip */ } } // ── Name label ──────────────────────────────────────────────────────── ctx.fillStyle = '#ffffff'; ctx.font = '10px Tahoma'; ctx.textAlign = 'center'; const maxW = ICON - 4; let name = item.name || ''; while (name.length > 3 && ctx.measureText(name).width > maxW) name = name.slice(0, -1); if (name !== item.name) name = name.slice(0, -1) + '..'; ctx.fillText(name, x + ICON / 2, y + ICON + 17, maxW); ctx.textAlign = 'left'; idx++; } // ── Footer ──────────────────────────────────────────────────────────────── const footerY = H - FTR_H; ctx.fillStyle = 'rgba(0,0,0,0.6)'; ctx.fillRect(0, footerY, W, FTR_H); ctx.strokeStyle = '#ffd700'; ctx.lineWidth = 1; ctx.beginPath(); ctx.moveTo(0, footerY); ctx.lineTo(W, footerY); ctx.stroke(); ctx.fillStyle = '#ffffff'; ctx.font = 'bold 11px TahomaB'; ctx.fillText(submittedBy ? `Submitted by: ${submittedBy}` : 'Made by jitterbuxg', PAD, footerY + 14); ctx.fillStyle = '#ffd700'; ctx.font = '10px Tahoma'; ctx.fillText('.gg/collecting', PAD, footerY + 28); ctx.fillStyle = '#888888'; ctx.font = '10px Tahoma'; ctx.textAlign = 'right'; ctx.fillText(new Date().toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' }), W - PAD, footerY + 21); ctx.textAlign = 'left'; return canvas.toBuffer('image/png'); } // Banner metadata cache — fetched once from fortnite-api.com/v1/banners let bannerMetaCache = null; async function getBannerMeta() { if (bannerMetaCache) return bannerMetaCache; try { const res = await fetchWithTimeout('https://fortnite-api.com/v1/banners', 10000); if (res.ok) { const json = await res.json(); const map = {}; for (const b of (json.data || [])) { map[b.id.toLowerCase()] = { name: b.devName || b.id, icon: b.images?.icon || b.images?.smallIcon, rarity: 'common', }; } bannerMetaCache = map; return map; } } catch { /* ignore */ } return {}; } const BASE_EXCLUSIVE_BANNER_IDS = [ // OT Banners 'ot1banner','ot2banner','ot3banner','ot4banner','ot5banner', 'ot6banner','ot7banner','ot8banner','ot9banner','ot10banner','ot11banner', // Alpha tester / rare 'otherbanner51', // World Cup / FNCS / Skirmish 'brworldcup2019','brs9worldcup2019','brs23_fncs_drops','s11fncs', 'brskirmishbushbandits','brskirmishdustydogs','brskirmishfortknights', 'brskirmishluckyllamas','brskirmishriftraiders', // Season level 100 'brs8level100','brs9lvl100','brs10level100', ]; function looksLikeBannerId(rawId) { const id = String(rawId || '').trim().toLowerCase(); if (!id) return false; return ( id.includes('banner') || id.startsWith('ot') || id.startsWith('brs') || id.startsWith('brseason') || id.startsWith('brworldcup') || id.startsWith('brskirmish') || id.startsWith('endurance') || id.startsWith('wargames') || id.startsWith('survival') || id.startsWith('achievement') || id.startsWith('starlight') || id.startsWith('winter2018') || id.startsWith('newsletter') || id.startsWith('horde') ); } function getExclusiveBannerIds() { const ids = new Set(BASE_EXCLUSIVE_BANNER_IDS); for (const req of ROLE_REQUIREMENTS) { if (req.bannerId) ids.add(req.bannerId.toLowerCase()); } try { const listPath = path.join(__dirname, 'RiftCheckerBot-main', 'exclusive.txt'); const lines = fs.readFileSync(listPath, 'utf8').split(/\r?\n/); for (const line of lines) { const id = line.trim().toLowerCase(); if (looksLikeBannerId(id)) ids.add(id); } } catch { /* bundled list is optional */ } return ids; } function commonCoreOwnsId(ccItems, wantedId) { const needle = wantedId.toLowerCase(); return Object.values(ccItems || {}).some(item => { const templateId = (item.templateId || '').toLowerCase(); const itemId = templateId.split(':').pop(); return itemId === needle || templateId.includes(needle); }); } function collectOwnedExclusiveBannerItems(ccItems, bannerMeta, metaMap = null) { const items = []; const seen = new Set(); for (const id of getExclusiveBannerIds()) { if (seen.has(id) || !commonCoreOwnsId(ccItems, id)) continue; seen.add(id); const info = bannerMeta[id] || {}; const name = info.name || id; items.push({ id, name, rarity: 'mythic' }); if (metaMap) metaMap[id] = { name, rarity: 'mythic' }; } return items; } // Batch fetch cosmetic metadata (name + rarity) for up to 50 IDs at once async function fetchCosmeticMeta(ids) { const map = {}; if (!ids || ids.length === 0) return map; for (let i = 0; i < ids.length; i += 50) { const chunk = ids.slice(i, i + 50); try { const qs = chunk.map(id => `id=${encodeURIComponent(id)}`).join('&'); const res = await fetchWithTimeout(`https://fortnite-api.com/v2/cosmetics/br/search/ids?language=en&${qs}`, 10000); if (!res.ok) continue; const json = await res.json(); for (const item of (json.data || [])) { map[item.id.toLowerCase()] = { name: item.name, rarity: item.rarity?.value || 'common' }; } } catch { /* skip chunk on error */ } } return map; } // Cosmetics that have OG style requirements - id -> one or more owned style tags const OG_STYLE_REQUIREMENTS = { 'cid_029_athena_commando_f_halloween': ['Mat3'], // Pink Ghoul 'cid_030_athena_commando_m_halloween': ['Mat1'], // Purple Skull 'cid_028_athena_commando_f': ['Mat3'], // Black & Gold Renegade Raider 'cid_017_athena_commando_m': ['Stage2'], // OG Aerial Assault Trooper 'cid_116_athena_commando_m_carbideblack': ['Stage5'], // Max Omega 'cid_701_athena_commando_m_bananaagent': ['Stage4'], // Gold Agent Peely 'pickaxe_lockjaw': ['Stage2'], // Black & Gold Raider's Revenge 'glider_id_001': ['Stage2'], // OG Aerial Assault One }; function requiredStyleTags(reqOrTags) { if (!reqOrTags) return []; if (Array.isArray(reqOrTags)) return reqOrTags; if (Array.isArray(reqOrTags.styleTags)) return reqOrTags.styleTags; return reqOrTags.styleTag ? [reqOrTags.styleTag] : []; } function ownsAnyStyleTag(ownedStyleSet, tags) { const wanted = requiredStyleTags(tags).map(tag => String(tag).toLowerCase()); if (wanted.length === 0) return true; if (!ownedStyleSet) return false; const owned = [...ownedStyleSet].map(tag => String(tag).toLowerCase()); return wanted.some(tag => owned.includes(tag)); } function qualifiesForRequirement(req, ownedMap) { const id = req.id.toLowerCase(); if (!ownedMap[id]) return false; return ownsAnyStyleTag(ownedMap[id], req); } function uniqueIds(ids) { return [...new Set(ids.filter(Boolean))]; } function buildOwnedMap(athenaData) { const owned = {}; // id -> Set of owned style tags const ogStyleOwned = new Set(); const outfits = []; const pickaxes = []; const emotes = []; const emoticons = []; // AthenaEmoji type (in-game emoticons, NOT emotes) const backblings = []; const gliders = []; const sprays = []; const items = athenaData?.profileChanges?.[0]?.profile?.items || {}; for (const item of Object.values(items)) { const tpl = (item.templateId || '').toLowerCase(); if (!tpl.startsWith('athena') && !tpl.startsWith('character')) continue; const id = tpl.split(':')[1] || ''; if (!owned[id]) owned[id] = new Set(); for (const v of (item.attributes?.variants || [])) for (const tag of (v.owned || [])) owned[id].add(tag); if (tpl.startsWith('athenacharacter:') || tpl.startsWith('character:')) outfits.push(id); else if (tpl.startsWith('athenapickaxe:')) pickaxes.push(id); else if (tpl.startsWith('athenadance:')) emotes.push(id); else if (tpl.startsWith('athenaemoji:')) emoticons.push(id); else if (tpl.startsWith('athenabackpack:')) backblings.push(id); else if (tpl.startsWith('athenaglider:')) gliders.push(id); else if (tpl.startsWith('athenasprays:') || tpl.startsWith('athenaspray:')) sprays.push(id); } // Populate ogStyleOwned after all variants are collected for (const id of Object.keys(owned)) { if (OG_STYLE_REQUIREMENTS[id] && ownsAnyStyleTag(owned[id], OG_STYLE_REQUIREMENTS[id])) { ogStyleOwned.add(id); } } return { owned, ogStyleOwned, outfits: uniqueIds(outfits), pickaxes: uniqueIds(pickaxes), emotes: uniqueIds(emotes), emoticons: uniqueIds(emoticons), backblings: uniqueIds(backblings), gliders: uniqueIds(gliders), sprays: uniqueIds(sprays), }; } function getRolesForLocker(ownedMap, ccItems) { const earned = []; for (const req of ROLE_REQUIREMENTS) { if (req.bannerId) { const has = Object.values(ccItems).some(i => (i.templateId || '').toLowerCase().includes(req.bannerId.toLowerCase()) ); if (has) earned.push(req.role); continue; } if (req.stw) { const ccIds = Object.values(ccItems).map(i => (i.templateId || '').toLowerCase()); if (ccIds.some(cc => cc.includes(req.id.toLowerCase()))) earned.push(req.role); continue; } if (qualifiesForRequirement(req, ownedMap)) earned.push(req.role); } const ccIds = Object.values(ccItems).map(i => (i.templateId || '').toLowerCase()); const hasFounder = ccIds.some(cc => cc.includes('founder')); const hasUltimate = ccIds.some(cc => cc.includes('tiertoken_collectionbook_ultimate')); if (hasUltimate) { earned.push('Ultimate STW Founders'); earned.push('STW Founders'); } else if (hasFounder) earned.push('STW Founders'); return [...new Set(earned)]; } // ───────────────────────────────────────────────────────────────────────────── // BOT READY // ───────────────────────────────────────────────────────────────────────────── client.once('ready', async () => { console.log(`✅ Logged in as ${client.user.tag}`); console.log('Commands: !setuproles | !addicons | !skipemoji | !stopemojis | !setroleicons | !removeoldemojis | !verify'); // Set bot avatar to Renegade Raider Black & Gold style try { const imgRes = await fetch('https://fortnite-api.com/images/cosmetics/br/cid_028_athena_commando_f/variants/material/mat3.png'); const imgBuf = Buffer.from(await imgRes.arrayBuffer()); await client.user.setAvatar(imgBuf); console.log('✅ Bot avatar set to Renegade Raider Black & Gold.'); } catch (err) { console.warn('Could not set avatar (may already be set or rate limited):', err.message); } // Register /commands slash command to guild for instant availability const rest = new REST({ version: '10' }).setToken(process.env.TOKEN); const slashCommands = [ new SlashCommandBuilder() .setName('commands') .setDescription('List all bot commands') .toJSON(), ]; try { await rest.put(Routes.applicationGuildCommands(client.user.id, process.env.EMOJI_GUILD_ID), { body: slashCommands }); console.log('✅ Slash commands registered to guild.'); } catch (err) { console.error('Failed to register slash commands:', err.message); } }); // ───────────────────────────────────────────────────────────────────────────── // /commands slash command // ───────────────────────────────────────────────────────────────────────────── client.on('interactionCreate', async (interaction) => { if (!interaction.isChatInputCommand() || interaction.commandName !== 'commands') return; const embed = new EmbedBuilder() .setColor(0x1a1a2e) .setAuthor({ name: 'jitterbuxg', iconURL: 'https://fortnite-api.com/images/cosmetics/br/cid_028_athena_commando_f/variants/material/mat3.png', }) .setTitle('Bot Commands') .setThumbnail('https://fortnite-api.com/images/cosmetics/br/pickaxe_lockjaw/variants/particle/particle1.png') .addFields( { name: 'Admin Setup', value: [ '`!setuproles` — Create all Fortnite roles', '`!addicons` — Upload skin icons as server emojis', '`!setroleicons` — Apply emojis as role icons', '`!removeoldemojis` — Wipe all emojis for a fresh upload', '`!setbg ` — Set custom background (supports GIFs)', '`!clearbg` — Reset to default background', ].join('\n'), inline: false, }, { name: 'During !addicons', value: '`!skipemoji` — Skip current · `!stopemojis` — Cancel session', inline: false, }, { name: 'Verification', value: '`!verify` — Link your Epic account and auto-assign roles based on your locker', inline: false, }, { name: 'What !verify checks', value: [ '**OG Skins** — Renegade Raider (B&G), Ghoul Trooper (Pink), Skull Trooper (Purple), + more', '**Exclusives** — Galaxy, Ikonik, Glow, Honor Guard, Double Helix, Wildcat, + more', '**Pickaxes** — Raider\'s Revenge (B&G), Axe of Champions, AC/DC, + more', '**Emotes** — Fresh, Floss, Take the L, Orange Justice, + more', '**Banners** — OT1–OT11, Creator Banners, STW Founders, + more', ].join('\n'), inline: false, } ) .setImage('https://fortnite-api.com/images/cosmetics/br/pickaxe_lockjaw/variants/particle/particle1.png') .setFooter({ text: 'Made by jitterbuxg • .gg/collecting' }) .setTimestamp(); await interaction.reply({ embeds: [embed], ephemeral: false }); }); // ───────────────────────────────────────────────────────────────────────────── // !setbg / !clearbg — custom background for skin checker images // ───────────────────────────────────────────────────────────────────────────── client.on('messageCreate', async (message) => { if (message.author.bot) return; if (message.content.startsWith('!setbg ')) { if (!message.member.permissions.has(PermissionsBitField.Flags.Administrator)) return message.reply('Need Administrator permission.'); const url = message.content.slice(7).trim(); if (!url.startsWith('http')) return message.reply('Please provide a valid image URL.'); customBgUrl = url; return message.reply(`Background set. All future skin checker images will use this background.`); } if (message.content === '!clearbg') { if (!message.member.permissions.has(PermissionsBitField.Flags.Administrator)) return message.reply('Need Administrator permission.'); customBgUrl = null; return message.reply('Background cleared. Back to default dark gradient.'); } }); // ───────────────────────────────────────────────────────────────────────────── // !setuproles // ───────────────────────────────────────────────────────────────────────────── client.on('messageCreate', async (message) => { if (message.author.bot || message.content !== '!setuproles') return; if (!message.member.permissions.has(PermissionsBitField.Flags.Administrator)) return message.reply('Need Administrator permission.'); await message.reply(`Creating up to ${roles.length} roles...`); let created = 0, skipped = 0; const existing = message.guild.roles.cache.map(r => r.name.toLowerCase()); for (const role of roles) { if (existing.includes(role.name.toLowerCase())) { skipped++; continue; } try { await message.guild.roles.create({ name: role.name, color: role.color, reason: 'Bulk role setup' }); console.log(`Created: ${role.name}`); created++; await new Promise(r => setTimeout(r, 300)); } catch (err) { console.error(`Failed "${role.name}": ${err.message}`); } } message.channel.send(`Done! Created **${created}** roles, skipped **${skipped}** existing.`); }); // ───────────────────────────────────────────────────────────────────────────── // !removeoldemojis // ───────────────────────────────────────────────────────────────────────────── client.on('messageCreate', async (message) => { if (message.author.bot || message.content !== '!removeoldemojis') return; if (!message.member.permissions.has(PermissionsBitField.Flags.Administrator)) return message.reply('Need Administrator permission.'); const emojiGuild = await client.guilds.fetch(process.env.EMOJI_GUILD_ID).catch(() => null); if (!emojiGuild) return message.reply('Could not find the emoji server. Check EMOJI_GUILD_ID in .env'); await message.reply(`Removing ALL emojis from **${emojiGuild.name}**...`); console.log(`🎯 Clearing all emojis from: ${emojiGuild.name} (${emojiGuild.id})`); await emojiGuild.emojis.fetch(); const allEmojis = [...emojiGuild.emojis.cache.values()]; console.log(`📋 Found ${allEmojis.length} emojis to delete`); let removed = 0; for (const emoji of allEmojis) { try { await emoji.delete('Clearing all emojis for fresh upload'); console.log(`🗑 Deleted: ${emoji.name}`); removed++; await new Promise(r => setTimeout(r, 400)); } catch (err) { console.error(`Failed to delete ${emoji.name}: ${err.message}`); } } message.channel.send(`Removed **${removed}** emojis from **${emojiGuild.name}**. Run !addicons to re-upload.`); }); // ───────────────────────────────────────────────────────────────────────────── // !addicons + !skipemoji + !stopemojis // ───────────────────────────────────────────────────────────────────────────── // Shared state for the active addicons session let emojiSession = null; // { resolve, name, channel } // !skipemoji — skip the currently stuck emoji client.on('messageCreate', async (message) => { if (message.author.bot || message.content !== '!skipemoji') return; if (!message.member.permissions.has(PermissionsBitField.Flags.Administrator)) return; if (!emojiSession) return message.reply('No emoji upload is currently in progress.'); message.reply(`Skipping **${emojiSession.name}**...`); emojiSession.resolve('skip'); }); // !stopemojis — stop the whole session client.on('messageCreate', async (message) => { if (message.author.bot || message.content !== '!stopemojis') return; if (!message.member.permissions.has(PermissionsBitField.Flags.Administrator)) return; if (!emojiSession) return message.reply('No emoji upload is currently in progress.'); message.reply('Stopping emoji upload session.'); emojiSession.resolve('stop'); }); // Lock to prevent !addicons running multiple times simultaneously let addIconsRunning = false; client.on('messageCreate', async (message) => { if (message.author.bot || message.content !== '!addicons') return; if (!message.member.permissions.has(PermissionsBitField.Flags.Administrator)) return message.reply('Need Administrator permission.'); if (addIconsRunning) return message.reply('An emoji upload is already in progress. Wait for it to finish or use `!stopemojis` to cancel.'); const emojiGuild = await client.guilds.fetch(process.env.EMOJI_GUILD_ID).catch(() => null); if (!emojiGuild) return message.reply('Could not find the emoji server. Check EMOJI_GUILD_ID in .env'); await message.reply(`Uploading ${roleIconMap.length} skin icons to **${emojiGuild.name}**.\nType \`!skipemoji\` to skip a stuck one, or \`!stopemojis\` to cancel.`); await emojiGuild.emojis.fetch(); const existing = new Set(emojiGuild.emojis.cache.map(e => e.name.toLowerCase())); let added = 0, skipped = 0, failed = 0; for (const entry of roleIconMap) { if (existing.has(entry.emoji.toLowerCase())) { console.log(`⏭ Exists: ${entry.emoji}`); skipped++; continue; } // Set up skip/stop listener for this entry let sessionResolve; const skipPromise = new Promise(res => { sessionResolve = res; }); emojiSession = { resolve: sessionResolve, name: entry.name, channel: message.channel }; process.stdout.write(`📥 ${entry.name} — fetching... `); try { const iconUrl = entry.overrideUrl || entry.bannerUrl || await fetchIconUrl(entry.id); process.stdout.write(`processing... `); const buffer = await downloadAndProcess(iconUrl); process.stdout.write(`uploading... `); // Race the upload against a skip/stop signal const result = await Promise.race([ createEmojiSafe(emojiGuild, buffer, entry.emoji, `Skin icon: ${entry.name}`) .then(() => 'done'), skipPromise, ]); if (result === 'stop') { console.log('🛑 Stopped by user.'); emojiSession = null; return message.channel.send(`Stopped. Added **${added}**, skipped **${skipped}**, failed **${failed}**.`); } if (result === 'skip') { console.log(`⏭ Skipped: ${entry.name}`); skipped++; emojiSession = null; continue; } console.log(`✅`); added++; } catch (err) { console.log(`❌ ${err.message}`); failed++; } emojiSession = null; await new Promise(r => setTimeout(r, 2000)); } emojiSession = null; message.channel.send(`Done. Added **${added}** emojis to **${emojiGuild.name}**. Skipped **${skipped}**. Failed **${failed}**.`); }); // ───────────────────────────────────────────────────────────────────────────── // !setroleicons // ───────────────────────────────────────────────────────────────────────────── client.on('messageCreate', async (message) => { if (message.author.bot || message.content !== '!setroleicons') return; if (!message.member.permissions.has(PermissionsBitField.Flags.Administrator)) return message.reply('Need Administrator permission.'); const emojiGuild = await client.guilds.fetch(process.env.EMOJI_GUILD_ID).catch(() => null); if (!emojiGuild) return message.reply('Could not find the emoji server. Check EMOJI_GUILD_ID in .env'); await message.reply('Applying emojis to roles...'); await emojiGuild.emojis.fetch(); await message.guild.roles.fetch(); let updated = 0, skipped = 0, failed = 0; for (const entry of roleIconMap) { const role = message.guild.roles.cache.find(r => r.name.toLowerCase() === entry.name.toLowerCase()); const emoji = emojiGuild.emojis.cache.find(e => e.name.toLowerCase() === entry.emoji.toLowerCase()); if (!role) { console.log(`⏭ Role not found: ${entry.name}`); skipped++; continue; } if (!emoji) { console.log(`⚠ Emoji missing: ${entry.emoji} — run !addicons first`); skipped++; continue; } try { await role.setIcon(emoji); console.log(`✅ ${entry.name} → :${entry.emoji}:`); updated++; } catch (err) { console.error(`❌ "${entry.name}": ${err.message}`); failed++; } await new Promise(r => setTimeout(r, 500)); } message.channel.send(`Done. Applied icons to **${updated}** roles. Skipped **${skipped}**. Failed **${failed}**.`); }); // ───────────────────────────────────────────────────────────────────────────── // !unverify — remove verification and strip roles // ───────────────────────────────────────────────────────────────────────────── client.on('messageCreate', async (message) => { if (message.author.bot || message.content !== '!unverify') return; const userId = message.author.id; if (!verifiedUsers.has(userId)) return message.reply('You are not currently verified. Use `!verify` to link your account.'); const prev = verifiedUsers.get(userId); const guild = client.guilds.cache.get(prev.guildId) || message.guild; if (guild) { try { const member = await guild.members.fetch(userId).catch(() => null); if (member) { for (const roleName of prev.assignedRoles) { const role = guild.roles.cache.find(r => r.name.toLowerCase() === roleName.toLowerCase()); if (role && member.roles.cache.has(role.id)) { await member.roles.remove(role, 'User unverified').catch(() => null); } } } } catch (err) { console.error('Unverify role removal error:', err.message); } } verifiedUsers.delete(userId); message.reply(`Your verification as **${prev.epicName}** has been removed and your roles have been stripped. You can now use \`!verify\` to link a different account.`); }); // ───────────────────────────────────────────────────────────────────────────── // !verify // ───────────────────────────────────────────────────────────────────────────── client.on('messageCreate', async (message) => { if (message.author.bot || message.content !== '!verify') return; const userId = message.author.id; // Support DMs — if in DM, we need to find the guild separately let guild = message.guild; if (!guild) { // Try to find the main guild from EMOJI_GUILD_ID guild = client.guilds.cache.get(process.env.EMOJI_GUILD_ID) || client.guilds.cache.first(); if (!guild) return message.reply('Could not find the server. Please use !verify in the server.'); } // Block if already verified if (verifiedUsers.has(userId)) { const prev = verifiedUsers.get(userId); return message.reply(`You are already verified as **${prev.epicName}**. Use \`!unverify\` to unlink your account and verify a different one.`); } if (pending.has(userId)) return message.reply('You already have a verification in progress. Check your DMs.'); await message.reply('A login link has been sent to your DMs.'); try { const switchToken = await getSwitchAccessToken(); const deviceData = await createDeviceCode(switchToken); if (!deviceData.user_code || !deviceData.device_code) return message.author.send('Failed to generate a login link. Try again later.'); const loginUrl = `https://www.epicgames.com/activate?userCode=${deviceData.user_code}`; const embed = new EmbedBuilder() .setColor(0x5865F2) .setTitle('Fortnite Account Verification') .setDescription( `**[Click here to verify →](${loginUrl})**\n\n` + `Code: \`${deviceData.user_code}\`\n\n` + `Expires in **5 minutes**. The bot will check your locker and assign your roles automatically.` ) .setFooter({ text: 'Read-only — the bot only checks your cosmetics.' }); await message.author.send({ embeds: [embed] }); pending.set(userId, { guildId: guild.id, channelId: message.channel.id }); const tokenData = await waitForLogin(deviceData.device_code); pending.delete(userId); if (!tokenData) return message.author.send('Login expired. Use !verify to try again.'); const iosData = await exchangeForIosToken(tokenData.access_token); if (!iosData.access_token) return message.author.send('Authentication failed. Please try again.'); const { account_id: accountId, displayName = 'Unknown', access_token: accessToken } = iosData; await message.author.send(`Logged in as **${displayName}**. Fetching locker...`); const [athenaData, commonCoreData] = await Promise.all([ getProfile(accountId, accessToken, 'athena'), getProfile(accountId, accessToken, 'common_core'), ]); if (!athenaData?.profileChanges) return message.author.send('Failed to read your locker. Please try again.'); const { owned: ownedMap, ogStyleOwned, outfits, pickaxes, emotes, emoticons, backblings, gliders, sprays } = buildOwnedMap(athenaData); const ccItems = commonCoreData?.profileChanges?.[0]?.profile?.items || {}; const earnedRoles = getRolesForLocker(ownedMap, ccItems); const member = await guild.members.fetch(userId).catch(() => null); if (!member) return message.author.send('Could not find you in the server. Make sure you are still a member.'); const assigned = [], notFound = []; for (const roleName of earnedRoles) { const role = guild.roles.cache.find(r => r.name.toLowerCase() === roleName.toLowerCase()); if (!role) { notFound.push(roleName); continue; } if (!member.roles.cache.has(role.id)) await member.roles.add(role, 'Fortnite locker verification').catch(() => null); assigned.push(roleName); } // Store verified state so they can't re-verify without !unverify verifiedUsers.set(userId, { guildId: guild.id, assignedRoles: assigned, epicName: displayName }); const verifyEmbed = new EmbedBuilder() .setColor(0x2B2D31) .setTitle(`${displayName}'s Verification`) .setDescription( assigned.length === 0 ? 'No exclusive roles found.' : assigned.map(r => `— ${r}`).join('\n') ) .addFields( { name: 'Total Outfits', value: String(outfits.length), inline: true }, { name: 'Total Pickaxes', value: String(pickaxes.length), inline: true }, ) .setFooter({ text: 'Made by jitterbuxg' }); await message.author.send({ embeds: [verifyEmbed] }); // Send verification log to designated channel try { const logChannel = await client.channels.fetch(LOG_CHANNEL_ID).catch(() => null); if (logChannel) { // Fetch server emojis to use next to role names const emojiGuild = await client.guilds.fetch(process.env.EMOJI_GUILD_ID).catch(() => null); await emojiGuild?.emojis.fetch().catch(() => null); const getServerEmoji = (roleName) => { if (!emojiGuild) return ''; // Map role names to emoji names we uploaded const emojiNameMap = { 'Galaxy': 'Galaxy', 'Ikonik': 'Ikonik', 'Glow': 'Glow', 'OG Renegade Raider': 'RenegadeRaider', 'OG Ghoul Trooper': 'GhoulTrooper', 'OG Skull Trooper': 'SkullTrooper', 'OG Aerial Assault Trooper': 'AerialAssaultTrooper', 'Black Knight': 'BlackKnight', 'Double Helix': 'DoubleHelix', 'Honor Guard': 'HonorGuard', 'Wonder': 'Wonder', 'Eon': 'Eon', 'Dark Vertex': 'DarkVertex', 'Neo Versa': 'NeoVersa', 'Royale Bomber': 'RoyaleBomber', 'Wildcat': 'Wildcat', 'Stealth Reflex': 'StealthReflex', 'Reflex': 'Reflex', "Raider's Revenge": 'RaidersRevenge', 'Stellar Axe': 'StellarAxe', 'AC/DC': 'ACDC', 'Merry Mint Axe': 'MerryMintAxe', 'Axe of Champions': 'AxeOfChampions', 'Blade of Champions': 'BladeOfChampions', 'Fully Golden Peely': 'FullyGoldenPeely', }; const emojiName = emojiNameMap[roleName]; if (!emojiName) return ''; const emoji = emojiGuild.emojis.cache.find(e => e.name.toLowerCase() === emojiName.toLowerCase()); return emoji ? `${emoji} ` : ''; }; const logEmbed = new EmbedBuilder() .setColor(assigned.length > 0 ? 0xFFD700 : 0x444444) .setAuthor({ name: displayName, iconURL: 'https://fortnite-api.com/images/cosmetics/br/cid_028_athena_commando_f/variants/material/mat3.png', }) .setTitle('Account Verified') .setDescription( assigned.length === 0 ? 'No exclusive cosmetics found on this account.' : assigned.map(r => `${getServerEmoji(r)}${r}`).join('\n') ) .addFields( { name: 'Discord', value: message.author.username, inline: true }, { name: 'Epic Account', value: displayName, inline: true }, { name: 'Roles Given', value: String(assigned.length), inline: true }, ) .setFooter({ text: '.gg/collecting • Made by jitterbuxg' }) .setTimestamp(); await logChannel.send({ embeds: [logEmbed] }); } } catch (err) { console.error('Log channel error:', err.message); } // ── 1. Fetch metadata for all owned cosmetics ───────────────────────── const allIds = [...outfits, ...pickaxes]; const metaMap = await fetchCosmeticMeta(allIds); // Override rarity to mythic for known exclusives for (const req of ROLE_REQUIREMENTS) { if (!req.bannerId) { const id = req.id.toLowerCase(); if (qualifiesForRequirement(req, ownedMap) && metaMap[id]) { metaMap[id].rarity = 'mythic'; if (requiredStyleTags(req).length > 0) metaMap[id].name = req.role; } } } // ── 2. Exclusives rendered image ────────────────────────────────────── try { const exclusiveItems = []; const exclusiveSeen = new Set(); const addExclusiveItem = (item) => { const key = item.id.toLowerCase(); if (exclusiveSeen.has(key)) return; exclusiveSeen.add(key); exclusiveItems.push(item); }; // Exclusive cosmetics (skins, pickaxes, emotes, emoticons) for (const req of ROLE_REQUIREMENTS) { if (req.bannerId || req.stw) continue; const id = req.id.toLowerCase(); if (!qualifiesForRequirement(req, ownedMap)) continue; const meta = metaMap[id] || {}; addExclusiveItem({ id, name: req.role || meta.name || id, rarity: 'mythic' }); } // Exclusive & STW banners - pulled from role requirements and bundled master list const bannerMeta = await getBannerMeta(); for (const item of collectOwnedExclusiveBannerItems(ccItems, bannerMeta, metaMap)) { addExclusiveItem(item); } if (exclusiveItems.length > 0) { await message.author.send('Building your exclusive cosmetics image...'); const exclusiveImg = await renderLockerImage( exclusiveItems, `${exclusiveItems.length} (0)`, 'EXCLUSIVES', message.author.username, ogStyleOwned ); if (exclusiveImg) { const top5 = exclusiveItems.slice(0, 5).map(i => i.name).join(' + '); const suffix = exclusiveItems.length > 5 ? ` + ${exclusiveItems.length - 5} more` : ''; await message.author.send({ content: `**${top5}${suffix}** (${outfits.length} skin count)`, files: [{ attachment: exclusiveImg, name: 'exclusives.png' }], }); } } } catch (err) { console.error('Exclusive image error:', err.message); } // ── 3. Full locker — all items, sorted by rarity, one image per category ── try { // Rarity sort order (matches RiftCheckerBot) const RARITY_ORDER = ['mythic','legendary','dark','lava','slurp','starwars','marvel','frozen','gaminglegends','shadow','icon','dc','epic','rare','uncommon','common']; const sortByRarity = (ids) => [...ids].sort((a, b) => { const ra = RARITY_ORDER.indexOf(metaMap[a.toLowerCase()]?.rarity || 'common'); const rb = RARITY_ORDER.indexOf(metaMap[b.toLowerCase()]?.rarity || 'common'); return (ra === -1 ? 99 : ra) - (rb === -1 ? 99 : rb); }); // Collect owned exclusive banners const bannerMeta = await getBannerMeta(); const ownedExclusiveBannerIds = []; for (const req of ROLE_REQUIREMENTS) { if (!req.bannerId) continue; const owned = Object.values(ccItems).some(i => (i.templateId || '').toLowerCase().includes(req.bannerId.toLowerCase()) ); if (owned) { const bid = req.bannerId.toLowerCase(); if (!metaMap[bid]) { metaMap[bid] = { name: bannerMeta[bid]?.name || req.role, rarity: 'mythic' }; } else { metaMap[bid].rarity = 'mythic'; } ownedExclusiveBannerIds.push(req.bannerId); } } // Fetch metadata for all cosmetic categories const allCategoryIds = [...outfits, ...pickaxes, ...emotes, ...emoticons, ...backblings, ...gliders, ...sprays]; const allMeta = await fetchCosmeticMeta(allCategoryIds); for (const [k, v] of Object.entries(allMeta)) metaMap[k] = metaMap[k] || v; const categories = [ { ids: sortByRarity(outfits), label: 'OUTFITS', subtitle: `${outfits.length} outfits` }, { ids: sortByRarity(pickaxes), label: 'PICKAXES', subtitle: `${pickaxes.length} pickaxes` }, { ids: sortByRarity(emotes), label: 'EMOTES', subtitle: `${emotes.length} emotes` }, { ids: sortByRarity(emoticons), label: 'EMOTICONS', subtitle: `${emoticons.length} emoticons` }, { ids: sortByRarity(backblings), label: 'BACK BLINGS', subtitle: `${backblings.length} back blings` }, { ids: sortByRarity(gliders), label: 'GLIDERS', subtitle: `${gliders.length} gliders` }, { ids: sortByRarity(sprays), label: 'SPRAYS', subtitle: `${sprays.length} sprays` }, ...(ownedExclusiveBannerIds.length > 0 ? [{ ids: ownedExclusiveBannerIds, label: 'BANNERS', subtitle: `${ownedExclusiveBannerIds.length} banners` }] : []), ].filter(c => c.ids.length > 0); if (categories.length === 0) { await message.author.send('No cosmetics found in locker.'); return; } await message.author.send(`Building your full locker (${categories.length} categories)...`); for (const cat of categories) { const items = cat.ids.map(id => ({ id, name: metaMap[id.toLowerCase()]?.name || id, rarity: metaMap[id.toLowerCase()]?.rarity || 'common', })); const img = await renderLockerImage( items, `${items.length} (0)`, cat.label, message.author.username, ogStyleOwned ); if (img) { await message.author.send({ content: `**${displayName} — ${cat.subtitle}**`, files: [{ attachment: img, name: `${cat.label.toLowerCase().replace(/\s/g, '_')}.png` }], }); } await new Promise(r => setTimeout(r, 500)); } } catch (err) { console.error('Locker image error:', err.message); } } catch (err) { pending.delete(userId); console.error('Verify error:', err); message.author.send('Unexpected error. Try again.').catch(() => null); } }); client.login(process.env.TOKEN);