Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

UI enhance and dynamic pixel-rate #14

Merged
merged 6 commits into from
Dec 6, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 17 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

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

Expand Down
32 changes: 25 additions & 7 deletions app.py
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand Down Expand Up @@ -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,
Expand All @@ -134,7 +134,8 @@ def PUT_update_pixel():
})

return jsonify({
"success": True
"success": True,
"rate": board_manager.get_pixel_rate()
}), 200


Expand Down Expand Up @@ -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
Expand All @@ -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)
25 changes: 17 additions & 8 deletions boards.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,20 +18,25 @@
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"
makedirs(TEMP_DIR, exist_ok=True)

PIXEL_RATE = int(getenv("PIXEL_RATE") or random.randint(100, 1000))


class BoardManager:
def __init__(self, db: Database):
self.board = db["boards"]
Expand Down Expand Up @@ -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"]}}
Expand All @@ -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}])

Expand Down Expand Up @@ -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):
Expand All @@ -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
11 changes: 9 additions & 2 deletions static/css.css
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
128 changes: 77 additions & 51 deletions static/js/pixelboard.js
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand All @@ -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:
Expand Down Expand Up @@ -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();
Expand All @@ -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")
Expand All @@ -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 = `
<div class="alert alert-danger alert-dismissible fade show" role="alert">
<div>Invalid Secret</div>
<button type="button" class="btn-close" data-dismiss="alert" aria-label="Close" ></button>
</div>
`;
} 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));
}
Loading