Compare commits

...

50 commits

Author SHA1 Message Date
Christopher Cookman e2c3da732b Sort listed orphans 2025-06-20 03:15:10 -06:00
Christopher Cookman d13a814bb6 Test fix list-orphans 2025-06-20 03:13:41 -06:00
Christopher Cookman 3bea94bee0 Add list-orphans command 2025-06-20 03:12:15 -06:00
Christopher Cookman 5f6cb95f52 Removed unfinished code 2025-06-20 03:06:52 -06:00
Christopher Cookman 1253de3bb4 Add link/unlink commands for staff 2025-06-20 03:05:24 -06:00
Christopher Cookman a7d4a91936 Remove dupes 2025-05-24 14:14:27 -06:00
Christopher Cookman 33d71ae1a6 Add logging. guh 2025-05-24 14:13:43 -06:00
Christopher Cookman fd27047cbb Remove full list from debug 2025-05-24 14:11:50 -06:00
Christopher Cookman 53fec894f7 Fix dingus 2025-05-24 14:11:14 -06:00
Christopher Cookman a7f52bb195 Fix debug 2025-05-24 14:09:07 -06:00
Christopher Cookman a87e1c92d5 Add simple debug program 2025-05-24 14:08:21 -06:00
Christopher Cookman 5dd4476f6a Prob not the best way to do it, but itll work 2025-02-17 10:11:05 -07:00
Christopher Cookman 44ff217949 Test fix for ext deletion 2025-02-17 10:09:01 -07:00
Christopher Cookman 41e80eebf9 Add debug logging to deletions 2025-02-17 10:06:44 -07:00
Christopher Cookman ef7d13cba1 Add catch to deletion system 2025-02-11 10:30:31 -07:00
Christopher Cookman 8a6d714ca1 Forgot to remove from users table, whoops 2025-02-11 10:25:05 -07:00
Christopher Cookman 5bd45cc801 Update admin commands 2025-02-11 09:07:05 -07:00
Christopher Cookman 6f3169d564 Add error msg to deletions 2025-02-11 09:01:45 -07:00
Christopher Cookman d9b52d841a Guuuh 2025-02-11 08:52:22 -07:00
Christopher Cookman b3943d3cdb Update URL in readme 2025-02-11 08:50:07 -07:00
Christopher Cookman a4f4b17198 Update README.md with updated setup instructions 2025-02-11 08:49:28 -07:00
Christopher Cookman 09b4eeabd6 Update .env.example 2025-02-11 08:40:50 -07:00
Christopher Cookman 192e979a9e Remove unneeded logs 2025-02-11 08:31:12 -07:00
Christopher Cookman 68d6402ffd - Disable unfinished CDR commands
- Impliment delayed auto-delete of orphaned extensions, with auto cancel.
2025-02-11 08:28:41 -07:00
Christopher Cookman 32cfeb354e Add PBX Role giver again 2025-02-01 19:15:12 -07:00
Christopher Cookman bb3e063987 Add placeholder for cdr command 2025-01-26 12:46:51 -07:00
Christopher Cookman b7559411c6 Add automatic client token renewal 2025-01-26 11:13:44 -07:00
Christopher Cookman 1ca300771f Sponsorship msg 2025-01-25 12:54:12 -07:00
Christopher Cookman 5cbb6ad445 Remove guild-command removal 2025-01-25 12:37:33 -07:00
Christopher Cookman 65dd44fd43 Remove non alphanumeric from names when creating extensions 2025-01-25 12:37:13 -07:00
Christopher Cookman 1c41dff700 Clear guild commands 2025-01-25 12:31:28 -07:00
Christopher Cookman 2989f40e3b Add reloads to paging lol 2025-01-25 12:30:30 -07:00
Christopher Cookman 6d585ce3af Merge pull request 'paging stuff from 2.0' (#3) from 2.0-rewrite into main
Reviewed-on: #3
2025-01-25 12:29:02 -07:00
Christopher Cookman b61e48c535 More page stuff 2025-01-25 12:27:35 -07:00
Christopher Cookman 2f59ff4147 Add paging stuff 2025-01-25 12:27:13 -07:00
Christopher Cookman 98ca59cd41 Merge pull request '2.0-rewrite' (#2) from 2.0-rewrite into main
Reviewed-on: #2
2025-01-25 12:06:49 -07:00
Christopher Cookman 0d2c8243a0 GUH 2025-01-25 12:02:53 -07:00
Christopher Cookman 5696df170f A lotta stuff 2025-01-25 11:49:40 -07:00
Christopher Cookman 2171b25dd6 nvm 2025-01-25 09:40:38 -07:00
Christopher Cookman 28b6201a3a Add some status stuff 2025-01-25 09:39:00 -07:00
Christopher Cookman 373b6e1926 Finishing touches to admin commands 2025-01-25 09:33:51 -07:00
Christopher Cookman db3cff0aa0 Kinda dumb, docker is a non-option lol 2025-01-25 09:29:13 -07:00
Christopher Cookman 14435d2345 Set up for docker compose file 2025-01-25 09:25:37 -07:00
Christopher Cookman f11f3905ca Debug admin 1 2025-01-25 09:17:34 -07:00
Christopher Cookman a6dcfeedb6 Do callbacks on admin commands 2025-01-25 09:15:14 -07:00
Christopher Cookman f26b8b22de Fix admin commands 2025-01-25 09:11:35 -07:00
Christopher Cookman 9f56b012b9 Bwuh 2025-01-25 09:10:16 -07:00
Christopher Cookman 5480402a8c Add admin commands 2025-01-25 09:06:26 -07:00
Christopher Cookman 1451549928 More commands, some buttons, and whatnot 2025-01-25 08:48:02 -07:00
Christopher Cookman 33d1c8ae9c Checkpoint; Full rewrite is underway, this shouldn't take long! 2025-01-25 07:40:09 -07:00
37 changed files with 1934 additions and 2175 deletions

11
.env.example Normal file
View file

@ -0,0 +1,11 @@
PBX_HOSTNAME=your_pbx_hostname
FREEPBX_URL=http://your_freepbx_url
FREEPBX_CLIENT_ID=your_freepbx_client_id
FREEPBX_CLIENT_SECRET=your_freepbx_client_secret
DB_HOST=your_db_host
DB_USER=your_db_user
DB_PASS=your_db_password
DISCORD_TOKEN=your_discord_token
OWNER_ID=your_owner_id
EXTENSION_ROLE_ID=your_extension_role_id
DISCORD_GUILD=your_discord_guild_id

2
.gitignore vendored
View file

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

View file

@ -24,7 +24,7 @@ To set up the config file, follow the steps below:
1. Clone the repository:
```shell
git clone https://github.com/ChrisChrome/discord-freepbx-manager.git
git clone https://git.chrischro.me/ChrisChrome/discord-freepbx-manager.git
cd discord-freepbx-manager
```
@ -33,63 +33,34 @@ To set up the config file, follow the steps below:
npm install --save
```
3. Create a new file named `config.json` in the root directory of the project and fill it with the following content:
```json
{
"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"
}
}
```
4. Replace the placeholders with your own values:
- `ntfyUrl`: The URL of the NTFY server.
- `sip-server-ip`: The IP address of the SIP server.
- `pbx-api-url`: The URL of the FreePBX API.
- `gql-client-id`: The client ID for the GraphQL API.
- `gql-secret`: The secret for the GraphQL API.
- `bot-token`: The token of the Discord bot.
- `guild-id`: The ID of the Discord guild.
- `user-role`: The ID of the role that users must have to use the bot.
- `log-channel`: The ID of the channel where logs will be sent.
- `extension-list-channel`: The ID of the channel where the extension list will be sent.
- `your-user-id`: Your Discord user ID.
- `db-hostname-here`: The hostname of the MariaDB server.
- Use `mysql -u root -p -e "CREATE USER 'bot'@'localhost' IDENTIFIED BY 'bot';"` to create a new user. Change `localhost` to the IP of the server running the bot if you aren't running the bot on the PBX server.
- `uptime-kuma-link`: The URL of the Uptime Kuma instance.
3. Copy `.env.example` to `.env`
4. Configure the `.env` file with the following options:
PBX_HOSTNAME: This is the hostname or IP address of your PBX (Private Branch Exchange) server. It is used to connect to the PBX system. This value is whats sent to the end users via the bot. Can be whatever and won't affect the functionality of the bot.
FREEPBX_URL: Base URL for the API of your PBX. i.e. `https://pbx.example.com`
FREEPBX_CLIENT_ID: This is the client ID used for authenticating with the FreePBX API. It is part of the OAuth2 authentication process.
FREEPBX_CLIENT_SECRET: This is the client secret used along with the client ID for authenticating with the FreePBX API.
DB_HOST: This is the hostname or IP address of your database server. It is used to connect to the database. If running on your PBX, set to `127.0.0.1`
DB_USER: This is the username used to authenticate with the database.
DB_PASS: This is the password used to authenticate with the database.
DISCORD_TOKEN: This is the token for your Discord bot. It is used to authenticate the bot with the Discord API.
OWNER_ID: This is the Discord user ID of the bot owner. It can be used to grant special permissions or access to the bot owner.
EXTENSION_ROLE_ID: Discord role ID of the role to give PBX users when they make their extension.
DISCORD_GUILD: This is the Discord server (guild) ID where the bot will operate. This is used to determine if extensions are orphaned, so make sure it's set correctly.
These environment variables are used to configure your application and provide necessary credentials and settings for connecting to various services. Make sure to keep this information secure and do not share it publicly.
5. Run the bot:
```shell
node .

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

307
commands.js Normal file
View file

@ -0,0 +1,307 @@
module.exports = [
{
"name": "whoami",
"description": "Get your extension info if you have one",
"type": 1
},
{
"name": "new",
"description": "Get an extension on the LiteNet Phone System",
"type": 1
},
{
"name": "delete",
"description": "Remove your extension from the LiteNet Phone System",
"type": 1,
"options": [
{
"name": "confirm",
"description": "Confirm that you want to delete your extension. THIS CANNOT BE UNDONE!",
"type": 5,
"required": true
}
]
},
{
"name": "paging",
"description": "Subcommands for managing your paging groups",
"type": 1,
"options": [
{
"name": "add",
"description": "Add yourself to a paging group",
"type": 1,
"options": [
{
"name": "group",
"description": "Page group to join",
"type": 3,
"required": true,
"choices": require("./pageGroups.json")
}
]
},
{
"name": "remove",
"description": "Remove yourself from a paging group",
"type": 1,
"options": [
{
"name": "group",
"description": "Page group to leave",
"type": 3,
"required": true,
"choices": require("./pageGroups.json")
}
]
}
]
},
{
"name": "list",
"description": "List all extensions on the LiteNet Phone System",
"type": 1
},
{
"name": "button",
"description": "Send the get an extension button!",
"type": 1,
"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)",
// "type": 1,
// "options": [
// {
// "name": "name",
// "description": "The new name for your extension",
// "type": 3,
// "required": false
// }
// ]
// },
{
"name": "admin",
"description": "Admin only commands",
"type": 1,
"default_member_permissions": 0,
"options": [
{
"name": "silence",
"description": "Kill all ongoing calls",
"type": 1,
"default_member_permissions": 0
},
{
"name": "reload",
"description": "Run asterisk reload",
"type": 1,
"default_member_permissions": 0
},
{
"name": "reboot",
"description": "Reboot the server (LAST RESORT)",
"type": 1,
"default_member_permissions": 0
},
{
"name": "list-deletions",
"description": "List pending deletions",
"type": 1
}
]
},
{
"name": "dev",
"description": "Developer only commands",
"type": 1,
"default_member_permissions": 0,
"options": [
{
"name": "fwconsole",
"description": "Run an fwconsole command",
"type": 1,
"default_member_permissions": 0,
"options": [
{
"name": "command",
"description": "The command to run",
"type": 3,
"required": true
}
]
},
{
"name": "asterisk",
"description": "Run an asterisk CLI command",
"type": 1,
"default_member_permissions": 0,
"options": [
{
"name": "command",
"description": "The command to run",
"type": 3,
"required": true
}
]
},
{
"name": "shell",
"description": "Run a shell command",
"type": 1,
"default_member_permissions": 0,
"options": [
{
"name": "command",
"description": "The command to run",
"type": 3,
"required": true
}
]
},
{
"name": "restart",
"description": "Restart the bot",
"type": 1,
"default_member_permissions": 0
}
]
},
// {
// "name": "cdr",
// "description": "Get the call detail records for your extension",
// "type": 1,
// "options": [
// {
// "name": "start_date",
// "description": "The start date for the CDR (mm/dd/yyyy)",
// "type": 3,
// "required": false
// },
// {
// "name": "end_date",
// "description": "The end date for the CDR (mm/dd/yyyy)",
// "type": 3,
// "required": false
// }
// ]
// },
{
"name": "lookup",
"description": "Find extension by Discord user",
"type": 1,
"options": [
{
"name": "user",
"description": "The Discord user to lookup",
"type": 6,
"required": true
}
]
},
{
"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
}
]
},
{
"name": "link",
"description": "Link an existing extension to a Discord user",
"type": 1,
"options": [
{
"name": "user",
"description": "The Discord user to link the extension to",
"type": 6,
"required": true
},
{
"name": "extension",
"description": "The extension number to link",
"type": 4,
"required": true
}
]
},
{
"name": "unlink",
"description": "Unlink an extension from a Discord user",
"type": 1,
"options": [
{
"name": "extension",
"description": "The extension number to unlink",
"type": 4,
"required": true
}
]
},
{
"name": "list-orphans",
"description": "List all orphaned extensions (extensions without a Discord user linked)",
"type": 1
}
]
}
]

View file

@ -1,170 +0,0 @@
[
{
"name": "whoami",
"description": "Get your extension info if you have one",
"type": 1
},
{
"name": "new",
"description": "Get an extension on the LiteNet Phone System",
"type": 1
},
{
"name": "delete",
"description": "Remove your extension from the LiteNet Phone System",
"type": 1,
"options": [
{
"name": "confirm",
"description": "Confirm that you want to delete your extension. THIS CANNOT BE UNDONE!",
"type": 5,
"required": true,
"choices": [
{
"name": "yes",
"value": "yes"
}
]
}
]
},
{
"name": "list",
"description": "List all extensions on the LiteNet Phone System",
"type": 1
},
{
"name": "button",
"description": "Send the get an extension button!",
"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
}
]
},
{
"name": "admin",
"description": "Admin only commands",
"type": 1,
"default_member_permissions": 0,
"options": [
{
"name": "silence",
"description": "Kill all ongoing calls",
"type": 1,
"default_member_permissions": 0
},
{
"name": "reload",
"description": "Run asterisk reload",
"type": 1,
"default_member_permissions": 0
},
{
"name": "reboot",
"description": "Reboot the server (LAST RESORT)",
"type": 1,
"default_member_permissions": 0
}
]
},
{
"name": "dev",
"description": "Developer only commands",
"type": 1,
"default_member_permissions": 0,
"options": [
{
"name": "fwconsole",
"description": "Run an fwconsole command",
"type": 1,
"default_member_permissions": 0,
"options": [
{
"name": "command",
"description": "The command to run",
"type": 3,
"required": true
}
]
},
{
"name": "asterisk",
"description": "Run an asterisk CLI command",
"type": 1,
"default_member_permissions": 0,
"options": [
{
"name": "command",
"description": "The command to run",
"type": 3,
"required": true
}
]
},
{
"name": "shell",
"description": "Run a shell command",
"type": 1,
"default_member_permissions": 0,
"options": [
{
"name": "command",
"description": "The command to run",
"type": 3,
"required": true
}
]
},
{
"name": "restart",
"description": "Restart the bot",
"type": 1,
"default_member_permissions": 0
}
]
},
{
"name": "cdr",
"description": "Get the call detail records for your extension",
"type": 1,
"options": [
{
"name": "start_date",
"description": "The start date for the CDR (mm/dd/yyyy)",
"type": 3,
"required": false
},
{
"name": "end_date",
"description": "The end date for the CDR (mm/dd/yyyy)",
"type": 3,
"required": false
}
]
},
{
"name": "Lookup Extension",
"type": 2
},
{
"name": "Create Extension",
"type": 2,
"default_member_permissions": 0
},
{
"name": "Delete Extension",
"type": 2,
"default_member_permissions": 0
}
]

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

36
debug.js Normal file
View file

@ -0,0 +1,36 @@
require("dotenv").config();
const cron = require("node-cron")
const fs = require('fs');
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,
});
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,
});
// fpbx.listExtensions()
// .then((extensions) => {
// console.log(JSON.stringify(extensions, null, 2));
// })
// .catch((err) => {
// console.error(err);
// });
fpbx.getNextAvailableExtension()
.then((extension) => {
console.log(`Next available extension: ${extension}`);
})
.catch((err) => {
console.error(err);
});

72
deletions.js Normal file
View file

@ -0,0 +1,72 @@
const pool = global.pool
const fpbx = global.fpbx
const client = global.client
const log = global.log
module.exports = {};
var error_shown = false;
module.exports.handleScheduled = async () => {
if (error_shown) return;
if (!process.env.DISCORD_GUILD) {
error_shown = true;
return log.error(`Environment variable DISCORD_GUILD is not set! Cannot handle automatic deletions. Please set this variable and restart to enable this feature.`);
}
const deletions = await pool.query('SELECT * FROM discord_deletions');
if (!deletions) return;
for (const deletion of deletions) {
const guild = client.guilds.cache.get(process.env.DISCORD_GUILD);
const member = guild ? await guild.members.fetch(deletion.discordId).catch(() => null) : null;
const ext = await fpbx.getExtension(deletion.extension);
if (!ext) {
log.error(`Failed to get extension for deletion: ${deletion.discordId}. Assuming it's already gone. Deleting from database.`);
await pool.query('DELETE FROM discord_deletions WHERE discordId = ?', [deletion.discordId]);
await pool.query('DELETE FROM discord_users WHERE discordId = ?', [deletion.discordId]);
return;
}
if (member) {
log.info(`User ${deletion.discordId} rejoined, removing deletion`);
await pool.query('DELETE FROM discord_deletions WHERE discordId = ?', [deletion.discordId]);
return;
} else if (new Date(deletion.deleteAt) < new Date()) {
log.info(`Deleting extension for ${deletion.discordId}`);
const deleteResp = await fpbx.deleteExtension(deletion.extension);
if (!deleteResp[0].deleteExtension.status) {
log.error(`Failed to delete extension for ${deletion.discordId}`);
await pool.query('DELETE FROM discord_deletions WHERE discordId = ?', [deletion.discordId]);
return;
} else {
log.info(`Deleted extension for ${deletion.discordId}`);
await pool.query('DELETE FROM discord_users WHERE discordId = ?', [deletion.discordId]);
}
await fpbx.reload();
await pool.query('DELETE FROM discord_deletions WHERE discordId = ?', [deletion.discordId]);
return;
}
}
};
module.exports.findOrphans = async () => {
if (error_shown) return;
if (!process.env.DISCORD_GUILD) {
error_shown = true;
return log.error(`Environment variable DISCORD_GUILD is not set! Cannot handle automatic deletions. Please set this variable and restart to enable this feature.`);
}
const users = await pool.query('SELECT * FROM discord_users');
const deletions = await pool.query('SELECT * FROM discord_deletions');
const guild = client.guilds.cache.get(process.env.DISCORD_GUILD);
if (!users) return
for (const user of users) {
const member = guild ? await guild.members.fetch(user.discordId).catch(() => null) : null;
if (!member) {
log.info(`User ${user.discordId} not found in guild, marking for deletion`);
const isMarkedForDeletion = deletions.some(deletion => deletion.discordId === user.discordId);
if (isMarkedForDeletion) {
log.info(`User ${user.discordId} is already marked for deletion`);
continue;
}
const deleteAt = new Date(Date.now() + 60 * 60 * 1000); // 1 hour in the future
await pool.query('INSERT INTO discord_deletions (discordId, extension, deleteAt) VALUES (?, ?, ?)', [user.discordId, user.extension, deleteAt]);
}
}
};

