From 700a912e28869ab980af9a6805cfca98f72c9e96 Mon Sep 17 00:00:00 2001 From: Matthew R Date: Mon, 30 Nov 2020 19:51:20 -0500 Subject: [PATCH] add pbx calling capabilities to page --- .gitignore | 3 ++ package.json | 3 ++ src/class/LocalStorage.ts | 1 + src/class/Report.ts | 74 +++++++++++++++++++++++++++++++++++++-- src/class/Util.ts | 38 ++++++++++++++++++++ src/commands/page.ts | 61 +++++++++++++++++++++++++++++++- src/models/PagerNumber.ts | 3 ++ 7 files changed, 180 insertions(+), 3 deletions(-) diff --git a/.gitignore b/.gitignore index c2a2fa5..0a8ac78 100644 --- a/.gitignore +++ b/.gitignore @@ -9,6 +9,9 @@ src/config.yaml build/config.yaml .vscode yarn-error.log +google.json +src/google.json +build/google.json # Build/Distribution Files build diff --git a/package.json b/package.json index f75c55b..bd9e7da 100644 --- a/package.json +++ b/package.json @@ -30,6 +30,9 @@ "typescript": "^3.9.2" }, "dependencies": { + "@google-cloud/text-to-speech": "^3.1.2", + "@types/ari-client": "^2.2.2", + "ari-client": "^2.2.0", "awesome-phonenumber": "^2.41.0", "axios": "^0.19.2", "body-parser": "^1.19.0", diff --git a/src/class/LocalStorage.ts b/src/class/LocalStorage.ts index 7203050..67928ff 100644 --- a/src/class/LocalStorage.ts +++ b/src/class/LocalStorage.ts @@ -30,6 +30,7 @@ export default class LocalStorage { const data = gzipSync(JSON.stringify(setup)); writeFileSync(this.storagePath, data); } + return this; } /** diff --git a/src/class/Report.ts b/src/class/Report.ts index 7ca0ebe..e14a7b7 100644 --- a/src/class/Report.ts +++ b/src/class/Report.ts @@ -1,7 +1,77 @@ +import { EventEmitter } from 'events'; import { Client, RichEmbed } from '.'; +import { ScoreHistoricalRaw } from '../models/ScoreHistorical'; export default class Report { - public static async soft(userID: string) { - // + public client: Client; + + constructor(client: Client) { + this.client = client; + } + + public async soft(userID: string) { + const report = await this.client.db.Score.findOne({ userID }); + if (!report) return null; + let totalScore: number; + let activityScore: number; + const moderationScore = Math.round(report.moderation); + let roleScore: number; + let cloudServicesScore: number; + const otherScore = Math.round(report.other); + let miscScore: number; + + if (report.total < 200) totalScore = 0; + else if (report.total > 800) totalScore = 800; + else totalScore = Math.round(report.total); + + if (report.activity < 10) activityScore = 0; + else if (report.activity > Math.floor((Math.log1p(3000 + 300 + 200 + 100) * 12))) activityScore = Math.floor((Math.log1p(3000 + 300 + 200 + 100) * 12)); + else activityScore = Math.round(report.activity); + + if (report.roles <= 0) roleScore = 0; + else if (report.roles > 54) roleScore = 54; + else roleScore = Math.round(report.roles); + + if (report.staff <= 0) miscScore = 0; + else miscScore = Math.round(report.staff); + + if (report.cloudServices === 0) cloudServicesScore = 0; + else if (report.cloudServices > 10) cloudServicesScore = 10; + else cloudServicesScore = Math.round(report.cloudServices); + + const historicalData = await this.client.db.ScoreHistorical.find({ userID: member.userID }).lean().exec(); + const array: ScoreHistoricalRaw[] = []; + for (const data of historicalData) { + let total: number; + let activity: number; + const moderation = Math.round(data.report.moderation); + let role: number; + let cloud: number; + const other = Math.round(data.report.other); + let misc: number; + + if (data.report.total < 200) total = 0; + else if (data.report.total > 800) total = 800; + else total = Math.round(data.report.total); + + if (data.report.activity < 10) activity = 0; + else if (data.report.activity > Math.floor((Math.log1p(3000 + 300 + 200 + 100) * 12))) activity = Math.floor((Math.log1p(3000 + 300 + 200 + 100) * 12)); + else activity = Math.round(data.report.activity); + + if (data.report.roles <= 0) role = 0; + else if (data.report.roles > 54) role = 54; + else role = Math.round(data.report.roles); + + if (data.report.staff <= 0) role = 0; + else misc = Math.round(data.report.staff); + + if (data.report.cloudServices === 0) cloud = 0; + else if (data.report.cloudServices > 10) cloud = 10; + else cloud = Math.round(data.report.cloudServices); + + data.report.total = total; data.report.activity = activity; data.report.moderation = moderation; data.report.roles = role; data.report.cloudServices = cloud; data.report.other = other; data.report.staff = misc; + + array.push(data); + } } } diff --git a/src/class/Util.ts b/src/class/Util.ts index f21e474..1341d69 100644 --- a/src/class/Util.ts +++ b/src/class/Util.ts @@ -1,5 +1,8 @@ /* eslint-disable no-bitwise */ import nodemailer from 'nodemailer'; +import GoogleTTS, { TextToSpeechClient } from '@google-cloud/text-to-speech'; +import childProcess from 'child_process'; +import ARIClient from 'ari-client'; import signale from 'signale'; import { Member, Message, Guild, PrivateChannel, GroupChannel, Role, AnyGuildChannel, WebhookPayload } from 'eris'; import { Client, Command, Moderation, RichEmbed } from '.'; @@ -14,6 +17,10 @@ export default class Util { public transporter: nodemailer.Transporter; + public ari: ARIClient.Client; + + public tts: TextToSpeechClient; + constructor(client: Client) { this.client = client; this.moderation = new Moderation(this.client); @@ -28,6 +35,16 @@ export default class Util { port: 587, auth: { user: 'internal', pass: this.client.config.emailPass }, }); + + this.load(); + } + + private async load() { + this.ari = await ARIClient.connect('http://10.8.0.1:8088/ari', 'cr0', this.client.config.ariClientKey); + this.ari.start('cr-zero'); + + process.env.GOOGLE_APPLICATION_CREDENTIALS = `${__dirname}/../../google.json`; + this.tts = new GoogleTTS.TextToSpeechClient(); } get emojis() { @@ -58,6 +75,27 @@ export default class Util { } */ + public async exec(command: string, options: childProcess.ExecOptions = {}): Promise { + return new Promise((res, rej) => { + let output = ''; + const writeFunction = (data: string|Buffer|Error) => { + output += `${data}`; + }; + const cmd = childProcess.exec(command, options); + cmd.stdout.on('data', writeFunction); + cmd.stderr.on('data', writeFunction); + cmd.on('error', writeFunction); + cmd.once('close', (code, signal) => { + cmd.stdout.off('data', writeFunction); + cmd.stderr.off('data', writeFunction); + cmd.off('error', writeFunction); + setTimeout(() => {}, 1000); + if (code !== 0) rej(new Error(`Command failed: ${command}\n${output}`)); + res(output); + }); + }); + } + /** * Resolves a command * @param query Command input diff --git a/src/commands/page.ts b/src/commands/page.ts index 59a76c2..5fc138f 100644 --- a/src/commands/page.ts +++ b/src/commands/page.ts @@ -1,6 +1,8 @@ /* eslint-disable no-case-declarations */ /* eslint-disable no-continue */ /* eslint-disable no-await-in-loop */ +import { promises as fs } from 'fs'; +import { randomBytes } from 'crypto'; import { Message, TextableChannel } from 'eris'; import { Client, Command, RichEmbed } from '../class'; @@ -67,6 +69,18 @@ export default class Page extends Command { return this.success(message.channel, 'You will now receive notifications by email for pages.'); } break; + case 'phone': + if (args[2] === 'off') { + if (pager.receivePhone === false) return this.error(message.channel, 'You are already set to not receive PBX calls.'); + await pager.updateOne({ $set: { receivePhone: false } }); + return this.success(message.channel, 'You will no longer receive PBX calls for pages.'); + } + if (args[2] === 'on') { + if (pager.receivePhone === true) return this.error(message.channel, 'You are already set to receive PBX calls.'); + await pager.updateOne({ $set: { receivePhone: true } }); + return this.success(message.channel, 'You will now receive PBX calls for pages.'); + } + break; default: this.error(message.channel, 'Invalid response provided.'); break; @@ -88,7 +102,7 @@ export default class Page extends Command { } } - public logPage(sender: { number: string, user?: string }, recipient: { number: string, user?: string }, type: 'discord' | 'email', code: string): void { + public logPage(sender: { number: string, user?: string }, recipient: { number: string, user?: string }, type: 'discord' | 'email' | 'phone', code: string): void { const chan = this.mainGuild.channels.get('722636436716781619'); chan.createMessage(`***[${type.toUpperCase()}] \`${sender.number} (${sender.user ? sender.user : ''})\` sent a page to \`${recipient.number} (${recipient.user ? recipient.user : ''})\` with code \`${code}\`.***`); this.client.util.signale.log(`PAGE (${type.toUpperCase()})| TO: ${recipient.number}, FROM: ${sender.number}, CODE: ${code}`); @@ -170,6 +184,51 @@ export default class Page extends Command { html: `

