Upload files to "/"
This commit is contained in:
commit
213b0d9ad4
5 files changed
+7964
No files matched your search
+249
@@ -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);
|
||||||
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
Reference in new issue
Block a user