From 33d1c8ae9c0b9c579357dbb597e029b8dbfad546 Mon Sep 17 00:00:00 2001 From: ChrisChrome Date: Sat, 25 Jan 2025 07:40:09 -0700 Subject: [PATCH 01/14] Checkpoint; Full rewrite is underway, this shouldn't take long! --- .gitignore | 4 +- TODO.md | 42 + commands.disabled | 13 - commands.json => commands.js | 69 +- config.json.default | 39 - embeds.json.default | 9 - freepbx.js | 185 ++ funcs.js | 97 - index.js | 1777 ++----------------- interactionHandlers/commands/delete.js | 30 + interactionHandlers/commands/list.js | 36 + interactionHandlers/commands/lookup.js | 19 + interactionHandlers/commands/new.js | 40 + interactionHandlers/commands/whoami.js | 25 + interactionHandlers/commands/whois.js | 19 + migrations.js | 72 + migrations/000_alter_users_table.sql | 1 + migrations/001_init_discord_users_table.sql | 4 + package-lock.json | 392 ++-- package.json | 1 + pageGroups.json.default | 6 - 21 files changed, 895 insertions(+), 1985 deletions(-) create mode 100644 TODO.md delete mode 100644 commands.disabled rename commands.json => commands.js (77%) delete mode 100644 config.json.default delete mode 100644 embeds.json.default create mode 100644 freepbx.js delete mode 100644 funcs.js create mode 100644 interactionHandlers/commands/delete.js create mode 100644 interactionHandlers/commands/list.js create mode 100644 interactionHandlers/commands/lookup.js create mode 100644 interactionHandlers/commands/new.js create mode 100644 interactionHandlers/commands/whoami.js create mode 100644 interactionHandlers/commands/whois.js create mode 100644 migrations.js create mode 100644 migrations/000_alter_users_table.sql create mode 100644 migrations/001_init_discord_users_table.sql delete mode 100644 pageGroups.json.default diff --git a/.gitignore b/.gitignore index 98d6135..87edb2c 100644 --- a/.gitignore +++ b/.gitignore @@ -136,4 +136,6 @@ config.json.disabled test.js embeds.json pageGroups.json -.ssh/ \ No newline at end of file +.ssh/ + +old/ \ No newline at end of file diff --git a/TODO.md b/TODO.md new file mode 100644 index 0000000..0b319cc --- /dev/null +++ b/TODO.md @@ -0,0 +1,42 @@ +# Bot Checklist + +## General Commands +- [X] **/whoami** - Get your extension info if you have one. +- [X] **/new** - Get an extension on the LiteNet Phone System. +- [X] **/delete** - Remove your extension from the LiteNet Phone System. +- [ ] **/list** - List all extensions on the LiteNet Phone System. +- [ ] **/button** - Send the "Get an extension" button! *(Requires default_member_permissions: 0)* + +## Admin Commands +*(Requires default_member_permissions: 0)* +- [ ] **/admin** + - [ ] **silence** - Kill all ongoing calls. + - [ ] **reload** - Run an Asterisk reload. + - [ ] **reboot** - Reboot the server. *(LAST RESORT)* + +## Developer Commands +*(Requires default_member_permissions: 0)* +- [ ] **/dev** + - [ ] **fwconsole** - Run an `fwconsole` command. + - [ ] **command** (required) - The command to run. + - [ ] **asterisk** - Run an Asterisk CLI command. + - [ ] **command** (required) - The command to run. + - [ ] **shell** - Run a shell command. + - [ ] **command** (required) - The command to run. + - [ ] **restart** - Restart the bot. + +## Call Detail Records (CDR) +- [ ] **/cdr** - Get the call detail records for your extension. + - [ ] **start_date** (optional) - The start date for the CDR (mm/dd/yyyy). + - [ ] **end_date** (optional) - The end date for the CDR (mm/dd/yyyy). + +## Context Menu Commands +- [ ] **Lookup Extension** - *(Type: 2)* +- [ ] **Create Extension** - *(Type: 2, Requires default_member_permissions: 0)* +- [ ] **Delete Extension** - *(Type: 2, Requires default_member_permissions: 0)* + + +## Buttons (On button message) +- **Get an Extension** - Sends the "Get an extension" button. +- **See Your Info** - Sends the "See your info" button. +- **Delete Your Extension** - Sends the "Delete your extension" button. \ No newline at end of file diff --git a/commands.disabled b/commands.disabled deleted file mode 100644 index 8be6d08..0000000 --- a/commands.disabled +++ /dev/null @@ -1,13 +0,0 @@ -{ - "name": "name", - "description": "Change your extension's name (Defaults to your Discord name)", - "type": 1, - "options": [ - { - "name": "name", - "description": "The new name for your extension", - "type": 3, - "required": false - } - ] - }, \ No newline at end of file diff --git a/commands.json b/commands.js similarity index 77% rename from commands.json rename to commands.js index 276987f..f3f8012 100644 --- a/commands.json +++ b/commands.js @@ -1,4 +1,4 @@ -[ +module.exports = [ { "name": "whoami", "description": "Get your extension info if you have one", @@ -18,13 +18,7 @@ "name": "confirm", "description": "Confirm that you want to delete your extension. THIS CANNOT BE UNDONE!", "type": 5, - "required": true, - "choices": [ - { - "name": "yes", - "value": "yes" - } - ] + "required": true } ] }, @@ -39,19 +33,20 @@ "type": 1, "default_member_permissions": 0 }, - { - "name": "name", - "description": "Change your extension's name (Defaults to your Discord name)", - "type": 1, - "options": [ - { - "name": "name", - "description": "The new name for your extension", - "type": 3, - "required": false - } - ] - }, + // TODO: Find a way to make the name command work again. Sadge + // { + // "name": "name", + // "description": "Change your extension's name (Defaults to your Discord name)", + // "type": 1, + // "options": [ + // { + // "name": "name", + // "description": "The new name for your extension", + // "type": 3, + // "required": false + // } + // ] + // }, { "name": "admin", "description": "Admin only commands", @@ -154,17 +149,29 @@ ] }, { - "name": "Lookup Extension", - "type": 2 + "name": "lookup", + "description": "Find extension by Discord user", + "type": 1, + "options": [ + { + "name": "user", + "description": "The Discord user to lookup", + "type": 6, + "required": true + } + ] }, { - "name": "Create Extension", - "type": 2, - "default_member_permissions": 0 - }, - { - "name": "Delete Extension", - "type": 2, - "default_member_permissions": 0 + "name": "whois", + "description": "Find Discord user by extension", + "type": 1, + "options": [ + { + "name": "extension", + "description": "The extension to lookup", + "type": 4, + "required": true + } + ] } ] \ No newline at end of file diff --git a/config.json.default b/config.json.default deleted file mode 100644 index b68e578..0000000 --- a/config.json.default +++ /dev/null @@ -1,39 +0,0 @@ -{ - "ntfyUrl": "ntfy-url", - "freepbx": { - "server": "sip-server-ip", - "url": "pbx-api-url", - "clientid": "gql-client-id", - "allowedscopes": "gql", - "secret": "gql-secret", - "startExt": 1000 - }, - "discord": { - "token": "bot-token", - "guildId": "guild-id", - "roleId": "user-role", - "logId": "log-channel", - "extList": "extension-list-channel", - "developers": [ - "your-user-id" - ] - }, - "mariadb": { - "host": "db-hostname0here", - "user": "bot", - "password": "bot", - "database": "asterisk", - "connectionLimit": 5 - }, - "cdrdb": { - "host": "db-hostname-here", - "user": "bot", - "password": "bot", - "database": "asteriskcdrdb", - "connectionLimit": 5 - }, - "status": { - "interval": 60, - "url": "uptime-kuma-link" - } -} \ No newline at end of file diff --git a/embeds.json.default b/embeds.json.default deleted file mode 100644 index 66aff81..0000000 --- a/embeds.json.default +++ /dev/null @@ -1,9 +0,0 @@ -{ - "controls": [ - { - "title": "Phone System Controls", - "color": 205442, - "description": "Use the buttons below to control your extension!" - } - ] -} \ No newline at end of file diff --git a/freepbx.js b/freepbx.js new file mode 100644 index 0000000..53be24f --- /dev/null +++ b/freepbx.js @@ -0,0 +1,185 @@ +const { FreepbxGqlClient, gql } = require("freepbx-graphql-client"); + +class FreepbxManager { + /** + * Creates an instance of the FreepbxGqlClient and initializes the connection pool. + * + * @param {Object} config - Configuration object for the FreepbxGqlClient. + * @param {string} config.url - The URL of the FreePBX GraphQL endpoint. + * @param {string} config.clientId - The client ID for authentication. + * @param {string} config.clientSecret - The client secret for authentication. + * @param {Object} config.dbPool - The connection pool for managing database connections. + */ + constructor(config) { + this.client = new FreepbxGqlClient(config.url, { + client: { + id: config.clientId, + secret: config.clientSecret, + }, + }); + this.pool = config.dbPool; + + if (!this.pool) { + throw new Error("Connection pool is required"); + } + + + } + + async getExtension(ext) { + ext = String(ext); + + const query = gql` + query fetchExtension($extensionId: ID!) { + fetchExtension(extensionId: $extensionId) { + user { + extension + name + extPassword + voicemail + } + } + } + `; + + const variables = { + extensionId: ext.match(/\d+/)[0], + }; + + return await this.client.request(query, variables); + } + + async listExtensions() { + const query = gql` + query { + fetchAllExtensions { + extension { + user { + extension + name + } + } + } + } + `; + + return await this.client.request(query); + } + + async addExtension(ext, name) { + ext = String(ext); + name = String(name); + + const query = gql` + mutation addExtension($ext: ID!, $name: String!, $vmPassword: String!) { + addExtension(input: { + extensionId: $ext + name: $name + vmEnable: true + vmPassword: $vmPassword + email: "" + maxContacts: "100" + umEnable: false + }) { + status + } + } + `; + + const variables = { + ext, + name, + vmPassword: ext, + }; + + return await this.client.request(query, variables); + } + + async deleteExtension(ext) { + ext = String(ext); + const query = gql` + mutation deleteExtension($ext: ID!) { + deleteExtension(input: { extensionId: $ext }) { + status + } + } + `; + + const variables = { + ext, + }; + + const fpbxQuery = this.client.request(query, variables); + const dbQuery = this.pool.query('DELETE FROM paging_groups WHERE ext = ?', [ext]); + return await Promise.all([fpbxQuery, dbQuery]); + } + + async reload() { + const query = gql` + mutation { + doreload(input: { clientMutationId: "${Math.random().toString(36).substring(2, 14)}" }) { + status + } + } + `; + + return await this.client.request(query); + } + + // async updateName(ext, name) { + // const query = gql` + // mutation updateName($ext: ID!, $name: String!) { + // updateExtension(input: {extensionId: $ext, name: $name}) { + // status, + // message + // } + // }`; + + // const variables = { + // ext, + // name, + // }; + + // return await this.client.request(query, variables); + // } + // TODO: Implement updateName method, Current implementation resets extension for some reason + + async joinPageGroup(ext, pageGroup) { + const [lookup] = await this.pool.query('SELECT * FROM paging_groups WHERE page_number = ? AND ext = ?', [pageGroup, ext]); + if (lookup) { + return false; + } + + await this.pool.query('INSERT INTO paging_groups (page_number, ext) VALUES (?, ?)', [pageGroup, ext]); + return true; + }; + + async leavePageGroup(ext, pageGroup) { + const [lookup] = await this.pool.query('SELECT * FROM paging_groups WHERE page_number = ? AND ext = ?', [pageGroup, ext]); + if (!lookup) { + return false; + } + + await this.pool.query('DELETE FROM paging_groups WHERE page_number = ? AND ext = ?', [pageGroup, ext]); + return true; + }; + + async getNextAvailableExtension() { + const extList = await this.listExtensions(); + const exts = extList.fetchAllExtensions.extension; + const startExt = process.env.START_EXT ? parseInt(process.env.START_EXT, 10) : 1000; + const existingExts = exts.map(ext => parseInt(ext.user.extension, 10)).sort((a, b) => a - b); + + let nextExt = startExt; + for (let i = 0; i < existingExts.length; i++) { + if (existingExts[i] !== nextExt) { + break; + } + nextExt++; + } + + return nextExt; + } +} + +module.exports = FreepbxManager; diff --git a/funcs.js b/funcs.js deleted file mode 100644 index 9e5cf83..0000000 --- a/funcs.js +++ /dev/null @@ -1,97 +0,0 @@ -// Some random functions, as to not clutter the main file -// Generate GraphQL query -const generateQuery = (type, args) => { - switch (type) { - case 'lookup': - return minifyQuery(`query { - fetchExtension(extensionId: "${args.ext}") { - user { - extension - name - extPassword - voicemail - } - } - fetchVoiceMail(extensionId: "${args.ext}") { - password - email - } - }`); - break; - case 'list': - return minifyQuery(`query { - fetchAllExtensions { - extension { - user { - extension - name - } - } - } - }`); - break; - case 'add': - return minifyQuery(`mutation { - addExtension(input: { - extensionId: "${args.ext}" - name: "${args.name}" - email: "${args.uid}" - vmEnable: true - vmPassword: "${args.ext}" - maxContacts: "5" - umEnable: false - }) { - status - } - }`); - break; - - case 'delete': - return minifyQuery(`mutation { - deleteExtension(input: {extensionId: ${args.ext}}) { - status - } - }`); - break; - case 'reload': - return minifyQuery(`mutation { - doreload(input: {clientMutationId: "${args.id}"}) { - status - } - }`); - break; - case 'update_name': - return minifyQuery(`mutation { - updateCoreUser (input: {extension: ${args.ext}, name: "${args.name}", noanswer_cid: "", busy_cid: "", chanunavail_cid: "", busy_dest: "", noanswer_dest: "", chanunavail_dest: ""}) { - coreuser { - name - } - } - }`); - } -} - -// minify query function -const minifyQuery = (query) => { - return query.replace(/\s+/g, ' ').trim(); -} - -module.exports = { - generateQuery, - minifyQuery, - // Input validation - validateInput: function (input, type) { - switch (type) { - case 'extention': - // Check if input is a 3 digit number - if (input.length != 3) { - return false; - } - if (isNaN(input)) { - return false; - } - return true; - break; - } - }, -} diff --git a/index.js b/index.js index 541e742..3f0a479 100644 --- a/index.js +++ b/index.js @@ -1,1707 +1,134 @@ -//Load static files -const config = require("./config.json"); -const funcs = require("./funcs.js"); -const colors = require("colors"); -const embeds = require("./embeds.json") -const axios = require('axios'); -const ping = require("ping") -var commandsBase = require("./commands.json") -const ssh2 = require('ssh2') -const sshConn = new ssh2.Client(); -// find first file in .ssh local to the script -const fs = require('fs'); -const path = require('path'); -// get the first file in the .ssh directory -const keyPath = path.join(__dirname, '.ssh'); -const keyFiles = fs.readdirSync(keyPath); -const keyFile = keyFiles[0]; -// read the key file -console.log(`Using key file: ${keyFile}`); -const privateKey = fs.readFileSync(".ssh/" + keyFile, 'utf8'); +require("dotenv").config(); -// FreePBX GraphQL Client -const { - FreepbxGqlClient, - gql -} = require("freepbx-graphql-client"); -var pbxClient = new FreepbxGqlClient(config.freepbx.url, { - client: { - id: config.freepbx.clientid, - secret: config.freepbx.secret, - } +const mariadb = require("mariadb"); +const pool = mariadb.createPool({ + host: process.env.DB_HOST, + port: process.env.DB_PORT || 3306, + user: process.env.DB_USER, + password: process.env.DB_PASS, + database: "asterisk", + connectionLimit: 5, }); -// 50 minute interval to refresh the token -setInterval(() => { - pbxClient = new FreepbxGqlClient(config.freepbx.url, { - client: { - id: config.freepbx.clientid, - secret: config.freepbx.secret, - } - }); -}, 3000000); +const FreepbxManager = require("./freepbx"); +const fpbx = new FreepbxManager({ + url: process.env.FREEPBX_URL, + clientId: process.env.FREEPBX_CLIENT_ID, + clientSecret: process.env.FREEPBX_CLIENT_SECRET, + dbPool: pool, +}); -// Set up mariadb connection -const mariadb = require('mariadb'); -const pool = mariadb.createPool(config.mariadb); -const cdrPool = mariadb.createPool(config.cdrdb); +const Discord = require('discord.js'); +const client = new Discord.Client({ + intents: [ "GuildMembers", "Guilds", "GuildPresences" ] +}); +const colors = require('colors'); -const json2csv = (obj) => { // Specifically for CDR - let csv = "call_date,src,dst,disposition,caller_id,duration,billsec,context,recordingfile\n"; - for (let key in obj) { - data = [ - obj[key].calldate, - obj[key].src, - obj[key].dst, - obj[key].disposition, - obj[key].clid, - obj[key].duration, - obj[key].billsec, - obj[key].dcontext, - obj[key].recordingfile - ]; - csv += data.join(",") + "\n"; +const log = { + log: (msg) => { + console.log(msg); + }, + info: (msg) => { + console.log(`${colors.cyan("[INFO]")} ${msg}`); + }, + warn: (msg) => { + console.log(`${colors.yellow("[WARN]")} ${msg}`); + }, + error: (msg) => { + console.log(`${colors.red("[ERROR]")} ${msg}`); + }, + success: (msg) => { + console.log(`${colors.green("[SUCCESS]")} ${msg}`); + }, + debug: (msg) => { + console.log(`${colors.magenta("[DEBUG]")} ${msg}`); } - return csv; } -// Some functions for FreePBX +// Put clients in global for use elsewhere. +global.pool = pool; +global.fpbx = fpbx; +global.client = client; +global.log = log; -const reload = () => { - // We're gonna start converting all the old gql commands to using mysql `system fwconsole reload` query - return new Promise((resolve, reject) => { - sshConn.exec('fwconsole reload --json', (err, stream) => { - if (err) { - reject(err); - } - stream.on('data', (data) => { - // is there a way to send this data without resolving the promise? - dt = JSON.parse(data.toString()) - if (dt.message == "Reload Complete") { - resolve(dt); - } - }); - }); - }); -} -const getExtCount = () => { - return new Promise((resolve, reject) => { - pbxClient.request(funcs.minifyQuery(funcs.generateQuery('list', {}))).then((result) => { - resolve(result.fetchAllExtensions.extension.length); - }).catch((error) => { - reject(error); - }); - }); -} +client.on('ready', async () => { + log.success(`Logged in as ${client.user.displayName}`); - -const createExtension = (ext, name, uid) => { - return new Promise(async (resolve, reject) => { - const conn = await pool.getConnection(); - await conn.query(`DELETE FROM devices WHERE id NOT IN (SELECT extension FROM users);`) - await conn.query(`DELETE FROM sip WHERE id NOT IN (SELECT extension FROM users);`); - conn.end(); - pbxClient.request(funcs.minifyQuery(funcs.generateQuery('lookup', { - ext: ext - }))).then((result) => { - // Extension exists - res = { - "status": "exists", - } - resolve(res); - }).catch((error) => { - // Extension does not exist, create it, reload, look it up, and return the result - pbxClient.request(funcs.minifyQuery(funcs.generateQuery('add', { - ext: ext, - name: name, - uid: uid - }))).then((result) => { - reload().then((result) => { - pbxClient.request(funcs.minifyQuery(funcs.generateQuery('lookup', { - ext: ext - }))).then((result) => { - res = { - "status": "created", - "result": result - } - resolve(res); - }).catch((error) => { - reject(error); - }); - }).catch((error) => { - reject(error); - }); - }).catch((error) => { - reject(error); - }); - }); - }); -} - -const fixNames = () => { // Gonna leave this here if I ever need it in the future - pbxClient.request(funcs.minifyQuery(funcs.generateQuery("list", {}))).then((result) => { - let extensions = result.fetchAllExtensions.extension; - extensions.forEach((extension) => { - pbxClient.request(funcs.minifyQuery(funcs.generateQuery("lookup", { - ext: extension.user.extension - }))).then((result) => { - // Get discord user - dcClient.users.fetch(result.fetchVoiceMail.email).then((user) => { - // Update extension name - updateName(extension.user.extension, user.displayName).then((result) => { - if (result.status == "updated") { - sendLog(`${colors.green("[INFO]")} Updated extension ${extension.user.extension} name to ${user.displayName}`) - } - }).catch((error) => { - sendLog(`${colors.red("[ERROR]")} ${error}`); - }); - }).catch((error) => { - sendLog(`${colors.red("[ERROR]")} ${error}`); - }); - }).catch((error) => { - sendLog(`${colors.red("[ERROR]")} ${error}`); - }); - }); - }); -} - -// deleteExtension, takes an extension number -const deleteExtension = (ext) => { - return new Promise(async (resolve, reject) => { - var conn = await cdrPool.getConnection(); - // delete from cel where cid_num = ext - const row = await conn.query(` - DELETE FROM cel - WHERE cid_num = ${ext} - `); - conn.end(); - pbxClient.request(funcs.minifyQuery(funcs.generateQuery('delete', { - ext: ext - }))).then((result) => { - reload().then((result) => { - res = { - "status": "deleted", - "result": result - } - resolve(res); - }).catch((error) => { - reject(error); - }); - }).catch((error) => { - reject(error); - }); - }); -} - -const updateName = (ext, name) => { - return new Promise(async (resolve, reject) => { - // update the extension name in the `users` table - const conn = await pool.getConnection(); - await conn.query(`UPDATE users SET name = '${name}' WHERE extension = ${ext};`) - //await conn.query(`UPDATE sip WHERE id = ${ext} AND keyword = 'callerid' SET data = '${name} <${ext}>';`); // this query is borked, im bad at sql - await conn.query(`UPDATE sip SET data = '${name} <${ext}>' WHERE id = ${ext} AND keyword = 'callerid';`); - conn.end(); - // reload - reload().then((result) => { - resolve({ - "status": "updated", - "result": name - }) - }).catch((error) => { - reject(error); - }); - }); -} - -const generateExtensionListEmbed = async () => { - return new Promise(async (resolve, reject) => { + const commands = require("./commands") + // Command registration + log.info("Registering commands...") + await (async () => { try { - var conn = await cdrPool.getConnection(); - const result = await pbxClient.request(funcs.minifyQuery(funcs.generateQuery("list", {}))); - let extensions = result.fetchAllExtensions.extension; - let extensionList = {}; - - // Generate a list of all unique extensions to be checked in the database - let uniqueExtensions = [...new Set(extensions.map(extension => extension.user.extension))]; - - // Construct SQL query to check all unique extensions at the same time - const row30 = await conn.query(` - SELECT cid_num, MAX(eventtime) - FROM cel - WHERE cid_num IN (${uniqueExtensions.join(",")}) - AND eventtime >= DATE_SUB(CURDATE(), INTERVAL 30 DAY) - GROUP BY cid_num - `); - - const row90 = await conn.query(` - SELECT cid_num, MAX(eventtime) - FROM cel - WHERE cid_num IN (${uniqueExtensions.join(",")}) - AND eventtime >= DATE_SUB(CURDATE(), INTERVAL 90 DAY) - GROUP BY cid_num - `); - // Get fresh/entirely unused extensions - const alltime = await conn.query(` - SELECT cid_num - FROM cel - WHERE cid_num IN (${uniqueExtensions.join(",")}) - GROUP BY cid_num - `); - // turn rows into an array of extension numbers - let active30 = row30.map(row => row.cid_num); - let active90 = row90.map(row => row.cid_num); - let used = alltime.map(row => row.cid_num); - - - // Generate inactiveFlag object, if it's a fresh extension set the flag to - - - let inactiveFlag = {}; - uniqueExtensions.forEach((ext) => { - if (used.includes(ext)) { - if (active30.includes(ext)) { - if (active90.includes(ext)) { - inactiveFlag[ext] = ""; - } else { - inactiveFlag[ext] = "**"; - } - } else { - inactiveFlag[ext] = "*"; - } - } else { - inactiveFlag[ext] = "-"; - } + const rest = new Discord.REST().setToken(client.token); + // Global Commands + log.info(`Registering global commands`); + rest.put(Discord.Routes.applicationCommands(client.user.id), { body: commands }).then(() => { + log.success("Global commands registered") + }).catch((error) => { + log.error(error) }); - extensions.forEach((extension) => { - extensionList[extension.user.extension] = extension.user.name; - }); - - // fullList will contain embeds, each embed will contain one field with as many extensions as it can fit (up to 1024 characters). Once the feild is full, make a new embed in the array without a title, just a description. The firrst embed will have a title - let field = ""; - let embeds = []; - let count = 0; - - // put for loop in function and await it - embeds.push({ - "title": "Extension List", - "color": 0x00ff00, - "description": `${extensions.length} extensions\n\`* = inactive for 30 days\`\n\`** = inactive for 90 days\`\n\`- = never used\``, - timestamp: new Date() - }) - await (async () => { - for (let key in extensionList) { - field += `\`${key}${inactiveFlag[key]}\`: ${extensionList[key]}\n`; - if (field.length >= 1024) { - // cut feilds at nearest newline and push to the embed - let lastNewline = field.lastIndexOf("\n", 1024); - embeds[count].fields = [{ - "name": "Extensions", - "value": field.slice(0, lastNewline) - }]; - embeds.push({ - "color": 0x00ff00, - "feilds": [ - { - "name": "Extensions (extended)", - "value": field, - timestamp: new Date() - } - ] - }); - // figure out any extensions that got cut off and add them to the next embed - field = field.slice(lastNewline); - count++; - } - embeds[count].fields = [{ - "name": "Extensions", - "value": field - }]; - } - })(); - - - // for (let key in extensionList) { - // extensionList1 += `\`${key}${inactiveFlag[key]}\`: ${extensionList[key]}\n`; - // } - - //}); - res = embeds; - // res = { - // "title": "Extension List", - // "color": 0x00ff00, - // "description": `${extensions.length} extensions\n\`* = inactive for 30 days\`\n\`** = inactive for 90 days\`\n\`- = never used\``, - // "fields": [{ - // "name": "Extensions", - // "value": `${extensionList1}` - // }], - // "timestamp": new Date() - // } - conn.end(); - resolve(res); } catch (error) { - reject(error); + log.error(error) } - }); -}; - -const lookupExtension = (ident, type) => { // type is either "ext" or "uid" - return new Promise((resolve, reject) => { - switch (type) { - case "ext": - pbxClient.request(funcs.minifyQuery(funcs.generateQuery('lookup', { - ext: ident - }))).then((result) => { - res = { - "status": "exists", - "result": result - } - resolve(res); - }).catch((error) => { - res = { - "status": "notfound", - "result": error - } - reject(res); - }); - break; - case "uid": - // Find the extension based on Discord ID in the voicemail email field - pbxClient.request(funcs.minifyQuery(funcs.generateQuery('list', {}))).then(async (result) => { - // loop through all extensions, run a lookup on each one, and return the first one that matches - var found = false; - var ext = ""; - var count = 0; - result.fetchAllExtensions.extension.forEach(async (ext) => { - pbxClient.request(funcs.minifyQuery(funcs.generateQuery('lookup', { - ext: ext.user.extension - }))).then((result) => { - if (result.fetchVoiceMail.email == ident && !found) { - found = true; - ext = result; - clearInterval(x); - resolve({ - "status": "exists", - "result": ext - }) - } - count++; - }).catch((error) => { - reject(error); - }); - }); - x = setInterval(() => { - if (count == result.fetchAllExtensions.extension.length) { - clearInterval(x); - if (!found) { - reject("Not found"); - } - } - }, 100); - - }).catch((error) => { - reject(error); - }); - break; - default: - reject("Invalid type"); - } - }); -} - -const findNextExtension = () => { - return new Promise((resolve, reject) => { - pbxClient.request(funcs.minifyQuery(funcs.generateQuery('list', {}))).then((result) => { - // Find the highest extension - var highest = 0; - // output looks like {fetchAllExtensions: { extension: [{user:{extension: 100, name: "Test"}}]}} - // Look out for gaps in the extension numbers, if there are any, use that one, if not, use the highest + 1 - var exts = []; - result.fetchAllExtensions.extension.forEach((ext) => { - exts.push(Number(ext.user.extension)); - }); - exts.sort((a, b) => a - b); - // Find duplicate extensions and remove all but the first - for (var i = 0; i < exts.length; i++) { - if (exts[i] == exts[i + 1]) { - exts.splice(i, 1); - i--; - } - } - - - - // Start should be the lowest extension. If none exists use config value - // Await if statement - var start = 0; - if (exts.length > 0) { - start = exts[0]; - } else { - start = config.freepbx.startExt; - exts[0] = start - 1; - } - for (var i = 0; i < exts.length; i++) { - if (exts[i] != i + config.freepbx.startExt) { - highest = i + start; - break; - } - } - if (highest == 0) { - highest = String(Number(exts[exts.length - 1]) + 1); - } - - // Return the next extension - res = { - "status": "success", - "result": String(highest) - } - resolve(res); - }).catch((error) => { - reject(error); - }); - }); -} - -// Load Discord.js -const Discord = require("discord.js"); -const { - REST, - Routes -} = require('discord.js'); -const dcClient = new Discord.Client({ - intents: ["Guilds", "GuildMembers"] + })(); }); -const rest = new REST({ - version: '10' -}).setToken(config.discord.token); -var logChannel; -var sendLog = (message) => { - console.log(`LOG BEFORE READY: ${message}`); -}; -var logMsg = null; // Used to store the log message, so it can be edited instead of sending a new one -var curMsg = ""; // Used to calculate the length of the log message, so it can be edited instead of sending a new one -dcClient.on('ready', async () => { - await dcClient.channels.fetch(config.discord.logId).then(async (channel) => { - await channel.send(`\`\`\`ansi\n${curMsg}\`\`\``).then((msg) => { - logMsg = msg; - }); - sendLog = async (message) => { - let timestamp = new Date() - message = `[${timestamp.toLocaleString()}] ${message}`; - if (curMsg.length + message.length <= 2000) { - curMsg = `${curMsg}\n${message}`; - await logMsg.edit(`\`\`\`ansi\n${curMsg}\`\`\``); - } else { - curMsg = message; - await channel.send(`\`\`\`ansi\n${message}\`\`\``).then((msg) => { - logMsg = msg; - }); - } - console.log(message); - }; - const conn = await pool.getConnection(); - await conn.query(`DELETE FROM devices WHERE id NOT IN (SELECT extension FROM users);`) - await conn.query(`DELETE FROM sip WHERE id NOT IN (SELECT extension FROM users);`); - conn.end(); - sendLog(`${colors.cyan("[INFO]")} Logged in as ${dcClient.user.displayName}!`); - const pageGroups = require('./pageGroups.json'); - const pageCommand = { - "name": "paging", - "description": "Add/Remove yourself from paging groups", - "type": 1, - "options": [ - { - "name": "method", - "description": "The method to use", - "type": 3, - "required": true, - "choices": [ - { - "name": "add", - "value": "add" - }, - { - "name": "remove", - "value": "remove" - } - ] - }, - { - "name": "group", - "description": "The group to add/remove yourself from", - "type": 3, - "required": true, - "choices": pageGroups - } - ] - }; +client.on('interactionCreate', async interaction => { + switch(interaction.type) { + case Discord.InteractionType.ApplicationCommand: + const command = require(`./interactionHandlers/commands/${interaction.commandName}`); - // make a non reference copy of the commands object - var commands = JSON.parse(JSON.stringify(commandsBase)); - commands.push(pageCommand); + if (!command) return; - - (async () => { try { - sendLog(`${colors.cyan("[INFO]")} Started refreshing application (/) commands.`); - await rest.put( - Routes.applicationGuildCommands(dcClient.user.id, config.discord.guildId), { - body: commands - } - ); - sendLog(`${colors.cyan("[INFO]")} Successfully reloaded application (/) commands.`); + await command.execute(interaction); } catch (error) { - console.error(`${colors.red("[ERROR]")} ${error}`); + log.error(error); + await interaction.reply({ content: 'There was an error while executing this command!', ephemeral: true }); } - })(); + break; + case Discord.InteractionType.MessageComponent: + const component = require(`./interactionHandlers/components/${interaction.customId}`); - // Presence Stuff - getExtCount().then((result) => { - dcClient.user.setPresence({ - activities: [{ - name: `${result} extensions`, - type: "WATCHING" - }], - status: "online" - }); - }).catch((error) => { - sendLog(`${colors.red("[ERROR]")} ${error}`); - }); + if (!component) return; - // Run every 5 minutes - setInterval(() => { - getExtCount().then((result) => { - dcClient.user.setPresence({ - activities: [{ - name: `${result} extensions`, - type: "WATCHING" - }], - status: "online" - }); - }).catch((error) => { - sendLog(`${colors.red("[ERROR]")} ${error}`); - }); - }, 300000); - - // Lookup all extensions and check if they're still in the server - // If they're not, delete them - // Run once on startup - pbxClient.request(funcs.minifyQuery(funcs.generateQuery("list", {}))).then((result) => { - let extensions = result.fetchAllExtensions.extension; - extensions.forEach((extension) => { - lookupExtension(extension.user.extension, "ext").then((result) => { - if (result.result.fetchVoiceMail.email == null) { - // Extension is not part of the bot, do nothing - return; - }; - // Fetch Discord user using ID stored in result.result.fetchVoiceMail.email, and see if they're in the server - dcClient.guilds.cache.get(config.discord.guildId).members.fetch(result.result.fetchVoiceMail.email).then((member) => { - // They're in the server, do nothing - }).catch((error) => { - // They're not in the server, delete the extension - sendLog(`${colors.cyan("[INFO]")} ${extension.user.extension} is not in the server, deleting it`); - deleteExtension(extension.user.extension).then((result) => { - sendLog(`${colors.cyan("[INFO]")} Deleted extension ${extension.user.extension} because the user is no longer in the server`); - }).catch((error) => { - sendLog(`${colors.red("[ERROR]")} ${error}`); - }); - }); - - }); - }); - }) - - // Run every 5 minutes - const extListChannel = dcClient.channels.cache.get(config.discord.extList); - // Find the latest message from the bot in extListChannel, if there isn't one, send one. There can be other messages in the channel - // Sends the same message as the list command - setInterval(async () => { - // run this goofy query just to make sure everything is happy - /* - DELETE FROM devices - WHERE id NOT IN (SELECT extension FROM users); - */ - // This will delete any devices that don't have a corresponding user - // This is a safety measure to prevent orphaned devices (it breaks the API entirely if there are any) - const conn = await pool.getConnection(); - await conn.query(`DELETE FROM devices WHERE id NOT IN (SELECT extension FROM users);`) - await conn.query(`DELETE FROM sip WHERE id NOT IN (SELECT extension FROM users);`); - conn.end(); - - - - await extListChannel.messages.fetch({ - limit: 1 - }).then((messages) => { - if (messages.size == 0) { - pbxClient.request(funcs.minifyQuery(funcs.generateQuery("list", {}))).then((result) => { - let extensions = result.fetchAllExtensions.extension; - // key:value pairs of extension:username - let extensionList = {}; - extensions.forEach((extension) => { - extensionList[extension.user.extension] = extension.user.name; - }); - extensionList1 = ""; - for (let key in extensionList) { - extensionList1 += `\`${key}\`: ${extensionList[key]}\n`; - } - generateExtensionListEmbed().then(embed => { - extListChannel.send({ - content: "", - embeds: embed - }); - }) - }) - } else { - pbxClient.request(funcs.minifyQuery(funcs.generateQuery("list", {}))).then((result) => { - let extensions = result.fetchAllExtensions.extension; - // key:value pairs of extension:username - let extensionList = {}; - extensions.forEach((extension) => { - extensionList[extension.user.extension] = extension.user.name; - }); - extensionList1 = ""; - for (let key in extensionList) { - extensionList1 += `\`${key}\`: ${extensionList[key]}\n`; - } - generateExtensionListEmbed().then(embed => { - const botMessage = messages.find(msg => msg.author.id === dcClient.user.id); - if (botMessage) { - botMessage.edit({ - content: "", - embeds: embed - }); - } else { - extListChannel.send({ - content: "", - embeds: embed - }); - } - }); - }) - } - }) - }, 300000); - // Also run on startup - extListChannel.messages.fetch({ - limit: 1 - }).then((messages) => { - if (messages.size == 0) { - pbxClient.request(funcs.minifyQuery(funcs.generateQuery("list", {}))).then((result) => { - let extensions = result.fetchAllExtensions.extension; - // key:value pairs of extension:username - let extensionList = {}; - extensions.forEach((extension) => { - extensionList[extension.user.extension] = extension.user.name; - }); - extensionList1 = ""; - for (let key in extensionList) { - extensionList1 += `\`${key}\`: ${extensionList[key]}\n`; - } - generateExtensionListEmbed().then(embed => { - extListChannel.send({ - content: "", - embeds: embed - }); - }); - }) - } else { - pbxClient.request(funcs.minifyQuery(funcs.generateQuery("list", {}))).then((result) => { - let extensions = result.fetchAllExtensions.extension; - // key:value pairs of extension:username - let extensionList = {}; - extensions.forEach((extension) => { - extensionList[extension.user.extension] = extension.user.name; - }); - extensionList1 = ""; - for (let key in extensionList) { - extensionList1 += `\`${key}\`: ${extensionList[key]}\n`; - } - generateExtensionListEmbed().then(embed => { - const botMessage = messages.find(msg => msg.author.id === dcClient.user.id); - if (botMessage) { - botMessage.edit({ - content: "", - embeds: embed - }); - } else { - extListChannel.send({ - content: "", - embeds: embed - }); - } - }); - }) + try { + await component.execute(interaction); + } catch (error) { + log.error(error); + await interaction.reply({ content: 'There was an error while executing this component!', ephemeral: true }); } - }) - - }); - - if (config.status?.url) { - // Uptime Kuma Ping - // Calculate ping to Discord API - - // Every X seconds (defined in config.status.interval), send a ping to Uptime Kuma, send push request to config.status.url - setInterval(() => { - // Send a ping to Uptime Kuma - // Send a push request to config.status.url - // Define URL arguments ?status=up&msg=OK&ping= - - // Calculate ping to Discord API - const start = Date.now(); - axios.get("https://discord.com/api/gateway").then((result) => { - const latency = Date.now() - start; - axios.get(config.status.url + `?status=up&msg=OK&ping=${latency}`).then((result) => { - //sendLog(`${colors.cyan("[INFO]")} Sent ping to Uptime Kuma`); - }).catch((error) => { - sendLog(`${colors.red("[ERROR]")} Error sending ping ${error}`); - }); - }) - }, config.status.interval * 1000); - const start = Date.now(); - - axios.get("https://discord.com/api/gateway").then((result) => { - const latency = Date.now() - start; - axios.get(config.status.url + `?status=up&msg=OK&ping=${latency}`).then((result) => { - //sendLog(`${colors.cyan("[INFO]")} Sent ping to Uptime Kuma`); - }).catch((error) => { - sendLog(`${colors.red("[ERROR]")} Error sending ping ${error}`); - }); - }) - } - - - - // Start doing SSH stuff - sendLog(`${colors.cyan("[INFO]")} Starting SSH connection`); - await sshConn.connect({ - host: config.freepbx.server, - username: "root", // Will make config later - privateKey: privateKey - }) - -}); - -sshConn.on('ready', () => { - sendLog(`${colors.cyan("[INFO]")} SSH connection established`); - console.log("Reloading PBX") - reload().then((result) => { - console.log("Reloaded PBX") - }).catch((error) => { - console.log("Error reloading PBX") - console.log(error) - }); -}); - - -dcClient.on("guildMemberRemove", (member) => { - // Delete the extension if the user leaves the server - sendLog(`${colors.cyan("[INFO]")} User ${member.id} left the server`) - lookupExtension(member.id, "uid").then((result) => { - if (result.status == "exists") { - sendLog(`${colors.cyan("[INFO]")} User ${member.id} has extension ${result.result.fetchExtension.user.extension}, deleting it`) - deleteExtension(result.result.fetchExtension.user.extension).then((delResult) => { - sendLog(`${colors.cyan("[INFO]")} Deleted extension ${result.result.fetchExtension.user.extension} because the user left the server`); - }).catch((error) => { - sendLog(`${colors.red("[ERROR]")} Error deleting ext ${error}`); - }); - } - }).catch((error) => { - sendLog(`${colors.red("[ERROR]")} Error looking up extension to delete ${error}`); - }); -}); - -dcClient.on('interactionCreate', async interaction => { - if (interaction.isCommand()) { - const { - commandName - } = interaction; - switch (commandName) { - case "new": - await interaction.deferReply({ - ephemeral: true - }); - lookupExtension(interaction.user.id, "uid").then((result) => { - if (result.status == "exists") { - // The user already has an extension, return an ephemeral message saying so - interaction.editReply({ - content: "You already have an extension!", - ephemeral: true - }); - } - }).catch((error) => { - // The user doesn't have an extension, create one - findNextExtension().then((result) => { - if (result.status == "success") { - let uid = interaction.user.id; - let ext = result.result; - let name = interaction.user.displayName; - interaction.editReply(`Creating extension ${ext}...`) - // Create the extension - createExtension(ext, name, uid).then((result) => { - if (result.status == "created") { - interaction.editReply({ - content: "", - embeds: [{ - "title": "Extension Created!", - "color": 0x00ff00, - "description": `The SIP server is \`${config.freepbx.server}\``, - "fields": [{ - "name": "Extension/Username", - "value": ext - }, - { - "name": "Password", - "value": `||${result.result.fetchExtension.user.extPassword}||` - } - ] - }] - }) - sendLog(`${colors.cyan("[INFO]")} Created extension ${ext} for user ${uid}`); - // Add the role to the user on Discord based on the ID in the config file - let role = interaction.guild.roles.cache.find(role => role.id === config.discord.roleId); - interaction.member.roles.add(role); - } - }).catch((error) => { - interaction.editReply(`Error creating extension: ${error}`); - }); - } - }).catch((error) => { - interaction.editReply(`Error finding next available extension: ${error}`); - }); - }); - break; - case "whoami": - await interaction.deferReply({ - ephemeral: true - }); - lookupExtension(interaction.user.id, "uid").then((result) => { - if (result.status == "exists") { - // The user already has an extension, return an ephemeral message saying so - interaction.editReply({ - content: "", - embeds: [{ - "title": "Extension Info", - "color": 0x00ff00, - "description": `The SIP server is \`${config.freepbx.server}\``, - "fields": [{ - "name": "Extension/Username", - "value": result.result.fetchExtension.user.extension - }, - { - "name": "Password", - "value": `||${result.result.fetchExtension.user.extPassword}||` - } - ] - }], - ephemeral: true - }) - } - }).catch((error) => { - // The user doesn't have an extension, create one - sendLog(`${colors.red("[ERROR]")} Error looking up extension ${error}`) - interaction.editReply({ - content: "You don't have an extension!", - ephemeral: true - }); - }); - break; - - case "list": - await interaction.deferReply({ - ephemeral: false - }); - generateExtensionListEmbed().then((result) => { - interaction.editReply({ - content: "", - embeds: result - }); - }).catch((error) => { - interaction.editReply(`Error generating extension list: ${error}`); - }); - break; - case "delete": - if (interaction.options.get("confirm").value == false) { - interaction.reply({ - content: "Please confirm you want to delete your extension by running `/delete confirm:true`", - ephemeral: true - }) - break; - } - await interaction.deferReply({ - ephemeral: true - }); - lookupExtension(interaction.user.id, "uid").then((result) => { - if (result.status == "exists") { - // The user has an extension, delete it - deleteExtension(result.result.fetchExtension.user.extension).then((result) => { - if (result.status == "deleted") { - interaction.editReply({ - content: "Extension Deleted!", - ephemeral: true - }); - sendLog(`${colors.green("[INFO]")} ${interaction.user.displayName} (${interaction.user.id}) deleted extension ${result.result.fetchExtension.user.extension}`) - // Remove the role from the user on Discord based on the ID in the config file - let role = interaction.guild.roles.cache.find(role => role.id === config.discord.roleId); - interaction.member.roles.remove(role); - } - }).catch((error) => { - interaction.reply(`Error deleting extension: ${error}`); - }); - } - }).catch((error) => { - // The user doesn't have an extension, return an ephemeral message saying so - interaction.editReply({ - content: "You don't have an extension!", - ephemeral: true - }); - }); - break; - case "button": - interaction.channel.send({ - embeds: embeds.controls, - components: [{ - type: 1, - components: [{ - type: 2, - label: "Get an Extension", - emoji: { - name: "✅" - }, - style: 3, - custom_id: "new" - }, - { - type: 2, - label: "Get your extension info", - emoji: { - name: "ℹ️" - }, - style: 1, - custom_id: "whoami" - }, - { - type: 2, - label: "Delete your extension", - emoji: { - name: "❌" - }, - style: 4, - custom_id: "delete" - }, - ] - }] - }).then(() => { - interaction.reply({ - content: "Button sent!", - ephemeral: true - }) - }); - break; - case "name": // Update the users extension name, name is optional and defaults to the users Discord displayName - // sanity check the name, remove any quotes, escape any escape characters - let name; - if (!interaction.options.get("name")) { - name = interaction.user.displayName; - } else { - name = interaction.options.get("name").value; - } - name = name.replace(/"/g, ""); - name = name.replace(/\\/g, "\\\\"); // Fuck you cayden - - await interaction.deferReply({ - ephemeral: true - }); - lookupExtension(interaction.user.id, "uid").then((result) => { - if (result.status == "exists") { - // The user has an extension, update the name - updateName(result.result.fetchExtension.user.extension, name).then((result2) => { - if (result2.status == "updated") { - interaction.editReply({ - content: "Extension Name Updated!", - ephemeral: true - }); - sendLog(`${colors.green("[INFO]")} ${interaction.user.displayName} (${interaction.user.id}) updated extension ${result.result.fetchExtension.user.extension} name to ${name}`) - } - }).catch((error) => { - interaction.editReply(`Error updating extension name: ${error}`); - }); - } - }).catch((error) => { - // The user doesn't have an extension, return an ephemeral message saying so - interaction.editReply({ - content: "You don't have an extension!", - ephemeral: true - }); - }); - break; - case "paging": // Add/Remove yourself from paging groups - var conn = await pool.getConnection(); - await interaction.deferReply({ - ephemeral: true - }); - // Get the users extension, if they don't have one, return an ephemeral message saying so - lookupExtension(interaction.user.id, "uid").then((result) => { - if (result.status == "exists") { - // The user has an extension, add/remove them from the paging group - let ext = result.result.fetchExtension.user.extension; - let group = interaction.options.get("group").value; - let method = interaction.options.get("method").value; - switch (method) { - case "add": - // Check the db if they're already in the group - conn.query(`SELECT * FROM paging_groups WHERE ext = ${ext} AND \`page_number\` = ${group}`).then((result) => { - if (result.length == 0) { - // They're not in the group, add them - conn.query(`INSERT INTO paging_groups (\`ext\`, \`page_number\`) VALUES (${ext}, ${group})`).then((result) => { - reload().then(() => { - interaction.editReply({ - content: "Added you to the paging group!", - ephemeral: true - }); - sendLog(`${colors.green("[INFO]")} ${interaction.user.displayName} (${interaction.user.id}) added themselves to paging group ${group}`) - }); - }).catch((error) => { - interaction.editReply(`Error adding you to the paging group: ${error}`); - sendLog(`${colors.red("[ERROR]")} Error adding user to pg group ${error}`); - }); - } else { - // They're already in the group, return an ephemeral message saying so - interaction.editReply({ - content: "You're already in that paging group!", - ephemeral: true - }); - } - }).catch((error) => { - interaction.editReply(`Error adding you to the paging group: ${error}`); - sendLog(`${colors.red("[ERROR]")} Error looking up user in pg group ${error}`); - }); - break; - case "remove": - // Check if they're in the group - conn.query(`SELECT * FROM paging_groups WHERE ext = ${ext} AND \`page_number\` = ${group}`).then((result) => { - if (result.length == 0) { - // They're not in the group, return an ephemeral message saying so - interaction.editReply({ - content: "You're not in that paging group!", - ephemeral: true - }); - } else { - // They're in the group, remove them - conn.query(`DELETE FROM paging_groups WHERE ext = ${ext} AND \`page_number\` = ${group}`).then((result) => { - reload().then(() => { - interaction.editReply({ - content: "Removed you from the paging group!", - ephemeral: true - }); - sendLog(`${colors.green("[INFO]")} ${interaction.user.displayName} (${interaction.user.id}) removed themselves from paging group ${group}`) - }); - }).catch((error) => { - interaction.editReply(`Error removing you from the paging group: ${error}`); - sendLog(`${colors.red("[ERROR]")} Error while removing user from page group ${error}`); - }); - } - }).catch((error) => { - interaction.editReply(`Error removing you from the paging group: ${error}`); - sendLog(`${colors.red("[ERROR]")} Error while looking if user is in pg grp ${error}`); - }); - break; - } - } - }).catch((error) => { - // The user doesn't have an extension, return an ephemeral message saying so, and how to get one (/new) - interaction.editReply({ - content: "You don't have an extension! Run `/new` to get one!", - ephemeral: true - }); - }) - conn.end(); - break; - - case "admin": // Admin commands - // switch subcommand - switch (interaction.options.getSubcommand()) { - case "silence": // SSH run `asterisk -x "channel request hangup all" - - sshConn.exec("asterisk -x 'channel request hangup all'", (err, stream) => { - if (err) { - interaction.reply({ - content: `Error killing calls: ${err}`, - ephemeral: true - }); - sendLog(`${colors.red("[ERROR]")} Error killing calls ${err}`); - } - stream.on("exit", (code) => { - interaction.reply({ - content: "Killed all calls!", - ephemeral: true - }); - sendLog(`${colors.green("[INFO]")} Silenced all channels`); - }) - }); - break; - case "reload": // Reload asterisk and freepbx - await interaction.deferReply({ - ephemeral: true - }); - // We got two commands to run to be safe - sshConn.exec("fwconsole reload", (err, stream) => { - if (err) { - interaction.editReply(`Error reloading FreePBX: ${err}`); - sendLog(`${colors.red("[ERROR]")} Error manually reloading fwconsole ${err}`); - } - stream.on('exit', (code, signal) => { - sshConn.exec("asterisk -x 'core reload'", (err, stream) => { - if (err) { - interaction.editReply(`Error reloading Asterisk: ${err}`); - sendLog(`${colors.red("[ERROR]")} Error manually reloading asterisk ${err}`); - } - stream.on('exit', (code, signal) => { - interaction.editReply("Reloaded FreePBX and Asterisk!"); - sendLog(`${colors.green("[INFO]")} Reloaded FreePBX and Asterisk`); - }); - }); - }); - }); - break; - case "reboot": // Reboot the whole server (last resort, after this happens kill all connections to the server, then set a 1m timer to kill the bot) - await interaction.deferReply({ - ephemeral: true - }); - sshConn.exec("reboot", (err, stream) => { - if (err) { - interaction.editReply(`Error rebooting server: ${err}`); - sendLog(`${colors.red("[ERROR]")} Error rebooting? ${err}`); - } - stream.on('exit', (code, signal) => { - interaction.editReply("Rebooting server...\nThe bot will now disconnect and restart in 1 minute. Please stand by...").then(() => { - sendLog(`${colors.green("[INFO]")} Rebooting server`); - dcClient.destroy().then(() => { - console.log("Disconnected from Discord"); - }); - conn.end().then(() => { - console.log("Disconnected from MySQL"); - }); - sshConn.end(); - console.log("Disconnected from SSH") - setTimeout(() => { - process.exit(); - }, 60000); - }); - }); - }); - } - break; - case "dev": // Developer commands - // check if the user is a developer - if (!config.discord.developers.includes(interaction.user.id)) { - interaction.reply({ - content: "You're not a developer!", - ephemeral: true - }); - break; - } - - // switch subcommand - switch (interaction.options.getSubcommand()) { - case "fwconsole": // Run a fwconsole command - await interaction.deferReply({ - ephemeral: true - }); - let cmd = interaction.options.get("command").value; - sshConn.exec(`fwconsole ${cmd}`, (err, stream) => { - if (err) { - interaction.editReply(`Error running command: ${err}`); - sendLog(`${colors.red("[ERROR]")} Error running fwconsole command ${err}`); - } - outputStream = "" - stream.on("data", (data) => { - outputStream += `${data}` - }) - stream.on('exit', (code, signal) => { - // generate message json - const msgJson = { - content: `Ran command \`${cmd}\`\n\`\`\`ansi\n${outputStream}\`\`\`` - } - // outputStream is too long for Discord, so we need to send it as a file - if (outputStream.length > 2000) { - // make the buffer - const buffer = Buffer.from(outputStream, 'utf-8'); - const attachment = { - name: "output.txt", - attachment: buffer - } - msgJson.files = [attachment]; - msgJson.content = `Ran command \`${cmd}\`\nOutput too long, sending as a file` - } - interaction.editReply(msgJson); - sendLog(`${colors.green("[INFO]")} Ran command ${cmd}`); - }); - }); - break; - case "restart": // Restart the bot - await interaction.reply({ - content: "Restarting the bot...", - ephemeral: true - }) - sendLog(`${colors.green("[INFO]")} Restarting the bot`); - dcClient.destroy().then(() => { - console.log("Disconnected from Discord"); - }); - conn.end().then(() => { - console.log("Disconnected from MySQL"); - }); - sshConn.end(); - console.log("Disconnected from SSH") - setTimeout(() => { - process.exit(); - }, 1000); - break; - case "asterisk": // Asterisk CLI command - await interaction.deferReply({ - ephemeral: true - }); - let cmd2 = interaction.options.get("command").value; - sshConn.exec(`asterisk -x '${cmd2}'`, (err, stream) => { - if (err) { - interaction.editReply(`Error running command: ${err}`); - sendLog(`${colors.red("[ERROR]")} Error running asterisk cli command ${err}`); - } - outputStream = "" - stream.on("data", (data) => { - outputStream += `${data}` - }) - stream.on('exit', (code, signal) => { - // generate message json - const msgJson = { - content: `Ran command \`${cmd2}\`\n\`\`\`ansi\n${outputStream}\`\`\`` - } - // outputStream is too long for Discord, so we need to send it as a file - if (outputStream.length > 2000) { - // make the buffer - const buffer = Buffer.from(outputStream, 'utf-8'); - const attachment = { - name: "output.txt", - attachment: buffer - } - msgJson.files = [attachment]; - msgJson.content = `Ran command \`${cmd2}\`\nOutput too long, sending as a file` - } - interaction.editReply(msgJson); - sendLog(`${colors.green("[INFO]")} Ran command ${cmd2}`); - }); - }); - break; - case "shell": // This is dangerous, only allow developers to use it - await interaction.deferReply({ - ephemeral: true - }); - let cmd3 = interaction.options.get("command").value; - // TODO: Timeout - //let cmd3timeout = interaction.options.get("timeout").value; - sshConn.exec(cmd3, (err, stream) => { - if (err) { - interaction.editReply(`Error running command: ${err}`); - sendLog(`${colors.red("[ERROR]")} Error running shell command ${err}`); - } - // if timeout is set, set a timeout before - //timeout = setTimeout(() => { - // stream.close(); - // interaction.editReply(`Command timed out after ${cmd3timeout}ms`); - //}) - outputStream = "" - stream.on("data", (data) => { - outputStream += `${data}` - }) - stream.on('exit', (code, signal) => { - // clear the timeout - //clearTimeout(timeout); - // generate message json - const msgJson = { - content: `Ran command \`${cmd3}\`\n\`\`\`ansi\n${outputStream}\`\`\`` - } - // outputStream is too long for Discord, so we need to send it as a file - if (outputStream.length > 2000) { - // make the buffer - const buffer = Buffer.from(outputStream, 'utf-8'); - const attachment = { - name: "output.txt", - attachment: buffer - } - msgJson.files = [attachment]; - msgJson.content = `Ran command \`${cmd3}\`\nOutput too long, sending as a file` - } - interaction.editReply(msgJson); - sendLog(`${colors.green("[INFO]")} Ran command ${cmd3}`); - }); - }); - break; - } - break; - case "cdr": // Get CDR records for the user - // default to beginning of time - let userStartDate = interaction.options.getString("start_date")?.match(/^\d{2}\/\d{2}\/\d{4}$/)?.[0] || new Date(0); // regex this to mm/dd/yyyy - let userEndDate = interaction.options.getString("end_date")?.match(/^\d{2}\/\d{2}\/\d{4}$/)?.[0] || new Date(); // regex this to mm/dd/yyyy - let startDate = new Date(userStartDate); - let endDate = new Date(userEndDate); - await interaction.deferReply({ - ephemeral: true - }); - lookupExtension(interaction.user.id, "uid").then((result) => { - if (result.status == "exists") { - // The user has an ext, use the cdrdb to get the records - cdrPool.getConnection().then((conn) => { - conn.query(`SELECT * FROM cdr WHERE src = '${result.result.fetchExtension.user.extension}' OR dst = '${result.result.fetchExtension.user.extension}' AND calldate BETWEEN '${startDate.toISOString()}' AND '${endDate.toISOString()}'`).then((result) => { - if (result.length == 0) { - interaction.editReply({ - content: "No CDR records found", - ephemeral: true - }); - } else { - // Generate the CSV - const fields = ["calldate", "src", "dst", "duration", "disposition", "recordingfile"]; - const csv = json2csv(result); - const buffer = Buffer.from(csv, 'utf-8'); - interaction.editReply({ - files: [ - { - attachment: buffer, - name: "cdr.csv" - } - ], - content: "Here are your CDR records" - }); - } - }).catch((error) => { - interaction.editReply(`Error getting CDR records: ${error}`); - }); - conn.end(); - }).catch((error) => { - interaction.editReply(`Error getting CDR records: ${error}`); - }); - } - }).catch((error) => { - // The user doesn't have an extension, return an ephemeral message saying so - interaction.editReply({ - content: "You don't have an extension!", - ephemeral: true - }); - }); - break; - default: - break; - } - } - if (interaction.isButton()) { - switch (interaction.customId) { - case "new": - await interaction.deferReply({ - ephemeral: true - }); - lookupExtension(interaction.user.id, "uid").then((result) => { - if (result.status == "exists") { - // The user already has an extension, return an ephemeral message saying so - interaction.editReply({ - content: "You already have an extension!", - ephemeral: true - }); - } - }).catch((error) => { - // The user doesn't have an extension, create one - findNextExtension().then((result) => { - if (result.status == "success") { - let uid = interaction.user.id; - let ext = result.result; - let name = interaction.user.displayName; - interaction.editReply(`Creating extension ${ext}...`) - // Create the extension - createExtension(ext, name, uid).then((result) => { - if (result.status == "created") { - interaction.editReply({ - content: "", - embeds: [{ - "title": "Extension Created!", - "color": 0x00ff00, - "description": `The SIP server is \`${config.freepbx.server}\``, - "fields": [{ - "name": "Extension/Username", - "value": ext - }, - { - "name": "Password", - "value": `||${result.result.fetchExtension.user.extPassword}||` - } - ] - }] - }) - sendLog(`${colors.cyan("[INFO]")} Created extension ${ext} for user ${uid}`); - // Add the role to the user on Discord based on the ID in the config file - let role = interaction.guild.roles.cache.find(role => role.id === config.discord.roleId); - interaction.member.roles.add(role); - } - }).catch((error) => { - interaction.editReply(`Error creating extension: ${error}`); - }); - } - }).catch((error) => { - interaction.editReply(`Error finding next available extension: ${error}`); - }); - }); - break; - case "delete": - interaction.reply({ - content: "Are you sure you want to delete your extension?\nThis action is **irreversible**!\nAll voicemails, call history, and other data will be **permanently deleted**!\n\n**Only do this if you're absolutely sure you want to delete your extension!**", - ephemeral: true, - components: [{ - type: 1, - components: [{ - type: 2, - label: "Yes", - emoji: { - name: "✅" - }, - style: 4, - custom_id: "delete2" - }] - }] - }).then(() => { - setTimeout(() => { - try { - interaction.deleteReply(); - } catch (error) { - // ignore - } - }, 10000); - }); - break; - case "delete2": - await interaction.deferReply({ - ephemeral: true - }); - lookupExtension(interaction.user.id, "uid").then((result) => { - if (result.status == "exists") { - // The user has an extension, delete it - deleteExtension(result.result.fetchExtension.user.extension).then((delResult) => { - if (delResult.status == "deleted") { - interaction.editReply({ - content: "Extension Deleted!", - ephemeral: true - }); - sendLog(`${colors.green("[INFO]")} ${interaction.user.displayName} (${interaction.user.id}) deleted extension ${result.result.fetchExtension.user.extension}`) - // Remove the role from the user on Discord based on the ID in the config file - let role = interaction.guild.roles.cache.find(role => role.id === config.discord.roleId); - interaction.member.roles.remove(role); - } - }).catch((error) => { - // sendLog full error with line number - - interaction.editReply(`Error deleting extension: ${error}`); - }); - } - }).catch((error) => { - // The user doesn't have an extension, return an ephemeral message saying so - interaction.editReply({ - content: "You don't have an extension!", - ephemeral: true - }); - }); - break; - case "whoami": - await interaction.deferReply({ - ephemeral: true - }); - lookupExtension(interaction.user.id, "uid").then((result) => { - if (result.status == "exists") { - // The user already has an extension, return an ephemeral message saying so - interaction.editReply({ - content: "", - embeds: [{ - "title": "Extension Info", - "color": 0x00ff00, - "description": `The SIP server is \`${config.freepbx.server}\``, - "fields": [{ - "name": "Extension/Username", - "value": result.result.fetchExtension.user.extension - }, - { - "name": "Password", - "value": `||${result.result.fetchExtension.user.extPassword}||` - } - ] - }], - ephemeral: true - }) - } - }).catch((error) => { - // The user doesn't have an extension, create one - sendLog(`${colors.red("[ERROR]")} Error finding extension for user? ${error}`) - interaction.editReply({ - content: "You don't have an extension!", - ephemeral: true - }); - }); - break; - } - } - if (interaction.isUserContextMenuCommand()) { - switch (interaction.commandName) { - case "Lookup Extension": - // Get the extension for the user if they have one - await interaction.deferReply({ - ephemeral: true - }); - lookupExtension(interaction.targetId, "uid").then((result) => { - if (result.status == "exists") { - // The user already has an extension, return an ephemeral message saying so - interaction.editReply({ - content: `${interaction.targetUser} has extension \`${result.result.fetchExtension.user.extension}\``, - ephemeral: true - }) - } - }).catch((error) => { - // The user doesn't have an extension, create one - sendLog(`${colors.red("[ERROR]")} User has not ext (lookup) ${error}`) - interaction.editReply({ - content: "That user doesn't have an extension!", - ephemeral: true - }); - }); - break; - case "Create Extension": // Create an extension for the user, if they have one, return the extension info - await interaction.deferReply({ - ephemeral: true - }); - lookupExtension(interaction.targetId, "uid").then((result) => { - if (result.status == "exists") { - // The user already has an extension, return an ephemeral message saying so - interaction.editReply({ - content: "", - embeds: [{ - "title": "Extension Info", - "color": 0x00ff00, - "description": `The SIP server is \`${config.freepbx.server}\``, - "fields": [{ - "name": "Extension/Username", - "value": result.result.fetchExtension.user.extension - }, - { - "name": "Password", - "value": `||${result.result.fetchExtension.user.extPassword}||` - } - ] - }] - }); - } - }).catch((error) => { - // The user doesn't have an extension, create one - findNextExtension().then((result) => { - if (result.status == "success") { - let uid = interaction.targetId; - let ext = result.result; - let name = interaction.targetUser.displayName; - interaction.editReply(`Creating extension ${ext}...`) - // Create the extension - createExtension(ext, name, uid).then((result) => { - if (result.status == "created") { - interaction.editReply({ - content: "", - embeds: [{ - "title": "Extension Created!", - "color": 0x00ff00, - "description": `The SIP server is \`${config.freepbx.server}\``, - "fields": [{ - "name": "Extension/Username", - "value": ext - }, - { - "name": "Password", - "value": `||${result.result.fetchExtension.user.extPassword}||` - } - ] - }] - }) - sendLog(`${colors.cyan("[INFO]")} Admin ${interaction.user.displayName} Created extension ${ext} for user ${interaction.targetUser.displayName} (${interaction.targetId})`); - // Add the role to the user on Discord based on the ID in the config file - let role = interaction.guild.roles.cache.find(role => role.id === config.discord.roleId); - interaction.targetMember.roles.add(role); - } - }).catch((error) => { - interaction.editReply(`Error creating extension: ${error}`); - }); - } - }).catch((error) => { - interaction.editReply(`Error finding next available extension: ${error}`); - }); - }); - break; - case "Delete Extension": // Delete the users extension, if they have one - await interaction.deferReply({ - ephemeral: true - }); - lookupExtension(interaction.targetId, "uid").then((result) => { - if (result.status == "exists") { - // The user has an extension, delete it - deleteExtension(result.result.fetchExtension.user.extension).then((delResult) => { - if (delResult.status == "deleted") { - interaction.editReply({ - content: "Extension Deleted!", - ephemeral: true - }); - sendLog(`${colors.green("[INFO]")} ${interaction.user.displayName} deleted ${interaction.targetUser.username}'s extension ${result.result.fetchExtension.user.extension}`) - // Remove the role from the user on Discord based on the ID in the config file - let role = interaction.guild.roles.cache.find(role => role.id === config.discord.roleId); - interaction.targetMember.roles.remove(role); - } - }).catch((error) => { - // sendLog full error with line number - - interaction.editReply(`Error deleting extension: ${error}`); - }); - } - }).catch((error) => { - // The user doesn't have an extension, return an ephemeral message saying so - interaction.editReply({ - content: "That user doesn't have an extension!", - ephemeral: true - }); - }); - break; - } + break; } }); -// Lets actually handle exceptions now -process.on('unhandledRejection', (error) => { - // Log a full error with line number - sendLog(`${colors.red("[ERROR]")} Unhandled Rejection ${error}`); - // If config.ntfyUrl is set, Send the exception to ntfy - if (config.ntfyUrl) fetch(config.ntfyUrl, { - method: 'POST', // PUT works too - body: JSON.stringify(error), - headers: { - 'Title': 'FreePBX Bot Rejection', - 'Priority': 5, - 'Tags': 'warning,phone,FreePBX Manager' - } - }); -}); +// DEBUG: Insert 200 extensions into discord_users table +// (async () => { +// try { +// const conn = await pool.getConnection(); +// for (let i = 1; i <= 200; i++) { +// const ext = await fpbx.getNextAvailableExtension(); +// await fpbx.addExtension(ext, `Test User ${i}`) +// await conn.query("INSERT INTO discord_users (extension, discordId) VALUES (?, ?)", [ext, '289884287765839882']); +// log.debug(`Inserted extension ${ext} into discord_users table.`); +// } +// log.success("Inserted 200 extensions into discord_users table."); +// conn.release(); +// } catch (err) { +// log.error(`Failed to insert extensions: ${err}`); +// } +// })(); -process.on('uncaughtException', (error) => { - // Log a full error with line number - sendLog(`${colors.red("[ERROR]")} Uncaught Exception ${error.stack}`); - // If config.ntfyUrl is set, Send the exception to ntfy - if (config.ntfyUrl) fetch(config.ntfyUrl, { - method: 'POST', // PUT works too - body: error.stack, - headers: { - 'Title': 'FreePBX Bot Exception', - 'Priority': 5, - 'Tags': 'warning,phone,FreePBX Manager' - } - }); -}); - -dcClient.login(config.discord.token); \ No newline at end of file +// Startup +require("./migrations")(pool).then(() => { + log.success("Database migrations complete."); + log.info("Starting Discord client..."); + client.login(process.env.DISCORD_TOKEN); +}).catch((err) => { + log.error(`Database migrations failed: ${err}`); + process.exit(1); +}); \ No newline at end of file diff --git a/interactionHandlers/commands/delete.js b/interactionHandlers/commands/delete.js new file mode 100644 index 0000000..227cf71 --- /dev/null +++ b/interactionHandlers/commands/delete.js @@ -0,0 +1,30 @@ +const pool = global.pool +const fpbx = global.fpbx +const client = global.client +const log = global.log + +module.exports = {}; + +module.exports.execute = async (interaction) => { + const [lookup] = await pool.query('SELECT * FROM discord_users WHERE discordId = ?', [interaction.user.id]); + if (!lookup) { + await interaction.reply({ content: `We're sorry, It doesn't look like you have an extension!`, ephemeral: true }); + return; + } + if (interaction.options.getBoolean("confirm") !== true) { + await interaction.reply({ content: `You must confirm you want to delete your extension!`, ephemeral: true }); + return; + } + fpbx.deleteExtension(lookup.extension).then(async (res) => { + if (res[0].deleteExtension.status != true) { + await interaction.reply({ content: `Something went wrong :(`, ephemeral: true }); + return; + } + await pool.query('DELETE FROM discord_users WHERE discordId = ?', [interaction.user.id]); + await fpbx.reload(); + await interaction.reply({ content: `Extension ${lookup.extension} deleted!`, ephemeral: true }); + }).catch(async (error) => { + log.error(error); + await interaction.reply({ content: 'There was an error while deleting your extension!', ephemeral: true }); + }); +} \ No newline at end of file diff --git a/interactionHandlers/commands/list.js b/interactionHandlers/commands/list.js new file mode 100644 index 0000000..aee0886 --- /dev/null +++ b/interactionHandlers/commands/list.js @@ -0,0 +1,36 @@ +const Discord = require('discord.js'); + +const pool = global.pool +const fpbx = global.fpbx +const client = global.client +const log = global.log + +module.exports = {}; + +module.exports.execute = async (interaction) => { + const lookup = await pool.query('SELECT * FROM discord_users'); + // lookup: [ { extension: '1001', discord_id: '1234567890' } ] + const embeds = []; + let description = ''; + + lookup.forEach((row, index) => { + const line = `${row.extension}: <@${row.discordId}>\n`; + if (description.length > 2048) { + embeds.push({ + description + }); + description = ''; + } + description += line; + }); + + if (description.length > 0) { + embeds.push({ + description + }); + } + embeds.forEach(async (embed) => { + await interaction.user.send({ embeds: [embed] }); + }) + await interaction.reply({ ephemeral: true, content: "Check your DMs!" }); +} \ No newline at end of file diff --git a/interactionHandlers/commands/lookup.js b/interactionHandlers/commands/lookup.js new file mode 100644 index 0000000..c80cc72 --- /dev/null +++ b/interactionHandlers/commands/lookup.js @@ -0,0 +1,19 @@ +const pool = global.pool +const fpbx = global.fpbx +const client = global.client +const log = global.log + +module.exports = {}; + +module.exports.execute = async (interaction) => { + const findUser = interaction.options.getUser('user'); + const [lookup] = await pool.query('SELECT * FROM discord_users WHERE discordId = ?', [findUser.id]); + + if (!lookup) { + await interaction.reply({ content: `No extension found for ${findUser.username}`, ephemeral: true }); + return; + } + + await interaction.reply({ content: `${findUser} has extension ${lookup.extension}`, ephemeral: true }); + +} \ No newline at end of file diff --git a/interactionHandlers/commands/new.js b/interactionHandlers/commands/new.js new file mode 100644 index 0000000..90123ea --- /dev/null +++ b/interactionHandlers/commands/new.js @@ -0,0 +1,40 @@ +const pool = global.pool +const fpbx = global.fpbx +const client = global.client +const log = global.log + +module.exports = {}; + +module.exports.execute = async (interaction) => { + await interaction.deferReply({ ephemeral: true }); + const [lookup] = await pool.query('SELECT * FROM discord_users WHERE discordId = ?', [interaction.user.id]); + if (lookup) { + await interaction.editReply({ content: `You already have an extension, it's ${lookup.extension}!`, ephemeral: true }); + return; + } + await interaction.editReply({ content: `Finding available extension`, ephemeral: true }); + fpbx.getNextAvailableExtension().then(async (nextExt) => { + await interaction.editReply({ content: `Found ${nextExt}. Creating..`, ephemeral: true }); + fpbx.addExtension(nextExt, interaction.user.username).then(async (res) => { + if (res.addExtension.status != true) { + await interaction.editReply({ content: `Something went wrong :(`, ephemeral: true }); + return; + } + await pool.query('INSERT INTO discord_users (discordId, extension) VALUES (?, ?)', [interaction.user.id, nextExt]); + await interaction.editReply({ content: `Extension ${nextExt} created! Getting info..`, ephemeral: true }); + await fpbx.reload(); + const extInfo = await fpbx.getExtension(nextExt); + await interaction.editReply({ embeds: [{ + title: "Your Extension Info", + description: `**PBX Address:** \`${process.env.PBX_HOSTNAME}\`\n**Extension:** \`${extInfo.fetchExtension.user.extension}\`\n**Name:** \`${extInfo.fetchExtension.user.name}\`\n**Password:** ||\`${extInfo.fetchExtension.user.extPassword}\`||`, + color: 0x00ff00 + }], ephemeral: true }) + }).catch(async (error) => { + log.error(error); + await interaction.editReply({ content: 'There was an error while creating your extension!', ephemeral: true }); + }); + }).catch(async (error) => { + log.error(error); + await interaction.editReply({ content: 'There was an error while creating your extension!', ephemeral: true }); + }); +} \ No newline at end of file diff --git a/interactionHandlers/commands/whoami.js b/interactionHandlers/commands/whoami.js new file mode 100644 index 0000000..e19c6c0 --- /dev/null +++ b/interactionHandlers/commands/whoami.js @@ -0,0 +1,25 @@ +const pool = global.pool +const fpbx = global.fpbx +const client = global.client +const log = global.log + +module.exports = {}; + +module.exports.execute = async (interaction) => { + const [lookup] = await pool.query('SELECT * FROM discord_users WHERE discordId = ?', [interaction.user.id]); + if (!lookup) { + await interaction.reply({ content: `We're sorry, It doesn't look like you have an extension!`, ephemeral: true }); + return; + } + const extInfo = await fpbx.getExtension(lookup.extension); + return await interaction.reply({ + ephemeral: true, embeds: [ + { + title: "Your Extension Info", + description: `**PBX Address:** \`${process.env.PBX_HOSTNAME}\`\n**Extension:** \`${extInfo.fetchExtension.user.extension}\`\n**Name:** \`${extInfo.fetchExtension.user.name}\`\n**Password:** ||\`${extInfo.fetchExtension.user.extPassword}\`||`, + color: 0x00ff00 + } + ] + }); + +} \ No newline at end of file diff --git a/interactionHandlers/commands/whois.js b/interactionHandlers/commands/whois.js new file mode 100644 index 0000000..3f55cf1 --- /dev/null +++ b/interactionHandlers/commands/whois.js @@ -0,0 +1,19 @@ +const pool = global.pool +const fpbx = global.fpbx +const client = global.client +const log = global.log + +module.exports = {}; + +module.exports.execute = async (interaction) => { + const findExt = interaction.options.getInteger('extension'); + const [lookup] = await pool.query('SELECT * FROM discord_users WHERE extension = ?', [findExt]); + + if (!lookup) { + await interaction.reply({ content: `No linked Discord account found for extension ${findExt}`, ephemeral: true }); + return; + } + + await interaction.reply({ content: `${findExt} belongs to <@${lookup.discordId}>`, ephemeral: true }); + +} \ No newline at end of file diff --git a/migrations.js b/migrations.js new file mode 100644 index 0000000..706c4f3 --- /dev/null +++ b/migrations.js @@ -0,0 +1,72 @@ +const mariadb = require('mariadb'); +const fs = require('fs'); +const path = require('path'); +const util = require("util") + + +function runMigrations(pool) { + return new Promise((resolve, reject) => { + let connection; + + pool.getConnection() + .then(conn => { + connection = conn; + + // Ensure a migrations table exists to track applied migrations + return connection.query(`CREATE TABLE IF NOT EXISTS migrations ( + id INT AUTO_INCREMENT PRIMARY KEY, + name VARCHAR(255) NOT NULL, + applied_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP + );`); + }) + .then(() => { + // Read all migration files + const migrationDir = path.join(__dirname, 'migrations'); + const files = fs.readdirSync(migrationDir).sort(); // Sort to apply in order + + return files.reduce((promise, file) => { + return promise.then(() => { + const migrationName = path.basename(file); + + // Check if the migration has already been applied + return connection.query( + 'SELECT 1 FROM migrations WHERE name = ? LIMIT 1', + [migrationName] + ).then(([rows]) => { + if (Object.keys(rows || {}).length > 0) { + //console.log(`Skipping already applied migration: ${migrationName}`); + return; // Skip this migration + } + + // Read and execute the migration SQL + const migrationPath = path.join(migrationDir, file); + const sql = fs.readFileSync(migrationPath, 'utf8'); + return connection.query(sql).then(() => { + // Record the applied migration + return connection.query( + 'INSERT INTO migrations (name) VALUES (?)', + [migrationName] + ).then(() => { + console.log(`Applied migration: ${migrationName}`); + }); + }); + }); + }); + }, Promise.resolve()); + }) + .then(() => { + console.log('All migrations applied successfully!'); + resolve(); + }) + .catch(err => { + console.error('Error running migrations:', err); + reject(err); + }) + .finally(() => { + if (connection) connection.release(); + }); + }); +} + + +module.exports = runMigrations \ No newline at end of file diff --git a/migrations/000_alter_users_table.sql b/migrations/000_alter_users_table.sql new file mode 100644 index 0000000..5c02e7c --- /dev/null +++ b/migrations/000_alter_users_table.sql @@ -0,0 +1 @@ +ALTER TABLE users ADD CONSTRAINT unique_extension UNIQUE (extension); \ No newline at end of file diff --git a/migrations/001_init_discord_users_table.sql b/migrations/001_init_discord_users_table.sql new file mode 100644 index 0000000..028a182 --- /dev/null +++ b/migrations/001_init_discord_users_table.sql @@ -0,0 +1,4 @@ +CREATE TABLE IF NOT EXISTS discord_users ( + extension VARCHAR(20) PRIMARY KEY, + discordId VARCHAR(25) NOT NULL +); \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 8fba4a6..7db98ed 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,6 +12,7 @@ "axios": "^1.6.0", "colors": "^1.4.0", "discord.js": "14.14.1", + "dotenv": "^16.4.7", "freepbx-graphql-client": "^0.1.1", "mariadb": "^3.2.0", "ping": "^0.4.4", @@ -20,85 +21,230 @@ } }, "node_modules/@discordjs/builders": { - "version": "1.6.5", - "resolved": "https://registry.npmjs.org/@discordjs/builders/-/builders-1.6.5.tgz", - "integrity": "sha512-SdweyCs/+mHj+PNhGLLle7RrRFX9ZAhzynHahMCLqp5Zeq7np7XC6/mgzHc79QoVlQ1zZtOkTTiJpOZu5V8Ufg==", + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@discordjs/builders/-/builders-1.10.0.tgz", + "integrity": "sha512-ikVZsZP+3shmVJ5S1oM+7SveUCK3L9fTyfA8aJ7uD9cNQlTqF+3Irbk2Y22KXTb3C3RNUahRkSInClJMkHrINg==", + "license": "Apache-2.0", "dependencies": { - "@discordjs/formatters": "^0.3.2", - "@discordjs/util": "^1.0.1", - "@sapphire/shapeshift": "^3.9.2", - "discord-api-types": "0.37.50", + "@discordjs/formatters": "^0.6.0", + "@discordjs/util": "^1.1.1", + "@sapphire/shapeshift": "^4.0.0", + "discord-api-types": "^0.37.114", "fast-deep-equal": "^3.1.3", - "ts-mixer": "^6.0.3", - "tslib": "^2.6.1" + "ts-mixer": "^6.0.4", + "tslib": "^2.6.3" }, "engines": { "node": ">=16.11.0" + }, + "funding": { + "url": "https://github.com/discordjs/discord.js?sponsor" } }, + "node_modules/@discordjs/builders/node_modules/@discordjs/formatters": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/@discordjs/formatters/-/formatters-0.6.0.tgz", + "integrity": "sha512-YIruKw4UILt/ivO4uISmrGq2GdMY6EkoTtD0oS0GvkJFRZbTSdPhzYiUILbJ/QslsvC9H9nTgGgnarnIl4jMfw==", + "license": "Apache-2.0", + "dependencies": { + "discord-api-types": "^0.37.114" + }, + "engines": { + "node": ">=16.11.0" + }, + "funding": { + "url": "https://github.com/discordjs/discord.js?sponsor" + } + }, + "node_modules/@discordjs/builders/node_modules/discord-api-types": { + "version": "0.37.117", + "resolved": "https://registry.npmjs.org/discord-api-types/-/discord-api-types-0.37.117.tgz", + "integrity": "sha512-d+Z6RKd7v3q22lsil7yASucqMfVVV0s0XSqu3cw7kyHVXiDO/mAnqMzqma26IYnIm2mk3TlupYJDGrdL908ZKA==", + "license": "MIT" + }, + "node_modules/@discordjs/builders/node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, "node_modules/@discordjs/collection": { "version": "1.5.3", "resolved": "https://registry.npmjs.org/@discordjs/collection/-/collection-1.5.3.tgz", "integrity": "sha512-SVb428OMd3WO1paV3rm6tSjM4wC+Kecaa1EUGX7vc6/fddvw/6lg90z4QtCqm21zvVe92vMMDt9+DkIvjXImQQ==", + "license": "Apache-2.0", "engines": { "node": ">=16.11.0" } }, "node_modules/@discordjs/formatters": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/@discordjs/formatters/-/formatters-0.3.2.tgz", - "integrity": "sha512-lE++JZK8LSSDRM5nLjhuvWhGuKiXqu+JZ/DsOR89DVVia3z9fdCJVcHF2W/1Zxgq0re7kCzmAJlCMMX3tetKpA==", + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/@discordjs/formatters/-/formatters-0.3.3.tgz", + "integrity": "sha512-wTcI1Q5cps1eSGhl6+6AzzZkBBlVrBdc9IUhJbijRgVjCNIIIZPgqnUj3ntFODsHrdbGU8BEG9XmDQmgEEYn3w==", + "license": "Apache-2.0", "dependencies": { - "discord-api-types": "0.37.50" + "discord-api-types": "0.37.61" }, "engines": { "node": ">=16.11.0" } }, "node_modules/@discordjs/rest": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/@discordjs/rest/-/rest-2.0.1.tgz", - "integrity": "sha512-/eWAdDRvwX/rIE2tuQUmKaxmWeHmGealttIzGzlYfI4+a7y9b6ZoMp8BG/jaohs8D8iEnCNYaZiOFLVFLQb8Zg==", + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/@discordjs/rest/-/rest-2.4.2.tgz", + "integrity": "sha512-9bOvXYLQd5IBg/kKGuEFq3cstVxAMJ6wMxO2U3wjrgO+lHv8oNCT+BBRpuzVQh7BoXKvk/gpajceGvQUiRoJ8g==", + "license": "Apache-2.0", "dependencies": { - "@discordjs/collection": "^1.5.3", - "@discordjs/util": "^1.0.1", - "@sapphire/async-queue": "^1.5.0", - "@sapphire/snowflake": "^3.5.1", - "@vladfrangu/async_event_emitter": "^2.2.2", - "discord-api-types": "0.37.50", - "magic-bytes.js": "^1.0.15", - "tslib": "^2.6.1", - "undici": "5.22.1" + "@discordjs/collection": "^2.1.1", + "@discordjs/util": "^1.1.1", + "@sapphire/async-queue": "^1.5.3", + "@sapphire/snowflake": "^3.5.3", + "@vladfrangu/async_event_emitter": "^2.4.6", + "discord-api-types": "^0.37.114", + "magic-bytes.js": "^1.10.0", + "tslib": "^2.6.3", + "undici": "6.19.8" }, "engines": { - "node": ">=16.11.0" + "node": ">=18" + }, + "funding": { + "url": "https://github.com/discordjs/discord.js?sponsor" + } + }, + "node_modules/@discordjs/rest/node_modules/@discordjs/collection": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@discordjs/collection/-/collection-2.1.1.tgz", + "integrity": "sha512-LiSusze9Tc7qF03sLCujF5iZp7K+vRNEDBZ86FT9aQAv3vxMLihUvKvpsCWiQ2DJq1tVckopKm1rxomgNUc9hg==", + "license": "Apache-2.0", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/discordjs/discord.js?sponsor" + } + }, + "node_modules/@discordjs/rest/node_modules/@sapphire/snowflake": { + "version": "3.5.5", + "resolved": "https://registry.npmjs.org/@sapphire/snowflake/-/snowflake-3.5.5.tgz", + "integrity": "sha512-xzvBr1Q1c4lCe7i6sRnrofxeO1QTP/LKQ6A6qy0iB4x5yfiSfARMEQEghojzTNALDTcv8En04qYNIco9/K9eZQ==", + "license": "MIT", + "engines": { + "node": ">=v14.0.0", + "npm": ">=7.0.0" + } + }, + "node_modules/@discordjs/rest/node_modules/discord-api-types": { + "version": "0.37.117", + "resolved": "https://registry.npmjs.org/discord-api-types/-/discord-api-types-0.37.117.tgz", + "integrity": "sha512-d+Z6RKd7v3q22lsil7yASucqMfVVV0s0XSqu3cw7kyHVXiDO/mAnqMzqma26IYnIm2mk3TlupYJDGrdL908ZKA==", + "license": "MIT" + }, + "node_modules/@discordjs/rest/node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/@discordjs/rest/node_modules/undici": { + "version": "6.19.8", + "resolved": "https://registry.npmjs.org/undici/-/undici-6.19.8.tgz", + "integrity": "sha512-U8uCCl2x9TK3WANvmBavymRzxbfFYG+tAu+fgx3zxQy3qdagQqBLwJVrdyO1TBfUXvfKveMKJZhpvUYoOjM+4g==", + "license": "MIT", + "engines": { + "node": ">=18.17" } }, "node_modules/@discordjs/util": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@discordjs/util/-/util-1.0.1.tgz", - "integrity": "sha512-d0N2yCxB8r4bn00/hvFZwM7goDcUhtViC5un4hPj73Ba4yrChLSJD8fy7Ps5jpTLg1fE9n4K0xBLc1y9WGwSsA==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@discordjs/util/-/util-1.1.1.tgz", + "integrity": "sha512-eddz6UnOBEB1oITPinyrB2Pttej49M9FZQY8NxgEvc3tq6ZICZ19m70RsmzRdDHk80O9NoYN/25AqJl8vPVf/g==", + "license": "Apache-2.0", "engines": { - "node": ">=16.11.0" + "node": ">=18" + }, + "funding": { + "url": "https://github.com/discordjs/discord.js?sponsor" } }, "node_modules/@discordjs/ws": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@discordjs/ws/-/ws-1.0.1.tgz", - "integrity": "sha512-avvAolBqN3yrSvdBPcJ/0j2g42ABzrv3PEL76e3YTp2WYMGH7cuspkjfSyNWaqYl1J+669dlLp+YFMxSVQyS5g==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@discordjs/ws/-/ws-1.2.0.tgz", + "integrity": "sha512-QH5CAFe3wHDiedbO+EI3OOiyipwWd+Q6BdoFZUw/Wf2fw5Cv2fgU/9UEtJRmJa9RecI+TAhdGPadMaEIur5yJg==", + "license": "Apache-2.0", "dependencies": { - "@discordjs/collection": "^1.5.3", - "@discordjs/rest": "^2.0.1", - "@discordjs/util": "^1.0.1", - "@sapphire/async-queue": "^1.5.0", - "@types/ws": "^8.5.5", - "@vladfrangu/async_event_emitter": "^2.2.2", - "discord-api-types": "0.37.50", - "tslib": "^2.6.1", - "ws": "^8.13.0" + "@discordjs/collection": "^2.1.0", + "@discordjs/rest": "^2.4.1", + "@discordjs/util": "^1.1.0", + "@sapphire/async-queue": "^1.5.2", + "@types/ws": "^8.5.10", + "@vladfrangu/async_event_emitter": "^2.2.4", + "discord-api-types": "^0.37.114", + "tslib": "^2.6.2", + "ws": "^8.17.0" }, "engines": { "node": ">=16.11.0" + }, + "funding": { + "url": "https://github.com/discordjs/discord.js?sponsor" + } + }, + "node_modules/@discordjs/ws/node_modules/@discordjs/collection": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@discordjs/collection/-/collection-2.1.1.tgz", + "integrity": "sha512-LiSusze9Tc7qF03sLCujF5iZp7K+vRNEDBZ86FT9aQAv3vxMLihUvKvpsCWiQ2DJq1tVckopKm1rxomgNUc9hg==", + "license": "Apache-2.0", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/discordjs/discord.js?sponsor" + } + }, + "node_modules/@discordjs/ws/node_modules/@types/ws": { + "version": "8.5.14", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.14.tgz", + "integrity": "sha512-bd/YFLW+URhBzMXurx7lWByOu+xzU9+kb3RboOteXYDfW+tr+JZa99OyNmPINEGB/ahzKrEuc8rcv4gnpJmxTw==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@discordjs/ws/node_modules/discord-api-types": { + "version": "0.37.117", + "resolved": "https://registry.npmjs.org/discord-api-types/-/discord-api-types-0.37.117.tgz", + "integrity": "sha512-d+Z6RKd7v3q22lsil7yASucqMfVVV0s0XSqu3cw7kyHVXiDO/mAnqMzqma26IYnIm2mk3TlupYJDGrdL908ZKA==", + "license": "MIT" + }, + "node_modules/@discordjs/ws/node_modules/ws": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz", + "integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/@fastify/busboy": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@fastify/busboy/-/busboy-2.1.1.tgz", + "integrity": "sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA==", + "license": "MIT", + "engines": { + "node": ">=14" } }, "node_modules/@gar/promisify": { @@ -151,31 +297,33 @@ } }, "node_modules/@sapphire/async-queue": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/@sapphire/async-queue/-/async-queue-1.5.0.tgz", - "integrity": "sha512-JkLdIsP8fPAdh9ZZjrbHWR/+mZj0wvKS5ICibcLrRI1j84UmLMshx5n9QmL8b95d4onJ2xxiyugTgSAX7AalmA==", + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@sapphire/async-queue/-/async-queue-1.5.5.tgz", + "integrity": "sha512-cvGzxbba6sav2zZkH8GPf2oGk9yYoD5qrNWdu9fRehifgnFZJMV+nuy2nON2roRO4yQQ+v7MK/Pktl/HgfsUXg==", + "license": "MIT", "engines": { "node": ">=v14.0.0", "npm": ">=7.0.0" } }, "node_modules/@sapphire/shapeshift": { - "version": "3.9.2", - "resolved": "https://registry.npmjs.org/@sapphire/shapeshift/-/shapeshift-3.9.2.tgz", - "integrity": "sha512-YRbCXWy969oGIdqR/wha62eX8GNHsvyYi0Rfd4rNW6tSVVa8p0ELiMEuOH/k8rgtvRoM+EMV7Csqz77YdwiDpA==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@sapphire/shapeshift/-/shapeshift-4.0.0.tgz", + "integrity": "sha512-d9dUmWVA7MMiKobL3VpLF8P2aeanRTu6ypG2OIaEv/ZHH/SUQ2iHOVyi5wAPjQ+HmnMuL0whK9ez8I/raWbtIg==", + "license": "MIT", "dependencies": { "fast-deep-equal": "^3.1.3", "lodash": "^4.17.21" }, "engines": { - "node": ">=v14.0.0", - "npm": ">=7.0.0" + "node": ">=v16" } }, "node_modules/@sapphire/snowflake": { "version": "3.5.1", "resolved": "https://registry.npmjs.org/@sapphire/snowflake/-/snowflake-3.5.1.tgz", "integrity": "sha512-BxcYGzgEsdlG0dKAyOm0ehLGm2CafIrfQTZGWgkfKYbj+pNNsorZ7EotuZukc2MT70E0UbppVbtpBrqpzVzjNA==", + "license": "MIT", "engines": { "node": ">=v14.0.0", "npm": ">=7.0.0" @@ -196,22 +344,28 @@ "integrity": "sha512-Nmh0K3iWQJzniTuPRcJn5hxXkfB1T1pgB89SBig5PlJQU5yocazeu4jATJlaA0GYFKWMqDdvYemoSnF2pXgLVA==" }, "node_modules/@types/node": { - "version": "20.5.7", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.5.7.tgz", - "integrity": "sha512-dP7f3LdZIysZnmvP3ANJYTSwg+wLLl8p7RqniVlV7j+oXSXAbt9h0WIBFmJy5inWZoX9wZN6eXx+YXd9Rh3RBA==" + "version": "22.10.10", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.10.tgz", + "integrity": "sha512-X47y/mPNzxviAGY5TcYPtYL8JsY3kAq2n8fMmKoRCxq/c4v4pyGNCzM2R6+M5/umG4ZfHuT+sgqDYqWc9rJ6ww==", + "license": "MIT", + "dependencies": { + "undici-types": "~6.20.0" + } }, "node_modules/@types/ws": { - "version": "8.5.5", - "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.5.tgz", - "integrity": "sha512-lwhs8hktwxSjf9UaZ9tG5M03PGogvFaH8gUgLNbN9HKIg0dvv6q+gkSuJ8HN4/VbyxkuLzCjlN7GquQ0gUJfIg==", + "version": "8.5.9", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.9.tgz", + "integrity": "sha512-jbdrY0a8lxfdTp/+r7Z4CkycbOFN8WX+IOchLJr3juT/xzbJ8URyTVSJ/hvNdadTgM1mnedb47n+Y31GsFnQlg==", + "license": "MIT", "dependencies": { "@types/node": "*" } }, "node_modules/@vladfrangu/async_event_emitter": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/@vladfrangu/async_event_emitter/-/async_event_emitter-2.2.2.tgz", - "integrity": "sha512-HIzRG7sy88UZjBJamssEczH5q7t5+axva19UbZLO6u0ySbYPrwzWiXBcC0WuHyhKKoeCyneH+FvYzKQq/zTtkQ==", + "version": "2.4.6", + "resolved": "https://registry.npmjs.org/@vladfrangu/async_event_emitter/-/async_event_emitter-2.4.6.tgz", + "integrity": "sha512-RaI5qZo6D2CVS6sTHFKg1v5Ohq/+Bo2LZ5gzUEwZ/WkHhwtGTCB/sVLw8ijOkAUxasZ+WshN/Rzj4ywsABJ5ZA==", + "license": "MIT", "engines": { "node": ">=v14.0.0", "npm": ">=7.0.0" @@ -350,17 +504,6 @@ "node": ">=10.0.0" } }, - "node_modules/busboy": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz", - "integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==", - "dependencies": { - "streamsearch": "^1.1.0" - }, - "engines": { - "node": ">=10.16.0" - } - }, "node_modules/cacache": { "version": "15.3.0", "resolved": "https://registry.npmjs.org/cacache/-/cacache-15.3.0.tgz", @@ -419,6 +562,7 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/colors/-/colors-1.4.0.tgz", "integrity": "sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA==", + "license": "MIT", "engines": { "node": ">=0.1.90" } @@ -512,34 +656,48 @@ } }, "node_modules/discord-api-types": { - "version": "0.37.50", - "resolved": "https://registry.npmjs.org/discord-api-types/-/discord-api-types-0.37.50.tgz", - "integrity": "sha512-X4CDiMnDbA3s3RaUXWXmgAIbY1uxab3fqe3qwzg5XutR3wjqi7M3IkgQbsIBzpqBN2YWr/Qdv7JrFRqSgb4TFg==" + "version": "0.37.61", + "resolved": "https://registry.npmjs.org/discord-api-types/-/discord-api-types-0.37.61.tgz", + "integrity": "sha512-o/dXNFfhBpYHpQFdT6FWzeO7pKc838QeeZ9d91CfVAtpr5XLK4B/zYxQbYgPdoMiTDvJfzcsLW5naXgmHGDNXw==", + "license": "MIT" }, "node_modules/discord.js": { - "version": "14.13.0", - "resolved": "https://registry.npmjs.org/discord.js/-/discord.js-14.13.0.tgz", - "integrity": "sha512-Kufdvg7fpyTEwANGy9x7i4od4yu5c6gVddGi5CKm4Y5a6sF0VBODObI3o0Bh7TGCj0LfNT8Qp8z04wnLFzgnbA==", + "version": "14.14.1", + "resolved": "https://registry.npmjs.org/discord.js/-/discord.js-14.14.1.tgz", + "integrity": "sha512-/hUVzkIerxKHyRKopJy5xejp4MYKDPTszAnpYxzVVv4qJYf+Tkt+jnT2N29PIPschicaEEpXwF2ARrTYHYwQ5w==", + "license": "Apache-2.0", "dependencies": { - "@discordjs/builders": "^1.6.5", - "@discordjs/collection": "^1.5.3", - "@discordjs/formatters": "^0.3.2", - "@discordjs/rest": "^2.0.1", - "@discordjs/util": "^1.0.1", - "@discordjs/ws": "^1.0.1", - "@sapphire/snowflake": "^3.5.1", - "@types/ws": "^8.5.5", - "discord-api-types": "0.37.50", - "fast-deep-equal": "^3.1.3", - "lodash.snakecase": "^4.1.1", - "tslib": "^2.6.1", - "undici": "5.22.1", - "ws": "^8.13.0" + "@discordjs/builders": "^1.7.0", + "@discordjs/collection": "1.5.3", + "@discordjs/formatters": "^0.3.3", + "@discordjs/rest": "^2.1.0", + "@discordjs/util": "^1.0.2", + "@discordjs/ws": "^1.0.2", + "@sapphire/snowflake": "3.5.1", + "@types/ws": "8.5.9", + "discord-api-types": "0.37.61", + "fast-deep-equal": "3.1.3", + "lodash.snakecase": "4.1.1", + "tslib": "2.6.2", + "undici": "5.27.2", + "ws": "8.14.2" }, "engines": { "node": ">=16.11.0" } }, + "node_modules/dotenv": { + "version": "16.4.7", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.7.tgz", + "integrity": "sha512-47qPchRCykZC03FhkYAhrvwU4xDBFIj1QPqaarj6mdM/hgUzfPHcpkHJOn3mJAufFeeAxAzeGsr5X0M4k6fLZQ==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, "node_modules/emoji-regex": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", @@ -583,7 +741,8 @@ "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "license": "MIT" }, "node_modules/follow-redirects": { "version": "1.15.3", @@ -621,6 +780,7 @@ "version": "0.1.1", "resolved": "https://registry.npmjs.org/freepbx-graphql-client/-/freepbx-graphql-client-0.1.1.tgz", "integrity": "sha512-JqDTlL0EA/bUMit9aODIupWSqF87WHWrCD6i716FzeOzS46cMFq/OajzftTMwOZQf20MMkJM2HI6CRnNlRGl6A==", + "license": "MIT", "dependencies": { "graphql": "^15.6.1", "graphql-request": "^3.6.1" @@ -831,7 +991,8 @@ "node_modules/lodash": { "version": "4.17.21", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "license": "MIT" }, "node_modules/lodash.snakecase": { "version": "4.1.1", @@ -850,9 +1011,10 @@ } }, "node_modules/magic-bytes.js": { - "version": "1.0.15", - "resolved": "https://registry.npmjs.org/magic-bytes.js/-/magic-bytes.js-1.0.15.tgz", - "integrity": "sha512-bpRmwbRHqongRhA+mXzbLWjVy7ylqmfMBYaQkSs6pac0z6hBTvsgrH0r4FBYd/UYVJBmS6Rp/O+oCCQVLzKV1g==" + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/magic-bytes.js/-/magic-bytes.js-1.10.0.tgz", + "integrity": "sha512-/k20Lg2q8LE5xiaaSkMXk4sfvI+9EGEykFS4b0CHHGWqDYU0bGUFSwchNOMA56D7TCs9GwVTkqe9als1/ns8UQ==", + "license": "MIT" }, "node_modules/make-dir": { "version": "3.1.0", @@ -1444,14 +1606,6 @@ "node": ">= 8" } }, - "node_modules/streamsearch": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz", - "integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==", - "engines": { - "node": ">=10.0.0" - } - }, "node_modules/string_decoder": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", @@ -1514,14 +1668,16 @@ "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" }, "node_modules/ts-mixer": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/ts-mixer/-/ts-mixer-6.0.3.tgz", - "integrity": "sha512-k43M7uCG1AkTyxgnmI5MPwKoUvS/bRvLvUb7+Pgpdlmok8AoqmUaZxUUw8zKM5B1lqZrt41GjYgnvAi0fppqgQ==" + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/ts-mixer/-/ts-mixer-6.0.4.tgz", + "integrity": "sha512-ufKpbmrugz5Aou4wcr5Wc1UUFWOLhq+Fm6qa6P0w0K5Qw2yhaUoiWszhCVuNQyNwrlGiscHOmqYoAox1PtvgjA==", + "license": "MIT" }, "node_modules/tslib": { "version": "2.6.2", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", - "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==", + "license": "0BSD" }, "node_modules/tweetnacl": { "version": "0.14.5", @@ -1529,16 +1685,23 @@ "integrity": "sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA==" }, "node_modules/undici": { - "version": "5.22.1", - "resolved": "https://registry.npmjs.org/undici/-/undici-5.22.1.tgz", - "integrity": "sha512-Ji2IJhFXZY0x/0tVBXeQwgPlLWw13GVzpsWPQ3rV50IFMMof2I55PZZxtm4P6iNq+L5znYN9nSTAq0ZyE6lSJw==", + "version": "5.27.2", + "resolved": "https://registry.npmjs.org/undici/-/undici-5.27.2.tgz", + "integrity": "sha512-iS857PdOEy/y3wlM3yRp+6SNQQ6xU0mmZcwRSriqk+et/cwWAtwmIGf6WkoDN2EK/AMdCO/dfXzIwi+rFMrjjQ==", + "license": "MIT", "dependencies": { - "busboy": "^1.6.0" + "@fastify/busboy": "^2.0.0" }, "engines": { "node": ">=14.0" } }, + "node_modules/undici-types": { + "version": "6.20.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", + "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", + "license": "MIT" + }, "node_modules/unique-filename": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-1.1.1.tgz", @@ -1605,9 +1768,10 @@ "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" }, "node_modules/ws": { - "version": "8.13.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.13.0.tgz", - "integrity": "sha512-x9vcZYTrFPC7aSIbj7sRCYo7L/Xb8Iy+pW0ng0wt2vCJv7M9HOMy0UoN3rr+IFC7hb7vXoqS+P9ktyLLLhO+LA==", + "version": "8.14.2", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.14.2.tgz", + "integrity": "sha512-wEBG1ftX4jcglPxgFCMJmZ2PLtSbJ2Peg6TmpJFTbe9GZYOQCDPdMYu/Tm0/bGZkw8paZnJY45J4K2PZrLYq8g==", + "license": "MIT", "engines": { "node": ">=10.0.0" }, diff --git a/package.json b/package.json index 68543f7..d3b61a2 100644 --- a/package.json +++ b/package.json @@ -12,6 +12,7 @@ "axios": "^1.6.0", "colors": "^1.4.0", "discord.js": "14.14.1", + "dotenv": "^16.4.7", "freepbx-graphql-client": "^0.1.1", "mariadb": "^3.2.0", "ping": "^0.4.4", diff --git a/pageGroups.json.default b/pageGroups.json.default deleted file mode 100644 index 395a430..0000000 --- a/pageGroups.json.default +++ /dev/null @@ -1,6 +0,0 @@ -[ - { - "name": "Test", - "value": "700" - } -] \ No newline at end of file -- 2.43.5 From 14515499282e62bc8f3c45bc1b21752ac1ec9a44 Mon Sep 17 00:00:00 2001 From: ChrisChrome Date: Sat, 25 Jan 2025 08:48:02 -0700 Subject: [PATCH 02/14] More commands, some buttons, and whatnot --- TODO.md | 2 +- interactionHandlers/commands/button.js | 37 +++++++++++++++++ interactionHandlers/commands/new.js | 39 +----------------- interactionHandlers/commands/whoami.js | 24 +---------- interactionHandlers/common/createExt.js | 40 +++++++++++++++++++ interactionHandlers/common/getExtInfo.js | 25 ++++++++++++ .../components/getExtensionInfo.js | 3 ++ .../components/newExtension.js | 3 ++ 8 files changed, 111 insertions(+), 62 deletions(-) create mode 100644 interactionHandlers/commands/button.js create mode 100644 interactionHandlers/common/createExt.js create mode 100644 interactionHandlers/common/getExtInfo.js create mode 100644 interactionHandlers/components/getExtensionInfo.js create mode 100644 interactionHandlers/components/newExtension.js diff --git a/TODO.md b/TODO.md index 0b319cc..de1b392 100644 --- a/TODO.md +++ b/TODO.md @@ -4,7 +4,7 @@ - [X] **/whoami** - Get your extension info if you have one. - [X] **/new** - Get an extension on the LiteNet Phone System. - [X] **/delete** - Remove your extension from the LiteNet Phone System. -- [ ] **/list** - List all extensions on the LiteNet Phone System. +- [X] **/list** - List all extensions on the LiteNet Phone System. - [ ] **/button** - Send the "Get an extension" button! *(Requires default_member_permissions: 0)* ## Admin Commands diff --git a/interactionHandlers/commands/button.js b/interactionHandlers/commands/button.js new file mode 100644 index 0000000..5c94be4 --- /dev/null +++ b/interactionHandlers/commands/button.js @@ -0,0 +1,37 @@ +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: "Placeholder", + description: "I'll put the full embed here once we get it finalized.", + }], + 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/new.js b/interactionHandlers/commands/new.js index 90123ea..5552d3e 100644 --- a/interactionHandlers/commands/new.js +++ b/interactionHandlers/commands/new.js @@ -1,40 +1,3 @@ -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 +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 index e19c6c0..e76d000 100644 --- a/interactionHandlers/commands/whoami.js +++ b/interactionHandlers/commands/whoami.js @@ -1,25 +1,3 @@ -const pool = global.pool -const fpbx = global.fpbx -const client = global.client -const log = global.log - module.exports = {}; -module.exports.execute = async (interaction) => { - const [lookup] = await pool.query('SELECT * FROM discord_users WHERE discordId = ?', [interaction.user.id]); - if (!lookup) { - await interaction.reply({ content: `We're sorry, It doesn't look like you have an extension!`, ephemeral: true }); - return; - } - const extInfo = await fpbx.getExtension(lookup.extension); - return await interaction.reply({ - ephemeral: true, embeds: [ - { - title: "Your Extension Info", - description: `**PBX Address:** \`${process.env.PBX_HOSTNAME}\`\n**Extension:** \`${extInfo.fetchExtension.user.extension}\`\n**Name:** \`${extInfo.fetchExtension.user.name}\`\n**Password:** ||\`${extInfo.fetchExtension.user.extPassword}\`||`, - color: 0x00ff00 - } - ] - }); - -} \ No newline at end of file +module.exports.execute = require("../common/getExtInfo").execute; \ 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 -- 2.43.5 From 5480402a8c223b6dd9a90c903b3c9acd6d1d403d Mon Sep 17 00:00:00 2001 From: ChrisChrome Date: Sat, 25 Jan 2025 09:06:26 -0700 Subject: [PATCH 03/14] Add admin commands --- index.js | 1 + interactionHandlers/commands/admin.js | 47 +++++++++++++++++++++++++++ 2 files changed, 48 insertions(+) create mode 100644 interactionHandlers/commands/admin.js diff --git a/index.js b/index.js index 3f0a479..fa44337 100644 --- a/index.js +++ b/index.js @@ -78,6 +78,7 @@ client.on('ready', async () => { }); 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}`); diff --git a/interactionHandlers/commands/admin.js b/interactionHandlers/commands/admin.js new file mode 100644 index 0000000..fd251ca --- /dev/null +++ b/interactionHandlers/commands/admin.js @@ -0,0 +1,47 @@ +const { exec } = require('child_process'); + +const pool = global.pool +const fpbx = global.fpbx +const client = global.client +const log = global.log + + +const exec = (command) => { + return new Promise((resolve, reject) => { + require('child_process').exec(command, (error, stdout, stderr) => { + if (error) { + reject(`error: ${error.message}`); + return; + } + if (stderr) { + reject(`stderr: ${stderr}`); + return; + } + resolve(stdout); + }); + }); +} + +module.exports = {}; + +module.exports.execute = async (interaction) => { + const subcommand = interaction.options.getSubcommand(); + + switch (subcommand) { + case 'silence': // Run `asterisk -x "channel request hangup all" + exec('asterisk -x "channel request hangup all"').then((res) => { + interaction.reply({ content: ``, ephemeral: true }); + }); + break; + case 'reload': // Run `fwconsole reload` + exec('fwconsole reload').then((res) => { + interaction.reply({ content: res, ephemeral: true }); + }); + break; + case 'reboot': // Run `reboot 0` + exec('reboot 0').then((res) => { + interaction.reply({ content: "Rebooting...", ephemeral: true }); + }); + break; + } +} \ No newline at end of file -- 2.43.5 From 9f56b012b9d084d44f6d3f739d8bb58c2a251840 Mon Sep 17 00:00:00 2001 From: ChrisChrome Date: Sat, 25 Jan 2025 09:10:16 -0700 Subject: [PATCH 04/14] Bwuh --- interactionHandlers/commands/admin.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/interactionHandlers/commands/admin.js b/interactionHandlers/commands/admin.js index fd251ca..8fca5f7 100644 --- a/interactionHandlers/commands/admin.js +++ b/interactionHandlers/commands/admin.js @@ -6,7 +6,7 @@ const client = global.client const log = global.log -const exec = (command) => { +const runCommand = (command) => { return new Promise((resolve, reject) => { require('child_process').exec(command, (error, stdout, stderr) => { if (error) { @@ -29,17 +29,17 @@ module.exports.execute = async (interaction) => { switch (subcommand) { case 'silence': // Run `asterisk -x "channel request hangup all" - exec('asterisk -x "channel request hangup all"').then((res) => { + runCommand('asterisk -x "channel request hangup all"').then((res) => { interaction.reply({ content: ``, ephemeral: true }); }); break; case 'reload': // Run `fwconsole reload` - exec('fwconsole reload').then((res) => { + runCommand('fwconsole reload').then((res) => { interaction.reply({ content: res, ephemeral: true }); }); break; case 'reboot': // Run `reboot 0` - exec('reboot 0').then((res) => { + runCommand('reboot 0').then((res) => { interaction.reply({ content: "Rebooting...", ephemeral: true }); }); break; -- 2.43.5 From f26b8b22de1b698763ce4415acfd08c45ac61fdd Mon Sep 17 00:00:00 2001 From: ChrisChrome Date: Sat, 25 Jan 2025 09:11:35 -0700 Subject: [PATCH 05/14] Fix admin commands --- interactionHandlers/commands/admin.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/interactionHandlers/commands/admin.js b/interactionHandlers/commands/admin.js index 8fca5f7..ca00737 100644 --- a/interactionHandlers/commands/admin.js +++ b/interactionHandlers/commands/admin.js @@ -30,7 +30,7 @@ module.exports.execute = async (interaction) => { switch (subcommand) { case 'silence': // Run `asterisk -x "channel request hangup all" runCommand('asterisk -x "channel request hangup all"').then((res) => { - interaction.reply({ content: ``, ephemeral: true }); + interaction.reply({ content: `Silenced`, ephemeral: true }); }); break; case 'reload': // Run `fwconsole reload` -- 2.43.5 From a6dcfeedb609809024834841620f336de5d00de5 Mon Sep 17 00:00:00 2001 From: ChrisChrome Date: Sat, 25 Jan 2025 09:15:14 -0700 Subject: [PATCH 06/14] Do callbacks on admin commands --- interactionHandlers/commands/admin.js | 35 ++++++++++++++++++--------- 1 file changed, 24 insertions(+), 11 deletions(-) diff --git a/interactionHandlers/commands/admin.js b/interactionHandlers/commands/admin.js index ca00737..0310f72 100644 --- a/interactionHandlers/commands/admin.js +++ b/interactionHandlers/commands/admin.js @@ -1,4 +1,5 @@ const { exec } = require('child_process'); +const process = require('child_process').exec(command); const pool = global.pool const fpbx = global.fpbx @@ -6,18 +7,29 @@ const client = global.client const log = global.log -const runCommand = (command) => { +const runCommand = (command, onData) => { return new Promise((resolve, reject) => { - require('child_process').exec(command, (error, stdout, stderr) => { - if (error) { - reject(`error: ${error.message}`); + + 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; } - if (stderr) { - reject(`stderr: ${stderr}`); - return; - } - resolve(stdout); + resolve('Command executed successfully'); + }); + + process.on('error', (error) => { + reject(`error: ${error.message}`); }); }); } @@ -34,8 +46,9 @@ module.exports.execute = async (interaction) => { }); break; case 'reload': // Run `fwconsole reload` - runCommand('fwconsole reload').then((res) => { - interaction.reply({ content: res, ephemeral: true }); + await interaction.deferReply({ ephemeral: true }); + runCommand('fwconsole reload', (data) => { + interaction.editReply({ content: data, ephemeral: true }); }); break; case 'reboot': // Run `reboot 0` -- 2.43.5 From f11f3905ca723dd4c61d5fc29406dd4fcd0b6df6 Mon Sep 17 00:00:00 2001 From: ChrisChrome Date: Sat, 25 Jan 2025 09:17:34 -0700 Subject: [PATCH 07/14] Debug admin 1 --- interactionHandlers/commands/admin.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/interactionHandlers/commands/admin.js b/interactionHandlers/commands/admin.js index 0310f72..bad63fd 100644 --- a/interactionHandlers/commands/admin.js +++ b/interactionHandlers/commands/admin.js @@ -1,5 +1,4 @@ const { exec } = require('child_process'); -const process = require('child_process').exec(command); const pool = global.pool const fpbx = global.fpbx @@ -9,6 +8,7 @@ const log = global.log const runCommand = (command, onData) => { return new Promise((resolve, reject) => { + const process = exec(command); process.stdout.on('data', (data) => { if (onData) { -- 2.43.5 From 14435d234509eafbf0b0ca78f8f65d9980f46a82 Mon Sep 17 00:00:00 2001 From: ChrisChrome Date: Sat, 25 Jan 2025 09:25:37 -0700 Subject: [PATCH 08/14] Set up for docker compose file --- docker-compose.yml | 15 +++++++++++++++ package.json | 3 ++- 2 files changed, 17 insertions(+), 1 deletion(-) create mode 100644 docker-compose.yml diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..fddc52a --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,15 @@ +services: + nodejs: + image: node:latest + container_name: nodejs_app + network_mode: host + restart: on-failure + deploy: + restart_policy: + condition: on-failure + max_attempts: 3 + window: 3600s + volumes: + - .:/app + working_dir: /app + command: sh -c "npm install && npm start" \ No newline at end of file diff --git a/package.json b/package.json index d3b61a2..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", -- 2.43.5 From db3cff0aa0e65bff4370cd5d0817cf93436e7ab3 Mon Sep 17 00:00:00 2001 From: ChrisChrome Date: Sat, 25 Jan 2025 09:29:13 -0700 Subject: [PATCH 09/14] Kinda dumb, docker is a non-option lol --- docker-compose.yml | 15 --------------- 1 file changed, 15 deletions(-) delete mode 100644 docker-compose.yml diff --git a/docker-compose.yml b/docker-compose.yml deleted file mode 100644 index fddc52a..0000000 --- a/docker-compose.yml +++ /dev/null @@ -1,15 +0,0 @@ -services: - nodejs: - image: node:latest - container_name: nodejs_app - network_mode: host - restart: on-failure - deploy: - restart_policy: - condition: on-failure - max_attempts: 3 - window: 3600s - volumes: - - .:/app - working_dir: /app - command: sh -c "npm install && npm start" \ No newline at end of file -- 2.43.5 From 373b6e1926682859bd6c3a13d4fd31bb518f1de4 Mon Sep 17 00:00:00 2001 From: ChrisChrome Date: Sat, 25 Jan 2025 09:33:51 -0700 Subject: [PATCH 10/14] Finishing touches to admin commands --- interactionHandlers/commands/admin.js | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/interactionHandlers/commands/admin.js b/interactionHandlers/commands/admin.js index bad63fd..ef9e668 100644 --- a/interactionHandlers/commands/admin.js +++ b/interactionHandlers/commands/admin.js @@ -52,6 +52,15 @@ module.exports.execute = async (interaction) => { }); break; case 'reboot': // Run `reboot 0` + 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').then((res) => { interaction.reply({ content: "Rebooting...", ephemeral: true }); }); -- 2.43.5 From 28b6201a3a2ff3b1ca703f3b79fdf1a0dcdb4669 Mon Sep 17 00:00:00 2001 From: ChrisChrome Date: Sat, 25 Jan 2025 09:39:00 -0700 Subject: [PATCH 11/14] Add some status stuff --- index.js | 10 +++++++--- interactionHandlers/commands/admin.js | 12 ++++++++---- 2 files changed, 15 insertions(+), 7 deletions(-) diff --git a/index.js b/index.js index fa44337..9b802db 100644 --- a/index.js +++ b/index.js @@ -20,7 +20,7 @@ const fpbx = new FreepbxManager({ const Discord = require('discord.js'); const client = new Discord.Client({ - intents: [ "GuildMembers", "Guilds", "GuildPresences" ] + intents: ["GuildMembers", "Guilds", "GuildPresences"] }); const colors = require('colors'); @@ -56,7 +56,11 @@ global.log = log; client.on('ready', async () => { log.success(`Logged in as ${client.user.displayName}`); - + await client.setStatus("dnd"); + await client.setActivity({ + type: Discord.ActivityType.Custom, + name: "Just chillin" + }) const commands = require("./commands") // Command registration log.info("Registering commands...") @@ -79,7 +83,7 @@ client.on('ready', async () => { 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) { + switch (interaction.type) { case Discord.InteractionType.ApplicationCommand: const command = require(`./interactionHandlers/commands/${interaction.commandName}`); diff --git a/interactionHandlers/commands/admin.js b/interactionHandlers/commands/admin.js index ef9e668..e0e0022 100644 --- a/interactionHandlers/commands/admin.js +++ b/interactionHandlers/commands/admin.js @@ -1,5 +1,5 @@ const { exec } = require('child_process'); - +const Discord = require('discord.js'); const pool = global.pool const fpbx = global.fpbx const client = global.client @@ -52,6 +52,12 @@ module.exports.execute = async (interaction) => { }); break; case 'reboot': // Run `reboot 0` + await interaction.reply({ content: "Rebooting...", ephemeral: true }); + await client.setStatus("dnd"); + await client.setActivity({ + type: Discord.ActivityType.Custom, + name: "Server Rebooting..." + }) await client.destroy(); log.info('Client destroyed.'); pool.end((err) => { @@ -61,9 +67,7 @@ module.exports.execute = async (interaction) => { log.info('Database pool closed.'); } }); - runCommand('reboot 0').then((res) => { - interaction.reply({ content: "Rebooting...", ephemeral: true }); - }); + runCommand('reboot 0'); break; } } \ No newline at end of file -- 2.43.5 From 2171b25dd6d1fa3c009f8d4e9a91ccdfd06790aa Mon Sep 17 00:00:00 2001 From: ChrisChrome Date: Sat, 25 Jan 2025 09:40:38 -0700 Subject: [PATCH 12/14] nvm --- index.js | 5 ----- interactionHandlers/commands/admin.js | 5 ----- 2 files changed, 10 deletions(-) diff --git a/index.js b/index.js index 9b802db..7bb38c3 100644 --- a/index.js +++ b/index.js @@ -56,11 +56,6 @@ global.log = log; client.on('ready', async () => { log.success(`Logged in as ${client.user.displayName}`); - await client.setStatus("dnd"); - await client.setActivity({ - type: Discord.ActivityType.Custom, - name: "Just chillin" - }) const commands = require("./commands") // Command registration log.info("Registering commands...") diff --git a/interactionHandlers/commands/admin.js b/interactionHandlers/commands/admin.js index e0e0022..46f3928 100644 --- a/interactionHandlers/commands/admin.js +++ b/interactionHandlers/commands/admin.js @@ -53,11 +53,6 @@ module.exports.execute = async (interaction) => { break; case 'reboot': // Run `reboot 0` await interaction.reply({ content: "Rebooting...", ephemeral: true }); - await client.setStatus("dnd"); - await client.setActivity({ - type: Discord.ActivityType.Custom, - name: "Server Rebooting..." - }) await client.destroy(); log.info('Client destroyed.'); pool.end((err) => { -- 2.43.5 From 5696df170ff3d4626e3b79950a783b4d12300926 Mon Sep 17 00:00:00 2001 From: ChrisChrome Date: Sat, 25 Jan 2025 11:49:40 -0700 Subject: [PATCH 13/14] A lotta stuff --- .env.example | 9 ++ TODO.md | 50 ++++++----- commands.js | 53 ++++++++++++ index.js | 2 - interactionHandlers/commands/button.js | 33 +++++++- interactionHandlers/commands/dev.js | 111 +++++++++++++++++++++++++ interactionHandlers/commands/manage.js | 83 ++++++++++++++++++ 7 files changed, 317 insertions(+), 24 deletions(-) create mode 100644 .env.example create mode 100644 interactionHandlers/commands/dev.js create mode 100644 interactionHandlers/commands/manage.js 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/TODO.md b/TODO.md index de1b392..82835c2 100644 --- a/TODO.md +++ b/TODO.md @@ -5,38 +5,48 @@ - [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. -- [ ] **/button** - Send the "Get an extension" button! *(Requires default_member_permissions: 0)* +- [X] **/button** - Send the "Get an extension" button! *(Requires default_member_permissions: 0)* ## Admin Commands *(Requires default_member_permissions: 0)* -- [ ] **/admin** - - [ ] **silence** - Kill all ongoing calls. - - [ ] **reload** - Run an Asterisk reload. - - [ ] **reboot** - Reboot the server. *(LAST RESORT)* +- [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)* -- [ ] **/dev** - - [ ] **fwconsole** - Run an `fwconsole` command. - - [ ] **command** (required) - The command to run. - - [ ] **asterisk** - Run an Asterisk CLI command. - - [ ] **command** (required) - The command to run. - - [ ] **shell** - Run a shell command. - - [ ] **command** (required) - The command to run. - - [ ] **restart** - Restart the bot. +- [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). -## Context Menu Commands -- [ ] **Lookup Extension** - *(Type: 2)* -- [ ] **Create Extension** - *(Type: 2, Requires default_member_permissions: 0)* -- [ ] **Delete Extension** - *(Type: 2, Requires default_member_permissions: 0)* +## 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) -- **Get an Extension** - Sends the "Get an extension" button. -- **See Your Info** - Sends the "See your info" button. -- **Delete Your Extension** - Sends the "Delete your extension" button. \ No newline at end of file +- [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.js b/commands.js index f3f8012..3cfd34a 100644 --- a/commands.js +++ b/commands.js @@ -173,5 +173,58 @@ module.exports = [ "required": true } ] + }, + { + "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/index.js b/index.js index 7bb38c3..e9c9fe1 100644 --- a/index.js +++ b/index.js @@ -52,8 +52,6 @@ global.fpbx = fpbx; global.client = client; global.log = log; - - client.on('ready', async () => { log.success(`Logged in as ${client.user.displayName}`); const commands = require("./commands") diff --git a/interactionHandlers/commands/button.js b/interactionHandlers/commands/button.js index 5c94be4..35d08b6 100644 --- a/interactionHandlers/commands/button.js +++ b/interactionHandlers/commands/button.js @@ -9,8 +9,37 @@ module.exports = {}; module.exports.execute = async (interaction) => { interaction.channel.send({ embeds: [{ - title: "Placeholder", - description: "I'll put the full embed here once we get it finalized.", + "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: [ { 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/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 -- 2.43.5 From 0d2c8243a027ce724fe29233b3431aa642402c60 Mon Sep 17 00:00:00 2001 From: ChrisChrome Date: Sat, 25 Jan 2025 12:02:53 -0700 Subject: [PATCH 14/14] GUH --- index.js | 36 ++++++++++++++++++++---------------- 1 file changed, 20 insertions(+), 16 deletions(-) diff --git a/index.js b/index.js index e9c9fe1..e0d3d61 100644 --- a/index.js +++ b/index.js @@ -1,5 +1,6 @@ require("dotenv").config(); +const fs = require('fs'); const mariadb = require("mariadb"); const pool = mariadb.createPool({ host: process.env.DB_HOST, @@ -104,22 +105,25 @@ client.on('interactionCreate', async interaction => { } }); -// DEBUG: Insert 200 extensions into discord_users table -// (async () => { -// try { -// const conn = await pool.getConnection(); -// for (let i = 1; i <= 200; i++) { -// const ext = await fpbx.getNextAvailableExtension(); -// await fpbx.addExtension(ext, `Test User ${i}`) -// await conn.query("INSERT INTO discord_users (extension, discordId) VALUES (?, ?)", [ext, '289884287765839882']); -// log.debug(`Inserted extension ${ext} into discord_users table.`); -// } -// log.success("Inserted 200 extensions into discord_users table."); -// conn.release(); -// } catch (err) { -// log.error(`Failed to insert extensions: ${err}`); -// } -// })(); +if (fs.existsSync("./import.json")) { + const importData = JSON.parse(fs.readFileSync("./import.json", "utf8")); + + const insertData = async () => { + try { + const conn = await pool.getConnection(); + for (const ext in importData) { + const discordId = importData[ext]; + await conn.query("INSERT INTO discord_users (extension, discordId) VALUES (?, ?)", [ext, discordId]); + } + conn.release(); + log.success("Data imported successfully."); + } catch (error) { + log.error(`Failed to import data: ${error}`); + } + }; + + insertData(); +} // Startup require("./migrations")(pool).then(() => { -- 2.43.5