Compare commits
124 Commits
c46b7c6ea6
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a4d07db766 | ||
|
|
03c940e07c | ||
|
|
fee40dd609 | ||
|
|
5671503594 | ||
|
|
0fd511cfaf | ||
|
|
9d0aa0191f | ||
|
|
9ce8928d82 | ||
|
|
abadcf5cb5 | ||
|
|
4b18df9e2c | ||
|
|
26a8d8a172 | ||
|
|
26f3493c8f | ||
|
|
fd261ef015 | ||
|
|
91201c3527 | ||
|
|
dfb4c6afa1 | ||
|
|
3b8e70208a | ||
|
|
99b60bf02c | ||
|
|
831317b9f0 | ||
|
|
2477b10900 | ||
|
|
67c29f1d72 | ||
|
|
91ed4fb1dc | ||
|
|
8ea02d1c0b | ||
|
|
b15ac7c973 | ||
|
|
c2c90019d7 | ||
|
|
686ddc8a0f | ||
|
|
463ccc1bd5 | ||
|
|
14a2b4a1b9 | ||
|
|
0c1f6cd867 | ||
|
|
7175487d77 | ||
|
|
f0eac45955 | ||
|
|
c81dce99e2 | ||
|
|
38cb1054e0 | ||
|
|
12441c5c6d | ||
|
|
1c1b071bd7 | ||
|
|
961c5c19e2 | ||
|
|
49e52d8944 | ||
|
|
63ccfa70d3 | ||
|
|
4bb8038a1d | ||
|
|
a353b9adbb | ||
|
|
bf69e93e29 | ||
|
|
6cae18e70c | ||
|
|
e7229322e4 | ||
|
|
34aa5d17dc | ||
|
|
565fd5af37 | ||
|
|
f04154d361 | ||
|
|
383663e980 | ||
|
|
7b21d4601f | ||
|
|
d0c2fcc8eb | ||
|
|
9ba90af464 | ||
|
|
6e8681e52d | ||
|
|
8ba1ed36d8 | ||
|
|
3074e88963 | ||
|
|
737b493b23 | ||
|
|
539dbd9518 | ||
|
|
59bfdbe143 | ||
|
|
560ed401cf | ||
|
|
7b91667414 | ||
|
|
fd9b6d9d1c | ||
|
|
3aae427249 | ||
|
|
bc4c8f72d0 | ||
|
|
cc4e5e762d | ||
|
|
31cedb2e9c | ||
|
|
61a616ec6b | ||
|
|
d7b7b04f78 | ||
|
|
2c9383824e | ||
|
|
cebd316939 | ||
|
|
af19db8e17 | ||
|
|
45b9a62c64 | ||
|
|
8c3164029f | ||
|
|
bec4072837 | ||
|
|
ac82b0efd0 | ||
|
|
854c73cc4e | ||
|
|
238fe6a254 | ||
|
|
96d9c38425 | ||
|
|
4df3de4d4a | ||
|
|
461b449194 | ||
|
|
757fdfb3b2 | ||
|
|
9bc80887ce | ||
|
|
0ce0f72ed3 | ||
|
|
49ae941e83 | ||
|
|
8e73659855 | ||
|
|
0a76804490 | ||
| 9ad24ca8ec | |||
| 7676e883a5 | |||
| 86a71d3d6f | |||
| 1dd53ffc84 | |||
| fc31026304 | |||
| 8a4b7685d2 | |||
|
|
49c1a1d724 | ||
|
|
a423417949 | ||
|
|
2fb0bc8920 | ||
| 38470bd788 | |||
| 8995f8b372 | |||
| a6c26d61da | |||
| ab0f94baf8 | |||
|
|
62c00eec09 | ||
|
|
ea63abcb93 | ||
|
|
1df2027a29 | ||
|
|
a36ddd9614 | ||
|
|
ce4a6e925e | ||
|
|
8790fb05fb | ||
|
|
0f8dc86dd5 | ||
| 4b8771b925 | |||
|
|
3319a9617e | ||
|
|
c78ed89707 | ||
|
|
be5943e9d4 | ||
|
|
64edc612df | ||
|
|
ed04e24fc6 | ||
|
|
3e50f0b2f3 | ||
|
|
2754494083 | ||
|
|
e62be27f74 | ||
|
|
96b32d1241 | ||
|
|
7983a45281 | ||
|
|
fea7ed2c7f | ||
|
|
bee95ed999 | ||
|
|
7d4f48a446 | ||
|
|
b209a672c6 | ||
|
|
7983670c81 | ||
|
|
73da7ee2f4 | ||
|
|
6ab0ec6d6f | ||
|
|
84aa4c5aff | ||
|
|
32f827fe5e | ||
|
|
d48d1155cb | ||
|
|
3cb53605c7 | ||
|
|
b7d4cbf46c |
32
.gitea/workflows/DRBv3-Client_Setup_tests.yaml.disabled
Normal file
32
.gitea/workflows/DRBv3-Client_Setup_tests.yaml.disabled
Normal file
@@ -0,0 +1,32 @@
|
||||
name: DRB Build Tests
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches:
|
||||
- master
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
|
||||
jobs:
|
||||
drb_test_setup:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '20'
|
||||
|
||||
- name: Test setup script
|
||||
working-directory: "./client"
|
||||
run: |
|
||||
sudo useradd -m -s /bin/bash pi
|
||||
sudo bash setup.sh --test
|
||||
|
||||
- name: Test running client node
|
||||
working-directory: "./client"
|
||||
run: |
|
||||
bash serviceStart.sh
|
||||
24
.gitea/workflows/DRBv3_server_build.yaml
Normal file
24
.gitea/workflows/DRBv3_server_build.yaml
Normal file
@@ -0,0 +1,24 @@
|
||||
name: DRB Server Build
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
|
||||
env:
|
||||
NODE_ENV: production
|
||||
SERVER_PORT: 3000
|
||||
MONGO_URL: ${{ secrets.MONGO_URL }}
|
||||
DISCORD_TOKEN: ${{ secrets.DISCORD_TOKEN }}
|
||||
|
||||
jobs:
|
||||
drb_server_build:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Build the latest code
|
||||
working-directory: './server'
|
||||
run: make build
|
||||
81
.gitea/workflows/DRBv3_tests.yaml
Normal file
81
.gitea/workflows/DRBv3_tests.yaml
Normal file
@@ -0,0 +1,81 @@
|
||||
name: DRB Tests
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches:
|
||||
- master
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
|
||||
env:
|
||||
NODE_ENV: development
|
||||
MONGO_INITDB_ROOT_USERNAME: admin
|
||||
MONGO_INITDB_ROOT_PASSWORD: admin
|
||||
MONGO_INITDB_DATABASE: drb
|
||||
SERVER_PORT: 3000
|
||||
MONGO_URL: "mongodb://mongodb:27017/drb"
|
||||
DISCORD_TOKEN: ${{ secrets.DISCORD_TOKEN }}
|
||||
TEST_GUILD_ID: ${{ secrets.TEST_GUILD_ID }}
|
||||
TEST_CLIENT_TOKEN: ${{ secrets.TEST_CLIENT_TOKEN }}
|
||||
TEST_CHANNEL_ID: ${{ secrets.TEST_CHANNEL_ID }}
|
||||
TEST_SYSTEM: ${{ secrets.TEST_SYSTEM }}
|
||||
EXPECTED_CLIENT_ID: ${{ secrets.TEST_CLIENT_ID }}
|
||||
EXPECTED_USERNAME: ${{ secrets.EXPECTED_USERNAME }}
|
||||
PDAB_PORT: ${{ vars.PDAB_PORT }}
|
||||
|
||||
jobs:
|
||||
drb_mocha_tests:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
services:
|
||||
mongodb:
|
||||
image: mongo:latest
|
||||
ports:
|
||||
- 27017:27017
|
||||
options: >-
|
||||
--health-cmd mongo
|
||||
--health-interval 10s
|
||||
--health-timeout 5s
|
||||
--health-retries 5
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '20'
|
||||
|
||||
- name: Remove package-lock.json (Server)
|
||||
working-directory: "./server"
|
||||
run: rm package-lock.json
|
||||
|
||||
- name: Install Node-GYP (Server)
|
||||
working-directory: "./server"
|
||||
run: npm install -g node-gyp
|
||||
|
||||
- name: Install dependencies (Server)
|
||||
working-directory: "./server"
|
||||
run: npm install
|
||||
|
||||
- name: Remove package-lock.json (Client)
|
||||
working-directory: "./client"
|
||||
run: rm package-lock.json
|
||||
|
||||
- name: Install dependencies (Client)
|
||||
working-directory: "./client"
|
||||
run: npm install
|
||||
|
||||
- run: echo "Node has finished installing dependencies"
|
||||
|
||||
- name: Run Server tests
|
||||
working-directory: "./server"
|
||||
run: npm test
|
||||
|
||||
- name: Run Client tests
|
||||
working-directory: "./client"
|
||||
run: npm test
|
||||
|
||||
- run: echo "Completed the DRB tests"
|
||||
@@ -2,6 +2,7 @@ import { generateUniqueID } from './modules/baseUtils.mjs';
|
||||
import { updateId } from './modules/updateConfig.mjs';
|
||||
import { ClientNodeConfig } from './modules/clientObjectDefinitions.mjs';
|
||||
import { initSocketConnection } from './modules/socketClient.mjs';
|
||||
import { checkForUpdates } from './modules/selfUpdater.mjs'
|
||||
|
||||
import dotenv from 'dotenv';
|
||||
dotenv.config()
|
||||
@@ -9,6 +10,9 @@ dotenv.config()
|
||||
var localNodeConfig = new ClientNodeConfig({})
|
||||
|
||||
async function boot() {
|
||||
// Check if there have been any updates
|
||||
await checkForUpdates();
|
||||
|
||||
if (localNodeConfig.node.nuid === undefined || localNodeConfig.node.nuid === '' || localNodeConfig.node.nuid === '0' || localNodeConfig.node.nuid === 0) {
|
||||
// Run the first time boot sequence
|
||||
await firstTimeBoot();
|
||||
@@ -24,15 +28,13 @@ async function boot() {
|
||||
*/
|
||||
async function firstTimeBoot() {
|
||||
// Generate a new ID for the node
|
||||
localNodeConfig.node.id = generateUniqueID();
|
||||
localNodeConfig.node.nuid = await generateUniqueID();
|
||||
console.log(`Generated a new unique ID for this node: '${localNodeConfig.node.nuid}'`);
|
||||
|
||||
// Update the config with the new ID
|
||||
updateId(localNodeConfig.node.nuid);
|
||||
await updateId(localNodeConfig.node.nuid);
|
||||
console.log("Updated the config with the new node ID");
|
||||
// TODO - Create the config file with the ID given and replace the update above
|
||||
// TODO - Check if the system is linux or windows and set the 'type' param in DAB
|
||||
|
||||
// TODO - Implement web server so users can update radio systems easily
|
||||
// TODO - Implement logic to check if the presets are set
|
||||
return
|
||||
|
||||
@@ -1,148 +0,0 @@
|
||||
import {
|
||||
NoSubscriberBehavior,
|
||||
StreamType,
|
||||
createAudioPlayer,
|
||||
createAudioResource,
|
||||
entersState,
|
||||
AudioPlayerStatus,
|
||||
VoiceConnectionStatus,
|
||||
joinVoiceChannel,
|
||||
getVoiceConnection,
|
||||
} from '@discordjs/voice';
|
||||
|
||||
import { GatewayIntentBits } from 'discord-api-types/v10';
|
||||
|
||||
import { Client, Events, ActivityType } from 'discord.js';
|
||||
|
||||
import prism_media from 'prism-media';
|
||||
const { FFmpeg } = prism_media;
|
||||
|
||||
// Import the DAB settings from the dynamic settings file
|
||||
import {device, maxTransmissionGap, type} from './dabSettings.mjs'
|
||||
|
||||
const player = createAudioPlayer({
|
||||
behaviors: {
|
||||
noSubscriber: NoSubscriberBehavior.Play,
|
||||
maxMissedFrames: Math.round(maxTransmissionGap / 20),
|
||||
},
|
||||
});
|
||||
|
||||
function attachRecorder() {
|
||||
player.play(
|
||||
createAudioResource(
|
||||
new FFmpeg({
|
||||
args: [
|
||||
'-analyzeduration',
|
||||
'0',
|
||||
'-loglevel',
|
||||
'0',
|
||||
'-f',
|
||||
type,
|
||||
'-i',
|
||||
type === 'dshow' ? `audio=${device}` : device,
|
||||
'-acodec',
|
||||
'libopus',
|
||||
'-f',
|
||||
'opus',
|
||||
'-ar',
|
||||
'48000',
|
||||
'-ac',
|
||||
'2',
|
||||
],
|
||||
}),
|
||||
{
|
||||
inputType: StreamType.OggOpus,
|
||||
},
|
||||
),
|
||||
);
|
||||
console.log('Attached recorder - ready to go!');
|
||||
}
|
||||
|
||||
player.on('stateChange', (oldState, newState) => {
|
||||
if (oldState.status === AudioPlayerStatus.Idle && newState.status === AudioPlayerStatus.Playing) {
|
||||
console.log('Playing audio output on audio player');
|
||||
} else if (newState.status === AudioPlayerStatus.Idle) {
|
||||
console.log('Playback has stopped. Attempting to restart.');
|
||||
attachRecorder();
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {any} channel
|
||||
* @returns {any}
|
||||
*/
|
||||
export async function connectToChannel(channel) {
|
||||
const connection = joinVoiceChannel({
|
||||
channelId: channel.id,
|
||||
guildId: channel.guild.id,
|
||||
adapterCreator: channel.guild.voiceAdapterCreator,
|
||||
});
|
||||
try {
|
||||
await entersState(connection, VoiceConnectionStatus.Ready, 30_000);
|
||||
await connection.subscribe(player);
|
||||
return connection;
|
||||
} catch (error) {
|
||||
connection.destroy();
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
export async function getVoiceChannelFromID(client, channelID) {
|
||||
return client.channels.cache.get(channelID)
|
||||
}
|
||||
|
||||
export async function checkIfConnectedToVC(guildId) {
|
||||
const connection = getVoiceConnection(guildId)
|
||||
console.log("Connection!", connection);
|
||||
return connection
|
||||
}
|
||||
|
||||
export const getVoiceConnectionFromGuild = async (guildId) => {
|
||||
return getVoiceConnection(guildId);
|
||||
}
|
||||
|
||||
export async function initDiscordBotClient(token, systemName, readyCallback) {
|
||||
const client = new Client({
|
||||
intents: [GatewayIntentBits.Guilds, GatewayIntentBits.GuildMessages, GatewayIntentBits.GuildVoiceStates, GatewayIntentBits.MessageContent],
|
||||
});
|
||||
|
||||
client.on(Events.ClientReady, () => {
|
||||
console.log('discord.js client is ready!');
|
||||
|
||||
// Attach the recorder to the VC connection
|
||||
attachRecorder();
|
||||
|
||||
// Set the activity of the bot user
|
||||
client.user.setPresence({
|
||||
activities: [{ name: `${systemName}`, type: ActivityType.Listening }],
|
||||
});
|
||||
|
||||
//
|
||||
readyCallback(client);
|
||||
});
|
||||
|
||||
/* on event create
|
||||
// TODO - Implement methods for discord users to interact directly with the bots for realtime info (last talked ID/TG, etc.)
|
||||
client.on(Events.MessageCreate, async (message) => {
|
||||
if (!message.guild) return;
|
||||
console.log(`New Message:`, message.content);
|
||||
if (message.content === '-join') {
|
||||
const channel = message.member?.voice.channel;
|
||||
if (channel) {
|
||||
try {
|
||||
const connection = await connectToChannel(channel);
|
||||
connection.subscribe(player);
|
||||
await message.reply('Playing now!');
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
} else {
|
||||
await message.reply('Join a voice channel then try again!');
|
||||
}
|
||||
}
|
||||
});
|
||||
*/
|
||||
|
||||
client.login(token);
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
import { executeCommand } from '../modules/cliHandler.mjs';
|
||||
import { extractValue } from '../modules/baseUtils.mjs';
|
||||
import os from 'os';
|
||||
|
||||
// Defaults to Windows values for testing
|
||||
let device = "VoiceMeeter VAIO3 Output (VB-Audio VoiceMeeter VAIO3)";
|
||||
let maxTransmissionGap = 500;
|
||||
let type = "dshow";
|
||||
|
||||
// Set Linux values for use in production
|
||||
if (os.platform() === 'linux') {
|
||||
await executeCommand("pactl", ['list', 'short', 'sources']).then(cliOutput => {
|
||||
device = extractValue(cliOutput, '(?:\d[ ]{4,12}(.+?)[ ]{4,12}(?:.+?[ ]{4,12}){2,4}RUNNING)')
|
||||
});
|
||||
type = "pulse"; // Replace with appropriate type for Linux
|
||||
}
|
||||
|
||||
export { device, maxTransmissionGap, type };
|
||||
@@ -1,114 +0,0 @@
|
||||
import { connectToChannel, checkIfConnectedToVC, initDiscordBotClient, getVoiceChannelFromID, getVoiceConnectionFromGuild } from './dab.mjs';
|
||||
import { openOP25, closeOP25 } from '../op25Handler/op25Handler.mjs';
|
||||
|
||||
let activeDiscordClient = undefined;
|
||||
const activeDiscordVoiceConnections = {};
|
||||
|
||||
/**
|
||||
* Join the requested server VC and listen to the requested system
|
||||
* @param {object} joinData The object containing all the information to join the server
|
||||
*/
|
||||
export const joinDiscordVC = async (joinData) => {
|
||||
console.log("Join requested: ", joinData)
|
||||
const connection = await new Promise((res) => {
|
||||
// Check if a client already exists
|
||||
if (!activeDiscordClient) {
|
||||
// Open a new client and join the requested channel with the requested ID
|
||||
initDiscordBotClient(joinData.clientID, joinData.system, client => {
|
||||
// Open an instance of OP25
|
||||
openOP25(joinData.system);
|
||||
|
||||
getVoiceChannelFromID(client, joinData.channelID).then(vc => {
|
||||
// Add the client object to the IO instance
|
||||
activeDiscordClient = client;
|
||||
const connection = connectToChannel(vc);
|
||||
activeDiscordVoiceConnections[vc.guild.id] = connection;
|
||||
console.log("Bot Connected to VC");
|
||||
res(connection);
|
||||
});
|
||||
});
|
||||
} else {
|
||||
// Join the requested channel with the requested ID
|
||||
getVoiceChannelFromID(activeDiscordClient, joinData.channelID).then(vc => {
|
||||
// Add the client object to the IO instance
|
||||
const connection = connectToChannel(vc);
|
||||
activeDiscordVoiceConnections[vc.guild.id] = connection;
|
||||
console.log("Bot Connected to VC");
|
||||
res(connection);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return connection;
|
||||
}
|
||||
|
||||
/**
|
||||
* Leave VC on the requested server
|
||||
* @param {string} guildId The guild ID to disconnect from VC
|
||||
*/
|
||||
export const leaveDiscordVC = async (guildId) => {
|
||||
console.log("Leave requested");
|
||||
if (await checkIfConnectedToVC(guildId)) {
|
||||
const connection = await getVoiceConnectionFromGuild(guildId);
|
||||
if (connection) {
|
||||
console.log("There is an open VC connection, closing it now");
|
||||
// Destroy the open VC connection
|
||||
connection.destroy();
|
||||
|
||||
// Remove the connection from the object
|
||||
delete activeDiscordVoiceConnections[guildId];
|
||||
|
||||
// Check if this was the last open VC connection
|
||||
if (Object.keys(activeDiscordVoiceConnections).length == 0) {
|
||||
console.log("No more open VC connections, closing the client-side discord client and OP25")
|
||||
// Close the active client if there are no open VCs after this one
|
||||
activeDiscordClient.destroy();
|
||||
activeDiscordClient = undefined;
|
||||
|
||||
// Close OP25
|
||||
await closeOP25();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Check if the bot is connected to a discord VC in the given server
|
||||
* @param {string} guildId The guild id to check the connection status in
|
||||
* @returns {boolean} If the node is connected to VC in the given guild
|
||||
*/
|
||||
export const checkIfDiscordVCConnected = async (guildId) => {
|
||||
console.log("Requested status check");
|
||||
if (await checkIfConnectedToVC(guildId)) {
|
||||
console.log("There is an open VC connection");
|
||||
return (true);
|
||||
} else {
|
||||
return (false);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get the username of the bot in a given guild
|
||||
* (there may be a server nickname given to the bot in a certain guild)
|
||||
* @param {string} guildId The guild id to check the connection status in
|
||||
* @returns {string} The username of the bot in the given guild's CV
|
||||
*/
|
||||
export const getDiscordUsername = async (guildId) => {
|
||||
console.log("Requested username");
|
||||
if (activeDiscordClient) return (activeDiscordClient.user.username);
|
||||
else return (undefined);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Check if there is an open discord client
|
||||
* @returns {boolean} If the client is open or not
|
||||
*/
|
||||
export const checkIfClientIsOpen = async () => {
|
||||
if (activeDiscordClient) {
|
||||
return (true);
|
||||
}
|
||||
return (false);
|
||||
}
|
||||
1
client/discordAudioBot/pdab
Submodule
1
client/discordAudioBot/pdab
Submodule
Submodule client/discordAudioBot/pdab added at fc6c114473
138
client/discordAudioBot/pdabHandler.mjs
Normal file
138
client/discordAudioBot/pdabHandler.mjs
Normal file
@@ -0,0 +1,138 @@
|
||||
// server.js
|
||||
import express from 'express';
|
||||
import http from 'http';
|
||||
import { Server } from 'socket.io';
|
||||
import { launchProcess } from '../modules/subprocessHandler.mjs';
|
||||
import { join, dirname } from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
|
||||
import dotenv from 'dotenv';
|
||||
dotenv.config()
|
||||
|
||||
const app = express();
|
||||
const server = http.createServer(app);
|
||||
const io = new Server(server);
|
||||
let pdabProcess = false;
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = dirname(__filename);
|
||||
|
||||
let botCallback;
|
||||
|
||||
export const initDiscordBotClient = (clientId, callback, runPDAB = true) => {
|
||||
botCallback = callback;
|
||||
|
||||
if (runPDAB) launchProcess("python", [join(__dirname, "./pdab/main.py"), process.env.AUDIO_DEVICE_ID, clientId, port], false, join(__dirname, "./pdab"));
|
||||
pdabProcess = true; // TODO - Make this more dynamic
|
||||
}
|
||||
|
||||
export const startPdabSocketServer = () => {
|
||||
const port = process.env.PDAB_PORT || 3000;
|
||||
|
||||
io.on('connection', (socket) => {
|
||||
console.log('A user connected');
|
||||
|
||||
socket.on('disconnect', () => {
|
||||
console.log('User disconnected');
|
||||
});
|
||||
|
||||
// Listen for the discord client ready event
|
||||
socket.on('discord_ready', (message) => {
|
||||
console.log("Message from local client", message);
|
||||
botCallback();
|
||||
});
|
||||
});
|
||||
|
||||
server.listen(port, async () => {
|
||||
console.log(`Server is running on port ${port}`);
|
||||
});
|
||||
return
|
||||
}
|
||||
|
||||
export const closePdabSocketServer = () => {
|
||||
if (io.sockets && io.sockets.length > 0) {
|
||||
io.sockets.forEach(socket => {
|
||||
socket.destroy();
|
||||
})
|
||||
}
|
||||
return io.close();
|
||||
}
|
||||
|
||||
// Function to emit a command to join a voice channel
|
||||
export const connectToChannel = (channelId) => {
|
||||
return new Promise((res) => {
|
||||
io.timeout(25000).emit('join_server', { channelId: channelId }, (status, value) => {
|
||||
console.log("Status returned from bot:", status, value);
|
||||
res(value[0]);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
// Function to emit a command to leave a voice channel
|
||||
export const leaveVoiceChannel = async (guildId) => {
|
||||
return await new Promise((res) => {
|
||||
io.timeout(25000).emit('leave_server', { guildId: guildId }, (status, clientRemainsOpen) => {
|
||||
console.log("Discord client remains open?", clientRemainsOpen);
|
||||
res(clientRemainsOpen[0])
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
// Set the presense of the discord client
|
||||
export const setDiscordClientPrsense = (system) => {
|
||||
return new Promise((res) => {
|
||||
io.timeout(25000).emit('set_system', { system: system }, (status) => {
|
||||
res();
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
// Placeholder functions (replace with actual implementation)
|
||||
export const checkIfConnectedToVC = async (guildId) => {
|
||||
console.log("Pdab process var:", pdabProcess);
|
||||
|
||||
if (!pdabProcess) return false;
|
||||
|
||||
return await new Promise((res) => {
|
||||
io.timeout(25000).emit('check_discord_vc_connected', { guildId: guildId }, (status, result) => {
|
||||
console.log(`Discord VC connected for guild ${guildId}: ${result}`);
|
||||
res((result[0]));
|
||||
});
|
||||
})
|
||||
};
|
||||
|
||||
export const requestDiscordUsername = (guildId) => {
|
||||
return new Promise((res) => {
|
||||
io.timeout(25000).emit('request_discord_username', { guildId: guildId }, (status, result) => {
|
||||
console.log(`Discord username: ${result[0]}`);
|
||||
res(result[0]);
|
||||
});
|
||||
})
|
||||
};
|
||||
|
||||
export const checkIfClientIsOpen = () => {
|
||||
return new Promise((res) => {
|
||||
io.timeout(25000).emit('check_client_is_open', (status, result) => {
|
||||
console.log(`Client is open: ${result}`);
|
||||
res(result[0])
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
export const requestDiscordID = () => {
|
||||
return new Promise((res) => {
|
||||
io.timeout(25000).emit('request_discord_id', (status, result) => {
|
||||
console.log(`Discord ID: ${result}`);
|
||||
res(result[0]);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
export const requestDiscordClientClose = () => {
|
||||
return new Promise((res) => {
|
||||
io.timeout(25000).emit('request_client_close');
|
||||
pdabProcess = false;
|
||||
res();
|
||||
});
|
||||
};
|
||||
122
client/discordAudioBot/pdabWrappers.mjs
Normal file
122
client/discordAudioBot/pdabWrappers.mjs
Normal file
@@ -0,0 +1,122 @@
|
||||
import { connectToChannel, leaveVoiceChannel, checkIfConnectedToVC, initDiscordBotClient, requestDiscordUsername, requestDiscordID, requestDiscordClientClose, closePdabSocketServer, setDiscordClientPrsense, startPdabSocketServer } from './pdabHandler.mjs';
|
||||
import { openOP25, closeOP25 } from '../op25Handler/op25Handler.mjs';
|
||||
|
||||
let activeDiscordClient = undefined;
|
||||
|
||||
/**
|
||||
* Join the requested server VC and listen to the requested system
|
||||
* @param {object} joinData The object containing all the information to join the server
|
||||
*/
|
||||
export const joinDiscordVC = async (joinData) => {
|
||||
console.log("Join requested: ", joinData);
|
||||
const connection = await new Promise(async (res) => {
|
||||
// Check if a client already exists
|
||||
console.log("Checking if there is a client open");
|
||||
if (!await checkIfClientIsOpen()) {
|
||||
console.log("There is no open client, starting it now");
|
||||
await startPdabSocketServer();
|
||||
// Open an instance of OP25
|
||||
console.log("Starting OP25")
|
||||
openOP25(joinData.system);
|
||||
|
||||
// Open a new client and join the requested channel with the requested ID
|
||||
initDiscordBotClient(joinData.clientID, () => {
|
||||
console.log("Started PDAB");
|
||||
|
||||
console.log("Setting the presense of the bot");
|
||||
setDiscordClientPrsense(joinData.system);
|
||||
|
||||
// Add the client object to the IO instance
|
||||
console.log("Connecting to channel")
|
||||
connectToChannel(joinData.channelID, (connectionStatus) => {
|
||||
console.log("Bot Connected to VC:", connectionStatus);
|
||||
res(connectionStatus);
|
||||
});
|
||||
});
|
||||
} else {
|
||||
// Join the requested channel with the requested ID
|
||||
console.log("There is an open client");
|
||||
|
||||
console.log("Connecting to channel")
|
||||
const connection = connectToChannel(joinData.channelID);
|
||||
console.log("Bot Connected to VC::");
|
||||
res(connection);
|
||||
}
|
||||
});
|
||||
|
||||
return connection;
|
||||
}
|
||||
|
||||
/**
|
||||
* Leave VC on the requested server
|
||||
* @param {string} guildId The guild ID to disconnect from VC
|
||||
*/
|
||||
export const leaveDiscordVC = async (guildId) => {
|
||||
console.log("Leave requested");
|
||||
if (await checkIfConnectedToVC(guildId)) {
|
||||
const clientRemainsOpen = await leaveVoiceChannel(guildId);
|
||||
console.log("Client should remain open: ", clientRemainsOpen);
|
||||
if (!clientRemainsOpen) {
|
||||
console.log("There are no open VC connections");
|
||||
await closeOP25();
|
||||
|
||||
// Close the python client
|
||||
await requestDiscordClientClose();
|
||||
|
||||
// Close the IPC server
|
||||
await closePdabSocketServer();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the bot is connected to a discord VC in the given server
|
||||
* @param {string} guildId The guild id to check the connection status in
|
||||
* @returns {boolean} If the node is connected to VC in the given guild
|
||||
*/
|
||||
export const checkIfDiscordVCConnected = async (guildId) => {
|
||||
console.log("Requested status check");
|
||||
if (await checkIfConnectedToVC(guildId)) {
|
||||
console.log("There is an open VC connection");
|
||||
return (true);
|
||||
} else {
|
||||
return (false);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the username of the bot in a given guild
|
||||
* (there may be a server nickname given to the bot in a certain guild)
|
||||
* @param {string} guildId The guild id to check the connection status in
|
||||
* @returns {string} The username of the bot in the given guild's VC
|
||||
*/
|
||||
export const getDiscordUsername = async (guildId) => {
|
||||
console.log("Requested username");
|
||||
if (checkIfClientIsOpen()) {
|
||||
return await requestDiscordUsername(guildId)
|
||||
} else return (undefined);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the ID of the currently running bot
|
||||
* @returns {string} The ID of the active client
|
||||
*/
|
||||
export const getDiscordID = async () => {
|
||||
console.log("Requested ID");
|
||||
if (checkIfClientIsOpen()) {
|
||||
return await requestDiscordID();
|
||||
}
|
||||
else return (undefined);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Check if there is an open discord client
|
||||
* @returns {boolean} If the client is open or not
|
||||
*/
|
||||
export const checkIfClientIsOpen = async () => {
|
||||
if (activeDiscordClient) {
|
||||
return (true);
|
||||
}
|
||||
return (false);
|
||||
}
|
||||
@@ -71,7 +71,7 @@ export const generateUniqueID = () => {
|
||||
* @returns {string|null} The value found after the pattern, or null if not found.
|
||||
*/
|
||||
export const extractValue = (input, pattern) => {
|
||||
const regex = new RegExp(`${pattern}\\s+(\\S+)`);
|
||||
const regex = new RegExp(`${pattern}`);
|
||||
const match = input.match(regex);
|
||||
return match ? match[1] : null;
|
||||
return match ? match : null;
|
||||
};
|
||||
@@ -1,5 +1,6 @@
|
||||
import simpleGit from 'simple-git';
|
||||
import { restartService } from './serviceHandler.mjs'
|
||||
import { launchProcess } from './subprocessHandler.mjs'
|
||||
|
||||
const git = simpleGit();
|
||||
|
||||
@@ -24,15 +25,21 @@ export const checkForUpdates = async () => {
|
||||
if (gitStatus.modified.length > 0){
|
||||
// There is locally modified code
|
||||
console.log("There is locally modified code, resetting...");
|
||||
await git.reset('hard', ['origin/master'])
|
||||
await git.stash();
|
||||
await git.reset('hard', ['origin/master']);
|
||||
}
|
||||
|
||||
// Pull the latest changes from the remote repository
|
||||
await git.pull();
|
||||
|
||||
console.log('Update completed successfully. Restarting the application...');
|
||||
// Run the post-update script
|
||||
console.log('Running post-update script...');
|
||||
await launchProcess("bash", ['./post-update.sh'], true);
|
||||
|
||||
// Restart the application to apply the updates
|
||||
console.log('Update completed successfully. Restarting the application...');
|
||||
restartApplication();
|
||||
|
||||
return true
|
||||
} else {
|
||||
console.log('The application is up to date.');
|
||||
|
||||
@@ -1,52 +1,58 @@
|
||||
import { exec } from 'child_process';
|
||||
|
||||
/**
|
||||
* Starts the given service from the command line.
|
||||
* @param {string} serviceName The service name to be started. Ex: ... start "{serviceName}.service"
|
||||
* @returns {Promise<void>}
|
||||
* Executes a system command with error handling.
|
||||
* @param {string} command The command to execute.
|
||||
* @returns {Promise<{ stdout: string, stderr: string }>} A promise resolving to an object containing stdout and stderr.
|
||||
*/
|
||||
export const startService = async (serviceName) => {
|
||||
const executeCommand = (command) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
exec(`sudo systemctl start ${serviceName}.service`, (error, stdout, stderr) => {
|
||||
exec(command, (error, stdout, stderr) => {
|
||||
if (error) {
|
||||
reject(error);
|
||||
console.error(`Command failed with error: ${error.message}`);
|
||||
resolve({ stdout, stderr });
|
||||
} else {
|
||||
resolve();
|
||||
resolve({ stdout, stderr });
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Starts the given service from the command line.
|
||||
* @param {string} serviceName The service name to be started.
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
export const startService = async (serviceName) => {
|
||||
try {
|
||||
await executeCommand(`sudo systemctl start ${serviceName}.service`);
|
||||
} catch (error) {
|
||||
console.error(`Failed to start service: ${error.message}`);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Restarts the given service from the command line.
|
||||
* @param {string} serviceName The service name to be restarted. Ex: ... restart "{serviceName}.service"
|
||||
* @param {string} serviceName The service name to be restarted.
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
export const restartService = async (serviceName) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
exec(`sudo systemctl restart ${serviceName}.service`, (error, stdout, stderr) => {
|
||||
if (error) {
|
||||
reject(error);
|
||||
} else {
|
||||
resolve();
|
||||
try {
|
||||
await executeCommand(`sudo systemctl restart ${serviceName}.service`);
|
||||
} catch (error) {
|
||||
console.error(`Failed to restart service: ${error.message}`);
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Stops the given service from the command line.
|
||||
* @param {string} serviceName The service name to be stopped. Ex: ... stop "{serviceName}.service"
|
||||
* @param {string} serviceName The service name to be stopped.
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
export const stopService = async (serviceName) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
exec(`sudo systemctl stop ${serviceName}.service`, (error, stdout, stderr) => {
|
||||
if (error) {
|
||||
reject(error);
|
||||
} else {
|
||||
resolve();
|
||||
try {
|
||||
await executeCommand(`sudo systemctl stop ${serviceName}.service`);
|
||||
} catch (error) {
|
||||
console.error(`Failed to stop service: ${error.message}`);
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
@@ -1,5 +1,5 @@
|
||||
import { io } from "socket.io-client";
|
||||
import { logIntoServerWrapper, nodeCheckStatus, nodeJoinServer, nodeLeaveServer, nodeGetUsername, nodeCheckDiscordClientStatus, nodeCheckCurrentSystem, nodeUpdate } from "./socketClientWrappers.mjs";
|
||||
import { logIntoServerWrapper, nodeCheckStatus, nodeJoinServer, nodeLeaveServer, nodeGetUsername, nodeCheckDiscordClientStatus, nodeCheckCurrentSystem, nodeUpdate, nodeGetDiscordID } from "./socketClientWrappers.mjs";
|
||||
|
||||
/**
|
||||
* Initialize the socket connection with the server, this will handle disconnects within itself
|
||||
@@ -36,6 +36,9 @@ export const initSocketConnection = async (localNodeConfig) => {
|
||||
// Requested to get the discord username in a given guild
|
||||
socket.on('node-get-discord-username', nodeGetUsername);
|
||||
|
||||
// Requested to get the ID of the active discord client
|
||||
socket.on('node-get-discord-id', nodeGetDiscordID);
|
||||
|
||||
// Requested to check if the node is connected to VC in a given guild
|
||||
socket.on('node-check-connected-status', nodeCheckStatus);
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { checkIfDiscordVCConnected, joinDiscordVC, leaveDiscordVC, getDiscordUsername, checkIfClientIsOpen } from '../discordAudioBot/dabWrappers.mjs';
|
||||
import { checkIfDiscordVCConnected, joinDiscordVC, leaveDiscordVC, getDiscordUsername, checkIfClientIsOpen, getDiscordID } from '../discordAudioBot/pdabWrappers.mjs';
|
||||
import { getCurrentSystem } from '../op25Handler/op25Handler.mjs';
|
||||
import { checkForUpdates } from './selfUpdater.mjs';
|
||||
|
||||
@@ -80,6 +80,15 @@ export const nodeGetUsername = async (guildId, socketCallback) => {
|
||||
socketCallback(await getDiscordUsername(guildId));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the ID of the active client
|
||||
* @param {any} socketCallback The callback function to return the result to
|
||||
* @callback {any}
|
||||
*/
|
||||
export const nodeGetDiscordID = async (socketCallback) => {
|
||||
socketCallback(await getDiscordID());
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Check if the local node has an open discord client in any server
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import { spawn } from "child_process";
|
||||
import dotenv from 'dotenv';
|
||||
dotenv.config()
|
||||
|
||||
/**
|
||||
* Object to store references to spawned processes.
|
||||
@@ -10,20 +12,52 @@ const runningProcesses = {};
|
||||
* Launches a new process if it's not already running.
|
||||
* @param {string} processName - The name of the process to launch.
|
||||
* @param {string[]} args - The arguments to pass to the process.
|
||||
* @param {boolean} waitForClose - Set this to wait to return until the process exits
|
||||
*/
|
||||
export const launchProcess = (processName, args) => {
|
||||
export const launchProcess = (processName, args, waitForClose = false, pcwd = undefined) => {
|
||||
if (!runningProcesses[processName]) {
|
||||
const childProcess = spawn(processName, args);
|
||||
let childProcess;
|
||||
if (pcwd) {
|
||||
childProcess = spawn(processName, args, { cwd: pcwd });
|
||||
}
|
||||
else {
|
||||
childProcess = spawn(processName, args);
|
||||
}
|
||||
|
||||
// Store reference to the spawned process
|
||||
runningProcesses[processName] = childProcess;
|
||||
|
||||
// Output the process output in development
|
||||
var scriptOutput = "";
|
||||
|
||||
// Get the stdout from the child process
|
||||
childProcess.stdout.setEncoding('utf8');
|
||||
childProcess.stdout.on('data', (data) => {
|
||||
if (process.env.NODE_ENV === "development") console.log(`Data from ${processName}:`, data);
|
||||
scriptOutput += data.toString();
|
||||
});
|
||||
|
||||
// Get the stderr from the child process
|
||||
childProcess.stderr.setEncoding('utf8');
|
||||
childProcess.stderr.on('data', (data) => {
|
||||
if (process.env.NODE_ENV === "development") console.log(`Data from ${processName}:`, data);
|
||||
scriptOutput += data.toString();
|
||||
})
|
||||
|
||||
let code = new Promise(res => {
|
||||
childProcess.on('exit', (code, signal) => {
|
||||
// Remove reference to the process when it exits
|
||||
delete runningProcesses[processName];
|
||||
console.log(`${processName} process exited with code ${code} and signal ${signal}`);
|
||||
console.log("Child process console output: ", scriptOutput);
|
||||
res(code);
|
||||
})
|
||||
});
|
||||
|
||||
if (waitForClose === true) {
|
||||
return code
|
||||
}
|
||||
|
||||
console.log(`${processName} process started.`);
|
||||
} else {
|
||||
console.log(`${processName} process is already running.`);
|
||||
|
||||
@@ -3,9 +3,9 @@ import replace from 'replace-in-file';
|
||||
|
||||
class Options {
|
||||
constructor(key, updatedValue) {
|
||||
this.files = "./.env";
|
||||
this.files = ".env";
|
||||
// A regex of the line containing the key in the config file
|
||||
this.from = new RegExp(`${key}="?(.+)"?`, "g");
|
||||
this.from = new RegExp(`${key}="?(.+)?"?`, "g");
|
||||
// Check to see if the value is a string and needs to be wrapped in double quotes
|
||||
if (Array(["string", "number"]).includes(typeof updatedValue)) this.to = `${key}="${updatedValue}",`;
|
||||
else this.to = `${key}=${updatedValue}`;
|
||||
@@ -16,10 +16,10 @@ class Options {
|
||||
* Wrapper to update the client's saved ID
|
||||
* @param updatedId The updated ID assigned to the node
|
||||
*/
|
||||
export function updateId (updatedId) {
|
||||
updateConfig('CLIENT_NUID', updatedId);
|
||||
export const updateId = async (updatedId) => {
|
||||
await updateConfig('CLIENT_NUID', updatedId);
|
||||
process.env.CLIENT_NUID = updatedId;
|
||||
console.log("Updated ID to: ", updatedId);
|
||||
console.log("Updated NUID to: ", updatedId);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -27,7 +27,7 @@ export function updateId (updatedId) {
|
||||
*
|
||||
* @param {Object} runningConfig Running config object
|
||||
* @param {Object} newConfigObject Object with what keys you wish to update (node object format, will be converted)
|
||||
* @param {number} newConfigObject.id The ID given to the node to update
|
||||
* @param {number} newConfigObject.nuid The ID given to the node to update
|
||||
* @param {string} newConfigObject.name The name of the node
|
||||
* @param {string} newConfigObject.ip The IP the server can contact the node on
|
||||
* @param {number} newConfigObject.port The port the server can contact the node on
|
||||
@@ -38,10 +38,10 @@ export function updateClientConfig (runningConfig, newConfigObject) {
|
||||
var updatedKeys = []
|
||||
const configKeys = Object.keys(newConfigObject);
|
||||
|
||||
if (configKeys.includes("id")) {
|
||||
if (runningConfig.id != newConfigObject.id) {
|
||||
this.updateId(newConfigObject.id);
|
||||
updatedKeys.push({ 'CLIENT_NUID': newConfigObject.id });
|
||||
if (configKeys.includes("nuid")) {
|
||||
if (runningConfig.nuid != newConfigObject.nuid) {
|
||||
this.updateId(newConfigObject.nuid);
|
||||
updatedKeys.push({ 'CLIENT_NUID': newConfigObject.nuid });
|
||||
}
|
||||
}
|
||||
if (configKeys.includes("name")) {
|
||||
@@ -88,6 +88,8 @@ export function updateClientConfig (runningConfig, newConfigObject) {
|
||||
export function updateConfig (key, value) {
|
||||
const options = new Options(key, value);
|
||||
|
||||
console.log("Options:", options);
|
||||
|
||||
updateConfigFile(options, (updatedFiles) => {
|
||||
// Do Something
|
||||
})
|
||||
|
||||
@@ -150,7 +150,7 @@ class trunkingConfig {
|
||||
}
|
||||
|
||||
class audioConfig {
|
||||
constructor({ module = "sockaudio.py", port = 23457, deviceName = "default" }) {
|
||||
constructor({ module = "sockaudio.py", port = 23456, deviceName = "default" }) {
|
||||
this.module = module;
|
||||
this.instances = [{
|
||||
"instance_name": "audio0",
|
||||
|
||||
1674
client/package-lock.json
generated
1674
client/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -4,7 +4,7 @@
|
||||
"description": "",
|
||||
"main": "client.js",
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
"test": "mocha --timeout 10000"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
@@ -15,13 +15,18 @@
|
||||
"convert-units": "^2.3.4",
|
||||
"discord.js": "^14.14.1",
|
||||
"dotenv": "^16.3.1",
|
||||
"express": "^4.19.2",
|
||||
"libsodium-wrappers": "^0.7.13",
|
||||
"prism-media": "^1.3.5",
|
||||
"replace-in-file": "^7.1.0",
|
||||
"simple-git": "^3.22.0",
|
||||
"socket.io": "^4.7.5",
|
||||
"socket.io-client": "^4.7.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"chai-http": "^4.4.0",
|
||||
"chai": "^5.1.0",
|
||||
"mocha": "^10.4.0",
|
||||
"typescript": "^5.3.3"
|
||||
}
|
||||
}
|
||||
12
client/post-update.sh
Normal file
12
client/post-update.sh
Normal file
@@ -0,0 +1,12 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Install client package updates
|
||||
npm install
|
||||
|
||||
# Install OP25 Updates
|
||||
#cd ./op25
|
||||
#bash rebuild.sh
|
||||
|
||||
# Check for PDAB updates
|
||||
cd ../discordAudioBot/pdab
|
||||
git pull
|
||||
140
client/setup.sh
140
client/setup.sh
@@ -16,23 +16,45 @@ if [[ ! -f "$(pwd)/package.json" ]]; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check to make sure the pi user exists
|
||||
if ! id "pi" &>/dev/null; then
|
||||
echo "Error: User pi does not exist."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
####------------------- Functions
|
||||
# Function to prompt user for input with a specific message and store the result in a variable
|
||||
prompt_user() {
|
||||
if [[ "$TEST_MODE" == "true" ]]; then
|
||||
echo "TESTING" # Use the pre-set value
|
||||
else
|
||||
read -p "$1: " input
|
||||
echo "$input"
|
||||
fi
|
||||
}
|
||||
|
||||
# Function to prompt user for capabilities options and store the result in a variable
|
||||
prompt_capabilities() {
|
||||
if [[ "$TEST_MODE" == "true" ]]; then
|
||||
echo "radio" # Use the pre-set value
|
||||
else
|
||||
default_capabilities="radio" # Default value
|
||||
read -p "Select CLIENT_CAPABILITIES (comma-separated, default: $default_capabilities): " capabilities
|
||||
capabilities="${capabilities:-$default_capabilities}" # Use default value if input is empty
|
||||
echo "$capabilities"
|
||||
fi
|
||||
}
|
||||
|
||||
# Function to prompt user for nearby systems details
|
||||
prompt_nearby_system() {
|
||||
if [[ "$TEST_MODE" == "true" ]]; then
|
||||
echo "\"TESTING-Node\": {
|
||||
\"frequencies\": [\"$(echo "155750000,154750000,156555550" | sed 's/,/","/g')\"],
|
||||
\"mode\": \"p25\",
|
||||
\"trunkFile\": \"testing_trunk.tsv\",
|
||||
\"whitelistFile\": \"testing_whitelist.tsv\"
|
||||
}," # Use the pre-set value
|
||||
else
|
||||
local system_name=""
|
||||
local frequencies=""
|
||||
local mode=""
|
||||
@@ -53,25 +75,94 @@ prompt_nearby_system() {
|
||||
\"mode\": \"$mode\",
|
||||
\"trunkFile\": \"$trunk_file\",
|
||||
\"whitelistFile\": \"$whitelist_file\"
|
||||
},"
|
||||
},"
|
||||
fi
|
||||
}
|
||||
|
||||
####------------------- Initial Prereqs and init steps
|
||||
# Check if test mode is enabled
|
||||
if [[ "$1" == "--test" ]]; then
|
||||
TEST_MODE="true"
|
||||
else
|
||||
TEST_MODE="false"
|
||||
fi
|
||||
|
||||
# Install Node Repo
|
||||
# Get the CPU architecture
|
||||
cpu_arch=$(uname -m)
|
||||
|
||||
# Print the CPU architecture for verification
|
||||
echo "Detected CPU Architecture: $cpu_arch"
|
||||
|
||||
# Check if the architecture is ARMv6
|
||||
if [[ "$cpu_arch" == "armv6"* ]]; then
|
||||
echo "----- CPU Architecture is ARMv6 or compatible. -----"
|
||||
echo "----- CPU Architectre is not compatible with dependencies of this project, please use a newer CPU architecture -----"
|
||||
exit
|
||||
fi
|
||||
|
||||
curl -fsSL https://deb.nodesource.com/setup_current.x | sudo -E bash -
|
||||
|
||||
# Update the system
|
||||
echo "Updating system..."
|
||||
apt update && apt upgrade -y
|
||||
apt update
|
||||
apt upgrade -y
|
||||
|
||||
# Install required dependencies: nodejs, ffmpeg
|
||||
# Install the necessary packages
|
||||
echo "Installing dependencies..."
|
||||
apt install -y nodejs ffmpeg git
|
||||
apt install -y \
|
||||
nodejs \
|
||||
libasound-dev \
|
||||
portaudio19-dev \
|
||||
libportaudio2 \
|
||||
libpulse-dev \
|
||||
pulseaudio \
|
||||
apulse \
|
||||
git \
|
||||
ffmpeg \
|
||||
python3 \
|
||||
python3-pip
|
||||
|
||||
echo "Setting up Pulse Audio"
|
||||
|
||||
# Ensure pulse audio is running as system so the service can see the audio device
|
||||
systemctl --global disable pulseaudio.service pulseaudio.socket
|
||||
|
||||
# Update the PulseAudio config to disable autospawning
|
||||
sed -i 's/autospawn = .*$/autospawn = no/' /etc/pulse/client.conf
|
||||
|
||||
# Add the system PulseAudio service
|
||||
echo "[Unit]
|
||||
Description=PulseAudio system server
|
||||
|
||||
[Service]
|
||||
Type=notify
|
||||
ExecStart=pulseaudio --daemonize=no --system --realtime --log-target=journal
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target" >> /etc/systemd/system/PulseAudio.service
|
||||
|
||||
# Add the root user to the pulse-access group
|
||||
usermod -aG pulse-access root
|
||||
usermod -aG pulse-access pi
|
||||
|
||||
# Enable the PulseAudio service
|
||||
systemctl enable PulseAudio.service
|
||||
|
||||
####------------------- Install and setup node
|
||||
# Run npm install to install dependencies listed in package.json
|
||||
echo "Installing npm dependencies..."
|
||||
npm install
|
||||
|
||||
# Get rid of PEP 668
|
||||
rm -rf /usr/lib/python3.11/EMTERNALL-MANAGED # Not sure if this was an attrocious fat finger or if this is needed, doesn't throw an error, so...
|
||||
rm -rf /usr/lib/python3.11/EXTERNALLY-MANAGED
|
||||
|
||||
# Getting the Python DAB
|
||||
echo "Installing PDAB and Dependencies"
|
||||
git clone -b DRBv3 https://git.vpn.cusano.net/logan/Python-Discord-Audio-Bot.git ./discordAudioBot/pdab
|
||||
pip3 install -r ./discordAudioBot/pdab/requirements.txt
|
||||
|
||||
# Generate .env file
|
||||
echo "Creating the config .env file..."
|
||||
echo "# Client Config" > .env
|
||||
echo "CLIENT_NUID=0" >> .env
|
||||
client_name=$(prompt_user "Enter the name for this node")
|
||||
@@ -97,6 +188,11 @@ echo "OP25_FULL_PATH=$op25_full_path" >> .env
|
||||
echo "" >> .env
|
||||
echo "# Core config, DO NOT TOUCH UNLESS YOU KNOW WHAT YOU ARE DOING" >> .env
|
||||
echo "CONFIG_PATH=./config/radioPresets.json" >> .env
|
||||
runuser -l pi -c 'python3 ./discordAudioBot/pdab/getDevices.py'
|
||||
audio_device_id=$(prompt_user "Enter the ID of the 'input' audio device you would like to use (most often 'default')")
|
||||
echo "AUDIO_DEVICE_ID=$audio_device_id" >> .env
|
||||
echo "PDAB_PORT=3110" >> .env
|
||||
echo "NODE_ENV=production" >> .env
|
||||
|
||||
echo ".env file generated successfully."
|
||||
|
||||
@@ -141,16 +237,17 @@ echo "$service_content" > /etc/systemd/system/discord-radio-bot.service
|
||||
|
||||
# Reload systemd daemon
|
||||
systemctl daemon-reload
|
||||
systemctl stop discord-radio-bot.service
|
||||
systemctl enable discord-radio-bot.service
|
||||
|
||||
echo "\n\n\t\tDiscord Client Node install completed!\n\n"
|
||||
|
||||
####------------------- OP25 Installation
|
||||
# Clone OP25 from the git repository
|
||||
echo "Cloning OP25 from the git repository..."
|
||||
git clone https://github.com/boatbod/op25.git
|
||||
git clone -b gr310 https://github.com/boatbod/op25.git
|
||||
|
||||
# Navigate to the OP25 directory
|
||||
ogPwd=$(pwd)
|
||||
cd op25
|
||||
|
||||
# Edit the startup script to use the active.cfg.json config file generated by the app
|
||||
@@ -183,10 +280,35 @@ echo "$service_content" > /etc/systemd/system/op25-multi_rx.service
|
||||
|
||||
# Reload systemd daemon
|
||||
systemctl daemon-reload
|
||||
systemctl stop op25-multi_rx.service
|
||||
|
||||
# Install OP25 using the provided installation script
|
||||
echo "Installing OP25..."
|
||||
./install.sh
|
||||
|
||||
echo "\n\n\t\tOP25 installation completed!\n\n"
|
||||
|
||||
# Setting permissions on the directories created
|
||||
cd $ogPwd
|
||||
chown -R 1000:1000 ./*
|
||||
chown 1000:1000 .env
|
||||
echo "Permissions set on the client directory!"
|
||||
|
||||
echo "\n\n\t\tNode installation Complete!"
|
||||
|
||||
# Prompt the user for reboot confirmation
|
||||
read -p "This script has installed all required components for the DRB client. Are you okay with rebooting? If not, you will have to reboot later before running the applications to finish the installation. (Reboot?: y/n): " confirm
|
||||
|
||||
# Convert user input to lowercase for case-insensitive comparison
|
||||
confirm="${confirm,,}"
|
||||
|
||||
#echo "To configure the app, please go to http://$nodeIP:$nodePort" # TODO - uncomment when webapp is built
|
||||
echo "Thank you for joining the network!"
|
||||
|
||||
if [[ "$confirm" == "y" && "$confirm" == "yes" ]]; then
|
||||
# Prompt user to press any key before rebooting
|
||||
read -rsp $'System will now reboot, press any key to continue or Ctrl+C to cancel...\n' -n1 key
|
||||
echo "Rebooting..."
|
||||
reboot
|
||||
else
|
||||
echo "Please restart your device to complete the installation"
|
||||
fi
|
||||
194
client/test/pdabHandler.test.js
Normal file
194
client/test/pdabHandler.test.js
Normal file
@@ -0,0 +1,194 @@
|
||||
// Import necessary modules for testing
|
||||
import { expect } from 'chai'
|
||||
import io from 'socket.io-client';
|
||||
const ioClient = io;
|
||||
|
||||
import dotenv from 'dotenv';
|
||||
dotenv.config()
|
||||
|
||||
import { initDiscordBotClient, connectToChannel, leaveVoiceChannel, checkIfConnectedToVC, requestDiscordUsername, requestDiscordID, requestDiscordClientClose, closePdabSocketServer } from '../discordAudioBot/pdabHandler.mjs';
|
||||
|
||||
let socket;
|
||||
|
||||
before(async done => {
|
||||
// Any setup needed before tests
|
||||
done();
|
||||
});
|
||||
|
||||
after(async () => {
|
||||
await closePdabSocketServer();
|
||||
})
|
||||
|
||||
describe('Socket Server Tests', done => {
|
||||
after(async () => {
|
||||
// Any teardown needed after tests
|
||||
try {
|
||||
await socket.close();
|
||||
}
|
||||
catch {
|
||||
console.log("Socket already closed");
|
||||
}
|
||||
console.log('Closing PDAB Socker Server');
|
||||
});
|
||||
|
||||
it('Should open a socket server and callback when the client is connected and ready', done => {
|
||||
const clientId = process.env.TEST_CLIENT_TOKEN;
|
||||
|
||||
const callback = () => {
|
||||
done();
|
||||
};
|
||||
|
||||
initDiscordBotClient(clientId, callback, false);
|
||||
|
||||
socket = ioClient.connect(`http://localhost:${process.env.PDAB_PORT}`);
|
||||
|
||||
socket.on('connect', () => {
|
||||
socket.emit('discord_ready')
|
||||
});
|
||||
});
|
||||
|
||||
it('Should emit command for and return status from join server', async () => {
|
||||
socket.on('join_server', (data, callback) => {
|
||||
console.log('Join data from server:', data);
|
||||
expect(data).to.deep.equal({ channelId: process.env.TEST_CHANNEL_ID });
|
||||
callback(true, true);
|
||||
})
|
||||
|
||||
// Simulate emitting 'join_server' event
|
||||
const status = await connectToChannel(process.env.TEST_CHANNEL_ID);
|
||||
|
||||
// Check the server sent the expected info
|
||||
|
||||
// Assert the status returned from the server
|
||||
expect(status).to.be.true;
|
||||
});
|
||||
|
||||
it('Should emit command for and return open status for leave server', async () => {
|
||||
socket.on('leave_server', (data, callback) => {
|
||||
console.log('Leave data from server:', data);
|
||||
expect(data).to.deep.equal({ guildId: process.env.TEST_GUILD_ID });
|
||||
callback(false, false);
|
||||
});
|
||||
|
||||
// Simulate emitting 'leave_server' event
|
||||
const openStatus = await leaveVoiceChannel(process.env.TEST_GUILD_ID);
|
||||
|
||||
// Assert the open status returned from the server
|
||||
expect(openStatus).to.be.false;
|
||||
});
|
||||
|
||||
it('Should emit command for and return status if connected to voice channel', async () => {
|
||||
socket.on('check_discord_vc_connected', (data, callback) => {
|
||||
console.log('Client Check data:', data);
|
||||
expect(data).to.deep.equal({ guildId: process.env.TEST_GUILD_ID });
|
||||
callback(true, true);
|
||||
});
|
||||
|
||||
// Simulate emitting 'check_discord_vc_connected' event
|
||||
const isConnected = await checkIfConnectedToVC(process.env.TEST_GUILD_ID);
|
||||
|
||||
// Assert the connection status returned from the server
|
||||
expect(isConnected).to.be.true;
|
||||
});
|
||||
|
||||
it('Should emit command for and return username for request discord username', async () => {
|
||||
socket.on('request_discord_username', (data, callback) => {
|
||||
console.log('Username Check data:', data);
|
||||
expect(data).to.deep.equal({ guildId: process.env.TEST_GUILD_ID });
|
||||
callback(process.env.EXPECTED_USERNAME);
|
||||
});
|
||||
// Simulate emitting 'request_discord_username' event
|
||||
const username = await requestDiscordUsername(process.env.TEST_GUILD_ID);
|
||||
|
||||
// Assert the username returned from the server
|
||||
expect(username).to.equal(process.env.EXPECTED_USERNAME);
|
||||
});
|
||||
|
||||
it('Should emit command for and return discord client ID', async () => {
|
||||
socket.on('request_discord_id', (callback) => {
|
||||
callback(process.env.EXPECTED_CLIENT_ID);
|
||||
});
|
||||
// Simulate emitting 'request_discord_id' event
|
||||
const clientId = await requestDiscordID();
|
||||
|
||||
// Assert the client ID returned from the server
|
||||
expect(clientId).to.equal(process.env.EXPECTED_CLIENT_ID);
|
||||
});
|
||||
|
||||
it('Should emit command for discord client to close', done => {
|
||||
socket.on('request_client_close', async () => {
|
||||
await socket.close();
|
||||
done()
|
||||
})
|
||||
// Simulate emitting 'request_client_close' event
|
||||
requestDiscordClientClose();
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('Socket Client & Python IPC Tests', done => {
|
||||
it('Should open a socket server and callback when the client is connected and ready', done => {
|
||||
let clientConnected = false;
|
||||
const clientId = process.env.TEST_CLIENT_TOKEN;
|
||||
|
||||
const callback = () => {
|
||||
clientConnected = true;
|
||||
expect(clientConnected).to.be.true;
|
||||
|
||||
done();
|
||||
};
|
||||
|
||||
initDiscordBotClient(clientId, callback);
|
||||
|
||||
});
|
||||
|
||||
it('Should emit command for and return status from join server', async () => {
|
||||
// Simulate emitting 'join_server' event
|
||||
const status = await connectToChannel(process.env.TEST_CHANNEL_ID);
|
||||
|
||||
// Check the server sent the expected info
|
||||
|
||||
// Assert the status returned from the server
|
||||
expect(status).to.be.true;
|
||||
});
|
||||
|
||||
it('Should emit command for and return status if connected to voice channel', async () => {
|
||||
// Simulate emitting 'check_discord_vc_connected' event
|
||||
const isConnected = await checkIfConnectedToVC(process.env.TEST_GUILD_ID);
|
||||
|
||||
// Assert the connection status returned from the server
|
||||
expect(isConnected).to.be.true;
|
||||
});
|
||||
|
||||
it('Should emit command for and return username for request discord username', async () => {
|
||||
// Simulate emitting 'request_discord_username' event
|
||||
const username = await requestDiscordUsername(process.env.TEST_GUILD_ID);
|
||||
|
||||
// Assert the username returned from the server
|
||||
expect(username).to.equal(process.env.EXPECTED_USERNAME);
|
||||
});
|
||||
|
||||
it('Should emit command for and return discord client ID', async () => {
|
||||
// Simulate emitting 'request_discord_id' event
|
||||
const clientId = await requestDiscordID();
|
||||
|
||||
console.log("type of client id", typeof clientId, typeof process.env.EXPECTED_CLIENT_ID);
|
||||
|
||||
// Assert the client ID returned from the server
|
||||
expect(clientId).to.equal(Number(process.env.EXPECTED_CLIENT_ID));
|
||||
});
|
||||
|
||||
it('Should emit command for and return open status for leave server', async () => {
|
||||
// Simulate emitting 'leave_server' event
|
||||
const openStatus = await leaveVoiceChannel(process.env.TEST_GUILD_ID);
|
||||
|
||||
// Assert the open status returned from the server
|
||||
expect(openStatus).to.be.false;
|
||||
});
|
||||
|
||||
it('Should emit command for discord client to close', async () => {
|
||||
// Simulate emitting 'request_client_close' event
|
||||
await requestDiscordClientClose();
|
||||
});
|
||||
|
||||
});
|
||||
40
client/test/pdabWrappers.test.js.bak
Normal file
40
client/test/pdabWrappers.test.js.bak
Normal file
@@ -0,0 +1,40 @@
|
||||
import { use } from 'chai';
|
||||
import chaiHttp from 'chai-http';
|
||||
const chai = use(chaiHttp)
|
||||
chai.should();
|
||||
|
||||
import dotenv from 'dotenv';
|
||||
dotenv.config()
|
||||
|
||||
import { joinDiscordVC, leaveDiscordVC } from '../discordAudioBot/pdabWrappers.mjs'
|
||||
|
||||
before(async () => {
|
||||
// Any setup needed before tests
|
||||
});
|
||||
|
||||
after(async () => {
|
||||
// Any teardown needed after tests
|
||||
});
|
||||
|
||||
describe('PDAB Wrapper Tests', () => {
|
||||
it('Should open the discord bot, and join the first server when requested', async () => {
|
||||
// Test case
|
||||
const joinData = {
|
||||
channelID: process.env.TEST_CHANNEL_ID,
|
||||
clientID: process.env.TEST_CLIENT_TOKEN,
|
||||
system: process.env.TEST_SYSTEM,
|
||||
};
|
||||
const connection = await joinDiscordVC(joinData);
|
||||
console.log("Connection:", connection);
|
||||
});
|
||||
|
||||
it('Should open OP25', async () => {
|
||||
const res = await chai.request('http://localhost:8081').get('/');
|
||||
expect(res).to.have.status(200); // Assuming 200 is the expected status code
|
||||
// Add more assertions if needed
|
||||
})
|
||||
|
||||
it("Should disconnect from the discord server", async () => {
|
||||
await leaveDiscordVC(process.env.TEST_GUILD_ID);
|
||||
})
|
||||
});
|
||||
21
server/Dockerfile
Normal file
21
server/Dockerfile
Normal file
@@ -0,0 +1,21 @@
|
||||
# Use the official Node.js image as the base image
|
||||
FROM node:20
|
||||
|
||||
# Set the working directory inside the container
|
||||
WORKDIR /server
|
||||
|
||||
# Copy package.json and package-lock.json (if available) to the working directory
|
||||
COPY package*.json ./
|
||||
|
||||
# Install dependencies
|
||||
RUN npm install -g node-gyp
|
||||
RUN npm install
|
||||
|
||||
# Copy the rest of the application code to the working directory
|
||||
COPY . .
|
||||
|
||||
# Expose the port on which your Node.js application will run
|
||||
EXPOSE 3000
|
||||
|
||||
# Command to run the Node.js application
|
||||
CMD ["node", "."]
|
||||
7
server/addons/example/config.json
Normal file
7
server/addons/example/config.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"name": "Addon 1",
|
||||
"enabled": false,
|
||||
"options": {
|
||||
"eventName": "connection"
|
||||
}
|
||||
}
|
||||
17
server/addons/example/index.js
Normal file
17
server/addons/example/index.js
Normal file
@@ -0,0 +1,17 @@
|
||||
// addons/addon1/index.js
|
||||
|
||||
// Function called by the main application to initialize the addon
|
||||
export function initialize(nodeIo, config) {
|
||||
console.log(`Initializing ${config.name}`);
|
||||
|
||||
// Call other functions within the addon module
|
||||
registerSocketEvents(nodeIo, config);
|
||||
// Call additional initialization functions if needed
|
||||
}
|
||||
|
||||
// Function to register Socket.IO event handlers
|
||||
function registerSocketEvents(nodeIo, config) {
|
||||
nodeIo.on(config.options.eventName, (data) => {
|
||||
console.log(`Received event "${config.options.eventName}" from client:`, data);
|
||||
});
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
import { SlashCommandBuilder, ActionRowBuilder, ButtonBuilder, ButtonStyle } from 'discord.js';
|
||||
import { requestNodeJoinSystem, checkIfNodeIsConnectedToVC, checkIfNodeHasOpenDiscordClient, getNodeCurrentListeningSystem } from '../../modules/socketServerWrappers.mjs';
|
||||
import { getSystemsByNuid, getAllSystems, getSystemByName } from '../../modules/mongoSystemsWrappers.mjs';
|
||||
import { getAvailableTokensInGuild } from '../modules/wrappers.mjs';
|
||||
|
||||
// Exporting data property
|
||||
export const data = new SlashCommandBuilder()
|
||||
@@ -40,9 +41,10 @@ export async function autocomplete(nodeIo, interaction) {
|
||||
*/
|
||||
export async function execute(nodeIo, interaction) {
|
||||
// Check if the user is in a VC
|
||||
if (!interaction.member.voice.channel) { return await interaction.reply({ content: `<@${interaction.member.id}>, you need to enter a voice channel before use the command`, ephemeral: true }) }
|
||||
if (!interaction.member.voice.channel) { return await interaction.editReply({ content: `<@${interaction.member.id}>, you need to enter a voice channel before you use this command`, ephemeral: true }) }
|
||||
// Grab the channel if the user is connected to VC
|
||||
const channelToJoin = interaction.member.voice.channel;
|
||||
console.log(`The user '${interaction.member.id}' is in the voice channel '${channelToJoin}'`);
|
||||
|
||||
// Get the selected system option from the command interaction
|
||||
const selectedSystem = interaction.options.getString('system');
|
||||
@@ -54,10 +56,20 @@ export async function execute(nodeIo, interaction) {
|
||||
// Function wrapper to request the selected/only node to join the selected system
|
||||
const joinSelectedNode = async (selectedNodeSocketId) => {
|
||||
const openSocket = await nodeIo.sockets.sockets.get(selectedNodeSocketId);
|
||||
console.log("Joining selected open socket:", selectedNodeSocketId, system.name, channelToJoin.id, openSocket.node.name);
|
||||
// Get the open ID for this connection\
|
||||
const ss = await getAvailableTokensInGuild(nodeIo, interaction.guild.id);
|
||||
console.log("Available discord tokens: ", discordTokens);
|
||||
|
||||
if (discordTokens.length >= 1) {
|
||||
// TODO - Implement a method to have preferred tokens (bot users) for specific systems
|
||||
console.log("Joining selected open socket:", selectedNodeSocketId, system.name, channelToJoin.id, openSocket.node.name, discordTokens[0].token);
|
||||
|
||||
// Ask the node to join the selected channel and system
|
||||
await requestNodeJoinSystem(openSocket, system.name, channelToJoin.id);
|
||||
await requestNodeJoinSystem(openSocket, system.name, channelToJoin.id, discordTokens[0].token);
|
||||
}
|
||||
else {
|
||||
return await interaction.editReply({ content: `<@${interaction.member.id}>, there are no free bots. Free up or create a new bot ID (discord app) to listen to this system.`, ephemeral: true })
|
||||
}
|
||||
}
|
||||
|
||||
// Get all open socket nodes
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { SlashCommandBuilder } from 'discord.js';
|
||||
import { checkIfNodeIsConnectedToVC, requestBotLeaveServer, getNodeDiscordUsername, getSocketIdByNuid } from '../../modules/socketServerWrappers.mjs';
|
||||
import { requestBotLeaveServer, getSocketIdByNuid } from '../../modules/socketServerWrappers.mjs';
|
||||
import { checkOnlineBotsInGuild } from '../modules/wrappers.mjs'
|
||||
|
||||
// Exporting data property
|
||||
export const data = new SlashCommandBuilder()
|
||||
@@ -22,23 +23,11 @@ export const deferInitialReply = true;
|
||||
*/
|
||||
export async function autocomplete(nodeIo, interaction) {
|
||||
const focusedValue = interaction.options.getFocused();
|
||||
const choices = [];
|
||||
const choices = (await checkOnlineBotsInGuild(nodeIo, interaction.guild.id));
|
||||
|
||||
const openSockets = [...await nodeIo.allSockets()];
|
||||
await Promise.all(openSockets.map(async openSocket => {
|
||||
openSocket = await nodeIo.sockets.sockets.get(openSocket);
|
||||
const connected = await checkIfNodeIsConnectedToVC(nodeIo, interaction.guild.id, openSocket.node.nuid);
|
||||
console.log("Connected:", connected);
|
||||
if (connected) {
|
||||
const username = await getNodeDiscordUsername(openSocket, interaction.guild.id);
|
||||
choices.push({
|
||||
name: username,
|
||||
value: openSocket.node.nuid
|
||||
});
|
||||
}
|
||||
}));
|
||||
console.log(choices);
|
||||
|
||||
const filtered = choices.filter(choice => choice.name.startsWith(focusedValue));
|
||||
const filtered = choices.filter(choice => choice.name.startsWith(focusedValue)).map(choice => choice = {name: choice.name, value: choice.nuid});
|
||||
|
||||
console.log(focusedValue, choices, filtered);
|
||||
|
||||
|
||||
@@ -74,7 +74,7 @@ export function addEnabledEventListeners(serverClient, _eventsPath = "./events")
|
||||
}
|
||||
|
||||
// The discord client
|
||||
export const serverClient = new Client({ intents: [GatewayIntentBits.Guilds] });
|
||||
export const serverClient = new Client({ intents: [GatewayIntentBits.Guilds, GatewayIntentBits.GuildVoiceStates] });
|
||||
|
||||
// Run when the bot is ready
|
||||
serverClient.on('ready', async () => {
|
||||
|
||||
48
server/discordBot/modules/wrappers.mjs
Normal file
48
server/discordBot/modules/wrappers.mjs
Normal file
@@ -0,0 +1,48 @@
|
||||
import { checkIfNodeIsConnectedToVC, getNodeDiscordID, getNodeDiscordUsername } from '../../modules/socketServerWrappers.mjs';
|
||||
import { getAllDiscordIDs } from '../../modules/mongoDiscordIDWrappers.mjs'
|
||||
|
||||
|
||||
export const checkOnlineBotsInGuild = async (nodeIo, guildId) => {
|
||||
let onlineBots = [];
|
||||
const openSockets = [...await nodeIo.allSockets()];
|
||||
await Promise.all(openSockets.map(async openSocket => {
|
||||
openSocket = await nodeIo.sockets.sockets.get(openSocket);
|
||||
const connected = await checkIfNodeIsConnectedToVC(nodeIo, guildId, openSocket.node.nuid);
|
||||
console.log("Connected:", connected);
|
||||
if (connected) {
|
||||
const username = await getNodeDiscordUsername(openSocket, guildId);
|
||||
const discordID = await getNodeDiscordID(openSocket);
|
||||
onlineBots.push({
|
||||
name: username,
|
||||
discord_id: discordID,
|
||||
nuid: openSocket.node.nuid
|
||||
});
|
||||
}
|
||||
}));
|
||||
|
||||
return onlineBots;
|
||||
}
|
||||
|
||||
|
||||
export const getAvailableTokensInGuild = async (nodeIo, guildId) => {
|
||||
try {
|
||||
// Execute both asynchronous functions concurrently
|
||||
const [discordIDs, onlineBots] = await Promise.all([
|
||||
getAllDiscordIDs(), // Fetch all Discord IDs
|
||||
checkOnlineBotsInGuild(nodeIo, guildId) // Check online bots in the guild
|
||||
]);
|
||||
|
||||
// Use the results of both promises here
|
||||
console.log("Available Discord IDs:", discordIDs);
|
||||
console.log("Online bots in the guild:", onlineBots);
|
||||
|
||||
// Filter any discordIDs that are not active
|
||||
const availableDiscordIDs = discordIDs.filter(discordID => discordID.active == true).filter(discordID => !onlineBots.some(bot => Number(bot.discord_id) == discordID.discord_id));
|
||||
|
||||
// Return the unavailable discordIDs
|
||||
return availableDiscordIDs;
|
||||
} catch (error) {
|
||||
console.error('Error getting available tokens in guild:', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
25
server/makefile
Normal file
25
server/makefile
Normal file
@@ -0,0 +1,25 @@
|
||||
# Define variables
|
||||
DOCKER_IMAGE_NAME := drb-server
|
||||
|
||||
# Define targets and rules
|
||||
.PHONY: clean build run
|
||||
|
||||
clean:
|
||||
@echo "Cleaning existing Docker images, containers, and builds..."
|
||||
docker stop drb || true
|
||||
docker rm drb || true
|
||||
docker rmi $(DOCKER_IMAGE_NAME) || true
|
||||
|
||||
build:
|
||||
@echo "Building Docker image..."
|
||||
docker build -t $(DOCKER_IMAGE_NAME) .
|
||||
|
||||
run:
|
||||
@echo "Running Docker container..."
|
||||
docker run -d -e NODE_ENV=${NODE_ENV} \
|
||||
-e SERVER_PORT=${SERVER_PORT} \
|
||||
-e MONGO_URL=${MONGO_URL} \
|
||||
-e DISCORD_TOKEN=${DISCORD_TOKEN} \
|
||||
-p ${SERVER_PORT}:${SERVER_PORT} \
|
||||
--name=drb \
|
||||
$(DOCKER_IMAGE_NAME)
|
||||
31
server/modules/addonManager.mjs
Normal file
31
server/modules/addonManager.mjs
Normal file
@@ -0,0 +1,31 @@
|
||||
import { fileURLToPath } from 'url';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
|
||||
// Function to load addons from the addons directory
|
||||
export const loadAddons = async (nodeIo) => {
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
|
||||
const addonsDir = path.join(__dirname, '../addons');
|
||||
|
||||
// Read the directory containing addon modules
|
||||
const addonDirectories = await fs.readdirSync(addonsDir, { withFileTypes: true });
|
||||
|
||||
addonDirectories.forEach(addonDir => {
|
||||
if (addonDir.isDirectory()) {
|
||||
const addonConfigPath = path.join(addonsDir, addonDir.name, 'config.json');
|
||||
if (fs.existsSync(addonConfigPath)) {
|
||||
const addonConfig = JSON.parse(fs.readFileSync(addonConfigPath, 'utf-8'));
|
||||
if (addonConfig.enabled) {
|
||||
const addonIndexPath = path.join(addonsDir, addonDir.name, 'index.js');
|
||||
import(`file://${addonIndexPath}`).then(addonModule => {
|
||||
console.log("Loading addon: ", addonModule);
|
||||
addonModule.initialize(nodeIo, addonConfig);
|
||||
console.log(`Addon ${addonConfig.name} loaded.`);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
90
server/modules/mongoDiscordIDWrappers.mjs
Normal file
90
server/modules/mongoDiscordIDWrappers.mjs
Normal file
@@ -0,0 +1,90 @@
|
||||
import { insertDocument, getDocuments, connectToDatabase } from "./mongoHandler.mjs";
|
||||
|
||||
const collectionName = 'discord-ids';
|
||||
|
||||
// Wrapper for inserting a Discord ID
|
||||
export const createDiscordID = async (discordID) => {
|
||||
try {
|
||||
const insertedId = await insertDocument(collectionName, discordID);
|
||||
return insertedId;
|
||||
} catch (error) {
|
||||
console.error('Error creating Discord ID:', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
// Wrapper for retrieving all Discord IDs
|
||||
export const getAllDiscordIDs = async () => {
|
||||
try {
|
||||
const discordIDs = await getDocuments(collectionName);
|
||||
return discordIDs;
|
||||
} catch (error) {
|
||||
console.error('Error getting all Discord IDs:', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
// Wrapper for retrieving a Discord ID by name or discord_id
|
||||
export const getDiscordID = async (identifier) => {
|
||||
const db = await connectToDatabase();
|
||||
try {
|
||||
const collection = db.db().collection(collectionName);
|
||||
const discordID = await collection.findOne({
|
||||
$or: [
|
||||
{ name: identifier },
|
||||
{ discord_id: identifier }
|
||||
]
|
||||
});
|
||||
return discordID;
|
||||
} catch (error) {
|
||||
console.error('Error getting Discord ID:', error);
|
||||
throw error;
|
||||
} finally {
|
||||
// Close the connection
|
||||
await db.close();
|
||||
}
|
||||
};
|
||||
|
||||
// Wrapper for updating a Discord ID by name or discord_id
|
||||
export const updateDiscordID = async (identifier, updatedFields) => {
|
||||
const db = await connectToDatabase();
|
||||
try {
|
||||
const collection = db.db().collection(collectionName);
|
||||
const result = await collection.updateOne({
|
||||
$or: [
|
||||
{ name: identifier },
|
||||
{ discord_id: identifier }
|
||||
]
|
||||
}, { $set: updatedFields });
|
||||
console.log('Discord ID updated:', result.modifiedCount);
|
||||
return result.modifiedCount;
|
||||
} catch (error) {
|
||||
console.error('Error updating Discord ID:', error);
|
||||
throw error;
|
||||
} finally {
|
||||
// Close the connection
|
||||
await db.close();
|
||||
}
|
||||
};
|
||||
|
||||
// Wrapper for deleting a Discord ID by name or discord_id
|
||||
export const deleteDiscordID = async (identifier) => {
|
||||
const db = await connectToDatabase();
|
||||
try {
|
||||
const collection = db.db().collection(collectionName);
|
||||
const result = await collection.deleteOne({
|
||||
$or: [
|
||||
{ name: identifier },
|
||||
{ discord_id: identifier }
|
||||
]
|
||||
});
|
||||
console.log('Discord ID deleted:', result.deletedCount);
|
||||
return result.deletedCount;
|
||||
} catch (error) {
|
||||
console.error('Error deleting Discord ID:', error);
|
||||
throw error;
|
||||
} finally {
|
||||
// Close the connection
|
||||
await db.close();
|
||||
}
|
||||
};
|
||||
@@ -17,13 +17,19 @@ app.get('/', (req, res) => {
|
||||
nodeIo.on('connection', (socket) => {
|
||||
console.log('a user connected', socket.id);
|
||||
|
||||
socket.on('node-login', (data) => {
|
||||
nodeLoginWrapper(data, socket);
|
||||
socket.on('node-login', async (data) => {
|
||||
await nodeLoginWrapper(data, socket);
|
||||
await socket.emit('node-login-successful');
|
||||
})
|
||||
|
||||
socket.on('node-update', (data) => {
|
||||
nodeUpdateWrapper(data.node);
|
||||
nearbySystemsUpdateWraper(data.node.nuid, data.nearbySystems)
|
||||
socket.on('node-update', async (data) => {
|
||||
let tempPromises = [];
|
||||
tempPromises.push(nodeUpdateWrapper(data.node));
|
||||
tempPromises.push(nearbySystemsUpdateWraper(data.node.nuid, data.nearbySystems));
|
||||
|
||||
await Promise.all(tempPromises);
|
||||
|
||||
await socket.emit('node-update-successful')
|
||||
})
|
||||
|
||||
socket.on('disconnect', () => {
|
||||
@@ -31,8 +37,3 @@ nodeIo.on('connection', (socket) => {
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
// Startup the node server
|
||||
server.listen(3000, () => {
|
||||
console.log('server running at http://localhost:3000');
|
||||
});
|
||||
@@ -28,15 +28,17 @@ export const nodeLoginWrapper = async (data, socket) => {
|
||||
console.log("After grabbing", node);
|
||||
if (!node) {
|
||||
const insertedId = await createNode(data);
|
||||
node = await getNodeByNuid(data.nuid);
|
||||
console.log("Added new node to the database:", insertedId);
|
||||
} else {
|
||||
// Check for updates
|
||||
const updatedNode = await updateNodeByNuid(data.nuid, data)
|
||||
console.log("Updated node:", updatedNode);
|
||||
}
|
||||
// Check for updates if so
|
||||
// Check for System updates
|
||||
|
||||
node = await getNodeByNuid(data.nuid);
|
||||
|
||||
// Add the socket/node connection
|
||||
socket.node = node;
|
||||
//socket.id = node.nuid;
|
||||
|
||||
return;
|
||||
}
|
||||
@@ -257,16 +259,29 @@ export const getNodeDiscordUsername = async (socket, guildId) => {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the discord ID from a given socket
|
||||
* @param {any} socket The socket object of the node to check the ID of
|
||||
* @returns {string} The ID of the bot
|
||||
*/
|
||||
export const getNodeDiscordID = async (socket) => {
|
||||
return await new Promise((res) => {
|
||||
socket.emit('node-get-discord-id', (discordID) => {
|
||||
res(discordID);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Request a given socket node to join a given voice channel
|
||||
* @param {any} socket The socket object of the node the request should be sent to
|
||||
* @param {any} systemName The system preset name that we would like to listen to
|
||||
* @param {string} discordChanelId The Discord channel ID to join the listening bot to
|
||||
*/
|
||||
export const requestNodeJoinSystem = async (socket, systemName, discordChanelId) => {
|
||||
// Check for open client IDs
|
||||
export const requestNodeJoinSystem = async (socket, systemName, discordChanelId, discordToken = "MTE5NjAwNTM2ODYzNjExMjk3Nw.GuCMXg.24iNNofNNumq46FIj68zMe9RmQgugAgfrvelEA") => {
|
||||
// Join the system
|
||||
const joinData = {
|
||||
'clientID': "MTE5NjAwNTM2ODYzNjExMjk3Nw.GuCMXg.24iNNofNNumq46FIj68zMe9RmQgugAgfrvelEA",
|
||||
'clientID': discordToken,
|
||||
'channelID': discordChanelId,
|
||||
'system': systemName
|
||||
}
|
||||
|
||||
980
server/package-lock.json
generated
980
server/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -4,12 +4,17 @@
|
||||
"description": "",
|
||||
"main": "server.js",
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1",
|
||||
"test": "mocha --timeout 5000",
|
||||
"start": "node server.js"
|
||||
},
|
||||
"author": "Logan Cusano",
|
||||
"license": "ISC",
|
||||
"type": "module",
|
||||
"devDependencies": {
|
||||
"chai": "^5.1.0",
|
||||
"mocha": "^10.4.0",
|
||||
"socket.io-client": "^4.7.5"
|
||||
},
|
||||
"dependencies": {
|
||||
"discord.js": "^14.14.1",
|
||||
"dotenv": "^16.3.1",
|
||||
|
||||
@@ -1,9 +1,18 @@
|
||||
import { nodeIo, app, server } from './modules/socketServer.mjs';
|
||||
import { loadAddons } from './modules/addonManager.mjs';
|
||||
import { serverClient, addEnabledEventListeners } from './discordBot/discordBot.mjs';
|
||||
|
||||
import dotenv from 'dotenv';
|
||||
dotenv.config()
|
||||
|
||||
// Startup the node server
|
||||
server.listen(process.env.SERVER_PORT || 3000, () => {
|
||||
console.log(`server running at http://localhost:${process.env.SERVER_PORT}`);
|
||||
});
|
||||
|
||||
// Add objects to the others
|
||||
serverClient.nodeIo = nodeIo;
|
||||
nodeIo.serverClient = serverClient;
|
||||
|
||||
// Load the addons
|
||||
loadAddons(nodeIo);
|
||||
290
server/test/socketServerWrappers.test.js
Normal file
290
server/test/socketServerWrappers.test.js
Normal file
@@ -0,0 +1,290 @@
|
||||
// Import necessary modules for testing
|
||||
import { expect } from 'chai';
|
||||
import ioClient from 'socket.io-client';
|
||||
import { deleteNodeByNuid, getNodeByNuid } from '../modules/mongoNodesWrappers.mjs';
|
||||
import { deleteSystemByName, getSystemByName } from '../modules/mongoSystemsWrappers.mjs';
|
||||
import { nodeIo } from '../modules/socketServer.mjs';
|
||||
|
||||
import dotenv from 'dotenv';
|
||||
dotenv.config()
|
||||
|
||||
process.env.SERVER_PORT = 6000
|
||||
|
||||
// Define necessary variables for testing, such as mocked database connections or socket instances
|
||||
const localNodeConfig = {
|
||||
serverIp: 'localhost',
|
||||
serverPort: process.env.SERVER_PORT,
|
||||
node: {
|
||||
nuid: "4f29a6340901a12affc87047c0ac16b01b92496c460c880a2459abe8c7928374",
|
||||
name: "testyv7",
|
||||
location: "china",
|
||||
capabilities: ["radio"]
|
||||
},
|
||||
nearbySystems: {
|
||||
"Testing P25 System Name": {
|
||||
"frequencies": [
|
||||
155344000,
|
||||
155444000,
|
||||
155555000,
|
||||
155588550
|
||||
],
|
||||
"mode": "p25",
|
||||
"trunkFile": "trunk.tsv",
|
||||
"whitelistFile": "whitelist.tsv"
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const updatedLocalNodeConfig = {
|
||||
node: {
|
||||
nuid: localNodeConfig.node.nuid,
|
||||
name: "updatedName",
|
||||
location: "updatedLocation",
|
||||
capabilities: ["radio", "weather"] // Updated capabilities
|
||||
},
|
||||
nearbySystems: {
|
||||
"Testing P25 System Name": {
|
||||
"frequencies": [
|
||||
155444000,
|
||||
155555000,
|
||||
155500000
|
||||
],
|
||||
"mode": "p25",
|
||||
"trunkFile": "trunk2.tsv",
|
||||
"whitelistFile": "whitelist2.tsv"
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Start the Socket.IO server before running tests
|
||||
let clientSocket; // The socket client
|
||||
let serverClientSocket // The open client socket on the server
|
||||
before(done => {
|
||||
// Startup the node server
|
||||
nodeIo.listen(process.env.SERVER_PORT || 3000, () => {
|
||||
console.log(`server running at http://localhost:${process.env.SERVER_PORT}`);
|
||||
});
|
||||
|
||||
// Connect a client socket to the server
|
||||
clientSocket = ioClient.connect(`http://localhost:${process.env.SERVER_PORT}`);
|
||||
|
||||
nodeIo.on('connection', (socket) => {
|
||||
serverClientSocket = socket;
|
||||
done();
|
||||
})
|
||||
});
|
||||
|
||||
// Close the Socket.IO server after running tests
|
||||
after(async () => {
|
||||
// Disconnect client socket
|
||||
clientSocket.disconnect();
|
||||
|
||||
// Close the server
|
||||
nodeIo.close();
|
||||
|
||||
// Remove the test data
|
||||
deleteNodeByNuid(localNodeConfig.node.nuid); // Delete the user
|
||||
deleteSystemByName(Object.keys(localNodeConfig.nearbySystems)[0])
|
||||
});
|
||||
|
||||
describe('Node Core Server Tests', () => {
|
||||
// Test Node Login functionality
|
||||
describe('Node Login', () => {
|
||||
it('Should add a new node if it does not exist', async () => {
|
||||
// Simulate a node login request
|
||||
// Use the getNodeByNuid mock function to simulate checking if node exists
|
||||
const existingNode = await getNodeByNuid(localNodeConfig.node.nuid);
|
||||
|
||||
// Assert that existingNode is null before node login
|
||||
expect(existingNode).to.be.null;
|
||||
|
||||
// Wait for the update
|
||||
const node_login = new Promise(res => {
|
||||
clientSocket.on('node-login-successful', async () => {
|
||||
res();
|
||||
});
|
||||
});
|
||||
|
||||
// Emit the login command
|
||||
clientSocket.emit("node-login", localNodeConfig.node);
|
||||
|
||||
// Wait for the successful login event
|
||||
await node_login;
|
||||
|
||||
// Now we need to check if the node is added to the database
|
||||
// We can use getNodeByNuid again to verify if the node was added correctly
|
||||
const addedNode = await getNodeByNuid(localNodeConfig.node.nuid);
|
||||
|
||||
console.log("Added Node:", addedNode);
|
||||
|
||||
// Assert that the node is added correctly
|
||||
expect(addedNode).to.have.property('_id'); // Check if _id property exists
|
||||
expect(addedNode).to.have.property('nuid', localNodeConfig.node.nuid);
|
||||
expect(addedNode).to.have.property('name', localNodeConfig.node.name);
|
||||
expect(addedNode).to.have.property('location', localNodeConfig.node.location);
|
||||
expect(addedNode).to.have.deep.property('capabilities', localNodeConfig.node.capabilities);
|
||||
})
|
||||
it('Should update a node if it exists', async () => {
|
||||
// Simulate a node login request
|
||||
// Use the getNodeByNuid mock function to simulate checking if node exists
|
||||
const existingNode = await getNodeByNuid(localNodeConfig.node.nuid);
|
||||
|
||||
// Assert that existingNode is matches the existing data before logging in
|
||||
expect(existingNode).to.have.property('_id'); // Check if _id property exists
|
||||
expect(existingNode).to.have.property('nuid', localNodeConfig.node.nuid);
|
||||
expect(existingNode).to.have.property('name', localNodeConfig.node.name);
|
||||
expect(existingNode).to.have.property('location', localNodeConfig.node.location);
|
||||
expect(existingNode).to.have.deep.property('capabilities', localNodeConfig.node.capabilities);
|
||||
|
||||
// Wait for the update
|
||||
const node_login = new Promise(res => {
|
||||
clientSocket.on('node-login-successful', async () => {
|
||||
res();
|
||||
});
|
||||
});
|
||||
|
||||
// Emit the login command
|
||||
clientSocket.emit("node-login", updatedLocalNodeConfig.node);
|
||||
|
||||
// Wait for the successful login event
|
||||
await node_login;
|
||||
|
||||
// Now we need to check if the node is added to the database
|
||||
// We can use getNodeByNuid again to verify if the node was added correctly
|
||||
const updatedNode = await getNodeByNuid(localNodeConfig.node.nuid);
|
||||
|
||||
console.log("Updated Node:", updatedNode);
|
||||
|
||||
// Assert that the node is added correctly
|
||||
expect(updatedNode).to.have.property('_id'); // Check if _id property exists
|
||||
expect(updatedNode).to.have.property('nuid', updatedLocalNodeConfig.node.nuid);
|
||||
expect(updatedNode).to.have.property('name', updatedLocalNodeConfig.node.name);
|
||||
expect(updatedNode).to.have.property('location', updatedLocalNodeConfig.node.location);
|
||||
expect(updatedNode).to.have.deep.property('capabilities', updatedLocalNodeConfig.node.capabilities);
|
||||
})
|
||||
});
|
||||
|
||||
// Test Node Update functionality
|
||||
describe('Node Update', () => {
|
||||
it('Should add a node\'s nearby systems', async () => {
|
||||
// Simulate an update request sent from the client to the server
|
||||
|
||||
// Get the existing node in the database
|
||||
const existingNode = await getNodeByNuid(localNodeConfig.node.nuid);
|
||||
|
||||
// Assert that existingNode matches the updatedLocalNodeConfig
|
||||
expect(existingNode).to.have.property('_id'); // Check if _id property exists
|
||||
expect(existingNode).to.have.property('nuid', updatedLocalNodeConfig.node.nuid);
|
||||
expect(existingNode).to.have.property('name', updatedLocalNodeConfig.node.name);
|
||||
expect(existingNode).to.have.property('location', updatedLocalNodeConfig.node.location);
|
||||
expect(existingNode).to.have.deep.property('capabilities', updatedLocalNodeConfig.node.capabilities);
|
||||
|
||||
// Get the system from the DB
|
||||
const existsingSystem = await getSystemByName("Testing P25 System Name");
|
||||
|
||||
// Assert that there is no existing system in the DB
|
||||
expect(existsingSystem).to.be.null;
|
||||
|
||||
// Wait for the update
|
||||
const node_system_update = new Promise(res => {
|
||||
clientSocket.on('node-update-successful', async () => {
|
||||
res();
|
||||
});
|
||||
});
|
||||
|
||||
// Emit the update command
|
||||
clientSocket.emit("node-update", updatedLocalNodeConfig);
|
||||
|
||||
// Wait for the successful update event
|
||||
await node_system_update;
|
||||
|
||||
// Now we need to check if the system is added to the database
|
||||
// We can use getNodeByNuid again to verify if the node was added correctly
|
||||
const updatedNode = await getNodeByNuid(localNodeConfig.node.nuid);
|
||||
|
||||
console.log("Updated Node:", updatedNode);
|
||||
|
||||
// Assert that the node is added correctly
|
||||
expect(updatedNode).to.have.property('_id'); // Check if _id property exists
|
||||
expect(updatedNode).to.have.property('nuid', updatedLocalNodeConfig.node.nuid);
|
||||
expect(updatedNode).to.have.property('name', updatedLocalNodeConfig.node.name);
|
||||
expect(updatedNode).to.have.property('location', updatedLocalNodeConfig.node.location);
|
||||
expect(updatedNode).to.have.deep.property('capabilities', updatedLocalNodeConfig.node.capabilities);
|
||||
|
||||
// Get the updated system
|
||||
const addedSystem = await getSystemByName("Testing P25 System Name");
|
||||
|
||||
console.log("Added system:", addedSystem);
|
||||
|
||||
expect(addedSystem).to.have.property('_id'); // Check if _id property exists
|
||||
expect(addedSystem).to.have.property('nodes'); // Check if nodes property exists
|
||||
expect(addedSystem.nodes).to.include(updatedLocalNodeConfig.node.nuid) // Check if this node ID is in the nodes array
|
||||
expect(addedSystem).to.have.deep.property('frequencies', updatedLocalNodeConfig.nearbySystems['Testing P25 System Name'].frequencies);
|
||||
expect(addedSystem).to.have.property('mode', updatedLocalNodeConfig.nearbySystems['Testing P25 System Name'].mode);
|
||||
});
|
||||
|
||||
it('Should update a node and its nearby systems', async () => {
|
||||
// Get the existing node in the database
|
||||
const existingNode = await getNodeByNuid(localNodeConfig.node.nuid);
|
||||
|
||||
// Assert that existingNode matches the updatedLocalNodeConfig
|
||||
expect(existingNode).to.have.property('_id'); // Check if _id property exists
|
||||
expect(existingNode).to.have.property('nuid', updatedLocalNodeConfig.node.nuid);
|
||||
expect(existingNode).to.have.property('name', updatedLocalNodeConfig.node.name);
|
||||
expect(existingNode).to.have.property('location', updatedLocalNodeConfig.node.location);
|
||||
expect(existingNode).to.have.deep.property('capabilities', updatedLocalNodeConfig.node.capabilities);
|
||||
|
||||
// Get the updated system
|
||||
const existingSystem = await getSystemByName("Testing P25 System Name");
|
||||
expect(existingSystem).to.have.property('_id'); // Check if _id property exists
|
||||
expect(existingSystem).to.have.property('nodes'); // Check if nodes property exists
|
||||
expect(existingSystem.nodes).to.include(updatedLocalNodeConfig.node.nuid) // Check if this node ID is in the nodes array
|
||||
expect(existingSystem).to.have.deep.property('frequencies', updatedLocalNodeConfig.nearbySystems['Testing P25 System Name'].frequencies);
|
||||
expect(existingSystem).to.have.property('mode', updatedLocalNodeConfig.nearbySystems['Testing P25 System Name'].mode);
|
||||
|
||||
|
||||
// Wait for the update
|
||||
const node_update = new Promise(res => {
|
||||
clientSocket.on('node-update-successful', async () => {
|
||||
res();
|
||||
});
|
||||
});
|
||||
|
||||
// Emit the update command
|
||||
clientSocket.emit("node-update", localNodeConfig);
|
||||
|
||||
// Wait for the successful update event
|
||||
await node_update;
|
||||
|
||||
const updatedNode = await getNodeByNuid(localNodeConfig.node.nuid);
|
||||
|
||||
console.log("Updated Node:", updatedNode);
|
||||
|
||||
// Assert that the node is added correctly
|
||||
expect(updatedNode).to.have.property('_id'); // Check if _id property exists
|
||||
expect(updatedNode).to.have.property('nuid', localNodeConfig.node.nuid);
|
||||
expect(updatedNode).to.have.property('name', localNodeConfig.node.name);
|
||||
expect(updatedNode).to.have.property('location', localNodeConfig.node.location);
|
||||
expect(updatedNode).to.have.deep.property('capabilities', localNodeConfig.node.capabilities);
|
||||
|
||||
// Get the updated system
|
||||
const updatedSystem = await getSystemByName("Testing P25 System Name");
|
||||
|
||||
console.log("Updated system:", updatedSystem);
|
||||
|
||||
expect(updatedSystem).to.have.property('_id'); // Check if _id property exists
|
||||
expect(updatedSystem).to.have.property('nodes'); // Check if nodes property exists
|
||||
expect(updatedSystem.nodes).include(localNodeConfig.node.nuid) // Check if this node ID is in the nodes array
|
||||
expect(updatedSystem).to.have.deep.property('frequencies', localNodeConfig.nearbySystems['Testing P25 System Name'].frequencies);
|
||||
expect(updatedSystem).to.have.property('mode', localNodeConfig.nearbySystems['Testing P25 System Name'].mode);
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
describe('Node Disconnect', () => {
|
||||
it('Should trigger cleanup actions upon socket disconnection', async () => {
|
||||
// Write test code to simulate a socket disconnection
|
||||
// Check if the appropriate cleanup actions are triggered
|
||||
});
|
||||
})
|
||||
});
|
||||
Reference in New Issue
Block a user