/** * Bulk emoji upload script * Usage: npx ts-node -r tsconfig-paths/register scripts/upload-emojis.ts [emoji_dir] * * Distributes emojis across a pool of donor servers (round-robin by available capacity). * Each emoji is unique across all servers — no duplicates. * Automatically updates messages/emojis.json with the uploaded emoji IDs. * * Required .env vars: * DISCORD_TOKEN — bot token * EMOJI_DONOR_GUILDS — comma-separated donor server IDs * e.g. EMOJI_DONOR_GUILDS=111111111111,222222222222,333333333333 */ import { REST, Routes } from "discord.js"; import fs from "fs"; import path from "path"; // Load .env manually since we're outside the bot runtime const envPath = path.join(__dirname, "../.env"); if (fs.existsSync(envPath)) { for (const line of fs.readFileSync(envPath, "utf8").split("\n")) { const [key, ...rest] = line.split("="); if (key?.trim() && rest.length) process.env[key.trim()] = rest.join("=").trim(); } } const TOKEN = process.env.DISCORD_TOKEN!; const DONOR_GUILD_IDS: string[] = (process.env.EMOJI_DONOR_GUILDS ?? "") .split(",") .map((id) => id.trim()) .filter(Boolean); if (!TOKEN) { console.error("❌ DISCORD_TOKEN must be set in .env"); process.exit(1); } if (DONOR_GUILD_IDS.length === 0) { console.error("❌ EMOJI_DONOR_GUILDS must be set in .env (comma-separated guild IDs)"); process.exit(1); } const emojiDir = process.argv[2] ?? path.join(__dirname, "../emoji-uploads"); const emojisPath = path.join(__dirname, "../messages/emojis.json"); if (!fs.existsSync(emojiDir)) { console.error(`❌ Emoji directory not found: ${emojiDir}`); console.error(` Create it and place your emoji PNG files inside.`); process.exit(1); } const rest = new REST({ version: "10" }).setToken(TOKEN); interface GuildEmojiSlot { guildId: string; name: string; // guild name for display existing: Map; // emojiName → emojiId capacity: number; } // Compute max emojis based on Nitro boost tier function maxEmojisForTier(premiumTier: number): number { switch (premiumTier) { case 1: return 100; case 2: return 150; case 3: return 250; default: return 50; } } async function fetchGuildSlots(guildIds: string[]): Promise { const slots: GuildEmojiSlot[] = []; for (const guildId of guildIds) { try { const [guild, emojis] = await Promise.all([ rest.get(Routes.guild(guildId)) as Promise, rest.get(Routes.guildEmojis(guildId)) as Promise, ]); const maxEmojis = maxEmojisForTier(guild.premium_tier ?? 0); const existingMap = new Map(emojis.map((e: any) => [e.name, e.id])); const capacity = maxEmojis - emojis.length; const guildName = guild.name ?? guildId; console.log(`🏠 ${guildName} (${guildId}): ${emojis.length}/${maxEmojis} emojis, ${capacity} slots free`); slots.push({ guildId, name: guildName, existing: existingMap, capacity }); } catch (err: any) { console.error(`❌ Could not fetch guild ${guildId}: ${err.message}`); } } return slots; } async function uploadEmojis(): Promise { const files = fs.readdirSync(emojiDir).filter((f) => [".png", ".jpg", ".gif", ".webp"].includes(path.extname(f).toLowerCase()) ); if (files.length === 0) { console.error("❌ No image files found in the emoji directory."); process.exit(1); } // Load existing emojis.json let emojiMap: Record = {}; try { emojiMap = JSON.parse(fs.readFileSync(emojisPath, "utf8")); } catch { console.warn("⚠️ Could not load emojis.json — will create fresh mapping."); } console.log(`\n📁 Found ${files.length} file(s) in ${emojiDir}`); console.log(`🔍 Scanning ${DONOR_GUILD_IDS.length} donor server(s)...\n`); const guildSlots = await fetchGuildSlots(DONOR_GUILD_IDS); if (guildSlots.length === 0) { console.error("❌ No accessible donor guilds found. Check EMOJI_DONOR_GUILDS and bot membership."); process.exit(1); } // Build global deduplication map across all donor guilds const globalExisting = new Map(); // emojiName → formatted string for (const slot of guildSlots) { for (const [name, id] of slot.existing) { globalExisting.set(name, `<:${name}:${id}>`); } } const totalCapacity = guildSlots.reduce((sum, s) => sum + s.capacity, 0); console.log(`\n📊 ${globalExisting.size} emoji(s) already exist · ${totalCapacity} slots available across all servers\n`); if (totalCapacity === 0) { console.error("❌ All donor servers are full! Add more servers to EMOJI_DONOR_GUILDS."); process.exit(1); } let uploaded = 0; let skipped = 0; let failed = 0; // Round-robin slot picker — distributes load evenly across guilds let slotIndex = 0; function nextAvailableSlot(): GuildEmojiSlot | null { const start = slotIndex; do { const slot = guildSlots[slotIndex % guildSlots.length]; slotIndex++; if (slot.capacity > 0) return slot; } while (slotIndex % guildSlots.length !== start % guildSlots.length); // Fallback: find any with capacity (in case loop exited without finding one) return guildSlots.find((s) => s.capacity > 0) ?? null; } for (const file of files) { const emojiName = path.basename(file, path.extname(file)); const filePath = path.join(emojiDir, file); const ext = path.extname(file).toLowerCase(); const mimeType = ext === ".gif" ? "image/gif" : ext === ".webp" ? "image/webp" : "image/png"; // Already exists in the pool — update map and skip if (globalExisting.has(emojiName)) { emojiMap[emojiName] = globalExisting.get(emojiName)!; console.log(`⏭️ Already exists: ${emojiName} → ${emojiMap[emojiName]}`); skipped++; continue; } const slot = nextAvailableSlot(); if (!slot) { console.error(`❌ All slots full — could not upload: ${emojiName}`); console.error(` Add more servers to EMOJI_DONOR_GUILDS in .env`); failed++; continue; } try { const base64 = `data:${mimeType};base64,${fs.readFileSync(filePath).toString("base64")}`; const result = await rest.post(Routes.guildEmojis(slot.guildId), { body: { name: emojiName, image: base64 }, }) as any; const formatted = `<:${emojiName}:${result.id}>`; emojiMap[emojiName] = formatted; slot.capacity--; console.log(`✅ Uploaded: ${emojiName} → ${formatted} [${slot.name}]`); uploaded++; // Rate limit buffer await new Promise((r) => setTimeout(r, 600)); } catch (err: any) { console.error(`❌ Failed: ${emojiName} — ${err.message}`); failed++; } } fs.writeFileSync(emojisPath, JSON.stringify(emojiMap, null, 2)); console.log(`\n📊 ${uploaded} uploaded · ${skipped} skipped · ${failed} failed`); console.log(`💾 messages/emojis.json updated`); console.log(`\nSlot usage after upload:`); for (const slot of guildSlots) { const used = slot.existing.size + (uploaded > 0 ? slot.existing.size : 0); console.log(` ${slot.name}: ${slot.capacity} slots remaining`); } } uploadEmojis().catch(console.error);