Compare commits

..

140 Commits

Author SHA1 Message Date
Logan Cusano
a4d07db766 #16 Update variable name
Some checks failed
DRB Server Build / drb_server_build (push) Successful in 36s
DRB Tests / drb_mocha_tests (push) Failing after 1m13s
2024-05-11 14:40:10 -04:00
Logan Cusano
03c940e07c Update PDAB handler
- Move PDAB socket server startup to function
- Updated join server wrapper to start the server before opening the python app
2024-05-05 23:39:05 -04:00
Logan Cusano
fee40dd609 #16 Updated join
-  After getting all available IDs, it now checks if there are IDs. If not telling the user
2024-05-05 22:00:04 -04:00
Logan Cusano
5671503594 Omitting OP25 update in the post update for now 2024-05-05 18:29:06 -04:00
Logan Cusano
0fd511cfaf Adding handlers for discord presense 2024-05-05 18:24:59 -04:00
Logan Cusano
9d0aa0191f Resolve bug when selecting ID on join 2024-05-05 18:24:11 -04:00
Logan Cusano
9ce8928d82 Revert base image and add global gyp install
Some checks failed
DRB Server Build / drb_server_build (push) Successful in 24s
DRB Tests / drb_mocha_tests (push) Failing after 59s
2024-04-29 23:25:34 -04:00
Logan Cusano
abadcf5cb5 Change base image of the dockerfile
Some checks failed
DRB Server Build / drb_server_build (push) Failing after 9s
DRB Tests / drb_mocha_tests (push) Failing after 1m38s
2024-04-29 22:16:17 -04:00
Logan Cusano
4b18df9e2c Fix server build name and removed clean and run code, just update
Some checks failed
DRB Server Build / drb_server_build (push) Successful in 6s
DRB Tests / drb_mocha_tests (push) Failing after 58s
2024-04-29 01:28:18 -04:00
Logan Cusano
26a8d8a172 Update build to clean and run the container
All checks were successful
DRB Server Build / drb_mocha_tests (push) Successful in 6s
2024-04-29 01:20:50 -04:00
Logan Cusano
26f3493c8f Remove node setup from build action
Some checks failed
DRB Tests / drb_mocha_tests (push) Failing after 58s
DRB Server Build / drb_mocha_tests (push) Successful in 8s
2024-04-29 01:17:14 -04:00
Logan Cusano
fd261ef015 Missed addition
Some checks failed
DRB Server Build / drb_mocha_tests (push) Failing after 7s
DRB Tests / drb_mocha_tests (push) Failing after 55s
- Server build action
2024-04-29 01:13:00 -04:00
Logan Cusano
91201c3527 Update tests
- Update test names
- Added new action to build the latest server code on the server it's running on
- Removed unnecessary vars from makefile
2024-04-29 01:12:30 -04:00
Logan Cusano
dfb4c6afa1 Remove build from tests 2024-04-28 22:52:39 -04:00
Logan Cusano
3b8e70208a Update build tests
Some checks failed
DRB Build Tests / build (push) Failing after 5s
DRB Build Tests / drb_build_and_test (push) Failing after 53s
Start docker in the test
2024-04-28 22:22:01 -04:00
Logan Cusano
99b60bf02c Update build tests
Some checks failed
DRB Build Tests / build (push) Failing after 5s
DRB Build Tests / drb_build_and_test (push) Failing after 58s
2024-04-28 22:19:09 -04:00
Logan Cusano
831317b9f0 Update build test
Some checks failed
DRB Build Tests / build (push) Failing after 7s
DRB Build Tests / drb_build_and_test (push) Failing after 56s
- Switch to installing docker io
2024-04-28 20:13:25 -04:00
Logan Cusano
2477b10900 Add docker install step to build test
Some checks failed
DRB Build Tests / build (push) Failing after 1m17s
DRB Build Tests / drb_build_and_test (push) Failing after 57s
2024-04-28 06:08:57 -04:00
Logan Cusano
67c29f1d72 Remove gitea debug after potential resolution
Some checks failed
DRB Build Tests / build (push) Failing after 8s
DRB Build Tests / drb_build_and_test (push) Failing after 1m1s
2024-04-28 06:07:06 -04:00
Logan Cusano
91ed4fb1dc gitea debug 2
Some checks failed
DRB Build Tests / build (push) Failing after 1m3s
DRB Build Tests / drb_build_and_test (push) Failing after 1m31s
2024-04-28 05:50:52 -04:00
Logan Cusano
8ea02d1c0b gitea debug
Some checks failed
DRB Build Tests / build (push) Failing after 16s
DRB Build Tests / drb_build_and_test (push) Has been cancelled
2024-04-28 05:49:57 -04:00
Logan Cusano
b15ac7c973 Added working directory to build step
Some checks failed
DRB Build Tests / build (push) Failing after 14s
DRB Build Tests / drb_build_and_test (push) Failing after 52s
2024-04-28 05:32:54 -04:00
Logan Cusano
c2c90019d7 Update tests
Some checks failed
DRB Build Tests / build (push) Failing after 32s
DRB Build Tests / drb_build_and_test (push) Failing after 58s
2024-04-28 05:30:13 -04:00
Logan Cusano
686ddc8a0f Gitea runner debug
Some checks failed
DRB Build Tests / build (push) Failing after 25s
DRB Build Tests / drb_build_and_test (push) Failing after 41s
2024-04-28 05:22:20 -04:00
Logan Cusano
463ccc1bd5 Setup docker in the test
Some checks failed
DRB Build Tests / build (push) Failing after 1m13s
DRB Build Tests / drb_build_and_test (push) Failing after 59s
2024-04-28 05:17:08 -04:00
Logan Cusano
14a2b4a1b9 Update tests
Some checks failed
DRB Build Tests / build (push) Failing after 41s
DRB Build Tests / drb_build_and_test (push) Failing after 43s
- Output docker ps before curl
2024-04-28 05:13:40 -04:00
Logan Cusano
0c1f6cd867 Update dockerfile
Some checks failed
DRB Build Tests / build (push) Failing after 34s
DRB Build Tests / drb_build_and_test (push) Failing after 51s
- Use the startup command used in the terminal
2024-04-28 05:11:49 -04:00
Logan Cusano
7175487d77 Update makefile
Some checks failed
DRB Build Tests / build (push) Failing after 31s
DRB Build Tests / drb_build_and_test (push) Failing after 43s
- Run docker detached
2024-04-28 05:06:50 -04:00
Logan Cusano
f0eac45955 Replace space with tab in makefile
Some checks failed
DRB Build Tests / build (push) Failing after 12s
DRB Build Tests / drb_build_and_test (push) Has been cancelled
2024-04-28 05:05:43 -04:00
Logan Cusano
c81dce99e2 Update makefile with clean step
Some checks failed
DRB Build Tests / build (push) Failing after 7s
DRB Build Tests / drb_build_and_test (push) Has been cancelled
2024-04-28 05:04:52 -04:00
Logan Cusano
38cb1054e0 Add DISCORD_TOKEN var to tests and makefile
Some checks failed
DRB Build Tests / build (push) Failing after 12s
DRB Build Tests / drb_build_and_test (push) Failing after 59s
2024-04-28 05:00:10 -04:00
Logan Cusano
12441c5c6d Update makefile
Some checks failed
DRB Build Tests / build (push) Failing after 1m38s
DRB Build Tests / drb_build_and_test (push) Failing after 56s
- Mistake in the docker build path
2024-04-28 04:54:53 -04:00
Logan Cusano
1c1b071bd7 Update makefile
Some checks failed
DRB Build Tests / build (push) Failing after 9s
DRB Build Tests / drb_build_and_test (push) Has been cancelled
- Removed all spaces for tabs
2024-04-28 04:53:55 -04:00
Logan Cusano
961c5c19e2 Update drb tests
Some checks failed
DRB Build Tests / build (push) Failing after 7s
DRB Build Tests / drb_build_and_test (push) Failing after 56s
- Update working directory for make
2024-04-28 04:49:56 -04:00
Logan Cusano
49e52d8944 Add makefile for server and related action build test
Some checks failed
DRB Build Tests / build (push) Failing after 7s
DRB Build Tests / drb_build_and_test (push) Failing after 58s
2024-04-28 04:46:08 -04:00
Logan Cusano
63ccfa70d3 Add server docker file and related action tests
Some checks failed
DRB Build Tests / build (push) Failing after 15s
DRB Build Tests / drb_build_and_test (push) Failing after 1m2s
2024-04-28 04:34:15 -04:00
Logan Cusano
4bb8038a1d Update variables in test action
Some checks failed
DRB Build Tests / drb_build_and_test (push) Failing after 57s
2024-04-28 03:26:06 -04:00
Logan Cusano
a353b9adbb Improvements to tests and from testing
- Moved OP25 start outside of PDAB start callback
- Added more logging
- Set the pdabProcess variable to false when closing the discord client
- Update test variable names
- Added new WIP pdabWrappers tests to test the full wrappers as the client would
2024-04-28 03:21:03 -04:00
Logan Cusano
bf69e93e29 Improve subprocess handler
- Will now output the subprocess console when in dev mode
2024-04-28 03:18:13 -04:00
Logan Cusano
6cae18e70c Update PDAB tests
Some checks failed
DRB Build Tests / drb_build_and_test (push) Failing after 58s
- These new tests will likely not run in actions due to pyaudio
2024-04-27 23:34:30 -04:00
Logan Cusano
e7229322e4 Update pdab handler
- Updated socket close
- Took server outside of the init (server is always running)
- initDiscordBotClient just starts the PDAB process
2024-04-27 23:33:53 -04:00
Logan Cusano
34aa5d17dc Updates to setup from testing
All checks were successful
DRB Build Tests / drb_build_and_test (push) Successful in 52s
2024-04-26 23:46:13 -04:00
Logan Cusano
565fd5af37 Updated join voice log
All checks were successful
DRB Build Tests / drb_build_and_test (push) Successful in 1m27s
2024-04-26 21:30:27 -04:00
Logan Cusano
f04154d361 Remove client build test for now 2024-04-26 21:29:10 -04:00
Logan Cusano
383663e980 #30 Update discordBot
- Added voice states intent
2024-04-26 21:28:49 -04:00
Logan Cusano
7b21d4601f #10 Updated setup
Some checks failed
DRB Build Tests / drb_test_setup (push) Failing after 2m34s
DRB Build Tests / drb_build_and_test (push) Successful in 47s
- Re-formatted apt deps. for readability with many packages
- Added libasound-dev to try and build pyaudio
2024-04-21 15:06:59 -04:00
Logan Cusano
d0c2fcc8eb #10 #33 Update client setup and setup test
Some checks failed
DRB Build Tests / drb_test_setup (push) Failing after 2m37s
DRB Build Tests / drb_build_and_test (push) Successful in 54s
- Added a step to run the service after setting it up
- Removed `--system` option from pip3
2024-04-21 14:57:17 -04:00
Logan Cusano
9ba90af464 #10 Update setup.sh
Some checks failed
DRB Build Tests / drb_build_and_test (push) Successful in 52s
DRB Build Tests / drb_test_setup (push) Failing after 2m15s
- Remove systemctl stops as they are not needed and impact testing
2024-04-21 02:43:43 -04:00
Logan Cusano
6e8681e52d #10 Update client setup test
Some checks failed
DRB Build Tests / drb_test_setup (push) Failing after 2m1s
DRB Build Tests / drb_build_and_test (push) Successful in 1m2s
- Updated test name
- Added user pi in test environment
2024-04-21 02:34:07 -04:00
Logan Cusano
8ba1ed36d8 #10 Update actions and setup script
Some checks failed
DRB Build Tests / test_setup (push) Failing after 2m5s
DRB Build Tests / drb_build_and_test (push) Successful in 54s
- Removed the step to run the DRB client from socket tests
- Added a testing switch to the install script to return all preset values for all user input
- Added test switch to client setup action step
2024-04-21 02:28:37 -04:00
Logan Cusano
3074e88963 #10 Updated client setup test action
Some checks failed
DRB Build Tests / test_setup (push) Failing after 2m27s
DRB Build Tests / drb_build_and_test (push) Failing after 41s
- Removed comment and fixed name typo
2024-04-21 02:15:38 -04:00
Logan Cusano
737b493b23 #10 Updated client setup test action
Some checks failed
DRB Build Tests / test_setup (push) Failing after 8s
DRB Build Tests / drb_build_and_test (push) Failing after 56s
- Added working dir for setup test
2024-04-21 02:14:32 -04:00
Logan Cusano
539dbd9518 #10 Removing makefile
Some checks failed
DRB Build Tests / test_setup (push) Failing after 9s
DRB Build Tests / drb_build_and_test (push) Has been cancelled
The makefile for the client is not as useful as a install script