View file

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

216
freepbx.js Normal file
View file

@ -0,0 +1,216 @@
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 = null;
this.renewClient = async () => {
this.client = new FreepbxGqlClient(config.url, {
client: {
id: config.clientId,
secret: config.clientSecret,
},
});
}
this.renewClient();
this.pbxCall = async (query, variables) => {
try {
return await this.client.request(query, variables).catch(err => {
throw err;
});
} catch (err) {
if (err.response && err.response.error && err.response.error.message === "The resource owner or authorization server denied the request.") {
await this.renewClient();
console.log("Client renewed");
return await this.pbxCall(query, variables);
} else {
throw err;
}
}
}
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],
};
try {
return await this.pbxCall(query, variables);
} catch (err) {
console.error("Failed to fetch extension:", err);
return false;
}
}
async listExtensions() {
const query = gql`
query {
fetchAllExtensions {
extension {
user {
extension
name
}
}
}
}
`;
return await this.pbxCall(query);
}
async addExtension(ext, name) {
ext = String(ext);
name = String(name);
name = name.replace(/[^a-zA-Z0-9\s]/g, '');
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.pbxCall(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.pbxCall(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.pbxCall(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.pbxCall(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;
// Remove duplicates by using a Set
const existingExtsSet = new Set(exts.map(ext => parseInt(ext.user.extension, 10)));
const existingExts = Array.from(existingExtsSet).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;
}
},
}

1831
index.js

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,81 @@
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;
case 'list-deletions':
const deletions = await pool.query('SELECT * FROM discord_deletions');
if (!deletions.length) {
await interaction.reply({ content: 'No pending deletions.', ephemeral: true });
return;
}
const deletionList = deletions.map((deletion) => {
return `Member: <@${deletion.discordId}> (${deletion.discordId}), Extension: ${deletion.extension}, Delete At: <t:${deletion.deleteAt / 1000}>`;
});
await interaction.reply({ content: deletionList.join('\n'), ephemeral: true });
break;
}
}

