Merge pull request '2.0-rewrite' (#2) from 2.0-rewrite into main

Reviewed-on: #2
This commit is contained in:
Christopher Cookman 2025-01-25 12:06:49 -07:00
commit 98ca59cd41
30 changed files with 1310 additions and 1985 deletions

9
.env.example Normal file
View file

@ -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

2
.gitignore vendored
View file

@ -137,3 +137,5 @@ test.js
embeds.json embeds.json
pageGroups.json pageGroups.json
.ssh/ .ssh/
old/

52
TODO.md Normal file
View file

@ -0,0 +1,52 @@
# Bot Checklist
## General Commands
- [X] **/whoami** - Get your extension info if you have one.
- [X] **/new** - Get an extension on the LiteNet Phone System.
- [X] **/delete** - Remove your extension from the LiteNet Phone System.
- [X] **/list** - List all extensions on the LiteNet Phone System.
- [X] **/button** - Send the "Get an extension" button! *(Requires default_member_permissions: 0)*
## Admin Commands
*(Requires default_member_permissions: 0)*
- [X] **/admin**
- [X] **silence** - Kill all ongoing calls.
- [X] **reload** - Run an Asterisk reload.
- [X] **reboot** - Reboot the server. *(LAST RESORT)*
## Developer Commands
*(Requires default_member_permissions: 0)*
- [X] **/dev**
- [X] **fwconsole** - Run an `fwconsole` command.
- [X] **command** (required) - The command to run.
- [X] **asterisk** - Run an Asterisk CLI command.
- [X] **command** (required) - The command to run.
- [X] **shell** - Run a shell command.
- [X] **command** (required) - The command to run.
- [X] **restart** - Restart the bot.
## Call Detail Records (CDR)
- [ ] **/cdr** - Get the call detail records for your extension.
- [ ] **start_date** (optional) - The start date for the CDR (mm/dd/yyyy).
- [ ] **end_date** (optional) - The end date for the CDR (mm/dd/yyyy).
## Management Commands (For admins to manage extensions)
*(Requires default_member_permissions: 0)*
- [X] **/manage**
- [X] **create** - Create an extension.
- [X] **extension** (required) - The extension number.
- [X] **discord user** (required) - The Discord user to assign the extension to.
- [X] **delete** - Delete an extension.
- [X] **discord user** (required) - The Discord user whose extension is to be deleted.
- [X] **lookup** - Lookup an extension. (With creds)
- [X] **extension** (required) - The extension number.
## Buttons (On button message)
- [X] **Get an Extension** - Sends the "Get an extension" button.
- [X] **See Your Info** - Sends the "See your info" button.
- [X] **Delete Your Extension** - Sends the "Delete your extension" button.
## Other Features
- [ ] **Extension List on Discord** - List all extensions on the Discord server. (Will do later)

View file

@ -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
}
]
},

View file

@ -1,4 +1,4 @@
[ module.exports = [
{ {
"name": "whoami", "name": "whoami",
"description": "Get your extension info if you have one", "description": "Get your extension info if you have one",
@ -18,13 +18,7 @@
"name": "confirm", "name": "confirm",
"description": "Confirm that you want to delete your extension. THIS CANNOT BE UNDONE!", "description": "Confirm that you want to delete your extension. THIS CANNOT BE UNDONE!",
"type": 5, "type": 5,
"required": true, "required": true
"choices": [
{
"name": "yes",
"value": "yes"
}
]
} }
] ]
}, },
@ -39,19 +33,20 @@
"type": 1, "type": 1,
"default_member_permissions": 0 "default_member_permissions": 0
}, },
{ // 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)", // "name": "name",
"type": 1, // "description": "Change your extension's name (Defaults to your Discord name)",
"options": [ // "type": 1,
{ // "options": [
"name": "name", // {
"description": "The new name for your extension", // "name": "name",
"type": 3, // "description": "The new name for your extension",
"required": false // "type": 3,
} // "required": false
] // }
}, // ]
// },
{ {
"name": "admin", "name": "admin",
"description": "Admin only commands", "description": "Admin only commands",
@ -154,17 +149,82 @@
] ]
}, },
{ {
"name": "Lookup Extension", "name": "lookup",
"type": 2 "description": "Find extension by Discord user",
}, "type": 1,
"options": [
{ {
"name": "Create Extension", "name": "user",
"type": 2, "description": "The Discord user to lookup",
"default_member_permissions": 0 "type": 6,
}, "required": true
{ }
"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
}
]
},
{
"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
}
]
}
]
} }
] ]

View file

@ -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"
}
}

View file

@ -1,9 +0,0 @@
{
"controls": [
{
"title": "Phone System Controls",
"color": 205442,
"description": "Use the buttons below to control your extension!"
}
]
}