- Updated test action to install client deps.
- Added new action to specifically test the install script for client
2024-04-21 02:13:35 -04:00
Logan Cusano
59bfdbe143 #10 Updating build test
Some checks failed
DRB Build Tests / drb_build_and_test (push) Failing after 2m17s
- Adding the pi user in the test environment to match raspbian
2024-04-21 01:46:34 -04:00
Logan Cusano
560ed401cf #10 Updated Makefile
Some checks failed
DRB Build Tests / drb_build_and_test (push) Failing after 2m39s
- Updated sed command in setup_pulse_audio
2024-04-21 01:38:45 -04:00
Logan Cusano
7b91667414 #10 Update Makefiles
Some checks failed
DRB Build Tests / drb_build_and_test (push) Failing after 2m26s
- Removed the client_dir var as it's not needed
2024-04-21 01:31:04 -04:00
Logan Cusano
fd9b6d9d1c #10 Updating build test to remove threads and sudo
Some checks failed
DRB Build Tests / drb_build_and_test (push) Failing after 29s
2024-04-21 01:26:44 -04:00
Logan Cusano
3aae427249 #10 Updating makefile to include sudo
Some checks failed
DRB Build Tests / drb_build_and_test (push) Has been cancelled
2024-04-21 01:24:24 -04:00
Logan Cusano
bc4c8f72d0 #10 Update makefile
Some checks failed
DRB Build Tests / drb_build_and_test (push) Failing after 2m27s
- Working on issue running OP25 install script
2024-04-21 01:14:40 -04:00
Logan Cusano
cc4e5e762d #10 Added sudo to makefile test
Some checks failed
DRB Build Tests / drb_build_and_test (push) Failing after 2m49s
2024-04-21 01:07:51 -04:00
Logan Cusano
31cedb2e9c #10 Combining tests
Some checks failed
DRB Build Tests / drb_build_and_test (push) Failing after 2m16s
- Combined all tests
2024-04-21 01:01:32 -04:00
Logan Cusano
61a616ec6b #10 Updates
Some checks failed
DRB Client Makefile Build Test / build (push) Failing after 54s
DRB Socket Communication Tests / test (push) Successful in 1m58s
- Added check for updates at boot
- Updated post update script to update OP25 and PDAB
- Added initial makefile to replace the `setup.sh`
- Added new test to make sure makefile builds
- Renamed socket tests
2024-04-21 00:52:15 -04:00
Logan Cusano
d7b7b04f78 Update PDAB Wrappers
All checks were successful
Run Discord Radio Bot v3 Tests / test (push) Successful in 46s
- Close the socket server when there are no active connections
2024-04-14 23:59:04 -04:00
Logan Cusano
2c9383824e Updated client setup script
- Added pip
- updated branch for op25
- Improved the python deps install
2024-04-14 23:46:43 -04:00
Logan Cusano
cebd316939 Fixed bug if statement
All checks were successful
Run Discord Radio Bot v3 Tests / test (push) Successful in 46s
2024-04-14 20:43:06 -04:00
Logan Cusano
af19db8e17 Add PDAB setup
All checks were successful
Run Discord Radio Bot v3 Tests / test (push) Successful in 53s
2024-04-14 20:41:22 -04:00
Logan Cusano
45b9a62c64 Removed dev comments stopping OP25
All checks were successful
Run Discord Radio Bot v3 Tests / test (push) Successful in 48s
2024-04-14 16:39:24 -04:00
Logan Cusano
8c3164029f Updated typo for pdab_port action var
All checks were successful
Run Discord Radio Bot v3 Tests / test (push) Successful in 48s
2024-04-14 16:03:30 -04:00
Logan Cusano
bec4072837 Merge test actions into one action
Some checks failed
Run Discord Radio Bot v3 Tests / test (push) Failing after 1m17s
2024-04-14 15:58:00 -04:00
Logan Cusano
ac82b0efd0 Removed hardcoded IDs in test 2024-04-14 15:51:31 -04:00
Logan Cusano
854c73cc4e Testing updates
All checks were successful
Client Tests / test (push) Successful in 1m30s
Run Socket Server Tests / test (push) Successful in 1m15s
- Added client tests, started with the pdabHandler interactions
- Updated caps in socket server tests
- Updated pdabHandler with uniform 'guild_id'
- Updated pdabHandler with production check to launch or not launch the python client
-
2024-04-14 15:47:29 -04:00
Logan Cusano
238fe6a254 Updating server workflows
All checks were successful
Run Socket Server Tests / test (push) Successful in 34s
- trying to get mongo container to work
2024-04-07 20:26:47 -04:00
Logan Cusano
96d9c38425 Removed unused imports from tests
Some checks failed
Run Socket Server Tests / test (push) Failing after 1m20s
2024-04-07 19:07:31 -04:00
Logan Cusano
4df3de4d4a Passing local server tests
Some checks failed
Run Socket Server Tests / test (push) Failing after 30s
2024-04-07 19:01:00 -04:00
Logan Cusano
461b449194 Added success emits for server functions 2024-04-07 17:01:51 -04:00
Logan Cusano
757fdfb3b2 Rename tests file to test for mocha 2024-04-07 16:59:51 -04:00
Logan Cusano
9bc80887ce Updated Package 2024-04-07 16:59:16 -04:00
Logan Cusano
0ce0f72ed3 Update Server
- Move socket server start to main server file
- This way tests and the main function can use the same code
2024-04-07 16:58:50 -04:00
Logan Cusano
49ae941e83 Updating tests
Some checks failed
Run Socket Server Tests / test (push) Failing after 33s
- Removed bugged --name param
2024-04-07 16:56:03 -04:00
Logan Cusano
8e73659855 Updating tests
Some checks failed
Run Socket Server Tests / test (push) Failing after 26s
2024-04-07 16:53:54 -04:00
Logan Cusano
0a76804490 Updating tests file to use mongo docker for testing
Some checks failed
Run Socket Server Tests / build (push) Failing after 0s
Run Socket Server Tests / test (push) Failing after 31s
2024-04-07 16:43:35 -04:00
9ad24ca8ec Update .gitea/workflows/server_tests.yaml
Some checks failed
Run Socket Server Tests / test (push) Failing after 30s
Removing deprecated dev set
2024-04-06 14:17:05 -04:00
7676e883a5 Update .gitea/workflows/server_tests.yaml
Some checks failed
Run Socket Server Tests / test (push) Has been cancelled
Setting the node environment to development
2024-04-06 14:16:21 -04:00
86a71d3d6f Update .gitea/workflows/server_tests.yaml
Some checks failed
Run Socket Server Tests / test (push) Failing after 17s
Setting production flag as passed 'dev' var to npm is installing the dev package
2024-04-06 14:14:31 -04:00
1dd53ffc84 Update .gitea/workflows/server_tests.yaml
Some checks failed
Run Socket Server Tests / test (push) Failing after 37s
Installing GYP before dev depends
2024-04-06 13:56:58 -04:00
fc31026304 Update .gitea/workflows/server_tests.yaml
Some checks failed
Run Socket Server Tests / test (push) Failing after 32s
Forgot the working directory
2024-04-06 03:42:34 -04:00
8a4b7685d2 Update .gitea/workflows/server_tests.yaml
Some checks failed
Run Socket Server Tests / test (push) Failing after 7s
Add a new step to remove package-lock.json
2024-04-06 03:41:41 -04:00
Logan Cusano
49c1a1d724 Clear the package-lock.json from in the workflow
Some checks failed
Run Socket Server Tests / test (push) Failing after 0s
2024-04-06 03:39:48 -04:00
Logan Cusano
a423417949 Update versions and install param
Some checks failed
Run Socket Server Tests / test (push) Failing after 25s
2024-04-06 03:36:36 -04:00
Logan Cusano
2fb0bc8920 Update server package.json to use mocha tests
Some checks failed
Run Socket Server Tests / test (push) Failing after 17s
2024-04-06 03:32:18 -04:00
38470bd788 Update .gitea/workflows/server_tests.yaml
Some checks failed
Run Socket Server Tests / test (push) Failing after 19s
Fix node version
2024-04-06 03:27:56 -04:00
8995f8b372 Update .gitea/workflows/server_tests.yaml
Some checks failed
Run Socket Server Tests / test (push) Failing after 8s
Potential fix for not running
2024-04-06 03:26:54 -04:00
a6c26d61da Update .gitea/workflows/server_tests.yaml
Fixing errors in the workflow
2024-04-06 03:25:07 -04:00
ab0f94baf8 Merge pull request 'Implementing-Python-Client_#19' (#32) from Implementing-Python-Client_#19 into master
Reviewed-on: #32
2024-04-06 03:22:18 -04:00
Logan Cusano
62c00eec09 Implementing disconnection of discord client 2024-04-06 01:02:42 -04:00
Logan Cusano
ea63abcb93 Update the subprocess handler to take a CWD 2024-04-06 01:01:36 -04:00
Logan Cusano
1df2027a29 Update test version 2024-04-03 22:57:45 -04:00
Logan Cusano
a36ddd9614 Added extra echo 2024-04-03 03:16:27 -04:00
Logan Cusano
ce4a6e925e Fixing the on for the action 2024-04-03 03:15:22 -04:00
Logan Cusano
8790fb05fb Merge branch 'master' into Implementing-Python-Client_#19 2024-04-03 03:14:10 -04:00
Logan Cusano
0f8dc86dd5 trying v3 2024-04-03 03:11:49 -04:00
4b8771b925 Update .gitea/workflows/server_tests.yaml
Update actions to v4
2024-04-03 03:06:55 -04:00
Logan Cusano
3319a9617e fixes to tests
Some checks failed
Run Socket Server Tests / test (push) Failing after 45s
2024-04-03 02:28:34 -04:00
Logan Cusano
c78ed89707 Initial implementation of python client with socket.io IPC 2024-04-03 02:24:21 -04:00
Logan Cusano
be5943e9d4 Adding basic tests 2024-04-03 02:20:30 -04:00
Logan Cusano
64edc612df #10
- Implemented a new param for launching client processes to wait for the process to close
- Waiting for the bash script to finish before restarting the application
2024-03-24 19:09:17 -04:00
Logan Cusano
ed04e24fc6 #10 Added console output for post-update script 2024-03-24 19:02:49 -04:00
Logan Cusano
3e50f0b2f3 Finalizing #10
- Added a stash command to stash any found changs
- Added a post-update script (needs testing)
2024-03-24 18:55:29 -04:00
Logan Cusano
2754494083 #16 changed where the map was done for leave AC 2024-03-24 09:55:45 -04:00
Logan Cusano
e62be27f74 #16 missed another reference 2024-03-24 03:03:47 -04:00
Logan Cusano
96b32d1241 #16 fixed bug on client when checking username 2024-03-24 03:01:33 -04:00
Logan Cusano
7983a45281 Improve service handler 2024-03-24 02:46:16 -04:00
Logan Cusano
fea7ed2c7f #16
- Server can now choose from IDs in the DB
- Implemented an active system to disable some IDs from being used
2024-03-24 02:45:34 -04:00
Logan Cusano
bee95ed999 #6
- Added a config system to the addons
- Added options in the config for dynamic variables
- Updated example and code to use new config and options
2024-03-10 04:50:16 -04:00
Logan Cusano
7d4f48a446 #6 The bones
- Added a module to load addons
- Added an example module that just extends the socketIO connection to add an console log
2024-03-10 04:03:40 -04:00
Logan Cusano
b209a672c6 Improve the client setup script 2024-03-10 03:22:04 -04:00
Logan Cusano
7983670c81 #18
- When grabbing the device from the CLI, we now add it to a new var
- We then add new var as the device so we can specify the group
- Updated the default UDP port in the audio device for OP25 config
2024-03-10 00:03:30 -05:00
Logan Cusano
73da7ee2f4 #13 Finializing fix
- Updated the regex to double exclude '\'
- Updated extractValue function to return the full result
2024-03-09 17:22:52 -05:00
Logan Cusano
6ab0ec6d6f Finishing #15
- setup script needs to set the permissions on the .env file explicitly
2024-03-09 15:15:16 -05:00
Logan Cusano
84aa4c5aff #15
- Changed the regex expression to not require a value after the key
2024-03-09 15:09:42 -05:00
Logan Cusano
32f827fe5e Fixing a bug in boot
- When booting for the first time, the generated NUID was saved to the wrong key
2024-03-08 23:36:02 -05:00
Logan Cusano
d48d1155cb #10 resolving bug in variable assignment 2024-03-08 22:09:22 -05:00
Logan Cusano
3cb53605c7 Working on #13
- Running the 'pactl' command
- Using regex on the output to find the active running device
2024-03-08 21:58:27 -05:00
Logan Cusano
b7d4cbf46c Adding to the setup script #10
- Change permissions on the whole project dir
- Add an echo for better visibility
2024-03-08 21:55:24 -05:00
Logan Cusano
c46b7c6ea6 Modified the self updater to reset any local changes when updating #10 2024-03-03 22:02:27 -05:00
Logan Cusano
5d2097ecd4 Updated gitignore with the op25 dir we will create 2024-03-03 22:02:00 -05:00
Logan Cusano
7efd0cd4f3 Fixed a bug in server update command #10
- Didn't properly get the open sockets
2024-03-03 21:31:14 -05:00
Logan Cusano
36c0ec8b13 Update DAB to include dynamic settings
- Settings will now get the active device(s) available on Linux (UNTESTED)
- Added CLI handler for interacting with the CLI
- Added wrapper for extracting values with regex
-
2024-03-03 21:29:50 -05:00
Logan Cusano
79574e188d Update bug in the startService script
- Was writing output to a file, thought it would write to actual stderr
2024-03-03 21:27:30 -05:00
Logan Cusano
d0a75dc557 Fixing bug in client boot that ignored 0
- Added check for string '0' and int 0
2024-03-03 20:54:47 -05:00
Logan Cusano
6b156b441a Updating client package with new modules #10 2024-03-03 20:54:00 -05:00
Logan Cusano
956dc89107 Adding functional usage of client self updater #10
- Added update command to the server
- Server can request nodes update
- Nodes have an 'endpoint' for updating
- Fixes to the install script
2024-03-03 20:49:29 -05:00
Logan Cusano
976c44838e Fix bugs in setup script #10 2024-03-03 20:21:50 -05:00
Logan Cusano
1395130d6d Starting to work on instal and update function #10
- Added a new self updater module that will update the git repo and restart the node service
- Added a setup script that will install and setup both OP25 and the DRB node
- Updated service names
2024-03-03 19:41:06 -05:00
Logan Cusano
54a6d544e4 Finalizing #9
- Added service handler for system services
- Control OP25 from service handler
- Generate and save OP25 config file
- Handlers for OP25
2024-03-03 17:25:07 -05:00
Logan Cusano
d253d0aef1 Fix readme 2024-03-03 00:12:32 -05:00
Logan Cusano
35f3f07793 Working on #9
- Can join and leave from voice channels
- Will check to make sure that the bot is in a given system or no system before joining
- Cleaned up the socket client with wrappers
- Added a new module to handle subprocesses for the client
- Beginning workings on OP25 handler
- Added OP25 config object generator with config exporter
2024-03-03 00:10:43 -05:00
Logan Cusano
63ad20b9b3 Updated link in the readme 2024-02-18 20:08:07 -05:00
Logan Cusano
e431db1337 Update the example/ping command
- Added the auto complete function commented out
- Added JSDoc and comments for clarity
2024-02-18 20:06:08 -05:00
Logan Cusano
42784f1852 Functional joining and leaving
- Needs to be tested on multiple servers
- Needs to be tested with multiple nodes
#1 #9
2024-02-18 20:05:10 -05:00
46 changed files with 5173 additions and 243 deletions

View 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

View 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

View 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
View File

@@ -297,3 +297,5 @@ cython_debug/
# Ignore the config dirs
config/
# Ignore the OP25 directory we will create
op25/

View File

@@ -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.

View File

@@ -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

View File

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

Submodule client/discordAudioBot/pdab added at fc6c114473

View 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();
});
};

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