View file

@ -0,0 +1,102 @@
const pool = global.pool
const fpbx = global.fpbx
const client = global.client
const log = global.log
const Discord = require("discord.js")
module.exports = {};
/* Holding for upcoming sponsorship embed
{
"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/nZFQTaZWqT)\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"
}
}
*/
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/nZFQTaZWqT)\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,5 @@
module.exports = {}
module.exports.execute = async (interaction) => {
interaction.reply({ephemeral: true, content: `To be implemented!`})
}

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,142 @@
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;
case "link": // Link an extension to a user (or at least try to)
await interaction.deferReply({ ephemeral: true });
var forUser = interaction.options.getUser('user');
var ext = interaction.options.getInteger('extension');
if (!ext) {
await interaction.editReply({ content: `You must provide an extension to link!`, ephemeral: true });
return;
}
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;
}
var [extLookup] = await pool.query('SELECT * FROM discord_users WHERE extension = ?', [ext]);
if (extLookup) {
await interaction.editReply({ content: `Extension ${ext} is already linked to another user! It's linked to ${extLookup.discordId}`, ephemeral: true });
return;
}
var [extExists] = await pool.query('SELECT * FROM asterisk.devices WHERE id = ?', [ext]);
if (!extExists) {
await interaction.editReply({ content: `Extension ${ext} does not exist!`, ephemeral: true });
return;
}
await pool.query('INSERT INTO discord_users (discordId, extension) VALUES (?, ?)', [forUser.id, ext]);
await interaction.editReply({ content: `Extension ${ext} linked to ${forUser.username}!`, ephemeral: true });
break;
case "unlink": // Unlink an extension from a user
await interaction.deferReply({ ephemeral: true });
var ext = interaction.options.getInteger('extension');
if (!ext) {
await interaction.editReply({ content: `You must provide an extension to unlink!`, ephemeral: true });
return;
}
var [lookup] = await pool.query('SELECT * FROM discord_users WHERE extension = ?', [ext]);
if (!lookup) {
await interaction.editReply({ content: `Extension ${ext} is not linked to any user!`, ephemeral: true });
return;
}
await pool.query('DELETE FROM discord_users WHERE extension = ?', [ext]);
await interaction.editReply({ content: `Extension ${ext} unlinked from ${lookup.discordId}!`, ephemeral: true });
break;
case "list-orphans": // List all orphaned extensions (extensions without a Discord user linked)
await interaction.deferReply({ ephemeral: true });
const orphans = await pool.query('SELECT id,description FROM asterisk.devices WHERE id NOT IN (SELECT extension FROM asterisk.discord_users)');
if (orphans.length === 0) {
await interaction.editReply({ content: 'No orphaned extensions found!', ephemeral: true });
return;
}
orphans.sort((a, b) => Number(a.id) - Number(b.id));
const orphanList = orphans.map(o => `**Extension:** \`${o.id}\` - **Name:** \`${o.description}\``).join('\n');
await interaction.editReply({
content: `**Orphaned Extensions:**\n${orphanList}`,
ephemeral: true
});
break;
default:
await interaction.reply({ content: 'Unknown subcommand!', ephemeral: true });
break;
}
}

