// ============================================= // DISCORD ROLE EMOJI ADDER + ROLE ICON SETTER // ============================================= // !addicons — fetches skin icons and uploads as server emojis // !setroleicons — applies uploaded emojis as role icons // !removeoldemojis — removes all skin emojis so you can re-add fresh // // Run with: node "add-icons.js" // ============================================= require('dotenv').config(); const { Client, GatewayIntentBits, PermissionsBitField } = require('discord.js'); const sharp = require('sharp'); const client = new Client({ intents: [ GatewayIntentBits.Guilds, GatewayIntentBits.GuildMessages, GatewayIntentBits.MessageContent ] }); // Direct cosmetic IDs — all verified to return valid icons from fortnite-api.com // overrideUrl = use a specific variant image instead of the default icon 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' }, { 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' }, ]; // ── fetch with timeout so nothing ever hangs ────────────────────────────────── 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 from fortnite-api.com by direct ID ─────────────────────────── async function fetchIconUrl(cosmeticId) { const url = `https://fortnite-api.com/v2/cosmetics/br/${cosmeticId}`; const res = await fetchWithTimeout(url); if (!res.ok) throw new Error(`API returned ${res.status} for ${cosmeticId}`); const json = await res.json(); if (!json.data?.images?.icon) throw new Error(`No icon in response for ${cosmeticId}`); return json.data.images.icon; } // ── download image and upscale to 256x256 for crisp Discord emoji ───────────── async function downloadAndProcess(imageUrl) { const res = await fetchWithTimeout(imageUrl, 15000); if (!res.ok) throw new Error(`Image download failed: ${res.status}`); const raw = Buffer.from(await res.arrayBuffer()); // Upscale to 256x256 with Lanczos resampling + sharpening return await sharp(raw) .resize(256, 256, { fit: 'cover', position: 'centre', kernel: 'lanczos3' }) .sharpen({ sigma: 0.8, m1: 1.0, m2: 2.0 }) .png({ compressionLevel: 6 }) .toBuffer(); } // ── create emoji with automatic rate-limit retry ────────────────────────────── async function createEmojiSafe(guild, buffer, name, reason) { for (let attempt = 1; attempt <= 6; attempt++) { try { return await guild.emojis.create({ attachment: buffer, name, reason }); } catch (err) { const isRateLimit = err.status === 429 || err.httpStatus === 429 || (err.message || '').toLowerCase().includes('rate limit'); if (isRateLimit) { const wait = ((err.retryAfter || 5) * 1000) + 1000; console.warn(` ⏳ Rate limited on "${name}", waiting ${wait}ms (attempt ${attempt}/6)...`); await new Promise(r => setTimeout(r, wait)); } else { throw err; } } } throw new Error(`Gave up creating emoji "${name}" after 6 attempts`); } // ───────────────────────────────────────────────────────────────────────────── client.once('ready', () => { console.log(`✅ Logged in as ${client.user.tag}`); console.log('Commands: !addicons | !setroleicons | !removeoldemojis'); }); // ── !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.'); await message.reply('Removing old skin emojis...'); const names = new Set(roleIconMap.map(e => e.emoji.toLowerCase())); let removed = 0; // Fetch fresh emoji list await message.guild.emojis.fetch(); for (const emoji of message.guild.emojis.cache.values()) { if (!names.has(emoji.name.toLowerCase())) continue; try { await emoji.delete('Replacing with corrected icons'); 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. Now run \`!addicons\`.`); }); // ── !addicons ───────────────────────────────────────────────────────────────── 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.'); await message.reply(`Starting — uploading ${roleIconMap.length} skin icons. This takes a few minutes, watch the terminal for progress.`); // Always fetch a fresh copy of existing emojis await message.guild.emojis.fetch(); const existingNames = new Set( message.guild.emojis.cache.map(e => e.name.toLowerCase()) ); let added = 0, skipped = 0, failed = 0; for (const entry of roleIconMap) { if (existingNames.has(entry.emoji.toLowerCase())) { console.log(` ⏭ Already exists: ${entry.emoji}`); skipped++; continue; } process.stdout.write(` 📥 ${entry.name} — fetching... `); try { const iconUrl = entry.overrideUrl || await fetchIconUrl(entry.id); process.stdout.write(`downloading... `); const buffer = await downloadAndProcess(iconUrl); process.stdout.write(`uploading... `); await createEmojiSafe(message.guild, buffer, entry.emoji, `Skin icon: ${entry.name}`); console.log(`✅`); added++; } catch (err) { console.log(`❌ ${err.message}`); failed++; } // 2s gap between uploads to stay under Discord rate limits await new Promise(r => setTimeout(r, 2000)); } message.channel.send( `Done! ✅ Added **${added}** emojis. ` + `⏭ Skipped **${skipped}** (already existed). ` + `❌ **${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.'); await message.reply('Applying emojis to roles...'); // Refresh both caches await message.guild.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() ); if (!role) { console.log(` ⏭ Role not found: ${entry.name}`); skipped++; continue; } const emoji = message.guild.emojis.cache.find( e => e.name.toLowerCase() === entry.emoji.toLowerCase() ); if (!emoji) { console.log(` ⚠ Emoji not found for: ${entry.name} — run !addicons first`); skipped++; continue; } try { await role.setIcon(emoji); console.log(` ✅ ${entry.name} → :${entry.emoji}:`); updated++; } catch (err) { console.error(` ❌ Failed "${entry.name}": ${err.message}`); failed++; } await new Promise(r => setTimeout(r, 500)); } message.channel.send( `Done! ✅ Set icons on **${updated}** roles. ` + `⏭ Skipped **${skipped}**. ❌ **${failed}** failed.` ); }); client.login(process.env.TOKEN);