@madzoffc/baileys-shard
Version:
A lightweight baileys multi session for whatsapp bot.
534 lines (527 loc) • 18.4 kB
JavaScript
"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
});