UNPKG

@madzoffc/baileys-shard

Version:

A lightweight baileys multi session for whatsapp bot.

534 lines (527 loc) 18.4 kB
"use strict"; var __create = Object.create; var __defProp = Object.defineProperty; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropNames = Object.getOwnPropertyNames; var __getProtoOf = Object.getPrototypeOf; var __hasOwnProp = Object.prototype.hasOwnProperty; var __export = (target, all) => { for (var name in all) __defProp(target, name, { get: all[name], enumerable: true }); }; var __copyProps = (to, from, except, desc) => { if (from && typeof from === "object" || typeof from === "function") { for (let key of __getOwnPropNames(from)) if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); } return to; }; var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps( // If the importer is in node compatibility mode or this is not an ESM // file that has been converted to a CommonJS file using a Babel- // compatible transform (i.e. "__esModule" has not been set), then set // "default" to the CommonJS "module.exports" for node compatibility. isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target, mod )); var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); // src/index.ts var index_exports = {}; __export(index_exports, { ShardError: () => Error_exports, ShardInfo: () => ShardInfo_exports, ShardManager: () => Client_exports, default: () => index_default }); module.exports = __toCommonJS(index_exports); // src/Client/index.ts var Client_exports = {}; __export(Client_exports, { ShardManager: () => ShardManager }); var import_events = require("events"); var import_pino = __toESM(require("pino")); var import_path = __toESM(require("path")); var import_qr_image = __toESM(require("qr-image")); var glob = __toESM(require("glob")); var import_fs = __toESM(require("fs")); // src/Utils/ShardInfo.ts var ShardInfo_exports = {}; __export(ShardInfo_exports, { ShardInfo: () => ShardInfo }); var ShardInfo = class { id; index; total; phoneNumber; status; updatedAt; constructor({ id, index, total, phoneNumber = null, status = "initializing" }) { this.id = id; this.index = index; this.total = total; this.phoneNumber = phoneNumber; this.status = status; this.updatedAt = /* @__PURE__ */ new Date(); } update(fields = {}) { Object.assign(this, fields); this.updatedAt = /* @__PURE__ */ new Date(); } }; // src/Utils/Error.ts var Error_exports = {}; __export(Error_exports, { ShardError: () => ShardError, default: () => Error_default }); var ShardError = class extends Error { code; constructor(message, code = "UNKNOWN") { super(message); this.name = "ShardError"; this.code = code; Object.setPrototypeOf(this, new.target.prototype); } }; var Error_default = { ShardError }; // src/Client/index.ts var logger = (0, import_pino.default)( { level: "info", formatters: { level(label) { return { level: label }; } }, timestamp: () => `,"time":"${(/* @__PURE__ */ new Date()).toISOString()}"`, base: { pid: false, hostname: false } }, import_pino.default.destination("./baileys-shard-logs.txt") ); function wrapShardError(fn, shardId, code = "UNKNOWN") { return async (...args) => { try { return await fn(...args); } catch (err) { const shardErr = err instanceof ShardError ? err : new ShardError(err.message, code); throw shardErr; } }; } var ShardManager = class extends import_events.EventEmitter { #sessionDirectory = "./sessions"; #shards = /* @__PURE__ */ new Map(); #shardsInfo = /* @__PURE__ */ new Map(); _patched; constructor(config = {}) { super(); this.#sessionDirectory = config?.session || this.#sessionDirectory; this.cleanupCorruptSessions().catch((err) => { logger.error(`Failed to cleanup sessions on startup: ${err}`); }); } async checkSessionStatus(sessionDirectory) { try { const credsPath = import_path.default.join(sessionDirectory, "creds.json"); if (!import_fs.default.existsSync(sessionDirectory) || !import_fs.default.existsSync(credsPath)) { return { exists: false, registered: false, valid: false }; } const raw = import_fs.default.readFileSync(credsPath, "utf8"); let creds; try { creds = JSON.parse(raw); } catch (e) { return { exists: true, registered: false, valid: false, reason: "Corrupt JSON" }; } const isRegistered = creds?.registered === true; const requiredFields = [ "noiseKey", "pairingEphemeralKeyPair", "signedIdentityKey", "signedPreKey" ]; const hasRequiredFields = requiredFields.every((f) => creds?.[f]); return { exists: true, registered: isRegistered, valid: isRegistered && hasRequiredFields, reason: !hasRequiredFields ? "Missing required fields" : void 0 }; } catch (err) { return { exists: true, registered: false, valid: false, reason: `Check error: ${err}` }; } } async validateAndCleanSession(sessionDirectory) { try { const status = await this.checkSessionStatus(sessionDirectory); if (status.valid && status.registered) { logger.info(`Session is valid and registered, keeping: ${sessionDirectory}`); return; } if (status.exists && (!status.registered || !status.valid)) { logger.warn(`Cleaning invalid session (${status.reason}): ${sessionDirectory}`); import_fs.default.rmSync(sessionDirectory, { recursive: true, force: true }); } } catch (err) { logger.error(`validateAndCleanSession error: ${err}`); if (import_fs.default.existsSync(sessionDirectory)) { import_fs.default.rmSync(sessionDirectory, { recursive: true, force: true }); } } } async cleanupCorruptSessions() { try { const sessions = glob.sync(this.#sessionDirectory + "/*"); for (const sessionPath of sessions) { await this.validateAndCleanSession(sessionPath); } } catch (error) { logger.error(`Error during session cleanup: ${error}`); } } setupShardEventHandlers(sock, id, saveCreds, options) { this.#shards.set(id, sock); this.#shardsInfo.set( id, new ShardInfo({ id, index: this.#shards.size, total: this.#shards.size, phoneNumber: options?.phoneNumber || null, status: "initializing" }) ); const forwardEvents = [ "messages.upsert", "messages.update", "messages.delete", "messages.reaction", "message-receipt.update", "messaging-history.set", "chats.upsert", "chats.update", "chats.delete", "blocklist.set", "blocklist.update", "call", "contacts.upsert", "contacts.update", "groups.upsert", "groups.update", "group-participants.update", "presence.update" ]; for (const ev of forwardEvents) { sock.ev.on(ev, (data) => { this.emit(ev, { shardId: id, sock, data }); }); } sock.ev.on("creds.update", (data) => { this.emit("creds.update", { shardId: id, sock, data }); }); sock.ev.on("connection.update", async (update) => { const { connection, lastDisconnect, qr } = update; if (qr) { const image = import_qr_image.default.imageSync(qr, { type: "png", size: 10, margin: 1 }); this.emit("login.update", { shardId: id, state: "connecting", type: "qr", image }); } if (connection === "open") { const isRegistered = sock?.authState?.creds?.registered ?? false; if (!isRegistered) { logger.warn(`Session ${id} connected but not registered, recreating...`); this.emit("login.update", { shardId: id, state: "logged_out", reason: "Session not registered, clearing session..." }); return await this.recreateShard({ id, ...options, clearSession: true }); } this.#shardsInfo.get(id)?.update({ status: "connected" }); this.emit("login.update", { shardId: id, state: "connected" }); } if (connection === "close") { const isRegistered = sock?.authState?.creds?.registered ?? false; const statusCode = lastDisconnect?.error?.output?.statusCode; const baileys = await import("baileys"); const shouldReconnect = statusCode !== baileys.DisconnectReason.loggedOut; if (!isRegistered || !shouldReconnect) { logger.warn(`Session ${id} closed and not registered or logged out, clearing...`); this.#shardsInfo.get(id)?.update({ status: "logged_out" }); this.emit("login.update", { shardId: id, state: "logged_out", reason: !isRegistered ? "Session not registered" : "Logged out" }); return await this.recreateShard({ id, ...options, clearSession: true }); } this.#shardsInfo.get(id)?.update({ status: "disconnected" }); this.emit("login.update", { shardId: id, state: "disconnected", reason: "Connection closed, retrying..." }); setTimeout(() => { this.recreateShard({ id, ...options }); }, 5e3); } }); sock.ev.on("creds.update", async () => { try { await saveCreds(); this.emit("login.update", { shardId: id, state: "creds_saved" }); } catch (err) { logger.error(`Failed to save creds for ${id}: ${err.message}`); this.emit("shard.error", { shardId: id, error: new ShardError(err.message, "CREDS_SAVE_FAILED") }); } }); if (options.phoneNumber && !sock.authState.creds?.registered) { setTimeout(async () => { try { const code = await sock.requestPairingCode(options.phoneNumber ?? ""); this.emit("login.update", { shardId: id, state: "connecting", type: "pairing", code }); } catch (err) { this.emit("shard.error", { shardId: id, error: new ShardError(err.message, "PAIRING_FAILED") }); } }, 5e3); } } async createShard(options = {}) { try { const baileys = await import("baileys"); const { default: makeWASocket, useMultiFileAuthState } = baileys; const currentShard = this.#shards.size; const id = options?.id || `shard-${currentShard + 1}`; const sessionDirectory = import_path.default.join(this.#sessionDirectory, id); if (this.#shards.has(id)) { const existingShardInfo = this.#shardsInfo.get(id); if (existingShardInfo?.status === "connected" || existingShardInfo?.status === "initializing") { logger.info(`Shard ${id} already exists and active, returning existing instance`); return { id, sock: this.#shards.get(id) }; } } const sessionStatus = await this.checkSessionStatus(sessionDirectory); if (sessionStatus.registered && sessionStatus.valid) { logger.info(`Session ${id} is already registered and valid, reusing existing session`); } else if (sessionStatus.exists && !sessionStatus.valid) { logger.warn(`Session ${id} exists but invalid (${sessionStatus.reason}), cleaning...`); await this.validateAndCleanSession(sessionDirectory); } else if (!sessionStatus.exists) { logger.info(`Creating new session for ${id}`); } let { state, saveCreds } = await useMultiFileAuthState(sessionDirectory); if (state.creds?.registered === false) { logger.warn(`Auth state shows not registered for ${id}, creating fresh session...`); if (import_fs.default.existsSync(sessionDirectory)) { import_fs.default.rmSync(sessionDirectory, { recursive: true, force: true }); } const recreated = await useMultiFileAuthState(sessionDirectory); state = recreated.state; saveCreds = recreated.saveCreds; } else if (state.creds?.registered === true) { logger.info(`Using existing registered session for ${id}`); } const sock = makeWASocket({ auth: state, printQRInTerminal: !options?.phoneNumber, logger, ...options?.socket }); this.setupShardEventHandlers(sock, id, saveCreds, options); return { id, sock }; } catch (err) { const shardErr = new ShardError(`Failed to create shard: ${err.message}`, "CREATE_FAILED"); this.emit("shard.error", { shardId: options?.id, error: shardErr }); throw shardErr; } } async recreateShard(options) { const { id, clearSession = false, retryCount = 0, forceRecreate = false, ...restOptions } = options; const maxRetries = 3; try { const sessionDirectory = import_path.default.join(this.#sessionDirectory, id); if (!forceRecreate && !clearSession) { const sessionStatus = await this.checkSessionStatus(sessionDirectory); if (sessionStatus.registered && sessionStatus.valid) { logger.info(`Session ${id} is already registered and valid, skipping recreation`); const oldSock2 = this.#shards.get(id); if (oldSock2) { try { if (oldSock2.ws) oldSock2.ws.close(); if (typeof oldSock2.end === "function") oldSock2.end(); } catch (cleanupErr) { logger.warn(`Error cleaning up old socket for ${id}: ${cleanupErr}`); } this.#shards.delete(id); this.#shardsInfo.delete(id); } await new Promise((r) => setTimeout(r, 2e3)); return await this.createShard({ id, ...restOptions }); } } const oldSock = this.#shards.get(id); if (oldSock) { try { if (oldSock.ws) oldSock.ws.close(); if (typeof oldSock.end === "function") oldSock.end(); } catch (cleanupErr) { logger.warn(`Error cleaning up old socket for ${id}: ${cleanupErr}`); } this.#shards.delete(id); this.#shardsInfo.delete(id); } if (clearSession) { if (import_fs.default.existsSync(sessionDirectory)) { import_fs.default.rmSync(sessionDirectory, { recursive: true, force: true }); logger.warn(`Session for ${id} forcefully cleared before recreating`); } } else { await this.validateAndCleanSession(sessionDirectory); } await new Promise((r) => setTimeout(r, 2e3)); return await this.createShard({ id, ...restOptions }); } catch (err) { if (retryCount < maxRetries) { logger.warn(`Retrying recreate shard ${id} (attempt ${retryCount + 1}/${maxRetries})`); await new Promise((r) => setTimeout(r, 5e3 * (retryCount + 1))); return await this.recreateShard({ ...options, retryCount: retryCount + 1, clearSession: retryCount >= 2 }); } const shardErr = new ShardError( `Failed to recreate shard after ${maxRetries} attempts: ${err.message}`, "RECREATE_FAILED" ); this.emit("shard.error", { shardId: id, error: shardErr }); throw shardErr; } } async getSessionInfo(id) { const sessionDirectory = import_path.default.join(this.#sessionDirectory, id); return await this.checkSessionStatus(sessionDirectory); } async connect(id) { return wrapShardError(this.recreateShard.bind(this), id, "CONNECT_FAILED")({ id }); } async stopShard(id) { const sock = this.#shards.get(id); if (!sock) { const err = new ShardError(`Shard ${id} not found`, "SHARD_NOT_FOUND"); this.emit("shard.error", { shardId: id, error: err }); throw err; } try { if (sock.ws) sock.ws.close(); if (typeof sock.end === "function") sock.end(); this.#shards.delete(id); this.#shardsInfo.get(id)?.update({ status: "stopped" }); return true; } catch (err) { const shardErr = new ShardError(`Failed to stop shard ${id}: ${err.message}`, "STOP_FAILED"); this.emit("shard.error", { shardId: id, error: shardErr }); throw shardErr; } } async loadAllShards() { try { const sessions = glob.sync(this.#sessionDirectory + "/*"); const ids = []; if (!sessions.length) { const err = new ShardError("No sessions found", "NO_SESSIONS"); this.emit("shard.error", { shardId: null, error: err }); } for (const file of sessions) { const shardId = import_path.default.basename(file); try { const { id } = await this.createShard({ id: shardId }); ids.push(id); } catch (err) { this.emit("shard.error", { shardId, error: new ShardError(err.message, "LOAD_FAILED") }); } } return ids; } catch (err) { const shardErr = new ShardError(`Failed to load shards: ${err.message}`, "LOAD_FAILED"); this.emit("shard.error", { shardId: null, error: shardErr }); return []; } } socket(id) { return this.#shards.get(id); } shard(id) { const sock = this.#shards.get(id); if (!sock) return null; const emitter = new import_events.EventEmitter(); this.onAny((event, payload) => { if (payload?.shardId === id) { emitter.emit(event, payload); } }); return emitter; } onAny(listener) { const origEmit = this.emit; if (this._patched) return; this.emit = (event, ...args) => { listener(event, ...args); return origEmit.call(this, event, ...args); }; this._patched = true; } getShardInfo(id) { return this.#shardsInfo.get(id) || null; } getAllShardInfo() { return Array.from(this.#shardsInfo.values()); } }; // src/index.ts var index_default = { ShardManager, ShardInfo, ShardError }; // Annotate the CommonJS export names for ESM import in node: 0 && (module.exports = { ShardError, ShardInfo, ShardManager });