Files
dmall/index.ts
T
2026-09-08 07:22:15 +02:00

349 lines
7.4 KiB
TypeScript

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<GuildMember[]> {
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<Client> {
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("========================================");