Utoljára aktív 2 weeks ago

nuno gist felülvizsgálása 2 weeks ago. Revízióhoz ugrás

1 file changed, 274 insertions

gistfile1.txt(fájl létrehozva)

@@ -0,0 +1,274 @@
1 + /**
2 + * Bulk emoji upload script with subdirectory support and round-robin distribution.
3 + *
4 + * Usage:
5 + * Upload: npx ts-node -r tsconfig-paths/register scripts/upload-emojis.ts [emoji_dir]
6 + * Delete: npx ts-node -r tsconfig-paths/register scripts/upload-emojis.ts --delete <pattern>
7 + * Pattern can be a prefix (e.g. "wrank_up") or exact name (e.g. "wrank_up_1")
8 + * Multiple patterns: --delete wrank_up wrank_down wrank_gold
9 + *
10 + * Directory naming conventions:
11 + * - Files in root dir → name = filename without extension
12 + * - Files in subdir → name determined by DIR_NAME_MAP or default (dirname_filename)
13 + * - Passthrough dirs → name = filename only (no prefix)
14 + *
15 + * Required .env vars:
16 + * DISCORD_TOKEN — bot token
17 + * EMOJI_DONOR_GUILDS — comma-separated donor server IDs
18 + */
19 +
20 + import { REST, Routes } from "discord.js";
21 + import fs from "fs";
22 + import path from "path";
23 + import { Config } from "@systems/config";
24 +
25 + // Load .env
26 + const envPath = path.join(__dirname, "../.env");
27 + if (fs.existsSync(envPath)) {
28 + for (const line of fs.readFileSync(envPath, "utf8").split("\n")) {
29 + const [key, ...rest] = line.split("=");
30 + if (key?.trim() && rest.length) process.env[key.trim()] = rest.join("=").trim();
31 + }
32 + }
33 +
34 + const TOKEN = process.env.DISCORD_TOKEN!;
35 + const DONOR_GUILD_IDS: string[] = Config.get({ section: "emoji", key: "donorGuilds" });
36 +
37 + Config.load();
38 +
39 + const donorGuilds = Config.get({ section: "emoji", key: "donorGuilds" });
40 +
41 + if (!TOKEN || donorGuilds.length === 0) {
42 + console.error("❌ DISCORD_TOKEN must be set in .env and emoji.donorGuilds must be configured in config.json");
43 + process.exit(1);
44 + }
45 +
46 + const emojiDir = path.join(__dirname, "../emoji-uploads");
47 + const emojisPath = path.join(__dirname, "../messages/emojis.json");
48 + const rest = new REST({ version: "10" }).setToken(TOKEN);
49 +
50 + // ─── Naming config ─────────────────────────────────────────────────────────────
51 +
52 + // Dirs listed here use filename only — no dir prefix
53 + const PASSTHROUGH_DIRS: string[] = ["classes", "nations", "misc"];
54 +
55 + // Custom naming functions per dir — (filename without ext) → emoji name
56 + const DIR_NAME_MAP: Record<string, (filename: string) => string> = {
57 + "wrank": (f) => `wrank_${f}`,
58 + "wrank_gold": (f) => `wrank_${f}_gold`,
59 + "wrank_up": (f) => `wrank_up_${f}`,
60 + "wrank_down": (f) => `wrank_down_${f}`,
61 + "wrank_x": (f) => `wrank_x_${f}`,
62 + };
63 +
64 + function resolveEmojiName(dirName: string, filename: string): string {
65 + if (PASSTHROUGH_DIRS.includes(dirName)) return filename;
66 + if (DIR_NAME_MAP[dirName]) return DIR_NAME_MAP[dirName](filename);
67 + return `${dirName}_${filename}`; // default: dirname_filename
68 + }
69 +
70 + // ─── File discovery ────────────────────────────────────────────────────────────
71 +
72 + interface EmojiFile {
73 + emojiName: string;
74 + filePath: string;
75 + mimeType: string;
76 + }
77 +
78 + const IMAGE_EXTS = [".png", ".jpg", ".gif", ".webp"];
79 +
80 + function mimeFor(ext: string): string {
81 + if (ext === ".gif") return "image/gif";
82 + if (ext === ".webp") return "image/webp";
83 + return "image/png";
84 + }
85 +
86 + function scanDir(dir: string, parentDirName?: string): EmojiFile[] {
87 + const results: EmojiFile[] = [];
88 + if (!fs.existsSync(dir)) return results;
89 +
90 + for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
91 + const fullPath = path.join(dir, entry.name);
92 + if (entry.isDirectory()) {
93 + results.push(...scanDir(fullPath, entry.name));
94 + } else {
95 + const ext = path.extname(entry.name).toLowerCase();
96 + if (!IMAGE_EXTS.includes(ext)) continue;
97 + const filename = path.basename(entry.name, ext);
98 + const emojiName = parentDirName
99 + ? resolveEmojiName(parentDirName, filename)
100 + : filename;
101 + results.push({ emojiName, filePath: fullPath, mimeType: mimeFor(ext) });
102 + }
103 + }
104 + return results;
105 + }
106 +
107 + // ─── Guild helpers ─────────────────────────────────────────────────────────────
108 +
109 + interface GuildSlot {
110 + guildId: string;
111 + name: string;
112 + existing: Map<string, string>; // emojiName → emojiId
113 + capacity: number;
114 + }
115 +
116 + function maxEmojisForTier(tier: number): number {
117 + return [50, 100, 150, 250][tier] ?? 50;
118 + }
119 +
120 + async function fetchGuildSlots(): Promise<GuildSlot[]> {
121 + const slots: GuildSlot[] = [];
122 + for (const guildId of DONOR_GUILD_IDS) {
123 + try {
124 + const [guild, emojis] = await Promise.all([
125 + rest.get(Routes.guild(guildId)) as Promise<any>,
126 + rest.get(Routes.guildEmojis(guildId)) as Promise<any[]>,
127 + ]);
128 + const max = maxEmojisForTier(guild.premium_tier ?? 0);
129 + const existing = new Map(emojis.map((e: any) => [e.name, e.id]));
130 + const capacity = max - emojis.length;
131 + console.log(`🏠 ${guild.name} (${guildId}): ${emojis.length}/${max} emojis, ${capacity} free`);
132 + slots.push({ guildId, name: guild.name, existing, capacity });
133 + } catch (err: any) {
134 + console.error(`❌ Could not fetch guild ${guildId}: ${err.message}`);
135 + }
136 + }
137 + return slots;
138 + }
139 +
140 + // ─── Upload ────────────────────────────────────────────────────────────────────
141 +
142 + async function upload(): Promise<void> {
143 + const files = scanDir(emojiDir);
144 + if (files.length === 0) {
145 + console.error(`❌ No image files found in ${emojiDir}`);
146 + process.exit(1);
147 + }
148 +
149 + let emojiMap: Record<string, string> = {};
150 + try { emojiMap = JSON.parse(fs.readFileSync(emojisPath, "utf8")); } catch {}
151 +
152 + console.log(`\n📁 Found ${files.length} file(s)\n🔍 Scanning donor servers...\n`);
153 + const slots = await fetchGuildSlots();
154 + if (slots.length === 0) { console.error("❌ No accessible donor servers."); process.exit(1); }
155 +
156 + // Build global dedup map
157 + const globalExisting = new Map<string, string>();
158 + for (const slot of slots) {
159 + for (const [name, id] of slot.existing) {
160 + globalExisting.set(name, `<:${name}:${id}>`);
161 + }
162 + }
163 +
164 + const totalCapacity = slots.reduce((s, g) => s + g.capacity, 0);
165 + console.log(`\n📊 ${globalExisting.size} existing · ${totalCapacity} slots free\n`);
166 +
167 + let slotIndex = 0;
168 + function nextSlot(): GuildSlot | null {
169 + const start = slotIndex;
170 + do {
171 + const s = slots[slotIndex % slots.length];
172 + slotIndex++;
173 + if (s.capacity > 0) return s;
174 + } while (slotIndex % slots.length !== start % slots.length);
175 + return slots.find((s) => s.capacity > 0) ?? null;
176 + }
177 +
178 + let uploaded = 0, skipped = 0, failed = 0;
179 +
180 + for (const file of files) {
181 + if (globalExisting.has(file.emojiName)) {
182 + emojiMap[file.emojiName] = globalExisting.get(file.emojiName)!;
183 + console.log(`⏭️ Exists: ${file.emojiName} → ${emojiMap[file.emojiName]}`);
184 + skipped++;
185 + continue;
186 + }
187 +
188 + const slot = nextSlot();
189 + if (!slot) {
190 + console.error(`❌ No slots available for: ${file.emojiName}`);
191 + failed++;
192 + continue;
193 + }
194 +
195 + try {
196 + const base64 = `data:${file.mimeType};base64,${fs.readFileSync(file.filePath).toString("base64")}`;
197 + const result = await rest.post(Routes.guildEmojis(slot.guildId), {
198 + body: { name: file.emojiName, image: base64 },
199 + }) as any;
200 +
201 + const formatted = `<:${file.emojiName}:${result.id}>`;
202 + emojiMap[file.emojiName] = formatted;
203 + slot.capacity--;
204 + console.log(`✅ Uploaded: ${file.emojiName} → ${formatted} [${slot.name}]`);
205 + uploaded++;
206 +
207 + await new Promise((r) => setTimeout(r, 600));
208 + } catch (err: any) {
209 + console.error(`❌ Failed: ${file.emojiName} — ${err.message}`);
210 + failed++;
211 + }
212 + }
213 +
214 + fs.writeFileSync(emojisPath, JSON.stringify(emojiMap, null, 2));
215 + console.log(`\n📊 ${uploaded} uploaded · ${skipped} skipped · ${failed} failed`);
216 + console.log(`💾 messages/emojis.json updated`);
217 + }
218 +
219 + // ─── Delete ────────────────────────────────────────────────────────────────────
220 +
221 + async function deleteEmojis(patterns: string[]): Promise<void> {
222 + console.log(`\n🗑️ Deleting emojis matching: ${patterns.join(", ")}`);
223 + console.log(`🔍 Scanning donor servers...\n`);
224 +
225 + const slots = await fetchGuildSlots();
226 + if (slots.length === 0) { console.error("❌ No accessible donor servers."); process.exit(1); }
227 +
228 + let emojiMap: Record<string, string> = {};
229 + try { emojiMap = JSON.parse(fs.readFileSync(emojisPath, "utf8")); } catch {}
230 +
231 + let deleted = 0, failed = 0;
232 +
233 + for (const slot of slots) {
234 + for (const [name, id] of slot.existing) {
235 + const matches = patterns.some((p) => name === p || name.startsWith(`${p}_`) || name.startsWith(p));
236 + if (!matches) continue;
237 +
238 + try {
239 + await rest.delete(Routes.guildEmoji(slot.guildId, id));
240 + console.log(`🗑️ Deleted: ${name} [${slot.name}]`);
241 + slot.existing.delete(name);
242 + delete emojiMap[name];
243 + deleted++;
244 + await new Promise((r) => setTimeout(r, 300));
245 + } catch (err: any) {
246 + console.error(`❌ Failed to delete ${name}: ${err.message}`);
247 + failed++;
248 + }
249 + }
250 + }
251 +
252 + fs.writeFileSync(emojisPath, JSON.stringify(emojiMap, null, 2));
253 + console.log(`\n📊 ${deleted} deleted · ${failed} failed`);
254 + console.log(`💾 messages/emojis.json updated`);
255 + }
256 +
257 + // ─── Entry point ───────────────────────────────────────────────────────────────
258 +
259 + const args = process.argv.slice(2);
260 +
261 + if (args[0] === "--delete") {
262 + const patterns = args.slice(1);
263 + if (patterns.length === 0) {
264 + console.error("❌ Specify at least one pattern: --delete <pattern> [pattern2] ...");
265 + console.error(" Examples:");
266 + console.error(" --delete wrank_up (deletes wrank_up_1, wrank_up_2, ...)");
267 + console.error(" --delete wrank_up_1 (deletes exact match)");
268 + console.error(" --delete wrank_up wrank_down wrank_gold");
269 + process.exit(1);
270 + }
271 + deleteEmojis(patterns).catch(console.error);
272 + } else {
273 + upload().catch(console.error);
274 + }
Újabb Régebbi