Compare commits

..

34 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: ChrisChrome/discord-freepbx-manager#3
2025-01-25 12:29:02 -07:00
Christopher Cookman 98ca59cd41 Merge pull request '2.0-rewrite' (#2) from 2.0-rewrite into main
Reviewed-on: ChrisChrome/discord-freepbx-manager#2
2025-01-25 12:06:49 -07:00
17 changed files with 522 additions and 159 deletions

View file

@ -1,9 +1,11 @@
PBX_HOSTNAME=what-gets-shown-to-users PBX_HOSTNAME=your_pbx_hostname
FREEPBX_URL=http://your-pbx.local FREEPBX_URL=http://your_freepbx_url
FREEPBX_CLIENT_ID= FREEPBX_CLIENT_ID=your_freepbx_client_id
FREEPBX_CLIENT_SECRET= FREEPBX_CLIENT_SECRET=your_freepbx_client_secret
DB_HOST=your-pbx.local DB_HOST=your_db_host
DB_USER=user DB_USER=your_db_user
DB_PASS=pass DB_PASS=your_db_password
DISCORD_TOKEN=your-discord-token DISCORD_TOKEN=your_discord_token
OWNER_ID=your-discord-id OWNER_ID=your_owner_id
EXTENSION_ROLE_ID=your_extension_role_id
DISCORD_GUILD=your_discord_guild_id

View file

@ -24,7 +24,7 @@ To set up the config file, follow the steps below:
1. Clone the repository: 1. Clone the repository:
```shell ```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 cd discord-freepbx-manager
``` ```
@ -33,63 +33,34 @@ To set up the config file, follow the steps below:
npm install --save 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: 3. Copy `.env.example` to `.env`
```json
{ 4. Configure the `.env` file with the following options:
"ntfyUrl": "ntfy-url",
"freepbx": { 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.
"server": "sip-server-ip",
"url": "pbx-api-url", FREEPBX_URL: Base URL for the API of your PBX. i.e. `https://pbx.example.com`
"clientid": "gql-client-id",
"allowedscopes": "gql", FREEPBX_CLIENT_ID: This is the client ID used for authenticating with the FreePBX API. It is part of the OAuth2 authentication process.
"secret": "gql-secret",
"startExt": 1000 FREEPBX_CLIENT_SECRET: This is the client secret used along with the client ID for authenticating with the FreePBX API.
},
"discord": { 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`
"token": "bot-token",
"guildId": "guild-id", DB_USER: This is the username used to authenticate with the database.
"roleId": "user-role",
"logId": "log-channel", DB_PASS: This is the password used to authenticate with the database.
"extList": "extension-list-channel",
"developers": [ DISCORD_TOKEN: This is the token for your Discord bot. It is used to authenticate the bot with the Discord API.
"your-user-id"
] 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.
},
"mariadb": { EXTENSION_ROLE_ID: Discord role ID of the role to give PBX users when they make their extension.
"host": "db-hostname0here",
"user": "bot", 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.
"password": "bot",
"database": "asterisk", 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.
"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.
5. Run the bot: 5. Run the bot:
```shell ```shell
node . node .

View file

@ -105,6 +105,11 @@ module.exports = [
"description": "Reboot the server (LAST RESORT)", "description": "Reboot the server (LAST RESORT)",
"type": 1, "type": 1,
"default_member_permissions": 0 "default_member_permissions": 0
},
{
"name": "list-deletions",
"description": "List pending deletions",
"type": 1
} }
] ]
}, },
@ -164,25 +169,25 @@ module.exports = [
} }
] ]
}, },
{ // {
"name": "cdr", // "name": "cdr",
"description": "Get the call detail records for your extension", // "description": "Get the call detail records for your extension",
"type": 1, // "type": 1,
"options": [ // "options": [
{ // {
"name": "start_date", // "name": "start_date",
"description": "The start date for the CDR (mm/dd/yyyy)", // "description": "The start date for the CDR (mm/dd/yyyy)",
"type": 3, // "type": 3,
"required": false // "required": false
}, // },
{ // {
"name": "end_date", // "name": "end_date",
"description": "The end date for the CDR (mm/dd/yyyy)", // "description": "The end date for the CDR (mm/dd/yyyy)",
"type": 3, // "type": 3,
"required": false // "required": false
} // }
] // ]
}, // },
{ {
"name": "lookup", "name": "lookup",
"description": "Find extension by Discord user", "description": "Find extension by Discord user",
@ -259,6 +264,43 @@ module.exports = [
"required": true "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
} }
] ]
} }

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

@ -11,12 +11,34 @@ class FreepbxManager {
* @param {Object} config.dbPool - The connection pool for managing database connections. * @param {Object} config.dbPool - The connection pool for managing database connections.
*/ */
constructor(config) { constructor(config) {
this.client = null;
this.renewClient = async () => {
this.client = new FreepbxGqlClient(config.url, { this.client = new FreepbxGqlClient(config.url, {
client: { client: {
id: config.clientId, id: config.clientId,
secret: config.clientSecret, 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; this.pool = config.dbPool;
if (!this.pool) { if (!this.pool) {
@ -26,6 +48,8 @@ class FreepbxManager {
} }
async getExtension(ext) { async getExtension(ext) {
ext = String(ext); ext = String(ext);
@ -46,7 +70,12 @@ class FreepbxManager {
extensionId: ext.match(/\d+/)[0], extensionId: ext.match(/\d+/)[0],
}; };
return await this.client.request(query, variables); try {
return await this.pbxCall(query, variables);
} catch (err) {
console.error("Failed to fetch extension:", err);
return false;
}
} }
async listExtensions() { async listExtensions() {
@ -63,13 +92,13 @@ class FreepbxManager {
} }
`; `;
return await this.client.request(query); return await this.pbxCall(query);
} }
async addExtension(ext, name) { async addExtension(ext, name) {
ext = String(ext); ext = String(ext);
name = String(name); name = String(name);
name = name.replace(/[^a-zA-Z0-9\s]/g, '');
const query = gql` const query = gql`
mutation addExtension($ext: ID!, $name: String!, $vmPassword: String!) { mutation addExtension($ext: ID!, $name: String!, $vmPassword: String!) {
addExtension(input: { addExtension(input: {
@ -92,7 +121,7 @@ class FreepbxManager {
vmPassword: ext, vmPassword: ext,
}; };
return await this.client.request(query, variables); return await this.pbxCall(query, variables);
} }
async deleteExtension(ext) { async deleteExtension(ext) {
@ -109,7 +138,7 @@ class FreepbxManager {
ext, ext,
}; };
const fpbxQuery = this.client.request(query, variables); const fpbxQuery = this.pbxCall(query, variables);
const dbQuery = this.pool.query('DELETE FROM paging_groups WHERE ext = ?', [ext]); const dbQuery = this.pool.query('DELETE FROM paging_groups WHERE ext = ?', [ext]);
return await Promise.all([fpbxQuery, dbQuery]); return await Promise.all([fpbxQuery, dbQuery]);
} }
@ -123,7 +152,7 @@ class FreepbxManager {
} }
`; `;
return await this.client.request(query); return await this.pbxCall(query);
} }
// async updateName(ext, name) { // async updateName(ext, name) {
@ -140,7 +169,7 @@ class FreepbxManager {
// name, // name,
// }; // };
// return await this.client.request(query, variables); // return await this.pbxCall(query, variables);
// } // }
// TODO: Implement updateName method, Current implementation resets extension for some reason // TODO: Implement updateName method, Current implementation resets extension for some reason
@ -168,7 +197,9 @@ class FreepbxManager {
const extList = await this.listExtensions(); const extList = await this.listExtensions();
const exts = extList.fetchAllExtensions.extension; const exts = extList.fetchAllExtensions.extension;
const startExt = process.env.START_EXT ? parseInt(process.env.START_EXT, 10) : 1000; 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); // 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; let nextExt = startExt;
for (let i = 0; i < existingExts.length; i++) { for (let i = 0; i < existingExts.length; i++) {

View file

@ -1,5 +1,5 @@
require("dotenv").config(); require("dotenv").config();
const cron = require("node-cron")
const fs = require('fs'); const fs = require('fs');
const mariadb = require("mariadb"); const mariadb = require("mariadb");
const pool = mariadb.createPool({ const pool = mariadb.createPool({
@ -53,6 +53,8 @@ global.fpbx = fpbx;
global.client = client; global.client = client;
global.log = log; global.log = log;
const deletion = require("./deletions.js");
client.on('ready', async () => { client.on('ready', async () => {
log.success(`Logged in as ${client.user.displayName}`); log.success(`Logged in as ${client.user.displayName}`);
const commands = require("./commands") const commands = require("./commands")
@ -69,10 +71,40 @@ client.on('ready', async () => {
log.error(error) log.error(error)
}); });
// // Clear guild commands
// log.info("Clearing guild commands...")
// const guilds = client.guilds.cache.map(guild => guild.id);
// for (const guild of guilds) {
// rest.put(Discord.Routes.applicationGuildCommands(client.user.id, guild), { body: [] }).then(() => {
// log.success(`Cleared commands for guild ${guild}`)
// }).catch((error) => {
// log.error(error)
// });
// }
} catch (error) { } catch (error) {
log.error(error) log.error(error)
} }
})(); })();
cron.schedule('* * * * *', () => {
try {
deletion.handleScheduled();
} catch (error) {
log.error(`Failed to execute deletion task: ${error}`);
}
});
cron.schedule('0 * * * *', () => {
log.info("Checking for orphaned extensions...");
try {
deletion.findOrphans();
} catch (error) {
log.error(`Failed to execute orphan task: ${error}`);
}
});
deletion.findOrphans();
deletion.handleScheduled();
}); });
client.on('interactionCreate', async interaction => { client.on('interactionCreate', async interaction => {
@ -87,7 +119,9 @@ client.on('interactionCreate', async interaction => {
await command.execute(interaction); await command.execute(interaction);
} catch (error) { } catch (error) {
log.error(error); log.error(error);
await interaction.reply({ content: 'There was an error while executing this command!', ephemeral: true }); await interaction.reply({ content: 'There was an error while executing this command!', ephemeral: true }).catch((error) => {
log.error(`Failed to inform user of error: ${error}`);
});;
} }
break; break;
case Discord.InteractionType.MessageComponent: case Discord.InteractionType.MessageComponent:
@ -99,12 +133,33 @@ client.on('interactionCreate', async interaction => {
await component.execute(interaction); await component.execute(interaction);
} catch (error) { } catch (error) {
log.error(error); log.error(error);
await interaction.reply({ content: 'There was an error while executing this component!', ephemeral: true }); await interaction.reply({ content: 'There was an error while executing this component!', ephemeral: true }).catch((error) => {
log.error(`Failed to inform user of error: ${error}`);
});;
} }
break; break;
} }
}); });
client.on('guildMemberAdd', async member => {
const [deletion] = await pool.query('SELECT * FROM discord_deletions WHERE discordId = ?', [member.id]);
if (deletion) {
await pool.query('DELETE FROM discord_deletions WHERE discordId = ?', [member.id]);
log.info(`User ${member.id} rejoined, removing deletion`);
}
});
client.on('guildMemberRemove', async member => {
const [deletion] = await pool.query('SELECT * FROM discord_deletions WHERE discordId = ?', [member.id]);
const [user] = await pool.query('SELECT * FROM discord_users WHERE discordId = ?', [member.id]);
if (!user) return;
if (!deletion) {
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 (?, ?, ?)', [member.id, user.extension, deleteAt]);
log.info(`User ${member.id} left and had an extension, marking for deletion`);
}
});
if (fs.existsSync("./import.json")) { if (fs.existsSync("./import.json")) {
const importData = JSON.parse(fs.readFileSync("./import.json", "utf8")); const importData = JSON.parse(fs.readFileSync("./import.json", "utf8"));
@ -125,6 +180,7 @@ if (fs.existsSync("./import.json")) {
insertData(); insertData();
} }
// Startup // Startup
require("./migrations")(pool).then(() => { require("./migrations")(pool).then(() => {
log.success("Database migrations complete."); log.success("Database migrations complete.");

View file

@ -64,5 +64,18 @@ module.exports.execute = async (interaction) => {
}); });
runCommand('reboot 0'); runCommand('reboot 0');
break; 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

@ -6,16 +6,15 @@ const Discord = require("discord.js")
module.exports = {}; module.exports = {};
module.exports.execute = async (interaction) => { /* Holding for upcoming sponsorship embed
interaction.channel.send({ {
embeds: [{
"title": "The LiteNet Community PBX", "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)", "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, "color": 7955428,
"fields": [ "fields": [
{ {
"name": "What's this?", "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!" "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?", "name": "What can it do?",
@ -31,7 +30,44 @@ module.exports.execute = async (interaction) => {
} }
], ],
"footer": { "footer": {
"text": "Made with <3 by Chris Chrome & The LiteNet Team • Sponsored by SnakeCraft Hosting", "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" "icon_url": "https://f.chrischro.me/assets/Snakecraft-Social-Media-purple-v2-smaller.png"
}, },
"image": { "image": {

View file

@ -0,0 +1,5 @@
module.exports = {}
module.exports.execute = async (interaction) => {
interaction.reply({ephemeral: true, content: `To be implemented!`})
}

View file

@ -79,5 +79,64 @@ module.exports.execute = async (interaction) => {
] ]
}); });
break; 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

@ -17,6 +17,7 @@ module.exports.execute = async (interaction) => {
case 'add': case 'add':
fpbx.joinPageGroup(lookup.extension, pageGroup).then(async (res) => { fpbx.joinPageGroup(lookup.extension, pageGroup).then(async (res) => {
if (res == true) { if (res == true) {
await fpbx.reload();
await interaction.reply({ content: `Added!`, ephemeral: true }); await interaction.reply({ content: `Added!`, ephemeral: true });
} else { } else {
await interaction.reply({ content: `Something went wrong (Or you're already in that page group!)`, ephemeral: true }); await interaction.reply({ content: `Something went wrong (Or you're already in that page group!)`, ephemeral: true });
@ -26,6 +27,7 @@ module.exports.execute = async (interaction) => {
case "remove": case "remove":
fpbx.leavePageGroup(lookup.extension, pageGroup).then(async (res) => { fpbx.leavePageGroup(lookup.extension, pageGroup).then(async (res) => {
if (res == true) { if (res == true) {
await fpbx.reload();
await interaction.reply({ content: `Removed!`, ephemeral: true }); await interaction.reply({ content: `Removed!`, ephemeral: true });
} else { } else {
await interaction.reply({ content: `Something went wrong (Or you're not in that page group!)`, ephemeral: true }); await interaction.reply({ content: `Something went wrong (Or you're not in that page group!)`, ephemeral: true });

View file

@ -29,6 +29,7 @@ module.exports.execute = async (interaction) => {
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}\`||`, 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 color: 0x00ff00
}], ephemeral: true }) }], ephemeral: true })
if (process.env.EXTENSION_ROLE_ID) await interaction.member.roles.add(process.env.EXTENSION_ROLE_ID);
}).catch(async (error) => { }).catch(async (error) => {
log.error(error); log.error(error);
await interaction.editReply({ content: 'There was an error while creating your extension!', ephemeral: true }); await interaction.editReply({ content: 'There was an error while creating your extension!', ephemeral: true });

View file

@ -11,8 +11,9 @@ module.exports.execute = async (interaction) => {
await interaction.reply({ content: `We're sorry, It doesn't look like you have an extension!`, ephemeral: true }); await interaction.reply({ content: `We're sorry, It doesn't look like you have an extension!`, ephemeral: true });
return; return;
} }
await interaction.deferReply({ ephemeral: true });
const extInfo = await fpbx.getExtension(lookup.extension); const extInfo = await fpbx.getExtension(lookup.extension);
return await interaction.reply({ await interaction.editReply({
ephemeral: true, embeds: [ ephemeral: true, embeds: [
{ {
title: "Your Extension Info", title: "Your Extension Info",
@ -22,4 +23,12 @@ module.exports.execute = async (interaction) => {
] ]
}); });
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,5 @@
CREATE TABLE IF NOT EXISTS discord_deletions (
extension VARCHAR(20) PRIMARY KEY,
discordId VARCHAR(25) NOT NULL,
deleteAt TIMESTAMP NOT NULL
);

22
package-lock.json generated
View file

@ -15,6 +15,7 @@
"dotenv": "^16.4.7", "dotenv": "^16.4.7",
"freepbx-graphql-client": "^0.1.1", "freepbx-graphql-client": "^0.1.1",
"mariadb": "^3.2.0", "mariadb": "^3.2.0",
"node-cron": "^3.0.3",
"ping": "^0.4.4", "ping": "^0.4.4",
"sqlite3": "^5.1.4", "sqlite3": "^5.1.4",
"ssh2": "^1.15.0" "ssh2": "^1.15.0"
@ -1247,6 +1248,18 @@
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-4.3.0.tgz", "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-4.3.0.tgz",
"integrity": "sha512-73sE9+3UaLYYFmDsFZnqCInzPyh3MqIwZO9cw58yIqAZhONrrabrYyYe3TuIqtIiOuTXVhsGau8hcrhhwSsDIQ==" "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": { "node_modules/node-fetch": {
"version": "2.7.0", "version": "2.7.0",
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz",
@ -1725,6 +1738,15 @@
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" "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": { "node_modules/webidl-conversions": {
"version": "3.0.1", "version": "3.0.1",
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",

View file

@ -16,6 +16,7 @@
"dotenv": "^16.4.7", "dotenv": "^16.4.7",
"freepbx-graphql-client": "^0.1.1", "freepbx-graphql-client": "^0.1.1",
"mariadb": "^3.2.0", "mariadb": "^3.2.0",
"node-cron": "^3.0.3",
"ping": "^0.4.4", "ping": "^0.4.4",
"sqlite3": "^5.1.4", "sqlite3": "^5.1.4",
"ssh2": "^1.15.0" "ssh2": "^1.15.0"