Page

${options?.emergencyNumber ? `

[SEN#${options.emergencyNumber}]` : ''}Recipient PN: ${recipientNumber}
Sender PN: ${senderNumber} (${sender ? `${sender.username}#${sender.discriminator}` : ''})
Initial Command: https://discordapp.com/channels/${this.mainGuild.id}/${message.channel.id}/${message.id} (<#${message.channel.id}>)

Pager Code: ${code} (${this.local.codeDict.get(code)})${txt ? `
Message: ${txt}` : ''}`, }); } + + for (const id of recipientEntry.discordIDs) { + const pager = await this.client.db.PagerNumber.findOne({ individualAssignID: id }); + if (!pager || !pager.receivePhone) continue; + const member = await this.client.db.Staff.findOne({ userID: pager.individualAssignID }); + if (!member || !member.extension) continue; + + const fileExtension = `${randomBytes(10).toString('hex')}`; + + const [response] = await this.client.util.tts.synthesizeSpeech({ + input: { text: `Hello, this call was automatically dialed by the Library of Code Private Branch Exchange. You have received a page from Pager Number, ${recipientNumber}. The Pager Code is ${code}. Please check your Direct Messages on Discord for further information.` }, + // Select the language and SSML voice gender (optional) + voice: { languageCode: 'en-US', ssmlGender: 'MALE' }, + // select the type of audio encoding + audioConfig: { audioEncoding: 'OGG_OPUS' }, + }); + await fs.writeFile(`/tmp/${fileExtension}.ogg`, response.audioContent, 'binary'); + await this.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`); + + const chan = await this.client.util.ari.channels.originate({ + endpoint: `PJSIP/${member.extension}`, + extension: `${member.extension}`, + callerId: `LOC PBX - PAGE FRM ${senderNumber} <00>`, + context: 'from-internal', + priority: 1, + app: 'cr-zero', + }); + chan.on('StasisStart', async (event, channel) => { + const playback = await channel.play({ + media: `sound:/tmp/${fileExtension}`, + }, undefined); + playback.on('PlaybackFinished', () => { + channel.hangup(); + }); + }); + + const recipient = this.mainGuild.members.get(recipientEntry.individualAssignID); + const sender = this.mainGuild.members.get(senderEntry.individualAssignID); + + if (!recipient || !sender) { + this.logPage({ number: senderNumber, user: 'N/A' }, { number: recipientNumber, user: 'N/A' }, 'phone', code); + } else { + this.logPage({ number: senderNumber, user: `${sender.username}#${sender.discriminator}` }, { number: recipientNumber, user: `${recipient.username}#${recipient.discriminator}` }, 'phone', code); + } + } this.client.db.Stat.updateOne({ name: 'pages' }, { $inc: { value: 1 } }).exec(); return { status: true, message: `Page to \`${recipientNumber}\` sent.` }; } catch (err) { diff --git a/src/models/PagerNumber.ts b/src/models/PagerNumber.ts index f64027a..5e952d5 100644 --- a/src/models/PagerNumber.ts +++ b/src/models/PagerNumber.ts @@ -7,6 +7,7 @@ export interface PagerNumberRaw { emailAddresses: string[], discordIDs: string[], receiveEmail: boolean, + receivePhone: boolean, } export interface PagerNumberInterface extends Document { @@ -16,6 +17,7 @@ export interface PagerNumberInterface extends Document { emailAddresses: string[], discordIDs: string[], receiveEmail: boolean, + receivePhone: boolean, } const PagerNumber: Schema = new Schema({ @@ -24,6 +26,7 @@ const PagerNumber: Schema = new Schema({ emailAddresses: Array, discordIDs: Array, receiveEmail: Boolean, + receivePhone: Boolean, }); export default model('PagerNumber', PagerNumber);