/** * Bulk emoji upload script * Usage: npx ts-node 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. */ import { REST, Routes } from "discord.js"; import fs from "fs"; import path from "path"; // Load .env manually since we're outside the bot 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 && rest.length) process.env[key.trim()] = rest.join("=").trim(); } } const TOKEN = process.env.DISCORD_TOKEN!; // Primary server (your main guild, not used for emoji storage) const GUILD_ID = process.env.GUILD_ID!; // Donor servers — comma-separated list of guild IDs in .env // e.g. EMOJI_DONOR_GUILDS=111111111111,222222222222,333333333333 const DONOR_GUILD_IDS: string[] = (process.env.EMOJI_DONOR_GUILDS ?? "") .split(",") .map((id) => id.trim()) .filter(Boolean); if (!TOKEN || !GUILD_ID) { console.error("❌ DISCORD_TOKEN and GUILD_ID 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}`); process.exit(1); } const rest = new REST({ version: "10" }).setToken(TOKEN); // Discord's base emoji limit per guild (static + animated counted separately, // but we treat total slots conservatively as 50 for safety unless you know your tiers) const EMOJI_LIMIT_PER_GUILD = 50; interface GuildEmojiSlot { guildId: string; existing: Map; // name → id capacity: number; // remaining slots } async function fetchGuildSlots(guildIds: string[]): Promise { const slots: GuildEmojiSlot[] = []; for (const guildId of guildIds) { const existing = await rest.get(Routes.guildEmojis(guildId)) as any[]; const existingMap = new Map(existing.map((e: any) => [e.name, e.id])); const capacity = EMOJI_LIMIT_PER_GUILD - existing.length; console.log(`🏠 Guild ${guildId}: ${existing.length} emojis, ${capacity} slots free`); slots.push({ guildId, existing: existingMap, capacity }); } 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); // Build a global map of all already-uploaded emojis across all donor guilds. // This is the deduplication layer — if an emoji exists in any guild, skip it. const globalExisting = new Map(); // name → formatted emoji string for (const slot of guildSlots) { for (const [name, id] of slot.existing) { globalExisting.set(name, `<:${name}:${id}>`); } } console.log(`\n📊 ${globalExisting.size} emoji(s) already exist across all donor servers\n`); let uploaded = 0; let skipped = 0; let failed = 0; // Pick the next guild with available capacity (round-robin style) function nextAvailableSlot(): GuildEmojiSlot | null { 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 somewhere in the pool — just ensure it's in the map 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 donor servers are full! Could not upload: ${emojiName}`); console.error(` Add more donor 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} (guild: ${slot.guildId})`); uploaded++; 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`); } uploadEmojis().catch(console.error);