diff --git a/README.md b/README.md index e6931bb..c30ee1c 100644 --- a/README.md +++ b/README.md @@ -129,9 +129,17 @@ This route receives a json file from the frontend and updates the pixel. Request Data: JSON containing the keys `"secret"`, `"row"`, `"col"` and `"color"`. The `"secret"` string can be any value right now, but must be present. -Response Status Codes: If the update is successful, a `200 Success` is sent. If the update is successful, a `400 Too frequent update` is sent. If the json file is missing, a `401 No json file` is sent. If the required field in the json file is missing, a `402 Missing attributes` is sent. +Response Status Codes: If everything is okay and the pixel is updated, `200 OK` is returned. If the response has an invalid token (i.e. the PG isn't registered), `401 Unauthorized` is returned. Finally, if the PG is attempting to add a pixel too soon after the last one, `429 Too Many Requests` is returned. + +--- + +**POST** `/changePixelRate` + +This route is used to update the pixel rate. + +Request Data: JSON containing the keys `"new_rate"` and `"token"`. -**Note:** The FrontendManager is responsible for checking the timestamp and secret for the requst. The required request interval can be changed by changing the constant `time_gap` which is set to 3 seconds by default. +Response Status Codes: If everything is okay and the pixel_rate is updated, `200 OK` is returned. If the response has an invalid token (i.e. the PG isn't registered), `401 Unauthorized` is returned. If there are missing attributes in the json file `400 Missing attributes` is returned. ## Technical Details About Middleware @@ -153,12 +161,13 @@ There are a number of configurable components to the middleware that should be s A summary of the environment variables is given below: -* `MONGO_HOST` and `MONGO_PORT` specify the host and port, respectively, of the MongoDB instance. -* `TEMP_DIR` specifies the location of a temporary directory the middleware can create, write to, and read files from. This is used to store the files sent by the `/timelapse` route. -* `INITIAL_WIDTH` and `INITIAL_HEIGHT` specify the initial dimensions of the pixel board. -* `INITIAL_PALETTE` specifies the initial palette used, as a comma separated list of hex color codes (*without* a precedeing `#`). - - For example, the following is the string corresponding to the initial 16 colors available in the 2022 version of Reddit's r/place: `ffffff,d4d7d9,898d90,000000,9c6926,ff99aa,b44ac0,811e9f,51e9f4,3690ea,2450a4,7eed56,00a368,ffd635,ffa800,ff4500`. -* `PIXEL_RATE` specifies the number of milliseconds between pixel updates that are permitted for a specific PG. +- `MONGO_HOST` and `MONGO_PORT` specify the host and port, respectively, of the MongoDB instance. +- `TEMP_DIR` specifies the location of a temporary directory the middleware can create, write to, and read files from. This is used to store the files sent by the `/timelapse` route. +- `INITIAL_WIDTH` and `INITIAL_HEIGHT` specify the initial dimensions of the pixel board. +- `INITIAL_PALETTE` specifies the initial palette used, as a comma separated list of hex color codes (_without_ a precedeing `#`). + - For example, the following is the string corresponding to the initial 16 colors available in the 2022 version of Reddit's r/place: `ffffff,d4d7d9,898d90,000000,9c6926,ff99aa,b44ac0,811e9f,51e9f4,3690ea,2450a4,7eed56,00a368,ffd635,ffa800,ff4500`. +- `PIXEL_RATE` specifies the number of milliseconds between pixel updates that are permitted for a specific PG. +- `CHANGE_PIXEL_RATE_TOKEN` specifies the secret token for updating pixel rate, should be private to administrator. ### Secrets diff --git a/app.py b/app.py index 6a80b2f..1ba35ff 100644 --- a/app.py +++ b/app.py @@ -58,7 +58,7 @@ def PUT_register_pg(): resp.status_code = 400 print(resp) return resp - + # Ensure that secret is in the list of secrets if secrets and request.json["secret"] not in secrets: resp = make_response(jsonify({ @@ -109,7 +109,7 @@ def PUT_update_pixel(): resp.status_code = 401 return resp - # If the server isn't, we reject the update + # If the server is still in cooldown, we reject the update elif server_timeout != 0: resp = make_response(jsonify({ "success": False, @@ -134,7 +134,8 @@ def PUT_update_pixel(): }) return jsonify({ - "success": True + "success": True, + "rate": board_manager.get_pixel_rate() }), 200 @@ -183,10 +184,6 @@ def getPixelAuthor(col,row): "color": color }), 200 -@app.route('/changeByClick', methods=['POST']) -def changeByClick(): - return PUT_update_pixel() - @app.route('/servers', methods=['GET']) def GET_servers(): # Route for render server page @@ -196,6 +193,27 @@ def GET_servers(): return render_template('server.html', data={"servers": sort_servers}) +@app.route('/changePixelRate', methods=['POST']) +def POST_change_pixel_rate(): + # Check required field + for requiredField in ["new_rate", "token"]: + if requiredField not in request.json: + resp = make_response(jsonify({ + "success": False, + "error": f"Required field `{requiredField}` not present.", + })) + resp.status_code = 400 + print(resp) + return resp + + # Check token + if getenv("CHANGE_PIXEL_RATE_TOKEN") == request.json['token']: + board_manager.change_pixel_rate(int(request.json['new_rate'])) + return "Success", 200 + else: + return "Unauthorized", 401 + + if __name__ == '__main__': sio.run(app, getenv("HOST") or "127.0.0.1", getenv("PORT") or 5000, debug=True) diff --git a/boards.py b/boards.py index f8dc2fd..d98e887 100644 --- a/boards.py +++ b/boards.py @@ -18,13 +18,17 @@ INITIAL_PALETTE = ["#" + x for x in getenv("INITIAL_PALETTE").split(",")] else: INITIAL_PALETTE = random.choice([ - ["#a7f542", "#7EA7DF", "#f3bcf5", "#6F6E69", "#F8F075", "#9ff5ec", "#E5E5E5"], - ["#B3E2E2", "#7EA7DF", "#F0A099", "#6F6E69", "#F8F075", "#FFFFFF", "#E5E5E5"], + ["#a7f542", "#7EA7DF", "#f3bcf5", "#6F6E69", + "#F8F075", "#9ff5ec", "#E5E5E5"], + ["#B3E2E2", "#7EA7DF", "#F0A099", "#6F6E69", + "#F8F075", "#FFFFFF", "#E5E5E5"], ["#000000", "#FFFFFF", "#FF0000", "#00FF00", "#0000FF"], - ["#FFFFFF", "#FF0000", "#00FF00", "#0000FF", "#FF69B4", "#FFFF00", "#FFA500", "#FFC0CB", "#800080", "#000000", "#808080", "#FF00FF", "#00FFFF", "#40E0D0", "#ADD8E6", "#90EE90", "#FFB6C1", "#FFFFE0", "#D3D3D3", "#AA7A6F", "#BA648C", "#164F82"], + ["#FFFFFF", "#FF0000", "#00FF00", "#0000FF", "#FF69B4", "#FFFF00", "#FFA500", "#FFC0CB", "#800080", "#000000", "#808080", + "#FF00FF", "#00FFFF", "#40E0D0", "#ADD8E6", "#90EE90", "#FFB6C1", "#FFFFE0", "#D3D3D3", "#AA7A6F", "#BA648C", "#164F82"], # Reddit r/place in 2022: - ["#ffffff" ,"#d4d7d9" ,"#898d90" ,"#000000", "#9c6926", "#ff99aa" ,"#b44ac0", "#811e9f", "#51e9f4" , "#3690ea" ,"#2450a4", "#7eed56", "#00a368", "#ffd635", "#ffa800", "#ff4500"], + ["#ffffff", "#d4d7d9", "#898d90", "#000000", "#9c6926", "#ff99aa", "#b44ac0", "#811e9f", + "#51e9f4", "#3690ea", "#2450a4", "#7eed56", "#00a368", "#ffd635", "#ffa800", "#ff4500"], ]) TEMP_DIR = getenv("TEMP_DIR") or "tmp" @@ -32,6 +36,7 @@ PIXEL_RATE = int(getenv("PIXEL_RATE") or random.randint(100, 1000)) + class BoardManager: def __init__(self, db: Database): self.board = db["boards"] @@ -71,7 +76,6 @@ def update_current_board_by_list(self, updates): self.cache["pixels"][update["row"]][update["col"]] = update["color"] self.cache["lastModify"][update["row"]][update["col"]] = update["author"] - # Update board in database self.board.update_one( {"current": True}, {"$set": {"pixels": self.cache["pixels"],"lastModify":self.cache["lastModify"]}} @@ -87,7 +91,6 @@ def update_current_board_by_list(self, updates): update["time"] = now self.updates.insert_many(updates) - def update_current_board(self, row, col, color, author): return self.update_current_board_by_list([{"row": row, "col": col, "color": color, "author": author}]) @@ -130,13 +133,15 @@ def generate_gif(self): # Get the list of updates from the database updates = self.updates.find({}).sort("time") for update in updates: - pixels[update["row"], update["col"]] = self.__get_rgb_color(update["color"]) + pixels[update["row"], update["col"] + ] = self.__get_rgb_color(update["color"]) frame = Image.fromarray(pixels) frames.append(frame) # Now create the GIF and save it to a temp file, returning the path temp_path = path.join(TEMP_DIR, "timelapse.gif") - im.save(temp_path, save_all=True, append_images=frames, duration=10, loop=0) + im.save(temp_path, save_all=True, + append_images=frames, duration=10, loop=0) return temp_path def __get_rgb_color(self, index): @@ -145,3 +150,7 @@ def __get_rgb_color(self, index): g = int(hex_color[3:5], 16) b = int(hex_color[5:7], 16) return (r, g, b) + + def change_pixel_rate(new_rate: int): + global PIXEL_RATE + PIXEL_RATE = new_rate diff --git a/static/css.css b/static/css.css index 279412b..a1712fa 100644 --- a/static/css.css +++ b/static/css.css @@ -103,14 +103,21 @@ nav a { } #selector { - display: flex; + display: inline-flex; flex-direction: row; justify-content: left; + margin: 0px auto; + gap: 4px; + flex-wrap: wrap; + position: relative; + width: 320px; +} + +#alert_placeholder { margin: 0px auto; gap: 4px; flex-wrap: wrap; position: relative; align-items: center; - width: auto; width: 320px; } \ No newline at end of file diff --git a/static/js/pixelboard.js b/static/js/pixelboard.js index ec073f9..826461f 100644 --- a/static/js/pixelboard.js +++ b/static/js/pixelboard.js @@ -3,8 +3,8 @@ var _canvas = undefined; var _sio = undefined; var _middlewareID = undefined; var _enableToken = undefined; -var _colorChoice = undefined; -var _previousChoice = undefined; +var _colorChoice = 0; +var _previousChoice = 0; // Fetch the settings: fetch("/settings") @@ -22,9 +22,6 @@ let initBoard = function() { _canvas.id = "canvas" _canvas.getContext("2d").scale(3, 3); - initalizeSecret(); - initalizeSelector(); - document.getElementById("pixelboard").appendChild(_canvas); // Load the current board edits onto this instance of the canvas: @@ -61,15 +58,14 @@ let initBoard = function() { ctx.fillRect(x, y, 1, 1); }) - // hoover + // Hover: const canvas = document.getElementById('canvas') function getCursorPosition(canvas, event) { var elemLeft = canvas.offsetLeft + canvas.clientLeft; var elemTop = canvas.offsetTop + canvas.clientTop; x = parseInt((event.pageX - elemLeft) / 3); y = parseInt((event.pageY - elemTop) / 3); - console.log("x: " + x + " y: " + y) - return [x,y] + return [x, y]; } canvas.addEventListener('mousemove', function(e) { e.preventDefault(); @@ -93,27 +89,32 @@ let initBoard = function() { }) canvas.addEventListener('mouseout', (event) => { const hidden = document.getElementById('mouse_over'); - function delay(time) { - return new Promise(resolve => setTimeout(resolve, time)); - } - delay(1000).then(() => {hidden.style.visibility = 'hidden';}); + hidden.style.visibility = 'hidden'; }); }) }; -let initalizeSelector = function() { +let initializeColorSelector = function() { // Initialize the color selector - var colorSelect = document.getElementById("selector") + var colorSelect = document.getElementById("colorSelector"); + for(var i = 0; i < _settings.palette.length; i++) { var option = document.createElement("div") option.style.backgroundColor = _settings.palette[i] option.setAttribute('value', i) option.style.height = '20px' option.style.width = '42px' + option.style.marginLeft = '2px' + option.style.marginRight = '2px' option.style.display = 'inline-block' + if (i == _colorChoice) { + option.style.outline = "solid blue 3px"; + _previousChoice = option; + } + option.addEventListener('click', function(event) { if(_previousChoice !== undefined) { - _previousChoice.style.outline = '' + _previousChoice.style.outline = ''; } _colorChoice = event.target.getAttribute("value") @@ -122,43 +123,68 @@ let initalizeSelector = function() { }) colorSelect.append(option) } + + colorSelect.style.display = "inline-block"; } -let initalizeSecret = function() { - _enableToken = document.getElementById("enable") - _enableToken.addEventListener('click', function(event) { - let secret = document.getElementById("secretTextBox").value; - - fetch("/register-pg", { - method: "PUT", - body: JSON.stringify({ - "name": "Frontend", - "author": "N/A", - "secret": secret - }), - headers: { 'Content-Type': 'application/json' } - }) - .then((response) => response.json()) - .then((json) => _middlewareID = json["id"]) - .catch((err) => console.log(err)); - }); - - _canvas.addEventListener('click', function(event) { - if(_middlewareID === undefined || _colorChoice === undefined) { - return; +let initializeSecret = function() { + _canvas.addEventListener('click', canvasListener, false); +} + +let canvasListener = function(event) { + var elem = document.getElementById('canvas'), + elemLeft = elem.offsetLeft + elem.clientLeft, + elemTop = elem.offsetTop + elem.clientTop, + col = parseInt((event.pageX - elemLeft) / 3), + row = parseInt((event.pageY - elemTop) / 3); + fetch(`/update-pixel`, { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ "id": _middlewareID, "row": row, "col": col, "color": _colorChoice}) + }) + .then((response) => { + if(response.status === 429) { + alert("Too many requests!", "danger") } - var elem = document.getElementById('canvas'), - elemLeft = elem.offsetLeft + elem.clientLeft, - elemTop = elem.offsetTop + elem.clientTop, - col = parseInt((event.pageX - elemLeft) / 3), - row = parseInt((event.pageY - elemTop) / 3); - fetch(`/changeByClick`, { - method: 'POST', - headers: { - 'Accept': 'application/json', - 'Content-Type': 'application/json' - }, - body: JSON.stringify({ "id": _middlewareID, "row": row, "col": col, "color": _colorChoice}) - }) - }, false); + }) + .catch((err) => console.log(err)); +}; + +let enableFrontend = function(event) { + let secret = document.getElementById("pg_secret").value; + fetch("/register-pg", { + method: "PUT", + body: JSON.stringify({ + "name": "Frontend", + "author": "N/A", + "secret": secret + }), + headers: { 'Content-Type': 'application/json' } + }) + .then((response) => { + if(response.status != "200") { + document.getElementById("enableFrontendEditModal_error").innerHTML = ` + + `; + } else { + return response.json(); + } + }) + .then((json) => { + _middlewareID = json["id"]; + console.log(`Frontend Enabled (Id=${_middlewareID})`) + + // Remove "Enable" button: + document.getElementById("enableFrontendEditButton").remove(); + initializeColorSelector(); + initializeSecret(); + + // Close Modal: + let modal = document.getElementById("enableFrontendEditModal"); + bootstrap.Modal.getInstance(modal).hide(); + }) + .catch((err) => console.log(err)); } \ No newline at end of file diff --git a/templates/index.html b/templates/index.html index 21e4151..fbaba13 100644 --- a/templates/index.html +++ b/templates/index.html @@ -9,6 +9,7 @@ + @@ -39,20 +40,47 @@ -
-
-
- -
- -
- -
- -
-
+
+ + + +
+ + + + + + +