import {PDU} from "../CommonObjects"; import Job from "../Job/Job"; import Logger from "../Logger"; import PduProcessor from "../PDUProcessor/PduProcessor"; import DeliverSmReplyProcessor from "../PDUProcessor/Postprocessor/Client/DeliverSmReplyProcessor"; import ProcessorManager from "../PDUProcessor/ProcessorManager"; import PersistentPromise from "../PersistentPromise"; import SmppSession from "../SmppSession"; const NanoTimer = require('nanotimer'); const smpp = require("smpp"); const AUTO_ENQUIRE_LINK_PERIOD: number = Number(process.env.AUTO_ENQUIRE_LINK_PERIOD) || 30000; export default class Client extends SmppSession { readonly STATUSES: string[] = [ "NOT CONNECTED", "CONNECTING", "CONNECTED", "BINDING", "BOUND", "BUSY", ] url: string; _username: string; _password: string; _id: number; _status: string = this.STATUSES[0]; readonly logger: Logger; private session?: any; private connectPromise: PersistentPromise | null = null; private bindPromise: PersistentPromise | null = null; private closePromise: PersistentPromise | null = null; constructor(id: number, url: string, username: string, password: string) { super(); this._id = id; this._username = username; this._password = password; this.url = url; this._defaultSingleJob = Job.createEmptySingle('submit_sm'); this._defaultMultipleJob = Job.createEmptyMultiple('submit_sm'); ProcessorManager.attachProcessor(this, ProcessorManager.getProcessor(DeliverSmReplyProcessor.name)); this.logger = new Logger(`Client-${id}`); } _defaultSingleJob: Job; get defaultSingleJob(): Job { return this._defaultSingleJob; } set defaultSingleJob(job: Job) { if (job.pdu && !job.pdu.command) { job.pdu.command = 'submit_sm'; } super.defaultSingleJob = job; } _defaultMultipleJob: Job; get defaultMultipleJob(): Job { return this._defaultMultipleJob; } set defaultMultipleJob(job: Job) { if (job.pdu && !job.pdu.command) { job.pdu.command = 'submit_sm'; } super.defaultMultipleJob = job; } doConnect(): PersistentPromise { this.connectPromise = new PersistentPromise((resolve, reject) => { if (this.status !== this.STATUSES[0]) { let errorString = `Client-${this.id} already connected`; this.logger.log1(errorString); reject(errorString); return; } this.logger.log1(`Client-${this.id} connecting to ${this.url}`); this.setStatus(1); this.connectSession().then(resolve, ((err: any) => { this.logger.log1(`Client-${this.id} connection failed: ${err}`); this.setStatus(0); this.session.close(); reject(err); })); }); return this.connectPromise; } doBind(): PersistentPromise { this.bindPromise = new PersistentPromise((resolve, reject) => { this.validateFields(reject); this.session.bind_transceiver({ system_id: this.username, password: this.password, }, this.eventBindReply.bind(this)); this.setStatus(3); }); return this.bindPromise; } connectAndBind(): Promise { return this.doConnect().then(this.doBind.bind(this), (error) => { this.logger.log1(`Client-${this.id} connectAndBind failed: ${error}`); }); } serialize(): object { // TODO: Generify this further by moving it to smpp session and creating a... "postSerialize" that is abstract return { id: this._id, url: this.url, username: this._username, password: this._password, status: this._status, defaultSingleJob: this._defaultSingleJob.serialize(), defaultMultipleJob: this._defaultMultipleJob.serialize(), preprocessors: this.processors.Preprocessor.map((p: PduProcessor) => p.serialize()), postprocessors: this.processors.Postprocessor.map((p: PduProcessor) => p.serialize()), availablePreprocessors: ProcessorManager.getPreprocessorsForType(this.constructor.name).map((p: PduProcessor) => p.serialize()), availablePostprocessors: ProcessorManager.getPostprocessorsForType(this.constructor.name).map((p: PduProcessor) => p.serialize()), }; } close(): Promise { this.logger.log1(`Client-${this.id} closing connection`); return Promise.resolve(this.session.close()); } sendPdu(pdu: PDU, force?: boolean): Promise { return new Promise((resolve, reject) => { if (!force) { this.validateSession(reject); this.validateBound(reject); } // Is this expensive...? let pduCopy = new smpp.PDU(pdu.command, {...pdu}); this.processors.Preprocessor.forEach((processor: PduProcessor) => processor.processPdu(this.session, pduCopy)); this.logger.log5(`Client-${this.id} sending PDU: ${JSON.stringify(pduCopy)}`); this.doSendPdu(pduCopy, this.session).then((replyPdu: any) => { resolve(replyPdu); }); }); } sendMultiple(job: Job): Promise { return new Promise((resolve, reject) => { this.validateSession(reject); this.validateBound(reject); if (!job.count || !job.perSecond) { reject(`Client-${this.id} sendMultiple failed: invalid job, missing fields`); } this.logger.log1(`Client-${this.id} sending multiple messages: ${JSON.stringify(job)}`); this.setStatus(4); let counter: number = 0; let previousUpdateCounter: number = 0; this.counterUpdateTimer.setInterval(() => { if (previousUpdateCounter !== counter) { this.eventEmitter.emit(this.EVENT.MESSAGE_SEND_COUNTER_UPDATE_EVENT, counter); previousUpdateCounter = counter; } }, '', `${this.MESSAGE_SEND_UPDATE_DELAY / 1000} s`); let count: number = job.count || 1; let interval: number = 1 / (job.perSecond || 1); this.setStatus(5); this.sendTimer.setInterval(() => { if (count > 0 && counter >= count) { this.cancelSendInterval(); } else { this.sendPdu(job.pdu, true) .catch(e => this.logger.log1(`Error sending message: ${e}`)); counter++; } }, '', `${interval} s`); resolve(); }); } private connectSession(): Promise { return new Promise((resolve, reject) => { this.validateFields(reject); this.logger.log1(`Client-${this.id} connecting to ${this.url}`); this.session = smpp.connect({ url: this.url, auto_enquire_link_period: AUTO_ENQUIRE_LINK_PERIOD, }, this.eventSessionConnected.bind(this)); this.session.on('error', this.eventSessionError.bind(this)); this.session.on('close', this.eventSessionClose.bind(this)); this.session.on('pdu', this.eventAnyPdu.bind(this, this.session)); }); } private eventSessionConnected(): void { this.logger.log1(`Client-${this.id} connected to ${this.url}`); this.setStatus(2); if (this.connectPromise) { this.connectPromise.resolve(); } else { this.logger.log1(`Client-${this.id} connected without connect promise`); } } private eventSessionError(pdu: any): void { this.logger.log1(`Client-${this.id} error on ${this.url} - ${pdu.message}`); this.setStatus(0); this.rejectPromises(); } private eventSessionClose(): void { this.logger.log1(`Client-${this.id} closed on ${this.url}`); this.setStatus(0); this.rejectPromises(); } private eventBindReply(pdu: PDU): void { if (pdu.command_status === 0) { this.logger.log1(`Client-${this.id} bound to ${this.url}`); this.setStatus(4); if (this.bindPromise) { this.bindPromise.resolve(); } } else { this.logger.log1(`Client-${this.id} bind failed to ${this.url}`); this.setStatus(2); if (this.bindPromise) { this.bindPromise.reject(pdu); } } } private rejectPromises(err?: any): void { if (this.connectPromise) { this.connectPromise.reject(err); } if (this.bindPromise) { this.bindPromise.reject(err); } if (this.closePromise) { this.closePromise.resolve(); } } private validateFields(reject: (reason?: any) => void) { if (!this.url) { let error = `Client-${this.id} has no url set`; this.logger.log1(error); reject(error); } if (!this.username) { let error = `Client-${this.id} has no username set`; this.logger.log1(error); reject(error); } if (!this.password) { let error = `Client-${this.id} has no password set`; this.logger.log1(error); reject(error); } } private validateSession(reject: (reason?: any) => void) { if (!this.session) { let errorMessage = `Client-${this.id} session is not defined`; this.logger.log1(errorMessage); reject(errorMessage); } } private validateBound(reject: (reason?: any) => void) { if (this.status !== this.STATUSES[4]) { let errorMessage = `Client-${this.id} is not bound`; this.logger.log1(errorMessage); reject(errorMessage); } } }