diff --git a/.gitignore b/.gitignore index 98d6135..87edb2c 100644 --- a/.gitignore +++ b/.gitignore @@ -136,4 +136,6 @@ config.json.disabled test.js embeds.json pageGroups.json -.ssh/ \ No newline at end of file +.ssh/ + +old/ \ No newline at end of file diff --git a/TODO.md b/TODO.md new file mode 100644 index 0000000..0b319cc --- /dev/null +++ b/TODO.md @@ -0,0 +1,42 @@ +# Bot Checklist + +## General Commands +- [X] **/whoami** - Get your extension info if you have one. +- [X] **/new** - Get an extension on the LiteNet Phone System. +- [X] **/delete** - Remove your extension from the LiteNet Phone System. +- [ ] **/list** - List all extensions on the LiteNet Phone System. +- [ ] **/button** - Send the "Get an extension" button! *(Requires default_member_permissions: 0)* + +## Admin Commands +*(Requires default_member_permissions: 0)* +- [ ] **/admin** + - [ ] **silence** - Kill all ongoing calls. + - [ ] **reload** - Run an Asterisk reload. + - [ ] **reboot** - Reboot the server. *(LAST RESORT)* + +## Developer Commands +*(Requires default_member_permissions: 0)* +- [ ] **/dev** + - [ ] **fwconsole** - Run an `fwconsole` command. + - [ ] **command** (required) - The command to run. + - [ ] **asterisk** - Run an Asterisk CLI command. + - [ ] **command** (required) - The command to run. + - [ ] **shell** - Run a shell command. + - [ ] **command** (required) - The command to run. + - [ ] **restart** - Restart the bot. + +## Call Detail Records (CDR) +- [ ] **/cdr** - Get the call detail records for your extension. + - [ ] **start_date** (optional) - The start date for the CDR (mm/dd/yyyy). + - [ ] **end_date** (optional) - The end date for the CDR (mm/dd/yyyy). + +## Context Menu Commands +- [ ] **Lookup Extension** - *(Type: 2)* +- [ ] **Create Extension** - *(Type: 2, Requires default_member_permissions: 0)* +- [ ] **Delete Extension** - *(Type: 2, Requires default_member_permissions: 0)* + + +## Buttons (On button message) +- **Get an Extension** - Sends the "Get an extension" button. +- **See Your Info** - Sends the "See your info" button. +- **Delete Your Extension** - Sends the "Delete your extension" button. \ No newline at end of file diff --git a/commands.disabled b/commands.disabled deleted file mode 100644 index 8be6d08..0000000 --- a/commands.disabled +++ /dev/null @@ -1,13 +0,0 @@ -{ - "name": "name", - "description": "Change your extension's name (Defaults to your Discord name)", - "type": 1, - "options": [ - { - "name": "name", - "description": "The new name for your extension", - "type": 3, - "required": false - } - ] - }, \ No newline at end of file diff --git a/commands.json b/commands.js similarity index 77% rename from commands.json rename to commands.js index 276987f..f3f8012 100644 --- a/commands.json +++ b/commands.js @@ -1,4 +1,4 @@ -[ +module.exports = [ { "name": "whoami", "description": "Get your extension info if you have one", @@ -18,13 +18,7 @@ "name": "confirm", "description": "Confirm that you want to delete your extension. THIS CANNOT BE UNDONE!", "type": 5, - "required": true, - "choices": [ - { - "name": "yes", - "value": "yes" - } - ] + "required": true } ] }, @@ -39,19 +33,20 @@ "type": 1, "default_member_permissions": 0 }, - { - "name": "name", - "description": "Change your extension's name (Defaults to your Discord name)", - "type": 1, - "options": [ - { - "name": "name", - "description": "The new name for your extension", - "type": 3, - "required": false - } - ] - }, + // TODO: Find a way to make the name command work again. Sadge + // { + // "name": "name", + // "description": "Change your extension's name (Defaults to your Discord name)", + // "type": 1, + // "options": [ + // { + // "name": "name", + // "description": "The new name for your extension", + // "type": 3, + // "required": false + // } + // ] + // }, { "name": "admin", "description": "Admin only commands", @@ -154,17 +149,29 @@ ] }, { - "name": "Lookup Extension", - "type": 2 + "name": "lookup", + "description": "Find extension by Discord user", + "type": 1, + "options": [ + { + "name": "user", + "description": "The Discord user to lookup", + "type": 6, + "required": true + } + ] }, { - "name": "Create Extension", - "type": 2, - "default_member_permissions": 0 - }, - { - "name": "Delete Extension", - "type": 2, - "default_member_permissions": 0 + "name": "whois", + "description": "Find Discord user by extension", + "type": 1, + "options": [ + { + "name": "extension", + "description": "The extension to lookup", + "type": 4, + "required": true + } + ] } ] \ No newline at end of file diff --git a/config.json.default b/config.json.default deleted file mode 100644 index b68e578..0000000 --- a/config.json.default +++ /dev/null @@ -1,39 +0,0 @@ -{ - "ntfyUrl": "ntfy-url", - "freepbx": { - "server": "sip-server-ip", - "url": "pbx-api-url", - "clientid": "gql-client-id", - "allowedscopes": "gql", - "secret": "gql-secret", - "startExt": 1000 - }, - "discord": { - "token": "bot-token", - "guildId": "guild-id", - "roleId": "user-role", - "logId": "log-channel", - "extList": "extension-list-channel", - "developers": [ - "your-user-id" - ] - }, - "mariadb": { - "host": "db-hostname0here", - "user": "bot", - "password": "bot", - "database": "asterisk", - "connectionLimit": 5 - }, - "cdrdb": { - "host": "db-hostname-here", - "user": "bot", - "password": "bot", - "database": "asteriskcdrdb", - "connectionLimit": 5 - }, - "status": { - "interval": 60, - "url": "uptime-kuma-link" - } -} \ No newline at end of file diff --git a/embeds.json.default b/embeds.json.default deleted file mode 100644 index 66aff81..0000000 --- a/embeds.json.default +++ /dev/null @@ -1,9 +0,0 @@ -{ - "controls": [ - { - "title": "Phone System Controls", - "color": 205442, - "description": "Use the buttons below to control your extension!" - } - ] -} \ No newline at end of file diff --git a/freepbx.js b/freepbx.js new file mode 100644 index 0000000..53be24f --- /dev/null +++ b/freepbx.js @@ -0,0 +1,185 @@ +const { FreepbxGqlClient, gql } = require("freepbx-graphql-client"); + +class FreepbxManager { + /** + * Creates an instance of the FreepbxGqlClient and initializes the connection pool. + * + * @param {Object} config - Configuration object for the FreepbxGqlClient. + * @param {string} config.url - The URL of the FreePBX GraphQL endpoint. + * @param {string} config.clientId - The client ID for authentication. + * @param {string} config.clientSecret - The client secret for authentication. + * @param {Object} config.dbPool - The connection pool for managing database connections. + */ + constructor(config) { + this.client = new FreepbxGqlClient(config.url, { + client: { + id: config.clientId, + secret: config.clientSecret, + }, + }); + this.pool = config.dbPool; + + if (!this.pool) { + throw new Error("Connection pool is required"); + } + + + } + + async getExtension(ext) { + ext = String(ext); + + const query = gql` + query fetchExtension($extensionId: ID!) { + fetchExtension(extensionId: $extensionId) { + user { + extension + name + extPassword + voicemail + } + } + } + `; + + const variables = { + extensionId: ext.match(/\d+/)[0], + }; + + return await this.client.request(query, variables); + } + + async listExtensions() { + const query = gql` + query { + fetchAllExtensions { + extension { + user { + extension + name + } + } + } + } + `; + + return await this.client.request(query); + } + + async addExtension(ext, name) { + ext = String(ext); + name = String(name); + + const query = gql` + mutation addExtension($ext: ID!, $name: String!, $vmPassword: String!) { + addExtension(input: { + extensionId: $ext + name: $name + vmEnable: true + vmPassword: $vmPassword + email: "" + maxContacts: "100" + umEnable: false + }) { + status + } + } + `; + + const variables = { + ext, + name, + vmPassword: ext, + }; + + return await this.client.request(query, variables); + } + + async deleteExtension(ext) { + ext = String(ext); + const query = gql` + mutation deleteExtension($ext: ID!) { + deleteExtension(input: { extensionId: $ext }) { + status + } + } + `; + + const variables = { + ext, + }; + + const fpbxQuery = this.client.request(query, variables); + const dbQuery = this.pool.query('DELETE FROM paging_groups WHERE ext = ?', [ext]); + return await Promise.all([fpbxQuery, dbQuery]); + } + + async reload() { + const query = gql` + mutation { + doreload(input: { clientMutationId: "${Math.random().toString(36).substring(2, 14)}" }) { + status + } + } + `; + + return await this.client.request(query); + } + + // async updateName(ext, name) { + // const query = gql` + // mutation updateName($ext: ID!, $name: String!) { + // updateExtension(input: {extensionId: $ext, name: $name}) { + // status, + // message + // } + // }`; + + // const variables = { + // ext, + // name, + // }; + + // return await this.client.request(query, variables); + // } + // TODO: Implement updateName method, Current implementation resets extension for some reason + + async joinPageGroup(ext, pageGroup) { + const [lookup] = await this.pool.query('SELECT * FROM paging_groups WHERE page_number = ? AND ext = ?', [pageGroup, ext]); + if (lookup) { + return false; + } + + await this.pool.query('INSERT INTO paging_groups (page_number, ext) VALUES (?, ?)', [pageGroup, ext]); + return true; + }; + + async leavePageGroup(ext, pageGroup) { + const [lookup] = await this.pool.query('SELECT * FROM paging_groups WHERE page_number = ? AND ext = ?', [pageGroup, ext]); + if (!lookup) { + return false; + } + + await this.pool.query('DELETE FROM paging_groups WHERE page_number = ? AND ext = ?', [pageGroup, ext]); + return true; + }; + + async getNextAvailableExtension() { + const extList = await this.listExtensions(); + const exts = extList.fetchAllExtensions.extension; + const startExt = process.env.START_EXT ? parseInt(process.env.START_EXT, 10) : 1000; + const existingExts = exts.map(ext => parseInt(ext.user.extension, 10)).sort((a, b) => a - b); + + let nextExt = startExt; + for (let i = 0; i < existingExts.length; i++) { + if (existingExts[i] !== nextExt) { + break; + } + nextExt++; + } + + return nextExt; + } +} + +module.exports = FreepbxManager; diff --git a/funcs.js b/funcs.js deleted file mode 100644 index 9e5cf83..0000000 --- a/funcs.js +++ /dev/null @@ -1,97 +0,0 @@ -// Some random functions, as to not clutter the main file -// Generate GraphQL query -const generateQuery = (type, args) => { - switch (type) { - case 'lookup': - return minifyQuery(`query { - fetchExtension(extensionId: "${args.ext}") { - user { - extension - name - extPassword - voicemail - } - } - fetchVoiceMail(extensionId: "${args.ext}") { - password - email - } - }`); - break; - case 'list': - return minifyQuery(`query { - fetchAllExtensions { - extension { - user { - extension - name - } - } - } - }`); - break; - case 'add': - return minifyQuery(`mutation { - addExtension(input: { - extensionId: "${args.ext}" - name: "${args.name}" - email: "${args.uid}" - vmEnable: true - vmPassword: "${args.ext}" - maxContacts: "5" - umEnable: false - }) { - status - } - }`); - break; - - case 'delete': - return minifyQuery(`mutation { - deleteExtension(input: {extensionId: ${args.ext}}) { - status - } - }`); - break; - case 'reload': - return minifyQuery(`mutation { - doreload(input: {clientMutationId: "${args.id}"}) { - status - } - }`); - break; - case 'update_name': - return minifyQuery(`mutation { - updateCoreUser (input: {extension: ${args.ext}, name: "${args.name}", noanswer_cid: "", busy_cid: "", chanunavail_cid: "", busy_dest: "", noanswer_dest: "", chanunavail_dest: ""}) { - coreuser { - name - } - } - }`); - } -} - -// minify query function -const minifyQuery = (query) => { - return query.replace(/\s+/g, ' ').trim(); -} - -module.exports = { - generateQuery, - minifyQuery, - // Input validation - validateInput: function (input, type) { - switch (type) { - case 'extention': - // Check if input is a 3 digit number - if (input.length != 3) { - return false; - } - if (isNaN(input)) { - return false; - } - return true; - break; - } - }, -} diff --git a/index.js b/index.js index 541e742..3f0a479 100644 --- a/index.js +++ b/index.js @@ -1,1707 +1,134 @@ -//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 -console.log(`Using key file: ${keyFile}`); -const privateKey = fs.readFileSync(".ssh/" + keyFile, 'utf8'); +require("dotenv").config(); -// 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, - } +const mariadb = require("mariadb"); +const pool = mariadb.createPool({ + host: process.env.DB_HOST, + port: process.env.DB_PORT || 3306, + user: process.env.DB_USER, + password: process.env.DB_PASS, + database: "asterisk", + connectionLimit: 5, }); -// 50 minute interval to refresh the token -setInterval(() => { - pbxClient = new FreepbxGqlClient(config.freepbx.url, { - client: { - id: config.freepbx.clientid, - secret: config.freepbx.secret, - } - }); -}, 3000000); +const FreepbxManager = require("./freepbx"); +const fpbx = new FreepbxManager({ + url: process.env.FREEPBX_URL, + clientId: process.env.FREEPBX_CLIENT_ID, + clientSecret: process.env.FREEPBX_CLIENT_SECRET, + dbPool: pool, +}); -// Set up mariadb connection -const mariadb = require('mariadb'); -const pool = mariadb.createPool(config.mariadb); -const cdrPool = mariadb.createPool(config.cdrdb); +const Discord = require('discord.js'); +const client = new Discord.Client({ + intents: [ "GuildMembers", "Guilds", "GuildPresences" ] +}); +const colors = require('colors'); -const json2csv = (obj) => { // Specifically for CDR - let csv = "call_date,src,dst,disposition,caller_id,duration,billsec,context,recordingfile\n"; - for (let key in obj) { - data = [ - obj[key].calldate, - obj[key].src, - obj[key].dst, - obj[key].disposition, - obj[key].clid, - obj[key].duration, - obj[key].billsec, - obj[key].dcontext, - obj[key].recordingfile - ]; - csv += data.join(",") + "\n"; +const log = { + log: (msg) => { + console.log(msg); + }, + info: (msg) => { + console.log(`${colors.cyan("[INFO]")} ${msg}`); + }, + warn: (msg) => { + console.log(`${colors.yellow("[WARN]")} ${msg}`); + }, + error: (msg) => { + console.log(`${colors.red("[ERROR]")} ${msg}`); + }, + success: (msg) => { + console.log(`${colors.green("[SUCCESS]")} ${msg}`); + }, + debug: (msg) => { + console.log(`${colors.magenta("[DEBUG]")} ${msg}`); } - return csv; } -// Some functions for FreePBX +// Put clients in global for use elsewhere. +global.pool = pool; +global.fpbx = fpbx; +global.client = client; +global.log = log; -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 --json', (err, stream) => { - if (err) { - reject(err); - } - stream.on('data', (data) => { - // is there a way to send this data without resolving the promise? - dt = JSON.parse(data.toString()) - if (dt.message == "Reload Complete") { - resolve(dt); - } - }); - }); - }); -} -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); - }); - }); -} +client.on('ready', async () => { + log.success(`Logged in as ${client.user.displayName}`); - -const createExtension = (ext, name, uid) => { - return new Promise(async (resolve, reject) => { - const conn = await pool.getConnection(); - await conn.query(`DELETE FROM devices WHERE id NOT IN (SELECT extension FROM users);`) - await conn.query(`DELETE FROM sip WHERE id NOT IN (SELECT extension FROM users);`); - conn.end(); - 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(async (resolve, reject) => { - // update the extension name in the `users` table - const conn = await pool.getConnection(); - await conn.query(`UPDATE users SET name = '${name}' WHERE extension = ${ext};`) - //await conn.query(`UPDATE sip WHERE id = ${ext} AND keyword = 'callerid' SET data = '${name} <${ext}>';`); // this query is borked, im bad at sql - await conn.query(`UPDATE sip SET data = '${name} <${ext}>' WHERE id = ${ext} AND keyword = 'callerid';`); - conn.end(); - // reload - reload().then((result) => { - resolve({ - "status": "updated", - "result": name - }) - }).catch((error) => { - reject(error); - }); - }); -} - -const generateExtensionListEmbed = async () => { - return new Promise(async (resolve, reject) => { + const commands = require("./commands") + // Command registration + log.info("Registering commands...") + await (async () => { 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] = "-"; - } + const rest = new Discord.REST().setToken(client.token); + // Global Commands + log.info(`Registering global commands`); + rest.put(Discord.Routes.applicationCommands(client.user.id), { body: commands }).then(() => { + log.success("Global commands registered") + }).catch((error) => { + log.error(error) }); - 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); + log.error(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 = (message) => { - console.log(`LOG BEFORE READY: ${message}`); -}; -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); - }; - const conn = await pool.getConnection(); - await conn.query(`DELETE FROM devices WHERE id NOT IN (SELECT extension FROM users);`) - await conn.query(`DELETE FROM sip WHERE id NOT IN (SELECT extension FROM users);`); - conn.end(); - 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 - } - ] - }; +client.on('interactionCreate', async interaction => { + switch(interaction.type) { + case Discord.InteractionType.ApplicationCommand: + const command = require(`./interactionHandlers/commands/${interaction.commandName}`); - // make a non reference copy of the commands object - var commands = JSON.parse(JSON.stringify(commandsBase)); - commands.push(pageCommand); + if (!command) return; - - (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.`); + await command.execute(interaction); } catch (error) { - console.error(`${colors.red("[ERROR]")} ${error}`); + log.error(error); + await interaction.reply({ content: 'There was an error while executing this command!', ephemeral: true }); } - })(); + break; + case Discord.InteractionType.MessageComponent: + const component = require(`./interactionHandlers/components/${interaction.customId}`); - // Presence Stuff - getExtCount().then((result) => { - dcClient.user.setPresence({ - activities: [{ - name: `${result} extensions`, - type: "WATCHING" - }], - status: "online" - }); - }).catch((error) => { - sendLog(`${colors.red("[ERROR]")} ${error}`); - }); + if (!component) return; - // 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 () => { - // run this goofy query just to make sure everything is happy - /* - DELETE FROM devices - WHERE id NOT IN (SELECT extension FROM users); - */ - // This will delete any devices that don't have a corresponding user - // This is a safety measure to prevent orphaned devices (it breaks the API entirely if there are any) - const conn = await pool.getConnection(); - await conn.query(`DELETE FROM devices WHERE id NOT IN (SELECT extension FROM users);`) - await conn.query(`DELETE FROM sip WHERE id NOT IN (SELECT extension FROM users);`); - conn.end(); - - - - 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 => { - const botMessage = messages.find(msg => msg.author.id === dcClient.user.id); - if (botMessage) { - botMessage.edit({ - content: "", - embeds: embed - }); - } else { - extListChannel.send({ - 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 => { - const botMessage = messages.find(msg => msg.author.id === dcClient.user.id); - if (botMessage) { - botMessage.edit({ - content: "", - embeds: embed - }); - } else { - extListChannel.send({ - content: "", - embeds: embed - }); - } - }); - }) + try { + await component.execute(interaction); + } catch (error) { + log.error(error); + await interaction.reply({ content: 'There was an error while executing this component!', ephemeral: true }); } - }) - - }); - - if (config.status?.url) { - // 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 deleting ext ${error}`); - }); - } - }).catch((error) => { - sendLog(`${colors.red("[ERROR]")} Error looking up extension to delete ${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 looking up extension ${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 adding user to pg group ${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 looking up user in pg group ${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 while removing user from page group ${error}`); - }); - } - }).catch((error) => { - interaction.editReply(`Error removing you from the paging group: ${error}`); - sendLog(`${colors.red("[ERROR]")} Error while looking if user is in pg grp ${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]")} Error killing calls ${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]")} Error manually reloading fwconsole ${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]")} Error manually reloading asterisk ${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]")} Error rebooting? ${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]")} Error running fwconsole command ${err}`); - } - outputStream = "" - stream.on("data", (data) => { - outputStream += `${data}` - }) - stream.on('exit', (code, signal) => { - // 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 - await interaction.deferReply({ - ephemeral: true - }); - let cmd2 = interaction.options.get("command").value; - sshConn.exec(`asterisk -x '${cmd2}'`, (err, stream) => { - if (err) { - interaction.editReply(`Error running command: ${err}`); - sendLog(`${colors.red("[ERROR]")} Error running asterisk cli command ${err}`); - } - outputStream = "" - stream.on("data", (data) => { - outputStream += `${data}` - }) - stream.on('exit', (code, signal) => { - // generate message json - const msgJson = { - content: `Ran command \`${cmd2}\`\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 \`${cmd2}\`\nOutput too long, sending as a file` - } - interaction.editReply(msgJson); - sendLog(`${colors.green("[INFO]")} Ran command ${cmd2}`); - }); - }); - break; - case "shell": // This is dangerous, only allow developers to use it - await interaction.deferReply({ - ephemeral: true - }); - let cmd3 = interaction.options.get("command").value; - // TODO: Timeout - //let cmd3timeout = interaction.options.get("timeout").value; - sshConn.exec(cmd3, (err, stream) => { - if (err) { - interaction.editReply(`Error running command: ${err}`); - sendLog(`${colors.red("[ERROR]")} Error running shell command ${err}`); - } - // if timeout is set, set a timeout before - //timeout = setTimeout(() => { - // stream.close(); - // interaction.editReply(`Command timed out after ${cmd3timeout}ms`); - //}) - outputStream = "" - stream.on("data", (data) => { - outputStream += `${data}` - }) - stream.on('exit', (code, signal) => { - // clear the timeout - //clearTimeout(timeout); - // generate message json - const msgJson = { - content: `Ran command \`${cmd3}\`\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 \`${cmd3}\`\nOutput too long, sending as a file` - } - interaction.editReply(msgJson); - sendLog(`${colors.green("[INFO]")} Ran command ${cmd3}`); - }); - }); - break; - } - break; - case "cdr": // Get CDR records for the user - // default to beginning of time - let userStartDate = interaction.options.getString("start_date")?.match(/^\d{2}\/\d{2}\/\d{4}$/)?.[0] || new Date(0); // regex this to mm/dd/yyyy - let userEndDate = interaction.options.getString("end_date")?.match(/^\d{2}\/\d{2}\/\d{4}$/)?.[0] || new Date(); // regex this to mm/dd/yyyy - let startDate = new Date(userStartDate); - let endDate = new Date(userEndDate); - await interaction.deferReply({ - ephemeral: true - }); - lookupExtension(interaction.user.id, "uid").then((result) => { - if (result.status == "exists") { - // The user has an ext, use the cdrdb to get the records - cdrPool.getConnection().then((conn) => { - conn.query(`SELECT * FROM cdr WHERE src = '${result.result.fetchExtension.user.extension}' OR dst = '${result.result.fetchExtension.user.extension}' AND calldate BETWEEN '${startDate.toISOString()}' AND '${endDate.toISOString()}'`).then((result) => { - if (result.length == 0) { - interaction.editReply({ - content: "No CDR records found", - ephemeral: true - }); - } else { - // Generate the CSV - const fields = ["calldate", "src", "dst", "duration", "disposition", "recordingfile"]; - const csv = json2csv(result); - const buffer = Buffer.from(csv, 'utf-8'); - interaction.editReply({ - files: [ - { - attachment: buffer, - name: "cdr.csv" - } - ], - content: "Here are your CDR records" - }); - } - }).catch((error) => { - interaction.editReply(`Error getting CDR records: ${error}`); - }); - conn.end(); - }).catch((error) => { - interaction.editReply(`Error getting CDR records: ${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; - 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 finding extension for user? ${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]")} User has not ext (lookup) ${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; - } + break; } }); -// Lets actually handle exceptions now -process.on('unhandledRejection', (error) => { - // Log a full error with line number - sendLog(`${colors.red("[ERROR]")} Unhandled Rejection ${error}`); - // If config.ntfyUrl is set, Send the exception to ntfy - if (config.ntfyUrl) fetch(config.ntfyUrl, { - method: 'POST', // PUT works too - body: JSON.stringify(error), - headers: { - 'Title': 'FreePBX Bot Rejection', - 'Priority': 5, - 'Tags': 'warning,phone,FreePBX Manager' - } - }); -}); +// DEBUG: Insert 200 extensions into discord_users table +// (async () => { +// try { +// const conn = await pool.getConnection(); +// for (let i = 1; i <= 200; i++) { +// const ext = await fpbx.getNextAvailableExtension(); +// await fpbx.addExtension(ext, `Test User ${i}`) +// await conn.query("INSERT INTO discord_users (extension, discordId) VALUES (?, ?)", [ext, '289884287765839882']); +// log.debug(`Inserted extension ${ext} into discord_users table.`); +// } +// log.success("Inserted 200 extensions into discord_users table."); +// conn.release(); +// } catch (err) { +// log.error(`Failed to insert extensions: ${err}`); +// } +// })(); -process.on('uncaughtException', (error) => { - // Log a full error with line number - sendLog(`${colors.red("[ERROR]")} Uncaught Exception ${error.stack}`); - // If config.ntfyUrl is set, Send the exception to ntfy - if (config.ntfyUrl) fetch(config.ntfyUrl, { - method: 'POST', // PUT works too - body: error.stack, - headers: { - 'Title': 'FreePBX Bot Exception', - 'Priority': 5, - 'Tags': 'warning,phone,FreePBX Manager' - } - }); -}); - -dcClient.login(config.discord.token); \ No newline at end of file +// Startup +require("./migrations")(pool).then(() => { + log.success("Database migrations complete."); + log.info("Starting Discord client..."); + client.login(process.env.DISCORD_TOKEN); +}).catch((err) => { + log.error(`Database migrations failed: ${err}`); + process.exit(1); +}); \ No newline at end of file diff --git a/interactionHandlers/commands/delete.js b/interactionHandlers/commands/delete.js new file mode 100644 index 0000000..227cf71 --- /dev/null +++ b/interactionHandlers/commands/delete.js @@ -0,0 +1,30 @@ +const pool = global.pool +const fpbx = global.fpbx +const client = global.client +const log = global.log + +module.exports = {}; + +module.exports.execute = async (interaction) => { + const [lookup] = await pool.query('SELECT * FROM discord_users WHERE discordId = ?', [interaction.user.id]); + if (!lookup) { + await interaction.reply({ content: `We're sorry, It doesn't look like you have an extension!`, ephemeral: true }); + return; + } + if (interaction.options.getBoolean("confirm") !== true) { + await interaction.reply({ content: `You must confirm you want to delete your extension!`, ephemeral: true }); + return; + } + fpbx.deleteExtension(lookup.extension).then(async (res) => { + if (res[0].deleteExtension.status != true) { + await interaction.reply({ content: `Something went wrong :(`, ephemeral: true }); + return; + } + await pool.query('DELETE FROM discord_users WHERE discordId = ?', [interaction.user.id]); + await fpbx.reload(); + await interaction.reply({ content: `Extension ${lookup.extension} deleted!`, ephemeral: true }); + }).catch(async (error) => { + log.error(error); + await interaction.reply({ content: 'There was an error while deleting your extension!', ephemeral: true }); + }); +} \ No newline at end of file diff --git a/interactionHandlers/commands/list.js b/interactionHandlers/commands/list.js new file mode 100644 index 0000000..aee0886 --- /dev/null +++ b/interactionHandlers/commands/list.js @@ -0,0 +1,36 @@ +const Discord = require('discord.js'); + +const pool = global.pool +const fpbx = global.fpbx +const client = global.client +const log = global.log + +module.exports = {}; + +module.exports.execute = async (interaction) => { + const lookup = await pool.query('SELECT * FROM discord_users'); + // lookup: [ { extension: '1001', discord_id: '1234567890' } ] + const embeds = []; + let description = ''; + + lookup.forEach((row, index) => { + const line = `${row.extension}: <@${row.discordId}>\n`; + if (description.length > 2048) { + embeds.push({ + description + }); + description = ''; + } + description += line; + }); + + if (description.length > 0) { + embeds.push({ + description + }); + } + embeds.forEach(async (embed) => { + await interaction.user.send({ embeds: [embed] }); + }) + await interaction.reply({ ephemeral: true, content: "Check your DMs!" }); +} \ No newline at end of file diff --git a/interactionHandlers/commands/lookup.js b/interactionHandlers/commands/lookup.js new file mode 100644 index 0000000..c80cc72 --- /dev/null +++ b/interactionHandlers/commands/lookup.js @@ -0,0 +1,19 @@ +const pool = global.pool +const fpbx = global.fpbx +const client = global.client +const log = global.log + +module.exports = {}; + +module.exports.execute = async (interaction) => { + const findUser = interaction.options.getUser('user'); + const [lookup] = await pool.query('SELECT * FROM discord_users WHERE discordId = ?', [findUser.id]); + + if (!lookup) { + await interaction.reply({ content: `No extension found for ${findUser.username}`, ephemeral: true }); + return; + } + + await interaction.reply({ content: `${findUser} has extension ${lookup.extension}`, ephemeral: true }); + +} \ No newline at end of file diff --git a/interactionHandlers/commands/new.js b/interactionHandlers/commands/new.js new file mode 100644 index 0000000..90123ea --- /dev/null +++ b/interactionHandlers/commands/new.js @@ -0,0 +1,40 @@ +const pool = global.pool +const fpbx = global.fpbx +const client = global.client +const log = global.log + +module.exports = {}; + +module.exports.execute = async (interaction) => { + await interaction.deferReply({ ephemeral: true }); + const [lookup] = await pool.query('SELECT * FROM discord_users WHERE discordId = ?', [interaction.user.id]); + if (lookup) { + await interaction.editReply({ content: `You already have an extension, it's ${lookup.extension}!`, ephemeral: true }); + return; + } + await interaction.editReply({ content: `Finding available extension`, ephemeral: true }); + fpbx.getNextAvailableExtension().then(async (nextExt) => { + await interaction.editReply({ content: `Found ${nextExt}. Creating..`, ephemeral: true }); + fpbx.addExtension(nextExt, interaction.user.username).then(async (res) => { + if (res.addExtension.status != true) { + await interaction.editReply({ content: `Something went wrong :(`, ephemeral: true }); + return; + } + await pool.query('INSERT INTO discord_users (discordId, extension) VALUES (?, ?)', [interaction.user.id, nextExt]); + await interaction.editReply({ content: `Extension ${nextExt} created! Getting info..`, ephemeral: true }); + await fpbx.reload(); + const extInfo = await fpbx.getExtension(nextExt); + await interaction.editReply({ embeds: [{ + title: "Your Extension Info", + description: `**PBX Address:** \`${process.env.PBX_HOSTNAME}\`\n**Extension:** \`${extInfo.fetchExtension.user.extension}\`\n**Name:** \`${extInfo.fetchExtension.user.name}\`\n**Password:** ||\`${extInfo.fetchExtension.user.extPassword}\`||`, + color: 0x00ff00 + }], ephemeral: true }) + }).catch(async (error) => { + log.error(error); + await interaction.editReply({ content: 'There was an error while creating your extension!', ephemeral: true }); + }); + }).catch(async (error) => { + log.error(error); + await interaction.editReply({ content: 'There was an error while creating your extension!', ephemeral: true }); + }); +} \ No newline at end of file diff --git a/interactionHandlers/commands/whoami.js b/interactionHandlers/commands/whoami.js new file mode 100644 index 0000000..e19c6c0 --- /dev/null +++ b/interactionHandlers/commands/whoami.js @@ -0,0 +1,25 @@ +const pool = global.pool +const fpbx = global.fpbx +const client = global.client +const log = global.log + +module.exports = {}; + +module.exports.execute = async (interaction) => { + const [lookup] = await pool.query('SELECT * FROM discord_users WHERE discordId = ?', [interaction.user.id]); + if (!lookup) { + await interaction.reply({ content: `We're sorry, It doesn't look like you have an extension!`, ephemeral: true }); + return; + } + const extInfo = await fpbx.getExtension(lookup.extension); + return await interaction.reply({ + ephemeral: true, embeds: [ + { + title: "Your Extension Info", + description: `**PBX Address:** \`${process.env.PBX_HOSTNAME}\`\n**Extension:** \`${extInfo.fetchExtension.user.extension}\`\n**Name:** \`${extInfo.fetchExtension.user.name}\`\n**Password:** ||\`${extInfo.fetchExtension.user.extPassword}\`||`, + color: 0x00ff00 + } + ] + }); + +} \ No newline at end of file diff --git a/interactionHandlers/commands/whois.js b/interactionHandlers/commands/whois.js new file mode 100644 index 0000000..3f55cf1 --- /dev/null +++ b/interactionHandlers/commands/whois.js @@ -0,0 +1,19 @@ +const pool = global.pool +const fpbx = global.fpbx +const client = global.client +const log = global.log + +module.exports = {}; + +module.exports.execute = async (interaction) => { + const findExt = interaction.options.getInteger('extension'); + const [lookup] = await pool.query('SELECT * FROM discord_users WHERE extension = ?', [findExt]); + + if (!lookup) { + await interaction.reply({ content: `No linked Discord account found for extension ${findExt}`, ephemeral: true }); + return; + } + + await interaction.reply({ content: `${findExt} belongs to <@${lookup.discordId}>`, ephemeral: true }); + +} \ No newline at end of file diff --git a/migrations.js b/migrations.js new file mode 100644 index 0000000..706c4f3 --- /dev/null +++ b/migrations.js @@ -0,0 +1,72 @@ +const mariadb = require('mariadb'); +const fs = require('fs'); +const path = require('path'); +const util = require("util") + + +function runMigrations(pool) { + return new Promise((resolve, reject) => { + let connection; + + pool.getConnection() + .then(conn => { + connection = conn; + + // Ensure a migrations table exists to track applied migrations + return connection.query(`CREATE TABLE IF NOT EXISTS migrations ( + id INT AUTO_INCREMENT PRIMARY KEY, + name VARCHAR(255) NOT NULL, + applied_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP + );`); + }) + .then(() => { + // Read all migration files + const migrationDir = path.join(__dirname, 'migrations'); + const files = fs.readdirSync(migrationDir).sort(); // Sort to apply in order + + return files.reduce((promise, file) => { + return promise.then(() => { + const migrationName = path.basename(file); + + // Check if the migration has already been applied + return connection.query( + 'SELECT 1 FROM migrations WHERE name = ? LIMIT 1', + [migrationName] + ).then(([rows]) => { + if (Object.keys(rows || {}).length > 0) { + //console.log(`Skipping already applied migration: ${migrationName}`); + return; // Skip this migration + } + + // Read and execute the migration SQL + const migrationPath = path.join(migrationDir, file); + const sql = fs.readFileSync(migrationPath, 'utf8'); + return connection.query(sql).then(() => { + // Record the applied migration + return connection.query( + 'INSERT INTO migrations (name) VALUES (?)', + [migrationName] + ).then(() => { + console.log(`Applied migration: ${migrationName}`); + }); + }); + }); + }); + }, Promise.resolve()); + }) + .then(() => { + console.log('All migrations applied successfully!'); + resolve(); + }) + .catch(err => { + console.error('Error running migrations:', err); + reject(err); + }) + .finally(() => { + if (connection) connection.release(); + }); + }); +} + + +module.exports = runMigrations \ No newline at end of file diff --git a/migrations/000_alter_users_table.sql b/migrations/000_alter_users_table.sql new file mode 100644 index 0000000..5c02e7c --- /dev/null +++ b/migrations/000_alter_users_table.sql @@ -0,0 +1 @@ +ALTER TABLE users ADD CONSTRAINT unique_extension UNIQUE (extension); \ No newline at end of file diff --git a/migrations/001_init_discord_users_table.sql b/migrations/001_init_discord_users_table.sql new file mode 100644 index 0000000..028a182 --- /dev/null +++ b/migrations/001_init_discord_users_table.sql @@ -0,0 +1,4 @@ +CREATE TABLE IF NOT EXISTS discord_users ( + extension VARCHAR(20) PRIMARY KEY, + discordId VARCHAR(25) NOT NULL +); \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 8fba4a6..7db98ed 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,6 +12,7 @@ "axios": "^1.6.0", "colors": "^1.4.0", "discord.js": "14.14.1", + "dotenv": "^16.4.7", "freepbx-graphql-client": "^0.1.1", "mariadb": "^3.2.0", "ping": "^0.4.4", @@ -20,85 +21,230 @@ } }, "node_modules/@discordjs/builders": { - "version": "1.6.5", - "resolved": "https://registry.npmjs.org/@discordjs/builders/-/builders-1.6.5.tgz", - "integrity": "sha512-SdweyCs/+mHj+PNhGLLle7RrRFX9ZAhzynHahMCLqp5Zeq7np7XC6/mgzHc79QoVlQ1zZtOkTTiJpOZu5V8Ufg==", + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@discordjs/builders/-/builders-1.10.0.tgz", + "integrity": "sha512-ikVZsZP+3shmVJ5S1oM+7SveUCK3L9fTyfA8aJ7uD9cNQlTqF+3Irbk2Y22KXTb3C3RNUahRkSInClJMkHrINg==", + "license": "Apache-2.0", "dependencies": { - "@discordjs/formatters": "^0.3.2", - "@discordjs/util": "^1.0.1", - "@sapphire/shapeshift": "^3.9.2", - "discord-api-types": "0.37.50", + "@discordjs/formatters": "^0.6.0", + "@discordjs/util": "^1.1.1", + "@sapphire/shapeshift": "^4.0.0", + "discord-api-types": "^0.37.114", "fast-deep-equal": "^3.1.3", - "ts-mixer": "^6.0.3", - "tslib": "^2.6.1" + "ts-mixer": "^6.0.4", + "tslib": "^2.6.3" }, "engines": { "node": ">=16.11.0" + }, + "funding": { + "url": "https://github.com/discordjs/discord.js?sponsor" } }, + "node_modules/@discordjs/builders/node_modules/@discordjs/formatters": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/@discordjs/formatters/-/formatters-0.6.0.tgz", + "integrity": "sha512-YIruKw4UILt/ivO4uISmrGq2GdMY6EkoTtD0oS0GvkJFRZbTSdPhzYiUILbJ/QslsvC9H9nTgGgnarnIl4jMfw==", + "license": "Apache-2.0", + "dependencies": { + "discord-api-types": "^0.37.114" + }, + "engines": { + "node": ">=16.11.0" + }, + "funding": { + "url": "https://github.com/discordjs/discord.js?sponsor" + } + }, + "node_modules/@discordjs/builders/node_modules/discord-api-types": { + "version": "0.37.117", + "resolved": "https://registry.npmjs.org/discord-api-types/-/discord-api-types-0.37.117.tgz", + "integrity": "sha512-d+Z6RKd7v3q22lsil7yASucqMfVVV0s0XSqu3cw7kyHVXiDO/mAnqMzqma26IYnIm2mk3TlupYJDGrdL908ZKA==", + "license": "MIT" + }, + "node_modules/@discordjs/builders/node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, "node_modules/@discordjs/collection": { "version": "1.5.3", "resolved": "https://registry.npmjs.org/@discordjs/collection/-/collection-1.5.3.tgz", "integrity": "sha512-SVb428OMd3WO1paV3rm6tSjM4wC+Kecaa1EUGX7vc6/fddvw/6lg90z4QtCqm21zvVe92vMMDt9+DkIvjXImQQ==", + "license": "Apache-2.0", "engines": { "node": ">=16.11.0" } }, "node_modules/@discordjs/formatters": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/@discordjs/formatters/-/formatters-0.3.2.tgz", - "integrity": "sha512-lE++JZK8LSSDRM5nLjhuvWhGuKiXqu+JZ/DsOR89DVVia3z9fdCJVcHF2W/1Zxgq0re7kCzmAJlCMMX3tetKpA==", + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/@discordjs/formatters/-/formatters-0.3.3.tgz", + "integrity": "sha512-wTcI1Q5cps1eSGhl6+6AzzZkBBlVrBdc9IUhJbijRgVjCNIIIZPgqnUj3ntFODsHrdbGU8BEG9XmDQmgEEYn3w==", + "license": "Apache-2.0", "dependencies": { - "discord-api-types": "0.37.50" + "discord-api-types": "0.37.61" }, "engines": { "node": ">=16.11.0" } }, "node_modules/@discordjs/rest": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/@discordjs/rest/-/rest-2.0.1.tgz", - "integrity": "sha512-/eWAdDRvwX/rIE2tuQUmKaxmWeHmGealttIzGzlYfI4+a7y9b6ZoMp8BG/jaohs8D8iEnCNYaZiOFLVFLQb8Zg==", + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/@discordjs/rest/-/rest-2.4.2.tgz", + "integrity": "sha512-9bOvXYLQd5IBg/kKGuEFq3cstVxAMJ6wMxO2U3wjrgO+lHv8oNCT+BBRpuzVQh7BoXKvk/gpajceGvQUiRoJ8g==", + "license": "Apache-2.0", "dependencies": { - "@discordjs/collection": "^1.5.3", - "@discordjs/util": "^1.0.1", - "@sapphire/async-queue": "^1.5.0", - "@sapphire/snowflake": "^3.5.1", - "@vladfrangu/async_event_emitter": "^2.2.2", - "discord-api-types": "0.37.50", - "magic-bytes.js": "^1.0.15", - "tslib": "^2.6.1", - "undici": "5.22.1" + "@discordjs/collection": "^2.1.1", + "@discordjs/util": "^1.1.1", + "@sapphire/async-queue": "^1.5.3", + "@sapphire/snowflake": "^3.5.3", + "@vladfrangu/async_event_emitter": "^2.4.6", + "discord-api-types": "^0.37.114", + "magic-bytes.js": "^1.10.0", + "tslib": "^2.6.3", + "undici": "6.19.8" }, "engines": { - "node": ">=16.11.0" + "node": ">=18" + }, + "funding": { + "url": "https://github.com/discordjs/discord.js?sponsor" + } + }, + "node_modules/@discordjs/rest/node_modules/@discordjs/collection": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@discordjs/collection/-/collection-2.1.1.tgz", + "integrity": "sha512-LiSusze9Tc7qF03sLCujF5iZp7K+vRNEDBZ86FT9aQAv3vxMLihUvKvpsCWiQ2DJq1tVckopKm1rxomgNUc9hg==", + "license": "Apache-2.0", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/discordjs/discord.js?sponsor" + } + }, + "node_modules/@discordjs/rest/node_modules/@sapphire/snowflake": { + "version": "3.5.5", + "resolved": "https://registry.npmjs.org/@sapphire/snowflake/-/snowflake-3.5.5.tgz", + "integrity": "sha512-xzvBr1Q1c4lCe7i6sRnrofxeO1QTP/LKQ6A6qy0iB4x5yfiSfARMEQEghojzTNALDTcv8En04qYNIco9/K9eZQ==", + "license": "MIT", + "engines": { + "node": ">=v14.0.0", + "npm": ">=7.0.0" + } + }, + "node_modules/@discordjs/rest/node_modules/discord-api-types": { + "version": "0.37.117", + "resolved": "https://registry.npmjs.org/discord-api-types/-/discord-api-types-0.37.117.tgz", + "integrity": "sha512-d+Z6RKd7v3q22lsil7yASucqMfVVV0s0XSqu3cw7kyHVXiDO/mAnqMzqma26IYnIm2mk3TlupYJDGrdL908ZKA==", + "license": "MIT" + }, + "node_modules/@discordjs/rest/node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/@discordjs/rest/node_modules/undici": { + "version": "6.19.8", + "resolved": "https://registry.npmjs.org/undici/-/undici-6.19.8.tgz", + "integrity": "sha512-U8uCCl2x9TK3WANvmBavymRzxbfFYG+tAu+fgx3zxQy3qdagQqBLwJVrdyO1TBfUXvfKveMKJZhpvUYoOjM+4g==", + "license": "MIT", + "engines": { + "node": ">=18.17" } }, "node_modules/@discordjs/util": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@discordjs/util/-/util-1.0.1.tgz", - "integrity": "sha512-d0N2yCxB8r4bn00/hvFZwM7goDcUhtViC5un4hPj73Ba4yrChLSJD8fy7Ps5jpTLg1fE9n4K0xBLc1y9WGwSsA==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@discordjs/util/-/util-1.1.1.tgz", + "integrity": "sha512-eddz6UnOBEB1oITPinyrB2Pttej49M9FZQY8NxgEvc3tq6ZICZ19m70RsmzRdDHk80O9NoYN/25AqJl8vPVf/g==", + "license": "Apache-2.0", "engines": { - "node": ">=16.11.0" + "node": ">=18" + }, + "funding": { + "url": "https://github.com/discordjs/discord.js?sponsor" } }, "node_modules/@discordjs/ws": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@discordjs/ws/-/ws-1.0.1.tgz", - "integrity": "sha512-avvAolBqN3yrSvdBPcJ/0j2g42ABzrv3PEL76e3YTp2WYMGH7cuspkjfSyNWaqYl1J+669dlLp+YFMxSVQyS5g==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@discordjs/ws/-/ws-1.2.0.tgz", + "integrity": "sha512-QH5CAFe3wHDiedbO+EI3OOiyipwWd+Q6BdoFZUw/Wf2fw5Cv2fgU/9UEtJRmJa9RecI+TAhdGPadMaEIur5yJg==", + "license": "Apache-2.0", "dependencies": { - "@discordjs/collection": "^1.5.3", - "@discordjs/rest": "^2.0.1", - "@discordjs/util": "^1.0.1", - "@sapphire/async-queue": "^1.5.0", - "@types/ws": "^8.5.5", - "@vladfrangu/async_event_emitter": "^2.2.2", - "discord-api-types": "0.37.50", - "tslib": "^2.6.1", - "ws": "^8.13.0" + "@discordjs/collection": "^2.1.0", + "@discordjs/rest": "^2.4.1", + "@discordjs/util": "^1.1.0", + "@sapphire/async-queue": "^1.5.2", + "@types/ws": "^8.5.10", + "@vladfrangu/async_event_emitter": "^2.2.4", + "discord-api-types": "^0.37.114", + "tslib": "^2.6.2", + "ws": "^8.17.0" }, "engines": { "node": ">=16.11.0" + }, + "funding": { + "url": "https://github.com/discordjs/discord.js?sponsor" + } + }, + "node_modules/@discordjs/ws/node_modules/@discordjs/collection": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@discordjs/collection/-/collection-2.1.1.tgz", + "integrity": "sha512-LiSusze9Tc7qF03sLCujF5iZp7K+vRNEDBZ86FT9aQAv3vxMLihUvKvpsCWiQ2DJq1tVckopKm1rxomgNUc9hg==", + "license": "Apache-2.0", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/discordjs/discord.js?sponsor" + } + }, + "node_modules/@discordjs/ws/node_modules/@types/ws": { + "version": "8.5.14", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.14.tgz", + "integrity": "sha512-bd/YFLW+URhBzMXurx7lWByOu+xzU9+kb3RboOteXYDfW+tr+JZa99OyNmPINEGB/ahzKrEuc8rcv4gnpJmxTw==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@discordjs/ws/node_modules/discord-api-types": { + "version": "0.37.117", + "resolved": "https://registry.npmjs.org/discord-api-types/-/discord-api-types-0.37.117.tgz", + "integrity": "sha512-d+Z6RKd7v3q22lsil7yASucqMfVVV0s0XSqu3cw7kyHVXiDO/mAnqMzqma26IYnIm2mk3TlupYJDGrdL908ZKA==", + "license": "MIT" + }, + "node_modules/@discordjs/ws/node_modules/ws": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz", + "integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/@fastify/busboy": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@fastify/busboy/-/busboy-2.1.1.tgz", + "integrity": "sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA==", + "license": "MIT", + "engines": { + "node": ">=14" } }, "node_modules/@gar/promisify": { @@ -151,31 +297,33 @@ } }, "node_modules/@sapphire/async-queue": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/@sapphire/async-queue/-/async-queue-1.5.0.tgz", - "integrity": "sha512-JkLdIsP8fPAdh9ZZjrbHWR/+mZj0wvKS5ICibcLrRI1j84UmLMshx5n9QmL8b95d4onJ2xxiyugTgSAX7AalmA==", + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@sapphire/async-queue/-/async-queue-1.5.5.tgz", + "integrity": "sha512-cvGzxbba6sav2zZkH8GPf2oGk9yYoD5qrNWdu9fRehifgnFZJMV+nuy2nON2roRO4yQQ+v7MK/Pktl/HgfsUXg==", + "license": "MIT", "engines": { "node": ">=v14.0.0", "npm": ">=7.0.0" } }, "node_modules/@sapphire/shapeshift": { - "version": "3.9.2", - "resolved": "https://registry.npmjs.org/@sapphire/shapeshift/-/shapeshift-3.9.2.tgz", - "integrity": "sha512-YRbCXWy969oGIdqR/wha62eX8GNHsvyYi0Rfd4rNW6tSVVa8p0ELiMEuOH/k8rgtvRoM+EMV7Csqz77YdwiDpA==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@sapphire/shapeshift/-/shapeshift-4.0.0.tgz", + "integrity": "sha512-d9dUmWVA7MMiKobL3VpLF8P2aeanRTu6ypG2OIaEv/ZHH/SUQ2iHOVyi5wAPjQ+HmnMuL0whK9ez8I/raWbtIg==", + "license": "MIT", "dependencies": { "fast-deep-equal": "^3.1.3", "lodash": "^4.17.21" }, "engines": { - "node": ">=v14.0.0", - "npm": ">=7.0.0" + "node": ">=v16" } }, "node_modules/@sapphire/snowflake": { "version": "3.5.1", "resolved": "https://registry.npmjs.org/@sapphire/snowflake/-/snowflake-3.5.1.tgz", "integrity": "sha512-BxcYGzgEsdlG0dKAyOm0ehLGm2CafIrfQTZGWgkfKYbj+pNNsorZ7EotuZukc2MT70E0UbppVbtpBrqpzVzjNA==", + "license": "MIT", "engines": { "node": ">=v14.0.0", "npm": ">=7.0.0" @@ -196,22 +344,28 @@ "integrity": "sha512-Nmh0K3iWQJzniTuPRcJn5hxXkfB1T1pgB89SBig5PlJQU5yocazeu4jATJlaA0GYFKWMqDdvYemoSnF2pXgLVA==" }, "node_modules/@types/node": { - "version": "20.5.7", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.5.7.tgz", - "integrity": "sha512-dP7f3LdZIysZnmvP3ANJYTSwg+wLLl8p7RqniVlV7j+oXSXAbt9h0WIBFmJy5inWZoX9wZN6eXx+YXd9Rh3RBA==" + "version": "22.10.10", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.10.tgz", + "integrity": "sha512-X47y/mPNzxviAGY5TcYPtYL8JsY3kAq2n8fMmKoRCxq/c4v4pyGNCzM2R6+M5/umG4ZfHuT+sgqDYqWc9rJ6ww==", + "license": "MIT", + "dependencies": { + "undici-types": "~6.20.0" + } }, "node_modules/@types/ws": { - "version": "8.5.5", - "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.5.tgz", - "integrity": "sha512-lwhs8hktwxSjf9UaZ9tG5M03PGogvFaH8gUgLNbN9HKIg0dvv6q+gkSuJ8HN4/VbyxkuLzCjlN7GquQ0gUJfIg==", + "version": "8.5.9", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.9.tgz", + "integrity": "sha512-jbdrY0a8lxfdTp/+r7Z4CkycbOFN8WX+IOchLJr3juT/xzbJ8URyTVSJ/hvNdadTgM1mnedb47n+Y31GsFnQlg==", + "license": "MIT", "dependencies": { "@types/node": "*" } }, "node_modules/@vladfrangu/async_event_emitter": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/@vladfrangu/async_event_emitter/-/async_event_emitter-2.2.2.tgz", - "integrity": "sha512-HIzRG7sy88UZjBJamssEczH5q7t5+axva19UbZLO6u0ySbYPrwzWiXBcC0WuHyhKKoeCyneH+FvYzKQq/zTtkQ==", + "version": "2.4.6", + "resolved": "https://registry.npmjs.org/@vladfrangu/async_event_emitter/-/async_event_emitter-2.4.6.tgz", + "integrity": "sha512-RaI5qZo6D2CVS6sTHFKg1v5Ohq/+Bo2LZ5gzUEwZ/WkHhwtGTCB/sVLw8ijOkAUxasZ+WshN/Rzj4ywsABJ5ZA==", + "license": "MIT", "engines": { "node": ">=v14.0.0", "npm": ">=7.0.0" @@ -350,17 +504,6 @@ "node": ">=10.0.0" } }, - "node_modules/busboy": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz", - "integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==", - "dependencies": { - "streamsearch": "^1.1.0" - }, - "engines": { - "node": ">=10.16.0" - } - }, "node_modules/cacache": { "version": "15.3.0", "resolved": "https://registry.npmjs.org/cacache/-/cacache-15.3.0.tgz", @@ -419,6 +562,7 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/colors/-/colors-1.4.0.tgz", "integrity": "sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA==", + "license": "MIT", "engines": { "node": ">=0.1.90" } @@ -512,34 +656,48 @@ } }, "node_modules/discord-api-types": { - "version": "0.37.50", - "resolved": "https://registry.npmjs.org/discord-api-types/-/discord-api-types-0.37.50.tgz", - "integrity": "sha512-X4CDiMnDbA3s3RaUXWXmgAIbY1uxab3fqe3qwzg5XutR3wjqi7M3IkgQbsIBzpqBN2YWr/Qdv7JrFRqSgb4TFg==" + "version": "0.37.61", + "resolved": "https://registry.npmjs.org/discord-api-types/-/discord-api-types-0.37.61.tgz", + "integrity": "sha512-o/dXNFfhBpYHpQFdT6FWzeO7pKc838QeeZ9d91CfVAtpr5XLK4B/zYxQbYgPdoMiTDvJfzcsLW5naXgmHGDNXw==", + "license": "MIT" }, "node_modules/discord.js": { - "version": "14.13.0", - "resolved": "https://registry.npmjs.org/discord.js/-/discord.js-14.13.0.tgz", - "integrity": "sha512-Kufdvg7fpyTEwANGy9x7i4od4yu5c6gVddGi5CKm4Y5a6sF0VBODObI3o0Bh7TGCj0LfNT8Qp8z04wnLFzgnbA==", + "version": "14.14.1", + "resolved": "https://registry.npmjs.org/discord.js/-/discord.js-14.14.1.tgz", + "integrity": "sha512-/hUVzkIerxKHyRKopJy5xejp4MYKDPTszAnpYxzVVv4qJYf+Tkt+jnT2N29PIPschicaEEpXwF2ARrTYHYwQ5w==", + "license": "Apache-2.0", "dependencies": { - "@discordjs/builders": "^1.6.5", - "@discordjs/collection": "^1.5.3", - "@discordjs/formatters": "^0.3.2", - "@discordjs/rest": "^2.0.1", - "@discordjs/util": "^1.0.1", - "@discordjs/ws": "^1.0.1", - "@sapphire/snowflake": "^3.5.1", - "@types/ws": "^8.5.5", - "discord-api-types": "0.37.50", - "fast-deep-equal": "^3.1.3", - "lodash.snakecase": "^4.1.1", - "tslib": "^2.6.1", - "undici": "5.22.1", - "ws": "^8.13.0" + "@discordjs/builders": "^1.7.0", + "@discordjs/collection": "1.5.3", + "@discordjs/formatters": "^0.3.3", + "@discordjs/rest": "^2.1.0", + "@discordjs/util": "^1.0.2", + "@discordjs/ws": "^1.0.2", + "@sapphire/snowflake": "3.5.1", + "@types/ws": "8.5.9", + "discord-api-types": "0.37.61", + "fast-deep-equal": "3.1.3", + "lodash.snakecase": "4.1.1", + "tslib": "2.6.2", + "undici": "5.27.2", + "ws": "8.14.2" }, "engines": { "node": ">=16.11.0" } }, + "node_modules/dotenv": { + "version": "16.4.7", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.7.tgz", + "integrity": "sha512-47qPchRCykZC03FhkYAhrvwU4xDBFIj1QPqaarj6mdM/hgUzfPHcpkHJOn3mJAufFeeAxAzeGsr5X0M4k6fLZQ==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, "node_modules/emoji-regex": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", @@ -583,7 +741,8 @@ "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "license": "MIT" }, "node_modules/follow-redirects": { "version": "1.15.3", @@ -621,6 +780,7 @@ "version": "0.1.1", "resolved": "https://registry.npmjs.org/freepbx-graphql-client/-/freepbx-graphql-client-0.1.1.tgz", "integrity": "sha512-JqDTlL0EA/bUMit9aODIupWSqF87WHWrCD6i716FzeOzS46cMFq/OajzftTMwOZQf20MMkJM2HI6CRnNlRGl6A==", + "license": "MIT", "dependencies": { "graphql": "^15.6.1", "graphql-request": "^3.6.1" @@ -831,7 +991,8 @@ "node_modules/lodash": { "version": "4.17.21", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "license": "MIT" }, "node_modules/lodash.snakecase": { "version": "4.1.1", @@ -850,9 +1011,10 @@ } }, "node_modules/magic-bytes.js": { - "version": "1.0.15", - "resolved": "https://registry.npmjs.org/magic-bytes.js/-/magic-bytes.js-1.0.15.tgz", - "integrity": "sha512-bpRmwbRHqongRhA+mXzbLWjVy7ylqmfMBYaQkSs6pac0z6hBTvsgrH0r4FBYd/UYVJBmS6Rp/O+oCCQVLzKV1g==" + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/magic-bytes.js/-/magic-bytes.js-1.10.0.tgz", + "integrity": "sha512-/k20Lg2q8LE5xiaaSkMXk4sfvI+9EGEykFS4b0CHHGWqDYU0bGUFSwchNOMA56D7TCs9GwVTkqe9als1/ns8UQ==", + "license": "MIT" }, "node_modules/make-dir": { "version": "3.1.0", @@ -1444,14 +1606,6 @@ "node": ">= 8" } }, - "node_modules/streamsearch": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz", - "integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==", - "engines": { - "node": ">=10.0.0" - } - }, "node_modules/string_decoder": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", @@ -1514,14 +1668,16 @@ "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" }, "node_modules/ts-mixer": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/ts-mixer/-/ts-mixer-6.0.3.tgz", - "integrity": "sha512-k43M7uCG1AkTyxgnmI5MPwKoUvS/bRvLvUb7+Pgpdlmok8AoqmUaZxUUw8zKM5B1lqZrt41GjYgnvAi0fppqgQ==" + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/ts-mixer/-/ts-mixer-6.0.4.tgz", + "integrity": "sha512-ufKpbmrugz5Aou4wcr5Wc1UUFWOLhq+Fm6qa6P0w0K5Qw2yhaUoiWszhCVuNQyNwrlGiscHOmqYoAox1PtvgjA==", + "license": "MIT" }, "node_modules/tslib": { "version": "2.6.2", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", - "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==", + "license": "0BSD" }, "node_modules/tweetnacl": { "version": "0.14.5", @@ -1529,16 +1685,23 @@ "integrity": "sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA==" }, "node_modules/undici": { - "version": "5.22.1", - "resolved": "https://registry.npmjs.org/undici/-/undici-5.22.1.tgz", - "integrity": "sha512-Ji2IJhFXZY0x/0tVBXeQwgPlLWw13GVzpsWPQ3rV50IFMMof2I55PZZxtm4P6iNq+L5znYN9nSTAq0ZyE6lSJw==", + "version": "5.27.2", + "resolved": "https://registry.npmjs.org/undici/-/undici-5.27.2.tgz", + "integrity": "sha512-iS857PdOEy/y3wlM3yRp+6SNQQ6xU0mmZcwRSriqk+et/cwWAtwmIGf6WkoDN2EK/AMdCO/dfXzIwi+rFMrjjQ==", + "license": "MIT", "dependencies": { - "busboy": "^1.6.0" + "@fastify/busboy": "^2.0.0" }, "engines": { "node": ">=14.0" } }, + "node_modules/undici-types": { + "version": "6.20.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", + "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", + "license": "MIT" + }, "node_modules/unique-filename": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-1.1.1.tgz", @@ -1605,9 +1768,10 @@ "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" }, "node_modules/ws": { - "version": "8.13.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.13.0.tgz", - "integrity": "sha512-x9vcZYTrFPC7aSIbj7sRCYo7L/Xb8Iy+pW0ng0wt2vCJv7M9HOMy0UoN3rr+IFC7hb7vXoqS+P9ktyLLLhO+LA==", + "version": "8.14.2", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.14.2.tgz", + "integrity": "sha512-wEBG1ftX4jcglPxgFCMJmZ2PLtSbJ2Peg6TmpJFTbe9GZYOQCDPdMYu/Tm0/bGZkw8paZnJY45J4K2PZrLYq8g==", + "license": "MIT", "engines": { "node": ">=10.0.0" }, diff --git a/package.json b/package.json index 68543f7..d3b61a2 100644 --- a/package.json +++ b/package.json @@ -12,6 +12,7 @@ "axios": "^1.6.0", "colors": "^1.4.0", "discord.js": "14.14.1", + "dotenv": "^16.4.7", "freepbx-graphql-client": "^0.1.1", "mariadb": "^3.2.0", "ping": "^0.4.4", diff --git a/pageGroups.json.default b/pageGroups.json.default deleted file mode 100644 index 395a430..0000000 --- a/pageGroups.json.default +++ /dev/null @@ -1,6 +0,0 @@ -[ - { - "name": "Test", - "value": "700" - } -] \ No newline at end of file