185
freepbx.js Normal file
View file

@ -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;

View file

@ -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;
}
},
}

1775
index.js

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,68 @@
const { exec } = require('child_process');
const Discord = require('discord.js');
const pool = global.pool
const fpbx = global.fpbx
const client = global.client
const log = global.log
const runCommand = (command, onData) => {
return new Promise((resolve, reject) => {
const process = exec(command);
process.stdout.on('data', (data) => {
if (onData) {
onData(data);
}
});
process.stderr.on('data', (data) => {
reject(`stderr: ${data}`);
});
process.on('close', (code) => {
if (code !== 0) {
reject(`process exited with code ${code}`);
return;
}
resolve('Command executed successfully');
});
process.on('error', (error) => {
reject(`error: ${error.message}`);
});
});
}
module.exports = {};
module.exports.execute = async (interaction) => {
const subcommand = interaction.options.getSubcommand();
switch (subcommand) {
case 'silence': // Run `asterisk -x "channel request hangup all"
runCommand('asterisk -x "channel request hangup all"').then((res) => {
interaction.reply({ content: `Silenced`, ephemeral: true });
});
break;
case 'reload': // Run `fwconsole reload`
await interaction.deferReply({ ephemeral: true });
runCommand('fwconsole reload', (data) => {
interaction.editReply({ content: data, ephemeral: true });
});
break;
case 'reboot': // Run `reboot 0`
await interaction.reply({ content: "Rebooting...", ephemeral: true });
await client.destroy();
log.info('Client destroyed.');
pool.end((err) => {
if (err) {
log.error('Error closing database pool:', err);
} else {
log.info('Database pool closed.');
}
});
runCommand('reboot 0');
break;
}
}

View file

@ -0,0 +1,66 @@
const pool = global.pool
const fpbx = global.fpbx
const client = global.client
const log = global.log
const Discord = require("discord.js")
module.exports = {};
module.exports.execute = async (interaction) => {
interaction.channel.send({
embeds: [{
"title": "The LiteNet Community PBX",
"description": "The LiteNet Community PBX is hosted through, and is sponsored by SnakeCraft Hosting!\nOffering affordable game hosting, Discord bot hosting, and VPS services since 2020.\nGet started at https://go.litenet.tel/sch-affiliate\nCheck them out on [Discord](https://discord.gg/invite/xcnKUD8)\n\n-# The link above is an affiliate link. We will receive credits from any purchase made via this link.\n-# SnakeCraft Hosting has no administrative control over, nor has access to private information stored on LiteNet. SnakeCraft Hosting provides hosting for LiteNet free of charge. Specific details regarding their affiliate program can be found [here](https://my.snakecrafthosting.com/index.php?rp=/knowledgebase/4/Affiliate-Program-FAQs.html)",
"color": 7955428,
"fields": [
{
"name": "What's this?",
"value": "The community PBX is a public, free to use [FreePBX](https://freepbx.org) based phone system that any server member is welcome to get a number on!\nEveryone on the system has their own 4 digit number, that can be used to call between other members on the system.\nThe PBX runs on a SnakeCraft Hosting VPS graciously provided to us at no cost!"
},
{
"name": "What can it do?",
"value": "The LiteNet phone system offers many features, including but not limited to the following:\n- Free inbound/outbound calling via +1 (610) LITENET (548-3638)\n- Private Voicemail\n- Intercom/Paging\n- Conference Rooms\n- Direct dial access to [AstroCom](https://astrocom.tel)\n- [Full extension status page](https://pbx.litenet.tel/status)\n- And more!"
},
{
"name": "Privacy Policy",
"value": "LiteNet respects the privacy of all members, and as such, only very few select staff members have access to the system files directly. Voicemails are not tracked nor listened to under any circumstances. Call logs are kept and only reviewed during investigations into violations of community guidelines, or possible illegal activity. Call recordings may be kept at the request of any individual member, and will NOT be reviewed unless prior permission was given from said member.\nAll user data may be deleted by request, or by simply running the `/delete` command.\n\nIf you believe your privacy has been violated in any way, please don't hesistate to reach out to any of our staff members!"
},
{
"name": "Can it do `X`?",
"value": "Any specific questions are welcome to be asked in our <#1102782499756724239> chat!\nIf you have a suggestion for something we should add to the PBX or Discord server, feel free to leave it in <#1148099609428762634>!"
}
],
"footer": {
"text": "Made with <3 by Chris Chrome & The LiteNet Team • Sponsored by SnakeCraft Hosting",
"icon_url": "https://f.chrischro.me/assets/Snakecraft-Social-Media-purple-v2-smaller.png"
},
"image": {
"url": "https://f.chrischro.me/assets/litenet-full.png"
},
"thumbnail": {
"url": "https://f.chrischro.me/assets/Snakecraft-Social-Media-purple-v2-smaller-rounder.png"
}
}],
components: [
{
type: 1,
components: [
{
type: Discord.ComponentType.Button,
label: "Get an Extension",
emoji: "✅",
style: Discord.ButtonStyle.Success,
custom_id: "newExtension"
},
{
type: Discord.ComponentType.Button,
label: "Get your extension info",
emoji: "",
style: Discord.ButtonStyle.Primary,
custom_id: "getExtensionInfo"
}
]
}
]
})
}

