├── maps ├── .gitignore └── blank.png ├── .gitignore ├── package.json ├── LICENSE ├── static ├── update.html └── index.html └── index.js /maps/.gitignore: -------------------------------------------------------------------------------- 1 | !.gitignore 2 | *.png 3 | !blank.png 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | uploads/ 3 | .DS_Store 4 | data.json 5 | -------------------------------------------------------------------------------- /maps/blank.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PlaceNL2022/Commando/HEAD/maps/blank.png -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "commando", 3 | "version": "1.0.0", 4 | "description": "De centrale orderserver", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git+https://github.com/PlaceNL/Commando.git" 12 | }, 13 | "author": "NoahvdAa", 14 | "license": "MIT", 15 | "bugs": { 16 | "url": "https://github.com/PlaceNL/Commando/issues" 17 | }, 18 | "homepage": "https://github.com/PlaceNL/Commando#readme", 19 | "dependencies": { 20 | "express.js": "^1.0.0", 21 | "get-pixels": "^3.3.3", 22 | "multer": "^1.4.4", 23 | "safe-compare": "^1.1.4", 24 | "ws": "^8.5.0" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Noah van der Aa 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 | -------------------------------------------------------------------------------- /static/update.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 11 | 12 | PlaceNL Commando 13 | 14 | 15 | 16 |
17 |
18 |

Nieuwe orders uploaden

19 |
20 |
21 | 22 | 23 |
24 |
25 | 26 | 27 |
28 |
29 | 30 | 31 |
32 |
33 | 34 |
35 |
36 |
37 |
38 | 39 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /static/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 11 | 12 | 16 | 17 | 57 | 58 | PlaceNL Commando 59 | 60 | 61 | 62 |
63 |
64 |

0

65 |

clients verbonden

66 |
67 |
68 |
69 |
70 | 71 |

huidige orders

72 |
73 |
74 |
75 |
76 |

ordergeschiedenis

