-
Notifications
You must be signed in to change notification settings - Fork 21
/
app.py
380 lines (282 loc) · 10.6 KB
/
app.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
from dotenv import load_dotenv
load_dotenv()
from os import environ
from flask import Flask, jsonify, redirect, render_template, request
from maze.maze import Maze
from servers import ServerManager
import json
import time
import requests
from datetime import datetime, timedelta
from global_maze import GlobalMaze
import uuid
FREE_SPACE_RADIUS = 10
ALLOW_DELETE_MAZE = True
DISABLE_INFINITE_MAZE = False
STATUS_OK = 0
STATUS_BAD = 1
app = Flask(__name__)
server_manager = ServerManager('cs240-infinite-maze')
crumbs = {}
cache = {}
'''`{ (<mg_url>, <author>): (<expiry_datetime>, <data>) }`'''
maze_state = GlobalMaze()
MAZE_ERR_503 = ["9a8088c","5b02024","5b49494","0a02020","5b4d1e5","1e571e5","3a282a6"]
DEFAULT_MG_1 = ["9aa2aac", "59aaaa4", "51aa8c5",
"459a651", "553ac55", "559a655", "3638a26"]
DEFAULT_MG_2 = ["988088c", "1000004", "1000004",
"0000000", "1000004", "1000004", "3220226"]
user_color_choice = {}
def get_user_color(user):
if user in user_color_choice:
return user_color_choice[user]
else:
return "000000"
@app.route('/', methods=["GET"])
def GET_index():
'''Route for "/" (frontend)'''
if DISABLE_INFINITE_MAZE:
return render_template("maze-disabled.html")
else:
return render_template("index.html")
@app.route('/one/<mid>/', methods=["GET"])
def GET_one_maze(mid):
'''Route for "/" (frontend)'''
server = server_manager.find_by_id(mid)
return render_template("one.html", mid=mid, server=server)
@app.route('/addUserColor/<user>/<color>', methods=["POST"])
def add_user_color(user, color):
user_color_choice[user] = color
return f"Added user color: {color}!", 200
@app.route('/<user>/generateSegment', methods=["GET"])
def gen_rand_maze_segment(user):
'''Route for maze generation with random generator'''
if DISABLE_INFINITE_MAZE:
return "The infinite maze is disabled until final exam session -- see you soon!", 503
# get row and col
row = 0
col = 0
if 'row' in request.args.keys():
row = int(request.args['row'])
if 'col' in request.args.keys():
col = int(request.args['col'])
old_segment = maze_state.get_state(row, col)
if old_segment != None: # segment already exists in maze state
tmp = old_segment[0]
tmp["color"] = old_segment[1]
return jsonify(tmp), 200
# scan free space
free_space = []
for coords in maze_state.get_free_space(row, col, FREE_SPACE_RADIUS):
free_space.append(coords[0])
free_space.append(coords[1])
# generate segment:
status = 0
while status // 100 != 2:
# If no MGs online, send the default one only
if not server_manager.has_servers():
print('No maze generators available')
return jsonify({"geom": MAZE_ERR_503}), 200
# return jsonify({"geom": DEFAULT_MG_1 if random.random() < 0.5 else DEFAULT_MG_2}), 200
mg_name = server_manager.select_random()
mid = server_manager.get_mid_from_name(mg_name)
print("MG Selected: " + mg_name)
output, status = gen_maze_segment(mid, data={'main': [row, col], 'free': free_space})
data = json.loads(output.data)
# intercept 'extern' key
if 'extern' in data.keys():
for key, val in data['extern'].items():
# add external segments to maze_state
r, c = [int(x) for x in key.split('_')]
if maze_state.get_state(r, c) == None:
maze_state.set_state(r, c, val, get_user_color(user), user)
# hide external segments from front-end
del data['extern']
maze_state.set_state(row, col, data, get_user_color(user), user)
server = server_manager.find(mg_name)
prevCount = int(server['count']) if 'count' in server else 0
server_manager.update(mg_name, {"count": prevCount + 1})
return jsonify(data), 200
@app.route('/generateSegment/<mid>', methods=['GET'])
def gen_maze_segment(mid: str, data=None):
'''Route for maze generation with specific generator'''
server = server_manager.find_by_id(mid)
if not server:
return 'Server not found', 404
mg_url = server['url']
if mg_url[-1] == '/': # handle trailing slash
mg_url = mg_url[:-1]
try:
r = requests.get(f'{mg_url}/generate',
params=dict(request.args), json=data, timeout=1)
except requests.exceptions.Timeout:
message = 'Error: Timeout Error'
server_manager.update(
mg_name, {"status": STATUS_BAD, "message": message})
return message, 500
except requests.exceptions.TooManyRedirects:
message = 'Error: Too Many Redirects'
server_manager.update(
mg_name, {"status": STATUS_BAD, "message": message})
return message, 500
except requests.exceptions.RequestException as e:
message = 'Error: Request Exception'
server_manager.update(
mg_name, {"status": STATUS_BAD, "message": message})
return message, 500
# if not a 200-level response
if r.status_code // 100 != 2:
message = f'Error: {r.status_code}'
server_manager.update(
mg_name, {"status": STATUS_BAD, "message": message})
return message, 500
data = r.json()
geom = data['geom']
try:
maze = Maze.decode(geom)
if maze and maze.width % 7 != 0 or maze.height % 7 != 0:
message = 'Maze has invalid dimensions'
server_manager.update(
mg_name, {"status": STATUS_BAD, "message": message})
return message, 500
# maze validation
# force boundaries if single-unit segment
if 'extern' not in data.keys():
maze = maze.add_boundary()
geom = maze.encode()
print(f'GEOM: {geom}')
data['geom'] = geom
except:
message = 'Failed to decode maze'
server_manager.update(
mg_name, {"status": STATUS_BAD, "message": message})
return message, 500
return jsonify(data), 200
@app.route('/addMG', methods=['PUT'])
def add_maze_generator():
'''Route to add a maze generator'''
data = request.json
if not data:
return 'Data is missing', 400
if 'name' not in data:
return 'MG name is missing', 400
mg_name = data['name']
if server_manager.find(mg_name) is not None:
# update
if not ALLOW_DELETE_MAZE:
return "The current server settings does not allow MGs to be modified.", 401
# assume MG is good
data['status'] = STATUS_OK
data['message'] = ''
status, message = server_manager.update(mg_name, data)
print(server_manager.servers)
mg = server_manager.find(mg_name)
print(mg)
return jsonify({"message": message, mg_name: server_manager.find(mg_name)}), status
# Validate packet:
for requiredKey in ['name', 'url', 'author']:
if requiredKey not in request.json.keys():
return f'Key "{requiredKey}" missing', 400
if 'weight' in request.json.keys():
new_weight = request.json['weight']
if new_weight <= 0:
return 'Weight cannot be 0 or negative', 400
else:
new_weight = 1
new_weight = 1
server = {
'name': request.json['name'],
'url': request.json['url'],
'author': request.json['author'],
'weight': new_weight,
'status': STATUS_OK,
'message': '',
'count': 0
}
# TODO: Test MG before adding
status, error_message = server_manager.insert(server)
print(server_manager.servers)
if status // 100 != 2:
return jsonify({"error": error_message}), status
server = server_manager.find(server['name'])
return jsonify(server), status
@app.route('/servers', methods=['GET'])
def FindServers():
serverList = server_manager.servers.values()
serverList = sorted(serverList, key = lambda e: e['author'])
return render_template('servers.html', data={"servers": serverList})
@app.route('/listMG', methods=['GET'])
def list_maze_generators():
'''Route to get list of maze generators'''
servers = server_manager.servers
return jsonify(servers), 200
@app.route('/mazeState', methods=['GET'])
def dump_maze_state():
'''Dump global maze state internal JSON.'''
return jsonify(maze_state.get_full_state()), 200
@app.route('/resetMaze', methods=['GET'])
def reset_maze_state():
'''Reset global maze state.'''
if not ALLOW_DELETE_MAZE:
return "The current server settings does not allow the maze to be reset.", 401
global maze_state
if not maze_state.is_empty():
maze_state.reset()
return 'OK', 200
@app.route('/removeMG/<mg_name>', methods=['DELETE'])
def RemoveMG(mg_name):
if not ALLOW_DELETE_MAZE:
return "The current server settings does not allow MGs to be removed.", 401
status, message = server_manager.remove(mg_name)
return message, status
@app.route('/updateMG/<mg_name>', methods=['PUT'])
def UpdateMG(mg_name):
if not ALLOW_DELETE_MAZE:
return "The current server settings does not allow MGs to be modified.", 401
data = request.get_json()
if not data:
return "data is missing", 400
status, message = server_manager.update(mg_name, data)
return message, status
@app.route('/log', methods=['GET'])
def logData():
return jsonify({
"names": server_manager.names,
"weights": server_manager.weights,
"servers": server_manager.servers
}), 200
@app.route('/heartbeat', methods=['PATCH'])
def heartbeat():
'''Route for exchanging player location information'''
# get POST data
data = request.json
# remove user id from data
u = data["user"]
# get current time
now = int(time.time())
# add a timestamp to the heartbeat data
data["time"] = now
# replace the user's entry in the breadcrumbs dict
crumbs[u] = data
# remove players that haven't sent a heartbeat in at least
# 10 seconds
for k in list(crumbs.keys()):
if k[0] == "_": continue
age = now - crumbs[k]["time"]
if age > 10:
try:
del crumbs[k]
except KeyError:
continue
crumbs["_totalBlocks"] = maze_state.get_size()
crumbs["_userBlocks"] = maze_state.get_segments_by_uid(u)
# return location information for all players
return jsonify(crumbs), 200
@app.route('/enableMaze', methods=['GET'])
def enable_maze():
global DISABLE_INFINITE_MAZE
if environ["ENABLE_MAZE"] == request.args["q"]:
DISABLE_INFINITE_MAZE = False
return '', 200
else:
return 'Unauthorized', 401