View file

@ -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 });
});
}

View file

@ -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;
}
}

View file

@ -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!" });
}

View file

@ -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 });
}

View file

@ -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;
}
}

View file

@ -0,0 +1,3 @@
module.exports = {};
module.exports.execute = require("../common/createExt").execute;

View file

@ -0,0 +1,3 @@
module.exports = {};
module.exports.execute = require("../common/getExtInfo").execute;

View file

@ -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 });
}

View file

@ -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 });
});
}

View file

@ -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
}
]
});
}

View file

@ -0,0 +1,3 @@
module.exports = {};
module.exports.execute = require("../common/getExtInfo").execute;

View file

@ -0,0 +1,3 @@
module.exports = {};
module.exports.execute = require("../common/createExt").execute;

72
migrations.js Normal file
View file

@ -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

View file

@ -0,0 +1 @@
ALTER TABLE users ADD CONSTRAINT unique_extension UNIQUE (extension);

View file

@ -0,0 +1,4 @@
CREATE TABLE IF NOT EXISTS discord_users (
extension VARCHAR(20) PRIMARY KEY,
discordId VARCHAR(25) NOT NULL
);

392
package-lock.json generated
View file

@ -12,6 +12,7 @@
"axios": "^1.6.0", "axios": "^1.6.0",
"colors": "^1.4.0", "colors": "^1.4.0",
"discord.js": "14.14.1", "discord.js": "14.14.1",
"dotenv": "^16.4.7",
"freepbx-graphql-client": "^0.1.1", "freepbx-graphql-client": "^0.1.1",
"mariadb": "^3.2.0", "mariadb": "^3.2.0",
"ping": "^0.4.4", "ping": "^0.4.4",
@ -20,85 +21,230 @@
} }
}, },
"node_modules/@discordjs/builders": { "node_modules/@discordjs/builders": {
"version": "1.6.5", "version": "1.10.0",
"resolved": "https://registry.npmjs.org/@discordjs/builders/-/builders-1.6.5.tgz", "resolved": "https://registry.npmjs.org/@discordjs/builders/-/builders-1.10.0.tgz",
"integrity": "sha512-SdweyCs/+mHj+PNhGLLle7RrRFX9ZAhzynHahMCLqp5Zeq7np7XC6/mgzHc79QoVlQ1zZtOkTTiJpOZu5V8Ufg==", "integrity": "sha512-ikVZsZP+3shmVJ5S1oM+7SveUCK3L9fTyfA8aJ7uD9cNQlTqF+3Irbk2Y22KXTb3C3RNUahRkSInClJMkHrINg==",
"license": "Apache-2.0",
"dependencies": { "dependencies": {
"@discordjs/formatters": "^0.3.2", "@discordjs/formatters": "^0.6.0",
"@discordjs/util": "^1.0.1", "@discordjs/util": "^1.1.1",
"@sapphire/shapeshift": "^3.9.2", "@sapphire/shapeshift": "^4.0.0",
"discord-api-types": "0.37.50", "discord-api-types": "^0.37.114",
"fast-deep-equal": "^3.1.3", "fast-deep-equal": "^3.1.3",
"ts-mixer": "^6.0.3", "ts-mixer": "^6.0.4",
"tslib": "^2.6.1" "tslib": "^2.6.3"
}, },
"engines": { "engines": {
"node": ">=16.11.0" "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": { "node_modules/@discordjs/collection": {
"version": "1.5.3", "version": "1.5.3",
"resolved": "https://registry.npmjs.org/@discordjs/collection/-/collection-1.5.3.tgz", "resolved": "https://registry.npmjs.org/@discordjs/collection/-/collection-1.5.3.tgz",
"integrity": "sha512-SVb428OMd3WO1paV3rm6tSjM4wC+Kecaa1EUGX7vc6/fddvw/6lg90z4QtCqm21zvVe92vMMDt9+DkIvjXImQQ==", "integrity": "sha512-SVb428OMd3WO1paV3rm6tSjM4wC+Kecaa1EUGX7vc6/fddvw/6lg90z4QtCqm21zvVe92vMMDt9+DkIvjXImQQ==",
"license": "Apache-2.0",
"engines": { "engines": {
"node": ">=16.11.0" "node": ">=16.11.0"
} }
}, },
"node_modules/@discordjs/formatters": { "node_modules/@discordjs/formatters": {
"version": "0.3.2", "version": "0.3.3",
"resolved": "https://registry.npmjs.org/@discordjs/formatters/-/formatters-0.3.2.tgz", "resolved": "https://registry.npmjs.org/@discordjs/formatters/-/formatters-0.3.3.tgz",
"integrity": "sha512-lE++JZK8LSSDRM5nLjhuvWhGuKiXqu+JZ/DsOR89DVVia3z9fdCJVcHF2W/1Zxgq0re7kCzmAJlCMMX3tetKpA==", "integrity": "sha512-wTcI1Q5cps1eSGhl6+6AzzZkBBlVrBdc9IUhJbijRgVjCNIIIZPgqnUj3ntFODsHrdbGU8BEG9XmDQmgEEYn3w==",
"license": "Apache-2.0",
"dependencies": { "dependencies": {
"discord-api-types": "0.37.50" "discord-api-types": "0.37.61"
}, },
"engines": { "engines": {
"node": ">=16.11.0" "node": ">=16.11.0"
} }
}, },
"node_modules/@discordjs/rest": { "node_modules/@discordjs/rest": {
"version": "2.0.1", "version": "2.4.2",
"resolved": "https://registry.npmjs.org/@discordjs/rest/-/rest-2.0.1.tgz", "resolved": "https://registry.npmjs.org/@discordjs/rest/-/rest-2.4.2.tgz",
"integrity": "sha512-/eWAdDRvwX/rIE2tuQUmKaxmWeHmGealttIzGzlYfI4+a7y9b6ZoMp8BG/jaohs8D8iEnCNYaZiOFLVFLQb8Zg==", "integrity": "sha512-9bOvXYLQd5IBg/kKGuEFq3cstVxAMJ6wMxO2U3wjrgO+lHv8oNCT+BBRpuzVQh7BoXKvk/gpajceGvQUiRoJ8g==",
"license": "Apache-2.0",
"dependencies": { "dependencies": {
"@discordjs/collection": "^1.5.3", "@discordjs/collection": "^2.1.1",
"@discordjs/util": "^1.0.1", "@discordjs/util": "^1.1.1",
"@sapphire/async-queue": "^1.5.0", "@sapphire/async-queue": "^1.5.3",
"@sapphire/snowflake": "^3.5.1", "@sapphire/snowflake": "^3.5.3",
"@vladfrangu/async_event_emitter": "^2.2.2", "@vladfrangu/async_event_emitter": "^2.4.6",
"discord-api-types": "0.37.50", "discord-api-types": "^0.37.114",
"magic-bytes.js": "^1.0.15", "magic-bytes.js": "^1.10.0",
"tslib": "^2.6.1", "tslib": "^2.6.3",
"undici": "5.22.1" "undici": "6.19.8"
}, },
"engines": { "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": { "node_modules/@discordjs/util": {
"version": "1.0.1", "version": "1.1.1",
"resolved": "https://registry.npmjs.org/@discordjs/util/-/util-1.0.1.tgz", "resolved": "https://registry.npmjs.org/@discordjs/util/-/util-1.1.1.tgz",
"integrity": "sha512-d0N2yCxB8r4bn00/hvFZwM7goDcUhtViC5un4hPj73Ba4yrChLSJD8fy7Ps5jpTLg1fE9n4K0xBLc1y9WGwSsA==", "integrity": "sha512-eddz6UnOBEB1oITPinyrB2Pttej49M9FZQY8NxgEvc3tq6ZICZ19m70RsmzRdDHk80O9NoYN/25AqJl8vPVf/g==",
"license": "Apache-2.0",
"engines": { "engines": {
"node": ">=16.11.0" "node": ">=18"
},
"funding": {
"url": "https://github.com/discordjs/discord.js?sponsor"
} }
}, },
"node_modules/@discordjs/ws": { "node_modules/@discordjs/ws": {
"version": "1.0.1", "version": "1.2.0",
"resolved": "https://registry.npmjs.org/@discordjs/ws/-/ws-1.0.1.tgz", "resolved": "https://registry.npmjs.org/@discordjs/ws/-/ws-1.2.0.tgz",
"integrity": "sha512-avvAolBqN3yrSvdBPcJ/0j2g42ABzrv3PEL76e3YTp2WYMGH7cuspkjfSyNWaqYl1J+669dlLp+YFMxSVQyS5g==", "integrity": "sha512-QH5CAFe3wHDiedbO+EI3OOiyipwWd+Q6BdoFZUw/Wf2fw5Cv2fgU/9UEtJRmJa9RecI+TAhdGPadMaEIur5yJg==",
"license": "Apache-2.0",
"dependencies": { "dependencies": {
"@discordjs/collection": "^1.5.3", "@discordjs/collection": "^2.1.0",
"@discordjs/rest": "^2.0.1", "@discordjs/rest": "^2.4.1",
"@discordjs/util": "^1.0.1", "@discordjs/util": "^1.1.0",
"@sapphire/async-queue": "^1.5.0", "@sapphire/async-queue": "^1.5.2",
"@types/ws": "^8.5.5", "@types/ws": "^8.5.10",
"@vladfrangu/async_event_emitter": "^2.2.2", "@vladfrangu/async_event_emitter": "^2.2.4",
"discord-api-types": "0.37.50", "discord-api-types": "^0.37.114",
"tslib": "^2.6.1", "tslib": "^2.6.2",
"ws": "^8.13.0" "ws": "^8.17.0"
}, },
"engines": { "engines": {
"node": ">=16.11.0" "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": { "node_modules/@gar/promisify": {
@ -151,31 +297,33 @@
} }
}, },
"node_modules/@sapphire/async-queue": { "node_modules/@sapphire/async-queue": {
"version": "1.5.0", "version": "1.5.5",
"resolved": "https://registry.npmjs.org/@sapphire/async-queue/-/async-queue-1.5.0.tgz", "resolved": "https://registry.npmjs.org/@sapphire/async-queue/-/async-queue-1.5.5.tgz",
"integrity": "sha512-JkLdIsP8fPAdh9ZZjrbHWR/+mZj0wvKS5ICibcLrRI1j84UmLMshx5n9QmL8b95d4onJ2xxiyugTgSAX7AalmA==", "integrity": "sha512-cvGzxbba6sav2zZkH8GPf2oGk9yYoD5qrNWdu9fRehifgnFZJMV+nuy2nON2roRO4yQQ+v7MK/Pktl/HgfsUXg==",
"license": "MIT",
"engines": { "engines": {
"node": ">=v14.0.0", "node": ">=v14.0.0",
"npm": ">=7.0.0" "npm": ">=7.0.0"
} }
}, },
"node_modules/@sapphire/shapeshift": { "node_modules/@sapphire/shapeshift": {
"version": "3.9.2", "version": "4.0.0",
"resolved": "https://registry.npmjs.org/@sapphire/shapeshift/-/shapeshift-3.9.2.tgz", "resolved": "https://registry.npmjs.org/@sapphire/shapeshift/-/shapeshift-4.0.0.tgz",
"integrity": "sha512-YRbCXWy969oGIdqR/wha62eX8GNHsvyYi0Rfd4rNW6tSVVa8p0ELiMEuOH/k8rgtvRoM+EMV7Csqz77YdwiDpA==", "integrity": "sha512-d9dUmWVA7MMiKobL3VpLF8P2aeanRTu6ypG2OIaEv/ZHH/SUQ2iHOVyi5wAPjQ+HmnMuL0whK9ez8I/raWbtIg==",
"license": "MIT",
"dependencies": { "dependencies": {
"fast-deep-equal": "^3.1.3", "fast-deep-equal": "^3.1.3",
"lodash": "^4.17.21" "lodash": "^4.17.21"
}, },
"engines": { "engines": {
"node": ">=v14.0.0", "node": ">=v16"
"npm": ">=7.0.0"
} }
}, },
"node_modules/@sapphire/snowflake": { "node_modules/@sapphire/snowflake": {
"version": "3.5.1", "version": "3.5.1",
"resolved": "https://registry.npmjs.org/@sapphire/snowflake/-/snowflake-3.5.1.tgz", "resolved": "https://registry.npmjs.org/@sapphire/snowflake/-/snowflake-3.5.1.tgz",
"integrity": "sha512-BxcYGzgEsdlG0dKAyOm0ehLGm2CafIrfQTZGWgkfKYbj+pNNsorZ7EotuZukc2MT70E0UbppVbtpBrqpzVzjNA==", "integrity": "sha512-BxcYGzgEsdlG0dKAyOm0ehLGm2CafIrfQTZGWgkfKYbj+pNNsorZ7EotuZukc2MT70E0UbppVbtpBrqpzVzjNA==",
"license": "MIT",
"engines": { "engines": {
"node": ">=v14.0.0", "node": ">=v14.0.0",
"npm": ">=7.0.0" "npm": ">=7.0.0"
@ -196,22 +344,28 @@
"integrity": "sha512-Nmh0K3iWQJzniTuPRcJn5hxXkfB1T1pgB89SBig5PlJQU5yocazeu4jATJlaA0GYFKWMqDdvYemoSnF2pXgLVA==" "integrity": "sha512-Nmh0K3iWQJzniTuPRcJn5hxXkfB1T1pgB89SBig5PlJQU5yocazeu4jATJlaA0GYFKWMqDdvYemoSnF2pXgLVA=="
}, },
"node_modules/@types/node": { "node_modules/@types/node": {
"version": "20.5.7", "version": "22.10.10",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.5.7.tgz", "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.10.tgz",
"integrity": "sha512-dP7f3LdZIysZnmvP3ANJYTSwg+wLLl8p7RqniVlV7j+oXSXAbt9h0WIBFmJy5inWZoX9wZN6eXx+YXd9Rh3RBA==" "integrity": "sha512-X47y/mPNzxviAGY5TcYPtYL8JsY3kAq2n8fMmKoRCxq/c4v4pyGNCzM2R6+M5/umG4ZfHuT+sgqDYqWc9rJ6ww==",
"license": "MIT",
"dependencies": {
"undici-types": "~6.20.0"
}
}, },
"node_modules/@types/ws": { "node_modules/@types/ws": {
"version": "8.5.5", "version": "8.5.9",
"resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.5.tgz", "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.9.tgz",
"integrity": "sha512-lwhs8hktwxSjf9UaZ9tG5M03PGogvFaH8gUgLNbN9HKIg0dvv6q+gkSuJ8HN4/VbyxkuLzCjlN7GquQ0gUJfIg==", "integrity": "sha512-jbdrY0a8lxfdTp/+r7Z4CkycbOFN8WX+IOchLJr3juT/xzbJ8URyTVSJ/hvNdadTgM1mnedb47n+Y31GsFnQlg==",
"license": "MIT",
"dependencies": { "dependencies": {
"@types/node": "*" "@types/node": "*"
} }
}, },
"node_modules/@vladfrangu/async_event_emitter": { "node_modules/@vladfrangu/async_event_emitter": {
"version": "2.2.2", "version": "2.4.6",
"resolved": "https://registry.npmjs.org/@vladfrangu/async_event_emitter/-/async_event_emitter-2.2.2.tgz", "resolved": "https://registry.npmjs.org/@vladfrangu/async_event_emitter/-/async_event_emitter-2.4.6.tgz",
"integrity": "sha512-HIzRG7sy88UZjBJamssEczH5q7t5+axva19UbZLO6u0ySbYPrwzWiXBcC0WuHyhKKoeCyneH+FvYzKQq/zTtkQ==", "integrity": "sha512-RaI5qZo6D2CVS6sTHFKg1v5Ohq/+Bo2LZ5gzUEwZ/WkHhwtGTCB/sVLw8ijOkAUxasZ+WshN/Rzj4ywsABJ5ZA==",
"license": "MIT",
"engines": { "engines": {
"node": ">=v14.0.0", "node": ">=v14.0.0",
"npm": ">=7.0.0" "npm": ">=7.0.0"
@ -350,17 +504,6 @@
"node": ">=10.0.0" "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": { "node_modules/cacache": {
"version": "15.3.0", "version": "15.3.0",
"resolved": "https://registry.npmjs.org/cacache/-/cacache-15.3.0.tgz", "resolved": "https://registry.npmjs.org/cacache/-/cacache-15.3.0.tgz",
@ -419,6 +562,7 @@
"version": "1.4.0", "version": "1.4.0",
"resolved": "https://registry.npmjs.org/colors/-/colors-1.4.0.tgz", "resolved": "https://registry.npmjs.org/colors/-/colors-1.4.0.tgz",
"integrity": "sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA==", "integrity": "sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA==",
"license": "MIT",
"engines": { "engines": {
"node": ">=0.1.90" "node": ">=0.1.90"
} }
@ -512,34 +656,48 @@
} }
}, },
"node_modules/discord-api-types": { "node_modules/discord-api-types": {
"version": "0.37.50", "version": "0.37.61",
"resolved": "https://registry.npmjs.org/discord-api-types/-/discord-api-types-0.37.50.tgz", "resolved": "https://registry.npmjs.org/discord-api-types/-/discord-api-types-0.37.61.tgz",
"integrity": "sha512-X4CDiMnDbA3s3RaUXWXmgAIbY1uxab3fqe3qwzg5XutR3wjqi7M3IkgQbsIBzpqBN2YWr/Qdv7JrFRqSgb4TFg==" "integrity": "sha512-o/dXNFfhBpYHpQFdT6FWzeO7pKc838QeeZ9d91CfVAtpr5XLK4B/zYxQbYgPdoMiTDvJfzcsLW5naXgmHGDNXw==",
"license": "MIT"
}, },
"node_modules/discord.js": { "node_modules/discord.js": {
"version": "14.13.0", "version": "14.14.1",
"resolved": "https://registry.npmjs.org/discord.js/-/discord.js-14.13.0.tgz", "resolved": "https://registry.npmjs.org/discord.js/-/discord.js-14.14.1.tgz",
"integrity": "sha512-Kufdvg7fpyTEwANGy9x7i4od4yu5c6gVddGi5CKm4Y5a6sF0VBODObI3o0Bh7TGCj0LfNT8Qp8z04wnLFzgnbA==", "integrity": "sha512-/hUVzkIerxKHyRKopJy5xejp4MYKDPTszAnpYxzVVv4qJYf+Tkt+jnT2N29PIPschicaEEpXwF2ARrTYHYwQ5w==",
"license": "Apache-2.0",
"dependencies": { "dependencies": {
"@discordjs/builders": "^1.6.5", "@discordjs/builders": "^1.7.0",
"@discordjs/collection": "^1.5.3", "@discordjs/collection": "1.5.3",
"@discordjs/formatters": "^0.3.2", "@discordjs/formatters": "^0.3.3",
"@discordjs/rest": "^2.0.1", "@discordjs/rest": "^2.1.0",
"@discordjs/util": "^1.0.1", "@discordjs/util": "^1.0.2",
"@discordjs/ws": "^1.0.1", "@discordjs/ws": "^1.0.2",
"@sapphire/snowflake": "^3.5.1", "@sapphire/snowflake": "3.5.1",
"@types/ws": "^8.5.5", "@types/ws": "8.5.9",
"discord-api-types": "0.37.50", "discord-api-types": "0.37.61",
"fast-deep-equal": "^3.1.3", "fast-deep-equal": "3.1.3",
"lodash.snakecase": "^4.1.1", "lodash.snakecase": "4.1.1",
"tslib": "^2.6.1", "tslib": "2.6.2",
"undici": "5.22.1", "undici": "5.27.2",
"ws": "^8.13.0" "ws": "8.14.2"
}, },
"engines": { "engines": {
"node": ">=16.11.0" "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": { "node_modules/emoji-regex": {
"version": "8.0.0", "version": "8.0.0",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
@ -583,7 +741,8 @@
"node_modules/fast-deep-equal": { "node_modules/fast-deep-equal": {
"version": "3.1.3", "version": "3.1.3",
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", "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": { "node_modules/follow-redirects": {
"version": "1.15.3", "version": "1.15.3",
@ -621,6 +780,7 @@
"version": "0.1.1", "version": "0.1.1",
"resolved": "https://registry.npmjs.org/freepbx-graphql-client/-/freepbx-graphql-client-0.1.1.tgz", "resolved": "https://registry.npmjs.org/freepbx-graphql-client/-/freepbx-graphql-client-0.1.1.tgz",
"integrity": "sha512-JqDTlL0EA/bUMit9aODIupWSqF87WHWrCD6i716FzeOzS46cMFq/OajzftTMwOZQf20MMkJM2HI6CRnNlRGl6A==", "integrity": "sha512-JqDTlL0EA/bUMit9aODIupWSqF87WHWrCD6i716FzeOzS46cMFq/OajzftTMwOZQf20MMkJM2HI6CRnNlRGl6A==",
"license": "MIT",
"dependencies": { "dependencies": {
"graphql": "^15.6.1", "graphql": "^15.6.1",
"graphql-request": "^3.6.1" "graphql-request": "^3.6.1"
@ -831,7 +991,8 @@
"node_modules/lodash": { "node_modules/lodash": {
"version": "4.17.21", "version": "4.17.21",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", "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": { "node_modules/lodash.snakecase": {
"version": "4.1.1", "version": "4.1.1",
@ -850,9 +1011,10 @@
} }
}, },
"node_modules/magic-bytes.js": { "node_modules/magic-bytes.js": {
"version": "1.0.15", "version": "1.10.0",
"resolved": "https://registry.npmjs.org/magic-bytes.js/-/magic-bytes.js-1.0.15.tgz", "resolved": "https://registry.npmjs.org/magic-bytes.js/-/magic-bytes.js-1.10.0.tgz",
"integrity": "sha512-bpRmwbRHqongRhA+mXzbLWjVy7ylqmfMBYaQkSs6pac0z6hBTvsgrH0r4FBYd/UYVJBmS6Rp/O+oCCQVLzKV1g==" "integrity": "sha512-/k20Lg2q8LE5xiaaSkMXk4sfvI+9EGEykFS4b0CHHGWqDYU0bGUFSwchNOMA56D7TCs9GwVTkqe9als1/ns8UQ==",
"license": "MIT"
}, },
"node_modules/make-dir": { "node_modules/make-dir": {
"version": "3.1.0", "version": "3.1.0",
@ -1444,14 +1606,6 @@
"node": ">= 8" "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": { "node_modules/string_decoder": {
"version": "1.3.0", "version": "1.3.0",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
@ -1514,14 +1668,16 @@
"integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="
}, },
"node_modules/ts-mixer": { "node_modules/ts-mixer": {
"version": "6.0.3", "version": "6.0.4",
"resolved": "https://registry.npmjs.org/ts-mixer/-/ts-mixer-6.0.3.tgz", "resolved": "https://registry.npmjs.org/ts-mixer/-/ts-mixer-6.0.4.tgz",
"integrity": "sha512-k43M7uCG1AkTyxgnmI5MPwKoUvS/bRvLvUb7+Pgpdlmok8AoqmUaZxUUw8zKM5B1lqZrt41GjYgnvAi0fppqgQ==" "integrity": "sha512-ufKpbmrugz5Aou4wcr5Wc1UUFWOLhq+Fm6qa6P0w0K5Qw2yhaUoiWszhCVuNQyNwrlGiscHOmqYoAox1PtvgjA==",
"license": "MIT"
}, },
"node_modules/tslib": { "node_modules/tslib": {
"version": "2.6.2", "version": "2.6.2",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", "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": { "node_modules/tweetnacl": {
"version": "0.14.5", "version": "0.14.5",
@ -1529,16 +1685,23 @@
"integrity": "sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA==" "integrity": "sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA=="
}, },
"node_modules/undici": { "node_modules/undici": {
"version": "5.22.1", "version": "5.27.2",
"resolved": "https://registry.npmjs.org/undici/-/undici-5.22.1.tgz", "resolved": "https://registry.npmjs.org/undici/-/undici-5.27.2.tgz",
"integrity": "sha512-Ji2IJhFXZY0x/0tVBXeQwgPlLWw13GVzpsWPQ3rV50IFMMof2I55PZZxtm4P6iNq+L5znYN9nSTAq0ZyE6lSJw==", "integrity": "sha512-iS857PdOEy/y3wlM3yRp+6SNQQ6xU0mmZcwRSriqk+et/cwWAtwmIGf6WkoDN2EK/AMdCO/dfXzIwi+rFMrjjQ==",
"license": "MIT",
"dependencies": { "dependencies": {
"busboy": "^1.6.0" "@fastify/busboy": "^2.0.0"
}, },
"engines": { "engines": {
"node": ">=14.0" "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": { "node_modules/unique-filename": {
"version": "1.1.1", "version": "1.1.1",
"resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-1.1.1.tgz", "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-1.1.1.tgz",
@ -1605,9 +1768,10 @@
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="
}, },
"node_modules/ws": { "node_modules/ws": {
"version": "8.13.0", "version": "8.14.2",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.13.0.tgz", "resolved": "https://registry.npmjs.org/ws/-/ws-8.14.2.tgz",
"integrity": "sha512-x9vcZYTrFPC7aSIbj7sRCYo7L/Xb8Iy+pW0ng0wt2vCJv7M9HOMy0UoN3rr+IFC7hb7vXoqS+P9ktyLLLhO+LA==", "integrity": "sha512-wEBG1ftX4jcglPxgFCMJmZ2PLtSbJ2Peg6TmpJFTbe9GZYOQCDPdMYu/Tm0/bGZkw8paZnJY45J4K2PZrLYq8g==",
"license": "MIT",
"engines": { "engines": {
"node": ">=10.0.0" "node": ">=10.0.0"
}, },

View file

@ -4,7 +4,8 @@
"description": "", "description": "",
"main": "index.js", "main": "index.js",
"scripts": { "scripts": {
"test": "echo \"Error: no test specified\" && exit 1" "test": "echo \"Error: no test specified\" && exit 1",
"start": "node index.js"
}, },
"author": "", "author": "",
"license": "ISC", "license": "ISC",
@ -12,6 +13,7 @@
"axios": "^1.6.0", "axios": "^1.6.0",
"colors": "^1.4.0", "colors": "^1.4.0",
"discord.js": "14.14.1", "discord.js": "14.14.1",
"dotenv": "^16.4.7",
"freepbx-graphql-client": "^0.1.1", "freepbx-graphql-client": "^0.1.1",
"mariadb": "^3.2.0", "mariadb": "^3.2.0",
"ping": "^0.4.4", "ping": "^0.4.4",

View file

@ -1,6 +0,0 @@
[
{
"name": "Test",
"value": "700"
}
]