View file

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

View file

@ -0,0 +1,38 @@
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();
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 pageGroup = await interaction.options.getString('group');
switch (subcommand) {
case 'add':
fpbx.joinPageGroup(lookup.extension, pageGroup).then(async (res) => {
if (res == true) {
await fpbx.reload();
await interaction.reply({ content: `Added!`, ephemeral: true });
} else {
await interaction.reply({ content: `Something went wrong (Or you're already in that page group!)`, ephemeral: true });
}
});
break;
case "remove":
fpbx.leavePageGroup(lookup.extension, pageGroup).then(async (res) => {
if (res == true) {
await fpbx.reload();
await interaction.reply({ content: `Removed!`, ephemeral: true });
} else {
await interaction.reply({ content: `Something went wrong (Or you're not in that page group!)`, ephemeral: true });
}
});
break;
}
}

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,41 @@
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 })
if (process.env.EXTENSION_ROLE_ID) await interaction.member.roles.add(process.env.EXTENSION_ROLE_ID);
}).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,34 @@
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;
}
await interaction.deferReply({ ephemeral: true });
const extInfo = await fpbx.getExtension(lookup.extension);
await interaction.editReply({
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
}
]
});
const EXTENSION_ROLE_ID = process.env.EXTENSION_ROLE_ID;
if (EXTENSION_ROLE_ID) {
const member = await interaction.guild.members.fetch(interaction.user.id);
if (!member.roles.cache.has(EXTENSION_ROLE_ID)) {
await member.roles.add(EXTENSION_ROLE_ID);
}
}
}

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

