first commit
This commit is contained in:
commit
d94c5c6cc0
134 files changed
+23720
No files matched your search
@@ -0,0 +1,249 @@
|
||||
// =============================================
|
||||
// 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);
|
||||
Reference in new issue
Block a user