☎️ Web interface for viewing and processing Asterisk call logs (2020)
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

297 lines
8.8 KiB

2 years ago
'use strict';
const Joi = require('joi');
const Dayjs = require('dayjs');
const Rfr = require('rfr');
Dayjs.extend(require('dayjs/plugin/customParseFormat'));
const Common = Rfr('/shared/common');
const ComSrv = Rfr('/server/comsrv');
const Db = Rfr('/server/db');
const typeTable = {
incoming: Common.ctIncoming,
outgoing: Common.ctOutgoing,
internal: Common.ctInternal
};
const resTable = { // not used now
answered: Common.resAnswered,
unanswered: Common.resNoAnswer,
busy: Common.resBusy,
failed: Common.resFailed,
other: Common.resOther,
cancelled: Common.resCancelled
};
const transformDate = (date, clamp = '') => {
if (date) {
date = Dayjs(date, 'YYYY-MM-DD');
if (!date.isValid()) {
throw new Error('Invalid date specified');
}
if (clamp === 'start') {
date = date.startOf('day');
} else if (clamp === 'end') {
date = date.endOf('day')
}
return date;
}
};
const sortableFields = ['source', 'destination', 'callDate', 'totalDuration', 'callDuration', 'callType', 'callResult'];
const selectableFields = ['id', 'calldate', 'calltype', 'callresult', 'source', 'destination', 'callduration', 'totalduration', 'dailyid', 'dailyct', 'uniqueid', 'comment', 'ivr'];
const getFilter = async filter => {
const schema = Joi.object({
source: Joi.string().pattern(/^\d+$/).max(128),
destination: Joi.link('source'),
any: Joi.link('source'),
callTypes: Joi.number().min(0).max(Common.ctMaxSet),
dateStart: Joi.string().pattern(/^[0-9]{1,}-[0-9]{1,2}-[0-9]{1,2}$/).max(64),
dateEnd: Joi.link('dateStart'),
id: Joi.string().max(100),
wcSource: Joi.boolean().default(false),
wcDestination: Joi.boolean().default(false),
wcAny: Joi.boolean().default(false),
limit: Joi.number().integer().min(1).max(100).default(15),
page: Joi.number().integer().min(1).default(1),
sort: Joi.string().valid(...sortableFields).default('callDate'),
dir: Joi.string().valid('asc', 'desc').default('desc'),
disableGrouping: Joi.number().min(0).max(1).default(0),
})
let res = {
hasUserFilters: (!!filter.dateStart || !!filter.dateEnd || !!filter.callTypes ||
!!filter.id || !!filter.source || !!filter.destination || !!filter.any ||
!!filter.wcSource || !!filter.wcDestination || !!filter.wcAny),
...(await schema.validateAsync(filter))
};
return res;
}
const filterQueryBuilder = (qb, parms) => {
if (parms.source) {
if (parms.wcSource) { qb.where('source', 'like', '%' + parms.source + '%'); }
else { qb.where('source', '=', parms.source); }
}
if (parms.destination) {
if (parms.wcDestination) { qb.where('destination', 'like', '%' + parms.destination + '%'); }
else { qb.where('destination', '=', parms.destination); }
}
if (parms.any) {
if (parms.wcAny) { qb.where(function() { this.where('source', 'like', '%' + parms.any + '%').orWhere('destination', 'like', '%' + parms.any + '%') }); }
else { qb.where(function() { this.where('source', '=', parms.any).orWhere('destination', '=', parms.any) }); }
}
if (parms.dateStart) { qb.where('calldate', '>=', transformDate(parms.dateStart, 'start')); }
if (parms.dateEnd) { qb.where('calldate', '<=', transformDate(parms.dateEnd, 'end')); }
if (parms.id) { qb.where('dailyct', parms.id); }
if (parms.callTypes) {
const a = [];
[Common.ctIncoming, Common.ctOutgoing, Common.ctInternal].forEach( e => {
if (parms.callTypes & (1 << e)) { a.push(e); }
});
if (a.length > 0) { qb.whereIn('calltype', a); }
}
}
const groupQueryBuilder = (qb, parms) => {
// special case: if grouping and sorting is enabled, also sort by date (desc)
if (!parms.disableGrouping && parms.sort !== 'callDate') {
qb.orderBy('calldate', 'desc');
}
};
const getRawRecords = async (parms) => {
const rec = await Db.query('ptable')
.select(...selectableFields)
.modify(filterQueryBuilder, parms)
.offset((parms.page - 1) * parms.limit)
.limit(parms.limit)
.orderBy(parms.sort.toLowerCase(), parms.dir.toLowerCase())
.modify(groupQueryBuilder, parms);
return Common.toArray(rec);
}
const mapRawRecords = rec => {
const f = (r => { return {
callDate: Dayjs(r.calldate),
callType: r.calltype,
callResult: r.callresult,
source: r.source,
destination: r.destination,
callDuration: r.callduration,
totalDuration: r.totalduration,
dailyID: r.dailyid,
dailyCT: r.dailyct,
uniqueID: r.uniqueid,
comment: (r.comment || ''),
ivr: (r.ivr || 0)
}});
return rec.map(f);
};
const countSinceLastUpdate = async lastUpdateAt => {
const res = await Db.query('ptable')
.where('calldate', '>', lastUpdateAt)
.count({num: 'calldate'});
return res[0].num;
};
const getLatest = async () => {
const res = Common.toArray(await Db.query('ptable')
.select('calldate')
.orderBy('calldate', 'desc')
.first()
);
return res[0].calldate;
};
const countRecords = async (parms) => {
const res = await Db.query('ptable')
.modify(filterQueryBuilder, parms)
.count({num: 'uniqueid'}).first();
return res.num;
};
// Check if 1st record extends "0th" record by date
const isFirstCont = async (parms, mapped) => {
const first = mapped[0];
if (first) {
const res = await Db.query('ptable')
.select(...selectableFields)
.modify(filterQueryBuilder, parms)
.modify( (qb, parms) => {
if (parms.sort === 'callDate' && parms.dir === 'asc') {
qb.where('calldate', '<', first.callDate)
.andWhere('calldate', '>=', Dayjs(first.callDate).startOf('day'));
} else {
qb.where('calldate', '>', first.callDate)
.andWhere('calldate', '<=', Dayjs(first.callDate).endOf('day'));
}
if (parms.sort !== 'callDate') {
qb.where(parms.sort.toLowerCase(), '=', first[parms.sort]);
}
}, parms);
if (res.length >= 1) {
return true;
}
}
return false;
}
const getDailyStats = async (parms, mapped) => {
const allDays = mapped.map( r => Dayjs(r.callDate).startOf('day').valueOf() )
.filter( (e, i, a) => a.indexOf(e) === i);
let result = [];
for (const e of allDays) {
const res = await Db.query('ptable')
.sum({ total_duration_sum: 'totalduration', call_duration_sum: 'callduration' })
.count({ total_calls: 'uniqueid' })
.modify(filterQueryBuilder, parms)
.modify( (qb, parms) => {
qb.where('calldate', '>=', Dayjs(e).startOf('day'))
.andWhere('calldate', '<=', Dayjs(e).endOf('day'));
}, parms);
result.push({
day: e,
totalDuration: res[0].total_duration_sum,
callDuration: res[0].call_duration_sum,
totalCalls: res[0].total_calls
});
}
return result;
}
const getCdr = async (filter) => {
const count = await countRecords(filter);
const raw = await getRawRecords(filter);
const mapped = mapRawRecords(raw);
return {
dailyStats: (await getDailyStats(filter, mapped)),
isFirstCont: (!filter.disableGrouping && (await isFirstCont(filter, mapped))),
maxPages: (filter.limit > 0 && count > 0) ? Math.ceil(count / filter.limit) : 1,
thisPage: filter.page,
records: mapped,
filter: filter,
latest: (await getLatest())
}
};
const getCdrById = async (id) => {
const raw = Common.toArray(await Db.query('ptable')
.select(...selectableFields)
.where('uniqueid', id)
.first()
);
const mapped = mapRawRecords(raw);
return {
...mapped[0],
date: Dayjs(mapped[0].callDate).format('YYYY-MM-DD_HH-mm-ss'),
};
}
const queryHandler = async (request, h) => {
return await getCdr(await getFilter(request.query));
};
const checkNewHandler = async(request, h) => {
return {
numSinceLastUpdate: await countSinceLastUpdate(Dayjs(request.query.lastUpdateAt))
};
};
module.exports = {
getCdr: getCdr,
getFilter: getFilter,
getCdrById: getCdrById,
routes: [{
method: 'GET',
path: '/api/query',
handler: queryHandler
},
{
method: 'GET',
path: '/api/check-new',
handler: checkNewHandler,
options: {
validate: {
query: Joi.object({
lastUpdateAt: Joi.date().timestamp('unix').required()
})
}
}
}]
};