bas-remote-node
Version:
NodeJS library, which allows you to automate Google Chrome browser.
304 lines (271 loc) • 9.04 kB
JavaScript
const path = require('path');
const { cwd } = require('process');
const BasThread = require('./thread');
const SocketService = require('./services/socket');
const EngineService = require('./services/engine');
const { once, inject, random } = require('./utils');
const DEFAULT_WORKING_DIR = path.join(cwd(), 'data');
module.exports = class BasRemoteClient {
/**
* Create an instance of BasRemoteClient class.
*
* @constructor
* @param {Object} options - remote control options object.
* @param {String} options.workingDir - location of the selected working folder.
* @param {String} options.scriptName - name of the selected private script.
* @param {String} options.password - password from a user account with access to the script.
* @param {String} options.login - login from a user account with access to the script.
* @param {String[]} options.args - additional arguments to be passed to the script.
*/
constructor({ workingDir = DEFAULT_WORKING_DIR, scriptName = '', password = '', login = '', args = [] } = {}) {
if (!workingDir) throw new Error(
"Please define 'options.workingDir' setting"
);
if (!scriptName) throw new Error(
"Please define 'options.scriptName' setting"
);
this.options = { scriptName, password, login, args };
this._socket = new SocketService(this.options);
this._engine = new EngineService(this.options);
this.setWorkingFolder(workingDir);
this._waitResolve = () => {};
this._waitReject = () => {};
this._isStarted = false;
this._requests = {};
this.on('messageReceived', (message) => {
const { async, data, type, id } = message;
if (type === 'thread_start') {
this._waitResolve();
} else if (type === 'message') {
this._waitReject(new Error(data.text));
} else if (type === 'initialize') {
this._send('accept_resources', { '-bas-empty-script-': true });
} else if (async && id) {
if (type === 'get_global_variable') {
const callback = this._requests[id];
delete this._requests[id];
callback(JSON.parse(data));
} else {
const callback = this._requests[id];
delete this._requests[id];
callback(data);
}
}
});
this.on('open', () => {
this._send('remote_control_data', {
script: this.options.scriptName,
password: this.options.password,
login: this.options.login,
});
});
}
/**
* Add one-time event listener with selected name.
*
* Possible event names:
* - `messageReceived` - invoked when WebSocket receive new message.
* - `messageSent` - invoked when WebSocket send new message.
*
* Event listeners related to sending and receiving messages
* accept a single argument `{ async, type, data, id }`.
* @example
* client.once('messageReceived', (message) => {
* console.log(`Message received: ${message.type}`));
* });
*/
once(event, listener) {
this._socket.once(event, listener);
return this;
}
/**
* Add event listener with selected name.
*
* Possible event names:
* - `messageReceived` - invoked when WebSocket receive new message.
* - `messageSent` - invoked when WebSocket send new message.
*
* Event listeners related to sending and receiving messages
* accept a single argument `{ async, type, data, id }`.
* @example
* client.on('messageReceived', (message) => {
* console.log(`Message received: ${message.type}`));
* });
*/
on(event, listener) {
this._socket.on(event, listener);
return this;
}
/**
* Remove event listener with selected name.
*
* Possible event names:
* - `messageReceived` - invoked when WebSocket receive new message.
* - `messageSent` - invoked when WebSocket send new message.
*
* Event listeners related to sending and receiving messages
* accept a single argument `{ async, type, data, id }`.
* @example
* const callback = (message) => {
* console.log(message.type);
* });
*
* client.on('messageReceived', callback);
* // any actions
* client.off('messageReceived', callback);
*/
off(event, listener) {
this._socket.off(event, listener);
return this;
}
/**
* Start the client and wait for it initialize.
* @param {Number} [timeout] - client initialization timeout (in milliseconds).
*/
async start(timeout = 60000) {
if (this._isStarted) return;
await this._engine.initialize();
const port = random(10000, 20000);
await this._engine.start(port);
await this._socket.start(port);
await new Promise((resolve, reject) => {
const id = setTimeout(() => reject(new Error('Timeout during client initialize')), timeout);
this._waitResolve = (res) => {
clearTimeout(id);
resolve(res);
};
this._waitReject = (err) => {
clearTimeout(id);
reject(err);
};
});
this._isStarted = true;
}
/**
* Change the client and engine working folder.
* @param {String} workingDir - location of the selected working folder.
*/
setWorkingFolder(workingDir = DEFAULT_WORKING_DIR) {
this._engine.setWorkingFolder(path.resolve(workingDir));
}
/**
* Call the BAS function asynchronously.
* @param {String} functionName - BAS function name as string.
* @param {Object} functionParams - BAS function arguments list.
* @example
* const links = await client.runFunction('GoogleSearch', {
* Query: 'cats'
* });
*
* links.forEach((link) => console.log(link));
*/
runFunction(functionName, functionParams = {}) {
if (!this._isStarted) {
return Promise.reject(new Error('Please start the client before calling this method'));
}
const threadId = random(1, 1000000);
const promise = new Promise((resolve, reject) => {
this._startThread(threadId);
this.sendAsync('run_task', {
params: JSON.stringify(functionParams),
function_name: functionName,
thread_id: threadId,
}).then((result) => {
const response = JSON.parse(result);
if (response.Success) {
resolve(response.Result);
} else {
reject(new Error(response.Message));
}
this._stopThread(threadId);
}).finally(
once(this._socket, 'close', () => reject(new Error(
'The client connection has been closed.'
)))
);
});
return inject(promise, this, threadId);
}
/**
* Send the custom message and get message id as result.
* @param {String} type - selected message type.
* @param {Object} data - message arguments.
* @param {Boolean} isAsync - is message async.
* @private
*/
_send(type, data, isAsync = false) {
return this._socket.send({
id: random(100000, 999999),
async: isAsync,
type,
data,
});
}
/**
* Send the custom message asynchronously.
* @param {String} type - selected message type.
* @param {Object} data - message arguments.
* @private
*/
_sendAsync(type, data) {
return new Promise((resolve) => {
const id = this.send(type, data, true);
this._requests[id] = resolve;
});
}
/**
* Send the custom message and get message id as result.
* @param {String} type - selected message type.
* @param {Object} data - message arguments.
* @param {Boolean} isAsync - is message async.
*/
send(type, data, isAsync = false) {
if (!this._isStarted) throw new Error(
'Please start the client before calling this method'
);
return this._send(type, data, isAsync);
}
/**
* Send the custom message asynchronously.
* @param {String} type - selected message type.
* @param {Object} data - message arguments.
*/
sendAsync(type, data) {
if (!this._isStarted) {
return Promise.reject(new Error('Please start the client before calling this method'));
}
return this._sendAsync(type, data);
}
/**
* Start thread with specified id.
* @param {Number} threadId - thread identifier.
* @private
*/
_startThread(threadId) {
this.send('start_thread', { thread_id: threadId });
}
/**
* Stop thread with specified id.
* @param {Number} threadId - thread identifier.
* @private
*/
_stopThread(threadId) {
this.send('stop_thread', { thread_id: threadId });
}
/**
* Create new BAS thread object.
*/
createThread() {
return new BasThread(this);
}
/**
* Close the client.
*/
async close() {
await Promise.all([
this._engine.close(),
this._socket.close(),
]);
this._isStarted = false;
}
};