195 Commits

Author SHA1 Message Date
Logan Cusano
ca2815ab8f Improve the update script to use the owner of the git repo 2023-08-06 16:49:31 -04:00
Logan Cusano
556697725a Much improved install script for clients #6 2023-08-06 16:40:10 -04:00
Logan Cusano
b448f04aec Commenting out variables not set by the initial config 2023-08-06 16:27:04 -04:00
Logan Cusano
fae8417b2f Add log file location to the example ENV 2023-08-06 00:46:00 -04:00
Logan Cusano
e06cc4762d Using absolute file path for pdab binaries 2023-08-06 00:45:24 -04:00
Logan Cusano
6deba2bad2 Fix bug that used wrong env var for client port 2023-08-05 03:11:59 -04:00
Logan Cusano
e0d1a4a2fe Update client setup
- get user input for client config
- update pulseaudio setup to run for all users
- add op25 installer
2023-08-05 03:11:32 -04:00
1078faa766 Merge pull request '#37 Implement v1 Web Apps' (#41) from #37-implement-webapps into master
Reviewed-on: #41
2023-08-04 23:46:46 -04:00
Logan Cusano
75580c0547 Fix bug in httprequests, hostname was overwritten 2023-08-04 23:44:28 -04:00
Logan Cusano
880f1ccb01 Implemented v1 admin dashboard 2023-08-04 23:21:35 -04:00
Logan Cusano
76c4d002a0 Update to filter presets function in utils 2023-08-04 22:10:28 -04:00
Logan Cusano
2260deee01 Added filter presets function to utils and code formatted 2023-08-04 22:10:09 -04:00
Logan Cusano
8a0baa5bc9 Small tweaks
- Update local webUI to refresh when joining server to clear modal
- Updated client webUI page title
- Re-added join modal to node page (client & server)
- updated join modal to use node.nearbySystems
2023-07-22 15:01:48 -04:00
Logan Cusano
ec091c0017 Only concat the stored toasts with new toast when there are stored toasts 2023-07-22 14:46:51 -04:00
Logan Cusano
a5996ccfc0 Added page titles to server webUI 2023-07-22 04:21:06 -04:00
Logan Cusano
3b248e36ec Merge branches '#37-implement-webapps' and '#37-implement-webapps' of https://git.vpn.cusano.net/logan/DRB-CnC into #37-implement-webapps 2023-07-22 03:59:20 -04:00
Logan Cusano
abdb725964 Semi functional client webUI
- can update client info for sure
- Working on notifications now
2023-07-22 03:57:50 -04:00
Logan Cusano
167f87128e Check if the presets exist when going to get them
- Return empty object if no preset file is found
2023-07-22 03:57:50 -04:00
Logan Cusano
bc09840dda Ignore local radio presets 2023-07-22 03:57:50 -04:00
Logan Cusano
c680c8fb2c Fixed bug when editing node systems
- Name defaulted to new system name
2023-07-22 03:57:50 -04:00
Logan Cusano
4ceb71bd84 Remove specific presets and left an example file if needed for clients 2023-07-22 03:57:50 -04:00
Logan Cusano
4b86621626 Finalizing Server webUI
Still needed:
- Way to update clients' versions
- Way to delete nodes
- working dashboard
- working search function
2023-07-22 03:57:50 -04:00
Logan Cusano
d847aa4fc7 Add new node if node tries to check in with an ID not in the DB 2023-07-22 03:57:50 -04:00
Logan Cusano
9ff87403b2 Update request node checkin in update node 2023-07-22 03:57:50 -04:00
Logan Cusano
cf9deb4841 Remove unnecessary online param from webUI update node 2023-07-22 03:57:50 -04:00
Logan Cusano
58b4b7ff40 Update design on navbar and sidebar 2023-07-22 03:57:50 -04:00
Logan Cusano
6b4ffc88b3 Implemented functional method to add a new system to a new through web app 2023-07-22 03:57:50 -04:00
Logan Cusano
0f114066a6 Implement functioning method to update systems on web app 2023-07-22 03:57:50 -04:00
Logan Cusano
648782658c Update API and add webapp saving 2023-07-22 03:57:50 -04:00
Logan Cusano
d7ea6bbbd4 Updated NodeCard Design 2023-07-22 03:57:50 -04:00
Logan Cusano
6ffa12911a Update toast creator to display proper date string 2023-07-22 03:57:50 -04:00
Logan Cusano
61d7b69c10 Only show heartbeat toast once HTTP request is complete 2023-07-22 03:57:50 -04:00
Logan Cusano
c14316933b Update heartbeat function location and name 2023-07-22 03:57:50 -04:00
Logan Cusano
f55361575e #37 Working Joining and Leaving 2023-07-22 03:57:50 -04:00
Logan Cusano
c5f7cc1da6 Additional changes for #37
- Updating side bar
- Updating nav bar
- Adding node details page
- Adding controller page
- Updating routes
2023-07-22 03:57:50 -04:00
Logan Cusano
02854fb783 Initial bones for #37 2023-07-22 03:57:50 -04:00
Logan Cusano
4a54be7e51 Update jsDoc in utils 2023-07-22 03:57:50 -04:00
Logan Cusano
cfeea57744 Update jsDoc in nodeController 2023-07-22 03:57:50 -04:00
Logan Cusano
0a8dc75a93 Update adminController to use join/leave command wrappers 2023-07-22 03:57:50 -04:00
Logan Cusano
0426f5eb27 Add jsDoc to leaveServerWrapper 2023-07-22 03:57:50 -04:00
Logan Cusano
d4b974f81b Update join command to accept a specific node ID 2023-07-22 03:57:50 -04:00
Logan Cusano
d05c266f75 Added basic info to readmes 2023-07-22 03:51:46 -04:00
Logan Cusano
57fa6be110 Update Readmes (sort of) 2023-07-22 03:34:37 -04:00
Logan Cusano
f5d58d45da Semi functional client webUI
- can update client info for sure
- Working on notifications now
2023-07-22 03:27:39 -04:00
Logan Cusano
62c0504028 Check if the presets exist when going to get them
- Return empty object if no preset file is found
2023-07-22 01:47:09 -04:00
Logan Cusano
5dd27f0bed Ignore local radio presets 2023-07-22 01:46:19 -04:00
Logan Cusano
e0bae665ed Fixed bug when editing node systems
- Name defaulted to new system name
2023-07-22 01:45:32 -04:00
Logan Cusano
598c802b28 Remove specific presets and left an example file if needed for clients 2023-07-22 01:29:47 -04:00
Logan Cusano
ace762fc76 Finalizing Server webUI
Still needed:
- Way to update clients' versions
- Way to delete nodes
- working dashboard
- working search function
2023-07-22 01:19:54 -04:00
Logan Cusano
75156d059e Add new node if node tries to check in with an ID not in the DB 2023-07-21 18:51:55 -04:00
Logan Cusano
abb833840a Update request node checkin in update node 2023-07-21 18:36:21 -04:00
Logan Cusano
11c8a149bb Remove unnecessary online param from webUI update node 2023-07-21 18:31:41 -04:00
Logan Cusano
9c111eda1a Update design on navbar and sidebar 2023-07-21 18:15:51 -04:00
Logan Cusano
31de3a040d Implemented functional method to add a new system to a new through web app 2023-07-16 23:21:05 -04:00
Logan Cusano
318ee7bf91 Implement functioning method to update systems on web app 2023-07-16 22:30:18 -04:00
Logan Cusano
5428ac6144 Update API and add webapp saving 2023-07-16 18:56:47 -04:00
Logan Cusano
e27dd9d9cb Updated NodeCard Design 2023-07-16 16:47:42 -04:00
Logan Cusano
c0927601b9 Update toast creator to display proper date string 2023-07-16 01:54:13 -04:00
Logan Cusano
ef45cf6539 Only show heartbeat toast once HTTP request is complete 2023-07-16 01:51:25 -04:00
Logan Cusano
23bea5f74e Update heartbeat function location and name 2023-07-16 01:50:01 -04:00
Logan Cusano
fc743cbb46 #37 Working Joining and Leaving 2023-07-16 01:43:13 -04:00
Logan Cusano
e522326576 Additional changes for #37
- Updating side bar
- Updating nav bar
- Adding node details page
- Adding controller page
- Updating routes
2023-07-15 23:30:41 -04:00
Logan Cusano
2e22fa66a6 Initial bones for #37 2023-07-15 18:16:42 -04:00
Logan Cusano
6b37a48061 Update jsDoc in utils 2023-07-15 18:15:17 -04:00
Logan Cusano
5d6d86fa47 Update jsDoc in nodeController 2023-07-15 18:14:53 -04:00
Logan Cusano
c35d3f3fa7 Update adminController to use join/leave command wrappers 2023-07-15 18:14:40 -04:00
Logan Cusano
c38bca4144 Add jsDoc to leaveServerWrapper 2023-07-15 17:58:35 -04:00
Logan Cusano
e6332dffc9 Update join command to accept a specific node ID 2023-07-15 17:58:12 -04:00
Logan Cusano
60b6eb7cda Fix for #40
- Moved general commands to the description for higher character limit
2023-07-13 21:39:55 -04:00
Logan Cusano
eec80f7239 Implement noisegate into recording
- Should remove dead air
- Should save space as it's not recording dead air
2023-07-01 18:24:19 -04:00
Logan Cusano
a58f314475 Lock numpy version for OP25 compatibility 2023-07-01 18:22:20 -04:00
Logan Cusano
a603a53602 Resolve bug when checking if user is in channel 2023-07-01 18:19:53 -04:00
7a0664ad0c Merge pull request '#33 Implement Recording' (#34) from #33-Implement-Recording into master
Reviewed-on: #34
2023-06-25 02:29:07 -04:00
8403c2e391 Merge branch 'master' into #33-Implement-Recording 2023-06-25 02:28:35 -04:00
Logan Cusano
c9890c7cc8 Update give-role filename to match convention 2023-06-25 02:09:04 -04:00
Logan Cusano
bec4c2d743 Add discord commands to allow users to start recording 2023-06-25 02:08:46 -04:00
Logan Cusano
45258d5e4b Updated verbose text for interaction create 2023-06-25 02:08:28 -04:00
Logan Cusano
ffe9c413ba Update client readme
- Basic info for hardware
2023-06-25 01:25:58 -04:00
Logan Cusano
959cfdf7af Implement routes to start and stop client recording 2023-06-25 01:24:46 -04:00
Logan Cusano
97acfc312c Add new python recorder for clients 2023-06-25 01:24:25 -04:00
Logan Cusano
c6cdc0809c Update gitignore to ignore recordings 2023-06-25 01:23:51 -04:00
Logan Cusano
851a9c55fa Fix for #32
- Add the response check when updating client info
2023-06-23 21:43:05 -04:00
Logan Cusano
fb9df3230e Fix for #32
- Add a retry loop when the server is down
2023-06-23 17:55:08 -04:00
Logan Cusano
7163df21e9 Potential final fix for #30 2023-06-23 17:48:28 -04:00
Logan Cusano
47d18c494d Fix misplacement of setting online for checkIn 2023-06-19 02:34:29 -04:00
Logan Cusano
ea2dbd9fb0 Always send online as true when checking in with server 2023-06-19 02:33:06 -04:00
Logan Cusano
9ce77a5be0 Improved fix for #30 2023-06-19 02:29:04 -04:00
Logan Cusano
57881a7e17 Potential fix for #30 2023-06-19 02:26:36 -04:00
Logan Cusano
e350cd6030 Fixing fix of join autocorrect 2023-06-19 00:25:35 -04:00
Logan Cusano
fba0a2a2b2 Fixed bug in join autocorect
- It would add both numbers and the names to the autocorrect
2023-06-19 00:23:29 -04:00
Logan Cusano
83cef63109 Improved number check in join command 2023-06-19 00:20:00 -04:00
Logan Cusano
2390bdc2c6 Update join command to handle updated preset objects 2023-06-19 00:16:58 -04:00
Logan Cusano
93be4ca9dc Update client startup for new nodes
- Still needs to get IP from linux nodes
- Other tasks still, idk
2023-06-19 00:05:27 -04:00
Logan Cusano
d96e6ad519 Updated nodesController to properly update nodes 2023-06-19 00:02:37 -04:00
Logan Cusano
b180f90973 Update recordHelper to use undefined instead of null 2023-06-18 23:40:51 -04:00
Logan Cusano
fd7435c7bc Update requirement versions 2023-06-18 23:21:18 -04:00
Logan Cusano
e062cf5794 Fix bug in HTTP response parsing 2023-06-18 22:53:23 -04:00
Logan Cusano
597546b73d Ensure async functions wait for node contstructor 2023-06-18 22:35:51 -04:00
Logan Cusano
333e7420f4 Removed erroneous indexing on sqlreseponse 2023-06-18 22:32:01 -04:00
Logan Cusano
37a03c5cc6 Forgot to await getting new node 2023-06-18 22:29:16 -04:00
Logan Cusano
d2e9f286e2 Improved logging for server side add new node 2023-06-18 22:25:41 -04:00
Logan Cusano
255b1282ec Improve logging for server SQL requests 2023-06-18 22:22:20 -04:00
Logan Cusano
878e64fa42 Fix bug in return from addNewNode
- Return the function to get a node from the ID since the add new doesn't return node rows
- Update the controller to send the correct key from the updated object
2023-06-18 19:13:03 -04:00
Logan Cusano
7a040a8e97 Handle if new node is added with no nearby systems 2023-06-18 19:06:14 -04:00
Logan Cusano
8dffeccf83 Fix bug with 'connected' key when adding new node to the server 2023-06-18 19:02:15 -04:00
Logan Cusano
2108a3b92b Improved error response for new node 2023-06-18 17:13:29 -04:00
Logan Cusano
960b801dd2 Working on initial startup for new clients 2023-06-18 16:36:38 -04:00
Logan Cusano
dd5b442377 Update client clientController to better handle no ID 2023-06-18 16:26:55 -04:00
Logan Cusano
c5a7131063 Improve validation when checking for nodeId 2023-06-18 15:31:15 -04:00
Logan Cusano
5d54f07af4 Enabled nodeMonitorService
- Updated logging for nodeMonitorService to use it's own debugBuilder
2023-06-18 14:56:54 -04:00
Logan Cusano
24faa5279d #29
- Update debugBuilder to inspect objects instead of stringify
- Moved tthe debug entry for saving post to after the post is validated
2023-06-18 14:49:15 -04:00
Logan Cusano
79d2ca1887 #6 Removed copy of env
- Should be done manually and edited before running setup script
2023-06-17 23:37:26 -04:00
Logan Cusano
c2b4b4bfa1 #16
- Allow bots to display the preset they are listening to
2023-06-17 22:02:09 -04:00
Logan Cusano
d8a697e583 #13
- Added command to allow users to give other users their group
- Shows all groups as options but will not let user add another user if the caller doesn't have the role
2023-06-17 21:49:46 -04:00
Logan Cusano
44caa11f7c Fixed typo in remove description 2023-06-17 21:28:33 -04:00
Logan Cusano
dc92b07426 Implemented function to remove commands from a given bot and guild 2023-06-17 19:37:13 -04:00
Logan Cusano
92f4caad0c Improve the join node verification system 2023-06-17 19:25:47 -04:00
Logan Cusano
b888a9233d Improve the response when a user requests a bot to join 2023-06-17 19:01:55 -04:00
Logan Cusano
b4e27162aa Update to mysql2 2023-06-17 18:40:09 -04:00
Logan Cusano
bfda15866e Streamlined joining and leaving autocomplete
- Removed custom command builder as it's no longer needed with autocomplete
2023-06-17 17:55:27 -04:00
Logan Cusano
f4475dc9d7 #19
- Update the wrapper called when a feed encounters an error
    - Will now use a more robus backoff system
    - Waits in increments of 30 seconds
    - Keeps track of ignored attempts and 'count'

- Updated wrapper to remove source from backoff list
    - Now removes the object after the first attempt irrespective of deletion station
2023-06-16 23:26:38 -04:00
Logan Cusano
c4650a9e99 Make the bot option in the leave command required 2023-06-16 22:02:54 -04:00
Logan Cusano
f5e119d845 Bugfixes and functional #7 & #9
- #7 needs to error check more
- both need to be cleaned up
2023-06-11 04:40:40 -04:00
Logan Cusano
e8d68b2da7 Initial #7 & #9
- Working commands
- Keeps track of open connections
2023-06-10 22:16:39 -04:00
Logan Cusano
041e0d485d Fix error status in client join 2023-06-10 20:46:43 -04:00
Logan Cusano
fc11324714 Add function to get all client IDs from JSON file #7 2023-06-04 00:24:50 -04:00
Logan Cusano
c6c048c919 Update default command with autocomplete 2023-06-03 23:35:07 -04:00
Logan Cusano
8ab611836b Allow commands to use autocomplete 2023-06-03 23:31:27 -04:00
7d8ad68e27 Merge pull request 'Add join command to server #7' (#15) from Add-join-command-to-server-#7 into master
Reviewed-on: #15
2023-06-03 23:05:39 -04:00
200ca9c926 Merge branch 'master' into Add-join-command-to-server-#7 2023-06-03 23:05:12 -04:00
Logan Cusano
ff8e86cc3a Updated client setup script
- Create a copy of the .env example
- Updated the installed packages to allow for installation
2023-06-03 23:02:41 -04:00
Logan Cusano
6b12c3e3df Remove unused keys from example .env file 2023-06-03 23:01:47 -04:00
Logan Cusano
fa2f28207e wrapping up join command
- API untested
2023-06-03 23:00:50 -04:00
Logan Cusano
5c8414b4d8 Moved to issues in git 2023-06-03 22:58:31 -04:00
Logan Cusano
edaeb261f7 Add additional info on connection status to nodeObject 2023-06-03 19:00:16 -04:00
Logan Cusano
c31ccff5ca Added JSDoc to Join wrapper and updated wrapper to also take just a client ID string 2023-06-03 16:03:07 -04:00
Logan Cusano
d2186e9471 Added a join command #7
- Added a JSON example for Known Client IDs
- Implemented a custom slash command builder to add the available presets as options in the discord command
2023-06-03 15:47:07 -04:00
Logan Cusano
07743cf8a3 Updated requirements and versions 2023-06-03 15:43:15 -04:00
Logan Cusano
18afa7c058 Added extra logging when deploying commands 2023-06-03 15:42:40 -04:00
Logan Cusano
a5cff9ec7e Check if the returned data from HTTP is valid JSON and parse if so, return the string if not 2023-06-03 15:42:23 -04:00
Logan Cusano
9450b78bd4 Updated all functions to return nodeObjects instead of SQL response 2023-06-03 15:41:29 -04:00
Logan Cusano
5757c51fa3 Added new utils
- isJsonString
    - This can be used to check if a string is valid json before parsing it
- getMembersInRole
    - This can be used to check online/offline/all members in a role
- (unused) SanitizePresetName
2023-06-03 15:40:48 -04:00
Logan Cusano
fa91cbc887 Used env var for the listening port 2023-06-03 15:39:16 -04:00
Logan Cusano
7fbaf31335 Updated server intents 2023-06-03 15:38:40 -04:00
Logan Cusano
0280cb5384 Update gitignore 2023-06-03 15:38:24 -04:00
Logan Cusano
a298be40d5 Accidentally set the wrong variable for the device ID 2023-06-03 15:32:08 -04:00
Logan Cusano
43d60a748b Remove device ID requirement for API
- The device ID is handled by the .env file
2023-06-03 15:28:46 -04:00
Logan Cusano
51f517cae5 Fixed node '^=' to python '>=' 2023-06-03 15:03:18 -04:00
Logan Cusano
06cb2cc352 Fix logging namespace and windows launch 2023-06-03 15:00:42 -04:00
Logan Cusano
5ce525f2b5 Updating install script #6 2023-06-03 02:51:19 -04:00
Logan Cusano
69fdc63983 Fixing linux bug, added noisegate switch to the call 2023-05-27 21:34:19 -04:00
Logan Cusano
a9d3c33af2 Improved client bot logging 2023-05-27 21:17:31 -04:00
Logan Cusano
3719fce86a Update client package.json 2023-05-27 18:25:12 -04:00
Logan Cusano
ba927bae8c Implement install and update system for the bot
- LINUX OR WINDOW WSL ONLY
2023-05-27 17:00:57 -04:00
79fe542143 Merge pull request 'Implement Python Discord Bot to Handle Voice Connection' (#5) from feature/implement-discordpy into master
Reviewed-on: #5
2023-05-27 16:19:38 -04:00
Logan Cusano
7512c8e1df Removing the static ENV var for nearby systems 2023-05-21 15:22:43 -04:00
Logan Cusano
c882fb63d3 Moving PDAB to pdab 2023-05-20 15:24:28 -04:00
Logan Cusano
7fc61bbf2e renaming PDAB to pdab to try to fix git issues 2023-05-20 15:20:53 -04:00
Logan Cusano
e1c2ce6484 Updating and streamlining radio controller side 2023-05-20 15:18:50 -04:00
Logan Cusano
c4070cc420 Initial working radio controller for OP25 2023-05-20 14:31:43 -04:00
Logan Cusano
0f003f907e Discord voice bot handler working 2023-05-20 00:01:12 -04:00
Logan Cusano
e7b802839e Initial removal of internal discord bot 2023-05-18 22:53:25 -04:00
Logan Cusano
48999e0d63 Resolved bug in Client with .env config migration 2023-05-07 19:31:53 -04:00
Logan Cusano
2c25be1de7 Removed Embedded discord 2023-05-07 19:31:33 -04:00
Logan Cusano
cf04e37f89 Implement example .env files 2023-05-07 04:48:19 -04:00
Logan Cusano
d04cc8d5b1 Update gitignore for .env files 2023-05-07 04:43:46 -04:00
Logan Cusano
4662f37a72 Removing real .env files 2023-05-07 04:43:00 -04:00
Logan Cusano
be34c5381b Removing real .envs 2023-05-07 04:42:05 -04:00
Logan Cusano
ed79403a9b Remove config file for client, moved to .env 2023-05-07 04:40:46 -04:00
e0b6e567c1 Merge pull request 'feature/Integrate-Emmelia-into-Core' (#4) from feature/Integrate-Emmelia-into-Cor into master
Reviewed-on: #4
2023-05-07 04:12:32 -04:00
205f285e0a Merge branch 'master' into feature/Integrate-Emmelia-into-Cor 2023-05-07 04:12:01 -04:00
Logan Cusano
4e67e21651 Update debugging to log to a file (for running as a service) 2023-05-07 04:00:51 -04:00
Logan Cusano
9b2d0c4bbb Implement Node Monitor Service
- Check in with online nodes every n ms
- Update nodes that do not reply
- Added node object record helper
- Updated mysql wrapper for updating node info to accept bool or number
2023-05-07 02:41:58 -04:00
Logan Cusano
f77eb5444a Updating the running config as well as the file 2023-05-06 17:18:21 -04:00
Logan Cusano
177d25e54e Didn't update require statement 2023-05-06 17:16:01 -04:00
Logan Cusano
6880c5952a Resolved bug updating the config 2023-05-06 17:14:30 -04:00
Logan Cusano
a14c56b645 Bug with whitespaces 2023-05-06 17:04:17 -04:00
Logan Cusano
35b81758e3 Bug in getting the IP address on windows 2023-05-06 16:44:39 -04:00
Logan Cusano
7871b07113 Improving config handling & startup logic 2023-05-06 16:40:15 -04:00
Logan Cusano
6682d97156 Improve client controller config handling 2023-05-06 16:22:20 -04:00
Logan Cusano
7b2215e9da Remove config files in favor of environment variables 2023-05-06 15:22:28 -04:00
Logan Cusano
b0e52920a7 Merge of /utilities 2023-05-06 15:09:19 -04:00
Logan Cusano
f3a4f25f85 Initial Emmelia merge 2023-05-06 14:56:51 -04:00
6e8af5dbcc Merge pull request 'feature/implement-bot-into-client-core' (#2) from feature/implement-bot-into-client-core into master
Reviewed-on: #2
2023-05-06 14:44:57 -04:00
Logan Cusano
4fbed169ab Fixed audio stopping bug
- Added basic noisegate
- Handling more voice connection statuses
2023-05-06 14:09:09 -04:00
Logan Cusano
d1a8059cb9 Resolved bug in port-audio implementation; Working Audio WIN32 2023-05-03 22:37:19 -04:00
Logan Cusano
7965a1161d Minor bugfixes in getting Windows IP 2023-04-30 04:51:16 -04:00
Logan Cusano
0cefdba00f Adding Server Checkin
- Update client handler to check IP
- Add checkin to startup
- Add acceptable commands
- Needs linux command
- Needs testing
2023-04-30 04:42:04 -04:00
Logan Cusano
95c99971a2 Revert to Naudiodon and Update Config
- Changed to .env file
2023-04-30 03:52:20 -04:00
Logan Cusano
b248e7f40e Update debugging
- Uniform client name
2023-04-30 03:29:38 -04:00
Logan Cusano
546d9e8829 Implement setup and update scripts 2023-03-31 23:45:17 -04:00
Logan Cusano
6b484ddda4 Restore default discord opus library 2023-03-31 22:59:45 -04:00
Logan Cusano
6f98e59b26 Switching to direct encoding (RPi4) 2023-03-30 23:19:38 -04:00
Logan Cusano
999affce93 Remove double buffer and added logging
Would likely work on RPI3 but need to figure out issue with opus and RPi4
2023-03-30 18:09:47 -04:00
Logan Cusano
2a3893b0e7 Update comment 2023-03-30 18:09:04 -04:00
135 changed files with 11080 additions and 5366 deletions

5
.gitignore vendored
View File

@@ -5,4 +5,7 @@ node_modules/
# Development files
*.log
*.txt
*.env
*.wav
!requirements.txt
*testOP25Dir/

View File

@@ -1 +0,0 @@
DEBUG="client:*";

23
Client/.env.example Normal file
View File

@@ -0,0 +1,23 @@
DEBUG="client:*"
# Audio Config
AUDIO_DEVICE_ID=""
AUDIO_DEVICE_NAME=""
# Client Config
CLIENT_ID=0
CLIENT_NAME=""
CLIENT_IP=""
CLIENT_PORT=3010
CLIENT_LOCATION=""
CLIENT_ONLINE=true
# Configuration for the connection to the server
SERVER_IP=""
SERVER_HOSTNAME=""
SERVER_PORT=3000
# Configuration of the local OP25 application
#OP25_BIN_PATH=""
# Logfile location config
#LOG_LOCATION=""

1
Client/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
*radioPresets.json

View File

@@ -5,37 +5,19 @@ var cookieParser = require('cookie-parser');
var logger = require('morgan');
var http = require('http');
require('dotenv').config();
const fs = require('fs');
const { DebugBuilder } = require("./utilities/debugBuilder");
const deployCommands = require('./utilities/deployCommands');
const { checkIn } = require("./controllers/clientController");
var indexRouter = require('./routes/index');
var botRouter = require('./routes/bot');
var clientRouter = require('./routes/client');
var radioRouter = require('./routes/radio');
var { attachRadioSessionToRequest } = require('./controllers/radioController');
const log = new DebugBuilder("client", "app");
const {
Client,
Events,
Collection,
GatewayIntentBits,
MessageActionRow,
MessageButton
} = require('discord.js');
var app = express();
var discordToken = process.env.TOKEN;
var port = process.env.HTTP_PORT || '3010';
const discordClient = new Client({
intents: [
GatewayIntentBits.Guilds,
GatewayIntentBits.GuildMessages,
GatewayIntentBits.MessageContent,
GatewayIntentBits.GuildVoiceStates
]
});
var port = process.env.CLIENT_PORT || '3010';
// view engine setup
app.set('views', path.join(__dirname, 'views'));
@@ -52,16 +34,13 @@ app.use(express.static(path.join(__dirname, 'public')));
app.use('/', indexRouter);
// Discord bot control route
app.use('/bot', (req, res, next) => {
req.discordClient = discordClient; // Add the discord client to bot requests to be used downstream
next();
}, botRouter);
app.use('/bot', attachRadioSessionToRequest, botRouter);
// Local client control route
app.use("/client", clientRouter);
// Local radio controller route
app.use("/radio", radioRouter);
app.use("/radio", attachRadioSessionToRequest, radioRouter);
// catch 404 and forward to error handler
app.use((req, res, next) => {
@@ -115,51 +94,8 @@ async function runHTTPServer() {
})
}
// Discord bot config
// Setup commands for the Discord bot
discordClient.commands = new Collection();
const commandsPath = path.join(__dirname, 'commands');
const commandFiles = fs.readdirSync(commandsPath).filter(file => file.endsWith('.js'));
//const commandFiles = fs.readdirSync('./commands').filter(file => file.endsWith('.js'));
for (const file of commandFiles) {
const filePath = path.join(commandsPath, file);
const command = require(filePath);
log.DEBUG("Importing command: ", command.data.name);
// Set a new item in the Collection
// With the key as the command name and the value as the exported module
discordClient.commands.set(command.data.name, command);
}
// Run when the bot is ready
discordClient.on('ready', () => {
log.DEBUG(`Discord server up and running with client: ${discordClient.user.tag}`);
log.INFO(`Logged in as ${discordClient.user.tag}!`);
// Deploy slash commands
log.DEBUG("Deploying slash commands");
deployCommands.deploy(discordClient.user.id, discordClient.guilds.cache.map(guild => guild.id));
log.DEBUG(`Starting HTTP Server`);
log.DEBUG(`Starting HTTP Server`);
runHTTPServer();
});
// Setup any additional event handlers
const eventsPath = path.join(__dirname, 'events');
if (fs.existsSync(eventsPath)) {
const eventFiles = fs.readdirSync(eventsPath).filter(file => file.endsWith('.js'));
if (eventFiles.length > 0) {
for (const file of eventFiles) {
const filePath = path.join(eventsPath, file);
const event = require(filePath);
if (event.once) {
discordClient.once(event.name, (...args) => event.execute(...args));
} else {
discordClient.on(event.name, (...args) => event.execute(...args));
}
}
}
}
discordClient.login(discordToken); //Load Client Discord Token
log.DEBUG("Checking in with the master server")
checkIn(true);

View File

@@ -1,76 +0,0 @@
#!/usr/bin/env node
/**
* Module dependencies.
*/
const app = require('../app');
// Debug
const { DebugBuilder } = require("../utilities/debugBuilder.js");
const log = new DebugBuilder("client", "www");
const http = require('http');
const config = require('../config/clientConfig');
const clientController = require('../controllers/clientController');
/**
* Get port from environment and store in Express.
*/
app.set('port', config.clientConfig.port);
/**
* Create HTTP server.
*/
const server = http.createServer(app);
/**
* Listen on provided port, on all network interfaces.
*/
server.listen(config.clientConfig.port);
server.on('error', onError);
server.on('listening', onListening);
/**
* Event listener for HTTP server "error" event.
*/
function onError(error) {
if (error.syscall !== 'listen') {
throw error;
}
const bind = typeof port === 'string'
? 'Pipe ' + port
: 'Port ' + port;
// handle specific listen errors with friendly messages
switch (error.code) {
case 'EACCES':
console.error(bind + ' requires elevated privileges');
process.exit(1);
break;
case 'EADDRINUSE':
console.error(bind + ' is already in use');
process.exit(1);
break;
default:
throw error;
}
}
/**
* Event listener for HTTP server "listening" event.
*/
function onListening() {
const addr = server.address();
const bind = typeof addr === 'string'
? 'pipe ' + addr
: 'port ' + addr.port;
log.DEBUG('Listening on ' + bind);
// check in with the server to add this node or come back online
clientController.checkIn();
}

View File

@@ -1,17 +0,0 @@
// Debug
const { DebugBuilder } = require("../utilities/debugBuilder");
const log = new DebugBuilder("client", "ping");
// Modules
const { SlashCommandBuilder } = require('discord.js');
const { join } = require("../controllers/commandController")
module.exports = {
data: new SlashCommandBuilder()
.setName('join')
.setDescription('Join a voice channel'),
example: "join",
isPrivileged: false,
async execute(interaction) {
await join({ interaction: interaction });
}
}

View File

@@ -1,17 +0,0 @@
// Debug
const { DebugBuilder } = require("../utilities/debugBuilder.js");
const log = new DebugBuilder("client-bot", "leave");
// Modules
const { SlashCommandBuilder } = require('discord.js');
const { leave } = require("../controllers/commandController")
module.exports = {
data: new SlashCommandBuilder()
.setName('leave')
.setDescription('Leave a voice channel'),
example: "leave",
isPrivileged: false,
async execute(interaction) {
await leave({ interaction: interaction })
}
}

View File

@@ -1,19 +0,0 @@
// Debug
const { DebugBuilder } = require("../utilities/debugBuilder.js");
const log = new DebugBuilder("client-bot", "status");
// Modules
const { status } = require('../controllers/commandController');
// Utilities
const { SlashCommandBuilder } = require('discord.js');
module.exports = {
data: new SlashCommandBuilder()
.setName('status')
.setDescription('Check the status of the bot'),
example: "status",
isPrivileged: false,
async execute(interaction) {
await status({ interaction: interaction });
}
}

View File

@@ -1,6 +0,0 @@
{
"ApplicationID": "943742040255115304",
"GuildID": "367396189529833472",
"DeviceID": "5",
"DeviceName": "VoiceMeeter Aux Output (VB-Audi"
}

View File

@@ -1,23 +0,0 @@
// Core config settings for the node, these are the settings that are checked with the server
const path = require("path");
exports.clientConfig = {
"id": 13,
"name": "boilin balls in the hall",
"ip": "172.16.100.150",
"port": 3001,
"location": "the house",
"nearbySystems": ["Westchester Cty. Simulcast"],
"online": true
}
// Configuration for the connection to the server
exports.serverConfig = {
"ip": "127.0.0.1",
"hostname": "localhost",
"port": 3000
}
// Configuration of the local OP25 application
exports.radioAppConfig = {
"bin": "H:/Logan/Projects/Discord-Radio-Bot-CnC/Client/.idea/testOP25Dir/multi_rx.py"
}

View File

@@ -1,16 +0,0 @@
// Core config settings for the node, these are the settings that are checked with the server
exports.nodeConfig = {
"id": 0,
"name": "",
"ip": "",
"port": 0,
"location": "",
"nearbySystems": {
"System Name": {
"frequencies": [],
"mode": "",
"trunkFile": ""
}
},
"online": false
}

View File

@@ -1 +0,0 @@
{"Westchester Cty. Simulcast":{"frequencies":[470575000,470375000,470525000,470575000,470550000],"mode":"p25","trunkFile":"trunk.tsv"},"coppies":{"frequencies":[154875000],"mode":"nbfm","trunkFile":"none"}}

View File

@@ -0,0 +1,18 @@
{
"Default P25 System Name": {
"frequencies": [
155344000,
155444000,
155555000
],
"mode": "p25",
"trunkFile": "trunk.tsv"
},
"Default NBFM System": {
"frequencies": [
154690000
],
"mode": "nbfm",
"trunkFile": "none"
}
}

View File

@@ -1,61 +0,0 @@
// Config
const { getDeviceID } = require('../utilities/configHandler.js');
// Modules
const alsaInstance = require('alsa-capture');
const { returnAlsaDeviceObject } = require("../utilities/executeConsoleCommands.js");
// Debug
const { DebugBuilder } = require("../utilities/debugBuilder.js");
// Global Vars
const log = new DebugBuilder("client-bot", "audioController");
/**
* Checks to make sure the selected audio device is available and returns the device object (PortAudio Device Info)
* At least one option must be supplied, it will prefer ID to device name
*
* @param deviceName The name of the device being queried
* @param deviceId The ID of the device being queried
* @returns {unknown}
*/
async function confirmAudioDevice({deviceName = undefined, deviceId = undefined}){
const deviceList = await getAudioDevices();
if (!deviceName && !deviceId) throw new Error("No device given");
if (deviceId) return deviceList.find(device => device.id === deviceId);
if (deviceName) return deviceList.find(device => device.name === deviceName);
}
exports.confirmAudioDevice = confirmAudioDevice;
/**
* Return a list of the audio devices connected with input channels
*
* @returns {unknown[]}
*/
async function getAudioDevices(){
// Exec output contains both stderr and stdout outputs
const deviceList = await returnAlsaDeviceObject();
log.DEBUG("Device list: ", deviceList);
return deviceList;
}
exports.getAudioDevices = getAudioDevices;
/**
* Create and return the audio instance from the saved settings
* TODO Allow the client to save and load these settings dynamically
*
* @returns new portAudio.AudioIO
*/
async function createAudioInstance() {
const selectedDevice = await confirmAudioDevice({deviceId: getDeviceID()});//{deviceName: "VoiceMeeter VAIO3 Output (VB-Au"});
log.DEBUG("Device selected from config: ", selectedDevice);
// Create an instance of AudioIO with outOptions (defaults are as below), which will return a WritableStream
return new alsaInstance({
channels: 2,
format: "S16_BE",
rate: 48000,
device: selectedDevice.name ?? "default", // Use -1 or omit the deviceId to select the default device
periodSize: 100, //(48000 / 1000) * 20, //(48000 * 16 * 2) / 1000 * 20 // (48000 * (16 / 8) * 2) / 60 / 1000 * 20 //0.025 * 48000 / 2
periodTime: undefined,
// highwaterMark: 3840
});
}
exports.createAudioInstance = createAudioInstance;

View File

@@ -1,98 +1,159 @@
// Debug
const { DebugBuilder } = require("../utilities/debugBuilder.js");
const log = new DebugBuilder("client", "clientController");
const log = new DebugBuilder("client", "botController");
const botLog = new DebugBuilder("client", "botController:bot");
// Modules
const { status, join, leave } = require("./commandController")
const spawn = require('child_process').spawn;
const { resolve } = require("path");
require('dotenv').config();
const { closeProcessWrapper } = require("../utilities/utilities");
/**
* Get an object of client guilds
* @param req The express request which includes the discord client
* @returns
*/
function getGuilds(req) {
return req.discordClient.guilds.cache.map(guild => guild.id)
}
/**
* Get an object of the channels in a guild
* @param {*} guildId The Guild ID to check the channels of
* @param {*} req The request object to use to check the discord client
*/
function getChannels(guildId, req) {
const guild = req.discordClient.guilds.find(guildId);
log.DEBUG("Found Guild channels with guild", guild.channels, guild);
return guild.channels;
}
/**
* Check to see if a given guild has a given channel
* @param {*} guildId The guild ID to check if the channel exists
* @param {*} channelId The channel ID to check if exists in the guild
* @param {*} req The express request param to use the discord client
* @returns {true|false}
*/
function checkIfGuildHasChannel(guildId, channelId, req){
const guildChannels = getChannels(guildId, req)
const checkedChannel = guildChannels.find(c => c.id === channelId);
if (!checkedChannel) return false;
return true;
}
function getGuildFromChannel(channelId, req){
const channel = req.discordClient.channels.cache.get(channelId);
if (!channel) return new Error("Error getting channel from client");
if (channel.guild) return channel.guild;
return new Error("No Guild found with the given ID");
}
// Global vars
let pythonProcess;
let recordingProcess;
/**
* Get Status of the discord process
*/
exports.getStatus = (req, res) => {
log.INFO("Getting the status of the bot");
guildIds = getGuilds(req);
log.DEBUG("Guild IDs: ", guildIds);
var guildStatuses = []
for (const guildId of guildIds){
status({guildID: guildId, callback: (statusObj) => {
log.DEBUG("Status Object string: ", statusObj);
guildStatuses.push(statusObj);
}});
}
return res.status(200).json(guildStatuses);
if (pythonProcess) return res.sendStatus(200);
return res.sendStatus(201);
}
/**
* Start the bot and join the server and preset specified
*/
exports.joinServer = (req, res) => {
const channelId = req.body.channelID;
exports.joinServer = async (req, res) => {
if (!req.body.clientId || !req.body.channelId) return res.status(500).json({"message": "You must include the client ID (discord token), channel ID (The discord ID of the channel to connect to)"});
const deviceId = process.env.AUDIO_DEVICE_ID;
const channelId = req.body.channelId;
const clientId = req.body.clientId;
const presetName = req.body.presetName;
const guildObj = getGuildFromChannel(channelId, req);
const NGThreshold = req.body.NGThreshold ?? 50
if (!channelId || !presetName || !guildObj) return res.status(400).json({'message': "Request does not have all components to proceed"});
// join the sever
join({guildID: guildObj.id, guildObj: guildObj, channelID: channelId, callback: () => {
return res.sendStatus(202);
}});
// Joining the discord server
log.INFO("Join requested to: ", deviceId, channelId, clientId, presetName, NGThreshold);
if (process.platform === "win32") {
log.DEBUG("Starting Windows Python");
pythonProcess = await spawn('python.exe', [resolve(__dirname, "../pdab/main.py"), deviceId, channelId, clientId, '-n', NGThreshold, '-p', presetName ], { cwd: resolve(__dirname, "../pdab/").toString() });
//pythonProcess = await spawn('C:\\Python310\\python.exe', [resolve(__dirname, "../PDAB/main.py"), deviceId, channelId, clientId, NGThreshold ]);
}
else {
log.DEBUG("Starting Linux Python");
pythonProcess = await spawn('python3', [resolve(__dirname, "../pdab/main.py"), deviceId, channelId, clientId,'-n', NGThreshold, '-p', presetName ], { cwd: resolve(__dirname, "../pdab/") });
}
log.VERBOSE("Python Process: ", pythonProcess);
let fullOutput;
pythonProcess.stdout.setEncoding('utf8');
pythonProcess.stdout.on("data", (data) => {
botLog.VERBOSE("From Process: ", data);
fullOutput += data.toString();
});
pythonProcess.stderr.on('data', (data) => {
botLog.VERBOSE(`stderr: ${data}`);
fullOutput += data.toString();
});
pythonProcess.on('close', (code) => {
log.DEBUG(`child process exited with code ${code}`);
log.VERBOSE("Full output from bot: ", fullOutput);
});
pythonProcess.on("error", (code, signal) => {
log.ERROR("Error from the discord bot process: ", code, signal);
});
// Starting the radio application
return res.sendStatus(200);
}
/**
* Leaves the server if it's in one
*/
exports.leaveServer = (req, res) => {
exports.leaveServer = async (req, res) => {
log.INFO("Leaving the server");
const guildIds = getGuilds(req);
log.DEBUG("Guild IDs: ", guildIds);
for (const guildId of guildIds){
leave({guildID: guildId, callback: (response) => {
log.DEBUG("Response from leaving server on guild ID", guildId, response);
}});
}
if (!pythonProcess) return res.sendStatus(200)
pythonProcess = await closeProcessWrapper(pythonProcess);
return res.sendStatus(202);
}
/**
* Start a recording of what the bot is listening to, if it's currently connected
*
* @param {*} req
* @param {*} res
*/
exports.startRecording = async (req, res) => {
log.INFO("Starting recording")
//if (pythonProcess === undefined) return res.sendStatus(204);
if (!recordingProcess === undefined) return res.sendStatus(202);
const deviceId = process.env.AUDIO_DEVICE_ID;
const filename = "./recordings/" + new Date().toJSON().slice(0,10) + ".wav";
// Joining the server to record
log.INFO("Start recording: ", deviceId, filename);
if (process.platform === "win32") {
log.DEBUG("Starting Windows Python");
recordingProcess = await spawn('python', [resolve(__dirname, "../pdab/recorder.py"), deviceId, filename ], { cwd: resolve(__dirname, "../pdab/").toString() });
}
else {
log.DEBUG("Starting Linux Python");
recordingProcess = await spawn('python3', [resolve(__dirname, "../pdab/recorder.py"), deviceId, filename ], { cwd: resolve(__dirname, "../pdab/") });
}
await this.getProcessOutput(recordingProcess);
return res.sendStatus(200);
}
/**
* Stop the recording if the bot is currently recording
*
* @param {*} req
* @param {*} res
*/
exports.stopRecording = async (req, res) => {
log.INFO("Stopping recording the server");
if (!recordingProcess) return res.sendStatus(202)
recordingProcess = await closeProcessWrapper(recordingProcess);
return res.sendStatus(200);
}
/**
* Get the output of a running process
*
* @param {*} runningProcess
* @returns
*/
exports.getProcessOutput = async (runningProcess) => {
let fullOutput;
runningProcess.stdout.setEncoding('utf8');
runningProcess.stdout.on("data", (data) => {
botLog.VERBOSE("From Process: ", data);
fullOutput += data.toString();
});
runningProcess.stderr.on('data', (data) => {
botLog.VERBOSE(`stderr: ${data}`);
fullOutput += data.toString();
});
runningProcess.on('close', (code) => {
log.DEBUG(`child process exited with code ${code}`);
log.VERBOSE("Full output from bot: ", fullOutput);
});
runningProcess.on("error", (code, signal) => {
log.ERROR("Error from the process: ", code, signal);
});
}

View File

@@ -1,13 +1,18 @@
// Debug
// const { DebugBuilder } = require("../utilities/debugBuilder.js");
// const log = new DebugBuilder("client", "clientController");
const { DebugBuilder } = require("../utilities/debugBuilder.js");
const log = new DebugBuilder("client", "clientController");
// Configs
const config = require("../config/clientConfig");
require('dotenv').config();
const modes = require("../config/modes");
// Modules
const { executeAsyncConsoleCommand, BufferToJson, nodeObject } = require("../utilities/utilities");
// Utilities
const updateConfig = require("../utilities/updateConfig");
const updatePreset = require("../utilities/updatePresets");
const requests = require("../utilities/httpRequests");
const { getFullConfig } = require("../utilities/configHandler");
const { updateId, updateConfig, updateClientConfig } = require("../utilities/updateConfig");
const { updatePreset, addNewPreset, getPresets, removePreset } = require("../utilities/updatePresets");
const { onHttpError, requestOptions, sendHttpRequest } = require("../utilities/httpRequests");
var runningClientConfig = getFullConfig()
/**
* Check the body for the required fields to update or add a preset
@@ -17,11 +22,11 @@ const requests = require("../utilities/httpRequests");
* @returns {*}
*/
function checkBodyForPresetFields(req, res, callback) {
if (!req.body?.systemName) return res.status(403).json({"message": "No system in the request"});
if (!req.body?.frequencies && Array.isArray(req.body.frequencies)) return res.status(403).json({"message": "No frequencies in the request or type is not an array"});
if (!req.body?.mode && typeof req.body.mode === "string") return res.status(403).json({"message": "No mode in the request"});
if (!req.body?.systemName) return res.status(403).json({ "message": "No system in the request" });
if (!req.body?.frequencies && Array.isArray(req.body.frequencies)) return res.status(403).json({ "message": "No frequencies in the request or type is not an array" });
if (!req.body?.mode && typeof req.body.mode === "string") return res.status(403).json({ "message": "No mode in the request" });
if (!req.body?.trunkFile) {
if (modes.digitalModes.includes(req.body.mode)) return res.status(403).json({"message": "No trunk file in the request but digital mode specified. If you are not using a trunk file for this frequency make sure to specify 'none' for trunk file in the request"})
if (modes.digitalModes.includes(req.body.mode)) return res.status(403).json({ "message": "No trunk file in the request but digital mode specified. If you are not using a trunk file for this frequency make sure to specify 'none' for trunk file in the request" })
// If there is a value keep it but if not, add nothing so the system can update that key (if needed)
req.body.trunkFile = req.body.trunkFile ?? "none";
}
@@ -29,36 +34,142 @@ function checkBodyForPresetFields(req, res, callback) {
return callback();
}
/** Check in with the server
* If the bot has a saved ID, check in with the server to update any information or just check back in
* If the bot does not have a saved ID, it will attempt to request a new ID from the server
*/
exports.checkIn = async () => {
let reqOptions;
// Check if there is an ID found, if not add the node to the server. If there was an ID, check in with the server to make sure it has the correct information
if (config.clientConfig.id === 0) {
// ID was not found in the config, creating a new node
reqOptions = new requests.requestOptions("/nodes/newNode", "POST");
delete config.clientConfig.id;
requests.sendHttpRequest(reqOptions, JSON.stringify(config.clientConfig), (responseObject) => {
// Update the client's ID if the server accepted it
if (responseObject.statusCode === 202) {
config.clientConfig.id = responseObject.body.nodeId;
updateConfig.updateId(responseObject.body.nodeId);
}
async function checkLocalIP() {
if (process.platform === "win32") {
// Windows
var networkConfig = await executeAsyncConsoleCommand("ipconfig");
log.DEBUG('Network Config: ', networkConfig);
var networkConfigLines = await networkConfig.split("\n").filter(line => {
if (!line.includes(":")) return false;
line = line.split(":");
if (!line.length === 2) return false;
return true;
}).map(line => {
line = String(line).split(':', 2);
line[0] = String(line[0]).replace(/[.]|[\s]/g, "").trim();
line[1] = String(line[1]).replace(/(\\r|\\n)/g, "").trim();
return line;
});
networkConfig = Object.fromEntries(networkConfigLines);
log.DEBUG("Parsed IP Config Results: ", networkConfig);
log.DEBUG("Local IP address: ", networkConfig['IPv4Address']);
return networkConfig['IPv4Address'];
}
else {
// ID is in the config, checking in with the server
reqOptions = new requests.requestOptions("/nodes/nodeCheckIn", "POST");
requests.sendHttpRequest(reqOptions, JSON.stringify(config.clientConfig), (responseObject) => {
if (responseObject.statusCode === 202) {
// Server accepted an update
}
if (responseObject.statusCode === 200) {
// Server accepted the response but there were no keys to be updated
}
});
// Linux
var networkConfig = await executeAsyncConsoleCommand("ip addr");
}
}
/**
* Checks the config file for all required fields or gets and updates the required fields
*/
exports.checkConfig = async function checkConfig() {
if (!runningClientConfig.id || runningClientConfig.id == 0 || runningClientConfig.id == '0') {
await updateId(0);
runningClientConfig.id = 0;
}
if (!runningClientConfig.ip) {
const ipAddr = await checkLocalIP();
await updateConfig('CLIENT_IP', ipAddr);
runningClientConfig.ip = ipAddr;
}
if (!runningClientConfig.name) {
const lastOctet = await String(checkLocalIP()).spit('.')[-1];
const name = `Radio-Node-${lastOctet}`;
await updateConfig('CLIENT_NAME', name);
runningClientConfig.name = name;
}
if (!runningClientConfig.port) {
const port = 3010;
await updateConfig('CLIENT_PORT', port);
runningClientConfig.port = port;
}
}
/** Check in with the server
* If the bot has a saved ID, check in with the server to get any updated information or just check back in
* If the bot does not have a saved ID, it will attempt to request a new ID from the server
*
* @param {boolean} update If set to true, the client will update the server to it's config, instead of taking the server's config
*/
exports.checkIn = async (update = false) => {
let reqOptions;
await this.checkConfig();
// Check if there is an ID found, if not add the node to the server. If there was an ID, check in with the server to make sure it has the correct information
try {
if (!runningClientConfig?.id || runningClientConfig.id == 0) {
// ID was not found in the config, creating a new node
reqOptions = new requestOptions("/nodes/newNode", "POST");
sendHttpRequest(reqOptions, JSON.stringify(runningClientConfig), async (responseObject) => {
// Check if the server responded
if (!responseObject) {
log.WARN("Server did not respond to checkIn. Will wait 60 seconds then try again");
setTimeout(() => {
// Run itself again to see if the server is up now
this.checkIn();
}, 60000);
return
}
// Update the client's ID if the server accepted its
if (responseObject.statusCode === 202) {
runningClientConfig.id = responseObject.body.nodeId;
log.DEBUG("Response object from new node: ", responseObject, runningClientConfig);
await updateId(runningClientConfig.id);
}
if (responseObject.statusCode >= 300) {
// Server threw an error
log.DEBUG("HTTP Error: ", responseObject, await BufferToJson(responseObject.body));
await onHttpError(responseObject.statusCode);
}
});
}
else {
// ID is in the config, checking in with the server
if (update) reqOptions = new requestOptions(`/nodes/${runningClientConfig.id}`, "PUT");
else reqOptions = new requestOptions(`/nodes/nodeCheckIn/${runningClientConfig.id}`, "POST");
sendHttpRequest(reqOptions, JSON.stringify(runningClientConfig), (responseObject) => {
log.DEBUG("Check In Respose: ", responseObject);
// Check if the server responded
if (!responseObject) {
log.WARN("Server did not respond to checkIn. Will wait 60 seconds then try again");
setTimeout(() => {
// Run itself again to see if the server is up now
this.checkIn();
}, 60000);
return
}
if (responseObject.statusCode === 202) {
log.DEBUG("Updated keys: ", responseObject.body.updatedKeys)
// Server accepted an update
}
if (responseObject.statusCode === 200) {
// Server accepted the response but there were no keys to be updated
if (!update){
const tempUpdatedConfig = updateClientConfig(responseObject.body);
if (!tempUpdatedConfig.length > 0) return;
}
}
if (responseObject.statusCode >= 300) {
// Server threw an error
onHttpError(responseObject.statusCode);
}
});
}
}
catch (err) {
log.ERROR("Error checking in: ", err);
}
}
@@ -70,11 +181,30 @@ exports.requestCheckIn = async (req, res) => {
return res.sendStatus(200);
}
/**
* Express JS Wrapper for checking and updating client config
* @param {*} req
* @param {*} res
* @returns
*/
exports.updateClientConfigWrapper = async (req, res) => {
// Convert the online status to a boolean to be worked with
log.DEBUG("REQ Body: ", req.body);
const updatedKeys = await updateClientConfig(req.body);
if (updatedKeys) {
log.DEBUG("Keys have been updated, updating running config and checking in with the server: ", updatedKeys);
runningClientConfig = await getFullConfig();
await this.checkIn(true);
}
res.status(200).json(updatedKeys);
}
/** Controller for the /client/presets endpoint
* This is the endpoint wrapper to get the presets object
*/
exports.getPresets = async (req, res) => {
return res.status(200).json(updatePreset.getPresets());
runningClientConfig.nearbySystems = getPresets();
return res.status(200).json(runningClientConfig.nearbySystems);
}
/** Controller for the /client/updatePreset endpoint
@@ -82,9 +212,11 @@ exports.getPresets = async (req, res) => {
*/
exports.updatePreset = async (req, res) => {
checkBodyForPresetFields(req, res, () => {
updatePreset.updatePreset(req.body.systemName, () => {
updatePreset(req.body.systemName, () => {
runningClientConfig.nearbySystems = getPresets();
this.checkIn(true);
return res.sendStatus(200);
}, {frequencies: req.body.frequencies, mode: req.body.mode, trunkFile: req.body.trunkFile});
}, { frequencies: req.body.frequencies, mode: req.body.mode, trunkFile: req.body.trunkFile });
})
}
@@ -93,10 +225,32 @@ exports.updatePreset = async (req, res) => {
*/
exports.addNewPreset = async (req, res) => {
checkBodyForPresetFields(req, res, () => {
updatePreset.addNewPreset(req.body.systemName, req.body.frequencies, req.body.mode, () => {
addNewPreset(req.body.systemName, req.body.frequencies, req.body.mode, () => {
runningClientConfig.nearbySystems = getPresets();
this.checkIn(true);
return res.sendStatus(200);
}, req.body.trunkFile);
});
}
/**
* Removes a preset from the client
*/
exports.removePreset = async (req, res) => {
checkBodyForPresetFields(req, res, () => {
if (!req.body.systemName) return res.status("500").json({ "message": "You must specify a system name to delete, this must match exactly to how the system name is saved." })
removePreset(req.body.systemName, () => {
runningClientConfig.nearbySystems = getPresets();
this.checkIn(true);
return res.sendStatus(200);
}, req.body.trunkFile);
});
}
/**
* Runs the updater service
*/
exports.updateClient = async (req, res) => {
await executeAsyncConsoleCommand("systemctl start RadioNodeUpdater.service");
return res.sendStatus(200);
}

View File

@@ -1,114 +0,0 @@
// Debug
const { DebugBuilder } = require("../utilities/debugBuilder.js");
const log = new DebugBuilder("client-bot", "commandController");
// Modules
const { joinVoiceChannel, VoiceConnectionStatus, getVoiceConnection } = require("@discordjs/voice");
const { OpusEncoder } = require("@discordjs/opus");
// Utilities
const {replyToInteraction} = require("../utilities/messageHandler.js");
const {createAudioInstance} = require("../controllers/audioController.js");
// Declare the encoder
const encoder = new OpusEncoder(48000, 2);
/**
* Join the specified voice channel
*
* @param interaction Message interaction from discord
* @param {string||any} guildID The specified Guild ID if this function is run from the client instead of from an interaction in Discord
* @param {string||any} channelID The channel ID to join
* @param guild The guild object to be used to create a voice adapter
* @param {function} callback The callback that will be needed if this function is run with a Guild ID instead of an interaction
*/
exports.join = async function join({interaction= undefined, guildID= undefined, channelID = undefined, guildObj = undefined, callback = undefined}){
if (interaction){
const voiceChannel = interaction.options.getChannel('voicechannel');
channelID = voiceChannel.id;
guildID = interaction.guildId;
guildObj = interaction.guild;
if (interaction) replyToInteraction(interaction, `Ok, Joining ${voiceChannel.name}`);
}
log.DEBUG("Channel ID: ", channelID)
log.DEBUG("Guild ID: ", guildID)
const voiceConnection = joinVoiceChannel({
channelId: channelID,
guildId: guildID,
adapterCreator: guildObj.voiceAdapterCreator,
selfMute: false,
selfDeaf: false,
});
const audioInstance = await createAudioInstance();
audioInstance.on('audio', (buffer) => {
buffer = Buffer.from(buffer);
log.DEBUG("Audio buffer: ", buffer);
const encoded = encoder.encode(buffer);
// TODO Add a function here to check the volume of either buffer and only play audio to discord when there is audio to be played
voiceConnection.playOpusPacket(encoded);
})
// Exit the audio handler when the bot disconnects
voiceConnection.on(VoiceConnectionStatus.Destroyed, () => {
audioInstance.close();
})
if (guildID && callback) callback();
else return;
}
/**
* If in a voice channel for the specified guild, leave
*
* @param interaction Message interaction from discord
* @param guildID
* @param callback
*/
exports.leave = async function leave({interaction = undefined, guildID= undefined, callback = undefined}) {
if(interaction) {
guildID = interaction.guild.id;
}
const voiceConnection = getVoiceConnection(guildID);
let response;
if (!voiceConnection){
response = "Not in a voice channel."
if (interaction) return replyToInteraction(interaction, response);
else callback(response);
}
voiceConnection.destroy();
response = "Goodbye"
if (interaction) return replyToInteraction(interaction, response);
else callback(response);
}
/**
* Get the voice status of the bots
* @param {*} param0
* @returns
*/
exports.status = async function status({interaction= undefined, guildID= undefined, callback = undefined}) {
//if (!interaction && !guildID) // Need error of sorts
if (interaction){
guildID = interaction.guild.id;
}
const voiceConnection = getVoiceConnection(guildID);
const statusObj = {
"guildID": guildID,
"voiceConnection": typeof g !== 'undefined' ? true : false // True if there is a voice connection, false if undefined
}
log.DEBUG('Status Object: ', statusObj);
// get the status and return it accordingly (message reply / module)
if (interaction) {
return replyToInteraction(interaction, "Pong! I have Aids and now you do too!");
}
else {
callback(statusObj);
}
}

View File

@@ -2,146 +2,72 @@
const { DebugBuilder } = require("../utilities/debugBuilder.js");
const log = new DebugBuilder("client", "radioController");
// Modules
const { resolve, dirname } = require('path');
const fs = require('fs');
const radioConfig = require('../config/clientConfig').radioAppConfig;
const radioConfigHelper = require("../utilities/radioConfigHelper");
const presetWrappers = require("../utilities/updatePresets");
const spawn = require('child_process').spawn;
const converter = require("convert-units");
require('dotenv').config();
const { closeProcessWrapper, changeCurrentConfigWrapper, openRadioSessionWrapper } = require("../utilities/utilities");
let radioChildProcess, tempRes, radioConfigPath;
let radioChildProcess;
/**
* Closes the radio executable if it's in one
*/
exports.closeRadioSession = (req, res) => {
if (!radioChildProcess) return res.sendStatus(200)
tempRes = res;
radioChildProcess.kill();
radioChildProcess = undefined;
exports.closeRadioSession = async (req, res) => {
if (!radioChildProcess || !req.body.radioSession) return res.sendStatus(204);
if (radioChildProcess) radioChildProcess = await closeProcessWrapper(radioChildProcess);
if (req.body.radioSession) req.body.radioSession = await closeProcessWrapper(req.body.radioSession);
if (!radioChildProcess) return res.sendStatus(200);
}
/**
* Change the current 'cfg.json' file to the preset specified
* @param {string} presetName
*/
exports.changeCurrentConfig = (req, res) => {
// Check if the given config is saved
log.DEBUG("[/radio/changeCurrentConfig] - Checking if provided preset is in the config");
if (!checkIfPresetExists(req.body.presetName)) return res.status(500).JSON("No preset with given name found in config"); // No preset with the given name is in the config
exports.changeCurrentConfig = async (req, res) => {
const presetName = req.body.presetName;
if (!presetName) return res.status(500).json("You must include the preset name")
// Check if the current config is the same as the preset given
const currentConfig = readOP25Config();
if (currentConfig.channels && currentConfig.channels.name === req.body.presetName) {
log.DEBUG("[/radio/changeCurrentConfig] - Current config is the same as the preset given");
return res.sendStatus(202);
const updatedConfigObject = await changeCurrentConfigWrapper(presetName);
// No change was made to the config
if (!updatedConfigObject) return res.sendStatus(200);
// Error was encountered
if (typeof updatedConfigObject === "string") return res.status(500).json(updatedConfigObject);
// There was a change made to the config, reopening the radio session if it was open
if (radioChildProcess) {
log.DEBUG("Radio session open, restarting to accept the new config");
const radioSessionResult = await openRadioSessionWrapper(radioChildProcess, presetName);
// throw an error to the client if the wrapper ran into an error
if (typeof radioSessionResult === "string") return res.status(500).json(updatedConfigObject);
}
// Convert radioPreset to OP25 'cfg.json. file
log.DEBUG("[/radio/changeCurrentConfig] - Converting radioPreset to OP25 config");
const updatedConfigObject = convertRadioPresetsToOP25Config(req.body.presetName);
// Replace current JSON file with the updated file
writeOP25Config(updatedConfigObject, () => {
res.sendStatus(200);
})
return res.sendStatus(202);
}
/**
* Open a new OP25 process tuned to the specified system
*/
exports.openRadioSession = () => {
if (radioChildProcess) closeRadioSession();
radioChildProcess = spawn(getRadioBinPath());
exports.openRadioSession = async (req, res) => {
const presetName = req.body.presetName;
if(!presetName) return res.status(500).json({"message": "You must include the preset name to start the radio session with"})
radioChildProcess = await openRadioSessionWrapper(radioChildProcess, presetName);
// throw an error to the client if the wrapper ran into an error
if (typeof radioSessionResult === "string") return res.status(500).json(updatedConfigObject);
return res.sendStatus(200);
}
/**
* Get the location of the 'multi_rx.py' binary from the config
* Attach the radio session to the request to be used elsewhere
*
* @param {*} req
* @param {*} res
*/
function getRadioBinPath(){
return resolve(radioConfig.bin);
}
/**
* Write the given config to the JSON file in OP25 the bin dir
* @param config The full config to be written to the file
* @param {function} callback The function to be called when this wrapper completes
*/
function writeOP25Config(config, callback = undefined) {
log.DEBUG("Updating OP25 config with: ", config);
fs.writeFile(getRadioConfigPath(), JSON.stringify(config), (err) => {
// Error checking
if (err) {
log.ERROR(err);
throw err;
}
log.DEBUG("Write Complete");
if (callback) callback()
});
}
/**
* Get the current config file in use by OP25
* @returns {object|*} The parsed config object currently set in OP25
*/
function readOP25Config() {
const configPath = getRadioConfigPath();
log.DEBUG(`Reading from config path: '${configPath}'`);
return JSON.parse(fs.readFileSync(configPath));
}
/**
* Get the path of the config for the radio app (OP25) and set the global variable
*/
function getRadioConfigPath(){
let radioConfigDirPath = dirname(getRadioBinPath());
return resolve(`${radioConfigDirPath}/cfg.json`);
}
/**
* Check to see if the preset name exists in the config
* @param {string} presetName The system name as saved in the preset
* @returns {true||false}
*/
function checkIfPresetExists(presetName) {
const savedPresets = presetWrappers.getPresets();
if (!Object.keys(savedPresets).includes(presetName)) return false;
else return true;
}
/**
* Convert a radioPreset to OP25's cfg.json file
*/
function convertRadioPresetsToOP25Config(presetName){
const savedPresets = presetWrappers.getPresets();
let frequencyString = "";
for (const frequency of savedPresets[presetName].frequencies){
frequencyString += `${converter(frequency).from("Hz").to("MHz")},`
}
frequencyString = frequencyString.slice(0, -1);
let updatedOP25Config;
switch (savedPresets[presetName].mode){
case "p25":
updatedOP25Config = new radioConfigHelper.P25({
"systemName": presetName,
"controlChannelsString": frequencyString,
"tagsFile": savedPresets[presetName].trunkFile
});
break;
case "nbfm":
//code for nbfm here
updatedOP25Config = new radioConfigHelper.NBFM({
"frequency": frequencyString,
"systemName": presetName
});
break;
default:
throw new Error("Radio mode of selected preset not recognized");
}
log.DEBUG(updatedOP25Config);
return updatedOP25Config;
exports.attachRadioSessionToRequest = async (req, res, next) => {
req.body.radioSession = radioChildProcess;
next();
}

View File

@@ -1,150 +0,0 @@
//Config
import { getTOKEN, getGuildID, getApplicationID } from './utilities/configHandler.js';
// Commands
import ping from './commands/ping.js';
import join from './commands/join.js';
import leave from './commands/leave.js';
import status from './commands/status.js';
// Debug
import ModuleDebugBuilder from "./utilities/moduleDebugBuilder.js";
const log = new ModuleDebugBuilder("bot", "app");
// Modules
import { Client, GatewayIntentBits } from 'discord.js';
// Utilities
import registerCommands from './utilities/registerCommands.js';
/**
* Host Process Object Builder
*
* This constructor is used to easily construct responses to the host process
*/
class HPOB {
/**
* Build an object to be passed to the host process
* @param command The command to that was run ("Status", "Join", "Leave", "ChgPreSet")
* @param response The response from the command that was run
*/
constructor(command = "Status"||"Join"||"Leave"||"ChgPreSet", response) {
this.cmd = command;
this.msg = response;
}
}
// Create the Discord client
const client = new Client({
intents: [
GatewayIntentBits.Guilds,
GatewayIntentBits.GuildMessages,
GatewayIntentBits.MessageContent,
GatewayIntentBits.GuildVoiceStates
]
});
/**
* When the parent process sends a message, this will interpret the message and act accordingly
*
* DRB IPC Message Structure:
* msg.cmd = The command keyword; Commands covered on the server side
* msg.params = An array containing the parameters for the command
*
*/
process.on('message', (msg) => {
log.DEBUG('IPC Message: ', msg);
const guildID = getGuilds()[0];
log.DEBUG("Guild Name: ", getGuildNameFromID(guildID));
switch (msg.cmd) {
// Check the status of the bot
case "Status":
log.INFO("Status command run from IPC");
status({guildID: guildID, callback: (statusObj) => {
log.DEBUG("Status Object string: ", statusObj);
if (!statusObj.voiceConnection) return process.send(new HPOB("Status", "VDISCONN"));
}});
break;
// Check the params for a server ID and if so join the server
case "Join":
log.INFO("Join command run from IPC");
join({guildID: guildID, guildObj: client.guilds.cache.get(guildID), channelID: msg.params.channelID, callback: () => {
process.send(new HPOB("Join", "AIDS"));
}})
break;
// Check to see if the bot is in a server and if so leave
case "Leave":
log.INFO("Leave command run from IPC");
leave({guildID: guildID, callback: (response) => {
process.send(new HPOB("Leave", response));
}});
break;
default:
// Command doesn't exist
log.INFO("Unknown command run from IPC");
break;
}
})
// When the client is connected and ready
client.on('ready', () =>{
log.INFO(`${client.user.tag} is ready`)
process.send({'msg': "INIT READY"});
});
/*
* Saved For later
client.on('messageCreate', (message) => {
log.DEBUG(`Message Sent by: ${message.author.tag}\n\t'${message.content}'`);
});
*/
// When a command is sent
client.on('interactionCreate', (interaction) => {
if (interaction.isChatInputCommand()){
switch (interaction.commandName) {
case "ping":
ping(interaction);
break;
case "join":
join({ interaction: interaction });
break;
case "leave":
leave({ interaction: interaction });
break;
case "status":
status({ interaction: interaction });
break;
default:
interaction.reply({ content: 'Command not found, try one that exists', fetchReply: true })
.then((message) => log.DEBUG(`Reply sent with content ${message.content}`))
.catch((err) => log.ERROR(err));
}
}
})
function loginBot(){
client.login(getTOKEN());
}
function getGuilds() {
return client.guilds.cache.map(guild => guild.id)
}
function getGuildNameFromID(guildID) {
return client.guilds.cache.map((guild) => {
if (guild.id === guildID) return guild.name;
})[0]
}
function main(){
registerCommands(() => {
loginBot();
});
}
main();
//module.exports = client;

File diff suppressed because it is too large Load Diff

View File

@@ -1,25 +0,0 @@
{
"name": "discord-bot",
"version": "1.0.0",
"description": "",
"main": "app.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"@discordjs/builders": "^1.4.0",
"@discordjs/opus": "^0.9.0",
"@discordjs/rest": "^1.4.0",
"@discordjs/voice": "^0.14.0",
"@mapbox/node-pre-gyp": "^1.0.10",
"debug": "^4.3.4",
"discord.js": "^14.7.1",
"node-gyp": "^9.3.0",
"libsodium-wrappers": "^0.7.10",
"alsa-capture": "0.3.0"
},
"type": "module"
}

View File

@@ -1,46 +0,0 @@
# Discord Radio Bot: Command & Control - Client: Discord Bot (Client)
---
Explanation here
## Requirements
---
Requirements here (not modules, that will be installed with npm)
## Installation
---
Notes here
### Installation here
```shell
```
## Configuration
---
Notes here
### Configuration here
```shell
```
## Usage
---
### Usage here
```javascript
```

View File

@@ -1,17 +0,0 @@
// Debug
import Debug from 'debug';
/**
* Create the different logging methods for a function
* Namespace template = ("[app]:[fileName]:['INFO', 'WARNING', 'DEBUG', 'ERROR']")
* @param {string} appName The name of the app to be used in the 'app' portion of the namespace
* @param {string} fileName The name of the file calling the builder to be used in the 'fileName' portion of the namespace
*/
export default class ModuleDebugBuilder {
constructor(appName, fileName) {
this.INFO = Debug(`${appName}:${fileName}:INFO`);
this.DEBUG = Debug(`${appName}:${fileName}:DEBUG`);
this.WARN = Debug(`${appName}:${fileName}:WARNING`);
this.ERROR = Debug(`${appName}:${fileName}:ERROR`);
}
}

View File

@@ -1,55 +0,0 @@
import {SlashCommandBuilder} from "@discordjs/builders";
import {REST} from "@discordjs/rest";
import {getApplicationID, getGuildID, getTOKEN} from "./configHandler.js";
import { Routes, ChannelType } from "discord.js";
// Debug
import ModuleDebugBuilder from "./moduleDebugBuilder.js";
const log = new ModuleDebugBuilder("bot", "registerCommands");
const pingCommand = new SlashCommandBuilder()
.setName("ping")
.setDescription("Confirm the bot is online")
.toJSON();
const joinCommand = new SlashCommandBuilder()
.setName('join')
.setDescription('Joins a voice channel')
.addChannelOption((option) => option
.setName('voicechannel')
.setDescription('The Channel to voiceController')
.setRequired(false)
.addChannelTypes(ChannelType.GuildVoice))
.toJSON();
const leaveCommand = new SlashCommandBuilder()
.setName("leave")
.setDescription("Leave current voice channel")
.toJSON();
const statusCommand = new SlashCommandBuilder()
.setName("status")
.setDescription("Returns if the bot is connected to a channel or not")
.toJSON();
export default async function registerCommands(callback){
const commands = [
pingCommand,
joinCommand,
leaveCommand,
statusCommand
];
try {
const rest = new REST({ version: '10' }).setToken(getTOKEN());
const clientID = getApplicationID();
const guildID = getGuildID();
await rest.put(Routes.applicationGuildCommands(clientID, guildID), {
body: commands,
});
log.DEBUG("Successfully registered the following commands: ", commands)
callback();
} catch (err) {
log.ERROR(err);
}
}

915
Client/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -3,6 +3,11 @@
"version": "0.0.0",
"private": true,
"main": "app.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"preinstall": "echo preinstall",
"postinstall": "echo postinstall"
},
"dependencies": {
"convert-units": "^2.3.4",
"cookie-parser": "~1.4.4",
@@ -12,15 +17,9 @@
"express": "~4.16.1",
"http-errors": "~1.6.3",
"morgan": "~1.9.1",
"replace-in-file": "~6.3.5",
"replace-in-file": "~7.0.1",
"@discordjs/builders": "^1.4.0",
"@discordjs/opus": "^0.9.0",
"@discordjs/rest": "^1.4.0",
"@discordjs/voice": "^0.14.0",
"@mapbox/node-pre-gyp": "^1.0.10",
"discord.js": "^14.7.1",
"node-gyp": "^9.3.0",
"libsodium-wrappers": "^0.7.10",
"alsa-capture": "0.3.0"
"discord.js": "^14.7.1"
}
}

5
Client/pdab/.gitignore vendored Normal file
View File

@@ -0,0 +1,5 @@
*venv/
*__pycache__/
*.html
*.exe
LICENSE

215
Client/pdab/NoiseGatev2.py Normal file
View File

@@ -0,0 +1,215 @@
import audioop
import logging
import math
import time
import pyaudio
import discord
import numpy
voice_connection = None
LOGGER = logging.getLogger("Discord_Radio_Bot.NoiseGateV2")
# noinspection PyUnresolvedReferences
class AudioStream:
def __init__(self, _channels: int = 2, _sample_rate: int = 48000, _frames_per_buffer: int = 1024,
_input_device_index: int = None, _output_device_index: int = None, _input: bool = True,
_output: bool = True, _init_on_startup: bool = True):
self.paInstance_kwargs = {
'format': pyaudio.paInt16,
'channels': _channels,
'rate': _sample_rate,
'input': _input,
'output': _output,
'frames_per_buffer': _frames_per_buffer
}
if _input_device_index:
if _input:
self.paInstance_kwargs['input_device_index'] = _input_device_index
else:
LOGGER.warning(f"[AudioStream.__init__]:\tInput was not enabled."
f" Reinitialize with '_input=True'")
if _output_device_index:
if _output:
self.paInstance_kwargs['output_device_index'] = _output_device_index
else:
LOGGER.warning(f"[AudioStream.__init__]:\tOutput was not enabled."
f" Reinitialize with '_output=True'")
if _init_on_startup:
# Init PyAudio instance
LOGGER.info("Creating PyAudio instance")
self.paInstance = pyaudio.PyAudio()
# Define and initialize stream object if we have been passed a device ID (pyaudio.open)
self.stream = None
if _output_device_index or _input_device_index:
if _init_on_startup:
LOGGER.info("Init stream")
self.init_stream()
def init_stream(self, _new_output_device_index: int = None, _new_input_device_index: int = None):
# Check what device was asked to be changed (or set)
if _new_input_device_index:
if self.paInstance_kwargs['input']:
self.paInstance_kwargs['input_device_index'] = _new_input_device_index
else:
LOGGER.warning(f"[AudioStream.init_stream]:\tInput was not enabled when initialized."
f" Reinitialize with '_input=True'")
if _new_output_device_index:
if self.paInstance_kwargs['output']:
self.paInstance_kwargs['output_device_index'] = _new_output_device_index
else:
LOGGER.warning(f"[AudioStream.init_stream]:\tOutput was not enabled when initialized."
f" Reinitialize with '_output=True'")
self.close_if_open()
# Open the stream
self.stream = self.paInstance.open(**self.paInstance_kwargs)
def close_if_open(self):
# Stop the stream if it is started
if self.stream:
if self.stream.is_active():
self.stream.stop_stream()
self.stream.close()
LOGGER.debug(f"[ReopenStream.close_if_open]:\t Stream was open; It was closed.")
def list_devices(self, _display_input_devices: bool = True, _display_output_devices: bool = True):
LOGGER.info('Getting a list of the devices connected')
info = self.paInstance.get_host_api_info_by_index(0)
numdevices = info.get('deviceCount')
devices = {
'Input': {},
'Output': {}
}
for i in range(0, numdevices):
if (self.paInstance.get_device_info_by_host_api_device_index(0, i).get('maxInputChannels')) > 0:
input_device = self.paInstance.get_device_info_by_host_api_device_index(0, i).get('name')
devices['Input'][i] = input_device
if _display_input_devices:
LOGGER.debug(f"Input Device id {i} - {input_device}")
if (self.paInstance.get_device_info_by_host_api_device_index(0, i).get('maxOutputChannels')) > 0:
output_device = self.paInstance.get_device_info_by_host_api_device_index(0, i).get('name')
devices['Output'][i] = output_device
if _display_output_devices:
LOGGER.debug(f"Output Device id {i} - {output_device}")
return devices
async def stop(self):
await voice_connection.disconnect()
self.close_if_open()
self.stream.close()
self.paInstance.terminate()
# noinspection PyUnresolvedReferences
class NoiseGate(AudioStream):
def __init__(self, _voice_connection, _noise_gate_threshold: int, **kwargs):
super(NoiseGate, self).__init__(_init_on_startup=True, **kwargs)
global voice_connection
voice_connection = _voice_connection
self.THRESHOLD = _noise_gate_threshold
self.NGStream = NoiseGateStream(self)
self.Voice_Connection_Thread = None
def run(self) -> None:
global voice_connection
# Start the audio stream
LOGGER.debug(f"Starting stream")
self.stream.start_stream()
# Start the stream to discord
self.core()
def core(self, error=None):
if error:
LOGGER.warning(error)
while not voice_connection.is_connected():
time.sleep(.2)
if not voice_connection.is_playing():
LOGGER.debug(f"Playing stream to discord")
voice_connection.play(self.NGStream, after=self.core)
async def close(self):
LOGGER.debug(f"Closing")
await voice_connection.disconnect()
if self.stream.is_active:
self.stream.stop_stream()
LOGGER.debug(f"Stopping stream")
# noinspection PyUnresolvedReferences
class NoiseGateStream(discord.AudioSource):
def __init__(self, _stream):
super(NoiseGateStream, self).__init__()
self.stream = _stream # The actual audio stream object
self.NG_fadeout = 240/20 # Fadeout value used to hold the noisegate after de-triggering
self.NG_fadeout_count = 0 # A count set when the noisegate is triggered and was de-triggered
self.process_set_count = 0 # Counts how many processes have been made
def read(self):
try:
while voice_connection.is_connected():
curr_buffer = bytearray(self.stream.stream.read(960))
buffer_rms = audioop.rms(curr_buffer, 2)
if buffer_rms > 0:
buffer_decibel = 20 * math.log10(buffer_rms)
if self.process_set_count % 10 == 0:
if buffer_decibel >= self.stream.THRESHOLD:
LOGGER.debug(f"[Noisegate Open] {buffer_decibel} db")
else:
LOGGER.debug(f"[Noisegate Closed] {buffer_decibel} db")
if buffer_decibel >= self.stream.THRESHOLD:
self.NG_fadeout_count = self.NG_fadeout
self.process_set_count += 1
if curr_buffer:
return bytes(curr_buffer)
else:
if self.NG_fadeout_count > 0:
self.NG_fadeout_count -= 1
LOGGER.debug(f"Frames in fadeout remaining: {self.NG_fadeout_count}")
self.process_set_count += 1
if curr_buffer:
return bytes(curr_buffer)
except OSError as e:
LOGGER.warning(e)
pass
def audio_datalist_set_volume(self, datalist, volume):
""" Change value of list of audio chunks """
sound_level = (volume / 100.)
for i in range(len(datalist)):
chunk = numpy.fromstring(datalist[i], numpy.int16)
chunk = chunk * sound_level
datalist[i] = chunk.astype(numpy.int16)
if __name__ == '__main__':
input_index = int(input("Input:\t"))
output_index = int(input("Output:\t"))
ng = NoiseGate(_input_device_index=input_index, _output_device_index=output_index)
ng.list_devices()
ng.start()

11
Client/pdab/getDevices.py Normal file
View File

@@ -0,0 +1,11 @@
from NoiseGatev2 import AudioStream
print('Getting a list of devices')
list_of_devices = AudioStream().list_devices()
print("----- INPUT DEVICES -----")
for inputDevice in list_of_devices['Input']:
print(f"{inputDevice}\t-\t{list_of_devices['Input'][inputDevice]}")
print("----- OUTPUT DEVICES -----")
for outputDevice in list_of_devices['Output']:
print(f"{outputDevice}\t-\t{list_of_devices['Output'][outputDevice]}")

81
Client/pdab/main.py Normal file
View File

@@ -0,0 +1,81 @@
import argparse, platform, os
from discord import Intents, Client, Member, opus, Activity, ActivityType
from discord.ext import commands
from NoiseGatev2 import NoiseGate
# Load the proper OPUS library for the device being used
async def load_opus():
# Check the system type and load the correct library
# Linux ARM AARCH64 running 32bit OS
processor = platform.machine()
script_dir = os.path.dirname(os.path.abspath(__file__))
print("Processor: ", processor)
if os.name == 'nt':
if processor == "AMD64":
opus.load_opus(os.path.join(script_dir, './opus/libopus_amd64.dll'))
print(f"Loaded OPUS library for AMD64")
return "AMD64"
else:
if processor == "aarch64":
opus.load_opus(os.path.join(script_dir, './opus/libopus_aarcch64.so'))
print(f"Loaded OPUS library for aarch64")
return "aarch64"
elif processor == "armv7l":
opus.load_opus(os.path.join(script_dir, './opus/libopus_armv7l.so'))
print(f"Loaded OPUS library for armv7l")
return "armv7l"
def main(clientId='OTQzNzQyMDQwMjU1MTE1MzA0.Yg3eRA.ZxEbRr55xahjfaUmPY8pmS-RHTY', channelId=367396189529833476, NGThreshold=50, deviceId=1, presence="the radio"):
intents = Intents.default()
client = commands.Bot(command_prefix='!', intents=intents)
@client.event
async def on_ready():
print(f'We have logged in as {client.user}')
# Set the presence of the bot (what it's listening to)
await client.change_presence(activity=Activity(type=ActivityType.listening, name=presence))
channelIdToJoin = client.get_channel(channelId)
print("Channel", channelIdToJoin)
print("Loading opus")
await load_opus()
if opus.is_loaded():
print("Joining voice")
channelConnection = await channelIdToJoin.connect(timeout=60.0, reconnect=True)
print("Voice Connected")
streamHandler = NoiseGate(
_input_device_index=deviceId,
_voice_connection=channelConnection,
_noise_gate_threshold=NGThreshold)
# Start the audio stream
streamHandler.run()
print("stream running")
client.run(clientId)
if __name__ == "__main__":
parser = argparse.ArgumentParser()
parser.add_argument("deviceId", type=int, help="The ID of the audio device to use")
parser.add_argument("channelId", type=int, help="The ID of the voice channel to use")
parser.add_argument("clientId", type=str, help="The discord client ID")
parser.add_argument("-n", "--NGThreshold", type=int, help="Change the noisegate threshold. This defaults to 50")
parser.add_argument("-p", "--presence", type=str, help="What the bot should be listening to")
args = parser.parse_args()
if (not args.NGThreshold):
args.NGThreshold = 50
print("Arguments:", args)
main(
clientId=args.clientId,
channelId=args.channelId,
NGThreshold=args.NGThreshold,
deviceId=args.deviceId,
presence=args.presence
)

Binary file not shown.

Binary file not shown.

Binary file not shown.

180
Client/pdab/recorder.py Normal file
View File

@@ -0,0 +1,180 @@
import pyaudio
import wave, logging, threading, time, queue, signal, argparse, audioop
from os import path, makedirs
logging.basicConfig(format="%(asctime)s: %(message)s", level=logging.INFO,datefmt="%H:%M:%S")
class DiscordRecorder:
def __init__(self, DEVICE_ID, CHUNK = 960, FORMAT = pyaudio.paInt16, CHANNELS = 2, RATE = 48000, FILENAME = "./recs/radio.wav"):
self.pa_instance = pyaudio.PyAudio()
self.DEVICE_ID = DEVICE_ID
self.CHUNK = CHUNK
self.FORMAT = FORMAT
self.CHANNELS = CHANNELS
self.RATE = RATE
self.NG_fadeout = 240/20 # Fadeout value used to hold the noisegate after de-triggering
self.NG_fadeout_count = 0 # A count set when the noisegate is triggered and was de-triggered
self.process_set_count = 0 # Counts how many processes have been made
self.FILENAME = FILENAME
self._check_file_path_exists()
self.queued_frames = queue.Queue()
self.stop_threads = threading.Event()
self.recording_thread = None
self.saving_thread = None
self.running_stream = None
# Wrapper to check if the given filepath (not file itself) exists
def _check_file_path_exists(self):
if not path.exists(path.dirname(self.FILENAME)):
makedirs(path.dirname(self.FILENAME), exist_ok=True)
# Wrapper for the recorder thread; Adds new data to the queue
def _recorder(self):
logging.info("* Recording Thread Starting")
while True:
try:
curr_buffer = bytearray(self.stream.stream.read(self.CHUNK))
buffer_rms = audioop.rms(curr_buffer, 2)
if buffer_rms > 0:
buffer_decibel = 20 * math.log10(buffer_rms)
if self.process_set_count % 10 == 0:
if buffer_decibel >= self.stream.THRESHOLD:
LOGGER.debug(f"[Noisegate Open] {buffer_decibel} db")
else:
LOGGER.debug(f"[Noisegate Closed] {buffer_decibel} db")
if buffer_decibel >= self.stream.THRESHOLD:
self.NG_fadeout_count = self.NG_fadeout
self.process_set_count += 1
if curr_buffer:
return self.queued_frames.put(curr_buffer)
else:
if self.NG_fadeout_count > 0:
self.NG_fadeout_count -= 1
LOGGER.debug(f"Frames in fadeout remaining: {self.NG_fadeout_count}")
self.process_set_count += 1
if curr_buffer:
return self.queued_frames.put(curr_buffer)
except OSError as e:
LOGGER.warning(e)
pass
# check for stop
if self.stop_threads.is_set():
break
# Wrapper for saver thread; Saves the queue to the file
def _saver(self):
logging.info("* Saving Thread Starting")
while True:
if not self.queued_frames.empty():
dequeued_frames = []
for i in range(self.queued_frames.qsize()):
dequeued_frames.append(self.queued_frames.get())
if not path.isfile(self.FILENAME):
wf = wave.open(self.FILENAME, 'wb')
wf.setnchannels(self.CHANNELS)
wf.setsampwidth(self.pa_instance.get_sample_size(self.FORMAT))
wf.setframerate(self.RATE)
wf.writeframes(b''.join(dequeued_frames))
wf.close()
else:
read_file = wave.open(self.FILENAME, 'rb')
read_file_data = read_file.readframes(read_file.getnframes())
read_file.close()
wf = wave.open(self.FILENAME, 'wb')
wf.setnchannels(self.CHANNELS)
wf.setsampwidth(self.pa_instance.get_sample_size(self.FORMAT))
wf.setframerate(self.RATE)
wf.writeframes(read_file_data)
wf.writeframes(b''.join(dequeued_frames))
wf.close()
# check for stop
if self.stop_threads.is_set():
break
time.sleep(5)
# Start the recording function
def start_recording(self):
logging.info("* Recording")
self.running_stream = self.pa_instance.open(
input_device_index=self.DEVICE_ID,
format=self.FORMAT,
channels=self.CHANNELS,
rate=self.RATE,
input=True,
frames_per_buffer=self.CHUNK
)
self.recording_thread = threading.Thread(target=self._recorder)
self.recording_thread.start()
self.saving_thread = threading.Thread(target=self._saver)
self.saving_thread.start()
# Stop the recording function
def stop_recording(self):
self.stop_threads.set()
self.recording_thread.join()
self.saving_thread.join()
self.running_stream.stop_stream()
self.running_stream.close()
self.pa_instance.terminate()
logging.info("* Done recording")
class GracefulExitCatcher:
def __init__(self, stop_callback):
self.stop = False
# The function to run when the exit signal is caught
self.stop_callback = stop_callback
# Update what happens when these signals are caught
signal.signal(signal.SIGINT, self.exit_gracefully)
signal.signal(signal.SIGTERM, self.exit_gracefully)
def exit_gracefully(self, *args):
logging.info("* Stop signal caught...")
# Stop the main loop
self.stop = True
# Run the given callback function
self.stop_callback()
if __name__ == "__main__":
parser = argparse.ArgumentParser()
parser.add_argument("deviceId", type=int, help="The ID of the audio device to use")
parser.add_argument("filename", type=str, help="The filepath/filename of the output file")
args = parser.parse_args()
logging.debug("Arguments:", args)
recorder = DiscordRecorder(args.deviceId, FILENAME=args.filename)
exit_catcher = GracefulExitCatcher(recorder.stop_recording)
recorder.start_recording()
while not exit_catcher.stop:
time.sleep(1)

View File

@@ -0,0 +1,5 @@
discord>=2.2.3
PyNaCl>=1.5.0
pyaudio>=0.2.13
numpy==1.24.3
argparse

View File

@@ -0,0 +1,183 @@
.node-card {
position: relative;
display: flex;
flex-direction: column;
min-width: 0;
word-wrap: break-word;
background-color: #fff;
background-clip: border-box;
border: 1px solid #eff0f2;
border-radius: 1rem;
margin-bottom: 24px;
box-shadow: 0 2px 3px #e4e8f0;
}
.avatar-md {
height: 4rem;
width: 4rem;
}
.rounded-circle {
border-radius: 50% !important;
}
.img-thumbnail {
padding: 0.25rem;
background-color: #f1f3f7;
border: 1px solid #eff0f2;
border-radius: 0.75rem;
}
.avatar-title {
align-items: center;
background-color: #3b76e1;
color: #fff;
display: flex;
font-weight: 500;
height: 100%;
justify-content: center;
width: 100%;
}
.bg-soft-primary {
background-color: rgba(59, 118, 225, .25) !important;
}
a {
text-decoration: none !important;
}
.badge-soft-danger {
color: #f56e6e !important;
background-color: rgba(245, 110, 110, .1);
}
.badge-soft-success {
color: #63ad6f !important;
background-color: rgba(99, 173, 111, .1);
}
.mb-0 {
margin-bottom: 0 !important;
}
.badge {
display: inline-block;
padding: 0.25em 0.6em;
font-size: 75%;
font-weight: 500;
line-height: 1;
color: #fff;
text-align: center;
white-space: nowrap;
vertical-align: baseline;
border-radius: 0.75rem;
}
/* Info Card Section */
.info-card {
background-color: #fff;
border-radius: 10px;
border: none;
position: relative;
margin-bottom: 30px;
box-shadow: 0 0.46875rem 2.1875rem rgba(90, 97, 105, 0.1), 0 0.9375rem 1.40625rem rgba(90, 97, 105, 0.1), 0 0.25rem 0.53125rem rgba(90, 97, 105, 0.12), 0 0.125rem 0.1875rem rgba(90, 97, 105, 0.1);
}
.info-card .card-statistic .card-icon-large .bi {
font-size: 110px;
}
.info-card .card-statistic .card-icon {
text-align: center;
line-height: 50px;
margin-left: 15px;
color: #000;
position: absolute;
right: -5px;
top: 20px;
opacity: 0.1;
}
/* Info Card Background Colors */
.l-bg-cherry {
background: linear-gradient(to right, #493240, #f09) !important;
color: #fff;
}
.l-bg-blue-dark {
background: linear-gradient(to right, #373b44, #4286f4) !important;
color: #fff;
}
.l-bg-green-dark {
background: linear-gradient(to right, #0a504a, #38ef7d) !important;
color: #fff;
}
.l-bg-orange-dark {
background: linear-gradient(to right, #a86008, #ffba56) !important;
color: #fff;
}
.l-bg-cyan {
background: linear-gradient(135deg, #289cf5, #84c0ec) !important;
color: #fff;
}
.l-bg-green {
background: linear-gradient(135deg, #23bdb8 0%, #43e794 100%) !important;
color: #fff;
}
.l-bg-orange {
background: linear-gradient(to right, #f9900e, #ffba56) !important;
color: #fff;
}
/* Global Section */
.sidebar-container {
min-height: 95vh;
}
.sidebar {
position: fixed;
top: 5vh;
bottom: 0;
left: 0;
}
/* User table section */
.label {
border-radius: 3px;
font-size: 1.1em;
font-weight: 600;
}
.user-list tbody td .user-subhead {
font-size: 1em;
font-style: italic;
}
.table thead tr th {
text-transform: uppercase;
font-size: 0.875em;
}
.table thead tr th {
border-bottom: 2px solid #e7ebee;
}
.table tbody tr td:first-child {
font-size: 1.125em;
font-weight: 300;
}
.table tbody tr td {
font-size: 0.875em;
vertical-align: middle;
border-top: 1px solid #e7ebee;
padding: 12px 8px;
}

View File

@@ -0,0 +1,402 @@
$(document).ready(async () => {
console.log("Loading stored notifications...");
await loadStoredToasts();
console.log("Showing stored notifications...");
await showStoredToasts();
});
/**
* Gets all toasts stored in local storage
*
* @returns {Object} Object of toasts in storage
*/
function getStoredToasts() {
if (localStorage.getItem("toasts")) {
const storedToasts = JSON.parse(localStorage.getItem("toasts"));
console.log("LOADED STORED TOASTS: ", storedToasts);
navbarUpdateNotificationBellCount(storedToasts);
return storedToasts;
}
else return false
}
/**
* Adds a toast to storage, will not allow duplicates
*
* @param {Date} time The date object from when the toast was created
* @param {*} message The message of the toast
*/
function addToastToStorage(time, message) {
var toasts = [{ 'time': time, 'message': message }]
var storedToasts = getStoredToasts();
console.log("Adding new notification to storage: ", toasts);
if (storedToasts) {
toasts = toasts.concat(storedToasts);
console.log("Combined new and stored notifications: ", toasts);
toasts = toasts.filter((value, index, self) =>
index === self.findIndex((t) => (
t.time === value.time && t.message === value.message
))
)
}
console.log("Deduped stored notifications: ", toasts);
localStorage.setItem("toasts", JSON.stringify(toasts));
navbarUpdateNotificationBellCount(toasts);
}
/**
* Removes a toast from the local storage
*
* @param {Date} time The date object from when the toast was created
* @param {*} message The message of the toast
*/
function removeToastFromStorage(time, message) {
const toastToRemove = { 'time': time, 'message': message }
console.log("Toast to remove: ", toastToRemove);
var toasts = getStoredToasts();
console.log("Stored toasts: ", toasts);
if (toasts.indexOf(toastToRemove)) toasts.splice(toasts.indexOf(toastToRemove) - 1, 1)
console.log("Toasts with selected toast removed: ", toasts);
localStorage.setItem("toasts", JSON.stringify(toasts));
navbarUpdateNotificationBellCount(toasts);
}
/**
* Shows all stored toasts
*/
function showStoredToasts() {
const storedToasts = getStoredToasts();
if (!storedToasts) return
console.log("Loaded stored notifications to show: ", storedToasts);
for (const toast of storedToasts) {
const toastId = `${toast.time}-toast`;
console.log("Showing stored toast: ", toast, toastId);
const toastElement = bootstrap.Toast.getOrCreateInstance(document.getElementById(toastId));
toastElement.show();
}
}
/**
* Loads all toasts stored in the local storage into the DOM of the webpage
*/
function loadStoredToasts() {
const storedToasts = getStoredToasts();
if (!storedToasts) return
console.log("Loaded stored notifications: ", storedToasts);
for (const toast of storedToasts) {
createToast(toast.message, { time: toast.time })
}
}
/**
* Will update the count of notifications on the bell icon in the navbar
*
* @param {Array} storedToasts An array of stored toasts to be counted and updated in the navbar
*/
function navbarUpdateNotificationBellCount(storedToasts) {
const notificationBellIcon = document.getElementById("navbar-notification-bell");
var notificationBellCount = document.getElementById("notification-bell-icon-count");
if (!notificationBellCount) {
notificationBellCount = document.createElement('span');
notificationBellCount.id = "notification-bell-icon-count";
notificationBellCount.classList.add('badge');
notificationBellCount.classList.add('text-bg-secondary');
notificationBellCount.appendChild(document.createTextNode(storedToasts.length));
}
else notificationBellCount.innerHTML = storedToasts.length;
notificationBellIcon.appendChild(notificationBellCount);
}
/**
* Remove a frequency input from the DOM
*
* @param {string} system The system name to add the frequency to
* @param {string} inputId [OPTIONAL] The ID of input, this can be anything unique to this input. If this is not provided the number of frequencies will be used as the ID
*/
function addFrequencyInput(system, inputId = null) {
if (!inputId) inputId = $(`[id^="${system}_systemFreqRow_"]`).length;
// Create new input
var icon = document.createElement('i');
icon.classList.add('bi');
icon.classList.add('bi-x-circle');
icon.classList.add('text-black');
var remove = document.createElement('a');
remove.classList.add('align-middle');
remove.classList.add('float-left');
remove.href = '#'
remove.onclick = () => { removeFrequencyInput(`${system}_systemFreqRow_${inputId}`) }
remove.appendChild(icon);
var childColRemoveIcon = document.createElement('div');
childColRemoveIcon.classList.add('col-2');
childColRemoveIcon.appendChild(remove);
var input = document.createElement('input');
input.classList.add('form-control');
input.id = `${system}_systemFreq_${inputId}`;
input.type = 'text';
var childColInput = document.createElement('div');
childColInput.classList.add('col-10');
childColInput.appendChild(input);
var childRow = document.createElement('div');
childRow.classList.add("row");
childRow.classList.add("px-1");
childRow.appendChild(childColInput);
childRow.appendChild(childColRemoveIcon);
var colParent = document.createElement('div');
colParent.classList.add("col-md-6");
colParent.classList.add("mb-1");
colParent.id = `${system}_systemFreqRow_${inputId}`
colParent.appendChild(childRow);
document.getElementById(`frequencyRow_${system.replaceAll(" ", "_")}`).appendChild(colParent);
}
/**
* Add a toast element to the DOM
*
* @param {*} notificationMessage The message of the notification
* @param {Date} param1.time The date object for when the toast was created, blank if creating new
* @param {boolean} param1.showNow Show the toast now or just store it
* @returns
*/
function createToast(notificationMessage, { time = undefined, showNow = false } = {}) {
if (!time) time = new Date(Date.now());
else time = new Date(Date.parse(time));
const toastTitle = document.createElement('strong');
toastTitle.classList.add('me-auto');
toastTitle.appendChild(document.createTextNode("Server Notification"));
const toastTime = document.createElement('small');
toastTime.appendChild(document.createTextNode(time.toLocaleString()));
const toastClose = document.createElement('button');
toastClose.type = 'button';
toastClose.classList.add('btn-close');
toastClose.ariaLabel = 'Close';
toastClose.setAttribute('data-bs-dismiss', 'toast');
toastClose.onclick = () => { removeToastFromStorage(time.toISOString(), notificationMessage); };
const toastHeader = document.createElement('div');
toastHeader.classList.add('toast-header');
toastHeader.appendChild(toastTitle);
toastHeader.appendChild(toastTime);
toastHeader.appendChild(toastClose);
const toastMessage = document.createElement('p');
toastMessage.classList.add("px-2");
toastMessage.appendChild(document.createTextNode(notificationMessage));
const toastBody = document.createElement('div');
toastBody.classList.add('toast-body');
toastBody.appendChild(toastMessage);
const wrapperDiv = document.createElement('div');
wrapperDiv.classList.add('toast');
//wrapperDiv.classList.add('position-fixed');
wrapperDiv.id = `${time.toISOString()}-toast`;
wrapperDiv.role = 'alert';
wrapperDiv.ariaLive = 'assertive';
wrapperDiv.ariaAtomic = true;
wrapperDiv.setAttribute('data-bs-delay', "7500");
wrapperDiv.setAttribute('data-bs-animation', true);
wrapperDiv.appendChild(toastHeader);
wrapperDiv.appendChild(toastMessage);
document.getElementById("toastZone").appendChild(wrapperDiv);
addToastToStorage(time.toISOString(), notificationMessage);
if (showNow) {
const toastElement = bootstrap.Toast.getOrCreateInstance(document.getElementById(`${time.toISOString()}-toast`));
toastElement.show();
}
return;
}
function sendNodeHeartbeat(nodeId) {
const Http = new XMLHttpRequest();
const url = '/client/requestCheckIn' + nodeId;
Http.open("GET", url);
Http.send();
Http.onloadend = (e) => {
console.log(Http.responseText)
createToast(Http.responseText, { showNow: true });
}
}
function joinServer() {
const preset = document.getElementById("selectRadioPreset").value;
const clientId = document.getElementById("inputDiscordClientId").value;
const channelId = document.getElementById("inputDiscordChannelId").value;
const reqBody = {
'preset': preset,
'clientId': clientId,
'channelId': channelId
};
console.log(reqBody);
const Http = new XMLHttpRequest();
const url = '/bot/join';
Http.open("POST", url);
Http.setRequestHeader("Content-Type", "application/json");
Http.send(JSON.stringify(reqBody));
Http.onloadend = (e) => {
const responseObject = JSON.parse(Http.responseText)
console.log(Http.status);
console.log(responseObject);
createToast(`${responseObject.name} will join shortly`);
location.reload();
}
}
function leaveServer() {
const reqBody = {};
const Http = new XMLHttpRequest();
const url = '/bot/leave';
Http.open("POST", url);
Http.setRequestHeader("Content-Type", "application/json");
Http.send(JSON.stringify(reqBody));
Http.onloadend = (e) => {
const responseObject = Http.responseText;
console.log(Http.status);
console.log(responseObject);
createToast(`${responseObject} is leaving`, { showNow: true });
}
}
function saveNodeDetails() {
const nodeName = document.getElementById("inputNodeName").value;
const nodeIp = document.getElementById("inputNodeIp").value;
const nodePort = document.getElementById("inputOrgName").value;
const nodeLocation = document.getElementById("inputNodeLocation").value;
const reqBody = {
'name': nodeName,
'ip': nodeIp,
'port': nodePort,
'location': nodeLocation
}
console.log("Request Body: ", reqBody);
const Http = new XMLHttpRequest();
const url = '/client';
Http.open("PUT", url);
Http.setRequestHeader("Content-Type", "application/json");
Http.send(JSON.stringify(reqBody));
Http.onloadend = (e) => {
const responseObject = JSON.parse(Http.responseText);
console.log(Http.status);
console.log(responseObject);
createToast(`Node Updated!`);
location.reload();
}
}
function addNewSystem() {
const nodeId = document.getElementById("nodeId").value;
const systemName = document.getElementById(`New System_systemName`).value;
const systemMode = document.getElementById(`New System_systemMode`).value;
const inputSystemFreqs = $(`[id^="New System_systemFreq_"]`);
let systemFreqs = [];
for (const inputFreq of inputSystemFreqs) {
systemFreqs.push(inputFreq.value);
}
const reqBody = {
'systemName': systemName,
'mode': systemMode,
'frequencies': systemFreqs
}
if (reqBody.mode == "p25") reqBody.trunkFile = 'none';
console.log("Request Body: ", reqBody);
const Http = new XMLHttpRequest();
const url = "/client/addPreset";
Http.open("POST", url);
Http.setRequestHeader("Content-Type", "application/json");
Http.send(JSON.stringify(reqBody));
Http.onloadend = (e) => {
const responseObject = Http.responseText
console.log(Http.status);
console.log(responseObject);
createToast(`${systemName} Added!`);
location.reload();
}
}
function updateSystem(systemName) {
const nodeId = document.getElementById("nodeId").value;
const systemMode = document.getElementById(`${systemName}_systemMode`).value;
const inputSystemFreqs = $(`[id^="${systemName}_systemFreq_"]`);
let systemFreqs = [];
for (const inputFreq of inputSystemFreqs) {
systemFreqs.push(inputFreq.value);
}
const reqBody = {
'systemName': systemName,
'mode': systemMode,
'frequencies': systemFreqs
}
if (reqBody.mode == "p25") reqBody.trunkFile = 'none';
console.log("Request Body: ", reqBody);
const Http = new XMLHttpRequest();
const url = "/client/updatePreset";
Http.open("POST", url);
Http.setRequestHeader("Content-Type", "application/json");
Http.send(JSON.stringify(reqBody));
Http.onloadend = (e) => {
const responseObject = Http.responseText;
console.log(Http.status);
console.log(responseObject);
createToast(`${systemName} Updated!`);
location.reload();
}
}
function removeSystem(systemName) {
const nodeId = document.getElementById("nodeId").value;
const reqBody = {
'systemName': systemName,
}
console.log("Request Body: ", reqBody);
const Http = new XMLHttpRequest();
const url = '/client/removePreset';
Http.open("POST", url);
Http.setRequestHeader("Content-Type", "application/json");
Http.send(JSON.stringify(reqBody));
Http.onloadend = (e) => {
const responseObject = Http.responseText;
console.log(Http.status);
console.log(responseObject);
createToast(`${systemName} Removed!`);
location.reload();
}
}
function requestNodeUpdate() {
}
function removeFrequencyInput(elementId) {
const element = document.getElementById(elementId);
element.remove();
}

View File

@@ -1,8 +0,0 @@
body {
padding: 50px;
font: 14px "Lucida Grande", Helvetica, Arial, sans-serif;
}
a {
color: #00B7FF;
}

View File

@@ -2,13 +2,26 @@
---
Explanation here
The client application communicates with the server through the provided API. Each client instance waits for join requests sent by users through Discord. Once a join request is received, the client uses the SDR application to tune into the specified radio preset. It then establishes a connection to Discord, allowing users to listen to the selected radio preset in real-time.
In addition to its interaction with the server, the client also has its own API and web application. This enables users to directly interface with the client, perform actions specific to the client application, and access relevant information about the connected SDR and radio presets.
## Requirements
---
### Hardware
- SBC
- [Orange Pi](https://www.amazon.com/dp/B0BN16ZLXB/r)
- [Raspberry Pi](https://www.canakit.com/raspberry-pi-4-4gb.html)
- [Rock Pi](https://www.okdo.com/us/p/okdo-rock-4-model-c-4gb-single-board-computer-rockchip-rk3399-t-arm-cortex-a72-cortex-a53/)
- SDR
- [Nooelec RTL-SDR v5 Bundle ](https://www.amazon.com/dp/B01GDN1T4S)
- [RTL-SDR Blog V3](https://www.amazon.com/dp/B0BMKB3L47)
- [Nooelec NESDR Mini](https://www.amazon.com/dp/B009U7WZCA)
- Proper Power Adapter (Sometimes comes in SBC Packs)
- SD Card (Sometimes comes in SBC Packs)
Requirements here (not modules, that will be installed with npm)

View File

@@ -0,0 +1,7 @@
#!/bin/bash
# This script should be another service on the machine to watch the main script for failures and restart it if there are any
( tail -f -n0 /opt/sdr-scanner/scanner_log & ) | grep -q ": cb transfer status: 1, canceling..."
systemctl restart radioNode.service
echo "Restarted SDR Scanner service"

View File

@@ -5,7 +5,7 @@ const botController = require("../controllers/botController");
/** GET bot status
* Check to see if the bot is online and if so, if it is currently connected to anything
*
* The status of the bot: 200 = client is online but not connected to discord, 201 = online on discord, 202 = connected to a channel, 500 + JSON = encountered error
* The status of the bot: 200 = connected to discord, 201 = not connected to discord, 500 + JSON = encountered error
* @returns status
*/
router.get('/status', botController.getStatus);
@@ -15,7 +15,9 @@ router.get('/status', botController.getStatus);
*
* @param req The request sent from the master
* @param req.body.channelId The channel ID to join
* @param req.body.clientId The discord Client ID to use when connecting to the server
* @param req.body.presetName The name of the preset to start listening to
* @param req.body.NGThreshold [OPTIONAL] The noisegate threshold, this will default to 50
*/
router.post('/join', botController.joinServer);
@@ -27,4 +29,20 @@ router.post('/join', botController.joinServer);
*/
router.post('/leave', botController.leaveServer);
/** POST bot start recording
* If the bot is in a channel, it will start to record what it hears
*
* The status of the bot: 200 = starting to record, 202 = already recording, 204 = not in a server, 500 + JSON = encountered error
* @returns status
*/
router.post('/startRecording', botController.startRecording);
/** POST bot stop recording
* If the bot is recording, it will stop recording
*
* The status of the bot: 200 = will stop the recording, 204 = not currently recording, 500 + JSON = encountered error
* @returns status
*/
router.post('/stopRecording', botController.stopRecording);
module.exports = router;

View File

@@ -2,22 +2,31 @@
const express = require('express');
const router = express.Router();
// Controllers
const clientController = require("../controllers/clientController");
const { requestCheckIn, getPresets, updatePreset, addNewPreset, removePreset, updateClient, updateClientConfigWrapper } = require("../controllers/clientController");
/** GET Request a check in from the client
* Queue the client to check in with the server
*
* The status of the checkin request: 200 = Queued
*/
router.get('/requestCheckIn', clientController.requestCheckIn);
router.get('/requestCheckIn', requestCheckIn);
/** GET Object of all known presets
* Query the client to get all the known presets
*/
router.get('/presets', clientController.getPresets);
router.get('/presets', getPresets);
/**
* PUT An update to the running client config (not radio config)
* @param {number} req.body.id The ID given to the node to update
* @param {string} req.body.name The name of the node
* @param {string} req.body.ip The IP the server can contact the node on
* @param {number} req.body.port The port the server can contact the node on
* @param {string} req.body.location The physical location of the node
*/
router.put('/', updateClientConfigWrapper);
/** POST Update to preset
* Join the channel specified listening to the specified freq/mode
*
* @param req The request sent from the master
* @param {string} req.body.systemName The name of the system to be updated
@@ -25,7 +34,7 @@ router.get('/presets', clientController.getPresets);
* @param {string} req.body.mode The listening mode for the SDR
* @param {string} req.body.trunkFile If the listening mode is digital this can be set to identify the communications
*/
router.post('/updatePreset', clientController.updatePreset);
router.post('/updatePreset', updatePreset);
/** POST Add new preset
* Join the channel specified listening to the specified freq/mode
@@ -36,6 +45,20 @@ router.post('/updatePreset', clientController.updatePreset);
* @param {string} req.body.mode The listening mode for the SDR
* @param {string} req.body.trunkFile If the listening mode is digital this can be set to identify the communications
*/
router.post('/addPreset', clientController.addNewPreset);
router.post('/addPreset', addNewPreset);
/** POST Remove a preset
*
* @param req The request sent from the master
* @param {string} req.body.systemName The name of the system to be updated
*/
router.post('/removePreset', removePreset);
/** POST Update the bot
*
* @param req The request sent from the master
*/
router.post('/updateClient', updateClient);
module.exports = router;

View File

@@ -1,9 +1,11 @@
var express = require('express');
var router = express.Router();
const { getFullConfig } = require('../utilities/configHandler');
/* GET home page. */
router.get('/', function(req, res, next) {
res.render('index', { title: 'Express' });
router.get('/', async function(req, res, next) {
const clientConfig = await getFullConfig();
res.render('index', { 'node': clientConfig });
});
module.exports = router;

View File

@@ -15,6 +15,7 @@ router.post('/start', radioController.openRadioSession);
/**
* POST Close the current radio session
* Response from the radio: 200: closed; 204: not connected
*/
router.post('/stop', radioController.closeRadioSession);

208
Client/setup.sh Normal file
View File

@@ -0,0 +1,208 @@
#!/bin/bash
# Check if the user is root
if [ "$EUID" -ne 0 ]
then echo "Please run as root"
exit
fi
# Prompt the user for reboot confirmation
read -p "This script will install all required components for the DRB client. Are you okay with rebooting afterward? 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,,}"
if [[ "$confirm" != "y" && "$confirm" != "yes" ]]; then
echo "Script will not reboot."
should_reboot=false
else
echo "Script will reboot after completion."
should_reboot=true
fi
echo "----- Starting Radio Node Client Install Script -----"
SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )
# Copy the example env file
cp .env.example .env
# Copy the radio config example file
cp config/radioPresets.json.EXAMPLE config/radioPresets.json
echo "----- Collecting Setup Information -----"
# Ask the user for input and store in variables
echo " \\Client Config"
read -p " Enter Node Name: " nodeName
read -p " Enter Node IP: " nodeIP
read -p " Enter Node Port: " nodePort
read -p " Enter Node Location: " nodeLocation
read -p " Enter Audio Device ID: " audioDeviceID
echo " \\Server Config"
read -p " Enter Server IP (leave blank if using hostname): " serverIP
if [ -z "$serverIP" ]; then
read -p " Enter Server Hostname: " serverHostname
fi
read -p " Enter Server Port: " serverPort
# Update the values in the env file using sed
sed -i "s/^AUDIO_DEVICE_ID=\".*\"$/AUDIO_DEVICE_ID=\"$audioDeviceID\"/" .env
sed -i "s/^CLIENT_NAME=\".*\"$/CLIENT_NAME=\"$nodeName\"/" .env
sed -i "s/^CLIENT_IP=\".*\"$/CLIENT_IP=\"$nodeIP\"/" .env
sed -i "s/^CLIENT_PORT=.*$/CLIENT_PORT=$nodePort/" .env
sed -i "s/^CLIENT_LOCATION=\".*\"$/CLIENT_LOCATION=\"$nodeLocation\"/" .env
if [ -z "$serverIP" ]; then
sed -i "s/^SERVER_HOSTNAME=\".*\"$/SERVER_HOSTNAME=\"$serverHostname\"/" .env
else
sed -i "s/^SERVER_IP=\".*\"$/SERVER_IP=\"$serverIP\"/" .env
fi
sed -i "s/^SERVER_PORT=\".*\"$/SERVER_PORT=\"$serverPort\"/" .env
echo "----- Config file has been updated -----"
# Display the updated values
echo "----- Start of Config File -----"
cat .env
echo "----- End of Config File -----"
echo "----- Getting Dependencies -----"
# 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
curl -fsSL https://deb.nodesource.com/setup_current.x | sudo -E bash -
# Update the system
apt-get update
apt-get upgrade -y
# Install the necessary packages
apt-get install -y nodejs portaudio19-dev libportaudio2 libpulse-dev pulseaudio apulse python3 python3-pip git
# Install the node packages from the project
npm i
# Install the python packages needed for the bot
pip install -r ./pdab/requirements.txt
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
echo "----- Setting up Radio Node Service -----"
# Setup bot service
echo "[Unit]
Description=Radio Node Service
After=network.target
[Service]
WorkingDirectory=$SCRIPT_DIR/
ExecStart=/usr/bin/node .
Restart=always
RestartDelay=10
Environment=\"DEBUG='client:*'\"
[Install]
WantedBy=multi-user.target" >> /etc/systemd/system/RadioNode.service
# Enable the Radio Node service
systemctl enable RadioNode.service
echo "----- Setting up Radio Node Update Service -----"
# Setup bot update service
echo "[Unit]
Description=Radio Node Updater Service
After=network.target
[Service]
WorkingDirectory=$SCRIPT_DIR/
ExecStart=/usr/bin/bash update.sh
Restart=on-failure
[Install]
WantedBy=multi-user.target" >> /etc/systemd/system/RadioNodeUpdater.service
# Install OP25
echo "----- Installing OP25 from Source -----"
# Clone the OP25 Git
cd /opt/
git clone https://github.com/boatbod/op25.git
cd op25
# Run the OP25 install script
bash ./install.sh
# Create the config file for the client or user to update later
cp /opt/op25/op25/gr-op25_repeater/apps/p25_rtl_example.json /opt/op25/op25/gr-op25_repeater/apps/radioNodeOP25Config.json
# Create the OP25 service
echo "[Unit]
Description=OP25 Service
After=network.target
[Service]
WorkingDirectory=/opt/op25/op25/gr-op25_repeater/apps
ExecStart=./multi_rx.py -c radioNodeOP25Config.json
Restart=always
[Install]
WantedBy=multi-user.target" >> /etc/systemd/system/OP25.service
echo "----- OP25 Setup Complete -----"
# Enable the OP25 service, don't start it though as the user needs to config
systemctl enable OP25.service
echo "----- OP25 Enabled; Please ensure to update the configuration and start the service -----"
# Move back to the directory that the user started in (might not be needed?)
cd $SCRIPT_DIR
echo "----- Setup Complete! -----"
# Reboot if the user confirmed earlier
if [ "$should_reboot" = true ]; then
echo "To configure the app, please go to http://$nodeIP:$nodePort"
echo "Thank you for joining the network!"
# 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 "To configure the app, please go to http://$nodeIP:$nodePort"
echo "Thank you for joining the network!"
echo "Please restart your device to complete the installation"
fi

34
Client/update.sh Normal file
View File

@@ -0,0 +1,34 @@
#!/bin/bash
# Check if the user is root
if [ "$EUID" -ne 0 ]
then echo "Please run as root"
exit
fi
# Sleeping to give the client time to respond to the requester
sleep 5
# Stating Message
echo "<!-- UPDATING ---!>"
# TODO - Add an updater for Stable Diffusion API
# Stop any running service
systemctl stop RadioNode
# Get the owner of the current working directory
cwd_owner=$(stat -c '%U' .)
# Update the git Repo as the owner of the current working directory
sudo su -l $cwd_owner -c 'git fetch'
sudo su -l $cwd_owner -c 'git pull'
# Install any new libraries
npm i
# Start the service
systemctl start RadioNode
# Update complete message
echo "<!--- UPDATE COMPLETE! ---!>"

View File

@@ -1,48 +1,42 @@
// Debug
const { DebugBuilder } = require("../utilities/debugBuilder.js");
const log = new DebugBuilder("client-bot", "configController");
const log = new DebugBuilder("client", "configController");
// Modules
const { nodeObject } = require("./utilities.js");
const { getPresets } = require("../utilities/updatePresets");
const { readFileSync } = require('fs');
const path = require("path");
require('dotenv').config();
function getConfig() {
const botConfigObj = JSON.parse(readFileSync(path.resolve("./config/botConfig.json")))
return botConfigObj;
}
exports.getConfig = getConfig;
const GuildID = process.env.GUILD_ID;
const ApplicationID = process.env.APPLICATION_ID;
const DeviceID = parseInt(process.env.AUDIO_DEVICE_ID);
const DeviceName = process.env.AUDIO_DEVICE_NAME;
function getGuildID() {
const parsedJSON = getConfig();
const guildID = parsedJSON.GuildID;
log.DEBUG("Guild ID: ", guildID);
return guildID;
log.DEBUG("Guild ID: ", GuildID);
return GuildID;
}
exports.getGuildID = getGuildID;
function getApplicationID() {
const parsedJSON = getConfig();
const appID = parsedJSON.ApplicationID;
log.DEBUG("Application ID: ", appID);
return appID;
log.DEBUG("Application ID: ", ApplicationID);
return ApplicationID;
}
exports.getApplicationID = getApplicationID;
function getDeviceID(){
const parsedJSON = getConfig();
const deviceID = parseInt(parsedJSON.DeviceID);
log.DEBUG("Device ID: ", deviceID);
return deviceID;
log.DEBUG("Device ID: ", DeviceID);
return DeviceID;
}
exports.getDeviceID = getDeviceID;
function getDeviceName(){
const parsedJSON = getConfig();
const deviceName = parsedJSON.DeviceName;
log.DEBUG("Device Name: ", deviceName);
return deviceName;
log.DEBUG("Device Name: ", DeviceName);
return DeviceName;
}
exports.getDeviceName = getDeviceID;
exports.getDeviceName = getDeviceID;
exports.getFullConfig = () => {
return new nodeObject({_id: process.env.CLIENT_ID, _ip: process.env.CLIENT_IP, _name: process.env.CLIENT_NAME, _port: process.env.CLIENT_PORT, _location: process.env.CLIENT_LOCATION, _nearbySystems: getPresets()});
}

View File

@@ -1,5 +1,28 @@
// Debug
const debug = require('debug');
// Read .env file to process.env
require('dotenv').config();
// Modules
const { writeFile } = require('fs');
const { inspect } = require('util');
const logLocation = process.env.LOG_LOCATION;
async function writeToLog(logMessage, appName) {
logMessage = String(logMessage + "\n");
writeFile(
logLocation ?? `./${appName}.log`,
logMessage,
{ encoding: "utf-8", flag: 'a+' },
(err) => {
if (err) console.error(err);
// file written successfully
return;
}
)
}
/**
* Create the different logging methods for a function
@@ -9,9 +32,38 @@ const debug = require('debug');
*/
exports.DebugBuilder = class DebugBuilder {
constructor(appName, fileName) {
this.INFO = debug(`${appName}:${fileName}:INFO`);
this.DEBUG = debug(`${appName}:${fileName}:DEBUG`);
this.WARN = debug(`${appName}:${fileName}:WARNING`);
this.ERROR = debug(`${appName}:${fileName}:ERROR`);
this.INFO = (...messageParts) => {
const _info = debug(`${appName}:${fileName}:INFO`);
_info(messageParts);
writeToLog(`${Date.now().toLocaleString('en-US', { timeZone: 'America/New_York' })} - ${appName}:${fileName}:INFO\t-\t${messageParts.map((messagePart, index, array) => {return inspect(messagePart)})}`, appName);
}
this.DEBUG = (...messageParts) => {
const _debug = debug(`${appName}:${fileName}:DEBUG`);
_debug(messageParts);
writeToLog(`${Date.now().toLocaleString('en-US', { timeZone: 'America/New_York' })} - ${appName}:${fileName}:DEBUG\t-\t${messageParts.map((messagePart, index, array) => {return inspect(messagePart)})}`, appName);
}
this.VERBOSE = (...messageParts) => {
const _verbose = debug(`${appName}:${fileName}:VERBOSE`);
_verbose(messageParts);
writeToLog(`${Date.now().toLocaleString('en-US', { timeZone: 'America/New_York' })} - ${appName}:${fileName}:VERBOSE\t-\t${messageParts.map((messagePart, index, array) => {return inspect(messagePart)})}`, appName);
}
this.WARN = (...messageParts) => {
const _warn = debug(`${appName}:${fileName}:WARNING`);
_warn(messageParts);
writeToLog(`${Date.now().toLocaleString('en-US', { timeZone: 'America/New_York' })} - ${appName}:${fileName}:WARNING\t-\t${messageParts.map((messagePart, index, array) => {return inspect(messagePart)})}`, appName);
}
this.ERROR = (...messageParts) => {
const _error = debug(`${appName}:${fileName}:ERROR`);
_error(messageParts);
writeToLog(`${Date.now().toLocaleString('en-US', { timeZone: 'America/New_York' })} - ${appName}:${fileName}:ERROR\t-\t${messageParts.map((messagePart, index, array) => {return inspect(messagePart)})}`, appName);
if (process.env.EXIT_ON_ERROR && process.env.EXIT_ON_ERROR > 0) {
writeToLog("!--- EXITING ---!", appName);
setTimeout(process.exit, process.env.EXIT_ON_ERROR_DELAY ?? 0);
}
}
}
}
}

View File

@@ -1,52 +0,0 @@
// Modules
const { promisify } = require('util');
const { exec } = require("child_process");
// Debug
const { DebugBuilder } = require("../utilities/debugBuilder.js");
// Global Vars
const log = new DebugBuilder("client-bot", "executeConsoleCommands");
const execCommand = promisify(exec);
async function executeAsyncConsoleCommand(consoleCommand) {
// Check to see if the command is a real command
// TODO needs to be improved
const acceptableCommands = [ "arecord -L" ];
if (!acceptableCommands.includes(consoleCommand)) {
log.WARN("Console command is not acceptable: ", consoleCommand);
return undefined;
}
log.DEBUG("Running console command: ", consoleCommand);
const tempOutput = await execCommand(consoleCommand);
const output = tempOutput.stdout.trim();
log.DEBUG("Executed Console Command Response: ", output)
// TODO add some error checking
return output;
}
exports.executeAsyncConsoleCommand = executeAsyncConsoleCommand;
async function returnAlsaDeviceObject() {
const listAlsaDevicesCommand = "arecord -L";
const commandResponse = await executeAsyncConsoleCommand(listAlsaDevicesCommand);
const brokenCommand = String(commandResponse).split('\n');
var devices = [];
var i = 0;
for (const responseLine of brokenCommand) {
if (String(responseLine) && !String(responseLine).match(/^\s/g)) {
const tempDevice = {
id: i,
name: responseLine
}
devices.push(tempDevice);
i += 1;
}
}
return devices;
}
exports.returnAlsaDeviceObject = returnAlsaDeviceObject;

View File

@@ -2,20 +2,25 @@
const { DebugBuilder } = require("../utilities/debugBuilder.js");
const log = new DebugBuilder("client", "httpRequests");
// Config
const config = require("../config/clientConfig");
require('dotenv').config();
// Modules
const http = require("http");
const { isJsonString } = require("./utilities.js");
exports.requestOptions = class requestOptions {
constructor(path, method, hostname = undefined, headers = undefined, port = undefined) {
if (method === "POST"){
this.hostname = hostname ?? config.serverConfig.hostname
this.path = path
this.port = port ?? config.serverConfig.port
this.method = method
if (["POST", "PUT"].includes(method)){
log.VERBOSE("Hostname Vars: ", hostname, process.env.SERVER_HOSTNAME, process.env.SERVER_IP);
if (hostname) this.hostname = hostname;
if (!this.hostname && process.env.SERVER_HOSTNAME) this.hostname = process.env.SERVER_HOSTNAME;
if (!this.hostname && process.env.SERVER_IP) this.hostname = process.env.SERVER_IP;
if (!this.hostname) throw new Error("No server hostname / IP was given when creating a request");
this.path = path;
this.port = port ?? process.env.SERVER_PORT;
this.method = method;
this.headers = headers ?? {
'Content-Type': 'application/json',
}
};
}
}
}
@@ -33,17 +38,36 @@ exports.sendHttpRequest = function sendHttpRequest(requestOptions, data, callbac
res.on('data', (data) => {
const responseObject = {
"statusCode": res.statusCode,
"body": JSON.parse(data)
"body": (isJsonString(data.toString())) ? JSON.parse(data.toString()) : data.toString()
};
log.DEBUG("Response Object: ", responseObject);
log.VERBOSE("Response Object: ", responseObject);
callback(responseObject);
})
}).on('error', err => {
log.ERROR('Error: ', err.message)
if (err.code === "ECONNREFUSED"){
// Bot refused connection, assumed offline
log.WARN("Connection Refused");
}
else log.ERROR('Error: ', err.message, err);
callback(undefined);
// TODO need to handle if the server is down
})
// Write the data to the request and send it
req.write(data)
req.end()
req.write(data);
req.end();
}
exports.onHttpError = function onHttpError(httpStatusCode) {
switch(httpStatusCode){
case 404:
// Endpoint not found
log.WARN("404 received");
break;
default:
// Unhandled HTTP error code
log.ERROR("HTTP request returned with status: ", httpStatusCode)
break;
}
}

View File

@@ -1,9 +0,0 @@
// Debug
const { DebugBuilder } = require("../utilities/debugBuilder.js");
const log = new DebugBuilder("client-bot", "messageHandler");
exports.replyToInteraction = async function replyToInteraction(interaction, message){
interaction.reply({ content: message, fetchReply: true })
.then((message) => log.DEBUG(`Reply sent with content ${message.content}`))
.catch((err) => log.ERROR(err));
}

View File

@@ -134,7 +134,7 @@ class audioConfig {
"instance_name": "audio0",
"device_name": deviceName,
"udp_port": port,
"audio_gain": 1.0,
"audio_gain": 2.0,
"number_channels": 1
}];
}

View File

@@ -3,15 +3,16 @@ const { DebugBuilder } = require("../utilities/debugBuilder.js");
const log = new DebugBuilder("client", "updateConfig");
// Modules
const replace = require('replace-in-file');
const { getFullConfig } = require("./configHandler.js");
class Options {
constructor(key, updatedValue) {
this.files = "./config/clientConfig.js";
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 (typeof updatedValue === "string") this.to = `"${key}": "${updatedValue}",`;
else this.to = `"${key}": ${updatedValue},`;
if (Array(["string", "number"]).includes(typeof updatedValue)) this.to = `${key}="${updatedValue}",`;
else this.to = `${key}=${updatedValue}`;
}
}
@@ -20,14 +21,83 @@ class Options {
* @param updatedId The updated ID assigned to the bot
*/
exports.updateId = (updatedId) => {
const options = new Options("id", updatedId);
this.updateConfig('CLIENT_ID', updatedId);
}
/**
* Wrapper to update any or all keys in the client config
*
* @param {Object} configObject Object with what keys you wish to update (node object format, will be converted)
* @param {number} configObject.id The ID given to the node to update
* @param {string} configObject.name The name of the node
* @param {string} configObject.ip The IP the server can contact the node on
* @param {number} configObject.port The port the server can contact the node on
* @param {string} configObject.location The physical location of the node
* @returns
*/
exports.updateClientConfig = (configObject) => {
const runningConfig = getFullConfig();
var updatedKeys = []
const configKeys = Object.keys(configObject);
if (configKeys.includes("id")) {
if (runningConfig.id != configObject.id) {
this.updateConfig('CLIENT_ID', configObject.id);
updatedKeys.push({'CLIENT_ID': configObject.id});
process.env.CLIENT_ID = configObject.id;
log.DEBUG("Updated ID to: ", configObject.id);
}
}
if (configKeys.includes("name")) {
if (runningConfig.name != configObject.name) {
this.updateConfig('CLIENT_NAME', configObject.name);
updatedKeys.push({'CLIENT_NAME': configObject.name});
process.env.CLIENT_NAME = configObject.name;
log.DEBUG("Updated name to: ", configObject.name);
}
}
if (configKeys.includes("ip")) {
if (runningConfig.ip != configObject.ip) {
this.updateConfig('CLIENT_IP', configObject.ip);
updatedKeys.push({'CLIENT_IP': configObject.ip});
process.env.CLIENT_IP = configObject.ip;
log.DEBUG("Updated ip to: ", configObject.ip);
}
}
if (configKeys.includes("port")) {
if (runningConfig.port != configObject.port) {
this.updateConfig('CLIENT_PORT', configObject.port);
updatedKeys.push({'CLIENT_PORT': configObject.port});
process.env.CLIENT_PORT = configObject.port;
log.DEBUG("Updated port to: ", configObject.port);
}
}
if (configKeys.includes("location")) {
if (runningConfig.location != configObject.location) {
this.updateConfig('CLIENT_LOCATION', configObject.location);
updatedKeys.push({'CLIENT_LOCATION': configObject.location});
process.env.CLIENT_LOCATION = configObject.location;
log.DEBUG("Updated location to: ", configObject.location);
}
}
return updatedKeys;
}
/**
*
* @param {string} key The config file key to update with the value
* @param {string} value The value to update the key with
*/
exports.updateConfig = function updateConfig(key, value) {
const options = new Options(key, value);
updateConfigFile(options, (updatedFiles) => {
// Do Something
log.DEBUG("Updated config file: ", updatedFiles);
})
}
/**
* Wrapper to write changes to the file
* @param options An instance of the Objects class specified to the key being updated
@@ -36,7 +106,7 @@ exports.updateId = (updatedId) => {
function updateConfigFile(options, callback){
replace(options, (error, changedFiles) => {
if (error) return console.error('Error occurred:', error);
log.DEBUG('Modified files:', changedFiles);
log.VERBOSE('Modified files:', changedFiles);
callback(changedFiles);
});
}

View File

@@ -17,7 +17,7 @@ function writePresets(presets, callback = undefined) {
// Error checking
if (err) throw err;
log.DEBUG("Write Complete");
if (callback) callback()
if (callback) callback(); else return
});
}
@@ -48,12 +48,14 @@ function convertFrequencyToHertz(frequency){
if (Number.isInteger(frequency)) {
log.DEBUG(`${frequency} is an integer.`);
// Check to see if the frequency has the correct length
if (frequency.toString().length >= 7 && frequency.toString().length <= 9) return frequency
if (frequency >= 1000000) return frequency
if (frequency >= 100 && frequency <= 999) return frequency * 1000000
log.WARN("Frequency hasn't matched filters: ", frequency);
}
else {
log.DEBUG(`${frequency} is a float value.`);
// Convert to a string to remove the decimal in place and then correct the length
return converter(frequency).from("MHz").to("Hz");
return parseInt(converter(frequency).from("MHz").to("Hz"));
}
} else {
log.DEBUG(`${frequency} is not a number`);
@@ -69,8 +71,9 @@ function convertFrequencyToHertz(frequency){
*/
exports.getPresets = function getPresets() {
const presetDir = path.resolve("./config/radioPresets.json");
log.DEBUG(`Getting presets from directory: '${presetDir}'`);
return JSON.parse(fs.readFileSync(presetDir));
log.DEBUG(`Getting presets from directory: '${presetDir}'`);
if (fs.existsSync(presetDir)) return JSON.parse(fs.readFileSync(presetDir));
else return {};
}
/**
@@ -116,5 +119,20 @@ exports.updatePreset = (systemName, callback, { frequencies = undefined, mode =
}
}
/**
* Deletes the specified system
*
* @param {string} systemName The name of the system being modified
* @param {function} callback The callback function to be called when the function completes
*/
exports.removePreset = (systemName, callback) => {
const presets = this.getPresets();
// Check if a system name was passed
if (systemName in presets) {
delete presets[systemName];
writePresets(presets, callback);
}
}

View File

@@ -0,0 +1,282 @@
// Modules
const { promisify } = require('util');
const { exec, spawn } = require("child_process");
const { resolve, dirname } = require('path');
const radioConfigHelper = require("../utilities/radioConfigHelper");
const presetWrappers = require("../utilities/updatePresets");
const converter = require("convert-units");
const fs = require('fs');
require('dotenv').config();
// Debug
const { DebugBuilder } = require("../utilities/debugBuilder.js");
// Global Vars
const log = new DebugBuilder("client", "executeConsoleCommands");
const execCommand = promisify(exec);
const radioBinPath = process.env.OP25_BIN_PATH;
/**
* An object containing the variables needed to run the local node
*/
exports.nodeObject = class nodeObject {
/**
*
* @param {*} param0._id The ID of the node
* @param {*} param0._name The name of the node
* @param {*} param0._ip The IP that the master can contact the node at
* @param {*} param0._port The port that the client is listening on
* @param {*} param0._location The physical location of the node
* @param {*} param0._nearbySystems An object array of nearby systems
*/
constructor({ _id = null, _name = null, _ip = null, _port = null, _location = null, _nearbySystems = null }) {
this.id = _id;
this.name = _name;
this.ip = _ip;
this.port = _port;
this.location = _location;
this.nearbySystems = _nearbySystems;
}
}
/**
*
* @param {*} consoleCommand
* @returns
*/
exports.executeAsyncConsoleCommand = async function executeAsyncConsoleCommand(consoleCommand) {
// Check to see if the command is a real command
// TODO needs to be improved
const acceptableCommands = [ "arecord -L", 'ipconfig', 'ip addr' ];
if (!acceptableCommands.includes(consoleCommand)) {
log.WARN("Console command is not acceptable: ", consoleCommand);
return undefined;
}
log.DEBUG("Running console command: ", consoleCommand);
const tempOutput = await execCommand(consoleCommand);
const output = tempOutput.stdout.trim();
log.DEBUG("Executed Console Command Response: ", output)
// TODO add some error checking
return output;
}
/**
*
* @param {*} process The process to close
* @returns {undefined} Undefined to replace the existing process in the parent
*/
exports.closeProcessWrapper = async (process) => {
log.INFO("Leaving the server");
if (!process) return undefined;
// Try to close the process gracefully
await process.kill(2);
// Wait 25 seconds and see if the process is still open, if it is force it close
await setTimeout(async () => {
if (process) await process.kill(9);
}, 25000)
return undefined;
}
/**
* This wrapper closes any open radio sessions and the opens a new one
*
* @returns {radioChildProcess} The process of the radio session for use
*/
exports.openRadioSessionWrapper = async (radioChildProcess, presetName) => {
if (radioChildProcess) radioChildProcess = await this.closeProcessWrapper(radioChildProcess);
const configChangeResult = await this.changeCurrentConfigWrapper(presetName);
// Throw an error to the client if the config change ran into an error
if (typeof configChangeResult === "string") return configChangeResult;
if (process.platform === "win32") {
log.DEBUG("Starting Windows OP25");
radioChildProcess = await spawn("C:\\Python310\\python.exe", [getRadioBinPath(), "-c", getRadioConfigPath()], { cwd: dirname(getRadioBinPath()) });
}
else {
log.DEBUG("Starting Linux OP25");
radioChildProcess = await spawn(getRadioBinPath(), ["-c", getRadioConfigPath()], { cwd: dirname(getRadioBinPath()) });
}
log.VERBOSE("Radio Process: ", radioChildProcess);
let fullOutput;
radioChildProcess.stdout.setEncoding('utf8');
radioChildProcess.stdout.on("data", (data) => {
log.VERBOSE("From Process: ", data);
fullOutput += data.toString();
});
radioChildProcess.stderr.on('data', (data) => {
log.VERBOSE(`stderr: ${data}`);
fullOutput += data.toString();
});
radioChildProcess.on('close', (code) => {
log.DEBUG(`child process exited with code ${code}`);
log.VERBOSE("Full output from radio: ", fullOutput);
});
radioChildProcess.on("error", (code, signal) => {
log.ERROR("Error from the radio process: ", code, signal);
});
// Starting the radio application
return radioChildProcess
}
/**
* Update the OP25 config with a preset
*
* @param {*} presetName The preset name to update the OP25 config file with
* @returns
*/
exports.changeCurrentConfigWrapper = async (presetName) => {
// Check if the given config is saved
log.DEBUG("Checking if provided preset is in the config");
const presetIsPresent = await checkIfPresetExists(presetName);
if (!presetIsPresent) return "No preset with given name found in config"; // No preset with the given name is in the config
// Check if the current config is the same as the preset given
try {
const currentConfig = readOP25Config();
if (currentConfig.channels && currentConfig.channels.name === presetName) {
log.DEBUG("Current config is the same as the preset given");
return undefined;
}
}
catch (err) {
log.WARN("Problem reading the config file, overwriting with the new config", err);
}
// Convert radioPreset to OP25 'cfg.json. file
log.DEBUG("Converting radioPreset to OP25 config");
const updatedConfigObject = convertRadioPresetsToOP25Config(presetName);
// Replace current JSON file with the updated file
writeOP25Config(updatedConfigObject, () => {
return updatedConfigObject;
})
}
/**
* Get the location of the 'multi_rx.py' binary from the config
*/
function getRadioBinPath(){
return resolve(radioBinPath);
}
/**
* Get the path of the config for the radio app (OP25) and set the global variable
*/
function getRadioConfigPath(){
let radioConfigDirPath = dirname(getRadioBinPath());
return resolve(`${radioConfigDirPath}/cfg.json`);
}
/**
* Write the given config to the JSON file in OP25 the bin dir
* @param config The full config to be written to the file
* @param {function} callback The function to be called when this wrapper completes
*/
function writeOP25Config(config, callback = undefined) {
log.DEBUG("Updating OP25 config with: ", config);
fs.writeFile(getRadioConfigPath(), JSON.stringify(config), (err) => {
// Error checking
if (err) {
log.ERROR(err);
throw err;
}
log.DEBUG("Write Complete");
if (callback) callback()
});
}
/**
* Get the current config file in use by OP25
* @returns {object|*} The parsed config object currently set in OP25
*/
function readOP25Config() {
const configPath = getRadioConfigPath();
log.DEBUG(`Reading from config path: '${configPath}'`);
const readFile = fs.readFileSync(configPath);
log.VERBOSE("File Contents: ", readFile.toString());
return JSON.parse(readFile);
}
/**
* Check to see if the preset name exists in the config
* @param {string} presetName The system name as saved in the preset
* @returns {true||false}
*/
function checkIfPresetExists(presetName) {
const savedPresets = presetWrappers.getPresets();
log.DEBUG("Found presets: ", savedPresets, presetName, Object.keys(savedPresets).includes(presetName));
if (!Object.keys(savedPresets).includes(presetName)) return false;
else return true;
}
/**
* Convert a radioPreset to OP25's cfg.json file
*/
function convertRadioPresetsToOP25Config(presetName){
const savedPresets = presetWrappers.getPresets();
let frequencyString = "";
for (const frequency of savedPresets[presetName].frequencies){
frequencyString += `${converter(frequency).from("Hz").to("MHz")},`
}
frequencyString = frequencyString.slice(0, -1);
let updatedOP25Config;
switch (savedPresets[presetName].mode){
case "p25":
updatedOP25Config = new radioConfigHelper.P25({
"systemName": presetName,
"controlChannelsString": frequencyString,
"tagsFile": savedPresets[presetName].trunkFile
});
break;
case "nbfm":
//code for nbfm here
updatedOP25Config = new radioConfigHelper.NBFM({
"frequency": frequencyString,
"systemName": presetName
});
break;
default:
throw new Error("Radio mode of selected preset not recognized");
}
log.DEBUG(updatedOP25Config);
return updatedOP25Config;
}
// Convert a buffer from the DB to JSON object
exports.BufferToJson = (buffer) => {
return JSON.parse(buffer.toString());
}
/**
* Check to see if the input is a valid JSON string
*
* @param {*} str The string to check for valud JSON
* @returns {true|false}
*/
exports.isJsonString = (str) => {
try {
JSON.parse(str);
} catch (e) {
return false;
}
return true;
}

View File

@@ -1,11 +1,169 @@
<!DOCTYPE html>
<html>
<head>
<title><%= title %></title>
<link rel='stylesheet' href='/stylesheets/style.css' />
</head>
<body>
<h1><%= title %></h1>
<p>Welcome to <%= title %></p>
</body>
</html>
<html lang="en" data-bs-theme="auto">
<head>
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>'<%=node.name%>' - Configuration</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet"
integrity="sha384-9ndCyUaIbzAi2FUVXJi0CjmCapSmO7SnpJef0486qhLnuZ2cdeRhO02iuK6FUUVM" crossorigin="anonymous">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.3.0/font/bootstrap-icons.css">
<link rel="stylesheet" href="/res/css/main.css">
</head>
<body>
<%- include('partials/navbar.ejs') %>
<div aria-live="polite" aria-atomic="true" class="position-relative">
<!-- Position it: -->
<!-- - `.toast-container` for spacing between toasts -->
<!-- - `top-0` & `end-0` to position the toasts in the upper right corner -->
<!-- - `.p-3` to prevent the toasts from sticking to the edge of the container -->
<div class="toast-container top-0 end-0 p-3 max" id="toastZone">
</div>
</div>
<div class="container">
<div class="card mb-4">
<div class="card-header">
<p>
<span class="fs-2 fw-semibold">
Node Details
</span>
</p>
</div>
<div class="card-body">
<div class="col-md-12 pt-2">
<label class="small mb-1" for="nodeStatus">Online Status:</label>
<span class="badge badge-soft-success mb-0 align-middle fs-6" id="nodeStatus">Online</span>
<br>
<div class="py-2"></div>
<!-- Join Server button-->
<a type="button" class="btn btn-info text-white" data-bs-toggle="modal" data-bs-target="#joinModal"
href="#">Join Server</a>
<!-- Leave Server button -->
<a type="button" class="btn btn-danger" href="#" onclick="leaveServer()">Leave Server</a>
<!-- Checkin with client button -->
<a type="button" class="btn btn-secondary" href="#" onclick="sendNodeHeartbeat('<%=node.id%>')">Check-in
with Node</a>
<!-- Update Client button -->
<a type="button" class="btn btn-warning disabled" href="#"
onclick="requestNodeUpdate('<%=node.id%>')">Update Node</a>
</div>
<hr>
<form>
<div class="row gx-3 mb-3">
<div class="col-md-6">
<label class="small mb-1" for="nodeId">Node ID (this is the assigned Node ID and cannot be
changed)</label>
<input class="form-control" id="nodeId" type="text" value="<%=node.id%>" disabled></input>
</div>
</div>
<div class="row gx-3 mb-3">
<div class="col-md-12">
<label class="small mb-1" for="inputNodeName">Node Name:</label>
<input class="form-control" id="inputNodeName" type="text" value="<%=node.name%>"></input>
</div>
</div>
<div class="row gx-3 mb-3">
<div class="col-md-4">
<label class="small mb-1" for="inputNodeIp">Node IP Address (that the server can
contact):</label>
<input class="form-control" id="inputNodeIp" type="text" value="<%=node.ip%>"></input>
</div>
<div class="col-md-2">
<label class="small mb-1" for="inputOrgName">Node Port (with the API):</label>
<input class="form-control" id="inputOrgName" type="number" value="<%=node.port%>"></input>
</div>
</div>
<div class="mb-3">
<label class="small mb-1" for="inputNodeLocation">Node Location (physical location):</label>
<input class="form-control" id="inputNodeLocation" type="location" value="<%=node.location%>"></input>
</div>
<h4>
Nearby Systems
</h4>
<hr>
<div class="row">
<div class="col-lg-12">
<div class="main-box no-header clearfix">
<div class="main-box-body clearfix">
<div class="table-responsive">
<table class="table user-list <% if(!node.online) { %>disabled<% } %>">
<thead>
<tr>
<th><span>System Name</span></th>
<th><span>Frequencies</span></th>
<th><span>Protocol</span></th>
<th>&nbsp;</th>
</tr>
</thead>
<tbody>
<% for(const system in node.nearbySystems){ %>
<tr>
<td>
<%= system %>
</td>
<td>
<% if(node.nearbySystems[system].frequencies.length> 1) { %>
<ul>
<% for(const frequency of node.nearbySystems[system].frequencies) { %>
<li>
<%=frequency%> MHz
</li>
<% } %>
</ul>
<% } else { const frequency=node.nearbySystems[system].frequencies[0] %>
<%=frequency%> MHz
<% } %>
</td>
<td>
<span class="label label-default text-uppercase">
<%= node.nearbySystems[system].mode %>
</span>
</td>
<td>
<a href="#" class="table-link text-info label" data-bs-toggle="modal"
data-bs-target="#updateSystemModal_<%=system.replaceAll(" ", " _")%>">
<i class="bi bi-pencil"></i>
</a>
<a class="table-link text-danger label" onclick="removeSystem('<%=system%>')">
<i class="bi bi-trash"></i>
</a>
</td>
</tr>
<% // Update system modal %>
<%- include("partials/modifySystemModal.ejs", {'system': system, 'frequencies' :
node.nearbySystems[system].frequencies, 'mode' : node.nearbySystems[system].mode}) %>
<% } %>
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
<!-- Save changes button-->
<button class="btn btn-primary" type="button" onclick="saveNodeDetails()">Save changes</button>
<!-- Button trigger modal -->
<button type="button" class="btn btn-primary float-right" data-bs-toggle="modal"
data-bs-target="#updateSystemModal_New_System">Add New System</button>
</form>
</div>
</div>
</div>
<% // new System Modal %>
<%- include("partials/modifySystemModal.ejs", {'system': "New System" , 'frequencies' : [], 'mode' : '' }) %>
<% // Join Server Modal %>
<%- include("partials/joinModal.ejs", {'node': node}) %>
</body>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"
integrity="sha384-geWF76RCwLtnZ8qwWowPQNguL3RmwHVBC9FhGdlKrxdiJJigb/j/68SIy3Te4Bkz" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.7.0/jquery.min.js"
integrity="sha512-3gJwYpMe3QewGELv8k/BX9vcqhryRdzRMxVfq6ngyWXwo03GFEzjsUm8Q7RZcHPHksttq7/GFoxjCVUjkjvPdw=="
crossorigin="anonymous" referrerpolicy="no-referrer"></script>
<script src="/res/js/node.js"></script>
</html>

View File

@@ -0,0 +1,44 @@
<div class="modal fade" id="joinModal" tabindex="-1" aria-labelledby="joinModal" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<div class="modal-header">
<h1 class="modal-title fs-5" id="joinModal">Join Node <%=node.id%> to a Discord Server</h1>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<div class="container">
<div class="card mb-4">
<div class="card-body">
<form>
<div class="row gx-3 mb-3">
<div class="col-md-12">
<label class="small mb-1" for="inputDiscordClientId">Discord Client ID:</label>
<input class="form-control" id="inputDiscordClientId" type="text" value="" required></input>
</div>
</div>
<div class="row gx-3 mb-3">
<div class="col-md-6">
<label class="small mb-1" for="inputDiscordChannelId">Discord Channel ID:</label>
<input class="form-control" id="inputDiscordChannelId" type="text" value="" required></input>
</div>
<div class="col-md-6">
<label class="small mb-1" for="selectRadioPreset">Selected Preset:</label>
<select class="custom-select" id="selectRadioPreset">
<% for(const system in node.nearbySystems) { %>
<option value="<%=system%>"><%=system%></option>
<% } %>
</select>
</div>
</div>
</form>
</div>
</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="button" class="btn btn-primary" onclick="joinServer()">Join</button>
</div>
</div>
</div>
</div>

View File

@@ -0,0 +1,61 @@
<div class="modal fade" id="updateSystemModal_<%=system.replaceAll(" ", "_")%>" tabindex="-1" aria-labelledby="updateSystemModal_<%=system.replaceAll(" ", "_")%>"
aria-hidden="true" data-bs-backdrop="static" data-bs-keyboard="false">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<div class="modal-header">
<h1 class="modal-title fs-5" id="updateSystemModal_<%=system.replaceAll(" ", "_")%>"><%if (!system == "New System") {%>Update<%} else {%>Add a<%}%> <%=system%></h1>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<div class="card mb-4">
<div class="card-body">
<form>
<div class="row gx-3 mb-3">
<label class="small mb-1 fs-6" for="systemName">System Name</label>
<input class="form-control" id="<%=system%>_systemName" type="text" value="<%if (system != "New System") {%><%= system %><%} else {%>Local Radio System<%}%>"></input>
</div>
<div class="row gx-3 mb-3" id="frequencyRow_<%=system.replaceAll(" ", "_")%>">
<label class="small mb-1 fs-6" for="systemFreq">Frequencies</label>
<% for(const frequency of frequencies) { %>
<div class="col-md-6 mb-1" id="<%=system%>_systemFreqRow_<%=frequency%>">
<div class="row px-1">
<div class="col-10">
<input class="form-control" id="<%=system%>_systemFreq_<%=frequency%>" type="text" value="<%= frequency %>"></input>
</div>
<div class="col-2">
<a class="align-middle float-left" href="#" onclick="removeFrequencyInput('<%=system%>_systemFreqRow_<%=frequency%>')"><i class="bi bi-x-circle text-black"></i></a>
</div>
</div>
</div>
<% } %>
</div>
<button type="button" class="btn btn-info text-white" onclick="addFrequencyInput('<%=system%>')">Add Frequency</button>
<hr>
<div class="row gx-3 mb-3">
<div class="col-md-6">
<label class="small mb-1 fs-6" for="<%=system%>_systemMode">Mode</label>
<br>
<select class="custom-select" id="<%=system%>_systemMode">
<option value="<%= mode ?? 'select' %>" selected><span class="text-uppercase"><%= mode ?? 'Select' %></span></option>
<% if(mode == "p25") { %>
<option value="nbfm">NBFM</option>
<% } else if (mode == "nbfm") { %>
<option value="p25">P25</option>
<% } else { %>
<option value="nbfm">NBFM</option>
<option value="p25">P25</option>
<%}%>
</select>
</div>
</div>
</form>
</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal" onclick="location.reload()">Close</button>
<button type="button" class="btn btn-primary" <%if(!system == "New System") {%>onclick="updateSystem('<%=system%>')"<%} else {%>onclick="addNewSystem('<%=system%>')"<%}%>>Save changes</button>
</div>
</div>
</div>
</div>

View File

@@ -0,0 +1,42 @@
<nav class="navbar fixed-top navbar-expand-lg bg-body-tertiary" data-bs-theme="dark">
<div class="container-fluid">
<a class="navbar-brand" href="#">Node Master</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarSupportedContent"
aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarSupportedContent">
<ul class="navbar-nav me-auto mb-2 mb-lg-0">
<% /*
<li class="nav-item">
<a class="nav-link active" aria-current="page" href="#">Home</a>
</li>
<li class="nav-item">
<a class="nav-link" href="#">Link</a>
</li>
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle" href="#" role="button" data-bs-toggle="dropdown"
aria-expanded="false">
Dropdown
</a>
<ul class="dropdown-menu">
<li><a class="dropdown-item" href="#">Action</a></li>
<li><a class="dropdown-item" href="#">Another action</a></li>
<li>
<hr class="dropdown-divider">
</li>
<li><a class="dropdown-item" href="#">Something else here</a></li>
</ul>
</li>
*/%>
<li class="nav-item">
<a class="nav-link" id="navbar-notification-bell" onclick="showStoredToasts()"><i class="bi bi-bell-fill"></i></a>
</li>
</ul>
<form class="d-flex" role="search">
<input class="form-control me-2" type="search" placeholder="Search" aria-label="Search">
<button class="btn btn-outline-success" type="submit">Search</button>
</form>
</div>
</div>
</nav>

View File

@@ -1 +0,0 @@
DEBUG="server:*";

58
Server/.env.example Normal file
View File

@@ -0,0 +1,58 @@
# Discord Bot Configs
# Bot Token
TOKEN=""
# Client ID
clientId=""
# Prefix (deprecated)
PREFIX="^"
# ID of the Group than Can Admin The Bot
BOT_ADMINS=""
# Default Voice Channel to Join if None are Specified
DEFAULT_VOICE_CHANNEL_ID=""
# HTTP Server Config (DRB_CNC)
# HTTP Port to listen on
HTTP_PORT=3000
# MySQL Config for Emmelia
# Core DB Info and Login
EM_DB_HOST=""
EM_DB_USER=""
EM_DB_PASS=""
EM_DB_NAME=""
# Names of DB Tables
DB_RSS_FEEDS_TABLE="RSSFeeds"
DB_RSS_POSTS_TABLE="RSSPosts"
DB_ACCOUNTS_TABLE="accounts"
DB_TRANSACTIONS_TABLE="transactions"
DB_PRICING_TABLE="pricing"
# MySQL Config for Node Control
NODE_DB_HOST=''
NODE_DB_USER=''
NODE_DB_PASS=''
NODE_DB_NAME=''
# Node Config
# Time betwen check ins with the nodes
NODE_MONITOR_REFRESH_INTERVAL=100000
# RSS Config
# Interval between refreshes
RSS_REFRESH_INTERVAL=3000000
# OpenAI Config
# OpenAI Organization ID
OPENAI_ORG=""
# OpenAI API Key
OPENAI_KEY=""
# Stable Diffusion (Stability AI) Config
# API KEY
STABILITY_API_KEY=""
# General Config
# Exit when the program encounters and error (this may be ignored in some instances, and the error will exit the program either way)
EXIT_ON_ERROR=false
# Delay the exit of the program for X miliseconds, this can be used if you want to see what happens just after the error occurs or see if something else errors
EXIT_ON_ERROR_DELAY=0

7
Server/.gitignore vendored Normal file
View File

@@ -0,0 +1,7 @@
node_modules/
.env
package-lock.json
*.bak
*.log
*._.*
clientIds.json

15
Server/LICENSE.md Normal file
View File

@@ -0,0 +1,15 @@
ISC License
Copyright (c) 2021
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.

View File

@@ -1,48 +0,0 @@
var createError = require('http-errors');
var express = require('express');
var path = require('path');
var cookieParser = require('cookie-parser');
var logger = require('morgan');
var indexRouter = require('./routes/index');
var nodesRouter = require('./routes/nodes');
var adminRouter = require('./routes/admin');
var app = express();
// view engine setup
app.set('views', path.join(__dirname, 'views'));
app.set('view engine', 'ejs');
app.use(logger('dev'));
app.use(express.json());
app.use(express.urlencoded({ extended: false }));
app.use(cookieParser());
app.use(express.static(path.join(__dirname, 'public')));
// Web Interface
app.use('/', indexRouter);
// Nodes API
app.use('/nodes', nodesRouter);
// Admin API
app.use('/admin', adminRouter);
// catch 404 and forward to error handler
app.use((req, res, next) => {
next(createError(404));
});
// error handler
app.use((err, req, res, next) => {
// set locals, only providing error in development
res.locals.message = err.message;
res.locals.error = req.app.get('env') === 'development' ? err : {};
// render the error page
res.status(err.status || 500);
res.render('error');
});
module.exports = app;

View File

@@ -1,94 +0,0 @@
#!/usr/bin/env node
/**
* Module dependencies.
*/
var app = require('../app');
// Debug
const debug = require('debug')('server');
const { DebugBuilder } = require("../utilities/debugBuilder.js");
const log = new DebugBuilder("server", "www");
var http = require('http');
/**
* Get port from environment and store in Express.
*/
var port = normalizePort(process.env.PORT || '3000');
app.set('port', port);
/**
* Create HTTP server.
*/
var server = http.createServer(app);
/**
* Listen on provided port, on all network interfaces.
*/
server.listen(port);
server.on('error', onError);
server.on('listening', onListening);
/**
* Normalize a port into a number, string, or false.
*/
function normalizePort(val) {
var port = parseInt(val, 10);
if (isNaN(port)) {
// named pipe
return val;
}
if (port >= 0) {
// port number
return port;
}
return false;
}
/**
* Event listener for HTTP server "error" event.
*/
function onError(error) {
if (error.syscall !== 'listen') {
throw error;
}
var bind = typeof port === 'string'
? 'Pipe ' + port
: 'Port ' + port;
// handle specific listen errors with friendly messages
switch (error.code) {
case 'EACCES':
console.error(bind + ' requires elevated privileges');
process.exit(1);
break;
case 'EADDRINUSE':
console.error(bind + ' is already in use');
process.exit(1);
break;
default:
throw error;
}
}
/**
* Event listener for HTTP server "listening" event.
*/
function onListening() {
var addr = server.address();
var bind = typeof addr === 'string'
? 'pipe ' + addr
: 'port ' + addr.port;
log.DEBUG('Listening on ' + bind);
debug("testing");
}

View File

@@ -0,0 +1,6 @@
{
"[ID from Discord]": {
"name": "[Nickname of the Bot]",
"id": "[Client ID from Discord Dev Portal]"
}
}

47
Server/commands/add.js Normal file
View File

@@ -0,0 +1,47 @@
const libCore = require("../libCore.js");
const { SlashCommandBuilder } = require('discord.js');
const { DebugBuilder } = require("../utilities/debugBuilder");
const log = new DebugBuilder("server", "add");
module.exports = {
data: new SlashCommandBuilder()
.setName('add')
.setDescription('Add RSS Source')
.addStringOption(option =>
option.setName('title')
.setDescription('The title of the RSS feed')
.setRequired(true))
.addStringOption(option =>
option.setName('link')
.setDescription('The link to the RSS feed')
.setRequired(true))
.addStringOption(option =>
option.setName('category')
.setDescription('The category for the RSS feed *("ALL" by default")*')
.setRequired(false)),
example: "add [title] [https://domain.com/feed.xml] [category]",
isPrivileged: false,
async execute(interaction, args) {
try {
var title = interaction.options.getString('title');
var link = interaction.options.getString('link');
var category = interaction.options.getString('category');
if (!category) category = "ALL";
await libCore.addSource(title, link, category, interaction.guildId, interaction.channelId, (err, result) => {
log.DEBUG("Result from adding entry", result);
if (result) {
interaction.reply(`Adding ${title} to the list of RSS sources`);
} else {
interaction.reply(`${title} already exists in the list of RSS sources`);
}
});
}catch(err){
log.ERROR(err)
await interaction.reply(err.toString());
}
}
};

View File

@@ -0,0 +1,28 @@
const { SlashCommandBuilder } = require('discord.js');
const { DebugBuilder } = require("../utilities/debugBuilder");
const log = new DebugBuilder("server", "balance");
const { checkBalance } = require("../controllers/accountController");
module.exports = {
data: new SlashCommandBuilder()
.setName('balance')
.setDescription('Check your balance of AI tokens'),
example: "balance",
isPrivileged: false,
requiresTokens: false,
defaultTokenUsage: 0,
deferInitialReply: false,
async execute(interaction) {
try{
checkBalance(interaction.member.id, async (err, balance) => {
if (err) throw err;
await interaction.reply({ content: `${interaction.member.user}, you have ${balance} tokens remaining`, ephemeral: true })
})
}catch(err){
log.ERROR(err)
await interaction.reply(`Sorry ${interaction.member.user}, something went wrong`);
}
}
};

View File

@@ -0,0 +1,27 @@
var libCore = require("../libCore.js");
const { SlashCommandBuilder } = require('discord.js');
const { DebugBuilder } = require("../utilities/debugBuilder");
const log = new DebugBuilder("server", "categories");
module.exports = {
data: new SlashCommandBuilder()
.setName('categories')
.setDescription('Return all categories'),
example: "categories",
isPrivileged: false,
async execute(interaction) {
await libCore.getCategories(async (err, categoryResults) => {
if (err) throw err;
log.DEBUG("Returned Categories: ", categoryResults);
var categories = [];
for (const record of categoryResults) {
categories.push(record.category);
}
await interaction.reply(
`Categories: [${categories}]`
);
});
}
};

61
Server/commands/chat.js Normal file
View File

@@ -0,0 +1,61 @@
const { submitTextPromptTransaction } = require("../controllers/openAiController");
const { SlashCommandBuilder } = require('discord.js');
const { DebugBuilder } = require("../utilities/debugBuilder");
const log = new DebugBuilder("server", "chat");
const { EmmeliaEmbedBuilder } = require('../libUtils');
const COST_OF_COMMAND = 100
module.exports = {
data: new SlashCommandBuilder()
.setName('chat')
.setDescription(`Send a text prompt to ChatGPT`)
.addStringOption(option =>
option.setName('prompt')
.setDescription('The prompt to be sent to ChatGPT')
.setRequired(true))
.addBooleanOption(option =>
option.setName('public')
.setDescription("Set this to false if you would like the message to only be visible to you. *defaults to public*")
.setRequired(false))
.addNumberOption(option =>
option.setName('temperature')
.setDescription('Set the temperature, 0 = repetitive, 1 = random; Defaults to 0')
.setRequired(false))
.addNumberOption(option =>
option.setName('tokens')
.setDescription(`The max amount of tokens to be spent, defaults to ${COST_OF_COMMAND}`)
.setRequired(false)),
example: "chat [tell me a story] [0.07] [400]", // Need to figure out the tokens
isPrivileged: false,
requiresTokens: true,
defaultTokenUsage: 100,
deferInitialReply: true,
async execute(interaction) {
const promptText = interaction.options.getString('prompt');
const temperature = interaction.options.getNumber('temperature') ?? undefined;
const maxTokens = interaction.options.getNumber('tokens') ?? undefined;
const discordAccountId = interaction.member.id;
try {
submitTextPromptTransaction(promptText, temperature, maxTokens, discordAccountId, interaction, this, async (err, result) => {
if (err) throw err;
const gptEmbed = new EmmeliaEmbedBuilder()
.setColor(0x0099FF)
.setTitle(`New GPT response`)
.setDescription(`${interaction.member.user} sent: '${promptText}'`)
.addFields(
{ name: 'Generated Text', value: result.promptResult },
)
.addFields({ name: 'Tokens Used', value: `${result.totalTokens}`, inline: true })
await interaction.editReply({ embeds: [gptEmbed], ephemeral: false });
});
// Needs reply code to reply to the generation
}catch(err){
log.ERROR(err)
//await interaction.reply(err.toString());
}
}
};

18
Server/commands/exit.js Normal file
View File

@@ -0,0 +1,18 @@
const libUtils = require("../libUtils.js");
const discordAuth = require("../middleware/discordAuthorization");
const { SlashCommandBuilder } = require('discord.js');
module.exports = {
data: new SlashCommandBuilder()
.setName('exit')
.setDescription('Exit the current application.'),
example: "exit",
isPrivileged: true,
async execute(interaction) {
// TODO - Need to add middleware for admins
await interaction.reply(
`Goodbye world - Disconnection imminent.`
);
libUtils.runAfter(process.exit, 5000);
}
};

View File

@@ -0,0 +1,52 @@
const { SlashCommandBuilder } = require('discord.js');
const { DebugBuilder } = require("../utilities/debugBuilder");
const log = new DebugBuilder("server", "give-role");
module.exports = {
data: new SlashCommandBuilder()
.setName('give-role')
.setDescription('Use this command to give a role you have to another member.')
.addUserOption(option =>
option.setName('user')
.setDescription('The user you wish to give the role to ')
.setRequired(true))
.addRoleOption(option =>
option.setName('role')
.setDescription('The role you wish to give the selected user')
.setRequired(true)),
example: "give-role",
isPrivileged: false,
requiresTokens: false,
defaultTokenUsage: 0,
deferInitialReply: true,
/*async autocomplete(interaction) {
const focusedValue = interaction.options.getFocused();
},*/
async execute(interaction) {
try{
// The role to give to the user
const selectedRole = interaction.options.getRole('role');
// The user who should be given the role
var selectedUser = interaction.options.getUser("user");
selectedUser = interaction.guild.members.cache.get(selectedUser.id);
// The user who initiated the command
const initUser = interaction.member;
log.DEBUG("Give Role DEBUG: ", initUser, selectedRole, selectedUser);
// Check if the user has the role selected
if (!initUser.roles.cache.find(role => role.name === selectedRole.name)) return await interaction.editReply(`Sorry ${initUser}, you don't have the group ${selectedRole} and thus you cannot give it to ${selectedUser}`);
// Give the selected user the role and let both the user and the initiator know
await selectedUser.roles.add(selectedRole);
return await interaction.editReply(`Ok ${initUser}, ${selectedUser} has been given the ${selectedRole} role!`)
}catch(err){
log.ERROR(err)
//await interaction.reply(err.toString());
}
}
};

63
Server/commands/help.js Normal file
View File

@@ -0,0 +1,63 @@
const fs = require('fs');
const path = require('node:path');
const { SlashCommandBuilder } = require('discord.js');
const { DebugBuilder } = require("../utilities/debugBuilder");
const log = new DebugBuilder("server", "help");
const { EmmeliaEmbedBuilder } = require("../libUtils");
const commandsPath = path.resolve(__dirname, '../commands'); // Resolves from either working dir or __dirname
const commandFiles = fs.readdirSync(commandsPath).filter(file => file.endsWith('.js'));
module.exports = {
data: new SlashCommandBuilder()
.setName('help')
.setDescription('Display this help message'),
example: "help",
isPrivileged: false,
async execute(interaction) {
try{
generalCommandText = "";
paidCommandText = "";
for (const file of commandFiles) {
const filePath = path.join(commandsPath, file);
const command = require(filePath);
if (!command.isPrivileged){ // TODO - Need to add middleware for admins
if (!command.requiresTokens){
if (generalCommandText.length > 1 && generalCommandText.slice(-2) != `\n`){
generalCommandText += `\n\n`;
}
generalCommandText += `**/${command.data.name}** - *${command.data.description}*`;
if (command.example) generalCommandText += `\n\t\t***Usage:*** \`/${command.example}\``
}
else{
if (paidCommandText.length > 1 && paidCommandText.slice(-2) != `\n`){
paidCommandText += `\n\n`;
}
paidCommandText += `**/${command.data.name}** - *${command.data.description}*`;
if (command.example) paidCommandText += `\n\t\t***Usage:*** \`/${command.example}\``
}
}
}
const helpEmbed = new EmmeliaEmbedBuilder()
.setColor(0x0099FF)
.setTitle(`Help`)
.setDescription(`**General Commands**\n\n${generalCommandText}`)
.addFields(
{ name: 'Paid Commands', value: `${paidCommandText}` }
)
await interaction.reply({ embeds: [helpEmbed], ephemeral: true });
}catch(err){
log.ERROR(err)
//await interaction.reply(err.toString());
}
}
};

View File

@@ -0,0 +1,81 @@
const { submitImagePromptTransaction, DALLE_COLOR } = require("../controllers/openAiController");
const { SlashCommandBuilder } = require('discord.js');
const { DebugBuilder } = require("../utilities/debugBuilder");
const log = new DebugBuilder("server", "imagine");
const { EmmeliaEmbedBuilder } = require('../libUtils');
const COST_OF_COMMAND = 800;
module.exports = {
data: new SlashCommandBuilder()
.setName('imagine')
.setDescription(`Submit an image generation prompt to DALL-E`)
.addStringOption(option =>
option.setName('prompt')
.setDescription('The prompt to be sent to DALL-E')
.setRequired(true))
.addBooleanOption(option =>
option.setName('public')
.setDescription("Set this to false if you would like the message to only be visible to you. *defaults to public*")
.setRequired(false))
.addNumberOption(option =>
option.setName('images')
.setDescription('The number of images you wish to generate [1 - 10] *(defaults to 1)*')
.setRequired(false))
.addStringOption(option =>
option.setName('size')
.setDescription('The size of the images to be generated *defaults to 256px*')
.addChoices(
{ name: '1024px - 1000 tokens', value: '1024x1024' },
{ name: '512px - 900 tokens', value: '512x512' },
{ name: '256px - 800 tokens', value: '256x256' },
)
.setRequired(false)),
example: "imagine [the sinking of the titanic on acid] [4] [", // Need to figure out the tokens
isPrivileged: false,
requiresTokens: true,
defaultTokenUsage: COST_OF_COMMAND,
deferInitialReply: true,
async execute(interaction) {
const promptText = interaction.options.getString('prompt');
const images = interaction.options.getNumber('images') ?? undefined;
const size = interaction.options.getString('size') ?? undefined;
const discordAccountId = interaction.member.id;
try {
submitImagePromptTransaction(promptText, discordAccountId, images, size, interaction, this, async (err, imageResults) => {
if (err) throw err;
var dalleEmbeds = [];
log.DEBUG("Image Results: ", imageResults)
// Add the information post
dalleEmbeds.push(new EmmeliaEmbedBuilder()
.setColor(DALLE_COLOR)
.setTitle(`New Image Result`)
.setDescription(`${interaction.member.user} sent the prompt: '${promptText}'`)
);
// Add the images to the result
const imagesInResult = Array(imageResults.results).length
log.DEBUG("Images in the result: ", imagesInResult);
if (imagesInResult >= 1) {
for (const imageData of imageResults.results.data){
const imageUrl = imageData.url;
dalleEmbeds.push(new EmmeliaEmbedBuilder().setURL(imageUrl).setImage(imageUrl).setColor(DALLE_COLOR));
}
}
// Add the information post
dalleEmbeds.push(new EmmeliaEmbedBuilder()
.setColor(DALLE_COLOR)
.addFields({ name: 'Tokens Used', value: `${imageResults.totalTokens}`, inline: true })
.addFields({ name: 'Images Generated', value: `${imagesInResult}`, inline: true })
.addFields({ name: 'Image Size Requested', value: `${imagesInResult}`, inline: true })
);
await interaction.editReply({ embeds: dalleEmbeds, ephemeral: false });
});
// Needs reply code to reply to the generation
}catch(err){
log.ERROR(err)
//await interaction.reply(err.toString());
}
}
};

58
Server/commands/join.js Normal file
View File

@@ -0,0 +1,58 @@
// Modules
const { SlashCommandBuilder } = require('discord.js');
const { DebugBuilder } = require("../utilities/debugBuilder");
const { filterAutocompleteValues, filterPresetsAvailable } = require("../utilities/utils");
const { getOnlineNodes, getAllConnections } = require("../utilities/mysqlHandler");
const { joinServerWrapper } = require("../controllers/adminController");
// Global Vars
const log = new DebugBuilder("server", "join");
module.exports = {
data: new SlashCommandBuilder()
.setName('join')
.setDescription('Join the channel you are in with the preset you choose')
.addStringOption(option =>
option.setName("preset")
.setDescription("The preset you would like to listen to")
.setAutocomplete(true)
.setRequired(true)),
example: "join",
isPrivileged: false,
requiresTokens: false,
defaultTokenUsage: 0,
deferInitialReply: true,
async autocomplete(interaction) {
const nodeObjects = await new Promise((recordResolve, recordReject) => {
getOnlineNodes((nodeRows) => {
recordResolve(nodeRows);
});
});
const options = await filterPresetsAvailable(nodeObjects);
// Filter the results to what the user is entering
filterAutocompleteValues(interaction, options);
},
async execute(interaction) {
try{
const guildId = interaction.guild.id;
const presetName = interaction.options.getString('preset');
const channelId = interaction.member.voice.channel.id;
if (!channelId) return interaction.editReply(`You need to be in a voice channel, ${interaction.user}`);
log.DEBUG(`Join requested by: ${interaction.user.username}, to: '${presetName}', in channel: ${channelId} / ${guildId}`);
const connections = await getAllConnections();
log.DEBUG("Current Connections: ", connections);
const selectedClientId = await joinServerWrapper(presetName, channelId, connections);
await interaction.editReply(`Ok, ${interaction.member}. **${selectedClientId.name}** is joining your channel.`);
//await interaction.channel.send('**Pong.**'); // This will send a message to the channel of the interaction outside of the initial reply
}catch(err){
log.ERROR(err)
//await interaction.reply(err.toString());
}
}
}

47
Server/commands/leave.js Normal file
View File

@@ -0,0 +1,47 @@
// Modules
const { SlashCommandBuilder } = require('discord.js');
const { DebugBuilder } = require("../utilities/debugBuilder");
const { getAllClientIds, getKeyByArrayValue, filterAutocompleteValues } = require("../utilities/utils");
const { getAllConnections } = require('../utilities/mysqlHandler');
const { leaveServerWrapper } = require('../controllers/adminController');
// Global Vars
const log = new DebugBuilder("server", "leave");
module.exports = {
data: new SlashCommandBuilder()
.setName('leave')
.setDescription('Disconnect a bot from the server')
.addStringOption(option =>
option.setName("bot")
.setDescription("The bot to disconnect from the server")
.setAutocomplete(true)
.setRequired(true)),
example: "leave",
isPrivileged: false,
requiresTokens: false,
defaultTokenUsage: 0,
deferInitialReply: true,
async autocomplete(interaction) {
const connections = await getAllConnections();
const options = connections.map(conn => conn.clientObject.name);
await filterAutocompleteValues(interaction, options);
},
async execute(interaction) {
try{
const botName = interaction.options.getString('bot');
log.DEBUG("Bot Name: ", botName)
const clinetIds = await getAllClientIds();
log.DEBUG("Client names: ", clinetIds);
const clientDiscordId = getKeyByArrayValue(clinetIds, {'name': botName});
log.DEBUG("Selected bot: ", clinetIds[clientDiscordId]);
await leaveServerWrapper(clinetIds[clientDiscordId]);
await interaction.editReply(`**${clinetIds[clientDiscordId].name}** has been disconnected`); // This will reply to the initial interaction
//await interaction.channel.send('**word.**'); // This will send a message to the channel of the interaction outside of the initial reply
}catch(err){
log.ERROR(err)
//await interaction.reply(err.toString());
}
}
}

View File

@@ -1,8 +1,6 @@
// Utilities
const { replyToInteraction } = require('../utilities/messageHandler.js');
const { SlashCommandBuilder } = require('discord.js');
const { DebugBuilder } = require("../utilities/debugBuilder");
const log = new DebugBuilder("client", "ping");
const log = new DebugBuilder("server", "ping");
module.exports = {
data: new SlashCommandBuilder()
@@ -17,9 +15,15 @@ module.exports = {
*/
example: "ping",
isPrivileged: false,
requiresTokens: false,
defaultTokenUsage: 0,
deferInitialReply: false,
/*async autocomplete(interaction) {
const focusedValue = interaction.options.getFocused();
},*/
async execute(interaction) {
try{
await replyToInteraction(interaction, "Pong! I have Aids and now you do too!"); // TODO - Add insults as the response to this command
await interaction.channel.send('**Pong.**'); // TODO - Add insults as the response to this command
}catch(err){
log.ERROR(err)
//await interaction.reply(err.toString());

View File

@@ -0,0 +1,34 @@
const { SlashCommandBuilder } = require('discord.js');
const { DebugBuilder } = require("../utilities/debugBuilder");
const log = new DebugBuilder("server", "pricing");
const { EmmeliaEmbedBuilder } = require("../libUtils");
module.exports = {
data: new SlashCommandBuilder()
.setName('pricing')
.setDescription('Replies with the pricing for tokens'),
example: "pricing",
isPrivileged: false,
requiresTokens: false,
defaultTokenUsage: 0,
deferInitialReply: false,
async execute(interaction) {
try{
const pricingEmbed = new EmmeliaEmbedBuilder()
.setColor(0x0099FF)
.setTitle(`Emmelia's Pricing`)
.addFields(
{ name: 'Tokens', value: `Tokens are a shared currency that is used between all AI models. Each model is charges tokens differently however, so do keep this in mind. $1 = 45,000 tokens` },
{ name: 'Text (ChatGPT)', value: `Tokens are used in the prompt and in the response of a generation. The max tokens will not be breached by the combined prompt and response. Keep this is mind when using text generations. Each syllable is one token. This section is 50 tokens.` },
{ name: 'Images (DALL-E)', value: `Tokens are used for each generation, variation, and upscale. The image size also affects the amount of tokens used: 256px = 800 tokens, 512px = 900 tokens, 1024px = 1000 tokens` }
)
await interaction.reply({ embeds: [pricingEmbed] });
}catch(err){
log.ERROR(err)
//await interaction.reply(err.toString());
}
}
};

36
Server/commands/remove.js Normal file
View File

@@ -0,0 +1,36 @@
var libCore = require("../libCore.js");
const { SlashCommandBuilder } = require('discord.js');
const { DebugBuilder } = require("../utilities/debugBuilder");
const log = new DebugBuilder("server", "remove");
module.exports = {
data: new SlashCommandBuilder()
.setName('remove')
.setDescription('Remove an RSS source by it\'s title')
.addStringOption(option =>
option.setName('title')
.setDescription('The title of the source to remove')
.setRequired(true)),
example: "remove ['Leafly']",
isPrivileged: false,
requiresTokens: false,
async execute(interaction) {
try{
var title = interaction.options.getString("title");
libCore.deleteSource(title, (err, result) => {
log.DEBUG("Result from removing entry", result);
if (result) {
interaction.reply(`Removing ${title} from the list of RSS sources`);
} else {
interaction.reply(`${title} does not exist in the list of RSS sources`);
}
});
}catch(err){
log.ERROR(err)
interaction.reply(err.toString());
}
}
};

View File

@@ -0,0 +1,29 @@
var libCore = require("../libCore.js");
const { SlashCommandBuilder } = require('discord.js');
const { DebugBuilder } = require("../utilities/debugBuilder");
const log = new DebugBuilder("server", "sources");
module.exports = {
data: new SlashCommandBuilder()
.setName('sources')
.setDescription('Reply with all of the available sources'),
example: "sources",
isPrivileged: false,
requiresTokens: false,
async execute(interaction) {
try{
var sourceArray = libCore.getSources();
var sourceString = "";
sourceArray.forEach(source => {
sourceString +=`[${source.title}](${source.link}) \n`;
});
await interaction.reply(sourceString);
}catch(err){
log.ERROR(err)
//await interaction.reply(err.toString());
}
}
};

View File

@@ -0,0 +1,37 @@
const { SlashCommandBuilder } = require('discord.js');
const { DebugBuilder } = require("../utilities/debugBuilder");
const log = new DebugBuilder("server", "start-record");
const { getAllConnections } = require("../utilities/mysqlHandler");
const { requestOptions, sendHttpRequest } = require("../utilities/httpRequests");
module.exports = {
data: new SlashCommandBuilder()
.setName('start-record')
.setDescription('Starts recording all bots online'),
example: "start-record",
isPrivileged: false,
requiresTokens: false,
defaultTokenUsage: 0,
deferInitialReply: false,
async execute(interaction) {
try{
await interaction.reply(`Ok, ${interaction.member}. **Recording** will begin shorting.`);
// Get nodes online
getAllConnections((connections) => {
for (const connection of connections){
const reqOptions = new requestOptions("/bot/startRecording", "POST", connection.node.ip, connection.node.port);
sendHttpRequest(reqOptions, JSON.stringify({}), async (responseObj) => {
log.VERBOSE("Response Object from node: ", connection, responseObj);
if (!responseObj || !responseObj.statusCode == 202 || !responseObj.statusCode == 204) return false;
if (responseObj.statusCode >= 300) return log.ERROR(responseObj.body);
// Bot is recording
await interaction.channel.send(`**${connection.clientObject.name} is now recording**`);
});
}
});
}catch(err){
log.ERROR(err)
//await interaction.reply(err.toString());
}
}
};

View File

@@ -0,0 +1,37 @@
const { SlashCommandBuilder } = require('discord.js');
const { DebugBuilder } = require("../utilities/debugBuilder");
const log = new DebugBuilder("server", "stop-record");
const { getAllConnections } = require("../utilities/mysqlHandler");
const { requestOptions, sendHttpRequest } = require("../utilities/httpRequests");
module.exports = {
data: new SlashCommandBuilder()
.setName('stop-record')
.setDescription('Starts recording all bots online'),
example: "stop-record",
isPrivileged: false,
requiresTokens: false,
defaultTokenUsage: 0,
deferInitialReply: false,
async execute(interaction) {
try{
await interaction.reply(`Ok, ${interaction.member}. **Recording** will stop shorting.`);
// Get nodes online
getAllConnections((connections) => {
for (const connection of connections){
const reqOptions = new requestOptions("/bot/stopRecording", "POST", connection.node.ip, connection.node.port);
sendHttpRequest(reqOptions, JSON.stringify({}), async (responseObj) => {
log.VERBOSE("Response Object from node: ", connection, responseObj);
if (!responseObj || !responseObj.statusCode == 204) return false;
if (responseObj.statusCode >= 300) return log.ERROR(responseObj.body);
// Bot is recording
await interaction.channel.send(`**${connection.clientObject.name} has stopped recording**`);
});
}
});
}catch(err){
log.ERROR(err)
//await interaction.reply(err.toString());
}
}
};

View File

@@ -0,0 +1,37 @@
var libCore = require("../libCore.js");
const { SlashCommandBuilder } = require('discord.js');
const { DebugBuilder } = require("../utilities/debugBuilder");
const log = new DebugBuilder("server", "alert");
module.exports = {
data: new SlashCommandBuilder()
.setName('weather')
.setDescription('Get any current weather alerts in the state specified.')
.addStringOption(option =>
option.setName('state')
.setDescription('The state to get any current weather alerts from')
.setRequired(false)
.addChoices()),
example: "alert [state]",
isPrivileged: false,
requiresTokens: false,
async execute(interaction) {
try{
var question = encodeURIComponent(interaction.options.getString("state").join(" "));
var answerData = await libCore.weatherAlert(question);
answerData.forEach(feature => {
interaction.reply(`
${feature.properties.areaDesc}
${feature.properties.headline}
${feature.properties.description}
`);
});
}catch(err){
log.ERROR(err)
//await interaction.reply(err.toString());
}
}
};

View File

@@ -1,8 +0,0 @@
const databaseConfig = {
database_host: '100.20.1.45',
database_user: 'DRB_CNC',
database_password: 'baMbC6IAl$Rn7$h0PS',
database_database: 'DRB_CNC'
}
module.exports = databaseConfig;

View File

@@ -1,5 +0,0 @@
const discordConfig = {
channelID: '367396189529833476'
}
module.exports = discordConfig;

View File

@@ -0,0 +1,85 @@
const { DebugBuilder } = require("../utilities/debugBuilder");
const log = new DebugBuilder("server", "accountController");
const { UserStorage } = require("../libStorage");
const userStorage = new UserStorage();
/**
* Check to see if the discord ID has an account associated with it
* @param {*} _discordAccountId The discord account to look for
* @param {*} callback The callback function to be called
* @callback false|*
*/
exports.checkForAccount = (_discordAccountId, callback) => {
userStorage.getRecordBy("discord_account_id", _discordAccountId, (err, results) => {
if (err) return callback(err, undefined);
if (!results) return callback(undefined, false);
return callback(undefined, results);
})
}
/**
* Create an account from a discord ID
* @param {*} _discordAccountId
* @param {*} callback
*/
exports.createAccount = (_discordAccountId, callback) => {
if (!_discordAccountId) return callback(new Error("No discord account specified before creation"));
userStorage.saveAccount(_discordAccountId, (err, results) => {
if (err) return callback(err, undefined);
if (!results) return callback(new Error("No results from creating account"), undefined);
return callback(undefined, results);
})
}
exports.withdrawBalance = async (_withdrawAmount, _discordAccountId, callback) => {
userStorage.updateBalance('withdraw', _withdrawAmount, _discordAccountId, async (err, result) => {
if (err) return callback(err, undefined);
if(result) return callback(undefined, result);
return callback(undefined, undefined);
})
}
exports.verifyBalance = (_tokensToBeUsed, _accountId, callback) => {
userStorage.checkBalance(_tokensToBeUsed, _accountId, (err, results) => {
if (err) return callback(err, undefined);
if(!results) return callback(undefined, false);
return callback(undefined, true);
})
}
/**
* Check the given account for the token balance
*
* @param {*} _discordAccountId
* @param {*} callback
*/
exports.checkBalance = (_discordAccountId, callback) => {
if (!_discordAccountId) return callback(new Error("No discord account given to check balance of"), undefined);
userStorage.getRecordBy("discord_account_id", _discordAccountId, (err, accountRecord) => {
if (err) return callback(err, undefined);
if (!accountRecord) return callback(new Error("No account record given"), undefined);
return callback(undefined, accountRecord.balance);
})
}
exports.insufficientTokensResponse = (interaction) => {
log.DEBUG("INSUFFICIENT TOKENS RESPONSE")
return interaction.reply({ content: `Sorry ${interaction.member.user}, you don't have enough tokens for this purchase. Please contact your bot rep to increase your balance to have more fun playing around!`, ephemeral: true });
}
exports.welcomeResponse = (interaction) => {
log.DEBUG("WELCOME RESPONSE")
return interaction.reply({ content: `Hey there ${interaction.member.user}! You haven't ran any commands that require tokens before. I've gone ahead and created an account for you. When you get a chance reach out to your nearest bot rep to fill your balance to start playing around! In the meantime however, you can run \`/pricing\` to view the current pricing.`, ephemeral: true });
}

View File

@@ -1,12 +1,12 @@
// Config
const discordConfig = require("../config/discordConfig");
require('dotenv').config();
// Debug
const { DebugBuilder } = require("../utilities/debugBuilder.js");
const log = new DebugBuilder("server", "adminController");
// Utilities
const mysqlHandler = require("../utilities/mysqlHandler");
const utils = require("../utilities/utils");
const requests = require("../utilities/httpRequests");
const { getAllClientIds } = require("../utilities/utils");
const { getOnlineNodes, updateNodeInfo, addNodeConnection, getConnectionByNodeId, getNodeInfoFromId, checkNodeConnectionByClientId, removeNodeConnectionByNodeId } = require("../utilities/mysqlHandler");
const { requestOptions, sendHttpRequest } = require("../utilities/httpRequests");
/** Get the presets of all online nodes, can be used for functions
*
@@ -14,61 +14,26 @@ const requests = require("../utilities/httpRequests");
* @returns {*} A list of the systems online
*/
async function getPresetsOfOnlineNodes(callback) {
mysqlHandler.getOnlineNodes((onlineNodes) => {
let systems = {};
onlineNodes.forEach(onlineNode => {
systems[onlineNode.id] = utils.BufferToJson(onlineNode.nearbySystems);
});
callback(systems);
getOnlineNodes((onlineNodes) => {
return callback(onlineNodes);
});
}
async function requestNodeListenToPreset(preset, nodeId, callback) {
mysqlHandler.getNodeInfoFromId(nodeId, (nodeObject) =>{
reqOptions = new requests.requestOptions("/bot/join", "POST", nodeObject.ip, nodeObject.port);
requests.sendHttpRequest(reqOptions, JSON.stringify({
"channelID": discordConfig.channelID,
"presetName": preset
}), (responseObject) => {
callback(responseObject)
});
})
}
async function getNodeBotStatus(nodeId, callback) {
mysqlHandler.getNodeInfoFromId(nodeId, (nodeObject) =>{
reqOptions = new requests.requestOptions("/bot/status", "GET", nodeObject.ip, nodeObject.port, undefined, 5);
requests.sendHttpRequest(reqOptions, JSON.stringify({}), (responseObject) => {
getNodeInfoFromId(nodeId, (nodeObject) =>{
reqOptions = new requestOptions("/bot/status", "GET", nodeObject.ip, nodeObject.port, undefined, 5);
sendHttpRequest(reqOptions, JSON.stringify({}), (responseObject) => {
if (responseObject === false) {
// Bot is joined
}
else {
// Bot is free
}
callback(responseObject);
return callback(responseObject);
});
});
}
async function requestNodeLeaveServer(nodeId, callback) {
getNodeBotStatus(nodeId, (responseObject) => {
if (responseObject === false) {
// Bot is joined
mysqlHandler.getNodeInfoFromId(nodeId, (nodeObject) =>{
reqOptions = new requests.requestOptions("/bot/leave", "POST", nodeObject.ip, nodeObject.port);
requests.sendHttpRequest(reqOptions, JSON.stringify({}), (responseObject) => {
callback(responseObject);
});
});
}
else {
// Bot is free
callback(false);
}
})
}
/** Return to requests for the presets of all online nodes, cannot be used in functions
*
@@ -87,43 +52,161 @@ exports.getAvailablePresets = async (req, res) => {
*
* @param {*} req Express request parameter
* @var {*} req.body.preset The preset to join (REQ)
* @var {*} req.body.nodeId The specific node to join (OPT/REQ if more than one node has the preset)
* @var {*} req.body.nodeId The specific node to join (OPT/REQ if more than one node has the preset)
* @var {*} req.body.clientId The ID of the client that we want to join with
* @var {*} req.body.channelId The channel Id of the discord channel to join
* @param {*} res Express response parameter
*/
exports.joinPreset = async (req, res) => {
if (!req.body.preset) return res.status(400).json("No preset specified");
await getPresetsOfOnlineNodes((systems) => {
const systemsWithSelectedPreset = Object.values(systems).filter(nodePresets => nodePresets.includes(req.body.preset)).length
if (!systemsWithSelectedPreset) return res.status(400).json("No system online with that preset");
if (systemsWithSelectedPreset > 1) {
if (!req.body.nodeId) return res.status(175).json("Multiple locations with the selected channel, please specify a nodeID (nodeId)")
requestNodeListenToPreset(req.body.preset, req.body.nodeId, (responseObject) => {
if (responseObject === false) return res.status(400).json("Timeout reached");
return res.sendStatus(responseObject.statusCode);
});
}
else {
let nodeId;
if (!req.body.nodeId) nodeId = utils.getKeyByArrayValue(systems, req.body.preset);
else nodeId = req.body.nodeId;
requestNodeListenToPreset(req.body.preset, nodeId, (responseObject) => {
if (responseObject === false) return res.status(400).json("Timeout reached");
return res.sendStatus(responseObject.statusCode);
});
}
});
if (!req.body.preset) return res.status(400).json("No preset specified");
if (!req.body.nodeId) return res.status(400).json("No node ID specified");
if (!req.body.clientId) return res.status(400).json("No client ID specified");
if (!req.body.channelId) return res.status(400).json("No channel ID specified");
const preset = req.body.preset;
const nodeId = req.body.nodeId;
const clientId = req.body.clientId;
const channelId = req.body.channelId;
const joinedClient = await joinServerWrapper(preset, channelId, clientId, nodeId);
if (!joinedClient) return res.send(400).json("No joined client");
return res.status(200).json(joinedClient);
}
/** Request a node to join the server listening to a specific preset
*
* @param {*} req Express request parameter
* @param {*} res Express response parameter
* @var {*} req.body.nodeId The ID of the node to disconnect
*/
exports.leaveServer = async (req, res) => {
if (!req.body.nodeId) return res.status(400).json("No nodeID specified");
if (!req.body.nodeId) return res.status(400).json("No node ID specified");
requestNodeLeaveServer(req.body.nodeId, (responseObject) => {
if (responseObject === false) return res.status(400).json("Bot not joined to server");
return res.sendStatus(responseObject.statusCode);
});
}
const nodeId = req.body.nodeId;
const currentConnection = await getConnectionByNodeId(nodeId);
log.DEBUG("Current Connection for node: ", currentConnection);
if (!currentConnection) return res.status(400).json("Node is not connected")
await leaveServerWrapper(currentConnection.clientObject)
return res.status(200).json(currentConnection.clientObject.name);
}
/**
* * This wrapper will check if there is an available node with the requested preset and if so checks for an available client ID to join with
*
* @param {*} presetName The preset name to listen to on the client
* @param {*} channelId The channel ID to join the bot to
* @param {*} connections EITHER A collection of clients that are currently connected OR a single discord client ID (NOT dev portal ID) that should be used to join the server with
* @param {number} nodeId [OPTIONAL] The node ID to join with (will join with another node if given node is not available)
* @returns
*/
async function joinServerWrapper(presetName, channelId, connections, nodeId = 0) {
// Get nodes online
var onlineNodes = await new Promise((recordResolve, recordReject) => {
getOnlineNodes((nodeRows) => {
recordResolve(nodeRows);
});
});
// Check which nodes have the selected preset
onlineNodes = onlineNodes.filter(node => node.presets.includes(presetName));
log.DEBUG("Filtered Online Nodes: ", onlineNodes);
// Check if any nodes with this preset are available
var nodesCurrentlyAvailable = [];
for (const node of onlineNodes) {
const currentConnection = await getConnectionByNodeId(node.id);
log.DEBUG("Checking to see if there is a connection for Node: ", node, currentConnection);
if(!currentConnection) nodesCurrentlyAvailable.push(node);
}
log.DEBUG("Nodes Currently Available: ", nodesCurrentlyAvailable);
// If not, let the user know
if (!nodesCurrentlyAvailable.length > 0) return Error("All nodes with this channel are unavailable, consider swapping one of the currently joined bots.");
// If so, join with the first node
var availableClientIds = await getAllClientIds();
log.DEBUG("All clients: ", Object.keys(availableClientIds));
var selectedClientId;
if (typeof connections === 'string') {
for (const availableClientId of availableClientIds) {
if (availableClientId.discordId != connections ) selectedClientId = availableClientId;
}
}
else {
log.DEBUG("Open connections: ", connections);
for (const connection of connections) {
log.DEBUG("Used Client ID: ", connection);
availableClientIds = availableClientIds.filter(cid => cid.discordId != connection.clientObject.discordId);
}
log.DEBUG("Available Client IDs: ", availableClientIds);
if (!Object.keys(availableClientIds).length > 0) return log.ERROR("All client ID have been used, consider swapping one of the curretly joined bots or adding more Client IDs to the pool.")
selectedClientId = availableClientIds[0];
}
let selectedNode;
if (nodeId > 0) {
for(const availableNode of nodesCurrentlyAvailable){
if (availableNode.id == nodeId) selectedNode = availableNode;
}
}
if (!selectedNode) selectedNode = nodesCurrentlyAvailable[0];
const reqOptions = new requestOptions("/bot/join", "POST", selectedNode.ip, selectedNode.port);
const postObject = {
"channelId": channelId,
"clientId": selectedClientId.clientId,
"presetName": presetName
};
log.INFO("Post Object: ", postObject);
sendHttpRequest(reqOptions, JSON.stringify(postObject), async (responseObj) => {
log.VERBOSE("Response Object from node ", selectedNode, responseObj);
if (!responseObj || !responseObj.statusCode == 200) return false;
// Node has connected to discord
// Updating node Object in DB
const updatedNode = await updateNodeInfo(selectedNode);
log.DEBUG("Updated Node: ", updatedNode);
// Adding a new node connection
const nodeConnection = await addNodeConnection(selectedNode, selectedClientId);
log.DEBUG("Node Connection: ", nodeConnection);
});
return selectedClientId;
}
exports.joinServerWrapper = joinServerWrapper;
/**
*
* @param {*} clientIdObject The client ID object for the node to leave the server. Either 'clientId'||'name' can be set.
* @returns
*/
async function leaveServerWrapper(clientIdObject) {
if (!clientIdObject.clientId || !clientIdObject.name) return log.ERROR("Tried to leave server without client ID and/or Name");
const node = await checkNodeConnectionByClientId(clientIdObject);
reqOptions = new requestOptions("/bot/leave", "POST", node.ip, node.port);
const responseObj = await new Promise((recordResolve, recordReject) => {
sendHttpRequest(reqOptions, JSON.stringify({}), async (responseObj) => {
recordResolve(responseObj);
});
});
log.VERBOSE("Response Object from node ", node, responseObj);
if (!responseObj || !responseObj.statusCode == 202) return false;
// Node has disconnected from discor
// Removing the node connection from the DB
const removedConnection = removeNodeConnectionByNodeId(node.id);
log.DEBUG("Removed Node Connection: ", removedConnection);
return;
}
exports.leaveServerWrapper = leaveServerWrapper;

View File

@@ -2,33 +2,97 @@
const { DebugBuilder } = require("../utilities/debugBuilder.js");
const log = new DebugBuilder("server", "nodesController");
// Utilities
const mysqlHander = require("../utilities/mysqlHandler");
const { getAllNodes, addNewNode, updateNodeInfo, getNodeInfoFromId, getOnlineNodes } = require("../utilities/mysqlHandler");
const utils = require("../utilities/utils");
const { sendHttpRequest, requestOptions } = require("../utilities/httpRequests.js");
const { nodeObject } = require("../utilities/recordHelper.js");
const refreshInterval = process.env.NODE_MONITOR_REFRESH_INTERVAL ?? 1200000;
const digitalModes = ['p25'];
/**
* Check in with a singular node, mark it offline if it's offline and
*
* @param {*} node The node Object to check in with
*/
async function checkInWithNode(node) {
const reqOptions = new requestOptions("/client/requestCheckIn", "GET", node.ip, node.port)
sendHttpRequest(reqOptions, "", (responseObj) => {
if (responseObj) {
log.DEBUG("Response from: ", node.name, responseObj);
const onlineNode = new nodeObject({ _online: true, _id: node.id });
log.DEBUG("Node update object: ", onlineNode);
updateNodeInfo(onlineNode, (sqlResponse) => {
if (!sqlResponse) this.log.ERROR("No response from SQL object");
log.DEBUG("Updated node: ", sqlResponse);
return true
})
}
else {
log.DEBUG("No response from node, assuming it's offline");
const offlineNode = new nodeObject({ _online: false, _id: node.id });
log.DEBUG("Offline node update object: ", offlineNode);
updateNodeInfo(offlineNode, (sqlResponse) => {
if (!sqlResponse) this.log.ERROR("No response from SQL object");
log.DEBUG("Updated offline node: ", sqlResponse);
return false
})
}
})
}
exports.checkInWithNode = checkInWithNode;
/**
* Check in with all online nodes and mark any nodes that are actually offline
*/
async function checkInWithOnlineNodes() {
getOnlineNodes((nodes) => {
log.DEBUG("Online Nodes: ", nodes);
for (const node of nodes) {
checkInWithNode(node);
}
return;
});
}
exports.checkInWithOnlineNodes = checkInWithOnlineNodes;
/**
*
* @param {*} req Default express req from router
* @param {*} res Defualt express res from router
*/
exports.listAllNodes = async (req, res) => {
mysqlHander.getAllNodes((allNodes) => {
getAllNodes((allNodes) => {
res.status(200).json({
"nodes_online": allNodes
});
});
}
// Add a new node to the
/**
* Add a new node to the storage
* @param {*} req Default express req from router
* @param {*} res Defualt express res from router
*/
exports.newNode = async (req, res) => {
if (!req.body.name) return res.send(400)
if (!req.body.name) return res.status(400).json("No name specified for new node");
try {
// Try to add the new user with defaults if missing options
mysqlHander.addNewNode({
'name': req.body.name,
'ip': req.body.ip ?? null,
'port': req.body.port ?? null,
'location': req.body.location ?? null,
'nearbySystems': req.body.nearbySystems ?? null,
'online': req.body.online ?? 0
}, (queryResults) => {
const newNode = new nodeObject({
_name: req.body.name,
_ip: req.body.ip ?? null,
_port: req.body.port ?? null,
_location: req.body.location ?? null,
_nearbySystems: req.body.nearbySystems ?? null,
_online: (req.body.online == "true" || req.body.online == "True") ? true : false
});
addNewNode(newNode, (newNodeObject) => {
// Send back a success if the user has been added and the ID for the client to keep track of
res.status(202).json({"nodeId": queryResults.insertId});
res.status(202).json({ "nodeId": newNodeObject.id });
})
}
catch (err) {
@@ -41,40 +105,254 @@ exports.newNode = async (req, res) => {
}
}
// Get the known info for the node specified
/** Get the known info for the node specified
*
* @param {*} req Default express req from router
* @param {*} res Defualt express res from router
*/
exports.getNodeInfo = async (req, res) => {
if (!req.query.id) return res.status(400).json("No id specified");
mysqlHander.getNodeInfoFromId(req.query.id, (nodeInfo) => {
if (!req.params.id) return res.status(400).json("No id specified");
getNodeInfoFromId(req.params.id, (nodeInfo) => {
res.status(200).json(nodeInfo);
})
}
// Updates the information received from the client based on ID
exports.nodeCheckIn = async (req, res) => {
if (!req.body.id) return res.status(400).json("No id specified");
mysqlHander.getNodeInfoFromId(req.body.id, (nodeInfo) => {
let nodeObject = {};
// Convert the DB systems buffer to a JSON object to be worked with
nodeInfo.nearbySystems = utils.BufferToJson(nodeInfo.nearbySystems)
// Convert the online status to a boolean to be worked with
nodeInfo.online = nodeInfo.online !== 0;
/** Adds a specific system/preset on a given node
*
* @param {*} req Default express req from router
* @param {*} res Defualt express res from router
* @param {*} req.params.nodeId The Node ID to add the preset/system to
* @param {*} req.body.systemName The name of the system to add
* @param {*} req.body.mode The radio mode of the preset
* @param {*} req.body.frequencies The frequencies of the preset
* @param {*} req.body.trunkFile The trunk file to use for digital stations
*/
exports.addNodeSystem = async (req, res) => {
if (!req.params.nodeId) return res.status(400).json("No id specified");
if (!req.body.systemName) return res.status(400).json("No system specified");
log.DEBUG("Adding system for node: ", req.params.nodeId, req.body);
getNodeInfoFromId(req.params.nodeId, (node) => {
const reqOptions = new requestOptions("/client/addPreset", "POST", node.ip, node.port);
const reqBody = {
'systemName': req.body.systemName,
'mode': req.body.mode,
'frequencies': req.body.frequencies,
}
if(digitalModes.includes(req.body.mode)) reqBody['trunkFile'] = req.body.trunkFile ?? 'none'
if (req.body.name && req.body.name !== nodeInfo.name) nodeObject.name = req.body.name
if (req.body.ip && req.body.ip !== nodeInfo.ip) nodeObject.ip = req.body.ip
if (req.body.port && req.body.port !== nodeInfo.port) nodeObject.port = req.body.port
if (req.body.location && req.body.location !== nodeInfo.location) nodeObject.location = req.body.location
if (req.body.nearbySystems && JSON.stringify(req.body.nearbySystems) !== JSON.stringify(nodeInfo.nearbySystems)) nodeObject.nearbySystems = req.body.nearbySystems
if (req.body.online && req.body.online !== nodeInfo.online) nodeObject.online = req.body.online
log.DEBUG("Request body for adding node system: ", reqBody, reqOptions);
sendHttpRequest(reqOptions, JSON.stringify(reqBody), async (responseObj) => {
if(responseObj){
// Good
log.DEBUG("Response from adding node system: ", reqBody, responseObj);
return res.sendStatus(200)
} else {
// Bad
log.DEBUG("No Response from adding Node system");
return res.status(400).json("No Response from adding Node, could be offline");
}
})
})
}
/** Updates a specific system/preset on a given node
*
* @param {*} req Default express req from router
* @param {*} res Defualt express res from router
* @param {*} req.params.nodeId The Node ID to update the preset/system on
* @param {*} req.body.systemName The name of the system to update
* @param {*} req.body.mode The radio mode of the preset to
* @param {*} req.body.frequencies The frequencies of the preset
* @param {*} req.body.trunkFile The trunk file to use for digital stations
*/
exports.updateNodeSystem = async (req, res) => {
if (!req.params.nodeId) return res.status(400).json("No id specified");
if (!req.body.systemName) return res.status(400).json("No system specified");
log.DEBUG("Updating system for node: ", req.params.nodeId, req.body);
getNodeInfoFromId(req.params.nodeId, (node) => {
const reqOptions = new requestOptions("/client/updatePreset", "POST", node.ip, node.port);
const reqBody = {
'systemName': req.body.systemName,
'mode': req.body.mode,
'frequencies': req.body.frequencies,
}
if(digitalModes.includes(req.body.mode)) reqBody['trunkFile'] = req.body.trunkFile ?? 'none'
log.DEBUG("Request body for updating node: ", reqBody, reqOptions);
sendHttpRequest(reqOptions, JSON.stringify(reqBody), async (responseObj) => {
if(responseObj){
// Good
log.DEBUG("Response from updating node system: ", reqBody, responseObj);
return res.sendStatus(200)
} else {
// Bad
log.DEBUG("No Response from updating Node system");
return res.status(400).json("No Response from updating Node, could be offline");
}
})
})
}
/** Deletes a specific system/preset from a given node
*
* @param {*} req Default express req from router
* @param {*} res Defualt express res from router
* @param {*} req.params.nodeId The Node ID to update the preset/system on
* @param {*} req.body.systemName The name of the system to update
*/
exports.removeNodeSystem = async (req, res) => {
if (!req.params.nodeId) return res.status(400).json("No id specified");
if (!req.body.systemName) return res.status(400).json("No system specified");
log.DEBUG("Updating system for node: ", req.params.nodeId, req.body);
getNodeInfoFromId(req.params.nodeId, (node) => {
const reqOptions = new requestOptions("/client/removePreset", "POST", node.ip, node.port);
const reqBody = {
'systemName': req.body.systemName
}
log.DEBUG("Request body for deleting preset: ", reqBody, reqOptions);
sendHttpRequest(reqOptions, JSON.stringify(reqBody), async (responseObj) => {
if(responseObj){
// Good
log.DEBUG("Response from deleting preset: ", reqBody, responseObj);
return res.sendStatus(200)
} else {
// Bad
log.DEBUG("No Response from deleting preset");
return res.status(400).json("No Response from deleting preset, could be offline");
}
})
})
}
/** Updates the information received from the client based on ID
*
* @param {*} req Default express req from router
* @param {*} res Defualt express res from router
*/
exports.updateExistingNode = async = (req, res) => {
if (!req.params.nodeId) return res.status(400).json("No id specified");
getNodeInfoFromId(req.params.nodeId, (nodeInfo) => {
let checkInObject = {};
// Convert the online status to a boolean to be worked with
log.DEBUG("REQ Body: ", req.body);
var isObjectUpdated = false;
if (req.body.name && req.body.name != nodeInfo.name) {
checkInObject._name = req.body.name;
isObjectUpdated = true;
}
if (req.body.ip && req.body.ip != nodeInfo.ip) {
checkInObject._ip = req.body.ip;
isObjectUpdated = true;
}
if (req.body.port && req.body.port != nodeInfo.port) {
checkInObject._port = req.body.port;
isObjectUpdated = true;
}
if (req.body.location && req.body.location != nodeInfo.location) {
checkInObject._location = req.body.location;
isObjectUpdated = true;
}
if (req.body.nearbySystems && JSON.stringify(req.body.nearbySystems) !== JSON.stringify(nodeInfo.nearbySystems)) {
checkInObject._nearbySystems = req.body.nearbySystems;
isObjectUpdated = true;
}
if (req.body.online != nodeInfo.online || req.body.online && (req.body.online === "true") != nodeInfo.online) {
checkInObject._online = req.body.online;
isObjectUpdated = true;
}
// If no changes are made tell the client
if (Object.keys(nodeObject).length === 0) return res.status(200).json("No keys updated");
if (!isObjectUpdated) return res.status(200).json("No keys updated");
log.INFO("Updating the following keys for ID: ", req.body.id, nodeObject);
// Adding the ID key to the body so that the client can double-check their ID
nodeObject.id = req.body.id;
mysqlHander.updateNodeInfo(nodeObject, () => {
return res.status(202).json({"updatedKeys": nodeObject});
})
})
log.INFO("Updating the following keys for ID: ", req.params.nodeId, checkInObject);
}
checkInObject._id = req.params.nodeId;
checkInObject = new nodeObject(checkInObject);
if (!nodeInfo) {
log.WARN("No existing node found with this ID, adding node: ", checkInObject);
addNewNode(checkInObject, async (newNode) => {
await checkInWithNode(newNode);
return res.status(201).json({ "updatedKeys": newNode });
});
}
else {
updateNodeInfo(checkInObject, async () => {
await checkInWithNode(nodeInfo);
return res.status(202).json({ "updatedKeys": checkInObject });
});
}
});
}
/** Allows the bots to check in and get any updates from the server
*
* @param {*} req Default express req from router
* @param {*} res Defualt express res from router
*/
exports.nodeCheckIn = async (req, res) => {
if (!req.params.nodeId) return res.status(400).json("No id specified");
getNodeInfoFromId(req.params.nodeId, (nodeInfo) => {
if (!nodeInfo) return this.newNode(req, res);
if (!nodeInfo.online) {
nodeInfo.online = true;
updateNodeInfo(nodeInfo, () => {
return res.status(200).json(nodeInfo);
})
}
else return res.status(200).json(nodeInfo);
});
}
/**
* Requests a specific node to check in with the server, if it's online
*
* @param {*} req Default express req from router
* @param {*} res Defualt express res from router
*/
exports.requestNodeCheckIn = async (req, res) => {
if (!req.params.nodeId) return res.status(400).json("No Node ID supplied in request");
const node = await getNodeInfoFromId(req.params.nodeId);
if (!node) return res.status(400).json("No Node with the ID given");
await checkInWithNode(node);
if (res) res.sendStatus(200);
}
/**
* The node monitor service, this will periodically check in on the online nodes to make sure they are still online
*/
exports.nodeMonitorService = class nodeMonitorService {
constructor() {
this.log = new DebugBuilder("server", "nodeMonitorService");
}
/**
* Start the node monitor service in the background
*/
async start() {
// Wait for the a portion of the refresh period before checking in with the nodes, so the rest of the bot can start
await new Promise(resolve => setTimeout(resolve, refreshInterval / 10));
log.INFO("Starting Node Monitor Service");
// Check in before starting the infinite loop
await checkInWithOnlineNodes();
while (true) {
// Wait for the refresh interval, then wait for the posts to return, then wait a quarter of the refresh interval to make sure everything is cleared up
await new Promise(resolve => setTimeout(resolve, refreshInterval));
await checkInWithOnlineNodes();
await new Promise(resolve => setTimeout(resolve, refreshInterval / 4));
continue;
}
}
}

View File

@@ -0,0 +1,179 @@
const { DebugBuilder } = require("../utilities/debugBuilder");
const log = new DebugBuilder("server", "openAiController");
const crypto = require('crypto')
const { createTransaction } = require("./transactionController");
const { authorizeTokenUsage } = require("../middleware/balanceAuthorization");
const { encode } = require("gpt-3-encoder")
const { Configuration, OpenAIApi } = require('openai');
const configuration = new Configuration({
organization: process.env.OPENAI_ORG,
apiKey: process.env.OPENAI_KEY
});
const openai = new OpenAIApi(configuration);
// Global Vars for Other functions
exports.DALLE_COLOR = 0x34c6eb;
exports.CHATGPT_COLOR = 0x34eb9b;
async function getImageGeneration(_prompt, { _images_to_generate = 1, _image_size = "256x256" }, callback){
const validImageSizes = ["256x256", "512x512", "1024x1024"];
if (!_prompt) callback(new Error("No prompt given before generating image"), undefined);
if (!validImageSizes.includes(_image_size)) callback(new Error("Image size given is not valid, valid size: ", validImageSizes));
if (!_images_to_generate || _images_to_generate === 0 || _images_to_generate > 10) callback(new Error("Invalid image count given"));
// Calculate token usage?
log.DEBUG("Getting image generation with these properties: ", _prompt, _images_to_generate, _image_size)
try{
const response = await openai.createImage({
prompt: _prompt,
n: _images_to_generate,
size: _image_size
})
if(!response?.data) return callback(new Error("Error in response data: ", response));
return callback(undefined, response.data);
} catch (err){
log.ERROR(err);
log.ERROR("Error when handing image model request");
return callback(err, undefined);
}
}
/**
* Get the response from GPT with the specified parameters
*
* @param {*} _prompt The text prompt to send to the model
* @param {*} callback The callback to call with errors or results
* @param {*} param2 Any parameters the user has changed for this request
* @returns
*/
async function getTextGeneration(_prompt, callback, { _model = "text-davinci-003", _temperature = 0, _max_tokens = 100}) {
// If the temperature is set to null
_temperature = _temperature ?? 0;
// If the tokens are set to null
_max_tokens = _max_tokens ?? 100;
const encodedPrompt = encode(_prompt);
const promptTokens = encodedPrompt.length;
log.DEBUG("Tokens in prompt: ", promptTokens);
if (promptTokens >= _max_tokens) return callback(new Error("Tokens of request are greater than the set max tokens", promptTokens, _max_tokens));
_max_tokens = _max_tokens - promptTokens;
log.DEBUG("Updated max tokens: ", _max_tokens);
log.DEBUG("Getting chat with these properties: ", _prompt, _model, _temperature, _max_tokens)
try{
const response = await openai.createCompletion({
model: _model,
prompt: _prompt,
temperature: _temperature,
max_tokens: _max_tokens
});
if(!response?.data) return callback(new Error("Error in response data: ", response));
return callback(undefined, response.data);
} catch (err){
log.ERROR("Error when handing text model request: ", err);
return callback(err, undefined);
}
//var responseData = response.data.choices[0].text;
}
/**
* Use ChatGPT to generate a response
*
* @param {*} _prompt The use submitted text prompt
* @param {*} param1 Default parameters can be modified
* @returns
*/
exports.submitTextPromptTransaction = async (prompt, temperature, max_tokens, discord_account_id, interaction, command, callback) => {
getTextGeneration(prompt, (err, gptResult) => {
if (err) callback(err, undefined);
// TODO - Use the pricing table to calculate discord tokens
log.DEBUG("GPT Response", gptResult);
const discordTokensUsed = gptResult.usage.total_tokens;
if (gptResult){
createTransaction(gptResult.id, discord_account_id, discordTokensUsed, gptResult.usage.total_tokens, 1, async (err, transactionResult) => {
if (err) callback(err, undefined);
if (transactionResult){
log.DEBUG("Transaction Created: ", transactionResult);
callback(undefined, ({ promptResult: gptResult.choices[0].text, totalTokens: discordTokensUsed}));
}
});
}
}, { _temperature: temperature, _max_tokens: max_tokens });
}
/**
* Wrapper to generate an image from a prompt and params and store this information in a transaction
*
* @param {*} prompt The prompt of the image
* @param {*} images_to_generate The number of images to generate
* @param {*} image_size The size of the image ["256x256" | "512x512" | "1024x1024"]
* @param {*} callback
*/
exports.submitImagePromptTransaction = async (prompt, discord_account_id, images_to_generate, image_size, interaction, command, callback) => {
let pricePerImage = 800;
log.DEBUG(image_size)
switch(image_size){
case "1024x1024":
log.DEBUG("1024 selected");
pricePerImage = 1000;
break;
case "512x512":
pricePerImage = 900;
log.DEBUG("512 selected");
break;
case "256x256":
log.DEBUG("256 selected");
pricePerImage = 800;
break;
default:
log.DEBUG("256px defaulted");
pricePerImage = 800;
break;
}
if (!images_to_generate) images_to_generate = 1;
if (!image_size) image_size = "256x256";
totalTokensToBeUsed = pricePerImage * images_to_generate;
log.DEBUG("Total tokens to be used", totalTokensToBeUsed, pricePerImage, images_to_generate);
authorizeTokenUsage(interaction, command, totalTokensToBeUsed, (isAuthorized) => {
if (isAuthorized) {
getImageGeneration(prompt, {
_image_size: image_size,
_images_to_generate: images_to_generate
}, (err, dalleResult) => {
if (err) callback(err, undefined);
// TODO - Use the pricing table to calculate discord tokens
log.DEBUG("DALL-E Result", dalleResult);
const dalleResultHash = crypto.createHash('sha1').update(JSON.stringify({ discord_account_id : prompt, images_to_generate: image_size })).digest('hex')
if (dalleResult){
createTransaction(dalleResultHash, discord_account_id, totalTokensToBeUsed, totalTokensToBeUsed, 2, async (err, transactionResult) => {
if (err) callback(err, undefined);
if (transactionResult){
log.DEBUG("Transaction Created: ", transactionResult);
callback(undefined, ({ results: dalleResult, totalTokens: totalTokensToBeUsed}));
}
});
}
});
}
})
}

View File

@@ -0,0 +1,38 @@
//Will handle updating feeds in all channels
const { DebugBuilder } = require("../utilities/debugBuilder");
const log = new DebugBuilder("server", "rssController");
const libCore = require("../libCore");
const libUtils = require("../libUtils");
const refreshInterval = process.env.RSS_REFRESH_INTERVAL ?? 300000;
exports.RSSController = class RSSController {
constructor(_client) {
this.client = _client;
}
async start(){
// Wait for the refresh period before starting rss feeds, so the rest of the bot can start
await new Promise(resolve => setTimeout(resolve, refreshInterval));
log.INFO("Starting RSS Controller");
// Get initial feeds before the starting the infinite loop
await libCore.updateFeeds(this.client);
while(true){
// Wait for the refresh interval, then wait for the posts to return, then wait a quarter of the refresh interval to make sure everything is cleared up
await new Promise(resolve => setTimeout(resolve, refreshInterval));
await this.collectLatestPosts();
await new Promise(resolve => setTimeout(resolve, refreshInterval / 4));
continue;
}
}
async collectLatestPosts(){
log.INFO("Updating sources");
await libCore.updateFeeds(this.client)
return;
}
}

View File

@@ -0,0 +1,35 @@
// Controller for managing transactions
const { DebugBuilder } = require("../utilities/debugBuilder");
const log = new DebugBuilder("server", "transactionController");
const { TransactionStorage } = require("../libStorage");
const transactionStorage = new TransactionStorage();
const { BaseTransaction } = require("../utilities/recordHelper");
const { withdrawBalance } = require("./accountController");
exports.createTransaction = async (_provider_transaction_id, _account_id, _discord_tokens_used, _provider_tokens_used, _provider_id, callback) => {
if (!_provider_transaction_id && !_account_id && !_discord_tokens_used && !_provider_id) return callback(new Error("Invalid vars when creating transaction", {vars: [_provider_transaction_id, _account_id, _discord_tokens_used, _provider_id, callback]}))
const newTransaction = new BaseTransaction(_provider_transaction_id, _account_id, _discord_tokens_used, _provider_tokens_used, _provider_id, callback);
log.DEBUG("New Transaction Object: ", newTransaction);
withdrawBalance(newTransaction.discord_tokens_used, newTransaction.account_id, (err, withdrawResult) => {
if (err) return callback(err, undefined);
if (withdrawResult){
log.DEBUG("New withdraw result: ", withdrawResult);
transactionStorage.createTransaction(newTransaction, async (err, transactionResult) =>{
if (err) return callback(err, undefined);
if(transactionResult){
log.DEBUG("New transaction result: ", transactionResult);
return callback(undefined, transactionResult);
}
})
}
else {
return callback(undefined, undefined);
}
});
}

View File

@@ -1,150 +0,0 @@
//Config
import { getTOKEN, getGuildID, getApplicationID } from './utilities/configHandler.js.js';
// Commands
import ping from './commands/ping.js';
import join from './commands/join.js.js';
import leave from './commands/leave.js.js';
import status from './commands/status.js.js';
// Debug
import ModuleDebugBuilder from "./utilities/moduleDebugBuilder.js.js";
const log = new ModuleDebugBuilder("bot", "app");
// Modules
import { Client, GatewayIntentBits } from 'discord.js';
// Utilities
import registerCommands from './utilities/registerCommands.js.js';
/**
* Host Process Object Builder
*
* This constructor is used to easily construct responses to the host process
*/
class HPOB {
/**
* Build an object to be passed to the host process
* @param command The command to that was run ("Status", "Join", "Leave", "ChgPreSet")
* @param response The response from the command that was run
*/
constructor(command = "Status"||"Join"||"Leave"||"ChgPreSet", response) {
this.cmd = command;
this.msg = response;
}
}
// Create the Discord client
const client = new Client({
intents: [
GatewayIntentBits.Guilds,
GatewayIntentBits.GuildMessages,
GatewayIntentBits.MessageContent,
GatewayIntentBits.GuildVoiceStates
]
});
/**
* When the parent process sends a message, this will interpret the message and act accordingly
*
* DRB IPC Message Structure:
* msg.cmd = The command keyword; Commands covered on the server side
* msg.params = An array containing the parameters for the command
*
*/
process.on('message', (msg) => {
log.DEBUG('IPC Message: ', msg);
const guildID = getGuilds()[0];
log.DEBUG("Guild Name: ", getGuildNameFromID(guildID));
switch (msg.cmd) {
// Check the status of the bot
case "Status":
log.INFO("Status command run from IPC");
status({guildID: guildID, callback: (statusObj) => {
log.DEBUG("Status Object string: ", statusObj);
if (!statusObj.voiceConnection) return process.send(new HPOB("Status", "VDISCONN"));
}});
break;
// Check the params for a server ID and if so join the server
case "Join":
log.INFO("Join command run from IPC");
join({guildID: guildID, guildObj: client.guilds.cache.get(guildID), channelID: msg.params.channelID, callback: () => {
process.send(new HPOB("Join", "AIDS"));
}})
break;
// Check to see if the bot is in a server and if so leave
case "Leave":
log.INFO("Leave command run from IPC");
leave({guildID: guildID, callback: (response) => {
process.send(new HPOB("Leave", response));
}});
break;
default:
// Command doesn't exist
log.INFO("Unknown command run from IPC");
break;
}
})
// When the client is connected and ready
client.on('ready', () =>{
log.INFO(`${client.user.tag} is ready`)
process.send({'msg': "INIT READY"});
});
/*
* Saved For later
client.on('messageCreate', (message) => {
log.DEBUG(`Message Sent by: ${message.author.tag}\n\t'${message.content}'`);
});
*/
// When a command is sent
client.on('interactionCreate', (interaction) => {
if (interaction.isChatInputCommand()){
switch (interaction.commandName) {
case "ping":
ping(interaction);
break;
case "join":
join({ interaction: interaction });
break;
case "leave":
leave({ interaction: interaction });
break;
case "status":
status({ interaction: interaction });
break;
default:
interaction.reply({ content: 'Command not found, try one that exists', fetchReply: true })
.then((message) => log.DEBUG(`Reply sent with content ${message.content}`))
.catch((err) => log.ERROR(err));
}
}
})
function loginBot(){
client.login(getTOKEN());
}
function getGuilds() {
return client.guilds.cache.map(guild => guild.id)
}
function getGuildNameFromID(guildID) {
return client.guilds.cache.map((guild) => {
if (guild.id === guildID) return guild.name;
})[0]
}
function main(){
registerCommands(() => {
loginBot();
});
}
main();
//module.exports = client;

View File

@@ -1,6 +0,0 @@
// Utilities
import { replyToInteraction } from '../utilities/messageHandler.js.js';
export default function ping(interaction) {
return replyToInteraction(interaction, "Pong! I have Aids and now you do too!");
}

View File

@@ -1,7 +0,0 @@
{
"TOKEN": "OTQzNzQyMDQwMjU1MTE1MzA0.Yg3eRA.ZxEbRr55xahjfaUmPY8pmS-RHTY",
"ApplicationID": "943742040255115304",
"GuildID": "367396189529833472",
"DeviceID": "5",
"DeviceName": "VoiceMeeter Aux Output (VB-Audi"
}

View File

@@ -0,0 +1,60 @@
const { Events } = require('discord.js');
const { authorizeCommand } = require('../middleware/discordAuthorization');
const { authorizeTokenUsage } = require('../middleware/balanceAuthorization');
const { DebugBuilder } = require("../utilities/debugBuilder");
const log = new DebugBuilder("server", "interactionCreate");
module.exports = {
name: Events.InteractionCreate,
async execute(interaction) {
const command = interaction.client.commands.get(interaction.commandName);
log.VERBOSE("Interaction created for command: ", command);
// Execute autocomplete if the user is checking autocomplete
if (interaction.isAutocomplete()) {
log.DEBUG("Running autocomplete for command: ", command.data.name);
return await command.autocomplete(interaction);
}
// Check if the interaction is a command
if (!interaction.isChatInputCommand()) return;
if (!command) {
log.ERROR(`No command matching ${interaction.commandName} was found.`);
return;
}
log.DEBUG(`${interaction.member.user} is running '${interaction.commandName}'`);
await authorizeCommand(interaction, command, async () => {
await authorizeTokenUsage(interaction, command, undefined, async () => {
try {
if (command.deferInitialReply) {
try {
if (interaction.options.getBool('public') && interaction.options.getBool('public') == false) await interaction.deferReply({ ephemeral: true });
else await interaction.deferReply({ ephemeral: false });
}
catch (err) {
if (err instanceof TypeError) {
// The public option doesn't exist in this command
await interaction.deferReply({ ephemeral: false });
} else {
throw err;
}
}
}
command.execute(interaction);
} catch (error) {
log.ERROR(error);
if (interaction.replied || interaction.deferred) {
interaction.followUp({ content: 'There was an error while executing this command!', ephemeral: true });
} else {
interaction.reply({ content: 'There was an error while executing this command!', ephemeral: true });
}
}
})
});
},
};

View File

@@ -0,0 +1,14 @@
const { Events } = require('discord.js');
const { authorizeCommand } = require('../middleware/discordAuthorization');
const { authorizeTokenUsage } = require('../middleware/balanceAuthorization');
const { linkCop } = require("../modules/linkCop");
const { DebugBuilder } = require("../utilities/debugBuilder");
const log = new DebugBuilder("server", "messageCreate");
module.exports = {
name: Events.MessageCreate,
async execute(interaction) {
//await linkCop(interaction);
},
};

View File

@@ -0,0 +1,7 @@
exports.linkCopInsults = [
"{%mtn_user%}, tsk tsk. Links belong here:\n '{%ref_og_msg%}'",
"{%mtn_user%}, the channel is quite literally called 'links':\n '{%ref_og_msg%}'",
"{%mtn_user%}. Well, well, well, if it isn't the man who's been posting links in the wrong channel.\n'{%ref_og_msg%}'",
"{%mtn_user%}, isn't this convenient. A whole channel for links and you put links in, and you put {%ref_links%} in {%ref_og_channel%}.\n'{%ref_og_msg%}'",
"{%mtn_user%}, that's odd. I don't recall {%ref_og_channel%} being called 'links. Maybe I misread?\n'{%ref_og_msg%}'",
]

171
Server/index.js Normal file
View File

@@ -0,0 +1,171 @@
// Modules
var createError = require('http-errors');
var express = require('express');
var path = require('path');
var cookieParser = require('cookie-parser');
var logger = require('morgan');
var http = require('http');
const fs = require('fs');
require('dotenv').config();
// Utilities
const { RSSController } = require("./controllers/rssController");
const libUtils = require("./libUtils");
const deployCommands = require("./utilities/deployCommands");
const { nodeMonitorService } = require('./controllers/nodesController');
// Debug
const { DebugBuilder } = require("./utilities/debugBuilder");
const log = new DebugBuilder("server", "index");
const {
Client,
Events,
Collection,
GatewayIntentBits,
MessageActionRow,
MessageButton
} = require('discord.js');
const client = new Client({
intents: [GatewayIntentBits.GuildMessages, GatewayIntentBits.Guilds, GatewayIntentBits.GuildPresences, GatewayIntentBits.GuildMembers]
});
prefix = process.env.PREFIX
discordToken = process.env.TOKEN;
rssTimeoutValue = process.env.RSS_TIMEOUT_VALUE ?? 300000;
var indexRouter = require('./routes/index');
var nodesRouter = require('./routes/nodes');
var adminRouter = require('./routes/admin');
// HTTP Server Config
var app = express();
// view engine setup
app.set('views', path.join(__dirname, 'views'));
app.set('view engine', 'ejs');
app.use(logger('dev'));
app.use(express.json());
app.use(express.urlencoded({ extended: false }));
app.use(cookieParser());
app.use(express.static(path.join(__dirname, 'public')));
// Web Interface
app.use('/', indexRouter);
// Nodes API
app.use('/nodes', nodesRouter);
// Admin API
app.use('/admin', adminRouter);
// catch 404 and forward to error handler
app.use((req, res, next) => {
next(createError(404));
});
var port = libUtils.normalizePort(process.env.HTTP_PORT || '3000');
app.set('port', port);
// error handler
app.use((err, req, res, next) => {
// set locals, only providing error in development
res.locals.message = err.message;
res.locals.error = req.app.get('env') === 'development' ? err : {};
// render the error page
res.status(err.status || 500);
res.render('error');
});
/**
* Start the HTTP background server
*/
async function runHTTPServer() {
var server = http.createServer(app);
server.listen(port);
server.on('error', libUtils.onError);
server.on('listening', () => {
log.INFO("HTTP server started!");
})
}
/**
* Start the node monitoring service
*/
async function runNodeMonitorService(){
const monitor = new nodeMonitorService();
monitor.start();
}
/**
* Start the RSS background process
*/
async function runRssService() {
const rssController = new RSSController(client);
rssController.start();
}
// Discord bot config
// Setup commands for the Discord bot
client.commands = new Collection();
const commandsPath = path.join(__dirname, 'commands');
const commandFiles = fs.readdirSync(commandsPath).filter(file => file.endsWith('.js'));
//const commandFiles = fs.readdirSync('./commands').filter(file => file.endsWith('.js'));
for (const file of commandFiles) {
const filePath = path.join(commandsPath, file);
const command = require(filePath);
if (command.data instanceof Promise) {
command.data.then(async (builder) => {
command.data = builder;
log.DEBUG("Importing command: ", command.data.name, command);
// Set a new item in the Collection
// With the key as the command name and the value as the exported module
client.commands.set(command.data.name, command);
});
}
else {
log.DEBUG("Importing command: ", command.data.name, command);
// Set a new item in the Collection
// With the key as the command name and the value as the exported module
client.commands.set(command.data.name, command);
}
}
// Run when the bot is ready
client.on('ready', () => {
log.DEBUG(`Discord server up and running with client: ${client.user.tag}`);
log.INFO(`Logged in as ${client.user.tag}!`);
// Deploy slash commands
log.DEBUG("Deploying slash commands");
deployCommands.deploy(client.user.id, client.guilds.cache.map(guild => guild.id));
log.DEBUG(`Starting HTTP Server`);
runHTTPServer();
log.DEBUG("Starting Node Monitoring Service");
runNodeMonitorService();
log.DEBUG("Starting RSS watcher");
runRssService();
});
// Setup any additional event handlers
const eventsPath = path.join(__dirname, 'events');
const eventFiles = fs.readdirSync(eventsPath).filter(file => file.endsWith('.js'));
for (const file of eventFiles) {
const filePath = path.join(eventsPath, file);
const event = require(filePath);
if (event.once) {
client.once(event.name, (...args) => event.execute(...args));
} else {
client.on(event.name, (...args) => event.execute(...args));
}
}
client.login(discordToken); //Load Client Discord Token

404
Server/libCore.js Normal file
View File

@@ -0,0 +1,404 @@
const { all } = require('axios');
const axios = require('axios');
const { FeedStorage, PostStorage } = require("./libStorage");
const libUtils = require("./libUtils");
const { DebugBuilder } = require("./utilities/debugBuilder");
const log = new DebugBuilder("server", "libCore");
const mysql = require("mysql2");
const UserAgent = require("user-agents");
process.env.USER_AGENT_STRING = new UserAgent({ platform: 'Win32' }).toString();
log.DEBUG("Generated User Agent string:", process.env.USER_AGENT_STRING);
// Initiate the parser
let Parser = require('rss-parser');
let parser = new Parser({
headers: {
'User-Agent': process.env.USER_AGENT_STRING,
"Accept": "application/rss+xml,application/xhtml+xml,application/xml"
}
});
// Setup Storage handlers
var feedStorage = new FeedStorage();
var postStorage = new PostStorage();
// Initiate a running array of objects to keep track of sources that have no feeds/posts
/*
var runningPostsToRemove = [{
"{SOURCE URL}": {NUMBER OF TIMES IT'S BEEN REMOVED}
}]
*/
var runningPostsToRemove = {};
const sourceFailureLimit = process.env.SOURCE_FAILURE_LIMIT ?? 15;
/**
* Wrapper for feeds that cause errors. By default it will wait over a day for the source to come back online before deleting it.
*
* @param {string} sourceURL The URL of the feed source causing issues
*/
exports.removeSource = function removeSource(sourceURL) {
log.INFO("Removing source URL: ", sourceURL);
// Check to see if this is the first time this source has been attempted
if (!Object.keys(runningPostsToRemove).includes(sourceURL)) {
runningPostsToRemove[sourceURL] = { count: 1, timestamp: Date.now(), ignoredAttempts: 0 };
return;
}
const backoffDateTimeDifference = (Date.now() - new Date(runningPostsToRemove[sourceURL].timestamp));
const backoffWaitTime = (runningPostsToRemove[sourceURL].count * 30000);
log.DEBUG("Datetime", runningPostsToRemove[sourceURL], backoffDateTimeDifference, backoffWaitTime);
// Check to see if the last error occurred within the backoff period or if we should try again
if (backoffDateTimeDifference <= backoffWaitTime) {
runningPostsToRemove[sourceURL].ignoredAttempts +=1;
return;
}
// Increase the retry counter
if (runningPostsToRemove[sourceURL].count < sourceFailureLimit) {
runningPostsToRemove[sourceURL].count += 1;
runningPostsToRemove[sourceURL].timestamp = Date.now();
return;
}
feedStorage.getRecordBy('link', sourceURL, (err, record) => {
if (err) log.ERROR("Error getting record from feedStorage", err);
if (!record) log.ERROR("No source returned from feedStorage");
feedStorage.destroy(record.id, (err, results) => {
if (err) log.ERROR("Error removing ID from results", err);
if (!results) log.WARN("No results from remove entry");
log.DEBUG("Source exceeded the limit of retries and has been removed", sourceURL);
return;
})
})
}
/**
* Unset a source URL from deletion if the source has not already been deleted
* @param {*} sourceURL The source URL to be unset from deletion
*/
exports.unsetRemoveSource = function unsetRemoveSource(sourceURL) {
log.INFO("Unsetting source URL from deletion (if not already deleted): ", sourceURL);
if (!Object.keys(runningPostsToRemove).includes(sourceURL)) return;
delete runningPostsToRemove[sourceURL];
return
}
/**
* Adds or updates new source url to configured storage
* @constructor
* @param {string} title - Title/Name of the RSS feed.
* @param {string} link - URL of RSS feed.
* @param {string} category - Category of RSS feed.
*/
exports.addSource = async (title, link, category, guildId, channelId, callback) => {
feedStorage.create([{
"fields": {
"title": title,
"link": link,
"category": category,
'guild_id': guildId,
"channel_id": channelId
}
}], function (err, record) {
if (err) {
log.ERROR("Error in create:", err);
return callback(err, undefined);
}
if (!record) return callback(undefined, false);
log.DEBUG("Record ID:", record.getId());
return callback(undefined, record);
});
}
/**
* Deletes a new source url by title
* @constructor
* @param {string} title - Title/Name of the RSS feed.
*/
exports.deleteSource = function (title, callback) {
feedStorage.getRecordBy('title', title, (err, results) => {
if (err) return callback(err, undefined);
if (!results?.id) {
log.DEBUG("No record found for title: ", title)
return callback(undefined, undefined);
}
feedStorage.destroy(results.id, function (err, deletedRecord) {
if (err) {
log.ERROR(err);
return callback(err, undefined);
}
log.DEBUG("Deleted Record: ", deletedRecord);
return callback(undefined, deletedRecord ?? true);
});
});
}
/**
* Update channels with new posts from sources
*/
exports.updateFeeds = (client) => {
if (!client) throw new Error("Client object not passed");
// Create a temp pool to use for all connections while updating the feed
var tempConnection = mysql.createPool({
host: process.env.EM_DB_HOST,
user: process.env.EM_DB_USER,
password: process.env.EM_DB_PASS,
database: process.env.EM_DB_NAME,
connectionLimit: 10
});
const tempFeedStorage = new FeedStorage(tempConnection);
const tempPostStorage = new PostStorage(tempConnection);
// Array of promises to wait on before closing the connection
var recordPromiseArray = [];
var sourcePromiseArray = [];
tempFeedStorage.getAllRecords(async (err, records) => {
// Load the posts from each RSS source
for (const source of records) {
sourcePromiseArray.push(new Promise((resolve, reject) => {
log.DEBUG('Record title: ', source.title);
log.DEBUG('Record link: ', source.link);
log.DEBUG('Record category: ', source.category);
log.DEBUG('Record guild ID: ', source.guild_id);
log.DEBUG('Record channel ID: ', source.channel_id);
// Parse the RSS feed
parser.parseURL(source.link, async (err, parsedFeed) => {
if (err) {
log.ERROR("Parser Error: ", runningPostsToRemove, source, err);
// Call the wrapper to make sure the site isn't just down at the time it checks and is back up the next time
this.removeSource(source.link);
reject;
}
try {
if (parsedFeed?.items){
this.unsetRemoveSource(source.link);
for (const post of parsedFeed.items.reverse()){
recordPromiseArray.push(new Promise((recordResolve, recordReject) => {
log.DEBUG("Parsed Source Keys", Object.keys(post), post?.title);
log.VERBOSE("Post from feed: ", post);
if (!post.title || !post.link) return recordReject("Missing information from the post");
if (!post.content || !post['content:encoded']) log.WARN("There is no content for post: ", post.title);
post.postId = post.postId ?? post.guid ?? post.id ?? libUtils.returnHash(post.title, post.link, post.pubDate ?? Date.now());
tempPostStorage.getRecordBy('post_guid', post.postId, (err, existingRecord) => {
if (err) throw err;
log.DEBUG("Existing post record: ", existingRecord);
if (existingRecord) return recordResolve("Existing record found for this post");
const channel = client.channels.cache.get(source.channel_id);
libUtils.sendPost(post, source, channel, (err, sendResults) =>{
if (err) throw err;
if (!sendResults) {
log.ERROR("No sending results from sending a post: ", sendResults, existingRecord, post);
return recordReject("No sending results from sending a post");
}
log.DEBUG("Saving post to database: ", sendResults, post.title, source.channel_id);
tempPostStorage.savePost(post, (err, saveResults) => {
if(err) throw err;
if (saveResults) {
log.DEBUG("Saved results: ", saveResults);
return recordResolve("Saved results", saveResults);
}
});
})
})
}))
}
}
else {
this.removeSource(source.link);
}
}
catch (err) {
log.ERROR("Error Parsing Feed: ", source.link, err);
this.removeSource(source.link);
throw err;
}
Promise.all(recordPromiseArray).then((values) => {
log.DEBUG("All posts finished for: ", source.title, values);
return resolve(source.title);
});
});
}))
}
// Wait for all connections to finish then close the temp connections
Promise.all(sourcePromiseArray).then((values) => {
log.DEBUG("All sources finished, closing temp connections: ", values);
tempConnection.end();
});
});
}
/**
* Search a state for any weather alerts
*
* @param {*} state The state to search for any weather alerts in
* @returns
*/
exports.weatherAlert = async function (state) {
var answerURL = `https://api.weather.gov/alerts/active?area=${state}`;
log.DEBUG(answerURL);
answerData = [];
await axios.get(answerURL)
.then(response => {
response.data.features.forEach(feature => {
answerData.push(feature);
})
return answerData;
})
.catch(error => {
log.DEBUG(error);
});
return answerData;
}
/**
* Gets a random food recipe
*
* @returns
*/
exports.getFood = async function () {
var answerURL = `https://www.themealdb.com/api/json/v1/1/random.php`;
log.DEBUG(answerURL);
answerData = {
text: `No answer found try using a simpler search term`,
source: ``
}
await axios.get(answerURL)
.then(response => {
//log.DEBUG(response.data.RelatedTopics[0].Text);
//log.DEBUG(response.data.RelatedTopics[0].FirstURL);
// if (response.data.meals.length != 0) {
answerData = {
strMeal: `No Data`,
strSource: `-`,
strInstructions: `-`,
strMealThumb: `-`,
strCategory: `-`
}
answerData = {
strMeal: `${unescape(response.data.meals[0].strMeal)}`,
strSource: `${unescape(response.data.meals[0].strSource)}`,
strInstructions: `${unescape(response.data.meals[0].strInstructions)}`,
strMealThumb: `${unescape(response.data.meals[0].strMealThumb)}`,
strCategory: `${unescape(response.data.meals[0].strCategory)}`
}
// } else {
//}
return answerData;
})
.catch(error => {
log.DEBUG(error);
});
return answerData;
}
/**
* Search Urban dictionary for a phrase
*
* @param {*} question The phrase to search urban dictionary for
* @returns
*/
exports.getSlang = async function (question) {
var answerURL = `https://api.urbandictionary.com/v0/define?term=${question}`;
log.DEBUG(answerURL);
slangData = {
definition: `No answer found try using a simpler search term`,
example: ``
}
await axios.get(answerURL)
.then(response => {
log.DEBUG(response.data.list[0]);
slangData = {
definition: `${unescape(response.data.list[0].definition) ? unescape(response.data.list[0].definition) : ''}`,
example: `${unescape(response.data.list[0].example) ? unescape(response.data.list[0].example) : ''}`,
thumbs_down: `${unescape(response.data.list[0].thumbs_down)}`,
thumbs_up: `${unescape(response.data.list[0].thumbs_up)}`
}
return slangData;
})
.catch(error => {
log.DEBUG(error);
});
return slangData;
}
/**
* getSources - Get the RSS sources currently in use
* @constructor
*/
exports.getSources = function () {
feedStorage.getAllRecords((err, records) => {
if (err) throw err;
return records;
})
}
/**
* getQuotes - Get a random quote from the local list
* @constructor
*/
exports.getQuotes = async function (quote_url) {
var data = [];
await axios.get(quote_url)
.then(response => {
log.DEBUG(response.data[0].q);
log.DEBUG(response.data[0].a);
data = response.data;
return data;
})
.catch(error => {
log.DEBUG(error);
});
return data;
}
/**
* getCategories - Returns feed categories
* @constructor
*/
exports.getCategories = async (callback) => {
feedStorage.getUniqueByKey("category", (err, results) => {
if (err) return callback(err, undefined);
return callback(undefined, results);
});
}

Some files were not shown because too many files have changed in this diff Show More