From 92c29ed50ae292e1b3e0c5a83bfd4ed36838e970 Mon Sep 17 00:00:00 2001 From: Matthew R Date: Tue, 1 Dec 2020 23:51:31 -0500 Subject: [PATCH] add intercom capabilities and tidy up PBX stuff --- src/class/Handler.ts | 28 +++++++++++ src/class/PBX.ts | 97 +++++++++++------------------------- src/class/index.ts | 1 + src/commands/index.ts | 1 + src/commands/intercom.ts | 51 +++++++++++++++++++ src/pbx/actions/Misc.ts | 26 ++++++++++ src/pbx/handlers/CRZero.ts | 18 +++++++ src/pbx/handlers/PageDTMF.ts | 77 ++++++++++++++++++++++++++++ src/pbx/index.ts | 6 +++ 9 files changed, 238 insertions(+), 67 deletions(-) create mode 100644 src/class/Handler.ts create mode 100644 src/commands/intercom.ts create mode 100644 src/pbx/actions/Misc.ts create mode 100644 src/pbx/handlers/CRZero.ts create mode 100644 src/pbx/handlers/PageDTMF.ts create mode 100644 src/pbx/index.ts diff --git a/src/class/Handler.ts b/src/class/Handler.ts new file mode 100644 index 0000000..9c406f7 --- /dev/null +++ b/src/class/Handler.ts @@ -0,0 +1,28 @@ +import ARI from 'ari-client'; +import { PBX } from '.'; + +export default class Handler { + public pbx: PBX; + + public app: string; + + public options: { + available?: boolean, + } + + constructor(pbx: PBX) { + this.pbx = pbx; + this.options = {}; + } + + get client() { return this.pbx.client; } + + public run(event: ARI.Event, channel: ARI.Channel): Promise { return Promise.resolve(); } + + public async unavailable(event: ARI.Event, channel: ARI.Channel) { + const playback = await channel.play({ + media: 'sound:all-outgoing-lines-unavailable', + }, undefined); + playback.once('PlaybackFinished', () => channel.hangup()); + } +} diff --git a/src/class/PBX.ts b/src/class/PBX.ts index 64e700b..03ca259 100644 --- a/src/class/PBX.ts +++ b/src/class/PBX.ts @@ -1,79 +1,42 @@ -/* eslint-disable consistent-return */ -import { TextableChannel } from 'eris'; -import { Client } from '.'; -import PageCommand from '../commands/page'; +/* eslint-disable no-continue */ +import { Client, Collection, Handler } from '.'; export default class PBX { public client: Client; + public handlers: Collection; + constructor(client: Client) { this.client = client; - this.pageDTMF(); + this.handlers = new Collection(); + + this.start(); } - public pageDTMF() { - this.client.util.ari.on('StasisStart', async (event, channel) => { - if (event.application !== 'page-dtmf') return; - const message = await ( this.client.guilds.get(this.client.config.guildID).channels.get('501089664040697858')).getMessage('775604192013320203'); - if (!message) return channel.hangup(); - const member = await this.client.db.Staff.findOne({ extension: channel.caller.number }).lean().exec(); - if (!member) return channel.hangup(); - const pager = await this.client.db.PagerNumber.findOne({ individualAssignID: member.userID }).lean().exec(); - if (!pager) return channel.hangup(); - let status = 0; - const pagerNumber: string[] = []; - const pagerCode: string[] = []; - channel.answer(); - const pnPlayback = await channel.play({ - media: 'sound:please-enter-the-pn', - }, undefined); - channel.on('ChannelDtmfReceived', async (ev) => { - if (status === 0) { - if (ev.digit === '#') { - pnPlayback.stop(); - await channel.play({ - media: 'sound:please-enter-the-pc', - }, undefined); - status = 1; - return null; + public start() { + this.client.util.ari.on('ChannelHangupRequest', (_, channel) => channel.hangup()); + const handlers = Object.values(require(`${__dirname}/../pbx`)); + for (const HandlerFile of handlers) { + const handler = new HandlerFile(this); + if (!handler.app) continue; + if (!handler.options?.available) { + this.client.util.ari.on('StasisStart', async (event, channel) => { + if (event.application !== handler.app) return; + await handler.unavailable(event, channel); + }); + } else { + this.client.util.ari.on('StasisStart', async (event, channel) => { + if (event.application !== handler.app) return; + try { + await handler.run(event, channel); + } catch (err) { + this.client.util.handleError(err); } - pagerNumber.push(ev.digit); - } else if (status === 1) { - const processingPlayback = this.client.util.ari.Playback(); - if (ev.digit === '#') { - await channel.play({ - media: 'sound:pls-hold-process-tx', - }, processingPlayback); - - const Page = this.client.commands.get('page'); - const page = await Page.page(pagerNumber.join(''), pager.num, pagerCode.join(''), message); - if (page.status === true) { - processingPlayback.stop(); - const playback = await channel.play({ - media: 'sound:page-delivered', - }, undefined); - playback.on('PlaybackFinished', () => channel.hangup()); - } else if (page.status === false) { - try { - const ch = await this.client.getDMChannel(member.userID); - if (ch) { - ch.createMessage(`***An error has occurred while trying to process your page request over the PBX.***\n*${page.message}*\n\nPlease check your parameters and try again.`); - } - } catch { - this.client.util.handleError(new Error(page.message)); - } - processingPlayback.stop(); - const playback = await channel.play({ - media: 'sound:request-error', - }, undefined); - playback.on('PlaybackFinished', () => channel.hangup()); - } - return null; - } - pagerCode.push(ev.digit); - } - }); - }); + }); + } + this.handlers.add(handler.app, handler); + this.client.util.signale.success(`Successfully loaded PBX Handler ${handler.app}`); + } } } diff --git a/src/class/index.ts b/src/class/index.ts index 08737a5..1cada86 100644 --- a/src/class/index.ts +++ b/src/class/index.ts @@ -2,6 +2,7 @@ export { default as Client } from './Client'; export { default as Collection } from './Collection'; export { default as Command } from './Command'; export { default as Event } from './Event'; +export { default as Handler } from './Handler'; export { default as LocalStorage } from './LocalStorage'; export { default as Moderation } from './Moderation'; export { default as PBX } from './PBX'; diff --git a/src/commands/index.ts b/src/commands/index.ts index 5810be1..2f5f41f 100644 --- a/src/commands/index.ts +++ b/src/commands/index.ts @@ -18,6 +18,7 @@ export { default as eval } from './eval'; export { default as game } from './game'; export { default as help } from './help'; export { default as info } from './info'; +export { default as intercom } from './intercom'; export { default as kick } from './kick'; export { default as listredirects } from './listredirects'; export { default as members } from './members'; diff --git a/src/commands/intercom.ts b/src/commands/intercom.ts new file mode 100644 index 0000000..460058d --- /dev/null +++ b/src/commands/intercom.ts @@ -0,0 +1,51 @@ +import { Message } from 'eris'; +import { Client, Command } from '../class'; +import { Misc as MiscPBXActions } from '../pbx'; + +export default class Intercom extends Command { + constructor(client: Client) { + super(client); + this.name = 'intercom'; + this.description = 'Will synthesize inputted text to a recording and dial an intercom to the extension specified, then play the recording.'; + this.usage = `${this.client.config.prefix}intercom `; + this.permissions = 1; + this.guildOnly = true; + this.enabled = true; + } + + public async run(message: Message, args: string[]) { + try { + if (!args[0]) return this.client.commands.get('help').run(message, [this.name]); + const loading = await this.loading(message.channel, 'Synthesizing text...'); + + const recordingLocation = await MiscPBXActions.TTS(this.client.util.pbx, `Hello, this is the Library of Code Private Branch Exchange dialing you at the request of ${message.author.username} to deliver you a message. Playing message: ${args.slice(1).join(' ')}`, 'MALE'); + await loading.edit(`***${this.client.util.emojis.LOADING} Preparing to dial...***`); + const channel = await this.client.util.ari.channels.originate({ + endpoint: `PJSIP/${args[0]}`, + extension: args[0], + callerId: `TTS PAGE FRM ${message.author.username} <00>`, + priority: 1, + app: 'cr-zero', + variables: { + 'PJSIP_HEADER(add,Call-Info)': ';answer-after=0', + 'PJSIP_HEADER(add,Alert-Info)': 'Ring Answer', + }, + }); + await loading.edit(`***${this.client.util.emojis.LOADING} Dialing call...***`); + channel.once('StasisStart', async (_, chan) => { + chan.answer(); + await loading.edit(`***${this.client.util.emojis.LOADING} Answer received, starting playback...***`); + const playback = await chan.play({ + media: ['sound:beep', recordingLocation], + }, undefined); + playback.once('PlaybackFinished', async () => { + chan.hangup(); + await loading.edit(`***${this.client.util.emojis.LOADING} Successfully delivered intercom message to EXT \`${args[0]}\`.***`); + }); + }); + return undefined; + } catch (err) { + return this.client.util.handleError(err, message, this, false); + } + } +} diff --git a/src/pbx/actions/Misc.ts b/src/pbx/actions/Misc.ts new file mode 100644 index 0000000..67f8c1c --- /dev/null +++ b/src/pbx/actions/Misc.ts @@ -0,0 +1,26 @@ +import ARI from 'ari-client'; +import { randomBytes } from 'crypto'; +import { promises as fs } from 'fs'; +import { PBX } from '../../class'; + +export default class Misc { + public static async accessDenied(channel: ARI.Channel) { + const playback = await channel.play({ + media: 'sound:access-denied', + }, undefined); + playback.once('PlaybackFinished', () => channel.hangup()); + } + + public static async TTS(pbx: PBX, text: string, gender: string | any): Promise { + const fileExtension = `${randomBytes(10).toString('hex')}`; + + const [response] = await pbx.client.util.tts.synthesizeSpeech({ + input: { text }, + voice: { languageCode: 'en-US', ssmlGender: gender }, + audioConfig: { audioEncoding: 'OGG_OPUS' }, + }); + await fs.writeFile(`/tmp/${fileExtension}.ogg`, response.audioContent, 'binary'); + await pbx.client.util.exec(`ffmpeg -i /tmp/${fileExtension}.ogg -af "highpass=f=300, lowpass=f=3400" -ar 8000 -ac 1 -ab 64k -f mulaw /tmp/${fileExtension}.ulaw`); + return `sound:${fileExtension}`; + } +} diff --git a/src/pbx/handlers/CRZero.ts b/src/pbx/handlers/CRZero.ts new file mode 100644 index 0000000..5abbe71 --- /dev/null +++ b/src/pbx/handlers/CRZero.ts @@ -0,0 +1,18 @@ +/* eslint-disable consistent-return */ +import ARI from 'ari-client'; +import { Handler, PBX } from '../../class'; + +export default class CRZero extends Handler { + constructor(pbx: PBX) { + super(pbx); + this.app = 'cr-zero'; + this.options = { available: true }; + } + + public async run(event: ARI.Event, channel: ARI.Channel) { + const playback = await channel.play({ + media: 'sound:pbx-transfer', + }, undefined); + playback.once('PlaybackFinished', () => channel.move({ app: 'page-dtmf' })); + } +} diff --git a/src/pbx/handlers/PageDTMF.ts b/src/pbx/handlers/PageDTMF.ts new file mode 100644 index 0000000..31c8aaa --- /dev/null +++ b/src/pbx/handlers/PageDTMF.ts @@ -0,0 +1,77 @@ +/* eslint-disable consistent-return */ +import ARI from 'ari-client'; +import { TextableChannel } from 'eris'; +import PageCommand from '../../commands/page'; +import { Handler, PBX } from '../../class'; +import { Misc } from '..'; + +export default class PageDTMF extends Handler { + constructor(pbx: PBX) { + super(pbx); + this.app = 'page-dtmf'; + this.options.available = true; + } + + public async run(event: ARI.Event, channel: ARI.Channel) { + if (event.application !== 'page-dtmf') return; + const message = await ( this.client.guilds.get(this.client.config.guildID).channels.get('501089664040697858')).getMessage('775604192013320203'); + if (!message) return Misc.accessDenied(channel); + const member = await this.client.db.Staff.findOne({ extension: channel.caller.number }).lean().exec(); + if (!member) return Misc.accessDenied(channel); + const pager = await this.client.db.PagerNumber.findOne({ individualAssignID: member.userID }).lean().exec(); + if (!pager) return Misc.accessDenied(channel); + let status = 0; + const pagerNumber: string[] = []; + const pagerCode: string[] = []; + channel.answer(); + const pnPlayback = await channel.play({ + media: 'sound:please-enter-the-pn', + }, undefined); + channel.on('ChannelDtmfReceived', async (ev) => { + if (status === 0) { + if (ev.digit === '#') { + pnPlayback.stop(); + await channel.play({ + media: 'sound:please-enter-the-pc', + }, undefined); + status = 1; + return null; + } + pagerNumber.push(ev.digit); + } else if (status === 1) { + const processingPlayback = this.client.util.ari.Playback(); + if (ev.digit === '#') { + await channel.play({ + media: 'sound:pls-hold-process-tx', + }, processingPlayback); + + const Page = this.client.commands.get('page'); + const page = await Page.page(pagerNumber.join(''), pager.num, pagerCode.join(''), message); + if (page.status === true) { + processingPlayback.stop(); + const playback = await channel.play({ + media: 'sound:page-delivered', + }, undefined); + playback.on('PlaybackFinished', () => channel.hangup()); + } else if (page.status === false) { + try { + const ch = await this.client.getDMChannel(member.userID); + if (ch) { + ch.createMessage(`***An error has occurred while trying to process your page request over the PBX.***\n*${page.message}*\n\nPlease check your parameters and try again.`); + } + } catch { + this.client.util.handleError(new Error(page.message)); + } + processingPlayback.stop(); + const errorPlayback = await channel.play({ + media: 'sound:request-error', + }, undefined); + errorPlayback.on('PlaybackFinished', () => channel.hangup()); + } + return null; + } + pagerCode.push(ev.digit); + } + }); + } +} diff --git a/src/pbx/index.ts b/src/pbx/index.ts new file mode 100644 index 0000000..bd48785 --- /dev/null +++ b/src/pbx/index.ts @@ -0,0 +1,6 @@ +// Actions +export { default as Misc } from './actions/Misc'; + +// Handlers +export { default as CRZero } from './handlers/CRZero'; +export { default as PageDTMF } from './handlers/PageDTMF';