Compare commits
140 Commits
6bc09df824
...
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 | ||
|
|
c46b7c6ea6 | ||
|
|
5d2097ecd4 | ||
|
|
7efd0cd4f3 | ||
|
|
36c0ec8b13 | ||
|
|
79574e188d | ||
|
|
d0a75dc557 | ||
|
|
6b156b441a | ||
|
|
956dc89107 | ||
|
|
976c44838e | ||
|
|
1395130d6d | ||
|
|
54a6d544e4 | ||
|
|
d253d0aef1 | ||
|
|
35f3f07793 | ||
|
|
63ad20b9b3 | ||
|
|
e431db1337 | ||
|
|
42784f1852 |
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
.gitignore
vendored
2
.gitignore
vendored
@@ -297,3 +297,5 @@ cython_debug/
|
||||
# Ignore the config dirs
|
||||
config/
|
||||
|
||||
# Ignore the OP25 directory we will create
|
||||
op25/
|
||||
|
||||
@@ -41,4 +41,4 @@ Contributions to Discord Radio Bot are welcome! If you have any ideas for improv
|
||||
|
||||
## License
|
||||
|
||||
This project is licensed under the MIT License - see the [LICENSE](/src/branch/master/LICENSE) file for details.
|
||||
This project is licensed under the GNU License - see the [LICENSE](/logan/DRBv3/src/branch/master/LICENSE) file for details.
|
||||
|
||||
@@ -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,7 +10,10 @@ dotenv.config()
|
||||
var localNodeConfig = new ClientNodeConfig({})
|
||||
|
||||
async function boot() {
|
||||
if (localNodeConfig.node.nuid === undefined || localNodeConfig.node.nuid === '' || localNodeConfig.node.nuid === 0) {
|
||||
// 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,137 +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;
|
||||
|
||||
const device = "VoiceMeeter VAIO3 Output (VB-Audio VoiceMeeter VAIO3)", maxTransmissionGap = 500, type = "dshow";
|
||||
|
||||
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 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!');
|
||||
attachRecorder();
|
||||
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
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);
|
||||
}
|
||||
@@ -62,3 +62,16 @@ export const generateUniqueID = () => {
|
||||
|
||||
return uniqueID;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Extracts the value after a specific pattern from a string using regular expressions.
|
||||
* @param {string} input - The input string.
|
||||
* @param {string} pattern - The pattern to match.
|
||||
* @returns {string|null} The value found after the pattern, or null if not found.
|
||||
*/
|
||||
export const extractValue = (input, pattern) => {
|
||||
const regex = new RegExp(`${pattern}`);
|
||||
const match = input.match(regex);
|
||||
return match ? match : null;
|
||||
};
|
||||
39
client/modules/cliHandler.mjs
Normal file
39
client/modules/cliHandler.mjs
Normal file
@@ -0,0 +1,39 @@
|
||||
import { spawn } from "child_process";
|
||||
|
||||
/**
|
||||
* Executes a command and retrieves its output.
|
||||
* @param {string} command - The command to execute.
|
||||
* @param {string[]} args - The arguments to pass to the command.
|
||||
* @returns {Promise<string>} A promise that resolves with the output of the command.
|
||||
*/
|
||||
export const executeCommand = (command, args) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const childProcess = spawn(command, args);
|
||||
|
||||
let commandOutput = '';
|
||||
|
||||
childProcess.stdout.on('data', (data) => {
|
||||
commandOutput += data.toString();
|
||||
});
|
||||
|
||||
childProcess.stderr.on('data', (data) => {
|
||||
// Log any errors to stderr
|
||||
console.error(data.toString());
|
||||
});
|
||||
|
||||
childProcess.on('error', (error) => {
|
||||
// Reject the promise if there's an error executing the command
|
||||
reject(error);
|
||||
});
|
||||
|
||||
childProcess.on('close', (code) => {
|
||||
if (code === 0) {
|
||||
// Resolve the promise with the command output if it exits successfully
|
||||
resolve(commandOutput.trim());
|
||||
} else {
|
||||
// Reject the promise if the command exits with a non-zero code
|
||||
reject(new Error(`Command '${command}' exited with code ${code}`));
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
57
client/modules/selfUpdater.mjs
Normal file
57
client/modules/selfUpdater.mjs
Normal file
@@ -0,0 +1,57 @@
|
||||
import simpleGit from 'simple-git';
|
||||
import { restartService } from './serviceHandler.mjs'
|
||||
import { launchProcess } from './subprocessHandler.mjs'
|
||||
|
||||
const git = simpleGit();
|
||||
|
||||
// Function to check for updates
|
||||
export const checkForUpdates = async () => {
|
||||
try {
|
||||
// Fetch remote changes
|
||||
await git.fetch();
|
||||
|
||||
// Get the latest commit hash
|
||||
const latestCommitHash = await git.revparse(['@{u}']);
|
||||
|
||||
// Compare with the local commit hash
|
||||
const localCommitHash = await git.revparse(['HEAD']);
|
||||
|
||||
if (latestCommitHash !== localCommitHash) {
|
||||
console.log('An update is available. Updating...');
|
||||
|
||||
// Check if there have been any changes to the code
|
||||
const gitStatus = await git.status()
|
||||
console.log(gitStatus);
|
||||
if (gitStatus.modified.length > 0){
|
||||
// There is locally modified code
|
||||
console.log("There is locally modified code, resetting...");
|
||||
await git.stash();
|
||||
await git.reset('hard', ['origin/master']);
|
||||
}
|
||||
|
||||
// Pull the latest changes from the remote repository
|
||||
await git.pull();
|
||||
|
||||
// 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.');
|
||||
return false
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error checking for updates:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Function to restart the application
|
||||
const restartApplication = () => {
|
||||
console.log('Restarting the application...');
|
||||
restartService('discord-radio-bot');
|
||||
}
|
||||
58
client/modules/serviceHandler.mjs
Normal file
58
client/modules/serviceHandler.mjs
Normal file
@@ -0,0 +1,58 @@
|
||||
import { exec } from 'child_process';
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
const executeCommand = (command) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
exec(command, (error, stdout, stderr) => {
|
||||
if (error) {
|
||||
console.error(`Command failed with error: ${error.message}`);
|
||||
resolve({ stdout, stderr });
|
||||
} else {
|
||||
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.
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
export const restartService = async (serviceName) => {
|
||||
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.
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
export const stopService = async (serviceName) => {
|
||||
try {
|
||||
await executeCommand(`sudo systemctl stop ${serviceName}.service`);
|
||||
} catch (error) {
|
||||
console.error(`Failed to stop service: ${error.message}`);
|
||||
}
|
||||
};
|
||||
@@ -1,52 +1,52 @@
|
||||
import { io } from "socket.io-client";
|
||||
import { connectToChannel, initDiscordBotClient, getVoiceChannelFromID, checkIfConnectedToVC } from '../discordAudioBot/dab.mjs';
|
||||
import { logIntoServerWrapper, sendNodeUpdateWrapper } 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
|
||||
* @param {Object} localNodeConfig The local node config object
|
||||
* @returns {any}
|
||||
*/
|
||||
export const initSocketConnection = async (localNodeConfig) => {
|
||||
const serverEndpoint = `http://${localNodeConfig.serverIp}:${localNodeConfig.serverPort}` || 'http://localhost:3000'; // Adjust the server endpoint
|
||||
|
||||
const socket = io.connect(serverEndpoint);
|
||||
|
||||
// Socket Events ('system' events persay)
|
||||
// When the socket connects to the node server
|
||||
socket.on('connect', async () => {
|
||||
console.log('Connected to the server');
|
||||
await logIntoServerWrapper(socket, localNodeConfig);
|
||||
});
|
||||
|
||||
socket.on('node-join', async (joinData) => {
|
||||
console.log("Join requested: ", joinData)
|
||||
// TODO - Implement logic to control OP25 for the requested channel/system
|
||||
|
||||
// Join the requested channel with the requested ID
|
||||
initDiscordBotClient(joinData.clientID, joinData.system, client => {
|
||||
getVoiceChannelFromID(client, joinData.channelID).then(vc => {
|
||||
const connection = connectToChannel(vc);
|
||||
console.log("Bot Connected to VC");
|
||||
})
|
||||
});
|
||||
});
|
||||
|
||||
socket.on('node-leave', async () => {
|
||||
console.log("Leave requested");
|
||||
const connection = await getVoiceConnection(myVoiceChannel.guild.id);
|
||||
if (connection) {
|
||||
console.log("There is an open VC connection, closing it now");
|
||||
connection.destroy();
|
||||
}
|
||||
});
|
||||
|
||||
socket.on('node-check-connected-status', async (guildId, socketCallback) => {
|
||||
console.log("Requested status check");
|
||||
if (await checkIfConnectedToVC(guildId)) {
|
||||
console.log("There is an open VC connection");
|
||||
socketCallback(true);
|
||||
} else {
|
||||
socketCallback(false);
|
||||
}
|
||||
});
|
||||
|
||||
// When the socket disconnects from the node server
|
||||
socket.on('disconnect', () => {
|
||||
console.log('Disconnected from the server');
|
||||
});
|
||||
|
||||
// Node events/commands
|
||||
// Requested the node update itself
|
||||
socket.on('node-update', nodeUpdate);
|
||||
|
||||
// Requested to join a discord guild and listen to a system
|
||||
socket.on('node-join', nodeJoinServer);
|
||||
|
||||
// Requested to leave a discord guild
|
||||
socket.on('node-leave', nodeLeaveServer);
|
||||
|
||||
// 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);
|
||||
|
||||
// Requested to check if the node has an open discord client
|
||||
socket.on('node-check-discord-open-client', nodeCheckDiscordClientStatus);
|
||||
|
||||
// Requested to get the current listening system
|
||||
socket.on('node-check-current-system', nodeCheckCurrentSystem);
|
||||
|
||||
return socket;
|
||||
}
|
||||
@@ -1,11 +1,108 @@
|
||||
import { checkIfDiscordVCConnected, joinDiscordVC, leaveDiscordVC, getDiscordUsername, checkIfClientIsOpen, getDiscordID } from '../discordAudioBot/pdabWrappers.mjs';
|
||||
import { getCurrentSystem } from '../op25Handler/op25Handler.mjs';
|
||||
import { checkForUpdates } from './selfUpdater.mjs';
|
||||
|
||||
|
||||
/**
|
||||
* Check if the bot has an update available
|
||||
* @param {any} socketCallback The callback function to return the result
|
||||
* @callback {boolean} If the node has an update available or not
|
||||
*/
|
||||
export const nodeUpdate = async (socketCallback) => {
|
||||
socketCallback(await checkForUpdates());
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Wrapper to log into the server
|
||||
* @param {any} socket The socket connection with the server
|
||||
* @param {object} localNodeConfig The local node object
|
||||
* @returns {any}
|
||||
*/
|
||||
export const logIntoServerWrapper = async (socket, localNodeConfig) => {
|
||||
// Log into the server
|
||||
socket.emit("node-login", localNodeConfig.node);
|
||||
|
||||
// Send an update to the server
|
||||
sendNodeUpdateWrapper(socket, localNodeConfig);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Send the server an update
|
||||
* @param {any} socket The socket connection with the server
|
||||
* @param {object} localNodeConfig The local node object
|
||||
*/
|
||||
export const sendNodeUpdateWrapper = async (socket, localNodeConfig) => {
|
||||
socket.emit('node-update', {
|
||||
'node': localNodeConfig.node,
|
||||
'nearbySystems': localNodeConfig.nearbySystems
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 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 nodeJoinServer = async (joinData) => {
|
||||
await joinDiscordVC(joinData);
|
||||
}
|
||||
|
||||
/**
|
||||
* Leave VC on the requested server
|
||||
* @param {string} guildId The guild ID to disconnect from VC
|
||||
*/
|
||||
export const nodeLeaveServer = async (guildId) => {
|
||||
await leaveDiscordVC(guildId);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 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
|
||||
* @param {any} socketCallback The callback function to return the result to
|
||||
* @callback {boolean} If the node is connected to VC in the given guild
|
||||
*/
|
||||
export const nodeCheckStatus = async (guildId, socketCallback) => {
|
||||
socketCallback(await checkIfDiscordVCConnected(guildId));
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 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
|
||||
* @param {any} socketCallback The callback function to return the result to
|
||||
* @callback {any}
|
||||
*/
|
||||
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
|
||||
* @callback {boolean} If the node has an open discord client or not
|
||||
*/
|
||||
export const nodeCheckDiscordClientStatus = async (socketCallback) => {
|
||||
socketCallback(await checkIfClientIsOpen());
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Check what system the local node is currently listening to
|
||||
* @callback {boolean} If the node has an open discord client or not
|
||||
*/
|
||||
export const nodeCheckCurrentSystem = async (socketCallback) => {
|
||||
socketCallback(await getCurrentSystem());
|
||||
}
|
||||
100
client/modules/subprocessHandler.mjs
Normal file
100
client/modules/subprocessHandler.mjs
Normal file
@@ -0,0 +1,100 @@
|
||||
import { spawn } from "child_process";
|
||||
import dotenv from 'dotenv';
|
||||
dotenv.config()
|
||||
|
||||
/**
|
||||
* Object to store references to spawned processes.
|
||||
* @type {Object.<string, import('child_process').ChildProcess>}
|
||||
*/
|
||||
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, waitForClose = false, pcwd = undefined) => {
|
||||
if (!runningProcesses[processName]) {
|
||||
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.`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks the status of a process.
|
||||
* @param {string} processName - The name of the process to check.
|
||||
* @returns {string} A message indicating whether the process is running or not.
|
||||
*/
|
||||
export const checkProcessStatus = (processName) => {
|
||||
const childProcess = runningProcesses[processName];
|
||||
if (childProcess) {
|
||||
// Check if the process is running
|
||||
if (!childProcess.killed) {
|
||||
return `${processName} process is running.`;
|
||||
} else {
|
||||
return `${processName} process is not running.`;
|
||||
}
|
||||
} else {
|
||||
return `${processName} process is not running.`;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Kills a running process.
|
||||
* @param {string} processName - The name of the process to kill.
|
||||
*/
|
||||
export const killProcess = (processName) => {
|
||||
const childProcess = runningProcesses[processName];
|
||||
if (childProcess) {
|
||||
childProcess.kill();
|
||||
console.log(`${processName} process killed.`);
|
||||
} else {
|
||||
console.log(`${processName} process is not running.`);
|
||||
}
|
||||
}
|
||||
|
||||
export const getRunningProcesses = () => runningProcesses;
|
||||
@@ -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
|
||||
})
|
||||
|
||||
183
client/op25Handler/modules/op25ConfigGenerators.mjs
Normal file
183
client/op25Handler/modules/op25ConfigGenerators.mjs
Normal file
@@ -0,0 +1,183 @@
|
||||
import { promises as fs } from 'fs';
|
||||
|
||||
class OP25ConfigObject {
|
||||
constructor() { }
|
||||
|
||||
async exportToFile(filename) {
|
||||
try {
|
||||
const jsonConfig = JSON.stringify(this, null, 2);
|
||||
await fs.writeFile(filename, jsonConfig);
|
||||
console.log(`Config exported to ${filename}`);
|
||||
} catch (error) {
|
||||
console.error(`Error exporting config to ${filename}: ${error}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class P25ConfigGenerator extends OP25ConfigObject {
|
||||
constructor({ systemName, controlChannels, tagsFile, whitelistFile = undefined }) {
|
||||
super();
|
||||
console.log("Generating P25 Config for:", systemName);
|
||||
const controlChannelsString = controlChannels.join(',');
|
||||
this.channels = [new channelConfig({
|
||||
"channelName": systemName,
|
||||
"systemName": systemName,
|
||||
"enableAnalog": "off",
|
||||
"demodType": "cqpsk",
|
||||
"cqpskTracking": true,
|
||||
"filterType": "rc"
|
||||
})];
|
||||
this.devices = [new deviceConfig({
|
||||
"gain": "LNA:36"
|
||||
})];
|
||||
this.trunking = new trunkingConfig({
|
||||
"module": "tk_p25.py",
|
||||
"systemName": systemName,
|
||||
"controlChannelsString": controlChannelsString,
|
||||
"tagsFile": tagsFile,
|
||||
"whitelist": whitelistFile
|
||||
});
|
||||
this.audio = new audioConfig({});
|
||||
this.terminal = new terminalConfig({});
|
||||
}
|
||||
}
|
||||
|
||||
export class NBFMConfigGenerator extends OP25ConfigObject {
|
||||
constructor({ systemName, frequency, nbfmSquelch = -70 }) {
|
||||
super();
|
||||
this.channels = new channelConfig({
|
||||
"channelName": systemName,
|
||||
"enableAnalog": "on",
|
||||
"nbfmSquelch": nbfmSquelch,
|
||||
"frequency": frequency,
|
||||
"demodType": "fsk4",
|
||||
"filterType": "widepulse"
|
||||
});
|
||||
this.devices = new deviceConfig({
|
||||
"gain": "LNA:32"
|
||||
});
|
||||
this.audio = new audioConfig({});
|
||||
this.terminal = new terminalConfig({});
|
||||
}
|
||||
}
|
||||
|
||||
class channelConfig {
|
||||
constructor({
|
||||
channelName = "Voice_ch1",
|
||||
device = "sdr0",
|
||||
systemName,
|
||||
metaStreamName,
|
||||
demodType, // cqpsk: P25; fsk4: everything else
|
||||
cqpskTracking,
|
||||
trackingThreshold = 120,
|
||||
trackingFeedback = 0.75,
|
||||
destination = "udp://127.0.0.1:23456",
|
||||
excess_bw = 0.2,
|
||||
filterType = "rc", // rc: P25; widepulse: analog
|
||||
ifRate = 24000,
|
||||
plot = "",
|
||||
symbolRate = 4800,
|
||||
enableAnalog, //[on, off, auto]
|
||||
nbfmDeviation = 4000, // only needed if analog is enabled
|
||||
nbfmSquelch = -50, // only needed if analog is enabled
|
||||
frequency, // only needed if analog is enabled
|
||||
blacklist,
|
||||
whitelist,
|
||||
cryptKeys
|
||||
}) {
|
||||
// Core Configs
|
||||
this.name = channelName;
|
||||
this.device = device;
|
||||
this.demod_type = demodType;
|
||||
this.destination = destination;
|
||||
this.excess_bw = excess_bw;
|
||||
this.filter_type = filterType;
|
||||
this.if_rate = ifRate;
|
||||
this.plot = plot;
|
||||
this.symbol_rate = symbolRate;
|
||||
this.enable_analog = enableAnalog;
|
||||
|
||||
// P25 config
|
||||
if (!enableAnalog || enableAnalog === "off" || systemName) this.trunking_sysname = systemName;
|
||||
if (!enableAnalog || enableAnalog === "off" || systemName && metaStreamName) this.meta_stream_name = metaStreamName ?? "";
|
||||
if (!enableAnalog || enableAnalog === "off" || systemName) this.cqpsk_tracking = cqpskTracking;
|
||||
if (!enableAnalog || enableAnalog === "off" || systemName) this.tracking_threshold = trackingThreshold;
|
||||
if (!enableAnalog || enableAnalog === "off" || systemName) this.tracking_feedback = trackingFeedback;
|
||||
if (!enableAnalog || enableAnalog === "off" || systemName && blacklist) this.blacklist = blacklist ?? "";
|
||||
if (!enableAnalog || enableAnalog === "off" || systemName && whitelist) this.whitelist = whitelist ?? "";
|
||||
if (!enableAnalog || enableAnalog === "off" || systemName && cryptKeys) this.crypt_keys = cryptKeys ?? "";
|
||||
|
||||
// Analog config
|
||||
if (enableAnalog === "on" || enableAnalog === "auto") this.nbfm_deviation = nbfmDeviation;
|
||||
if (enableAnalog === "on" || enableAnalog === "auto") this.nbfm_squelch = nbfmSquelch;
|
||||
if (enableAnalog === "on" || enableAnalog === "auto") this.frequency = frequency;
|
||||
}
|
||||
}
|
||||
|
||||
class deviceConfig {
|
||||
constructor({ args = "rtl", gain = "LNA:32", gainMode = false, name = "sdr0", offset = 0, ppm = 0.0, sampleRate = 1920000, tunable = true }) {
|
||||
this.args = args
|
||||
this.gains = gain
|
||||
this.gain_mode = gainMode
|
||||
this.name = name
|
||||
this.offset = offset
|
||||
this.ppm = ppm
|
||||
this.rate = sampleRate
|
||||
this.usable_bw_pct = 0.85
|
||||
this.tunable = tunable
|
||||
}
|
||||
}
|
||||
|
||||
class trunkingConfig {
|
||||
/**
|
||||
*
|
||||
* @param {object} *
|
||||
*/
|
||||
constructor({ module, systemName, controlChannelsString, tagsFile = "", nac = "0x0", wacn = "0x0", cryptBehavior = 2, whitelist = "", blacklist = "" }) {
|
||||
this.module = module;
|
||||
this.chans = [{
|
||||
"nac": nac,
|
||||
"wacn": wacn,
|
||||
"sysname": systemName,
|
||||
"control_channel_list": controlChannelsString,
|
||||
"whitelist": whitelist,
|
||||
"blacklist": blacklist,
|
||||
"tgid_tags_file": tagsFile,
|
||||
"tdma_cc": false,
|
||||
"crypt_behavior": cryptBehavior
|
||||
}];
|
||||
}
|
||||
}
|
||||
|
||||
class audioConfig {
|
||||
constructor({ module = "sockaudio.py", port = 23456, deviceName = "default" }) {
|
||||
this.module = module;
|
||||
this.instances = [{
|
||||
"instance_name": "audio0",
|
||||
"device_name": deviceName,
|
||||
"udp_port": port,
|
||||
"audio_gain": 2.0,
|
||||
"number_channels": 1
|
||||
}];
|
||||
}
|
||||
}
|
||||
|
||||
class metadataStreamConfig {
|
||||
constructor({ }) {
|
||||
this.module = "";
|
||||
this.streams = [];
|
||||
}
|
||||
}
|
||||
|
||||
class terminalConfig {
|
||||
constructor({ module = "terminal.py", terminalType = "http:0.0.0.0:8081" }) {
|
||||
this.module = module;
|
||||
this.terminal_type = terminalType;
|
||||
this.curses_plot_interval = 0.1;
|
||||
this.http_plot_interval = 1.0;
|
||||
this.http_plot_directory = "../www/images";
|
||||
this.tuning_step_large = 1200;
|
||||
this.tuning_step_small = 100;
|
||||
}
|
||||
}
|
||||
|
||||
84
client/op25Handler/op25Handler.mjs
Normal file
84
client/op25Handler/op25Handler.mjs
Normal file
@@ -0,0 +1,84 @@
|
||||
import { P25ConfigGenerator, NBFMConfigGenerator } from './modules/op25ConfigGenerators.mjs';
|
||||
import { getAllPresets } from '../modules/radioPresetHandler.mjs';
|
||||
import { startService, stopService } from '../modules/serviceHandler.mjs';
|
||||
|
||||
import dotenv from 'dotenv';
|
||||
dotenv.config()
|
||||
|
||||
let currentSystem = undefined;
|
||||
|
||||
/**
|
||||
* Creates configuration based on the preset and restarts the OP25 service.
|
||||
* @param {Object} preset The preset object containing system configuration.
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
const createConfigAndRestartService = async (systemName, preset) => {
|
||||
const { mode, frequencies, trunkFile, whitelistFile } = preset;
|
||||
|
||||
let generator;
|
||||
if (mode === 'p25') {
|
||||
console.log("Using P25 Config Generator based on preset mode", systemName, mode);
|
||||
generator = new P25ConfigGenerator({
|
||||
systemName,
|
||||
controlChannels: frequencies,
|
||||
tagsFile: trunkFile,
|
||||
whitelistFile: whitelistFile !== 'none' ? whitelistFile : undefined
|
||||
});
|
||||
} else if (mode === 'nbfm') {
|
||||
console.log("Using NBFM Config Generator based on preset mode", systemName, mode);
|
||||
generator = new NBFMConfigGenerator({
|
||||
systemName,
|
||||
frequencies,
|
||||
tagsFile: trunkFile
|
||||
});
|
||||
} else {
|
||||
throw new Error(`Unsupported mode: ${mode}`);
|
||||
}
|
||||
|
||||
const op25FilePath = process.env.OP25_FULL_PATH || './'; // Default to current directory if OP25_FULL_PATH is not set
|
||||
const op25ConfigPath = `${op25FilePath}${op25FilePath.endsWith('/') ? 'active.cfg.json' : '/active.cfg.json'}`;
|
||||
await generator.exportToFile(op25ConfigPath);
|
||||
|
||||
// Restart the service
|
||||
await stopService('op25-multi_rx');
|
||||
await startService('op25-multi_rx');
|
||||
};
|
||||
|
||||
/**
|
||||
* Opens the OP25 service for the specified system.
|
||||
* @param {string} systemName The name of the system to open.
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
export const openOP25 = async (systemName) => {
|
||||
currentSystem = systemName;
|
||||
|
||||
// Retrieve preset for the specified system name
|
||||
const presets = await getAllPresets();
|
||||
const preset = presets[systemName];
|
||||
|
||||
console.log("Found preset:", preset);
|
||||
|
||||
if (!preset) {
|
||||
throw new Error(`Preset for system "${systemName}" not found.`);
|
||||
}
|
||||
|
||||
await createConfigAndRestartService(systemName, preset);
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Closes the OP25 service.
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
export const closeOP25 = async () => {
|
||||
currentSystem = undefined;
|
||||
await stopService('op25-multi_rx');
|
||||
};
|
||||
|
||||
/**
|
||||
* Gets the current system.
|
||||
* @returns {Promise<string | undefined>} The name of the current system.
|
||||
*/
|
||||
export const getCurrentSystem = async () => {
|
||||
return currentSystem;
|
||||
};
|
||||
1702
client/package-lock.json
generated
1702
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,12 +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
|
||||
1
client/serviceStart.sh
Normal file
1
client/serviceStart.sh
Normal file
@@ -0,0 +1 @@
|
||||
node .
|
||||
314
client/setup.sh
Normal file
314
client/setup.sh
Normal file
@@ -0,0 +1,314 @@
|
||||
#!/bin/bash
|
||||
|
||||
####------------------- Pre-Flight Checks
|
||||
# Exit on error
|
||||
set -e
|
||||
|
||||
# Check if the script is run as root
|
||||
if [[ $(id -u) -ne 0 ]]; then
|
||||
echo "Please run this script as root."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check if the working directory is 'client' and contains package.json
|
||||
if [[ ! -f "$(pwd)/package.json" ]]; then
|
||||
echo "Error: Please make sure the working directory is 'client' and contains package.json."
|
||||
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=""
|
||||
local trunk_file=""
|
||||
local whitelist_file=""
|
||||
|
||||
read -p "Enter system name: " system_name
|
||||
read -p "Enter frequencies (comma-separated): " frequencies
|
||||
read -p "Enter mode (p25/nbfm): " mode
|
||||
|
||||
if [[ "$mode" == "p25" ]]; then
|
||||
read -p "Enter trunk file: " trunk_file
|
||||
read -p "Enter whitelist file: " whitelist_file
|
||||
fi
|
||||
|
||||
echo "\"$system_name\": {
|
||||
\"frequencies\": [\"$(echo "$frequencies" | sed 's/,/","/g')\"],
|
||||
\"mode\": \"$mode\",
|
||||
\"trunkFile\": \"$trunk_file\",
|
||||
\"whitelistFile\": \"$whitelist_file\"
|
||||
},"
|
||||
fi
|
||||
}
|
||||
|
||||
# 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
|
||||
apt update
|
||||
apt upgrade -y
|
||||
|
||||
# Install the necessary packages
|
||||
echo "Installing dependencies..."
|
||||
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")
|
||||
echo "CLIENT_NAME=$client_name" >> .env
|
||||
client_location=$(prompt_user "Enter the location of this node")
|
||||
echo "CLIENT_LOCATION=$client_location" >> .env
|
||||
client_capabilities=$(prompt_capabilities)
|
||||
echo "CLIENT_CAPABILITIES=$client_capabilities" >> .env
|
||||
|
||||
# Server configuration (preset values)
|
||||
echo "" >> .env
|
||||
echo "# Configuration for the connection to the server" >> .env
|
||||
echo "SERVER_IP=vpn.cusano.net" >> .env
|
||||
echo "SERVER_PORT=3000" >> .env
|
||||
|
||||
# OP25 configuration (preset values)
|
||||
echo "" >> .env
|
||||
echo "# Configuration for OP25" >> .env
|
||||
op25_full_path="$(pwd)/op25/op25/gr-op25_repeater/apps" # Update this with the actual path
|
||||
echo "OP25_FULL_PATH=$op25_full_path" >> .env
|
||||
|
||||
# Core configuration (preset value)
|
||||
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."
|
||||
|
||||
# Create a JSON object to store nearby systems
|
||||
systems_json="{"
|
||||
while true; do
|
||||
systems_json+="$(prompt_nearby_system)"
|
||||
read -p "Do you want to add another system? (yes/no): " choice
|
||||
if [[ "$choice" != "yes" ]]; then
|
||||
break
|
||||
fi
|
||||
done
|
||||
systems_json="${systems_json%,}" # Remove trailing comma
|
||||
systems_json+="}"
|
||||
|
||||
# Append the created systems to the presets file
|
||||
mkdir -p ./config
|
||||
echo "$systems_json" >> "./config/radioPresets.json"
|
||||
|
||||
echo "Systems added to radioPresets.json."
|
||||
|
||||
# Create a systemd service file
|
||||
echo "Adding DRB Node service..."
|
||||
service_content="[Unit]
|
||||
Description=Discord-Radio-Bot_v3
|
||||
After=syslog.target network.target nss-lookup.target network-online.target
|
||||
Requires=network-online.target
|
||||
|
||||
[Service]
|
||||
User=1000
|
||||
Group=1000
|
||||
WorkingDirectory=$(pwd)
|
||||
ExecStart=/bin/bash -- serviceStart.sh
|
||||
RestartSec=5
|
||||
Restart=on-failure
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target"
|
||||
|
||||
# Write the systemd service file
|
||||
echo "$service_content" > /etc/systemd/system/discord-radio-bot.service
|
||||
|
||||
# Reload systemd daemon
|
||||
systemctl daemon-reload
|
||||
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 -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
|
||||
echo "Editing startup script..."
|
||||
sed -i 's/p25_rtl_example.json/active.cfg.json/g' op25-multi_rx.sh
|
||||
|
||||
# Move the startup script to the apps dir
|
||||
mv op25-multi_rx.sh op25/gr-op25_repeater/apps/
|
||||
|
||||
# Install the OP25 service
|
||||
echo "Adding OP25 service..."
|
||||
service_content="[Unit]
|
||||
Description=op25-multi_rx
|
||||
After=syslog.target network.target nss-lookup.target network-online.target
|
||||
Requires=network-online.target
|
||||
|
||||
[Service]
|
||||
User=1000
|
||||
Group=1000
|
||||
WorkingDirectory=$(pwd)/op25/gr-op25_repeater/apps
|
||||
ExecStart=/bin/bash -- op25-multi_rx.sh
|
||||
RestartSec=5
|
||||
Restart=on-failure
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target"
|
||||
|
||||
# Write the systemd service file
|
||||
echo "$service_content" > /etc/systemd/system/op25-multi_rx.service
|
||||
|
||||
# Reload systemd daemon
|
||||
systemctl daemon-reload
|
||||
|
||||
# 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 } from '../../modules/socketServerWrappers.mjs';
|
||||
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()
|
||||
@@ -12,7 +13,16 @@ export const data = new SlashCommandBuilder()
|
||||
.setRequired(true)
|
||||
.setAutocomplete(true));
|
||||
|
||||
export async function autocomplete(interaction) {
|
||||
// Exporting other properties
|
||||
export const example = "/join";
|
||||
export const deferInitialReply = true;
|
||||
|
||||
/**
|
||||
* Function to give the user auto-reply suggestions
|
||||
* @param {any} nodeIo The nodeIO server for manipulation of sockets
|
||||
* @param {any} interaction The interaction object
|
||||
*/
|
||||
export async function autocomplete(nodeIo, interaction) {
|
||||
const focusedValue = interaction.options.getFocused();
|
||||
const choices = await getAllSystems();
|
||||
const filtered = choices.filter(choice => choice.name.startsWith(focusedValue));
|
||||
@@ -24,16 +34,17 @@ export async function autocomplete(interaction) {
|
||||
);
|
||||
}
|
||||
|
||||
// Exporting other properties
|
||||
export const example = "/join";
|
||||
export const deferInitialReply = true;
|
||||
|
||||
// Exporting execute function
|
||||
/**
|
||||
* The function to run when the command is called by a discord user
|
||||
* @param {any} nodeIo The nodeIO server for manipulation of sockets
|
||||
* @param {any} interaction The interaction object
|
||||
*/
|
||||
export async function execute(nodeIo, interaction) {
|
||||
// Check if the user is in a VC
|
||||
if (!interaction.member.voice.channel) { return await interaction.reply({ content: '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');
|
||||
@@ -45,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);
|
||||
|
||||
// Ask the node to join the selected channel and system
|
||||
await requestNodeJoinSystem(openSocket, system.name, channelToJoin.id);
|
||||
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, 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
|
||||
@@ -59,6 +80,17 @@ export async function execute(nodeIo, interaction) {
|
||||
// Check each open socket to see if the node has the requested system
|
||||
await Promise.all(openSockets.map(async openSocket => {
|
||||
openSocket = await nodeIo.sockets.sockets.get(openSocket);
|
||||
// Check if the node has an existing open client (meaning the radio is already being listened to)
|
||||
const hasOpenClient = await checkIfNodeHasOpenDiscordClient(openSocket);
|
||||
if (hasOpenClient) {
|
||||
let currentSystem = await getNodeCurrentListeningSystem(openSocket);
|
||||
if (currentSystem != system.name) {
|
||||
console.log("Node is listening to a different system than requested", openSocket.node.name);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Check if the bot has an open voice connection in the requested server already
|
||||
const connected = await checkIfNodeIsConnectedToVC(nodeIo, interaction.guild.id, openSocket.node.nuid);
|
||||
console.log("Connected:", connected);
|
||||
if (!connected) {
|
||||
@@ -67,6 +99,7 @@ export async function execute(nodeIo, interaction) {
|
||||
availableNodes.push(openSocket);
|
||||
}
|
||||
}
|
||||
|
||||
}));
|
||||
|
||||
console.log("Availble nodes:", availableNodes.map(socket => socket.node.name));
|
||||
@@ -74,7 +107,7 @@ export async function execute(nodeIo, interaction) {
|
||||
// If there are no available nodes, let the user know there are none available
|
||||
if (availableNodes.length == 0) {
|
||||
// There are no nodes availble for the requested system
|
||||
return await interaction.editReply("The selected system has no available nodes");
|
||||
return await interaction.editReply(`<@${interaction.member.id}>, the selected system has no available nodes`);
|
||||
} else if (availableNodes.length == 1) {
|
||||
// There is only one node available for the requested system
|
||||
// Request the node to join
|
||||
@@ -94,7 +127,7 @@ export async function execute(nodeIo, interaction) {
|
||||
|
||||
// Reply to the user with the button prompts
|
||||
const response = await interaction.editReply({
|
||||
content: "Please select the Node you would like to join with this system",
|
||||
content: `<@${interaction.member.id}>, Please select the Node you would like to join with this system`,
|
||||
components: [actionRow]
|
||||
});
|
||||
|
||||
|
||||
56
server/discordBot/commands/leave.mjs
Normal file
56
server/discordBot/commands/leave.mjs
Normal file
@@ -0,0 +1,56 @@
|
||||
import { SlashCommandBuilder } from 'discord.js';
|
||||
import { requestBotLeaveServer, getSocketIdByNuid } from '../../modules/socketServerWrappers.mjs';
|
||||
import { checkOnlineBotsInGuild } from '../modules/wrappers.mjs'
|
||||
|
||||
// Exporting data property
|
||||
export const data = new SlashCommandBuilder()
|
||||
.setName('leave')
|
||||
.setDescription('Disconnect a bot from the server')
|
||||
.addStringOption(system =>
|
||||
system.setName('bot')
|
||||
.setDescription('The bot you would like to disconnect')
|
||||
.setRequired(true)
|
||||
.setAutocomplete(true));;
|
||||
|
||||
// Exporting other properties
|
||||
export const example = "/leave *{Bot Name}*";
|
||||
export const deferInitialReply = true;
|
||||
|
||||
/**
|
||||
* Function to give the user auto-reply suggestions
|
||||
* @param {any} nodeIo The nodeIO server for manipulation of sockets
|
||||
* @param {any} interaction The interaction object
|
||||
*/
|
||||
export async function autocomplete(nodeIo, interaction) {
|
||||
const focusedValue = interaction.options.getFocused();
|
||||
const choices = (await checkOnlineBotsInGuild(nodeIo, interaction.guild.id));
|
||||
|
||||
console.log(choices);
|
||||
|
||||
const filtered = choices.filter(choice => choice.name.startsWith(focusedValue)).map(choice => choice = {name: choice.name, value: choice.nuid});
|
||||
|
||||
console.log(focusedValue, choices, filtered);
|
||||
|
||||
await interaction.respond(filtered);
|
||||
}
|
||||
|
||||
/**
|
||||
* The function to run when the command is called by a discord user
|
||||
* @param {any} nodeIo The nodeIO server for manipulation of sockets
|
||||
* @param {any} interaction The interaction object
|
||||
*/
|
||||
export async function execute(nodeIo, interaction) {
|
||||
try {
|
||||
// Get the requested bot
|
||||
const selectedNode = interaction.options.getString('bot');
|
||||
const socket = await getSocketIdByNuid(nodeIo, selectedNode);
|
||||
console.log("All open sockets:", socket, selectedNode);
|
||||
await requestBotLeaveServer(socket, interaction.guild.id);
|
||||
//await interaction.reply(`**Online Sockets: '${sockets}'**`);
|
||||
await interaction.editReply(`Ok <@${interaction.member.id}>, the bot is leaving shortly`);
|
||||
//await interaction.channel.send('**Pong.**');
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
// await interaction.reply(err.toString());
|
||||
}
|
||||
}
|
||||
@@ -1,16 +1,35 @@
|
||||
import { SlashCommandBuilder } from 'discord.js';
|
||||
|
||||
// Exporting data property
|
||||
// Exporting data property that contains the command structure for discord including any params
|
||||
export const data = new SlashCommandBuilder()
|
||||
.setName('ping')
|
||||
.setDescription('Replies with your input!');
|
||||
|
||||
// Exporting other properties
|
||||
export const example = "/ping";
|
||||
export const deferInitialReply = false;
|
||||
export const example = "/ping"; // An example of how the command would be run in discord chat, this will be used for the help command
|
||||
export const deferInitialReply = false; // If we the initial reply in discord should be deferred. This gives extra time to respond, however the method of replying is different.
|
||||
|
||||
// Exporting execute function
|
||||
export async function execute(nodeIo, interaction) {
|
||||
/**
|
||||
* Function to give the user auto-reply suggestions
|
||||
* @param {any} nodeIo The nodeIO server for manipulation of sockets
|
||||
* @param {any} interaction The interaction object
|
||||
*/
|
||||
/*
|
||||
export async function autocomplete(nodeIo, interaction) {
|
||||
const focusedValue = interaction.options.getFocused();
|
||||
const choices = [];
|
||||
const filtered = choices.filter(choice => choice.name.startsWith(focusedValue));
|
||||
console.log(focusedValue, choices, filtered);
|
||||
await interaction.respond(filtered);
|
||||
}
|
||||
*/
|
||||
|
||||
/**
|
||||
* The function to run when the command is called by a discord user
|
||||
* @param {any} nodeIo The nodeIO server for manipulation of sockets
|
||||
* @param {any} interaction The interaction object
|
||||
*/
|
||||
export const execute = async (nodeIo, interaction) => {
|
||||
try {
|
||||
const sockets = await nodeIo.allSockets();
|
||||
console.log("All open sockets: ",sockets);
|
||||
|
||||
35
server/discordBot/commands/update.mjs
Normal file
35
server/discordBot/commands/update.mjs
Normal file
@@ -0,0 +1,35 @@
|
||||
import { SlashCommandBuilder } from 'discord.js';
|
||||
import { requestNodeUpdate } from '../../modules/socketServerWrappers.mjs';
|
||||
|
||||
// Exporting data property that contains the command structure for discord including any params
|
||||
export const data = new SlashCommandBuilder()
|
||||
.setName('update')
|
||||
.setDescription('Updates all nodes currently logged on');
|
||||
|
||||
// Exporting other properties
|
||||
export const example = "/update"; // An example of how the command would be run in discord chat, this will be used for the help command
|
||||
export const deferInitialReply = false; // If we the initial reply in discord should be deferred. This gives extra time to respond, however the method of replying is different.
|
||||
|
||||
/**
|
||||
* The function to run when the command is called by a discord user
|
||||
* @param {any} nodeIo The nodeIO server for manipulation of sockets
|
||||
* @param {any} interaction The interaction object
|
||||
*/
|
||||
export const execute = async (nodeIo, interaction) => {
|
||||
try {
|
||||
const openSockets = [...await nodeIo.allSockets()]; // TODO - Filter the returned nodes to only nodes that have the radio capability
|
||||
console.log("All open sockets: ", openSockets);
|
||||
|
||||
// Check each open socket to see if the node has the requested system
|
||||
await Promise.all(openSockets.map(openSocket => {
|
||||
openSocket = nodeIo.sockets.sockets.get(openSocket);
|
||||
requestNodeUpdate(openSocket);
|
||||
}));
|
||||
//await interaction.reply(`**Online Sockets: '${sockets}'**`);
|
||||
await interaction.reply('All nodes have been requested to update');
|
||||
//await interaction.channel.send('**Pong.**');
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
// await interaction.reply(err.toString());
|
||||
}
|
||||
}
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -9,7 +9,7 @@ export async function execute(nodeIo, interaction) {
|
||||
// Execute autocomplete if the user is checking autocomplete
|
||||
if (interaction.isAutocomplete()) {
|
||||
console.log("Running autocomplete for command: ", command.data.name);
|
||||
return await command.autocomplete(interaction);
|
||||
return await command.autocomplete(nodeIo, interaction);
|
||||
}
|
||||
|
||||
// Check if the interaction is a command
|
||||
|
||||
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;
|
||||
}
|
||||
@@ -66,7 +68,6 @@ export const nodeUpdateWrapper = async (nodeData) => {
|
||||
* Wrapper to update the systems from the nearbySystems object passed from clients
|
||||
* @param {string} nuid The NUID of the node that sent the update
|
||||
* @param {object} nearbySystems The nearby systems object passed from the node to be updated
|
||||
* @returns {any}
|
||||
*/
|
||||
export const nearbySystemsUpdateWraper = async (nuid, nearbySystems) => {
|
||||
console.log("System updates sent by node: ", nuid, nearbySystems);
|
||||
@@ -142,9 +143,12 @@ export const nearbySystemsUpdateWraper = async (nuid, nearbySystems) => {
|
||||
* @param {string} nuid The NUID to find within the open sockets
|
||||
* @returns {string|null} Will return the open socket ID or NULL
|
||||
*/
|
||||
const getSocketIdByNuid = async (nodeIo, nuid) => {
|
||||
for (const openSocket in await nodeIo.allSockets()) {
|
||||
if (openSockets[openSocket] == nuid)
|
||||
export const getSocketIdByNuid = async (nodeIo, nuid) => {
|
||||
const openSockets = await nodeIo.allSockets();
|
||||
for (const openSocketId of openSockets) {
|
||||
console.log(openSockets)
|
||||
const openSocket = await nodeIo.sockets.sockets.get(openSocketId);
|
||||
if (openSocket.node.nuid == nuid)
|
||||
return openSocket;
|
||||
}
|
||||
return null;
|
||||
@@ -152,9 +156,9 @@ const getSocketIdByNuid = async (nodeIo, nuid) => {
|
||||
|
||||
/**
|
||||
* Get all nodes that are connected to a voice channel
|
||||
* @param {any} nodeIo
|
||||
* @param {any} guildId The guild ID string for the guild we are looking in
|
||||
* @returns {any}
|
||||
* @param {any} nodeIo The nodeIo object that contains the IO server
|
||||
* @param {string} guildId The guild ID string for the guild we are looking in
|
||||
* @returns {Array} The sockets connected to VC in a given server
|
||||
*/
|
||||
export const getAllSocketsConnectedToVC = async (nodeIo, guildId) => {
|
||||
// Get all open socket nodes
|
||||
@@ -167,7 +171,7 @@ export const getAllSocketsConnectedToVC = async (nodeIo, guildId) => {
|
||||
await new Promise((res) => {
|
||||
openSocket.emit('node-check-connected-status', guildId, (status) => {
|
||||
if (status) {
|
||||
console.log("Socket is connected to VC:", openSocket.node.name);
|
||||
console.log("Socket is connected to VC:", openSocket.node.name, status);
|
||||
socketsConnectedToVC.push(openSocket);
|
||||
} else {
|
||||
console.log("Socket is NOT connected to VC:", openSocket.node.name);
|
||||
@@ -180,11 +184,56 @@ export const getAllSocketsConnectedToVC = async (nodeIo, guildId) => {
|
||||
return socketsConnectedToVC;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Check if the given node has an open discord client
|
||||
* @param {any} openSocket The open socket connection with the node to check
|
||||
* @returns {boolean} If the given node has an open discord client or not
|
||||
*/
|
||||
export const checkIfNodeHasOpenDiscordClient = async (openSocket) => {
|
||||
// Check the open socket to see if the node has an open discord client
|
||||
let hasOpenDiscordClient = false;
|
||||
await new Promise((res) => {
|
||||
openSocket.emit('node-check-discord-open-client', (status) => {
|
||||
if (status) {
|
||||
console.log("Socket has an open discord client:", openSocket.node.name, status);
|
||||
hasOpenDiscordClient = true;
|
||||
} else {
|
||||
console.log("Socket does NOT have an open discord client:", openSocket.node.name);
|
||||
}
|
||||
res();
|
||||
})
|
||||
});
|
||||
|
||||
return hasOpenDiscordClient;
|
||||
}
|
||||
|
||||
export const getNodeCurrentListeningSystem = async (openSocket) => {
|
||||
const hasOpenClient = checkIfNodeHasOpenDiscordClient(openSocket);
|
||||
if (!hasOpenClient) return undefined;
|
||||
|
||||
// check what system the socket is listening to
|
||||
let currentSystem = undefined;
|
||||
await new Promise((res) => {
|
||||
openSocket.emit('node-check-current-system', (system) => {
|
||||
if (system) {
|
||||
console.log("Socket is listening to system:", openSocket.node.name, system);
|
||||
currentSystem = system;
|
||||
} else {
|
||||
console.log("Socket is not currently listening to a system:", openSocket.node.name);
|
||||
}
|
||||
res();
|
||||
})
|
||||
});
|
||||
|
||||
return currentSystem;
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrapper to check if the given NUID is connected to a VC
|
||||
* @param {any} nodeIo The nodeIo object that contains the IO server
|
||||
* @param {any} nuid The NUID string that we would like to find in the open socket connections
|
||||
* @returns {any}
|
||||
* @param {string} nuid The NUID string that we would like to find in the open socket connections
|
||||
* @returns {boolean} If the node is connected to VC in the given server
|
||||
*/
|
||||
export const checkIfNodeIsConnectedToVC = async (nodeIo, guildId, nuid) => {
|
||||
const socketsConnectedToVC = await getAllSocketsConnectedToVC(nodeIo, guildId);
|
||||
@@ -196,17 +245,43 @@ export const checkIfNodeIsConnectedToVC = async (nodeIo, guildId, nuid) => {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the discord username from a given socket
|
||||
* @param {any} socket The socket object of the node to check the username of
|
||||
* * @param {string} guildId The guild ID to check the username in
|
||||
* @returns {string} The username of the bot in the requested server
|
||||
*/
|
||||
export const getNodeDiscordUsername = async (socket, guildId) => {
|
||||
return await new Promise((res) => {
|
||||
socket.emit('node-get-discord-username', guildId, (username) => {
|
||||
res(username);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 {any} discordChanelId The Discord channel ID to join the listening bot to
|
||||
* @returns {any}
|
||||
* @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
|
||||
}
|
||||
@@ -214,6 +289,27 @@ export const requestNodeJoinSystem = async (socket, systemName, discordChanelId)
|
||||
await sendNodeCommand(socket, "node-join", joinData);
|
||||
}
|
||||
|
||||
export const requestBotLeave = async () => {
|
||||
|
||||
/**
|
||||
* Request a given socket node to leave VC in a given server
|
||||
* @param {any} socket The socket object of the node the request should be sent to
|
||||
* @param {string} guildId The guild ID to disconnect the socket node from
|
||||
*/
|
||||
export const requestBotLeaveServer = async (socket, guildId) => {
|
||||
// Send the command to the node
|
||||
await sendNodeCommand(socket, "node-leave", guildId);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Requset a given socket node to update themselves
|
||||
* @param {any} socket The socket object of the node to request to update
|
||||
*/
|
||||
export const requestNodeUpdate = async (socket) => {
|
||||
await sendNodeCommand(socket, 'node-update', (status) => {
|
||||
if (status) {
|
||||
console.log("Node is out of date, updating now", socket.node.name);
|
||||
} else {
|
||||
console.log("Node is up to date", socket.node.name);
|
||||
}
|
||||
});
|
||||
}
|
||||
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