├── .gitignore ├── LICENSE ├── README.md ├── package.json ├── public ├── index.html ├── media │ ├── smartalarm.png │ └── smartswitch.png ├── pair.css ├── pair.html ├── pair.js ├── rustplussocket.js ├── styles.css └── success.html └── server.js /.gitignore: -------------------------------------------------------------------------------- 1 | .env 2 | db.json 3 | node_modules 4 | examples 5 | rustplus-api-master 6 | package-lock.json 7 | .eslintrc.yml 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Finn 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # Rust Plus+ 3 | Rust Plus+ is a web-based equivalent to the Facepunch released companion app Rust+. In addition to the standard features of Rust+ this web app is also able to provide notifications through discord web hooks and also RPI buzzer integrations. 4 | 5 | ![Device ID photo](https://i.imgur.com/wNbbGKh.png) 6 | 7 | # New Features! 8 | 9 | - Ability to turn smart switches on and off through the web GUI. 10 | - Ability to configure server settings and configure notification settings. 11 | - Ability to turn on a physical buzzer on a Raspberry Pi (No more 5am offlines! rip your sleep). 12 | - Ability to use discord web hooks to send notifications with custom messages. 13 | - An easier pairing method for devices 14 | - A web-based way of retrieving your Rust player token. 15 | 16 | Features to come: 17 | - Maybe IFTTT integration if requested? 18 | 19 | Huge shout out to [liamcottle] for the hard work on creating a nice nodeJS library to interact with Rust+. 20 | 21 | ## Installation 22 | There are two releases for Rust Plus+, one being a general purpose build that will run on most operating systems, and two being one build specifically for Raspbian. The non OS specific build does not include any mention of buzzer integrations whereas the Raspberry Pi build does. I will be going over the installation for both Windows and Raspbian. 23 | 24 | ### Windows Guide 25 | This installation guide will be taking the assumption that you are using a Windows machine as the host. 26 | #### Step 1: 27 | Download and install the prerequisites for Rust Plus+ which is only [nodeJS]. 28 | #### Step 2: 29 | Download the latest general release of Rust Plus+ and extract the file to your chosen location. 30 | #### Step 3: 31 | Open up command prompt and navigate to the directory of Rust Plus+ and type: 32 | 33 | ```shell 34 | npm install 35 | npm start 36 | ``` 37 | #### Step 4: 38 | Navigate to the IP address of the machine you are hosting in a web browser. 39 | 40 | ### Raspberry Pi Guide 41 | This installation guide will be taking the assumption that you are using a Raspberry Pi as the host. I used a version 1 model b for my tests. 42 | 43 | #### Step 1: 44 | Install NOOBS onto your raspberry Pi from the [Raspberry Pi website](https://www.raspberrypi.org/downloads/). You will need to have your RPI's networking configured before you continue. 45 | #### Step 2: 46 | Open up a terminal on the Raspberry Pi and run the following commands: 47 | ```sh 48 | $ sudo apt-get update -y 49 | $ sudo apt-get dist-upgrade -y 50 | $ sudo apt-get install -y git 51 | 52 | $ wget https://nodejs.org/dist/latest-v11.x/node-v11.15.0-linux-armv6l.tar.gz 53 | $ tar -xzf node-v11.15.0-linux-armv6l.tar.gz 54 | $ cd node-v11.15.0-linux-armv6l/ 55 | $ sudo cp -R * /usr/local/ 56 | 57 | $ git clone https://github.com/nerif-tafu/rustplusplus /home/pi/rustplusplus 58 | $ cd /home/pi/rustplusplus 59 | 60 | $ npm install 61 | $ sudo npm start 62 | ``` 63 | 64 | Go to your favourite web browser and navigate to http://serveripaddress to see your Rust Plus+ website 65 | 66 | ### Getting your server details 67 | To use Rust Plus+ you must supply your server information and device details to receive notifications. You can get this information by heading to http://serveripaddress/pair and following the on-screen steps. 68 | ![Pairing device photo](https://i.imgur.com/hDgfzLg.png) 69 | 70 | 71 | /pair is GUI version of liamcottles pairing CLI tool. Again props to him for doing all the hard work for this. To find out more about the CLI version please visit [liamcottle's lovely guide] on getting your pairing information. 72 | 73 | Another way to get device ID's from within the game is to use the command debug.lookingat_debug when you are looking at a smart device. You can stop the information from displaying in game by running the command again while pointing at the device. 74 | 75 | ![Device ID photo](https://i.imgur.com/uYiuq2I.png) 76 | 77 | ### Discord notifications 78 | 79 | To get Discord notifications setup you must first create a web hook from within your Discord server. Navigating to Server settings > Integrations > Web hooks > New Web hook > Copy Web hook URL will get your new web hook URL. Once you have the URL you can add the web hook URL to the "Notification settings" from your Rust Plus+ website. If you would like to mention a discord role you will need to get the role ID, you can get this by enabling developer settings from within Discord and then right clicking on the role from within Server Settings > Roles. To get the web hook to tag the role you must wrap the role ID with "<@ROLEID>". More info on this can be found [Here] (https://discordjs.guide/miscellaneous/parsing-mention-arguments.html) 80 | 81 | ### Buzzer notifications 82 | To physically setup the buzzer for your Raspberry Pi you will need a module similar [to this]. Most piezoelectric buzzers are 5v compared to the output of a GPIO pin which is 3.3v, you need to watch out for this if you are trying to use a regular PC motherboard buzzer. If you are using that module you need to connect VCC to the Raspberry Pi's 5v pin, GND to the ground pin and I/O to the GPIO pin you want to use (I used 22 on the RaspbPi v1 b). 83 | 84 | [to this]: 85 | [liamcottle]: 86 | [liamcottle's lovely guide]: 87 | [nodeJS]: -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "rustplusplus", 3 | "version": "1.0.0", 4 | "description": "Rust+ web interface for RPI", 5 | "main": "server.js", 6 | "repository": { 7 | "type": "git", 8 | "url": "git+https://github.com/nerif-tafu/rustplusplus.git" 9 | }, 10 | "scripts": { 11 | "test": "echo \"Error: no test specified\" && exit 1" 12 | }, 13 | "author": "Tafu", 14 | "license": "MIT", 15 | "dependencies": { 16 | "axios": "^0.21.0", 17 | "discord-webhook-node": "^1.1.8", 18 | "dotenv": "^8.2.0", 19 | "express": "^4.17.1", 20 | "lowdb": "^1.0.0", 21 | "rustplus-api": "github:liamcottle/rustplus-api", 22 | "socket.io": "^3.0.0", 23 | "uuid": "^8.1.0", 24 | "push-receiver": "^2.1.0" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Rust+ Web Controller 6 | 7 | 8 | 9 | 10 |
11 |
12 |

Rust+ Web Controller

13 |

Current server: N/A

14 |
15 |
16 |