View File

@@ -61,4 +61,17 @@ export const generateUniqueID = () => {
.digest('hex');
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;
};

View 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}`));
}
});
});
};

View 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');
}

View 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}`);
}
};

View File

@@ -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;
}

View File

@@ -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());
}

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

View File

@@ -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
})

View 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;
}
}

View 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

File diff suppressed because it is too large Load Diff

View File

@@ -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
View 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
View File

@@ -0,0 +1 @@
node .

314
client/setup.sh Normal file
View 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

View 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();
});
});

View 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
View 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", "."]

View File

@@ -0,0 +1,7 @@
{
"name": "Addon 1",
"enabled": false,
"options": {
"eventName": "connection"
}
}

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

View File

@@ -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,20 +56,41 @@ 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
const openSockets = [...await nodeIo.allSockets()]; // TODO - Filter the returned nodes to only nodes that have the radio capability
console.log("All open sockets: ", openSockets);
var availableNodes = [];
// 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]
});

View 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());
}
}

View File

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

View 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());
}
}

View File

@@ -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 () => {

View File

@@ -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

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

View 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.`);
});
}
}
}
});
}

View 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();
}
};

View File

@@ -17,22 +17,23 @@ 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', () => {
nodeDisconnectWrapper(socket.id);
});
});
// Startup the node server
server.listen(3000, () => {
console.log('server running at http://localhost:3000');
});

View File

@@ -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

File diff suppressed because it is too large Load Diff

View File

@@ -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",

View File

@@ -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;
nodeIo.serverClient = serverClient;
// Load the addons
loadAddons(nodeIo);

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