diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..e5d0e67 --- /dev/null +++ b/.env.example @@ -0,0 +1,9 @@ +PBX_HOSTNAME=what-gets-shown-to-users +FREEPBX_URL=http://your-pbx.local +FREEPBX_CLIENT_ID= +FREEPBX_CLIENT_SECRET= +DB_HOST=your-pbx.local +DB_USER=user +DB_PASS=pass +DISCORD_TOKEN=your-discord-token +OWNER_ID=your-discord-id \ No newline at end of file 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..82835c2 --- /dev/null +++ b/TODO.md @@ -0,0 +1,52 @@ +# 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. +- [X] **/list** - List all extensions on the LiteNet Phone System. +- [X] **/button** - Send the "Get an extension" button! *(Requires default_member_permissions: 0)* + +## Admin Commands +*(Requires default_member_permissions: 0)* +- [X] **/admin** + - [X] **silence** - Kill all ongoing calls. + - [X] **reload** - Run an Asterisk reload. + - [X] **reboot** - Reboot the server. *(LAST RESORT)* + +## Developer Commands +*(Requires default_member_permissions: 0)* +- [X] **/dev** + - [X] **fwconsole** - Run an `fwconsole` command. + - [X] **command** (required) - The command to run. + - [X] **asterisk** - Run an Asterisk CLI command. + - [X] **command** (required) - The command to run. + - [X] **shell** - Run a shell command. + - [X] **command** (required) - The command to run. + - [X] **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). + +## Management Commands (For admins to manage extensions) +*(Requires default_member_permissions: 0)* +- [X] **/manage** + - [X] **create** - Create an extension. + - [X] **extension** (required) - The extension number. + - [X] **discord user** (required) - The Discord user to assign the extension to. + - [X] **delete** - Delete an extension. + - [X] **discord user** (required) - The Discord user whose extension is to be deleted. + - [X] **lookup** - Lookup an extension. (With creds) + - [X] **extension** (required) - The extension number. + + + +## Buttons (On button message) +- [X] **Get an Extension** - Sends the "Get an extension" button. +- [X] **See Your Info** - Sends the "See your info" button. +- [X] **Delete Your Extension** - Sends the "Delete your extension" button. + +## Other Features +- [ ] **Extension List on Discord** - List all extensions on the Discord server. (Will do later) \ 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 60% rename from commands.json rename to commands.js index 276987f..3cfd34a 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,82 @@ ] }, { - "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": "whois", + "description": "Find Discord user by extension", + "type": 1, + "options": [ + { + "name": "extension", + "description": "The extension to lookup", + "type": 4, + "required": true + } + ] }, { - "name": "Delete Extension", - "type": 2, - "default_member_permissions": 0 + "name": "manage", + "description": "Manage extensions", + "type": 1, + "default_member_permissions": 0, + "options": [ + { + "name": "create", + "description": "Create an extension", + "type": 1, + "options": [ + { + "name": "user", + "description": "The Discord user to assign the extension to", + "type": 6, + "required": true + }, + { + "name": "extension", + "description": "The extension number", + "type": 4, + "required": false + } + ] + }, + { + "name": "delete", + "description": "Delete an extension", + "type": 1, + "options": [ + { + "name": "user", + "description": "The Discord user whose extension is to be deleted", + "type": 6, + "required": true + } + ] + }, + { + "name": "lookup", + "description": "Lookup an extension", + "type": 1, + "options": [ + { + "name": "user", + "description": "The Discord user whose extension is to be deleted", + "type": 6, + "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..e0d3d61 100644 --- a/index.js +++ b/index.js @@ -1,1707 +1,136 @@ -//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 +require("dotenv").config(); + 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'); - -// 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); - }); - }); -} - - -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) => { +client.on('ready', async () => { + log.success(`Logged in as ${client.user.displayName}`); + 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 => { + if (!interaction.inGuild()) return interaction.reply({ content: "This bot is not designed to be used in DMs.", ephemeral: true }); + 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); + try { + await component.execute(interaction); + } catch (error) { + log.error(error); + await interaction.reply({ content: 'There was an error while executing this component!', ephemeral: true }); + } + break; + } +}); - // 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}`); - }); - }); +if (fs.existsSync("./import.json")) { + const importData = JSON.parse(fs.readFileSync("./import.json", "utf8")); - }); - }); - }) - - // 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 insertData = async () => { + try { 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 - }); - } - }); - }) + for (const ext in importData) { + const discordId = importData[ext]; + await conn.query("INSERT INTO discord_users (extension, discordId) VALUES (?, ?)", [ext, discordId]); } - }) - - }); - - 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}`); - }); + conn.release(); + log.success("Data imported successfully."); + } catch (error) { + log.error(`Failed to import data: ${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; + insertData(); +} - 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; - } - } -}); - -// 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' - } - }); -}); - -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/admin.js b/interactionHandlers/commands/admin.js new file mode 100644 index 0000000..46f3928 --- /dev/null +++ b/interactionHandlers/commands/admin.js @@ -0,0 +1,68 @@ +const { exec } = require('child_process'); +const Discord = require('discord.js'); +const pool = global.pool +const fpbx = global.fpbx +const client = global.client +const log = global.log + + +const runCommand = (command, onData) => { + return new Promise((resolve, reject) => { + const process = exec(command); + + process.stdout.on('data', (data) => { + if (onData) { + onData(data); + } + }); + + process.stderr.on('data', (data) => { + reject(`stderr: ${data}`); + }); + + process.on('close', (code) => { + if (code !== 0) { + reject(`process exited with code ${code}`); + return; + } + resolve('Command executed successfully'); + }); + + process.on('error', (error) => { + reject(`error: ${error.message}`); + }); + }); +} + +module.exports = {}; + +module.exports.execute = async (interaction) => { + const subcommand = interaction.options.getSubcommand(); + + switch (subcommand) { + case 'silence': // Run `asterisk -x "channel request hangup all" + runCommand('asterisk -x "channel request hangup all"').then((res) => { + interaction.reply({ content: `Silenced`, ephemeral: true }); + }); + break; + case 'reload': // Run `fwconsole reload` + await interaction.deferReply({ ephemeral: true }); + runCommand('fwconsole reload', (data) => { + interaction.editReply({ content: data, ephemeral: true }); + }); + break; + case 'reboot': // Run `reboot 0` + await interaction.reply({ content: "Rebooting...", ephemeral: true }); + await client.destroy(); + log.info('Client destroyed.'); + pool.end((err) => { + if (err) { + log.error('Error closing database pool:', err); + } else { + log.info('Database pool closed.'); + } + }); + runCommand('reboot 0'); + break; + } +} \ No newline at end of file diff --git a/interactionHandlers/commands/button.js b/interactionHandlers/commands/button.js new file mode 100644 index 0000000..35d08b6 --- /dev/null +++ b/interactionHandlers/commands/button.js @@ -0,0 +1,66 @@ +const pool = global.pool +const fpbx = global.fpbx +const client = global.client +const log = global.log +const Discord = require("discord.js") + +module.exports = {}; + +module.exports.execute = async (interaction) => { + interaction.channel.send({ + embeds: [{ + "title": "The LiteNet Community PBX", + "description": "The LiteNet Community PBX is hosted through, and is sponsored by SnakeCraft Hosting!\nOffering affordable game hosting, Discord bot hosting, and VPS services since 2020.\nGet started at https://go.litenet.tel/sch-affiliate\nCheck them out on [Discord](https://discord.gg/invite/xcnKUD8)\n\n-# The link above is an affiliate link. We will receive credits from any purchase made via this link.\n-# SnakeCraft Hosting has no administrative control over, nor has access to private information stored on LiteNet. SnakeCraft Hosting provides hosting for LiteNet free of charge. Specific details regarding their affiliate program can be found [here](https://my.snakecrafthosting.com/index.php?rp=/knowledgebase/4/Affiliate-Program-FAQs.html)", + "color": 7955428, + "fields": [ + { + "name": "What's this?", + "value": "The community PBX is a public, free to use [FreePBX](https://freepbx.org) based phone system that any server member is welcome to get a number on!\nEveryone on the system has their own 4 digit number, that can be used to call between other members on the system.\nThe PBX runs on a SnakeCraft Hosting VPS graciously provided to us at no cost!" + }, + { + "name": "What can it do?", + "value": "The LiteNet phone system offers many features, including but not limited to the following:\n- Free inbound/outbound calling via +1 (610) LITENET (548-3638)\n- Private Voicemail\n- Intercom/Paging\n- Conference Rooms\n- Direct dial access to [AstroCom](https://astrocom.tel)\n- [Full extension status page](https://pbx.litenet.tel/status)\n- And more!" + }, + { + "name": "Privacy Policy", + "value": "LiteNet respects the privacy of all members, and as such, only very few select staff members have access to the system files directly. Voicemails are not tracked nor listened to under any circumstances. Call logs are kept and only reviewed during investigations into violations of community guidelines, or possible illegal activity. Call recordings may be kept at the request of any individual member, and will NOT be reviewed unless prior permission was given from said member.\nAll user data may be deleted by request, or by simply running the `/delete` command.\n\nIf you believe your privacy has been violated in any way, please don't hesistate to reach out to any of our staff members!" + }, + { + "name": "Can it do `X`?", + "value": "Any specific questions are welcome to be asked in our <#1102782499756724239> chat!\nIf you have a suggestion for something we should add to the PBX or Discord server, feel free to leave it in <#1148099609428762634>!" + } + ], + "footer": { + "text": "Made with <3 by Chris Chrome & The LiteNet Team • Sponsored by SnakeCraft Hosting", + "icon_url": "https://f.chrischro.me/assets/Snakecraft-Social-Media-purple-v2-smaller.png" + }, + "image": { + "url": "https://f.chrischro.me/assets/litenet-full.png" + }, + "thumbnail": { + "url": "https://f.chrischro.me/assets/Snakecraft-Social-Media-purple-v2-smaller-rounder.png" + } + }], + components: [ + { + type: 1, + components: [ + { + type: Discord.ComponentType.Button, + label: "Get an Extension", + emoji: "✅", + style: Discord.ButtonStyle.Success, + custom_id: "newExtension" + }, + { + type: Discord.ComponentType.Button, + label: "Get your extension info", + emoji: "ℹ️", + style: Discord.ButtonStyle.Primary, + custom_id: "getExtensionInfo" + } + ] + } + ] + }) +} \ 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/dev.js b/interactionHandlers/commands/dev.js new file mode 100644 index 0000000..d088950 --- /dev/null +++ b/interactionHandlers/commands/dev.js @@ -0,0 +1,111 @@ +const { exec } = require('child_process'); +const Discord = require('discord.js'); +const { on } = require('events'); +const pool = global.pool +const fpbx = global.fpbx +const client = global.client +const log = global.log + + +const runCommand = (command, onData) => { + try { + return new Promise((resolve, reject) => { + const process = exec(command); + let output = ''; + + const timeout = setTimeout(() => { + process.kill(); + resolve({ output: output, code: 1 }); + }, 60000); + + process.stdout.on('data', (data) => { + output += data; + if (onData) { + onData(data); + } + }); + + process.stderr.on('data', (data) => { + onData(data); + }); + + process.on('close', (code) => { + clearTimeout(timeout); + if (code !== 0) { + resolve({ output: output, code: code }); + } + resolve({ output: output, code: code }); + }); + + process.on('error', (error) => { + reject(`error: ${error.message}`); + }); + }); + } catch (err) { + log.error(err) + } +} + +module.exports = {}; + +module.exports.execute = async (interaction) => { + if (interaction.user.id !== process.env.OWNER_ID) { + await interaction.reply({ content: "You do not have permission to run this command.", ephemeral: true }); + return; + } + const subcommand = interaction.options.getSubcommand(); + + switch (subcommand) { + case 'fwconsole': // Run an arbitrary fwconsole command + const command = interaction.options.getString('command'); + await interaction.deferReply({ ephemeral: true }); + var output = ''; + runCommand(`fwconsole ${command}`, (data) => { + output += data; + if (output.length >= 1500) { + output = output.substring(output.length - 1500); + } + interaction.editReply({ content: `\`\`\`ansi\n${output}\`\`\``, ephemeral: true }); + }).then((fullOutput) => { + output = output.length > 1500 ? output.substring(output.length - 1500) : output; + const buffer = Buffer.from(fullOutput.output, 'utf-8'); + const attachment = new Discord.AttachmentBuilder(buffer, {name: 'output.txt'}); + interaction.editReply({ content: `\`\`\`ansi\n${output}\`\`\`\nProcess returned code ${fullOutput.code}`, files: [attachment], ephemeral: true }); + }) + break; + case 'asterisk': // Run arbitrary asterisk command with asterisk -x "command" + const asteriskCommand = interaction.options.getString('command'); + await interaction.deferReply({ ephemeral: true }); + var output = ''; + runCommand(`asterisk -x "${asteriskCommand}"`, (data) => { + output += data; + if (output.length >= 1500) { + output = output.substring(output.length - 1500); + } + interaction.editReply({ content: `\`\`\`ansi\n${output}\`\`\``, ephemeral: true }); + }).then((fullOutput) => { + output = output.length > 1500 ? output.substring(output.length - 1500) : output; + const buffer = Buffer.from(fullOutput.output, 'utf-8'); + const attachment = new Discord.AttachmentBuilder(buffer, {name: 'output.txt'}); + interaction.editReply({ content: `\`\`\`ansi\n${output}\`\`\`\nProcess returned code ${fullOutput.code}`, files: [attachment], ephemeral: true }); + }) + break; + case 'shell': // Run any arbitrary shell command + const shellCommand = interaction.options.getString('command'); + await interaction.deferReply({ ephemeral: true }); + var output = ''; + runCommand(shellCommand, (data) => { + output += data; + if (output.length >= 1500) { + output = output.substring(output.length - 1500); + } + interaction.editReply({ content: `\`\`\`ansi\n${output}\`\`\``, ephemeral: true }); + }).then((fullOutput) => { + output = output.length > 1500 ? output.substring(output.length - 1500) : output; + const buffer = Buffer.from(fullOutput.output, 'utf-8'); + const attachment = new Discord.AttachmentBuilder(buffer, {name: 'output.txt'}); + interaction.editReply({ content: `Process returned code ${fullOutput.code}`, files: [attachment], ephemeral: true }); + }) + break; + } +} \ 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/manage.js b/interactionHandlers/commands/manage.js new file mode 100644 index 0000000..e2e73aa --- /dev/null +++ b/interactionHandlers/commands/manage.js @@ -0,0 +1,83 @@ +const pool = global.pool +const fpbx = global.fpbx +const client = global.client +const log = global.log + +module.exports = {}; + +module.exports.execute = async (interaction) => { + const subcommand = interaction.options.getSubcommand(); + + switch (subcommand) { + case 'create': // Create an extension for a user + await interaction.deferReply({ ephemeral: true }); + var forUser = interaction.options.getUser('user'); + var newExt = interaction.options.getInteger('extension') ? interaction.options.getInteger('extension') : await fpbx.getNextAvailableExtension(); + var [lookup] = await pool.query('SELECT * FROM discord_users WHERE discordId = ?', [forUser.id]); + if (lookup) { + await interaction.editReply({ content: `User already has an extension, it's ${lookup.extension}!`, ephemeral: true }); + return; + } + await interaction.editReply({ content: `Creating extension ${newExt} for ${forUser.username}`, ephemeral: true }); + fpbx.addExtension(newExt, forUser.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 (?, ?)', [forUser.id, newExt]); + await interaction.editReply({ content: `Extension ${newExt} created! Getting info..`, ephemeral: true }); + await fpbx.reload(); + const extInfo = await fpbx.getExtension(newExt); + await interaction.editReply({ embeds: [{ + title: "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 the extension!', ephemeral: true }); + }); + break; + case "delete": // Delete an extension for a user + await interaction.deferReply({ ephemeral: true }); + var forUser = interaction.options.getUser('user'); + var [lookup] = await pool.query('SELECT * FROM discord_users WHERE discordId = ?', [forUser.id]); + if (!lookup) { + await interaction.editReply({ content: `User does not have an extension!`, ephemeral: true }); + return; + } + await interaction.editReply({ content: `Deleting extension ${lookup.extension} for ${forUser.username}`, ephemeral: true }); + fpbx.deleteExtension(lookup.extension).then(async (res) => { + console.log(res) + if (res[0].deleteExtension.status != true) { + await interaction.editReply({ content: `Something went wrong :(`, ephemeral: true }); + return; + } + await pool.query('DELETE FROM discord_users WHERE discordId = ?', [forUser.id]); + await fpbx.reload(); + await interaction.editReply({ content: `Extension ${lookup.extension} deleted!`, ephemeral: true }); + }).catch(async (error) => { + log.error(error); + await interaction.editReply({ content: 'There was an error while deleting the extension!', ephemeral: true }); + }); + break; + case "lookup": // Get user extension info + var forUser = interaction.options.getUser('user'); + var [lookup] = await pool.query('SELECT * FROM discord_users WHERE discordId = ?', [forUser.id]); + if (!lookup) { + await interaction.reply({ content: `User does not have an extension!`, ephemeral: true }); + return; + } + const extInfo = await fpbx.getExtension(lookup.extension); + return await interaction.reply({ + ephemeral: true, embeds: [ + { + title: "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 + } + ] + }); + break; + } +} \ No newline at end of file diff --git a/interactionHandlers/commands/new.js b/interactionHandlers/commands/new.js new file mode 100644 index 0000000..5552d3e --- /dev/null +++ b/interactionHandlers/commands/new.js @@ -0,0 +1,3 @@ +module.exports = {}; + +module.exports.execute = require("../common/createExt").execute; \ No newline at end of file diff --git a/interactionHandlers/commands/whoami.js b/interactionHandlers/commands/whoami.js new file mode 100644 index 0000000..e76d000 --- /dev/null +++ b/interactionHandlers/commands/whoami.js @@ -0,0 +1,3 @@ +module.exports = {}; + +module.exports.execute = require("../common/getExtInfo").execute; \ 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/interactionHandlers/common/createExt.js b/interactionHandlers/common/createExt.js new file mode 100644 index 0000000..90123ea --- /dev/null +++ b/interactionHandlers/common/createExt.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/common/getExtInfo.js b/interactionHandlers/common/getExtInfo.js new file mode 100644 index 0000000..3c7ae18 --- /dev/null +++ b/interactionHandlers/common/getExtInfo.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/Username:** \`${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/components/getExtensionInfo.js b/interactionHandlers/components/getExtensionInfo.js new file mode 100644 index 0000000..e76d000 --- /dev/null +++ b/interactionHandlers/components/getExtensionInfo.js @@ -0,0 +1,3 @@ +module.exports = {}; + +module.exports.execute = require("../common/getExtInfo").execute; \ No newline at end of file diff --git a/interactionHandlers/components/newExtension.js b/interactionHandlers/components/newExtension.js new file mode 100644 index 0000000..5552d3e --- /dev/null +++ b/interactionHandlers/components/newExtension.js @@ -0,0 +1,3 @@ +module.exports = {}; + +module.exports.execute = require("../common/createExt").execute; \ 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..4f1610e 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,8 @@ "description": "", "main": "index.js", "scripts": { - "test": "echo \"Error: no test specified\" && exit 1" + "test": "echo \"Error: no test specified\" && exit 1", + "start": "node index.js" }, "author": "", "license": "ISC", @@ -12,6 +13,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