import { Client, GatewayIntentBits, Guild, GuildMember, } from "discord.js"; const tokenFile = Bun.file("tokens.txt"); const messageFile = Bun.file("Message.txt"); const optInFile = Bun.file("opted-in.txt"); const tokens = (await tokenFile.text()) .split(/\r?\n/) .map((token) => token.trim()) .filter(Boolean); const message = await messageFile.text(); export const GUILD_ID = process.env.GUILD_ID; if (!GUILD_ID) { throw new Error("GUILD_ID n'est pas défini."); } const clients: Client[] = []; /** * Membres ayant explicitement accepté de recevoir les DM. * * opted-in.txt : * * 123456789012345678 * 987654321098765432 */ async function getOptedInMembers(guild: Guild): Promise { const content = await optInFile.text(); const optedInIds = new Set( content .split(/\r?\n/) .map((id) => id.trim()) .filter(Boolean), ); const members = await guild.members.fetch(); return members .filter((member) => optedInIds.has(member.id)) .toJSON(); } /** * Connecte un bot et attend réellement son clientReady. */ function loginClient(token: string): Promise { const client = new Client({ intents: [ GatewayIntentBits.Guilds, GatewayIntentBits.GuildMembers, ], }); clients.push(client); return new Promise((resolve, reject) => { const onReady = () => { cleanup(); console.log( `[${client.user?.username ?? client.user?.id}] ready`, ); resolve(client); }; const onError = (error: Error) => { cleanup(); reject(error); }; const cleanup = () => { client.off("clientReady", onReady); client.off("error", onError); }; client.once("clientReady", onReady); client.once("error", onError); client.login(token).catch(onError); }); } /** * Répartit les membres équitablement entre les bots. */ function batchMembers( members: GuildMember[], botCount: number, ): GuildMember[][] { const batches: GuildMember[][] = Array.from( { length: botCount }, () => [], ); members.forEach((member, index) => { batches[index % botCount].push(member); }); return batches; } /** * Envoie le message uniquement aux membres opt-in * assignés à ce bot. */ async function sendBatch( client: Client, members: GuildMember[], ): Promise<{ sent: number; failed: number; }> { let sent = 0; let failed = 0; console.log( `[${client.user?.username}] ${members.length} membre(s) dans son batch.`, ); for (const member of members) { const parsedMessage = message.replace( /\{USER\}/g, member.toString(), ); try { await member.send(parsedMessage); sent++; console.log( `[${client.user?.username}] DM envoyé à @${member.user.username}`, ); } catch (error) { failed++; console.error( `[${client.user?.username}] Impossible de DM @${member.user.username}`, error, ); } // Petit délai entre les envois. await Bun.sleep(1500); } return { sent, failed, }; } process.on("uncaughtException", (error) => { console.error("uncaughtException:", error); }); process.on("unhandledRejection", (error) => { console.error("unhandledRejection:", error); }); /* * ============================================================ * 1. CONNECTER TOUS LES BOTS * ============================================================ */ console.log(`Connexion de ${tokens.length} bot(s)...`); const loginResults = await Promise.allSettled( tokens.map((token) => loginClient(token)), ); const readyClients: Client[] = []; for (const [index, result] of loginResults.entries()) { if (result.status === "fulfilled") { readyClients.push(result.value); } else { console.error( `[Bot ${index + 1}] Échec de connexion :`, result.reason, ); } } console.log( `${readyClients.length}/${tokens.length} bot(s) sont prêts.`, ); if (readyClients.length === 0) { throw new Error("Aucun bot n'est connecté."); } /* * ============================================================ * 2. RÉCUPÉRER LE SERVEUR * ============================================================ */ const guilds = await Promise.all( readyClients.map(async (client) => { const guild = await client.guilds .fetch(GUILD_ID) .catch(() => null); return { client, guild, }; }), ); const availableBots = guilds.filter( ( item, ): item is { client: Client; guild: Guild; } => item.guild !== null, ); for (const { client } of guilds) { const guild = await client.guilds .fetch(GUILD_ID) .catch(() => null); if (!guild) { console.log( `[${client.user?.username}] Le bot n'est pas présent sur le serveur.`, ); console.log( `https://discord.com/oauth2/authorize?client_id=${client.user?.id}&scope=bot`, ); } } if (availableBots.length === 0) { throw new Error( "Aucun des bots connectés n'est présent sur ce serveur.", ); } /* * ============================================================ * 3. RÉCUPÉRER LES MEMBRES OPT-IN * ============================================================ */ const mainGuild = availableBots[0].guild; console.log( `[${mainGuild.name}] Récupération des membres opt-in...`, ); const optedInMembers = await getOptedInMembers(mainGuild); console.log( `[${mainGuild.name}] ${optedInMembers.length} membre(s) ont accepté les DM.`, ); if (optedInMembers.length === 0) { console.log("Aucun membre opt-in. Rien à envoyer."); process.exit(0); } /* * ============================================================ * 4. RÉPARTITION DES MEMBRES ENTRE LES BOTS * ============================================================ */ const batches = batchMembers( optedInMembers, availableBots.length, ); console.log( `Répartition de ${optedInMembers.length} membre(s) entre ${availableBots.length} bot(s).`, ); for (let i = 0; i < availableBots.length; i++) { console.log( `[${availableBots[i].client.user?.username}] ${batches[i].length} membre(s)`, ); } /* * ============================================================ * 5. ENVOI DES BATCHS * ============================================================ */ console.log("Début de l'envoi..."); const sendResults = await Promise.allSettled( availableBots.map(({ client }, index) => sendBatch(client, batches[index]), ), ); /* * ============================================================ * 6. RÉSULTATS * ============================================================ */ let totalSent = 0; let totalFailed = 0; for (const [index, result] of sendResults.entries()) { const username = availableBots[index].client.user?.username ?? availableBots[index].client.user?.id ?? `Bot ${index + 1}`; if (result.status === "fulfilled") { totalSent += result.value.sent; totalFailed += result.value.failed; console.log( `[${username}] terminé : ${result.value.sent} envoyé(s), ${result.value.failed} échec(s).`, ); } else { console.error( `[${username}] le batch a échoué :`, result.reason, ); } } console.log(""); console.log("========================================"); console.log(" ENVOI TERMINÉ"); console.log("========================================"); console.log(`Membres opt-in : ${optedInMembers.length}`); console.log(`Messages envoyés : ${totalSent}`); console.log(`Échecs : ${totalFailed}`); console.log("========================================");