To add an interactive device please enter the device ID and give it a nickname. To find server info or device ID go here

17 |
18 | 19 | 20 |
21 | 22 |
23 |
24 | 25 |
26 |
27 | 28 |
29 |
30 |

Here you can edit the notification options for your smart alarms

31 | 32 | 33 | 34 |
35 |
36 | 37 |
38 |
39 |

Edit server settings and user details. See github repo for more information.

40 | 41 | 42 | 43 | 44 | 45 |
46 |
47 |
48 |
49 | 50 | 51 | 52 | 53 | 54 | 67 | 68 | 78 | -------------------------------------------------------------------------------- /public/media/smartalarm.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nerif-tafu/rustplusplus/f015d9be964750f6eec74670af9b1947d8caec56/public/media/smartalarm.png -------------------------------------------------------------------------------- /public/media/smartswitch.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nerif-tafu/rustplusplus/f015d9be964750f6eec74670af9b1947d8caec56/public/media/smartswitch.png -------------------------------------------------------------------------------- /public/pair.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --small-width: 300px; 3 | --medium-width: 570px; 4 | --large-width: 1010px; 5 | } 6 | 7 | @media (prefers-color-scheme:light) { 8 | :root { 9 | --main-bg-colour: #23303B; 10 | } 11 | } 12 | 13 | @media (prefers-color-scheme:dark) { 14 | :root { 15 | --accept-bg-colour: #DAE8FC; 16 | --accept-border-colour: #6C8EBF; 17 | --active-bg-colour: #D5E8D4; 18 | --active-border-colour: #82B366; 19 | --inactive-bg-colour: #F8CECC; 20 | --inactive-border-colour: #B85450; 21 | --no-response-bg-colour: #FFF2CC; 22 | --no-response-border-colour: #D6B656; 23 | --remove-bg-colour: #FF3333; 24 | --remove-border-colour: #B85450; 25 | --remove-text-colour: #FFFFFF; 26 | 27 | --foreground-bg-colour: #FFFFFF; 28 | --foreground-border-colour: #000000; 29 | } 30 | } 31 | 32 | .--error-highlight{ 33 | border-color: red; 34 | } 35 | 36 | 37 | #container, 38 | body, 39 | html { 40 | min-height: 100%; 41 | height: 100%; 42 | margin: 0; 43 | } 44 | 45 | * { 46 | font-family: helvetica; 47 | } 48 | 49 | button { 50 | border: 1px solid black; 51 | } 52 | 53 | 54 | #container { 55 | display: flex; 56 | flex-direction: column; 57 | justify-content: space-between; 58 | align-items: center 59 | } 60 | 61 | .container--narrow-width{ 62 | 63 | } 64 | 65 | .setting--narrow-width{ 66 | border: 1px solid grey; 67 | } 68 | 69 | @media (min-width:1080px) { 70 | .container--narrow-width { 71 | max-width: var(--large-width); 72 | min-width: var(--large-width); 73 | } 74 | .setting--narrow-width { 75 | max-width: calc(var(--large-width) - 60px); 76 | min-width: calc(var(--large-width) - 60px); 77 | } 78 | } 79 | 80 | @media (max-width:1079px) and (min-width:640px) { 81 | .container--narrow-width { 82 | max-width: var(--medium-width); 83 | min-width: var(--medium-width); 84 | } 85 | .setting--narrow-width { 86 | max-width: calc(var(--medium-width) - 60px); 87 | min-width: calc(var(--medium-width) - 60px); 88 | } 89 | } 90 | 91 | @media (max-width:640px) { 92 | .container--narrow-width { 93 | max-width: var(--small-width); 94 | min-width: var(--small-width); 95 | } 96 | .setting--narrow-width { 97 | max-width: calc(var(--small-width) - 60px); 98 | min-width: calc(var(--small-width) - 60px); 99 | } 100 | } 101 | 102 | /* HEADER SECTION */ 103 | 104 | #header__container { 105 | flex: 2; 106 | display: flex; 107 | flex-direction: column; 108 | justify-content: center; 109 | 110 | } 111 | 112 | #title-text__header, 113 | #server-status__header { 114 | margin: 0px 10px 0px 10px; 115 | text-align: center; 116 | flex: 0; 117 | padding:5px; 118 | } 119 | 120 | #server-status__header { 121 | font-weight: normal; 122 | } 123 | 124 | /* DEVICE CREATION SECTION */ 125 | 126 | #server-info__container { 127 | border-top: 1px solid black; 128 | border-bottom: 1px solid black; 129 | flex: 2; 130 | display: flex; 131 | flex-direction: column; 132 | } 133 | 134 | .server-info-holder__server-info { 135 | flex: 1; 136 | display: flex; 137 | flex-direction: row; 138 | justify-content: space-around; 139 | } 140 | 141 | .server-info-title { 142 | flex:2; 143 | min-width: 10%; 144 | padding: 10px; 145 | margin: 10px 5px 10px 10px; 146 | } 147 | 148 | .server-info-textbox { 149 | flex:5; 150 | min-width: 10%; 151 | padding: 10px; 152 | margin: 10px 10px 10px 5px; 153 | } 154 | 155 | /* DEVICE HOLDER SECTION */ 156 | 157 | #device-holder__container { 158 | flex: 10; 159 | overflow-y: auto; 160 | min-height: 100px; 161 | padding: 10px; 162 | } 163 | 164 | .device__device-holder { 165 | min-height: 60px; 166 | max-height: 60px; 167 | display: flex; 168 | margin-bottom: 10px; 169 | padding: 10px; 170 | } 171 | 172 | .device-id__device { 173 | flex: 8; 174 | padding: 10px; 175 | margin-left: 5px; 176 | min-width: 50px; 177 | } 178 | 179 | .device-image__device{ 180 | padding-right: 10px; 181 | } 182 | 183 | /* HOME NAVIGATION SECTION */ 184 | 185 | #home-navigation__container { 186 | border-top: 1px solid black; 187 | border-bottom: 1px solid black; 188 | flex: 2; 189 | display: flex; 190 | flex-direction: column; 191 | justify-content: center; 192 | } 193 | 194 | #h3__home-navigation{ 195 | text-align: center; 196 | min-width: 100%; 197 | flex:0.1; 198 | } 199 | 200 | /* COLOUR HIGHLIGHTING SECTION */ 201 | 202 | .device--active { 203 | background-color: var(--active-bg-colour); 204 | border-color: var(--active-border-colour); 205 | } 206 | 207 | .device--inactive { 208 | background-color: var(--inactive-bg-colour); 209 | border-color: var(--inactive-border-colour); 210 | } 211 | 212 | .device--no-response { 213 | background-color: var(--no-response-bg-colour); 214 | border-color: var(--no-response-border-colour); 215 | } 216 | -------------------------------------------------------------------------------- /public/pair.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Server and Device Discovery 6 | 7 | 8 | 9 |
10 |
11 |

Server and Device Discovery

12 |

Current status: Waiting for Steam Authentication...

13 |
14 |
15 |
16 |

Server IP Address:

17 | 18 |
19 |
20 |

Server Port:

21 | 22 |
23 |
24 |

Steam ID:

25 | 26 |
27 |
28 |

Player Token:

29 | 30 |
31 |
32 |
33 | 34 |
35 |
36 |

Back to homepage

37 |
38 |
39 | 40 | 41 | 42 | 43 | 44 | 50 | -------------------------------------------------------------------------------- /public/pair.js: -------------------------------------------------------------------------------- 1 | let socket, steamAuthToken, expoPushToken; 2 | 3 | function initVariables() { 4 | serverStatus = document.querySelector('#server-status__header'); 5 | 6 | serverIPTextbox = document.querySelector('#textbox__server-ip-holder'); 7 | serverPortTextbox = document.querySelector('#textbox__server-port-holder'); 8 | steamIDTextbox = document.querySelector('#textbox__steam-id-holder'); 9 | playerTokenTextbox = document.querySelector('#textbox__player-token-holder'); 10 | 11 | deviceContainer = document.querySelector('#device-holder__container'); 12 | deviceTemplate = document.querySelector('#smart-device-template'); 13 | } 14 | 15 | function startWebSocket() { 16 | socket = io(); 17 | socket.emit('pair_client_connect', {'connectedFrom': window.location.href}); 18 | 19 | socket.on('pair_server_popup', function(data){ 20 | let newTab = window.open(); 21 | newTab.location.href = data.url; 22 | }); 23 | 24 | socket.on('pair_server_synced', function(data){ 25 | serverStatus.innerHTML = "Steam successfully authenticated, next pair a Rust server or smart device in-game" 26 | serverStatus.classList.add('device--active'); 27 | }); 28 | 29 | socket.on('pair_server_deleteOnLeave', function(data){ 30 | steamAuthToken = data.steamAuthToken; 31 | expoPushToken = data.expoPushToken; 32 | }); 33 | 34 | socket.on('pair_server_rustplusinfo', function(data){ 35 | if (data.type === 'server') { 36 | serverIPTextbox.value = data.ip; 37 | serverPortTextbox.value = data.port; 38 | steamIDTextbox.value = data.steamID; 39 | playerTokenTextbox.value = data.playerToken; 40 | 41 | [...document.querySelectorAll('.server-info-title')].forEach(item => { 42 | item.classList.remove('device--no-response'); 43 | item.classList.add('device--active'); 44 | }); 45 | } else { 46 | if (deviceContainer.querySelector(`#device${data.deviceID}`) === null) { 47 | let device = deviceTemplate.content.cloneNode(true); 48 | device.querySelector('.device__device-holder').id = `device${data.deviceID}`; 49 | 50 | let image; 51 | if (data.deviceType === 'Switch') { image = 'media/smartswitch.png'; } else { image = 'media/smartalarm.png'; } 52 | device.querySelector('.device-image__device').src = image; 53 | 54 | deviceContainer.prepend(device.querySelector('.device__device-holder')); 55 | } 56 | deviceContainer.prepend(deviceContainer.querySelector(`#device${data.deviceID}`)); 57 | 58 | const today = new Date(); 59 | const time = today.getHours() + ":" + today.getMinutes() + ":" + today.getSeconds(); 60 | deviceContainer.querySelector(`#device${data.deviceID}`).querySelector('.device-id__device').value = `Time Recieved: ${time} | Device ID: ${data.deviceID}`; 61 | } 62 | }); 63 | } 64 | 65 | function shutdown(){ 66 | if (steamAuthToken !== null && expoPushToken !== null) { 67 | socket.emit('pair_server_disconnect', {'steamAuthToken': steamAuthToken, 'expoPushToken': expoPushToken}); 68 | } 69 | } 70 | 71 | window.addEventListener('load', (event) => { 72 | startWebSocket(); 73 | initVariables(); 74 | window.addEventListener('beforeunload', function(e){ 75 | shutdown(); 76 | }); 77 | }); 78 | -------------------------------------------------------------------------------- /public/rustplussocket.js: -------------------------------------------------------------------------------- 1 | let socket; 2 | 3 | function startWebSocket() { 4 | socket = io(); 5 | 6 | socket.on('server_UpdateDevice', function(data){ // Recieved when a device needs to be re/rendered on the client side. 7 | let clone; 8 | if (deviceHolder.querySelector(`#device${data.deviceID}`) === null) { 9 | if (data.deviceType === 'smartSwitch') { 10 | clone = smartSwitchTemplate.content.cloneNode(true); 11 | const deviceState = clone.querySelector('.state-btn__device-primitives'); 12 | 13 | deviceState.addEventListener('click', () => { 14 | socket.emit('client_UpdateDevice', { updateType : 'deviceState', state: deviceState.stateValue, deviceID: data.deviceID }); 15 | }); 16 | } 17 | if (data.deviceType === 'smartAlarm') { 18 | clone = smartAlarmTemplate.content.cloneNode(true); 19 | const discordNotificationBtn = clone.querySelector('.discord-notification-btn__notification-options'); 20 | 21 | discordNotificationBtn.addEventListener('click', () => { 22 | socket.emit('client_UpdateDevice', { updateType : 'discordNotificationState', state: discordNotificationBtn.stateValue, deviceID: data.deviceID }); 23 | }); 24 | 25 | } 26 | clone.querySelector('.device__device-holder').id = `device${data.deviceID}`; 27 | const deleteDeviceButton = clone.querySelector('.remove-btn__device-primitives'); 28 | const deviceNameTextbox = clone.querySelector('.name-txt__device-primitives'); 29 | 30 | 31 | deviceNameTextbox.addEventListener("focus", () => { 32 | deviceNameTextbox.onfocustext = deviceNameTextbox.value; 33 | }); 34 | 35 | deviceNameTextbox.addEventListener("focusout", () => { 36 | if ( deviceNameTextbox.onfocustext === deviceNameTextbox.value) { return } 37 | socket.emit('client_UpdateDevice', { updateType : 'deviceNameChange', state: deviceNameTextbox.value, deviceID: data.deviceID }); 38 | }); 39 | 40 | deleteDeviceButton.addEventListener('click', () => { 41 | socket.emit('client_DeleteDevice', data); 42 | }); 43 | 44 | deviceHolder.appendChild(clone.querySelector('.device__device-holder')); 45 | } 46 | 47 | clone = deviceHolder.querySelector(`#device${data.deviceID}`); 48 | 49 | if (data.deviceType === 'smartAlarm'){ 50 | const discordNotificationButton = clone.querySelector('.discord-notification-btn__notification-options'); 51 | 52 | discordNotificationButton.classList.remove('device--active'); 53 | discordNotificationButton.classList.remove('device--inactive'); 54 | 55 | discordNotificationButton.classList.add(data.notificationDiscord ? 'device--active' : 'device--inactive'); 56 | discordNotificationButton.stateValue = data.notificationDiscord; 57 | discordNotificationButton.innerHTML = data.notificationDiscord ? 'Discord message notifications: ON' : 'Discord message notifications: OFF'; 58 | } 59 | 60 | clone.querySelector('.name-txt__device-primitives').value = data.deviceName; 61 | const deviceState = clone.querySelector('.state-btn__device-primitives'); 62 | 63 | const possibleStates = ['Inactive', 'Active', 'No Response']; 64 | const possibleStateClasses = ['device--inactive', 'device--active','device--no-response']; 65 | 66 | possibleStates.forEach((state, i) => { 67 | if (data.deviceState === state){ 68 | deviceState.innerHTML = state; 69 | possibleStateClasses.forEach(item => { 70 | deviceState.classList.remove(item); 71 | }); 72 | deviceState.classList.add(possibleStateClasses[i]); 73 | deviceState.stateValue = Boolean(i); 74 | } 75 | }); 76 | }); 77 | 78 | socket.on('server_UpdateNotificationSettings', function(data){ // Recieved when a notification setting has been updated and needs to be rerendered. 79 | discordWebhookLinkTextbox.value = data.discordWebhookLink; 80 | discordMessageTextbox.value = data.discordMessage; 81 | }); 82 | 83 | socket.on('server_UpdateServerSettings', function(data){ // Recieved when a notification setting has been updated and needs to be rerendered. 84 | if (data.hostname === '') { data.hostname = 'N/A' } 85 | serverInfoHeader.innerHTML = `Current server: ${data.hostname} (${data.connectionState ? 'Connected' : 'Disconnected'})`; 86 | }); 87 | 88 | socket.on('server_ErrorDevice', function(data){ // Recieved when a device ID does not exist or already added. 89 | alert(data.error); 90 | }); 91 | 92 | socket.on('server_DeleteDevice', function(data){ // Recieved when a user has deleted a device and needs it to be destroyed in all current socket sessions. 93 | deviceHolder.querySelector(`#device${data.deviceID}`).remove(); 94 | }); 95 | } 96 | 97 | let serverSettingsOverlayBtn, notificationOverlayBtn, serverSettingsOverlay, notificationOverlay, serverSettingsSaveButton, 98 | notificationSaveButton, hostnameTextbox, serverPortTextbox, steamIDTextbox, playerTokenTextbox, discordWebhookLinkTextbox, 99 | discordMessageTextbox, serverInfoHeader, deviceHolder, addDeviceBtn, deviceIDtxt, deviceNametxt; 100 | 101 | function initVariables() { 102 | serverSettingsOverlayBtn = document.querySelector('#server-settings-btn__configuration'); 103 | notificationOverlayBtn = document.querySelector('#notification-btn__configuration'); 104 | 105 | serverSettingsOverlay = document.querySelector('#server-settings-overlay__server-settings-btn'); 106 | notificationOverlay = document.querySelector('#notification-overlay__notification-btn'); 107 | 108 | serverSettingsSaveButton= document.querySelector('#save__server-settings-container'); 109 | notificationSaveButton = document.querySelector('#save__notification-container'); 110 | 111 | hostnameTextbox = document.querySelector('#hostname__server-settings-container'); 112 | serverPortTextbox = document.querySelector('#server-port__server-settings-container'); 113 | steamIDTextbox = document.querySelector('#steamid__server-settings-container'); 114 | playerTokenTextbox = document.querySelector('#player-token__server-settings-container'); 115 | 116 | discordWebhookLinkTextbox = document.querySelector('#discord-webhook-link__notification-container'); 117 | discordMessageTextbox = document.querySelector('#discord-message__notification-container'); 118 | 119 | serverInfoHeader = document.querySelector('#server-info__header'); 120 | 121 | deviceHolder = document.querySelector('#device-holder__container'); 122 | smartSwitchTemplate = document.querySelector('#smart-switch-template'); 123 | smartAlarmTemplate = document.querySelector('#smart-alarm-template'); 124 | 125 | addDeviceBtn = document.querySelector('#add-device-btn__device-creation'); 126 | deviceIDtxt = document.querySelector('#device-id__device-detail-holder'); 127 | deviceNametxt = document.querySelector('#device-name__device-detail-holder'); 128 | } 129 | 130 | function initEventHandlers() { 131 | serverSettingsOverlayBtn.addEventListener('click', () => { 132 | serverSettingsOverlay.style.display = "block"; 133 | }); 134 | 135 | notificationOverlayBtn.addEventListener('click', () => { 136 | notificationOverlay.style.display = "block"; 137 | }); 138 | 139 | document.body.addEventListener('click', e => { // Was having werid click through events when using eventListeners and xxx-container 140 | if(e.target === serverSettingsOverlay || e.target === notificationOverlay){ 141 | notificationOverlay.style.display = "none"; 142 | serverSettingsOverlay.style.display = "none"; 143 | } 144 | }); 145 | 146 | serverSettingsSaveButton.addEventListener('click', () => { 147 | listOfTextboxToCheck = [hostnameTextbox, serverPortTextbox, steamIDTextbox, playerTokenTextbox]; 148 | let errorCheck = false; 149 | 150 | listOfTextboxToCheck.forEach(textbox => { 151 | textbox.classList.remove("--error-highlight"); 152 | if(textbox.value === "") { 153 | textbox.classList.add("--error-highlight"); 154 | errorCheck = true; 155 | } 156 | }); 157 | 158 | if(errorCheck) { 159 | alert("Error! Please fill in all the textboxes") 160 | } else { 161 | let dataToSend = { 162 | "hostname": hostnameTextbox.value, 163 | "rustPlusPort": parseInt(serverPortTextbox.value), 164 | "steamID": steamIDTextbox.value, 165 | "playerToken": parseInt(playerTokenTextbox.value) 166 | }; 167 | socket.emit('client_UpdateServerSettings', dataToSend); 168 | } 169 | }); 170 | 171 | notificationSaveButton.addEventListener('click', () => { 172 | listOfTextboxToCheck = [discordWebhookLinkTextbox, discordMessageTextbox]; 173 | let errorCheck = false; 174 | 175 | listOfTextboxToCheck.forEach(textbox => { 176 | textbox.classList.remove("--error-highlight"); 177 | if(textbox.value === "") { 178 | textbox.classList.add("--error-highlight"); 179 | errorCheck = true; 180 | } 181 | }); 182 | 183 | if(errorCheck) { 184 | alert("Error! Please fill in all the textboxes") 185 | } else { 186 | let dataToSend = { 187 | "discordWebhookLink": discordWebhookLinkTextbox.value, 188 | "discordMessage": discordMessageTextbox.value 189 | }; 190 | socket.emit('client_UpdateNotificationSettings', dataToSend); 191 | } 192 | }); 193 | 194 | addDeviceBtn.addEventListener('click', () => { 195 | 196 | let err = false; 197 | [deviceIDtxt, deviceNametxt].forEach(item => { 198 | item.classList.remove("--error-highlight"); 199 | if (item.value === '') { 200 | item.classList.add("--error-highlight"); 201 | err = true; 202 | } 203 | }); 204 | 205 | if (err) { 206 | alert('Error! missing required fields.'); 207 | } else { 208 | socket.emit('client_AddDevice', {deviceID : deviceIDtxt.value, deviceName : deviceNametxt.value}); 209 | } 210 | }); 211 | } 212 | 213 | window.addEventListener('load', (event) => { 214 | initVariables(); 215 | initEventHandlers(); 216 | startWebSocket(); 217 | }); 218 | -------------------------------------------------------------------------------- /public/styles.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --small-width: 300px; 3 | --medium-width: 570px; 4 | --large-width: 1010px; 5 | } 6 | 7 | @media (prefers-color-scheme:light) { 8 | :root { 9 | --main-bg-colour: #23303B; 10 | } 11 | } 12 | 13 | @media (prefers-color-scheme:dark) { 14 | :root { 15 | --accept-bg-colour: #DAE8FC; 16 | --accept-border-colour: #6C8EBF; 17 | --active-bg-colour: #D5E8D4; 18 | --active-border-colour: #82B366; 19 | --inactive-bg-colour: #F8CECC; 20 | --inactive-border-colour: #B85450; 21 | --no-response-bg-colour: #FFF2CC; 22 | --no-response-border-colour: #D6B656; 23 | --remove-bg-colour: #FF3333; 24 | --remove-border-colour: #B85450; 25 | --remove-text-colour: #FFFFFF; 26 | 27 | --foreground-bg-colour: #FFFFFF; 28 | --foreground-border-colour: #000000; 29 | } 30 | } 31 | 32 | .--error-highlight{ 33 | border-color: red; 34 | } 35 | 36 | #container, 37 | body, 38 | html { 39 | min-height: 100%; 40 | height: 100%; 41 | margin: 0; 42 | } 43 | 44 | * { 45 | font-family: helvetica; 46 | } 47 | 48 | button { 49 | border: 1px solid black; 50 | } 51 | 52 | /* CONTAINER SECTION */ 53 | 54 | #container { 55 | display: flex; 56 | flex-direction: column; 57 | justify-content: space-between; 58 | align-items: center 59 | } 60 | 61 | .container--narrow-width{ 62 | 63 | } 64 | 65 | .setting--narrow-width{ 66 | border: 1px solid grey; 67 | } 68 | 69 | @media (min-width:1080px) { 70 | .container--narrow-width { 71 | max-width: var(--large-width); 72 | min-width: var(--large-width); 73 | } 74 | .setting--narrow-width { 75 | max-width: calc(var(--large-width) - 60px); 76 | min-width: calc(var(--large-width) - 60px); 77 | } 78 | } 79 | 80 | @media (max-width:1079px) and (min-width:640px) { 81 | .container--narrow-width { 82 | max-width: var(--medium-width); 83 | min-width: var(--medium-width); 84 | } 85 | .setting--narrow-width { 86 | max-width: calc(var(--medium-width) - 60px); 87 | min-width: calc(var(--medium-width) - 60px); 88 | } 89 | } 90 | 91 | @media (max-width:640px) { 92 | .container--narrow-width { 93 | max-width: var(--small-width); 94 | min-width: var(--small-width); 95 | } 96 | .setting--narrow-width { 97 | max-width: calc(var(--small-width) - 60px); 98 | min-width: calc(var(--small-width) - 60px); 99 | } 100 | } 101 | 102 | /* HEADER SECTION */ 103 | 104 | #header__container { 105 | flex: 2; 106 | display: flex; 107 | flex-direction: column; 108 | justify-content: center; 109 | } 110 | 111 | #title-text__header, 112 | #server-info__header { 113 | margin: 0; 114 | text-align: center; 115 | flex: 0; 116 | } 117 | 118 | #server-info__header { 119 | font-weight: normal; 120 | } 121 | 122 | /* DEVICE CREATION SECTION */ 123 | 124 | #device-creation__container { 125 | border-top: 1px solid black; 126 | border-bottom: 1px solid black; 127 | flex: 2; 128 | display: flex; 129 | flex-direction: column; 130 | } 131 | 132 | #title__device-creation { 133 | text-align: center; 134 | font-weight: normal; 135 | margin-top: 15px; 136 | margin-bottom: 5px; 137 | } 138 | 139 | #device-detail-holder__device-creation { 140 | flex: 1; 141 | display: flex; 142 | flex-direction: row; 143 | justify-content: space-around; 144 | } 145 | 146 | #add-device-btn__device-creation{ 147 | flex:0.8; 148 | margin: 0px 0px 15px 0px; 149 | padding: 5px; 150 | background-color: var(--accept-bg-colour); 151 | border-color: var(--accept-border-colour); 152 | } 153 | 154 | #device-id__device-detail-holder { 155 | flex:2; 156 | min-width: 10%; 157 | padding: 10px; 158 | margin: 10px 5px 10px 0px; 159 | } 160 | #device-name__device-detail-holder { 161 | flex:2; 162 | min-width: 10%; 163 | padding: 10px; 164 | margin: 10px 0px 10px 5px; 165 | } 166 | 167 | /* DEVICE HOLDER SECTION */ 168 | 169 | #device-holder__container { 170 | flex: 12; 171 | overflow-y: auto; 172 | min-height: 100px; 173 | } 174 | 175 | .device__device-holder { 176 | border: 1px solid black; 177 | min-height: 60px; 178 | max-height: 140px; 179 | padding: 10px; 180 | margin-top: 20px; 181 | background-color: #F5F5F5; 182 | border-color: #666666; 183 | padding-top: 15px; 184 | padding-bottom: 15px; 185 | } 186 | 187 | .device-primitives__device { 188 | min-height: 60px; 189 | max-height: 60px; 190 | display: flex; 191 | } 192 | 193 | .notification-options__device{ 194 | margin-top: 5px; 195 | min-height: 40px; 196 | max-height: 40px; 197 | display: flex; 198 | flex-direction: column; 199 | } 200 | 201 | .discord-notification-btn__notification-options, 202 | .buzzer-notification-btn__notification-options { 203 | flex: 1; 204 | margin-top: 5px; 205 | padding: 5px; 206 | } 207 | 208 | .image-img__device-primitives { 209 | 210 | } 211 | 212 | .name-txt__device-primitives { 213 | flex: 8; 214 | padding: 10px; 215 | margin-left: 5px; 216 | min-width: 50px; 217 | } 218 | 219 | .state-btn__device-primitives { 220 | flex: 4; 221 | margin-left: 5px; 222 | } 223 | 224 | .remove-btn__device-primitives { 225 | flex: 2; 226 | margin-left: 5px; 227 | background-color: var(--remove-bg-colour); 228 | border-color: var(--remove-border-colour); 229 | color: var(--remove-text-colour); 230 | } 231 | 232 | .device--active { 233 | background-color: var(--active-bg-colour); 234 | border-color: var(--active-border-colour); 235 | } 236 | 237 | .device--inactive { 238 | background-color: var(--inactive-bg-colour); 239 | border-color: var(--inactive-border-colour); 240 | } 241 | 242 | .device--no-response { 243 | background-color: var(--no-response-bg-colour); 244 | border-color: var(--no-response-border-colour); 245 | } 246 | 247 | /* CONFIGURATION SECTION */ 248 | 249 | #configuration__container { 250 | flex: 1; 251 | display: flex; 252 | flex-direction: row; 253 | margin: 10px 0px 10px 0px; 254 | } 255 | 256 | #notification-btn__configuration { 257 | flex: 1; 258 | margin: 10px 5px 10px 0px; 259 | } 260 | #server-settings-btn__configuration { 261 | flex: 1; 262 | margin: 10px 0px 10px 5px; 263 | } 264 | 265 | #notification-overlay__notification-btn, 266 | #server-settings-overlay__server-settings-btn { 267 | position: fixed; 268 | display: none; 269 | width: 100%; 270 | height: 100%; 271 | top: 0; 272 | left: 0; 273 | right: 0; 274 | bottom: 0; 275 | background-color: rgba(0,0,0,0.5); 276 | cursor: pointer; 277 | } 278 | #server-settings-container__notification-overlay, 279 | #notification-container__notification-overlay { 280 | position: absolute; 281 | top: 50%; 282 | left: 50%; 283 | transform: translate(-50%,-50%); 284 | -ms-transform: translate(-50%,-50%); 285 | display: flex; 286 | flex-direction: column; 287 | background-color: var(--foreground-bg-colour); 288 | border-color: var(--foreground-border-colour); 289 | padding:10px; 290 | } 291 | 292 | /* SERVER SETTINGS */ 293 | 294 | #title__server-settings-container, 295 | #hostname__server-settings-container, 296 | #server-port__server-settings-container, 297 | #steamid__server-settings-container, 298 | #player-token__server-settings-container, 299 | #save__server-settings-container { 300 | flex: 1; 301 | margin: 5px; 302 | } 303 | 304 | #title__server-settings-container { 305 | text-align: center; 306 | text-decoration: underline; 307 | flex: 1; 308 | margin-top: 15px; 309 | margin-bottom: 15px; 310 | font-weight: normal; 311 | } 312 | 313 | #hostname__server-settings-container, 314 | #server-port__server-settings-container, 315 | #steamid__server-settings-container, 316 | #player-token__server-settings-container { 317 | padding: 5px; 318 | border-width: 1px; 319 | border-style: solid; 320 | } 321 | 322 | #save__server-settings-container { 323 | margin-top: 10px; 324 | margin-bottom: 10px; 325 | padding: 5px; 326 | background-color: var(--accept-bg-colour); 327 | border-color: var(--accept-border-colour); 328 | } 329 | 330 | /* NOTIFICATION SETTINGS */ 331 | 332 | #title__notification-container { 333 | text-align: center; 334 | text-decoration: underline; 335 | flex: 1; 336 | margin-top: 15px; 337 | margin-bottom: 15px; 338 | font-weight: normal; 339 | } 340 | 341 | #gpio-pin-container__notification-container, 342 | #discord-webhook-link__notification-container, 343 | #discord-message__notification-container, 344 | #save__notification-container { 345 | flex: 1; 346 | margin: 5px; 347 | } 348 | 349 | #gpio-pin-container__notification-container { 350 | display: flex; 351 | flex-direction: row; 352 | } 353 | 354 | #label__gpio-pin-container, 355 | #select__gpio-pin-container { 356 | padding: 5px; 357 | } 358 | 359 | #label__gpio-pin-container{ 360 | flex: 2; 361 | } 362 | 363 | #select__gpio-pin-container{ 364 | flex: 5; 365 | border-width: 1px; 366 | border-style: solid; 367 | } 368 | 369 | #discord-webhook-link__notification-container, 370 | #discord-message__notification-container { 371 | padding: 5px; 372 | border-width: 1px; 373 | border-style: solid; 374 | } 375 | 376 | #save__notification-container { 377 | margin-top: 10px; 378 | margin-bottom: 10px; 379 | padding: 5px; 380 | background-color: var(--accept-bg-colour); 381 | border-color: var(--accept-border-colour); 382 | } 383 | -------------------------------------------------------------------------------- /public/success.html: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | require('dotenv').config(); 2 | const RustPlus = require('rustplus-api'); 3 | const { Webhook } = require('discord-webhook-node'); 4 | const express = require('express'); 5 | const socket = require('socket.io'); 6 | const low = require('lowdb'); 7 | const FileSync = require('lowdb/adapters/FileSync'); 8 | const axios = require('axios'); 9 | const { v4: uuidv4 } = require('uuid'); 10 | const { register, listen } = require('push-receiver'); 11 | 12 | let currentlyConnected = false; 13 | let rustplus; 14 | const adapter = new FileSync('db.json'); 15 | const db = low(adapter); 16 | 17 | db.defaults({ 18 | severSettingsHostname: "", 19 | severSettingsRustPlusPort: 0, 20 | severSettingsSteamID: "", 21 | serverSettingsPlayerToken: 0, 22 | notificationSettingsDiscordURL: "", 23 | notificationSettingsDiscordMessage: "", 24 | devices: [] 25 | }).write(); // See examples/jsonLayout.txt for more info on database storage. 26 | 27 | connectToRustPlus(); // Used to setup rustPlus object. 28 | 29 | 30 | const app = express(); 31 | const server = app.listen(80, function(){ 32 | console.log('listening for requests on port 80,'); 33 | }); 34 | app.use(express.static('public')); 35 | 36 | app.use(express.static('public', { 37 | extensions: 'html' 38 | })); 39 | 40 | 41 | const io = socket(server); 42 | io.on('connection', (socket) => { 43 | if (currentlyConnected) { 44 | db.get('devices').value().forEach(smartDevice => { 45 | socket.emit('server_UpdateDevice', smartDevice); 46 | rustplus.getEntityInfo(smartDevice.deviceID, (message) => { 47 | return true; 48 | }); 49 | }); 50 | } 51 | 52 | socket.emit('server_UpdateServerSettings', {"hostname": db.get('severSettingsHostname').value(), "connectionState": currentlyConnected}); 53 | socket.emit('server_UpdateNotificationSettings', { 54 | "discordWebhookLink": db.get('notificationSettingsDiscordURL').value(), 55 | "discordMessage": db.get('notificationSettingsDiscordMessage').value()} 56 | ); 57 | 58 | socket.on('client_AddDevice', function(data){ // Used when the client pressed the new device button. 59 | rustplus.getEntityInfo(parseInt(data.deviceID), message => { 60 | if(message.response.entityInfo === null) { 61 | socket.emit('server_ErrorDevice', {error: "Failed to create device, either the server is not responding or the device ID does not exist."}); 62 | } else { 63 | if (db.get('devices').find({ deviceID: parseInt(data.deviceID) }).value() != undefined){ 64 | socket.emit('server_ErrorDevice', { error: "Failed to create device, device already exists." }); 65 | } else { 66 | if (message.response.entityInfo['type'] - 1) { 67 | db.get('devices') 68 | .push({ 69 | deviceID: parseInt(data.deviceID), 70 | deviceName : data.deviceName, 71 | deviceState: message.response.entityInfo.payload.value ? 'Active': 'Inactive', 72 | deviceType: 'smartAlarm', 73 | notificationDiscord: false, 74 | notificationBuzzer: false 75 | }) 76 | .write(); 77 | } else { 78 | db.get('devices') 79 | .push({ 80 | deviceID: parseInt(data.deviceID), 81 | deviceName : data.deviceName, 82 | deviceState: message.response.entityInfo.payload.value ? 'Active': 'Inactive', 83 | deviceType: 'smartSwitch' 84 | }) 85 | .write(); 86 | } 87 | io.emit('server_UpdateDevice', db.get('devices').find({ deviceID: parseInt(data.deviceID) }).value()); 88 | } 89 | } 90 | return true; 91 | }); 92 | }); 93 | 94 | socket.on('client_UpdateDevice', function(data){ // Used as a generic way of updating any info of a component. 95 | 96 | if (data.updateType === 'deviceState') { 97 | rustplus.getEntityInfo(parseInt(data.deviceID), message => { 98 | if(message.response.entityInfo === null) { 99 | db.get('devices') 100 | .find({ deviceID: data.deviceID }) 101 | .assign({ deviceState: 'No Response'}) 102 | .write(); 103 | } else { 104 | if (data.state) { 105 | rustplus.turnSmartSwitchOff(data.deviceID, () => { return true; }); 106 | } else { 107 | rustplus.turnSmartSwitchOn(data.deviceID, () => { return true; }); 108 | } 109 | rustplus.getEntityInfo(data.deviceID, (messageAfterToggle) => { 110 | db.get('devices') 111 | .find({ deviceID: data.deviceID }) 112 | .assign({ deviceState: messageAfterToggle.response.entityInfo.payload.value ? 'Active': 'Inactive' }) 113 | .write(); 114 | return true; 115 | }); 116 | } 117 | return true; 118 | }); 119 | } 120 | 121 | if (data.updateType === 'discordNotificationState') { 122 | db.get('devices') 123 | .find({ deviceID: data.deviceID }) 124 | .assign({ notificationDiscord: !data.state}) 125 | .write(); 126 | } 127 | 128 | if (data.updateType === 'buzzerNotificationState') { 129 | db.get('devices') 130 | .find({ deviceID: data.deviceID }) 131 | .assign({ notificationBuzzer: !data.state}) 132 | .write(); 133 | } 134 | 135 | if (data.updateType === 'deviceNameChange'){ 136 | db.get('devices') 137 | .find({ deviceID: data.deviceID }) 138 | .assign({ deviceName: data.state}) 139 | .write(); 140 | } 141 | 142 | io.emit('server_UpdateDevice', db.get('devices').find({ deviceID: data.deviceID }).value()); 143 | }); 144 | 145 | 146 | socket.on('client_DeleteDevice', function(data){ 147 | db.get('devices') 148 | .remove({ deviceID: data.deviceID }) 149 | .write(); 150 | io.emit('server_DeleteDevice', data); 151 | }); 152 | 153 | socket.on('client_UpdateNotificationSettings', function(data){ 154 | db.set('notificationSettingsDiscordURL', data.discordWebhookLink).write(); 155 | db.set('notificationSettingsDiscordMessage', data.discordMessage).write(); 156 | 157 | io.sockets.emit('server_UpdateNotificationSettings', { 158 | "discordWebhookLink": db.get('notificationSettingsDiscordURL').value(), 159 | "discordMessage": db.get('notificationSettingsDiscordMessage').value()} 160 | ); 161 | }); 162 | 163 | socket.on('client_UpdateServerSettings', function(data){ // NEED TO FINISH. 164 | db.set('severSettingsHostname', data.hostname).write(); 165 | db.set('severSettingsRustPlusPort', data.rustPlusPort).write(); 166 | db.set('severSettingsSteamID', data.steamID).write(); 167 | db.set('serverSettingsPlayerToken', data.playerToken).write(); 168 | 169 | connectToRustPlus(); 170 | 171 | io.sockets.emit('server_UpdateServerSettings', {"hostname": db.get('severSettingsHostname').value(), "connectionState": currentlyConnected}); 172 | }); 173 | 174 | // Stuff for pair.js 175 | 176 | socket.on('pair_client_connect', function(data){ // NEED TO FINISH. 177 | pairClientConnect({'socketID': socket.id, 'connectedFrom': data.connectedFrom}); 178 | }); 179 | 180 | socket.on('pair_server_disconnect', function(data) { 181 | shutdown(data.steamAuthToken, data.expoPushToken); 182 | }); 183 | }); 184 | 185 | async function pairClientConnect(data) { 186 | const credentials = await register('976529667804'); 187 | axios.post('https://exp.host/--/api/v2/push/getExpoPushToken', { 188 | deviceId: uuidv4(), 189 | experienceId: '@facepunch/RustCompanion', 190 | appId: 'com.facepunch.rust.companion', 191 | deviceToken: credentials.fcm.token, 192 | type: 'fcm', 193 | development: false, 194 | }).then(async (response) => { 195 | 196 | expoPushToken = response.data.data.expoPushToken; 197 | 198 | app.get(`/pair/${data.socketID}`, (req, res) => { 199 | 200 | steamAuthToken = req.query.token; 201 | 202 | io.to(data.socketID).emit('pair_server_deleteOnLeave',{'steamAuthToken': steamAuthToken, 'expoPushToken': expoPushToken}); 203 | 204 | axios.post('https://companion-rust.facepunch.com:443/api/push/register', { 205 | AuthToken: steamAuthToken, 206 | DeviceId: 'rustplus-api', 207 | PushKind: 0, 208 | PushToken: expoPushToken, 209 | }).then((response) => { 210 | io.to(data.socketID).emit('pair_server_synced',{}); 211 | waitForRustPlusInfo(credentials, data.socketID); 212 | }).catch((error) => { 213 | console.log("Failed to register with Rust Companion API"); 214 | console.log(error); 215 | }); 216 | 217 | res.sendFile('public/success.html', {root: __dirname }) 218 | }); 219 | const url = "https://companion-rust.facepunch.com/login?returnUrl=" + encodeURIComponent(`${data.connectedFrom}/${data.socketID}?expoPushToken=expoPushToken`); 220 | io.to(data.socketID).emit('pair_server_popup', { 'url': url }); 221 | 222 | }).catch((error) => { 223 | console.log("Failed to fetch Expo Push Token"); 224 | console.log(error); 225 | }); 226 | } 227 | 228 | async function waitForRustPlusInfo(credentials, socketID){ 229 | await listen(credentials, ({ notification, persistentId }) => { 230 | const body = JSON.parse(notification.data.body); 231 | 232 | if (body.type === 'server') { 233 | io.to(socketID).emit('pair_server_rustplusinfo', { 'type': body.type,'ip': body.ip, 'port': body.port, 'playerToken': body.playerToken, 'steamID': body.playerId}); 234 | } else { 235 | io.to(socketID).emit('pair_server_rustplusinfo', { 'type': body.type, 'deviceID': body.entityId, 'deviceType': body.entityName}); 236 | } 237 | }); 238 | } 239 | 240 | async function shutdown(steamAuthToken, expoPushToken) { 241 | // unregister with Rust Companion API 242 | if(steamAuthToken){ 243 | await axios.delete('https://companion-rust.facepunch.com:443/api/push/unregister', { 244 | data: { 245 | AuthToken: steamAuthToken, 246 | PushToken: expoPushToken, 247 | DeviceId: 'rustplus-api', 248 | }, 249 | }).then((response) => { 250 | 251 | }).catch((error) => { 252 | console.log(error); 253 | }); 254 | } 255 | } 256 | 257 | function connectToRustPlus(){ 258 | const hostname = db.get('severSettingsHostname').value(); 259 | const rustPlusPort = db.get('severSettingsRustPlusPort').value(); 260 | const steamID = db.get('severSettingsSteamID').value(); 261 | const playerToken = db.get('serverSettingsPlayerToken').value(); 262 | 263 | rustplus = new RustPlus(hostname, rustPlusPort, steamID, playerToken); 264 | 265 | 266 | rustplus.on('error', e => { 267 | currentlyConnected = false; 268 | io.sockets.emit('server_UpdateServerSettings', {"hostname": db.get('severSettingsHostname').value(), "connectionState": currentlyConnected}); 269 | 270 | db.get('devices').value().forEach(smartDevice => { 271 | smartDevice.deviceState = 'No Response'; 272 | io.sockets.emit('server_UpdateDevice', smartDevice); 273 | }); 274 | }); 275 | 276 | rustplus.on('connected', () => { 277 | currentlyConnected = true; 278 | io.sockets.emit('server_UpdateServerSettings', {"hostname": db.get('severSettingsHostname').value(), "connectionState": currentlyConnected}); 279 | // rustplus.sendTeamMessage(`Hello! Server initialised at ${new Date()}`); 280 | 281 | db.get('devices').value().forEach(smartDevice => { 282 | rustplus.getEntityInfo(smartDevice.deviceID, message => { 283 | if(message.response.entityInfo === null) { 284 | db.get('devices') 285 | .find({ deviceID: smartDevice.deviceID }) 286 | .assign({ deviceState: 'No Response'}) 287 | .write(); 288 | } else { 289 | db.get('devices') 290 | .find({ deviceID: smartDevice.deviceID }) 291 | .assign({ deviceState: message.response.entityInfo.payload.value ? 'Active': 'Inactive'}) 292 | .write() 293 | } 294 | io.sockets.emit('server_UpdateDevice', smartDevice); 295 | return true; 296 | }); 297 | }); 298 | }); 299 | 300 | rustplus.on('message', (message) => { 301 | if(message.broadcast && message.broadcast.teamMessage && message.broadcast.teamMessage.message.message.includes('!time')){ 302 | rustplus.sendRequest({ 303 | getTime: {} 304 | }, (message) => { 305 | const currentTimeHour = parseInt(message.response.time.time); 306 | const currentTimeMinute = Math.floor((message.response.time.time % 1) * 60); 307 | if (message.response.time.time < message.response.time.sunset && message.response.time.time > message.response.time.sunrise){ 308 | const hoursUntilSunset = parseInt((message.response.time.sunset - message.response.time.time)); 309 | const minutesUntilSunset = Math.floor(((message.response.time.sunset - message.response.time.time) % 1) * 60); 310 | rustplus.sendTeamMessage(`The current time is ${currentTimeHour}:${addZeroIfBelowTen(currentTimeMinute)} and there is ${hoursUntilSunset} hours and ${minutesUntilSunset} minutes until sunset`); 311 | } else { 312 | let hoursUntilSunrise; 313 | let minutesUntilSunrise; 314 | if (message.response.time.time > message.response.time.sunrise){ 315 | hoursUntilSunrise = parseInt((24 % message.response.time.time) + message.response.time.sunrise); 316 | minutesUntilSunrise = Math.floor((((24 % message.response.time.time) + message.response.time.sunrise) % 1) * 60) 317 | } else { 318 | hoursUntilSunrise = parseInt(message.response.time.sunrise - message.response.time.time); 319 | minutesUntilSunrise = Math.floor(((message.response.time.sunrise - message.response.time.time) % 1) * 60); 320 | } 321 | rustplus.sendTeamMessage(`The current time is ${currentTimeHour}:${addZeroIfBelowTen(currentTimeMinute)} and there is ${hoursUntilSunrise} hours and ${minutesUntilSunrise} minutes until sunrise`); 322 | } 323 | }); 324 | } 325 | 326 | if(message.broadcast && message.broadcast.entityChanged){ 327 | const entityChanged = message.broadcast.entityChanged; 328 | db.get('devices') 329 | .find({ deviceID: entityChanged.entityId }) 330 | .assign({ deviceState: entityChanged.payload.value ? 'Active' : 'Inactive'}) 331 | .write(); 332 | io.emit('server_UpdateDevice',db.get('devices').find({ deviceID: entityChanged.entityId }).value()); 333 | 334 | if (db.get('devices').find({ deviceID: entityChanged.entityId }).value().deviceState === 'Inactive') { return; } 335 | 336 | if (db.get('devices').find({ deviceID: entityChanged.entityId }).value().notificationDiscord ) { 337 | const hook = new Webhook(db.get('notificationSettingsDiscordURL').value()); 338 | const IMAGE_URL = 'https://static.wikia.nocookie.net/play-rust/images/0/06/Rocket_Launcher_icon.png'; 339 | hook.setAvatar(IMAGE_URL); 340 | hook.setUsername('RUST+ ALERT'); 341 | hook.send(`${db.get('devices').find({ deviceID: entityChanged.entityId }).value().deviceName.toUpperCase()} : ${db.get('notificationSettingsDiscordMessage').value()}`); 342 | } 343 | } 344 | }); 345 | } 346 | 347 | function addZeroIfBelowTen(numberToCheck){ 348 | return (numberToCheck.toString().length < 2) ? "0"+numberToCheck : numberToCheck; 349 | } 350 | --------------------------------------------------------------------------------