Files
smsgw-tester/src/Client/Client.ts
2023-04-06 18:29:41 +02:00

288 lines
8.3 KiB
TypeScript

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<void> {
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<void> {
this.logger.log1(`Client-${this.id} closing connection`);
return Promise.resolve(this.session.close());
}
sendPdu(pdu: PDU, force?: boolean): Promise<object> {
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<void> {
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<void> {
return new Promise<void>((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);
}
}
}