77 | 79 |
80 |
81 | 82 |
☀️
83 | 84 | order control 85 | 86 | 89 | 90 | 93 | 94 | 160 | 161 | 162 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const fs = require('fs'); 3 | const ws = require('ws'); 4 | 5 | const app = express(); 6 | const getPixels = require('get-pixels'); 7 | 8 | const multer = require('multer') 9 | const upload = multer({ dest: `${__dirname}/uploads/` }); 10 | 11 | const safeCompare = require('safe-compare') 12 | 13 | const VALID_COLORS = ['#6D001A', '#BE0039', '#FF4500', '#FFA800', '#FFD635', '#FFF8B8', '#00A368', '#00CC78', '#7EED56', '#00756F', '#009EAA', '#00CCC0', '#2450A4', '#3690EA', '#51E9F4', '#493AC1', '#6A5CFF', '#94B3FF', '#811E9F', '#B44AC0', '#E4ABFF', '#DE107F', '#FF3881', '#FF99AA', '#6D482F', '#9C6926', '#FFB470', '#000000', '#515252', '#898D90', '#D4D7D9', '#FFFFFF']; 14 | 15 | var appData = { 16 | currentMap: 'blank.png', 17 | mapHistory: [ 18 | { file: 'blank.png', reason: 'Init ^Noah', date: 1648890843309 } 19 | ] 20 | }; 21 | var brandUsage = {}; 22 | var userCount = 0; 23 | var socketId = 0; 24 | 25 | if (fs.existsSync(`${__dirname}/data.json`)) { 26 | appData = require(`${__dirname}/data.json`); 27 | } 28 | 29 | const server = app.listen(3987); 30 | const wsServer = new ws.Server({ server: server, path: '/api/ws' }); 31 | 32 | app.use('/maps', (req, res, next) => { 33 | res.header('Access-Control-Allow-Origin', '*'); 34 | next(); 35 | }); 36 | app.use('/maps', express.static(`${__dirname}/maps`)); 37 | app.use(express.static(`${__dirname}/static`)); 38 | 39 | app.get('/api/stats', (req, res) => { 40 | res.json({ 41 | rawConnectionCount: wsServer.clients.size, 42 | connectionCount: userCount, 43 | ...appData, 44 | brands: brandUsage, 45 | date: Date.now() 46 | }); 47 | }); 48 | 49 | app.post('/updateorders', upload.single('image'), async (req, res) => { 50 | if (!req.body || !req.file || !req.body.reason || !req.body.password || !safeCompare(req.body.password, process.env.PASSWORD)) { 51 | res.send('Ongeldig wachtwoord!'); 52 | fs.unlinkSync(req.file.path); 53 | return; 54 | } 55 | 56 | if (req.file.mimetype !== 'image/png') { 57 | res.send('Bestand moet een PNG zijn!'); 58 | fs.unlinkSync(req.file.path); 59 | return; 60 | } 61 | 62 | getPixels(req.file.path, 'image/png', function (err, pixels) { 63 | if (err) { 64 | res.send('Fout bij lezen bestand!'); 65 | console.log(err); 66 | fs.unlinkSync(req.file.path); 67 | return 68 | } 69 | 70 | if (pixels.data.length !== 16000000) { 71 | res.send('Bestand moet 2000x2000 zijn!'); 72 | fs.unlinkSync(req.file.path); 73 | return; 74 | } 75 | 76 | for (var i = 0; i < 4000000; i++) { 77 | const r = pixels.data[i * 4]; 78 | const g = pixels.data[(i * 4) + 1]; 79 | const b = pixels.data[(i * 4) + 2]; 80 | 81 | const hex = rgbToHex(r, g, b); 82 | if (VALID_COLORS.indexOf(hex) === -1) { 83 | res.send(`Pixel op ${i % 2000}, ${Math.floor(i / 2000)} heeft ongeldige kleur.`); 84 | fs.unlinkSync(req.file.path); 85 | return; 86 | } 87 | } 88 | 89 | const file = `${Date.now()}.png`; 90 | fs.copyFileSync(req.file.path, `${__dirname}/maps/${file}`); 91 | fs.unlinkSync(req.file.path); 92 | appData.currentMap = file; 93 | appData.mapHistory.push({ 94 | file, 95 | reason: req.body.reason, 96 | date: Date.now() 97 | }) 98 | wsServer.clients.forEach((client) => client.send(JSON.stringify({ type: 'map', data: file, reason: req.body.reason }))); 99 | fs.writeFileSync(`${__dirname}/data.json`, JSON.stringify(appData)); 100 | res.redirect('/'); 101 | }); 102 | }); 103 | 104 | wsServer.on('connection', (socket) => { 105 | socket.id = socketId++; 106 | socket.brand = 'unknown'; 107 | socket.lastActivity = Date.now() - (5 * 6 * 1000); 108 | console.log(`[${new Date().toLocaleString()}] [+] Client connected: ${socket.id}`); 109 | 110 | socket.on('close', () => { 111 | console.log(`[${new Date().toLocaleString()}] [-] Client disconnected: ${socket.id}`); 112 | }); 113 | 114 | socket.on('message', (message) => { 115 | var data; 116 | try { 117 | data = JSON.parse(message); 118 | } catch (e) { 119 | socket.send(JSON.stringify({ type: 'error', data: 'Failed to parse message!' })); 120 | return; 121 | } 122 | 123 | if (!data.type) { 124 | socket.send(JSON.stringify({ type: 'error', data: 'Data missing type!' })); 125 | } 126 | 127 | switch (data.type.toLowerCase()) { 128 | case 'brand': 129 | const { brand } = data; 130 | if (brand === undefined || brand.length < 1 || brand.length > 32 || !isAlphaNumeric(brand)) return; 131 | socket.brand = data.brand; 132 | break; 133 | case 'getmap': 134 | socket.send(JSON.stringify({ type: 'map', data: appData.currentMap, reason: null })); 135 | break; 136 | case 'ping': 137 | socket.send(JSON.stringify({ type: 'pong' })); 138 | break; 139 | case 'placepixel': 140 | const { x, y, color } = data; 141 | if (x === undefined || y === undefined || color === undefined && x < 0 || x > 1999 || y < 0 || y > 1999 || color < 0 || color > 32) return; 142 | socket.lastActivity = Date.now(); 143 | // console.log(`[${new Date().toLocaleString()}] Pixel placed by ${socket.id}: ${x}, ${y}: ${color}`); 144 | break; 145 | default: 146 | socket.send(JSON.stringify({ type: 'error', data: 'Unknown command!' })); 147 | break; 148 | } 149 | }); 150 | }); 151 | 152 | setInterval(() => { 153 | const threshold = Date.now() - (11 * 60 * 1000); // 11 min cooldown. 154 | userCount = Array.from(wsServer.clients).filter(c => c.lastActivity >= threshold && c.brand !== 'unknown').length; 155 | brandUsage = Array.from(wsServer.clients).filter(c => c.lastActivity >= threshold).map(c => c.brand).reduce(function (acc, curr) { 156 | return acc[curr] ? ++acc[curr] : acc[curr] = 1, acc 157 | }, {}); 158 | }, 1000); 159 | 160 | function rgbToHex(r, g, b) { 161 | return '#' + ((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1).toUpperCase(); 162 | } 163 | 164 | function isAlphaNumeric(str) { 165 | var code, i, len; 166 | 167 | for (i = 0, len = str.length; i < len; i++) { 168 | code = str.charCodeAt(i); 169 | if (!(code > 47 && code < 58) && // numeric (0-9) 170 | !(code > 64 && code < 91) && // upper alpha (A-Z) 171 | !(code > 96 && code < 123) && // lower alpha (a-z) 172 | !(code == 45)) { // `-` character 173 | return false; 174 | } 175 | } 176 | return true; 177 | } 178 | --------------------------------------------------------------------------------