diff --git a/.gitignore b/.gitignore index 2309cc8..a14702c 100644 --- a/.gitignore +++ b/.gitignore @@ -1,77 +1,19 @@ -# ---> Node -# Logs -logs -*.log -npm-debug.log* -yarn-debug.log* -yarn-error.log* -lerna-debug.log* -.pnpm-debug.log* +# dependencies (bun install) +node_modules -# Diagnostic reports (https://nodejs.org/api/report.html) -report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json +# output +out +dist +*.tgz -# Runtime data -pids -*.pid -*.seed -*.pid.lock - -# Directory for instrumented libs generated by jscoverage/JSCover -lib-cov - -# Coverage directory used by tools like istanbul +# code coverage coverage *.lcov -# nyc test coverage -.nyc_output - -# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) -.grunt - -# Bower dependency directory (https://bower.io/) -bower_components - -# node-waf configuration -.lock-wscript - -# Compiled binary addons (https://nodejs.org/api/addons.html) -build/Release - -# Dependency directories -node_modules/ -jspm_packages/ - -# Snowpack dependency directory (https://snowpack.dev/) -web_modules/ - -# TypeScript cache -*.tsbuildinfo - -# Optional npm cache directory -.npm - -# Optional eslint cache -.eslintcache - -# Optional stylelint cache -.stylelintcache - -# Microbundle cache -.rpt2_cache/ -.rts2_cache_cjs/ -.rts2_cache_es/ -.rts2_cache_umd/ - -# Optional REPL history -.node_repl_history - -# Output of 'npm pack' -*.tgz - -# Yarn Integrity file -.yarn-integrity +# logs +logs +_.log +report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json # dotenv environment variable files .env @@ -80,59 +22,13 @@ web_modules/ .env.production.local .env.local -# parcel-bundler cache (https://parceljs.org/) +# caches +.eslintcache .cache -.parcel-cache +*.tsbuildinfo -# Next.js build output -.next -out - -# Nuxt.js build / generate output -.nuxt -dist - -# Gatsby files -.cache/ -# Comment in the public line in if your project uses Gatsby and not Next.js -# https://nextjs.org/blog/next-9-1#public-directory-support -# public - -# vuepress build output -.vuepress/dist - -# vuepress v2.x temp and cache directory -.temp -.cache - -# vitepress build output -**/.vitepress/dist - -# vitepress cache directory -**/.vitepress/cache - -# Docusaurus cache and generated files -.docusaurus - -# Serverless directories -.serverless/ - -# FuseBox cache -.fusebox/ - -# DynamoDB Local files -.dynamodb/ - -# TernJS port file -.tern-port - -# Stores VSCode versions used for testing VSCode extensions -.vscode-test - -# yarn v2 -.yarn/cache -.yarn/unplugged -.yarn/build-state.yml -.yarn/install-state.gz -.pnp.* +# IntelliJ based IDEs +.idea +# Finder (MacOS) folder config +.DS_Store diff --git a/DM_ALL_BY_KISAKAY_MULTI_TOKEN(ALLBOT)_(WALLAH).zip b/DM_ALL_BY_KISAKAY_MULTI_TOKEN(ALLBOT)_(WALLAH).zip new file mode 100644 index 0000000..5bd1c5a Binary files /dev/null and b/DM_ALL_BY_KISAKAY_MULTI_TOKEN(ALLBOT)_(WALLAH).zip differ diff --git a/Message.txt b/Message.txt new file mode 100644 index 0000000..3506773 --- /dev/null +++ b/Message.txt @@ -0,0 +1,2 @@ +# WSH {USER} TA TJR PAS REJOINS LE NOUVEAU PV UHQ +discord.gg/pvsephora WSH \ No newline at end of file diff --git a/README.md b/README.md index cf55369..41b23bf 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,15 @@ -# dmall +# dm-all -dmall \ No newline at end of file +To install dependencies: + +```bash +bun install +``` + +To run: + +```bash +bun run index.ts +``` + +This project was created using `bun init` in bun v1.3.14. [Bun](https://bun.com) is a fast all-in-one JavaScript runtime. diff --git a/bun.lock b/bun.lock new file mode 100644 index 0000000..0ab8df7 --- /dev/null +++ b/bun.lock @@ -0,0 +1,75 @@ +{ + "lockfileVersion": 1, + "configVersion": 1, + "workspaces": { + "": { + "name": "dm-all", + "dependencies": { + "discord.js": "^14.27.0", + }, + "devDependencies": { + "@types/bun": "latest", + }, + "peerDependencies": { + "typescript": "^5", + }, + }, + }, + "packages": { + "@discordjs/builders": ["@discordjs/builders@1.14.1", "", { "dependencies": { "@discordjs/formatters": "^0.6.2", "@discordjs/util": "^1.2.0", "@sapphire/shapeshift": "^4.0.0", "discord-api-types": "^0.38.40", "fast-deep-equal": "^3.1.3", "ts-mixer": "^6.0.4", "tslib": "^2.6.3" } }, "sha512-gSKkhXLqs96TCzk66VZuHHl8z2bQMJFGwrXC0f33ngK+FLNau4hU1PYny3DNJfNdSH+gVMzE85/d5FQ2BpcNwQ=="], + + "@discordjs/collection": ["@discordjs/collection@1.5.3", "", {}, "sha512-SVb428OMd3WO1paV3rm6tSjM4wC+Kecaa1EUGX7vc6/fddvw/6lg90z4QtCqm21zvVe92vMMDt9+DkIvjXImQQ=="], + + "@discordjs/formatters": ["@discordjs/formatters@0.6.2", "", { "dependencies": { "discord-api-types": "^0.38.33" } }, "sha512-y4UPwWhH6vChKRkGdMB4odasUbHOUwy7KL+OVwF86PvT6QVOwElx+TiI1/6kcmcEe+g5YRXJFiXSXUdabqZOvQ=="], + + "@discordjs/rest": ["@discordjs/rest@2.6.3", "", { "dependencies": { "@discordjs/collection": "^2.1.1", "@discordjs/util": "^1.2.0", "@sapphire/async-queue": "^1.5.3", "@sapphire/snowflake": "^3.5.5", "@vladfrangu/async_event_emitter": "^2.4.6", "discord-api-types": "^0.38.50", "magic-bytes.js": "^1.13.0", "tslib": "^2.6.3", "undici": "^6.27.0" } }, "sha512-wvOylxNYJkwKjctS/Mn5GP1w9r3/rzyH+ThD1JlAca6zEdlHs8QWBBUQJpU5Q+W6DoIj/Ljh1IPlZs7hTU+UAg=="], + + "@discordjs/util": ["@discordjs/util@1.2.0", "", { "dependencies": { "discord-api-types": "^0.38.33" } }, "sha512-3LKP7F2+atl9vJFhaBjn4nOaSWahZ/yWjOvA4e5pnXkt2qyXRCHLxoBQy81GFtLGCq7K9lPm9R517M1U+/90Qg=="], + + "@discordjs/ws": ["@discordjs/ws@1.2.3", "", { "dependencies": { "@discordjs/collection": "^2.1.0", "@discordjs/rest": "^2.5.1", "@discordjs/util": "^1.1.0", "@sapphire/async-queue": "^1.5.2", "@types/ws": "^8.5.10", "@vladfrangu/async_event_emitter": "^2.2.4", "discord-api-types": "^0.38.1", "tslib": "^2.6.2", "ws": "^8.17.0" } }, "sha512-wPlQDxEmlDg5IxhJPuxXr3Vy9AjYq5xCvFWGJyD7w7Np8ZGu+Mc+97LCoEc/+AYCo2IDpKioiH0/c/mj5ZR9Uw=="], + + "@sapphire/async-queue": ["@sapphire/async-queue@1.5.5", "", {}, "sha512-cvGzxbba6sav2zZkH8GPf2oGk9yYoD5qrNWdu9fRehifgnFZJMV+nuy2nON2roRO4yQQ+v7MK/Pktl/HgfsUXg=="], + + "@sapphire/shapeshift": ["@sapphire/shapeshift@4.0.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "lodash": "^4.17.21" } }, "sha512-d9dUmWVA7MMiKobL3VpLF8P2aeanRTu6ypG2OIaEv/ZHH/SUQ2iHOVyi5wAPjQ+HmnMuL0whK9ez8I/raWbtIg=="], + + "@sapphire/snowflake": ["@sapphire/snowflake@3.5.5", "", {}, "sha512-xzvBr1Q1c4lCe7i6sRnrofxeO1QTP/LKQ6A6qy0iB4x5yfiSfARMEQEghojzTNALDTcv8En04qYNIco9/K9eZQ=="], + + "@types/bun": ["@types/bun@1.3.14", "", { "dependencies": { "bun-types": "1.3.14" } }, "sha512-h1hFqFVcvAvD9j9K7ZW7vd82aSA+rTdznZa+5bwvCwqSB1jmmfLcbIWhOLx1/+boy/xmjgCs/OMUL8hRJSmnPw=="], + + "@types/node": ["@types/node@26.2.0", "", { "dependencies": { "undici-types": "~8.3.0" } }, "sha512-5IviulTZeRNp2vAJ514cc/HUlY5nZ9fCbq9DMyC52BrhFZACo3nI0R7qBxhQmo/d27NFe96ur/b7Wwxklda+kg=="], + + "@types/ws": ["@types/ws@8.18.1", "", { "dependencies": { "@types/node": "*" } }, "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg=="], + + "@vladfrangu/async_event_emitter": ["@vladfrangu/async_event_emitter@2.4.7", "", {}, "sha512-Xfe6rpCTxSxfbswi/W/Pz7zp1WWSNn4A0eW4mLkQUewCrXXtMj31lCg+iQyTkh/CkusZSq9eDflu7tjEDXUY6g=="], + + "bun-types": ["bun-types@1.3.14", "", { "dependencies": { "@types/node": "*" } }, "sha512-4N0ig0fEomHt5R0KCFWjovxow98rIoRwKolrYdCcknNwMekCXRnWEUvgu5soYV8QXtVsrUD8B95MBOZGPvr6KQ=="], + + "discord-api-types": ["discord-api-types@0.38.53", "", {}, "sha512-HL1zz/UuZ+bbJjA/X8Kbxx9gk8v9rJAbTeWRNYKmIdjwJ7EovjlHgoJTxcLpATfNJ+AonOtMdy3Y5MVIJAAd/A=="], + + "discord.js": ["discord.js@14.27.0", "", { "dependencies": { "@discordjs/builders": "^1.14.1", "@discordjs/collection": "1.5.3", "@discordjs/formatters": "^0.6.2", "@discordjs/rest": "^2.6.2", "@discordjs/util": "^1.2.0", "@discordjs/ws": "^1.2.3", "@sapphire/snowflake": "3.5.5", "discord-api-types": "^0.38.49", "fast-deep-equal": "3.1.3", "lodash.snakecase": "4.1.1", "magic-bytes.js": "^1.13.0", "tslib": "^2.6.3", "undici": "^6.27.0" } }, "sha512-qHbFlFG2N7y3LjPySYsL6A1+BnX6bkTVgo842EX0CqVPk/KTMwZkojPHEXKsQUpWZNyz5BISNHK1cPpQw0+m4A=="], + + "fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="], + + "lodash": ["lodash@4.18.1", "", {}, "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q=="], + + "lodash.snakecase": ["lodash.snakecase@4.1.1", "", {}, "sha512-QZ1d4xoBHYUeuouhEq3lk3Uq7ldgyFXGBhg04+oRLnIz8o9T65Eh+8YdroUwn846zchkA9yDsDl5CVVaV2nqYw=="], + + "magic-bytes.js": ["magic-bytes.js@1.13.1", "", {}, "sha512-x5sn4UX2k5gCWlcfmoFwG4TPie8+dctESyqOBdhB5p6MsgWXdBKGmt9nXPObj/JI50TTL928lc5Yt1WntMn1bw=="], + + "ts-mixer": ["ts-mixer@6.0.4", "", {}, "sha512-ufKpbmrugz5Aou4wcr5Wc1UUFWOLhq+Fm6qa6P0w0K5Qw2yhaUoiWszhCVuNQyNwrlGiscHOmqYoAox1PtvgjA=="], + + "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], + + "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], + + "undici": ["undici@6.28.0", "", {}, "sha512-LIY910g9TI13YS95lrMFrs8Rm/u/irgHeTWoKCoteeJ04CUJ92eEfj0rVn+7VKMPBpUPiUoBKfhNyLI23EE/KA=="], + + "undici-types": ["undici-types@8.3.0", "", {}, "sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ=="], + + "ws": ["ws@8.21.3", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-201TZ/kPWxoPr/OKWjquZR1SWKXcvxdH+e1xrx89b3YbmzLMFCLfnaG1HFIgWzJOEWZ7MvpK++odZufgYR50Rw=="], + + "@discordjs/rest/@discordjs/collection": ["@discordjs/collection@2.1.1", "", {}, "sha512-LiSusze9Tc7qF03sLCujF5iZp7K+vRNEDBZ86FT9aQAv3vxMLihUvKvpsCWiQ2DJq1tVckopKm1rxomgNUc9hg=="], + + "@discordjs/ws/@discordjs/collection": ["@discordjs/collection@2.1.1", "", {}, "sha512-LiSusze9Tc7qF03sLCujF5iZp7K+vRNEDBZ86FT9aQAv3vxMLihUvKvpsCWiQ2DJq1tVckopKm1rxomgNUc9hg=="], + } +} diff --git a/index.ts b/index.ts new file mode 100644 index 0000000..1245c49 --- /dev/null +++ b/index.ts @@ -0,0 +1,349 @@ +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("========================================"); \ No newline at end of file diff --git a/package.json b/package.json new file mode 100644 index 0000000..699231b --- /dev/null +++ b/package.json @@ -0,0 +1,15 @@ +{ + "name": "dm-all", + "module": "index.ts", + "type": "module", + "private": true, + "devDependencies": { + "@types/bun": "latest" + }, + "peerDependencies": { + "typescript": "^5" + }, + "dependencies": { + "discord.js": "^14.27.0" + } +} diff --git a/tokens.txt b/tokens.txt new file mode 100644 index 0000000..e69de29 diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..359f52e --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,30 @@ +{ + "compilerOptions": { + // Environment setup & latest features + "lib": ["ESNext"], + "target": "ESNext", + "module": "Preserve", + "moduleDetection": "force", + "jsx": "react-jsx", + "allowJs": true, + "types": ["bun"], + + // Bundler mode + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "noEmit": true, + + // Best practices + "strict": true, + "skipLibCheck": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedIndexedAccess": false, + "noImplicitOverride": true, + + // Some stricter flags (disabled by default) + "noUnusedLocals": false, + "noUnusedParameters": false, + "noPropertyAccessFromIndexSignature": false + } +}