import argparse import json import logging import os import tornado.ioloop import tornado.web import tornado.websocket from urllib.parse import urlparse import uuid import coloredlogs import glob logger = logging.getLogger("drawing") argParser = argparse.ArgumentParser(description='Start up the server to have non-Mechanical Turks draw their sketches.') argParser.add_argument( '--port', '-p', default=8888, help='The port for the server to listen' ) argParser.add_argument( '--verbose', '-v', action="store_true", ) generated_image_dir = os.path.join('www','generated') def strokes2D(strokes): # strokes to a d attribute for a path d = ""; last_stroke = None; cmd = ""; for stroke in strokes: if not last_stroke: d += f"M{stroke[0]},{stroke[1]} " cmd = 'M' else: if last_stroke[2] == 1: d += " m" cmd = 'm' elif cmd != 'l': d+=' l ' cmd = 'l' rel_stroke = [stroke[0] - last_stroke[0], stroke[1] - last_stroke[1]]; d += f"{rel_stroke[0]},{rel_stroke[1]} " last_stroke = stroke; return d; class StaticFileWithHeaderHandler(tornado.web.StaticFileHandler): def set_extra_headers(self, path): """For subclass to add extra headers to the response""" if path[-5:] == '.html': self.set_header("Access-Control-Allow-Origin", "*") if path[-4:] == '.svg': self.set_header("Content-Type", "image/svg+xml") class WebSocketHandler(tornado.websocket.WebSocketHandler): CORS_ORIGINS = ['localhost', '.mturk.com'] connections = set() def check_origin(self, origin): parsed_origin = urlparse(origin) # parsed_origin.netloc.lower() gives localhost:3333 valid = any([parsed_origin.hostname.endswith(origin) for origin in self.CORS_ORIGINS]) return valid # the client connected def open(self, p = None): self.__class__.connections.add(self) logger.info("New client connected") self.strokes = [] # self.write_message("hello!") # the client sent the message def on_message(self, message): logger.debug(f"recieve: {message}") try: msg = json.loads(message) # TODO: sanitize input: min/max, limit strokes if msg['action'] == 'move': # TODO: min/max input point = [float(msg['direction'][0]),float(msg['direction'][1]), 0] self.strokes.append(point) elif msg['action'] == 'up': logger.info(f'up: {msg}') point = [msg['direction'][0],msg['direction'][1], 1] self.strokes.append(point) elif msg['action'] == 'submit': logger.info(f'up: {msg}') id = self.submit_strokes() if not id: self.write_message(json.dumps('error')) return self.write_message(json.dumps({ 'action': 'submitted', 'msg': f"Submission ok, please refer to your submission as: {id}" })) elif msg['action'] == 'down': # not used, implicit in move? pass else: # self.send({'alert': 'Unknown request: {}'.format(message)}) logger.warn('Unknown request: {}'.format(message)) except Exception as e: # self.send({'alert': 'Invalid request: {}'.format(e)}) logger.exception(e) # client disconnected def on_close(self): self.__class__.rmConnection(self) logger.info("Client disconnected") def submit_strokes(self): if len(self.strokes) < 1: return False d = strokes2D(self.strokes) svg = f""" """ id = uuid.uuid4().hex filename = os.path.join(generated_image_dir , id+'.svg') with open(filename, 'w') as fp: logger.info(f"Wrote {filename}") fp.write(svg) return id @classmethod def rmConnection(cls, client): if client not in cls.connections: return cls.connections.remove(client) class LatestImageHandler(tornado.web.RequestHandler): def get(self): self.set_header('Cache-Control', 'no-store, no-cache, must-revalidate, max-age=0') self.set_header("Content-Type", "image/svg+xml") list_of_files = glob.glob(os.path.join(generated_image_dir,'*.svg')) latest_file = max(list_of_files, key=os.path.getctime) with open(latest_file, 'r') as fp: self.write(fp.read()) if __name__ == "__main__": args = argParser.parse_args() coloredlogs.install( level=logging.DEBUG if args.verbose else logging.INFO, ) logger.addHandler( logging.handlers.RotatingFileHandler( 'mt_server.log', maxBytes=1024*512, backupCount=5 ) ) application = tornado.web.Application([ (r"/ws(.*)", WebSocketHandler), (r"/latest.svg", LatestImageHandler), # TODO: have js request the right image, based on a 'start' button. This way we can trace the history of a drawing (r"/(.*)", StaticFileWithHeaderHandler, {"path": 'www', "default_filename": 'index.html'}), ], debug=True) application.listen(args.port) tornado.ioloop.IOLoop.current().start()