upload-emojis.ts
· 5.9 KiB · TypeScript
Неформатований
/**
* 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<string, string>; // name → id
capacity: number; // remaining slots
}
async function fetchGuildSlots(guildIds: string[]): Promise<GuildEmojiSlot[]> {
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<void> {
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<string, string> = {};
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<string, string>(); // 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);
| 1 | /** |
| 2 | * Bulk emoji upload script |
| 3 | * Usage: npx ts-node scripts/upload-emojis.ts [emoji_dir] |
| 4 | * |
| 5 | * Distributes emojis across a pool of donor servers (round-robin by available capacity). |
| 6 | * Each emoji is unique across all servers — no duplicates. |
| 7 | * Automatically updates messages/emojis.json with the uploaded emoji IDs. |
| 8 | */ |
| 9 | |
| 10 | import { REST, Routes } from "discord.js"; |
| 11 | import fs from "fs"; |
| 12 | import path from "path"; |
| 13 | |
| 14 | // Load .env manually since we're outside the bot |
| 15 | const envPath = path.join(__dirname, "../.env"); |
| 16 | if (fs.existsSync(envPath)) { |
| 17 | for (const line of fs.readFileSync(envPath, "utf8").split("\n")) { |
| 18 | const [key, ...rest] = line.split("="); |
| 19 | if (key && rest.length) process.env[key.trim()] = rest.join("=").trim(); |
| 20 | } |
| 21 | } |
| 22 | |
| 23 | const TOKEN = process.env.DISCORD_TOKEN!; |
| 24 | |
| 25 | // Primary server (your main guild, not used for emoji storage) |
| 26 | const GUILD_ID = process.env.GUILD_ID!; |
| 27 | |
| 28 | // Donor servers — comma-separated list of guild IDs in .env |
| 29 | // e.g. EMOJI_DONOR_GUILDS=111111111111,222222222222,333333333333 |
| 30 | const DONOR_GUILD_IDS: string[] = (process.env.EMOJI_DONOR_GUILDS ?? "") |
| 31 | .split(",") |
| 32 | .map((id) => id.trim()) |
| 33 | .filter(Boolean); |
| 34 | |
| 35 | if (!TOKEN || !GUILD_ID) { |
| 36 | console.error("❌ DISCORD_TOKEN and GUILD_ID must be set in .env"); |
| 37 | process.exit(1); |
| 38 | } |
| 39 | |
| 40 | if (DONOR_GUILD_IDS.length === 0) { |
| 41 | console.error("❌ EMOJI_DONOR_GUILDS must be set in .env (comma-separated guild IDs)"); |
| 42 | process.exit(1); |
| 43 | } |
| 44 | |
| 45 | const emojiDir = process.argv[2] ?? path.join(__dirname, "../emoji-uploads"); |
| 46 | const emojisPath = path.join(__dirname, "../messages/emojis.json"); |
| 47 | |
| 48 | if (!fs.existsSync(emojiDir)) { |
| 49 | console.error(`❌ Emoji directory not found: ${emojiDir}`); |
| 50 | process.exit(1); |
| 51 | } |
| 52 | |
| 53 | const rest = new REST({ version: "10" }).setToken(TOKEN); |
| 54 | |
| 55 | // Discord's base emoji limit per guild (static + animated counted separately, |
| 56 | // but we treat total slots conservatively as 50 for safety unless you know your tiers) |
| 57 | const EMOJI_LIMIT_PER_GUILD = 50; |
| 58 | |
| 59 | interface GuildEmojiSlot { |
| 60 | guildId: string; |
| 61 | existing: Map<string, string>; // name → id |
| 62 | capacity: number; // remaining slots |
| 63 | } |
| 64 | |
| 65 | async function fetchGuildSlots(guildIds: string[]): Promise<GuildEmojiSlot[]> { |
| 66 | const slots: GuildEmojiSlot[] = []; |
| 67 | |
| 68 | for (const guildId of guildIds) { |
| 69 | const existing = await rest.get(Routes.guildEmojis(guildId)) as any[]; |
| 70 | const existingMap = new Map(existing.map((e: any) => [e.name, e.id])); |
| 71 | const capacity = EMOJI_LIMIT_PER_GUILD - existing.length; |
| 72 | |
| 73 | console.log(`🏠 Guild ${guildId}: ${existing.length} emojis, ${capacity} slots free`); |
| 74 | slots.push({ guildId, existing: existingMap, capacity }); |
| 75 | } |
| 76 | |
| 77 | return slots; |
| 78 | } |
| 79 | |
| 80 | async function uploadEmojis(): Promise<void> { |
| 81 | const files = fs.readdirSync(emojiDir).filter((f) => |
| 82 | [".png", ".jpg", ".gif", ".webp"].includes(path.extname(f).toLowerCase()) |
| 83 | ); |
| 84 | |
| 85 | if (files.length === 0) { |
| 86 | console.error("❌ No image files found in the emoji directory."); |
| 87 | process.exit(1); |
| 88 | } |
| 89 | |
| 90 | // Load existing emojis.json |
| 91 | let emojiMap: Record<string, string> = {}; |
| 92 | try { |
| 93 | emojiMap = JSON.parse(fs.readFileSync(emojisPath, "utf8")); |
| 94 | } catch { |
| 95 | console.warn("⚠️ Could not load emojis.json — will create fresh mapping."); |
| 96 | } |
| 97 | |
| 98 | console.log(`\n📁 Found ${files.length} file(s) in ${emojiDir}`); |
| 99 | console.log(`🔍 Scanning ${DONOR_GUILD_IDS.length} donor server(s)...\n`); |
| 100 | |
| 101 | const guildSlots = await fetchGuildSlots(DONOR_GUILD_IDS); |
| 102 | |
| 103 | // Build a global map of all already-uploaded emojis across all donor guilds. |
| 104 | // This is the deduplication layer — if an emoji exists in any guild, skip it. |
| 105 | const globalExisting = new Map<string, string>(); // name → formatted emoji string |
| 106 | for (const slot of guildSlots) { |
| 107 | for (const [name, id] of slot.existing) { |
| 108 | globalExisting.set(name, `<:${name}:${id}>`); |
| 109 | } |
| 110 | } |
| 111 | |
| 112 | console.log(`\n📊 ${globalExisting.size} emoji(s) already exist across all donor servers\n`); |
| 113 | |
| 114 | let uploaded = 0; |
| 115 | let skipped = 0; |
| 116 | let failed = 0; |
| 117 | |
| 118 | // Pick the next guild with available capacity (round-robin style) |
| 119 | function nextAvailableSlot(): GuildEmojiSlot | null { |
| 120 | return guildSlots.find((s) => s.capacity > 0) ?? null; |
| 121 | } |
| 122 | |
| 123 | for (const file of files) { |
| 124 | const emojiName = path.basename(file, path.extname(file)); |
| 125 | const filePath = path.join(emojiDir, file); |
| 126 | const ext = path.extname(file).toLowerCase(); |
| 127 | const mimeType = ext === ".gif" ? "image/gif" : ext === ".webp" ? "image/webp" : "image/png"; |
| 128 | |
| 129 | // Already exists somewhere in the pool — just ensure it's in the map |
| 130 | if (globalExisting.has(emojiName)) { |
| 131 | emojiMap[emojiName] = globalExisting.get(emojiName)!; |
| 132 | console.log(`⏭️ Already exists: ${emojiName} → ${emojiMap[emojiName]}`); |
| 133 | skipped++; |
| 134 | continue; |
| 135 | } |
| 136 | |
| 137 | const slot = nextAvailableSlot(); |
| 138 | if (!slot) { |
| 139 | console.error(`❌ All donor servers are full! Could not upload: ${emojiName}`); |
| 140 | console.error(` Add more donor servers to EMOJI_DONOR_GUILDS in .env`); |
| 141 | failed++; |
| 142 | continue; |
| 143 | } |
| 144 | |
| 145 | try { |
| 146 | const base64 = `data:${mimeType};base64,${fs.readFileSync(filePath).toString("base64")}`; |
| 147 | const result = await rest.post(Routes.guildEmojis(slot.guildId), { |
| 148 | body: { name: emojiName, image: base64 }, |
| 149 | }) as any; |
| 150 | |
| 151 | const formatted = `<:${emojiName}:${result.id}>`; |
| 152 | emojiMap[emojiName] = formatted; |
| 153 | slot.capacity--; |
| 154 | |
| 155 | console.log(`✅ Uploaded: ${emojiName} → ${formatted} (guild: ${slot.guildId})`); |
| 156 | uploaded++; |
| 157 | |
| 158 | await new Promise((r) => setTimeout(r, 600)); |
| 159 | } catch (err: any) { |
| 160 | console.error(`❌ Failed: ${emojiName} — ${err.message}`); |
| 161 | failed++; |
| 162 | } |
| 163 | } |
| 164 | |
| 165 | fs.writeFileSync(emojisPath, JSON.stringify(emojiMap, null, 2)); |
| 166 | |
| 167 | console.log(`\n📊 ${uploaded} uploaded · ${skipped} skipped · ${failed} failed`); |
| 168 | console.log(`💾 messages/emojis.json updated`); |
| 169 | } |
| 170 | |
| 171 | uploadEmojis().catch(console.error); |