'use strict';
/* Require Optional Dependencies */
try {
var fs = require('fs-extra');
var AdmZip = require('adm-zip');
var archiver = require('archiver');
} catch {
fs = undefined;
AdmZip = undefined;
archiver = undefined;
}
const path = require('path');
const { Events } = require('./../util/Constants');
const BaseAuthStrategy = require('./BaseAuthStrategy');
/**
* Remote-based authentication
* @param {object} options - options
* @param {object} options.store - Remote database store instance
* @param {string} options.clientId - Client id to distinguish instances if you are using multiple, otherwise keep null if you are using only one instance
* @param {string} options.dataPath - Change the default path for saving session files, default is: "./.wwebjs_auth/"
* @param {number} options.backupSyncIntervalMs - Sets the time interval for periodic session backups. Accepts values starting from 60000ms {1 minute}
* @param {number} options.rmMaxRetries - Sets the maximum number of retries for removing the session directory
*/
class RemoteAuth extends BaseAuthStrategy {
constructor({ clientId, dataPath, store, backupSyncIntervalMs, rmMaxRetries } = {}) {
if (!fs && !AdmZip && !archiver) throw new Error('Optional Dependencies [fs-extra, adm-zip, archiver] are required to use RemoteAuth. Make sure to run npm install correctly and remove the --no-optional flag');
super();
const idRegex = /^[-_\w]+$/i;
if (clientId && !idRegex.test(clientId)) {
throw new Error('Invalid clientId. Only alphanumeric characters, underscores and hyphens are allowed.');
}
if (!backupSyncIntervalMs || backupSyncIntervalMs < 60000) {
throw new Error('Invalid backupSyncIntervalMs. Accepts values starting from 60000ms {1 minute}.');
}
if(!store) throw new Error('Remote database store is required.');
this.store = store;
this.clientId = clientId;
this.backupSyncIntervalMs = backupSyncIntervalMs;
this.dataPath = path.resolve(dataPath || './.wwebjs_auth/');
this.tempDir = `${this.dataPath}/wwebjs_temp_session_${this.clientId}`;
this.requiredDirs = ['Default', 'IndexedDB', 'Local Storage']; /* => Required Files & Dirs in WWebJS to restore session */
this.rmMaxRetries = rmMaxRetries ?? 4;
}
async beforeBrowserInitialized() {
const puppeteerOpts = this.client.options.puppeteer;
const sessionDirName = this.clientId ? `RemoteAuth-${this.clientId}` : 'RemoteAuth';
const dirPath = path.join(this.dataPath, sessionDirName);
if (puppeteerOpts.userDataDir && puppeteerOpts.userDataDir !== dirPath) {
throw new Error('RemoteAuth is not compatible with a user-supplied userDataDir.');
}
this.userDataDir = dirPath;
this.sessionName = sessionDirName;
await this.extractRemoteSession();
this.client.options.puppeteer = {
...puppeteerOpts,
userDataDir: dirPath
};
}
async logout() {
await this.disconnect();
}
async destroy() {
clearInterval(this.backupSync);
}
async disconnect() {
await this.deleteRemoteSession();
let pathExists = await this.isValidPath(this.userDataDir);
if (pathExists) {
await fs.promises.rm(this.userDataDir, {
recursive: true,
force: true,
maxRetries: this.rmMaxRetries,
}).catch(() => {});
}
clearInterval(this.backupSync);
}
async afterAuthReady() {
const sessionExists = await this.store.sessionExists({session: this.sessionName});
if(!sessionExists) {
await this.delay(60000); /* Initial delay sync required for session to be stable enough to recover */
await this.storeRemoteSession({emit: true});
}
var self = this;
this.backupSync = setInterval(async function () {
await self.storeRemoteSession();
}, this.backupSyncIntervalMs);
}
async storeRemoteSession(options) {
const pathExists = await this.isValidPath(this.userDataDir);
if (!pathExists) return;
let compressedSessionPath;
try {
compressedSessionPath = await this.compressSession();
await this.store.save({ session: path.join(this.dataPath, this.sessionName) });
if(options && options.emit) this.client.emit(Events.REMOTE_SESSION_SAVED);
} finally {
const paths = [
this.tempDir,
...(compressedSessionPath ? [compressedSessionPath] : [])
];
await Promise.allSettled(
paths.map((p) =>
fs.promises.rm(p, {
recursive: true,
force: true,
maxRetries: this.rmMaxRetries,
})
)
);
}
}
async extractRemoteSession() {
const pathExists = await this.isValidPath(this.userDataDir);
const compressedSessionPath = path.join(this.dataPath, `${this.sessionName}.zip`);
const sessionExists = await this.store.sessionExists({session: this.sessionName});
if (pathExists) {
await fs.promises.rm(this.userDataDir, {
recursive: true,
force: true,
maxRetries: this.rmMaxRetries,
}).catch(() => {});
}
if (sessionExists) {
await this.store.extract({session: this.sessionName, path: compressedSessionPath});
await this.unCompressSession(compressedSessionPath);
} else {
fs.mkdirSync(this.userDataDir, { recursive: true });
}
}
async deleteRemoteSession() {
const sessionExists = await this.store.sessionExists({session: this.sessionName});
if (sessionExists) await this.store.delete({session: this.sessionName});
}
async compressSession() {
const stageDefaultPath = path.join(this.tempDir, 'Default');
const userDataDefaultPath = path.join(this.userDataDir, 'Default');
await fs.emptyDir(stageDefaultPath);
await this.copyByRequiredDirs(userDataDefaultPath, stageDefaultPath);
const archive = archiver('zip');
const outPath = path.join(this.dataPath, `${this.sessionName}.zip`);
const out = fs.createWriteStream(outPath);
await new Promise((resolve, reject) => {
out.once('close', resolve);
out.once('error', reject);
archive.once('error', reject);
archive.pipe(out);
archive.directory(this.tempDir, false);
archive.finalize();
});
return outPath;
}
async unCompressSession(compressedSessionPath) {
await new Promise((resolve, reject) => {
var zip = new AdmZip(compressedSessionPath);
zip.extractAllToAsync(this.userDataDir, false, false, (err) => {
if (err) {
reject(err);
} else {
resolve();
}
});
});
await fs.promises.unlink(compressedSessionPath);
}
async copyByRequiredDirs(from, to) {
for (const d of this.requiredDirs) {
const src = path.join(from, d);
if (await this.isValidPath(src)) {
const dest = path.join(to, path.basename(src));
await this.copyResilient(src, dest);
}
}
}
async copyResilient(srcDir, destDir) {
await fs.ensureDir(destDir);
const items = await fs.readdir(srcDir);
for (const item of items) {
const srcPath = path.join(srcDir, item);
const destPath = path.join(destDir, item);
try {
const stat = await fs.stat(srcPath);
if (stat.isDirectory()) {
await this.copyResilient(srcPath, destPath);
} else {
await fs.copy(srcPath, destPath);
}
} catch (err) {
console.error(`Error copying ${srcPath}:`, err);
}
}
}
async isValidPath(path) {
try {
await fs.promises.access(path);
return true;
} catch {
return false;
}
}
async delay(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
}
module.exports = RemoteAuth;