Zuletzt aktiv 1 month ago

Änderung 7609032df85ef7252e659ea8e6ea19928e866a0d

upload-emojis.ts Originalformat
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);