Compare commits
No commits in common. "main" and "2.0-rewrite" have entirely different histories.
main
...
2.0-rewrit
20
.env.example
20
.env.example
|
@ -1,11 +1,9 @@
|
|||
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
|
||||
PBX_HOSTNAME=what-gets-shown-to-users
|
||||
FREEPBX_URL=http://your-pbx.local
|
||||
FREEPBX_CLIENT_ID=
|
||||
FREEPBX_CLIENT_SECRET=
|
||||
DB_HOST=your-pbx.local
|
||||
DB_USER=user
|
||||
DB_PASS=pass
|
||||
DISCORD_TOKEN=your-discord-token
|
||||
OWNER_ID=your-discord-id
|
87
README.md
87
README.md
|
@ -24,7 +24,7 @@ To set up the config file, follow the steps below:
|
|||
|
||||
1. Clone the repository:
|
||||
```shell
|
||||
git clone https://git.chrischro.me/ChrisChrome/discord-freepbx-manager.git
|
||||
git clone https://github.com/ChrisChrome/discord-freepbx-manager.git
|
||||
|
||||
cd discord-freepbx-manager
|
||||
```
|
||||
|
@ -33,34 +33,63 @@ To set up the config file, follow the steps below:
|
|||
npm install --save
|
||||
```
|
||||
|
||||
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.
|
||||
|
||||
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.
|
||||
5. Run the bot:
|
||||
```shell
|
||||
node .
|
||||
|
|
80
commands.js
80
commands.js
|
@ -105,11 +105,6 @@ module.exports = [
|
|||
"description": "Reboot the server (LAST RESORT)",
|
||||
"type": 1,
|
||||
"default_member_permissions": 0
|
||||
},
|
||||
{
|
||||
"name": "list-deletions",
|
||||
"description": "List pending deletions",
|
||||
"type": 1
|
||||
}
|
||||
]
|
||||
},
|
||||
|
@ -169,25 +164,25 @@ module.exports = [
|
|||
}
|
||||
]
|
||||
},
|
||||
// {
|
||||
// "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": "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",
|
||||
|
@ -264,43 +259,6 @@ module.exports = [
|
|||
"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
36
debug.js
|
@ -1,36 +0,0 @@
|
|||
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
72
deletions.js
|
@ -1,72 +0,0 @@
|
|||
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]);
|
||||
}
|
||||
}
|
||||
};
|
155
freepbx.js
155
freepbx.js
|
@ -10,76 +10,47 @@ class FreepbxManager {
|
|||
* @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;
|
||||
constructor(config) {
|
||||
this.client = new FreepbxGqlClient(config.url, {
|
||||
client: {
|
||||
id: config.clientId,
|
||||
secret: config.clientSecret,
|
||||
},
|
||||
});
|
||||
this.pool = config.dbPool;
|
||||
|
||||
if (!this.pool) {
|
||||
throw new Error("Connection pool is required");
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
async getExtension(ext) {
|
||||
async getExtension(ext) {
|
||||
ext = String(ext);
|
||||
|
||||
const query = gql`
|
||||
query fetchExtension($extensionId: ID!) {
|
||||
fetchExtension(extensionId: $extensionId) {
|
||||
user {
|
||||
extension
|
||||
name
|
||||
extPassword
|
||||
voicemail
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
const query = gql`
|
||||
query fetchExtension($extensionId: ID!) {
|
||||
fetchExtension(extensionId: $extensionId) {
|
||||
user {
|
||||
extension
|
||||
name
|
||||
extPassword
|
||||
voicemail
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const variables = {
|
||||
extensionId: ext.match(/\d+/)[0],
|
||||
};
|
||||
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;
|
||||
}
|
||||
}
|
||||
return await this.client.request(query, variables);
|
||||
}
|
||||
|
||||
async listExtensions() {
|
||||
const query = gql`
|
||||
async listExtensions() {
|
||||
const query = gql`
|
||||
query {
|
||||
fetchAllExtensions {
|
||||
extension {
|
||||
|
@ -92,14 +63,14 @@ class FreepbxManager {
|
|||
}
|
||||
`;
|
||||
|
||||
return await this.pbxCall(query);
|
||||
}
|
||||
return await this.client.request(query);
|
||||
}
|
||||
|
||||
async addExtension(ext, name) {
|
||||
async addExtension(ext, name) {
|
||||
ext = String(ext);
|
||||
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!) {
|
||||
addExtension(input: {
|
||||
extensionId: $ext
|
||||
|
@ -115,18 +86,18 @@ class FreepbxManager {
|
|||
}
|
||||
`;
|
||||
|
||||
const variables = {
|
||||
ext,
|
||||
name,
|
||||
vmPassword: ext,
|
||||
};
|
||||
const variables = {
|
||||
ext,
|
||||
name,
|
||||
vmPassword: ext,
|
||||
};
|
||||
|
||||
return await this.pbxCall(query, variables);
|
||||
}
|
||||
return await this.client.request(query, variables);
|
||||
}
|
||||
|
||||
async deleteExtension(ext) {
|
||||
async deleteExtension(ext) {
|
||||
ext = String(ext);
|
||||
const query = gql`
|
||||
const query = gql`
|
||||
mutation deleteExtension($ext: ID!) {
|
||||
deleteExtension(input: { extensionId: $ext }) {
|
||||
status
|
||||
|
@ -134,17 +105,17 @@ class FreepbxManager {
|
|||
}
|
||||
`;
|
||||
|
||||
const variables = {
|
||||
ext,
|
||||
};
|
||||
const variables = {
|
||||
ext,
|
||||
};
|
||||
|
||||
const fpbxQuery = this.pbxCall(query, variables);
|
||||
const fpbxQuery = this.client.request(query, variables);
|
||||
const dbQuery = this.pool.query('DELETE FROM paging_groups WHERE ext = ?', [ext]);
|
||||
return await Promise.all([fpbxQuery, dbQuery]);
|
||||
}
|
||||
return await Promise.all([fpbxQuery, dbQuery]);
|
||||
}
|
||||
|
||||
async reload() {
|
||||
const query = gql`
|
||||
async reload() {
|
||||
const query = gql`
|
||||
mutation {
|
||||
doreload(input: { clientMutationId: "${Math.random().toString(36).substring(2, 14)}" }) {
|
||||
status
|
||||
|
@ -152,11 +123,11 @@ class FreepbxManager {
|
|||
}
|
||||
`;
|
||||
|
||||
return await this.pbxCall(query);
|
||||
}
|
||||
return await this.client.request(query);
|
||||
}
|
||||
|
||||
// async updateName(ext, name) {
|
||||
// const query = gql`
|
||||
// async updateName(ext, name) {
|
||||
// const query = gql`
|
||||
// mutation updateName($ext: ID!, $name: String!) {
|
||||
// updateExtension(input: {extensionId: $ext, name: $name}) {
|
||||
// status,
|
||||
|
@ -164,13 +135,13 @@ class FreepbxManager {
|
|||
// }
|
||||
// }`;
|
||||
|
||||
// const variables = {
|
||||
// ext,
|
||||
// name,
|
||||
// };
|
||||
// const variables = {
|
||||
// ext,
|
||||
// name,
|
||||
// };
|
||||
|
||||
// return await this.pbxCall(query, variables);
|
||||
// }
|
||||
// return await this.client.request(query, variables);
|
||||
// }
|
||||
// TODO: Implement updateName method, Current implementation resets extension for some reason
|
||||
|
||||
async joinPageGroup(ext, pageGroup) {
|
||||
|
@ -197,9 +168,7 @@ class FreepbxManager {
|
|||
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);
|
||||
const existingExts = exts.map(ext => parseInt(ext.user.extension, 10)).sort((a, b) => a - b);
|
||||
|
||||
let nextExt = startExt;
|
||||
for (let i = 0; i < existingExts.length; i++) {
|
||||
|
|
62
index.js
62
index.js
|
@ -1,5 +1,5 @@
|
|||
require("dotenv").config();
|
||||
const cron = require("node-cron")
|
||||
|
||||
const fs = require('fs');
|
||||
const mariadb = require("mariadb");
|
||||
const pool = mariadb.createPool({
|
||||
|
@ -53,8 +53,6 @@ global.fpbx = fpbx;
|
|||
global.client = client;
|
||||
global.log = log;
|
||||
|
||||
const deletion = require("./deletions.js");
|
||||
|
||||
client.on('ready', async () => {
|
||||
log.success(`Logged in as ${client.user.displayName}`);
|
||||
const commands = require("./commands")
|
||||
|
@ -71,40 +69,10 @@ client.on('ready', async () => {
|
|||
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) {
|
||||
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 => {
|
||||
|
@ -119,9 +87,7 @@ client.on('interactionCreate', async interaction => {
|
|||
await command.execute(interaction);
|
||||
} catch (error) {
|
||||
log.error(error);
|
||||
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}`);
|
||||
});;
|
||||
await interaction.reply({ content: 'There was an error while executing this command!', ephemeral: true });
|
||||
}
|
||||
break;
|
||||
case Discord.InteractionType.MessageComponent:
|
||||
|
@ -133,33 +99,12 @@ client.on('interactionCreate', async interaction => {
|
|||
await component.execute(interaction);
|
||||
} catch (error) {
|
||||
log.error(error);
|
||||
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}`);
|
||||
});;
|
||||
await interaction.reply({ content: 'There was an error while executing this component!', ephemeral: true });
|
||||
}
|
||||
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")) {
|
||||
const importData = JSON.parse(fs.readFileSync("./import.json", "utf8"));
|
||||
|
||||
|
@ -180,7 +125,6 @@ if (fs.existsSync("./import.json")) {
|
|||
insertData();
|
||||
}
|
||||
|
||||
|
||||
// Startup
|
||||
require("./migrations")(pool).then(() => {
|
||||
log.success("Database migrations complete.");
|
||||
|
|
|
@ -64,18 +64,5 @@ module.exports.execute = async (interaction) => {
|
|||
});
|
||||
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;
|
||||
}
|
||||
}
|
|
@ -6,52 +6,16 @@ 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)",
|
||||
"description": "The LiteNet Community PBX is hosted through, and is sponsored by SnakeCraft Hosting!\nOffering affordable game hosting, Discord bot hosting, and VPS services since 2020.\nGet started at https://go.litenet.tel/sch-affiliate\nCheck them out on [Discord](https://discord.gg/invite/xcnKUD8)\n\n-# The link above is an affiliate link. We will receive credits from any purchase made via this link.\n-# SnakeCraft Hosting has no administrative control over, nor has access to private information stored on LiteNet. SnakeCraft Hosting provides hosting for LiteNet free of charge. Specific details regarding their affiliate program can be found [here](https://my.snakecrafthosting.com/index.php?rp=/knowledgebase/4/Affiliate-Program-FAQs.html)",
|
||||
"color": 7955428,
|
||||
"fields": [
|
||||
{
|
||||
"name": "What's this?",
|
||||
"value": "The community PBX is a public, free to use [FreePBX](https://freepbx.org) based phone system that any server member is welcome to get a number on!\nEveryone on the system has their own 4 digit number, that can be used to call between other members on the system.\nThe PBX runs on a Snakecraft Hosting VPS graciously provided to us at no cost!"
|
||||
"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?",
|
||||
|
@ -67,7 +31,7 @@ module.exports.execute = async (interaction) => {
|
|||
}
|
||||
],
|
||||
"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": {
|
||||
|
|
|
@ -1,5 +0,0 @@
|
|||
|
||||
module.exports = {}
|
||||
module.exports.execute = async (interaction) => {
|
||||
interaction.reply({ephemeral: true, content: `To be implemented!`})
|
||||
}
|
|
@ -79,64 +79,5 @@ module.exports.execute = async (interaction) => {
|
|||
]
|
||||
});
|
||||
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;
|
||||
}
|
||||
}
|
|
@ -17,7 +17,6 @@ module.exports.execute = async (interaction) => {
|
|||
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 });
|
||||
|
@ -27,7 +26,6 @@ module.exports.execute = async (interaction) => {
|
|||
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 });
|
||||
|
|
|
@ -29,7 +29,6 @@ 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}\`||`,
|
||||
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 });
|
||||
|
|
|
@ -11,9 +11,8 @@ module.exports.execute = async (interaction) => {
|
|||
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({
|
||||
return await interaction.reply({
|
||||
ephemeral: true, embeds: [
|
||||
{
|
||||
title: "Your Extension Info",
|
||||
|
@ -23,12 +22,4 @@ 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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -1,5 +0,0 @@
|
|||
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
22
package-lock.json
generated
|
@ -15,7 +15,6 @@
|
|||
"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"
|
||||
|
@ -1248,18 +1247,6 @@
|
|||
"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",
|
||||
|
@ -1738,15 +1725,6 @@
|
|||
"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",
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
"main": "index.js",
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1",
|
||||
"start": "node index.js"
|
||||
"start": "node index.js"
|
||||
},
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
|
@ -16,7 +16,6 @@
|
|||
"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"
|
||||
|
|
Loading…
Reference in a new issue