View file

@ -0,0 +1,5 @@
CREATE TABLE IF NOT EXISTS discord_deletions (
extension VARCHAR(20) PRIMARY KEY,
discordId VARCHAR(25) NOT NULL,
deleteAt TIMESTAMP NOT NULL
);

414
package-lock.json generated
View file

@ -12,93 +12,240 @@
"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",
"node-cron": "^3.0.3",
"ping": "^0.4.4",
"sqlite3": "^5.1.4",
"ssh2": "^1.15.0"
}
},
"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 +298,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 +345,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 +505,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 +563,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 +657,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 +742,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 +781,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 +992,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 +1012,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",
@ -1085,6 +1248,18 @@
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-4.3.0.tgz",
"integrity": "sha512-73sE9+3UaLYYFmDsFZnqCInzPyh3MqIwZO9cw58yIqAZhONrrabrYyYe3TuIqtIiOuTXVhsGau8hcrhhwSsDIQ=="
},
"node_modules/node-cron": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/node-cron/-/node-cron-3.0.3.tgz",
"integrity": "sha512-dOal67//nohNgYWb+nWmg5dkFdIwDm8EpeGYMekPMrngV3637lqnX0lbUcCtgibHTz6SEz7DAIjKvKDFYCnO1A==",
"license": "ISC",
"dependencies": {
"uuid": "8.3.2"
},
"engines": {
"node": ">=6.0.0"
}
},
"node_modules/node-fetch": {
"version": "2.7.0",
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz",
@ -1444,14 +1619,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 +1681,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 +1698,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",
@ -1562,6 +1738,15 @@
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="
},
"node_modules/uuid": {
"version": "8.3.2",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
"integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==",
"license": "MIT",
"bin": {
"uuid": "dist/bin/uuid"
}
},
"node_modules/webidl-conversions": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
@ -1605,9 +1790,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"
},

View file

@ -4,7 +4,8 @@
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
"test": "echo \"Error: no test specified\" && exit 1",
"start": "node index.js"
},
"author": "",
"license": "ISC",
@ -12,8 +13,10 @@
"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",
"node-cron": "^3.0.3",
"ping": "^0.4.4",
"sqlite3": "^5.1.4",
"ssh2": "^1.15.0"

View file

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