@ntlab/sms-gateway
Version:
436 lines (409 loc) • 15.1 kB
JavaScript
/**
* The MIT License (MIT)
*
* Copyright (c) 2018-2024 Toha <tohenk@yahoo.com>
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
* of the Software, and to permit persons to whom the Software is furnished to do
* so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
const EventEmitter = require('events');
const { Op } = require('@sequelize/core');
const AppStorage = require('./storage');
/**
* Queue dispatcher.
*/
class AppDispatcher extends EventEmitter {
constructor() {
super();
this.count = 0;
this.queues = [];
this.inqueues = [];
this.loading = false;
this.loadTime = Date.now();
this.reloadInterval = 300000; // 5 minutes
}
reload() {
this.count++;
this.check();
return this;
}
load() {
if (this.count > 0 && !this.loading) {
this.loading = true;
this.count = 0;
this.queues = [];
this.getQueues(results => {
this.loading = false;
this.loadTime = Date.now();
this.queues = results;
this.check();
});
}
}
getQueues(done) {
}
inQueue(item) {
const result = this.inqueues.indexOf(item) >= 0 ? true : false;
if (!result) {
this.inqueues.push(item);
}
return result;
}
endQueue(item) {
const index = this.inqueues.indexOf(item);
if (index >= 0) {
this.inqueues.splice(index, 1);
}
return this;
}
check() {
}
reloadIfNeeded() {
if (this.count > 0 || (this.count === 0 && this.queues.length === 0)) {
if (this.count === 0 && ((Date.now() - this.loadTime) >= this.reloadInterval) && !this.loading) {
this.count++;
}
this.load();
}
}
}
/**
* Terminal Dispatcher.
*/
class AppTerminalDispatcher extends AppDispatcher {
constructor(term) {
super();
this.maxRetry = 3;
this.term = term;
this.term.on('idle', () => {
this.reloadIfNeeded();
if (this.queues.length && !this.term.busy) {
const queue = this.queues.shift();
if (!this.inQueue(queue.id)) {
console.log('Processing queue: %s <= %s (%d)', queue.imsi, queue.hash, queue.id);
this.emit('pre-queue', queue);
this.process(queue);
}
}
this.check();
});
}
getQueues(done) {
AppStorage.GwQueue.findAll({
where: {
imsi: this.term.name,
[Op.or]: [
{
[Op.and]: [
{processed: false},
{type: {[Op.in]: [AppStorage.ACTIVITY_CALL, AppStorage.ACTIVITY_SMS, AppStorage.ACTIVITY_USSD]}}
]
},
{
[Op.and]: [
{processed: true},
{retry: {[Op.lt]: this.maxRetry}},
{type: AppStorage.ACTIVITY_SMS},
{status: 0}
]
}
]
},
order: [
['priority', 'ASC'],
['processed', 'ASC'],
['time', 'ASC']
]
})
.then(results => {
done(results);
})
;
}
check() {
this.term.con.emit('state');
return this;
}
update(GwQueue, success) {
const updates = {processed: true};
if (success) {
updates.status = this.term.reply.success ? 1 : 0;
}
if (!success && GwQueue.type === AppStorage.ACTIVITY_SMS) {
updates.retry = GwQueue.retry ? GwQueue.retry + 1 : 1;
}
GwQueue.update(updates)
.then(result => {
if (GwQueue.type !== AppStorage.ACTIVITY_USSD) {
AppStorage.saveLog(GwQueue.imsi, result, GwLog => this.endQueue(GwQueue.id));
} else {
this.endQueue(GwQueue.id);
}
})
.catch(err => {
console.error(err);
this.endQueue(GwQueue.id);
})
;
}
process(GwQueue) {
const f = action => {
if (action) {
action
.then(result => {
this.update(GwQueue, result.success);
})
.catch(() => {
this.update(GwQueue, false);
})
.finally(() => {
console.log('Queue done: %s <= %s (%d)', GwQueue.imsi, GwQueue.hash, GwQueue.id);
this.emit('post-queue', GwQueue);
});
;
}
}
switch (GwQueue.type) {
case AppStorage.ACTIVITY_CALL:
f(this.term.dial(GwQueue));
break;
case AppStorage.ACTIVITY_SMS:
// if it is a message retry then ensure the status is really failed
if (GwQueue.retry !== null) {
this.term.query('status', GwQueue.hash)
.then(status => {
if (status.success && status.hash === GwQueue.hash) {
if (status.status) {
// it was success, update status
GwQueue.update({status: 1});
} else {
// retry message
f(this.term.sendMessage(GwQueue));
}
} else {
// message not processed yet, okay to send
f(this.term.sendMessage(GwQueue));
}
})
;
} else {
f(this.term.sendMessage(GwQueue));
}
break;
case AppStorage.ACTIVITY_USSD:
f(this.term.ussd(GwQueue));
break;
}
}
}
/**
* Activity dispatcher.
*/
class AppActivityDispatcher extends AppDispatcher {
constructor(appterm) {
super();
this.appterm = appterm;
this.processing = false;
}
getQueues(done) {
AppStorage.GwQueue.findAll({
where: {
processed: false,
type: {[Op.in]: [AppStorage.ACTIVITY_RING, AppStorage.ACTIVITY_INBOX, AppStorage.ACTIVITY_CUSD]}
},
order: [
['priority', 'ASC'],
['time', 'ASC']
]
})
.then(results => {
done(results);
})
;
}
check() {
if (this.appterm.terminals.length) {
if (this.appterm.gwclients.length === 0 && this.appterm.plugins.length === 0) {
console.log('Activity processing skipped, no consumer registered.');
} else {
this.reloadIfNeeded();
this.process();
}
}
return this;
}
add(data, group, cb) {
const term = this.selectTerminal(data.type, data.address, group);
if (!term) {
console.log('No terminal available for activity %s => %s (%s)', data.type,
data.address, group ? group : '-');
} else {
term.addQueue(data, cb);
}
}
selectTerminal(type, address, group) {
const terminals = this.getTerminal(type, address, group);
if (terminals.length) {
let index = 0;
if (terminals.length > 1) {
terminals.sort((a, b) => a.options.priority - b.options.priority);
index = Math.floor(Math.random() * terminals.length);
}
return terminals[index];
}
}
getTerminal(type, address, group) {
const result = [];
const priorities = [];
for (let i = 0; i < this.appterm.terminals.length; i++) {
const term = this.appterm.terminals[i];
if (!term.connected) {
continue;
}
if (group && !term.options.groups.includes(group)) {
continue;
}
if (type === AppStorage.ACTIVITY_CALL && !term.options.allowCall) {
continue;
}
if (type === AppStorage.ACTIVITY_SMS && !term.options.sendMessage) {
continue;
}
if (term.options.operators.length && type !== AppStorage.ACTIVITY_USSD) {
const op = this.appterm.getOperator(address);
if (!op) {
continue;
}
if (term.options.operators.indexOf(op) < 0) {
continue;
}
// give an assigned operator as priority
priorities.push(term);
}
result.push(term);
}
if (result.length > 1 && priorities.length) {
Array.prototype.push.apply(result, priorities);
}
return result;
}
process() {
if (this.queues.length && !this.processing) {
this.processing = true;
process.nextTick(() => {
if (this.queues.length) {
const queue = this.queues.shift();
this.emit('queue', queue);
}
});
this.once('queue', queue => {
this.processQueue(queue, () => {
this.processing = false;
this.emit('queue-processed', queue);
this.check();
});
});
}
if (this.queues.length === 0) {
if (!this.timeout) {
this.timeout = setTimeout(() => {
this.timeout = null;
this.check();
}, this.reloadInterval);
}
}
}
processQueue(GwQueue, done) {
const term = this.appterm.get(GwQueue.imsi);
if (term) {
let processed = true;
if (GwQueue.type === AppStorage.ACTIVITY_RING || GwQueue.type === AppStorage.ACTIVITY_INBOX) {
processed = this.addressAllowed(GwQueue.address) ? true : false;
}
// skip message based its terminal setting
if (processed && !term.options.receiveMessage && GwQueue.type === AppStorage.ACTIVITY_INBOX) {
processed = false;
}
if (processed) {
if (this.appterm.gwclients.length) {
this.appterm.gwclients.forEach(socket => {
if (term.options.groups.includes(socket.group) || (term.options.groups.length === 0 && !socket.group)) {
console.log('Sending activity notification %d-%s to %s', GwQueue.type,
GwQueue.hash, socket.id);
switch (GwQueue.type) {
case AppStorage.ACTIVITY_RING:
socket.emit('ring', GwQueue.hash, GwQueue.address, GwQueue.time);
break;
case AppStorage.ACTIVITY_INBOX:
socket.emit('message', GwQueue.hash, GwQueue.address, GwQueue.data, GwQueue.time);
break;
case AppStorage.ACTIVITY_CUSD:
socket.emit('ussd', GwQueue.hash, GwQueue.address, GwQueue.data, GwQueue.time);
break;
}
} else {
console.log('Skipping activity notification %d-%s for %s', GwQueue.type,
GwQueue.hash, socket.id);
}
});
}
this.appterm.plugins.forEach(plugin => {
if (plugin.group === undefined || term.options.groups.includes(plugin.group)) {
plugin.handle(GwQueue);
if (GwQueue.veto) {
return true;
}
}
});
}
GwQueue.update({processed: true, status: processed ? 1 : 0})
.then(() => done())
.catch(err => {
console.error(err);
done();
})
;
} else {
done();
}
}
addressAllowed(address) {
if (address) {
const blacklists = this.appterm.config.blacklists || [];
const premiumlen = this.appterm.config.premiumlen || 5;
if (isNaN(address)) {
console.log('Number %s is unreachable', address);
return false;
}
if (address.length <= premiumlen) {
console.log('Number %s is premium', address);
return false;
}
if (blacklists.indexOf(address) >= 0) {
console.log('Number %s is blacklisted', address);
return false;
}
return true;
}
}
}
module.exports = {
AppDispatcher,
AppTerminalDispatcher,
AppActivityDispatcher,
}