//Load static files const config = require("./config.json"); const funcs = require("./funcs.js"); const colors = require("colors"); const embeds = require("./embeds.json") const axios = require('axios'); const ping = require("ping") var commandsBase = require("./commands.json") const ssh2 = require('ssh2') const sshConn = new ssh2.Client(); // find first file in .ssh local to the script const fs = require('fs'); const path = require('path'); // get the first file in the .ssh directory const keyPath = path.join(__dirname, '.ssh'); const keyFiles = fs.readdirSync(keyPath); const keyFile = keyFiles[0]; // read the key file const privateKey = fs.readFileSync(".ssh/" + keyFile, 'utf8'); // FreePBX GraphQL Client const { FreepbxGqlClient, gql } = require("freepbx-graphql-client"); var pbxClient = new FreepbxGqlClient(config.freepbx.url, { client: { id: config.freepbx.clientid, secret: config.freepbx.secret, } }); // 50 minute interval to refresh the token setInterval(() => { pbxClient = new FreepbxGqlClient(config.freepbx.url, { client: { id: config.freepbx.clientid, secret: config.freepbx.secret, } }); }, 3000000); // Set up mariadb connection const mariadb = require('mariadb'); const pool = mariadb.createPool(config.mariadb); const cdrPool = mariadb.createPool(config.cdrdb); // Some functions for FreePBX const reload = () => { // We're gonna start converting all the old gql commands to using mysql `system fwconsole reload` query return new Promise((resolve, reject) => { sshConn.exec('fwconsole reload', (err, stream) => { if (err) { reject(err); } stream.on('data', (data) => { // is there a way to send this data without resolving the promise? console.log(data.toString()); }); stream.on('exit', (code, signal) => { if (code == 0) { resolve(code); } else { reject("Error reloading FreePBX"); } }) }); }); } const getExtCount = () => { return new Promise((resolve, reject) => { pbxClient.request(funcs.minifyQuery(funcs.generateQuery('list', {}))).then((result) => { resolve(result.fetchAllExtensions.extension.length); }).catch((error) => { reject(error); }); }); } const createExtension = (ext, name, uid) => { return new Promise((resolve, reject) => { pbxClient.request(funcs.minifyQuery(funcs.generateQuery('lookup', { ext: ext }))).then((result) => { // Extension exists res = { "status": "exists", } resolve(res); }).catch((error) => { // Extension does not exist, create it, reload, look it up, and return the result pbxClient.request(funcs.minifyQuery(funcs.generateQuery('add', { ext: ext, name: name, uid: uid }))).then((result) => { reload().then((result) => { pbxClient.request(funcs.minifyQuery(funcs.generateQuery('lookup', { ext: ext }))).then((result) => { res = { "status": "created", "result": result } resolve(res); }).catch((error) => { reject(error); }); }).catch((error) => { reject(error); }); }).catch((error) => { reject(error); }); }); }); } const fixNames = () => { // Gonna leave this here if I ever need it in the future pbxClient.request(funcs.minifyQuery(funcs.generateQuery("list", {}))).then((result) => { let extensions = result.fetchAllExtensions.extension; extensions.forEach((extension) => { pbxClient.request(funcs.minifyQuery(funcs.generateQuery("lookup", { ext: extension.user.extension }))).then((result) => { // Get discord user dcClient.users.fetch(result.fetchVoiceMail.email).then((user) => { // Update extension name updateName(extension.user.extension, user.displayName).then((result) => { if (result.status == "updated") { sendLog(`${colors.green("[INFO]")} Updated extension ${extension.user.extension} name to ${user.displayName}`) } }).catch((error) => { sendLog(`${colors.red("[ERROR]")} ${error}`); }); }).catch((error) => { sendLog(`${colors.red("[ERROR]")} ${error}`); }); }).catch((error) => { sendLog(`${colors.red("[ERROR]")} ${error}`); }); }); }); } // deleteExtension, takes an extension number const deleteExtension = (ext) => { return new Promise(async (resolve, reject) => { var conn = await cdrPool.getConnection(); // delete from cel where cid_num = ext const row = await conn.query(` DELETE FROM cel WHERE cid_num = ${ext} `); conn.end(); pbxClient.request(funcs.minifyQuery(funcs.generateQuery('delete', { ext: ext }))).then((result) => { reload().then((result) => { res = { "status": "deleted", "result": result } resolve(res); }).catch((error) => { reject(error); }); }).catch((error) => { reject(error); }); }); } const updateName = (ext, name) => { return new Promise((resolve, reject) => { pbxClient.request(funcs.minifyQuery(funcs.generateQuery('lookup', { ext: ext }))).then((result) => { pbxClient.request(funcs.minifyQuery(funcs.generateQuery('update_name', { ext: ext, name: name }))).then((result) => { reload().then((result) => { res = { "status": "updated", "result": result } resolve(res); }).catch((error) => { reject(error); }); }).catch((error) => { reject(error); }); }).catch((error) => { reject(error); }); }); } const generateExtensionListEmbed = async () => { return new Promise(async (resolve, reject) => { try { var conn = await cdrPool.getConnection(); const result = await pbxClient.request(funcs.minifyQuery(funcs.generateQuery("list", {}))); let extensions = result.fetchAllExtensions.extension; let extensionList = {}; // Generate a list of all unique extensions to be checked in the database let uniqueExtensions = [...new Set(extensions.map(extension => extension.user.extension))]; // Construct SQL query to check all unique extensions at the same time const row30 = await conn.query(` SELECT cid_num, MAX(eventtime) FROM cel WHERE cid_num IN (${uniqueExtensions.join(",")}) AND eventtime >= DATE_SUB(CURDATE(), INTERVAL 30 DAY) GROUP BY cid_num `); const row90 = await conn.query(` SELECT cid_num, MAX(eventtime) FROM cel WHERE cid_num IN (${uniqueExtensions.join(",")}) AND eventtime >= DATE_SUB(CURDATE(), INTERVAL 90 DAY) GROUP BY cid_num `); // Get fresh/entirely unused extensions const alltime = await conn.query(` SELECT cid_num FROM cel WHERE cid_num IN (${uniqueExtensions.join(",")}) GROUP BY cid_num `); // turn rows into an array of extension numbers let active30 = row30.map(row => row.cid_num); let active90 = row90.map(row => row.cid_num); let used = alltime.map(row => row.cid_num); // Generate inactiveFlag object, if it's a fresh extension set the flag to - let inactiveFlag = {}; uniqueExtensions.forEach((ext) => { if (used.includes(ext)) { if (active30.includes(ext)) { if (active90.includes(ext)) { inactiveFlag[ext] = ""; } else { inactiveFlag[ext] = "**"; } } else { inactiveFlag[ext] = "*"; } } else { inactiveFlag[ext] = "-"; } }); extensions.forEach((extension) => { extensionList[extension.user.extension] = extension.user.name; }); // fullList will contain embeds, each embed will contain one field with as many extensions as it can fit (up to 1024 characters). Once the feild is full, make a new embed in the array without a title, just a description. The firrst embed will have a title let field = ""; let embeds = []; let count = 0; // put for loop in function and await it embeds.push({ "title": "Extension List", "color": 0x00ff00, "description": `${extensions.length} extensions\n\`* = inactive for 30 days\`\n\`** = inactive for 90 days\`\n\`- = never used\``, timestamp: new Date() }) await (async () => { for (let key in extensionList) { field += `\`${key}${inactiveFlag[key]}\`: ${extensionList[key]}\n`; if (field.length >= 1024) { // cut feilds at nearest newline and push to the embed let lastNewline = field.lastIndexOf("\n", 1024); embeds[count].fields = [{ "name": "Extensions", "value": field.slice(0, lastNewline) }]; embeds.push({ "color": 0x00ff00, "feilds": [ { "name": "Extensions (extended)", "value": field, timestamp: new Date() } ] }); // figure out any extensions that got cut off and add them to the next embed field = field.slice(lastNewline); count++; } embeds[count].fields = [{ "name": "Extensions", "value": field }]; } })(); // for (let key in extensionList) { // extensionList1 += `\`${key}${inactiveFlag[key]}\`: ${extensionList[key]}\n`; // } //}); res = embeds; // res = { // "title": "Extension List", // "color": 0x00ff00, // "description": `${extensions.length} extensions\n\`* = inactive for 30 days\`\n\`** = inactive for 90 days\`\n\`- = never used\``, // "fields": [{ // "name": "Extensions", // "value": `${extensionList1}` // }], // "timestamp": new Date() // } conn.end(); resolve(res); } catch (error) { reject(error); } }); }; const lookupExtension = (ident, type) => { // type is either "ext" or "uid" return new Promise((resolve, reject) => { switch (type) { case "ext": pbxClient.request(funcs.minifyQuery(funcs.generateQuery('lookup', { ext: ident }))).then((result) => { res = { "status": "exists", "result": result } resolve(res); }).catch((error) => { res = { "status": "notfound", "result": error } reject(res); }); break; case "uid": // Find the extension based on Discord ID in the voicemail email field pbxClient.request(funcs.minifyQuery(funcs.generateQuery('list', {}))).then(async (result) => { // loop through all extensions, run a lookup on each one, and return the first one that matches var found = false; var ext = ""; var count = 0; result.fetchAllExtensions.extension.forEach(async (ext) => { pbxClient.request(funcs.minifyQuery(funcs.generateQuery('lookup', { ext: ext.user.extension }))).then((result) => { if (result.fetchVoiceMail.email == ident && !found) { found = true; ext = result; clearInterval(x); resolve({ "status": "exists", "result": ext }) } count++; }).catch((error) => { reject(error); }); }); x = setInterval(() => { if (count == result.fetchAllExtensions.extension.length) { clearInterval(x); if (!found) { reject("Not found"); } } }, 100); }).catch((error) => { reject(error); }); break; default: reject("Invalid type"); } }); } const findNextExtension = () => { return new Promise((resolve, reject) => { pbxClient.request(funcs.minifyQuery(funcs.generateQuery('list', {}))).then((result) => { // Find the highest extension var highest = 0; // output looks like {fetchAllExtensions: { extension: [{user:{extension: 100, name: "Test"}}]}} // Look out for gaps in the extension numbers, if there are any, use that one, if not, use the highest + 1 var exts = []; result.fetchAllExtensions.extension.forEach((ext) => { exts.push(Number(ext.user.extension)); }); exts.sort((a, b) => a - b); // Find duplicate extensions and remove all but the first for (var i = 0; i < exts.length; i++) { if (exts[i] == exts[i + 1]) { exts.splice(i, 1); i--; } } // Start should be the lowest extension. If none exists use config value // Await if statement var start = 0; if (exts.length > 0) { start = exts[0]; } else { start = config.freepbx.startExt; exts[0] = start - 1; } for (var i = 0; i < exts.length; i++) { if (exts[i] != i + config.freepbx.startExt) { highest = i + start; break; } } if (highest == 0) { highest = String(Number(exts[exts.length - 1]) + 1); } // Return the next extension res = { "status": "success", "result": String(highest) } resolve(res); }).catch((error) => { reject(error); }); }); } // Load Discord.js const Discord = require("discord.js"); const { REST, Routes } = require('discord.js'); const dcClient = new Discord.Client({ intents: ["Guilds", "GuildMembers"] }); const rest = new REST({ version: '10' }).setToken(config.discord.token); var logChannel; var sendLog; var logMsg = null; // Used to store the log message, so it can be edited instead of sending a new one var curMsg = ""; // Used to calculate the length of the log message, so it can be edited instead of sending a new one dcClient.on('ready', async () => { await dcClient.channels.fetch(config.discord.logId).then(async (channel) => { await channel.send(`\`\`\`ansi\n${curMsg}\`\`\``).then((msg) => { logMsg = msg; }); sendLog = async (message) => { let timestamp = new Date() message = `[${timestamp.toLocaleString()}] ${message}`; if (curMsg.length + message.length <= 2000) { curMsg = `${curMsg}\n${message}`; await logMsg.edit(`\`\`\`ansi\n${curMsg}\`\`\``); } else { curMsg = message; await channel.send(`\`\`\`ansi\n${message}\`\`\``).then((msg) => { logMsg = msg; }); } console.log(message); }; sendLog(`${colors.cyan("[INFO]")} Logged in as ${dcClient.user.displayName}!`); const pageGroups = require('./pageGroups.json'); const pageCommand = { "name": "paging", "description": "Add/Remove yourself from paging groups", "type": 1, "options": [ { "name": "method", "description": "The method to use", "type": 3, "required": true, "choices": [ { "name": "add", "value": "add" }, { "name": "remove", "value": "remove" } ] }, { "name": "group", "description": "The group to add/remove yourself from", "type": 3, "required": true, "choices": pageGroups } ] }; // make a non reference copy of the commands object var commands = JSON.parse(JSON.stringify(commandsBase)); commands.push(pageCommand); (async () => { try { sendLog(`${colors.cyan("[INFO]")} Started refreshing application (/) commands.`); await rest.put( Routes.applicationGuildCommands(dcClient.user.id, config.discord.guildId), { body: commands } ); sendLog(`${colors.cyan("[INFO]")} Successfully reloaded application (/) commands.`); } catch (error) { console.error(`${colors.red("[ERROR]")} ${error}`); } })(); // Presence Stuff getExtCount().then((result) => { dcClient.user.setPresence({ activities: [{ name: `${result} extensions`, type: "WATCHING" }], status: "online" }); }).catch((error) => { sendLog(`${colors.red("[ERROR]")} ${error}`); }); // Run every 5 minutes setInterval(() => { getExtCount().then((result) => { dcClient.user.setPresence({ activities: [{ name: `${result} extensions`, type: "WATCHING" }], status: "online" }); }).catch((error) => { sendLog(`${colors.red("[ERROR]")} ${error}`); }); }, 300000); // Lookup all extensions and check if they're still in the server // If they're not, delete them // Run once on startup pbxClient.request(funcs.minifyQuery(funcs.generateQuery("list", {}))).then((result) => { let extensions = result.fetchAllExtensions.extension; extensions.forEach((extension) => { lookupExtension(extension.user.extension, "ext").then((result) => { if (result.result.fetchVoiceMail.email == null) { // Extension is not part of the bot, do nothing return; }; // Fetch Discord user using ID stored in result.result.fetchVoiceMail.email, and see if they're in the server dcClient.guilds.cache.get(config.discord.guildId).members.fetch(result.result.fetchVoiceMail.email).then((member) => { // They're in the server, do nothing }).catch((error) => { // They're not in the server, delete the extension sendLog(`${colors.cyan("[INFO]")} ${extension.user.extension} is not in the server, deleting it`); deleteExtension(extension.user.extension).then((result) => { sendLog(`${colors.cyan("[INFO]")} Deleted extension ${extension.user.extension} because the user is no longer in the server`); }).catch((error) => { sendLog(`${colors.red("[ERROR]")} ${error}`); }); }); }); }); }) // Run every 5 minutes const extListChannel = dcClient.channels.cache.get(config.discord.extList); // Find the latest message from the bot in extListChannel, if there isn't one, send one. There can be other messages in the channel // Sends the same message as the list command setInterval(async () => { await extListChannel.messages.fetch({ limit: 1 }).then((messages) => { if (messages.size == 0) { pbxClient.request(funcs.minifyQuery(funcs.generateQuery("list", {}))).then((result) => { let extensions = result.fetchAllExtensions.extension; // key:value pairs of extension:username let extensionList = {}; extensions.forEach((extension) => { extensionList[extension.user.extension] = extension.user.name; }); extensionList1 = ""; for (let key in extensionList) { extensionList1 += `\`${key}\`: ${extensionList[key]}\n`; } generateExtensionListEmbed().then(embed => { extListChannel.send({ content: "", embeds: embed }); }) }) } else { pbxClient.request(funcs.minifyQuery(funcs.generateQuery("list", {}))).then((result) => { let extensions = result.fetchAllExtensions.extension; // key:value pairs of extension:username let extensionList = {}; extensions.forEach((extension) => { extensionList[extension.user.extension] = extension.user.name; }); extensionList1 = ""; for (let key in extensionList) { extensionList1 += `\`${key}\`: ${extensionList[key]}\n`; } generateExtensionListEmbed().then(embed => { messages.first().edit({ content: "", embeds: embed }); }); }) } }) }, 300000); // Also run on startup extListChannel.messages.fetch({ limit: 1 }).then((messages) => { if (messages.size == 0) { pbxClient.request(funcs.minifyQuery(funcs.generateQuery("list", {}))).then((result) => { let extensions = result.fetchAllExtensions.extension; // key:value pairs of extension:username let extensionList = {}; extensions.forEach((extension) => { extensionList[extension.user.extension] = extension.user.name; }); extensionList1 = ""; for (let key in extensionList) { extensionList1 += `\`${key}\`: ${extensionList[key]}\n`; } generateExtensionListEmbed().then(embed => { extListChannel.send({ content: "", embeds: embed }); }); }) } else { pbxClient.request(funcs.minifyQuery(funcs.generateQuery("list", {}))).then((result) => { let extensions = result.fetchAllExtensions.extension; // key:value pairs of extension:username let extensionList = {}; extensions.forEach((extension) => { extensionList[extension.user.extension] = extension.user.name; }); extensionList1 = ""; for (let key in extensionList) { extensionList1 += `\`${key}\`: ${extensionList[key]}\n`; } generateExtensionListEmbed().then(embed => { messages.first().edit({ content: "", embeds: embed }); }); }) } }) }); // Uptime Kuma Ping // Calculate ping to Discord API // Every X seconds (defined in config.status.interval), send a ping to Uptime Kuma, send push request to config.status.url setInterval(() => { // Send a ping to Uptime Kuma // Send a push request to config.status.url // Define URL arguments ?status=up&msg=OK&ping= // Calculate ping to Discord API const start = Date.now(); axios.get("https://discord.com/api/gateway").then((result) => { const latency = Date.now() - start; axios.get(config.status.url + `?status=up&msg=OK&ping=${latency}`).then((result) => { //sendLog(`${colors.cyan("[INFO]")} Sent ping to Uptime Kuma`); }).catch((error) => { sendLog(`${colors.red("[ERROR]")} Error sending ping ${error}`); }); }) }, config.status.interval * 1000); const start = Date.now(); axios.get("https://discord.com/api/gateway").then((result) => { const latency = Date.now() - start; axios.get(config.status.url + `?status=up&msg=OK&ping=${latency}`).then((result) => { //sendLog(`${colors.cyan("[INFO]")} Sent ping to Uptime Kuma`); }).catch((error) => { sendLog(`${colors.red("[ERROR]")} Error sending ping ${error}`); }); }) // Start doing SSH stuff sendLog(`${colors.cyan("[INFO]")} Starting SSH connection`); await sshConn.connect({ host: config.freepbx.server, username: "root", // Will make config later privateKey: privateKey }) }); sshConn.on('ready', () => { sendLog(`${colors.cyan("[INFO]")} SSH connection established`); console.log("Reloading PBX") reload().then((result) => { console.log("Reloaded PBX") }).catch((error) => { console.log("Error reloading PBX") console.log(error) }); }); dcClient.on("guildMemberRemove", (member) => { // Delete the extension if the user leaves the server sendLog(`${colors.cyan("[INFO]")} User ${member.id} left the server`) lookupExtension(member.id, "uid").then((result) => { if (result.status == "exists") { sendLog(`${colors.cyan("[INFO]")} User ${member.id} has extension ${result.result.fetchExtension.user.extension}, deleting it`) deleteExtension(result.result.fetchExtension.user.extension).then((delResult) => { sendLog(`${colors.cyan("[INFO]")} Deleted extension ${result.result.fetchExtension.user.extension} because the user left the server`); }).catch((error) => { sendLog(`${colors.red("[ERROR]")} ${error}`); }); } }).catch((error) => { sendLog(`${colors.red("[ERROR]")} ${error}`); }); }); dcClient.on('interactionCreate', async interaction => { if (interaction.isCommand()) { const { commandName } = interaction; switch (commandName) { case "new": await interaction.deferReply({ ephemeral: true }); lookupExtension(interaction.user.id, "uid").then((result) => { if (result.status == "exists") { // The user already has an extension, return an ephemeral message saying so interaction.editReply({ content: "You already have an extension!", ephemeral: true }); } }).catch((error) => { // The user doesn't have an extension, create one findNextExtension().then((result) => { if (result.status == "success") { let uid = interaction.user.id; let ext = result.result; let name = interaction.user.displayName; interaction.editReply(`Creating extension ${ext}...`) // Create the extension createExtension(ext, name, uid).then((result) => { if (result.status == "created") { interaction.editReply({ content: "", embeds: [{ "title": "Extension Created!", "color": 0x00ff00, "description": `The SIP server is \`${config.freepbx.server}\``, "fields": [{ "name": "Extension/Username", "value": ext }, { "name": "Password", "value": `||${result.result.fetchExtension.user.extPassword}||` } ] }] }) sendLog(`${colors.cyan("[INFO]")} Created extension ${ext} for user ${uid}`); // Add the role to the user on Discord based on the ID in the config file let role = interaction.guild.roles.cache.find(role => role.id === config.discord.roleId); interaction.member.roles.add(role); } }).catch((error) => { interaction.editReply(`Error creating extension: ${error}`); }); } }).catch((error) => { interaction.editReply(`Error finding next available extension: ${error}`); }); }); break; case "whoami": await interaction.deferReply({ ephemeral: true }); lookupExtension(interaction.user.id, "uid").then((result) => { if (result.status == "exists") { // The user already has an extension, return an ephemeral message saying so interaction.editReply({ content: "", embeds: [{ "title": "Extension Info", "color": 0x00ff00, "description": `The SIP server is \`${config.freepbx.server}\``, "fields": [{ "name": "Extension/Username", "value": result.result.fetchExtension.user.extension }, { "name": "Password", "value": `||${result.result.fetchExtension.user.extPassword}||` } ] }], ephemeral: true }) } }).catch((error) => { // The user doesn't have an extension, create one sendLog(`${colors.red("[ERROR]")} ${error}`) interaction.editReply({ content: "You don't have an extension!", ephemeral: true }); }); break; case "list": await interaction.deferReply({ ephemeral: false }); generateExtensionListEmbed().then((result) => { interaction.editReply({ content: "", embeds: result }); }).catch((error) => { interaction.editReply(`Error generating extension list: ${error}`); }); break; case "delete": if (interaction.options.get("confirm").value == false) { interaction.reply({ content: "Please confirm you want to delete your extension by running `/delete confirm:true`", ephemeral: true }) break; } await interaction.deferReply({ ephemeral: true }); lookupExtension(interaction.user.id, "uid").then((result) => { if (result.status == "exists") { // The user has an extension, delete it deleteExtension(result.result.fetchExtension.user.extension).then((result) => { if (result.status == "deleted") { interaction.editReply({ content: "Extension Deleted!", ephemeral: true }); sendLog(`${colors.green("[INFO]")} ${interaction.user.displayName} (${interaction.user.id}) deleted extension ${result.result.fetchExtension.user.extension}`) // Remove the role from the user on Discord based on the ID in the config file let role = interaction.guild.roles.cache.find(role => role.id === config.discord.roleId); interaction.member.roles.remove(role); } }).catch((error) => { interaction.reply(`Error deleting extension: ${error}`); }); } }).catch((error) => { // The user doesn't have an extension, return an ephemeral message saying so interaction.editReply({ content: "You don't have an extension!", ephemeral: true }); }); break; case "button": interaction.channel.send({ embeds: embeds.controls, components: [{ type: 1, components: [{ type: 2, label: "Get an Extension", emoji: { name: "✅" }, style: 3, custom_id: "new" }, { type: 2, label: "Get your extension info", emoji: { name: "ℹ️" }, style: 1, custom_id: "whoami" }, { type: 2, label: "Delete your extension", emoji: { name: "❌" }, style: 4, custom_id: "delete" }, ] }] }).then(() => { interaction.reply({ content: "Button sent!", ephemeral: true }) }); break; case "name": // Update the users extension name, name is optional and defaults to the users Discord displayName // sanity check the name, remove any quotes, escape any escape characters let name; if (!interaction.options.get("name")) { name = interaction.user.displayName; } else { name = interaction.options.get("name").value; } name = name.replace(/"/g, ""); name = name.replace(/\\/g, "\\\\"); // Fuck you cayden await interaction.deferReply({ ephemeral: true }); lookupExtension(interaction.user.id, "uid").then((result) => { if (result.status == "exists") { // The user has an extension, update the name updateName(result.result.fetchExtension.user.extension, name).then((result2) => { if (result2.status == "updated") { interaction.editReply({ content: "Extension Name Updated!", ephemeral: true }); sendLog(`${colors.green("[INFO]")} ${interaction.user.displayName} (${interaction.user.id}) updated extension ${result.result.fetchExtension.user.extension} name to ${name}`) } }).catch((error) => { interaction.editReply(`Error updating extension name: ${error}`); }); } }).catch((error) => { // The user doesn't have an extension, return an ephemeral message saying so interaction.editReply({ content: "You don't have an extension!", ephemeral: true }); }); break; case "paging": // Add/Remove yourself from paging groups var conn = await pool.getConnection(); await interaction.deferReply({ ephemeral: true }); // Get the users extension, if they don't have one, return an ephemeral message saying so lookupExtension(interaction.user.id, "uid").then((result) => { if (result.status == "exists") { // The user has an extension, add/remove them from the paging group let ext = result.result.fetchExtension.user.extension; let group = interaction.options.get("group").value; let method = interaction.options.get("method").value; switch (method) { case "add": // Check the db if they're already in the group conn.query(`SELECT * FROM paging_groups WHERE ext = ${ext} AND \`page_number\` = ${group}`).then((result) => { if (result.length == 0) { // They're not in the group, add them conn.query(`INSERT INTO paging_groups (\`ext\`, \`page_number\`) VALUES (${ext}, ${group})`).then((result) => { reload().then(() => { interaction.editReply({ content: "Added you to the paging group!", ephemeral: true }); sendLog(`${colors.green("[INFO]")} ${interaction.user.displayName} (${interaction.user.id}) added themselves to paging group ${group}`) }); }).catch((error) => { interaction.editReply(`Error adding you to the paging group: ${error}`); sendLog(`${colors.red("[ERROR]")} ${error}`); }); } else { // They're already in the group, return an ephemeral message saying so interaction.editReply({ content: "You're already in that paging group!", ephemeral: true }); } }).catch((error) => { interaction.editReply(`Error adding you to the paging group: ${error}`); sendLog(`${colors.red("[ERROR]")} ${error}`); }); break; case "remove": // Check if they're in the group conn.query(`SELECT * FROM paging_groups WHERE ext = ${ext} AND \`page_number\` = ${group}`).then((result) => { if (result.length == 0) { // They're not in the group, return an ephemeral message saying so interaction.editReply({ content: "You're not in that paging group!", ephemeral: true }); } else { // They're in the group, remove them conn.query(`DELETE FROM paging_groups WHERE ext = ${ext} AND \`page_number\` = ${group}`).then((result) => { reload().then(() => { interaction.editReply({ content: "Removed you from the paging group!", ephemeral: true }); sendLog(`${colors.green("[INFO]")} ${interaction.user.displayName} (${interaction.user.id}) removed themselves from paging group ${group}`) }); }).catch((error) => { interaction.editReply(`Error removing you from the paging group: ${error}`); sendLog(`${colors.red("[ERROR]")} ${error}`); }); } }).catch((error) => { interaction.editReply(`Error removing you from the paging group: ${error}`); sendLog(`${colors.red("[ERROR]")} ${error}`); }); break; } } }).catch((error) => { // The user doesn't have an extension, return an ephemeral message saying so, and how to get one (/new) interaction.editReply({ content: "You don't have an extension! Run `/new` to get one!", ephemeral: true }); }) conn.end(); break; case "admin": // Admin commands // switch subcommand switch (interaction.options.getSubcommand()) { case "silence": // SSH run `asterisk -x "channel request hangup all" sshConn.exec("asterisk -x 'channel request hangup all'", (err, stream) => { if (err) { interaction.reply({ content: `Error killing calls: ${err}`, ephemeral: true }); sendLog(`${colors.red("[ERROR]")} ${err}`); } stream.on("exit", (code) => { interaction.reply({ content: "Killed all calls!", ephemeral: true }); sendLog(`${colors.green("[INFO]")} Silenced all channels`); }) }); break; case "reload": // Reload asterisk and freepbx await interaction.deferReply({ ephemeral: true }); // We got two commands to run to be safe sshConn.exec("fwconsole reload", (err, stream) => { if (err) { interaction.editReply(`Error reloading FreePBX: ${err}`); sendLog(`${colors.red("[ERROR]")} ${err}`); } stream.on('exit', (code, signal) => { sshConn.exec("asterisk -x 'core reload'", (err, stream) => { if (err) { interaction.editReply(`Error reloading Asterisk: ${err}`); sendLog(`${colors.red("[ERROR]")} ${err}`); } stream.on('exit', (code, signal) => { interaction.editReply("Reloaded FreePBX and Asterisk!"); sendLog(`${colors.green("[INFO]")} Reloaded FreePBX and Asterisk`); }); }); }); }); break; case "reboot": // Reboot the whole server (last resort, after this happens kill all connections to the server, then set a 1m timer to kill the bot) await interaction.deferReply({ ephemeral: true }); sshConn.exec("reboot", (err, stream) => { if (err) { interaction.editReply(`Error rebooting server: ${err}`); sendLog(`${colors.red("[ERROR]")} ${err}`); } stream.on('exit', (code, signal) => { interaction.editReply("Rebooting server...\nThe bot will now disconnect and restart in 1 minute. Please stand by...").then(() => { sendLog(`${colors.green("[INFO]")} Rebooting server`); dcClient.destroy().then(() => { console.log("Disconnected from Discord"); }); conn.end().then(() => { console.log("Disconnected from MySQL"); }); sshConn.end(); console.log("Disconnected from SSH") setTimeout(() => { process.exit(); }, 60000); }); }); }); } break; case "dev": // Developer commands // check if the user is a developer if (!config.discord.developers.includes(interaction.user.id)) { interaction.reply({ content: "You're not a developer!", ephemeral: true }); break; } // switch subcommand switch (interaction.options.getSubcommand()) { case "fwconsole": // Run a fwconsole command await interaction.deferReply({ ephemeral: true }); let cmd = interaction.options.get("command").value; sshConn.exec(`fwconsole ${cmd}`, (err, stream) => { if (err) { interaction.editReply(`Error running command: ${err}`); sendLog(`${colors.red("[ERROR]")} ${err}`); } outputStream = "" stream.on("data", (data) => { outputStream += `${data}` }) stream.on('exit', (code, signal) => { console.log(`Ran command ${cmd}:\n${outputStream}`) console.log("TEST" + outputStream.length) // generate message json const msgJson = { content: `Ran command \`${cmd}\`\n\`\`\`ansi\n${outputStream}\`\`\`` } // outputStream is too long for Discord, so we need to send it as a file if (outputStream.length > 2000) { // make the buffer const buffer = Buffer.from(outputStream, 'utf-8'); const attachment = { name: "output.txt", attachment: buffer } msgJson.files = [attachment]; msgJson.content = `Ran command \`${cmd}\`\nOutput too long, sending as a file` } interaction.editReply(msgJson); sendLog(`${colors.green("[INFO]")} Ran command ${cmd}`); }); }); break; case "restart": // Restart the bot await interaction.reply({ content: "Restarting the bot...", ephemeral: true }) sendLog(`${colors.green("[INFO]")} Restarting the bot`); dcClient.destroy().then(() => { console.log("Disconnected from Discord"); }); conn.end().then(() => { console.log("Disconnected from MySQL"); }); sshConn.end(); console.log("Disconnected from SSH") setTimeout(() => { process.exit(); }, 1000); break; case "asterisk": // Asterisk CLI command break; // for now } break; default: break; } } if (interaction.isButton()) { switch (interaction.customId) { case "new": await interaction.deferReply({ ephemeral: true }); lookupExtension(interaction.user.id, "uid").then((result) => { if (result.status == "exists") { // The user already has an extension, return an ephemeral message saying so interaction.editReply({ content: "You already have an extension!", ephemeral: true }); } }).catch((error) => { // The user doesn't have an extension, create one findNextExtension().then((result) => { if (result.status == "success") { let uid = interaction.user.id; let ext = result.result; let name = interaction.user.displayName; interaction.editReply(`Creating extension ${ext}...`) // Create the extension createExtension(ext, name, uid).then((result) => { if (result.status == "created") { interaction.editReply({ content: "", embeds: [{ "title": "Extension Created!", "color": 0x00ff00, "description": `The SIP server is \`${config.freepbx.server}\``, "fields": [{ "name": "Extension/Username", "value": ext }, { "name": "Password", "value": `||${result.result.fetchExtension.user.extPassword}||` } ] }] }) sendLog(`${colors.cyan("[INFO]")} Created extension ${ext} for user ${uid}`); // Add the role to the user on Discord based on the ID in the config file let role = interaction.guild.roles.cache.find(role => role.id === config.discord.roleId); interaction.member.roles.add(role); } }).catch((error) => { interaction.editReply(`Error creating extension: ${error}`); }); } }).catch((error) => { interaction.editReply(`Error finding next available extension: ${error}`); }); }); break; case "delete": interaction.reply({ content: "Are you sure you want to delete your extension?\nThis action is **irreversible**!\nAll voicemails, call history, and other data will be **permanently deleted**!\n\n**Only do this if you're absolutely sure you want to delete your extension!**", ephemeral: true, components: [{ type: 1, components: [{ type: 2, label: "Yes", emoji: { name: "✅" }, style: 4, custom_id: "delete2" }] }] }).then(() => { setTimeout(() => { try { interaction.deleteReply(); } catch (error) { // ignore } }, 10000); }); break; case "delete2": await interaction.deferReply({ ephemeral: true }); lookupExtension(interaction.user.id, "uid").then((result) => { if (result.status == "exists") { // The user has an extension, delete it deleteExtension(result.result.fetchExtension.user.extension).then((delResult) => { if (delResult.status == "deleted") { interaction.editReply({ content: "Extension Deleted!", ephemeral: true }); sendLog(`${colors.green("[INFO]")} ${interaction.user.displayName} (${interaction.user.id}) deleted extension ${result.result.fetchExtension.user.extension}`) // Remove the role from the user on Discord based on the ID in the config file let role = interaction.guild.roles.cache.find(role => role.id === config.discord.roleId); interaction.member.roles.remove(role); } }).catch((error) => { // sendLog full error with line number interaction.editReply(`Error deleting extension: ${error}`); }); } }).catch((error) => { // The user doesn't have an extension, return an ephemeral message saying so interaction.editReply({ content: "You don't have an extension!", ephemeral: true }); }); break; case "whoami": await interaction.deferReply({ ephemeral: true }); lookupExtension(interaction.user.id, "uid").then((result) => { if (result.status == "exists") { // The user already has an extension, return an ephemeral message saying so interaction.editReply({ content: "", embeds: [{ "title": "Extension Info", "color": 0x00ff00, "description": `The SIP server is \`${config.freepbx.server}\``, "fields": [{ "name": "Extension/Username", "value": result.result.fetchExtension.user.extension }, { "name": "Password", "value": `||${result.result.fetchExtension.user.extPassword}||` } ] }], ephemeral: true }) } }).catch((error) => { // The user doesn't have an extension, create one sendLog(`${colors.red("[ERROR]")} ${error}`) interaction.editReply({ content: "You don't have an extension!", ephemeral: true }); }); break; } } if (interaction.isUserContextMenuCommand()) { switch (interaction.commandName) { case "Lookup Extension": // Get the extension for the user if they have one await interaction.deferReply({ ephemeral: true }); lookupExtension(interaction.targetId, "uid").then((result) => { if (result.status == "exists") { // The user already has an extension, return an ephemeral message saying so interaction.editReply({ content: `${interaction.targetUser} has extension \`${result.result.fetchExtension.user.extension}\``, ephemeral: true }) } }).catch((error) => { // The user doesn't have an extension, create one sendLog(`${colors.red("[ERROR]")} ${error}`) interaction.editReply({ content: "That user doesn't have an extension!", ephemeral: true }); }); break; case "Create Extension": // Create an extension for the user, if they have one, return the extension info await interaction.deferReply({ ephemeral: true }); lookupExtension(interaction.targetId, "uid").then((result) => { if (result.status == "exists") { // The user already has an extension, return an ephemeral message saying so interaction.editReply({ content: "", embeds: [{ "title": "Extension Info", "color": 0x00ff00, "description": `The SIP server is \`${config.freepbx.server}\``, "fields": [{ "name": "Extension/Username", "value": result.result.fetchExtension.user.extension }, { "name": "Password", "value": `||${result.result.fetchExtension.user.extPassword}||` } ] }] }); } }).catch((error) => { // The user doesn't have an extension, create one findNextExtension().then((result) => { if (result.status == "success") { let uid = interaction.targetId; let ext = result.result; let name = interaction.targetUser.displayName; interaction.editReply(`Creating extension ${ext}...`) // Create the extension createExtension(ext, name, uid).then((result) => { if (result.status == "created") { interaction.editReply({ content: "", embeds: [{ "title": "Extension Created!", "color": 0x00ff00, "description": `The SIP server is \`${config.freepbx.server}\``, "fields": [{ "name": "Extension/Username", "value": ext }, { "name": "Password", "value": `||${result.result.fetchExtension.user.extPassword}||` } ] }] }) sendLog(`${colors.cyan("[INFO]")} Admin ${interaction.user.displayName} Created extension ${ext} for user ${interaction.targetUser.displayName} (${interaction.targetId})`); // Add the role to the user on Discord based on the ID in the config file let role = interaction.guild.roles.cache.find(role => role.id === config.discord.roleId); interaction.targetMember.roles.add(role); } }).catch((error) => { interaction.editReply(`Error creating extension: ${error}`); }); } }).catch((error) => { interaction.editReply(`Error finding next available extension: ${error}`); }); }); break; case "Delete Extension": // Delete the users extension, if they have one await interaction.deferReply({ ephemeral: true }); lookupExtension(interaction.targetId, "uid").then((result) => { if (result.status == "exists") { // The user has an extension, delete it deleteExtension(result.result.fetchExtension.user.extension).then((delResult) => { if (delResult.status == "deleted") { interaction.editReply({ content: "Extension Deleted!", ephemeral: true }); sendLog(`${colors.green("[INFO]")} ${interaction.user.displayName} deleted ${interaction.targetUser.username}'s extension ${result.result.fetchExtension.user.extension}`) // Remove the role from the user on Discord based on the ID in the config file let role = interaction.guild.roles.cache.find(role => role.id === config.discord.roleId); interaction.targetMember.roles.remove(role); } }).catch((error) => { // sendLog full error with line number interaction.editReply(`Error deleting extension: ${error}`); }); } }).catch((error) => { // The user doesn't have an extension, return an ephemeral message saying so interaction.editReply({ content: "That user doesn't have an extension!", ephemeral: true }); }); break; } } }); // Lets actually handle exceptions now process.on('unhandledRejection', (error) => { // Log a full error with line number sendLog(`${colors.red("[ERROR]")} ${error}`); // If config.ntfyUrl is set, Send the exception to ntfy if (config.ntfyUrl) fetch(config.ntfyUrl, { method: 'POST', // PUT works too body: error, headers: { 'Title': 'FreePBX Bot Rejection', 'Priority': 5, 'Tags': 'warning,phone,FreePBX Manager' } }); }); process.on('uncaughtException', (error) => { // Log a full error with line number sendLog(`${colors.red("[ERROR]")} ${error}`); // If config.ntfyUrl is set, Send the exception to ntfy if (config.ntfyUrl) fetch(config.ntfyUrl, { method: 'POST', // PUT works too body: error, headers: { 'Title': 'FreePBX Bot Exception', 'Priority': 5, 'Tags': 'warning,phone,FreePBX Manager' } }); }); dcClient.login(config.discord.token);