def submit_puzzle(pieces, bg_color, user, permission, description, link, upload_file): """ Submit a puzzle to be reviewed. Generates the puzzle_id and original.jpg. """ unsplash_image_thread = None puzzle_id = None cur = db.cursor() unsplash_match = re.search( r"^(http://|https://)?unsplash.com/photos/([^/]+)", link) if link and unsplash_match: if not current_app.config.get("UNSPLASH_APPLICATION_ID"): cur.close() abort(400) d = time.strftime("%Y_%m_%d.%H_%M_%S", time.localtime()) filename = unsplash_match.group(2) u_id = "%s" % (hashlib.sha224(bytes("%s%s" % (filename, d), "utf-8")).hexdigest()[0:9]) puzzle_id = "unsplash{filename}-mxyz-{u_id}".format(filename=filename, u_id=u_id) # Create puzzle dir puzzle_dir = os.path.join(current_app.config.get("PUZZLE_RESOURCES"), puzzle_id) os.mkdir(puzzle_dir) # Download the unsplash image unsplash_image_thread = UnsplashPuzzleThread( puzzle_id, filename, description, current_app.config_file, current_app.config.get("SECURE_COOKIE_SECRET"), ) else: if not upload_file: cur.close() abort(400) filename = secure_filename(upload_file.filename) filename = filename.lower() # Check the filename to see if the extension is allowed if os.path.splitext(filename)[1][1:].lower() not in ALLOWED_EXTENSIONS: cur.close() abort(400) puzzle_id = generate_new_puzzle_id(filename) # Create puzzle dir puzzle_dir = os.path.join(current_app.config.get("PUZZLE_RESOURCES"), puzzle_id) os.mkdir(puzzle_dir) # Convert the uploaded file to jpg upload_file_path = os.path.join(puzzle_dir, filename) upload_file.save(upload_file_path) # verify the image file format identify_format = subprocess.check_output( ["identify", "-format", "%m", upload_file_path], encoding="utf-8") identify_format = identify_format.lower() if identify_format not in ALLOWED_EXTENSIONS: os.unlink(upload_file_path) os.rmdir(puzzle_dir) cur.close() abort(400) # Abort if imagemagick fails converting the image to jpg try: subprocess.check_call([ "convert", upload_file_path, "-quality", "85%", "-format", "jpg", os.path.join(puzzle_dir, "original.jpg"), ]) except subprocess.CalledProcessError: os.unlink(upload_file_path) os.rmdir(puzzle_dir) cur.close() abort(400) os.unlink(upload_file_path) # The preview_full image is only created in the pieceRender process. d = { "puzzle_id": puzzle_id, "pieces": pieces, "name": filename, "link": link, "description": description, "bg_color": bg_color, "owner": user, "queue": QUEUE_NEW, "status": NEEDS_MODERATION, "permission": permission, } cur.execute( fetch_query_string("insert_puzzle.sql"), d, ) db.commit() puzzle = rowify( cur.execute( fetch_query_string("select_puzzle_id_by_puzzle_id.sql"), { "puzzle_id": puzzle_id }, ).fetchall(), cur.description, )[0][0] puzzle = puzzle["puzzle"] cur.execute( fetch_query_string("add-puzzle-file.sql"), { "puzzle": puzzle, "name": "original", "url": "/resources/{0}/original.jpg".format( puzzle_id), # Not a public file (only on admin page) }, ) cur.execute( fetch_query_string("add-puzzle-file.sql"), { "puzzle": puzzle, "name": "preview_full", "url": "/resources/{0}/preview_full.jpg".format(puzzle_id), }, ) classic_variant = cur.execute( fetch_query_string("select-puzzle-variant-id-for-slug.sql"), { "slug": CLASSIC }).fetchone()[0] cur.execute( fetch_query_string("insert-puzzle-instance.sql"), { "original": puzzle, "instance": puzzle, "variant": classic_variant }, ) db.commit() cur.close() if unsplash_image_thread: # Go download the unsplash image and update the db unsplash_image_thread.start() return puzzle_id
def setUp(self): super().setUp() with self.app.app_context(): cur = self.db.cursor() # TODO: create players # Create fake source puzzle that will be forked ( fake_source_puzzle_data, fake_source_piece_properties, ) = self.fabricate_fake_puzzle() self.source_puzzle_id = fake_source_puzzle_data.get("puzzle_id") result = cur.execute( fetch_query_string( "select-valid-puzzle-for-new-puzzle-instance-fork.sql"), { "puzzle_id": self.source_puzzle_id, "ACTIVE": ACTIVE, "IN_QUEUE": IN_QUEUE, "COMPLETED": COMPLETED, "FROZEN": FROZEN, }, ).fetchall() if not result: raise Exception("fake puzzle failed") (result, col_names) = rowify(result, cur.description) self.source_puzzle_data = result[0] self.puzzle_id = generate_new_puzzle_id("fork-puzzle") d = { "puzzle_id": self.puzzle_id, "pieces": self.source_puzzle_data["pieces"], "name": self.source_puzzle_data["name"], "link": self.source_puzzle_data["link"], "description": "forky", "bg_color": "#f041EE", "owner": 3, "queue": 1, "status": MAINTENANCE, "permission": PRIVATE, } cur.execute( fetch_query_string("insert_puzzle.sql"), d, ) db.commit() result = cur.execute( fetch_query_string("select-all-from-puzzle-by-puzzle_id.sql"), { "puzzle_id": self.puzzle_id }, ).fetchall() (result, col_names) = rowify(result, cur.description) self.puzzle_data = result[0] puzzle = self.puzzle_data["id"] classic_variant = cur.execute( fetch_query_string("select-puzzle-variant-id-for-slug.sql"), { "slug": CLASSIC }, ).fetchone()[0] cur.execute( fetch_query_string("insert-puzzle-instance.sql"), { "original": self.source_puzzle_data["id"], "instance": puzzle, "variant": classic_variant, }, ) cur.execute( fetch_query_string("fill-user-puzzle-slot.sql"), { "player": 3, "puzzle": puzzle }, ) self.db.commit()
def submit_puzzle( pieces, bg_color, user, permission, description, link, upload_file, secret_message="", features=set(), ): """ Submit a puzzle to be reviewed. Generates the puzzle_id and original.jpg. """ puzzle_id = None filename = "" original_slip = uuid.uuid4().hex[:10] cur = db.cursor() unsplash_match = re.search(unsplash_url_regex, link) is_unsplash_link = True if link and unsplash_match else False if is_unsplash_link: if not current_app.config.get("UNSPLASH_APPLICATION_ID"): cur.close() abort(400) filename = unsplash_match.group(2) u_id = uuid.uuid4().hex[:20] puzzle_id = f"unsplash-mxyz-{u_id}" else: if not upload_file: cur.close() abort(400) filename = secure_filename(upload_file.filename) filename = filename.lower() # Check the filename to see if the extension is allowed if os.path.splitext(filename)[1][1:].lower() not in ALLOWED_EXTENSIONS: cur.close() abort(400) puzzle_id = generate_new_puzzle_id(filename) CDN_BASE_URL = current_app.config["CDN_BASE_URL"] prefix_resources_url = ("" if current_app.config["LOCAL_PUZZLE_RESOURCES"] else CDN_BASE_URL) pr = PuzzleResource(puzzle_id, current_app.config, is_local_resource=not bool(prefix_resources_url)) tmp_dir = tempfile.mkdtemp() tmp_puzzle_dir = os.path.join(tmp_dir, puzzle_id) os.mkdir(tmp_puzzle_dir) if not is_unsplash_link: # Convert the uploaded file to jpg with tempfile.NamedTemporaryFile() as temp_upload_file: upload_file.save(temp_upload_file) # verify the image file format identify_format = subprocess.check_output( ["identify", "-format", "%m", temp_upload_file.name], encoding="utf-8") identify_format = identify_format.lower() if identify_format not in ALLOWED_EXTENSIONS: pr.delete() cur.close() abort(400) # Abort if imagemagick fails converting the image to jpg try: subprocess.check_call([ "convert", temp_upload_file.name, "-quality", "85%", "-format", "jpg", os.path.join(tmp_puzzle_dir, f"original.{original_slip}.jpg"), ]) except subprocess.CalledProcessError: pr.delete() cur.close() abort(400) d = { "puzzle_id": puzzle_id, "pieces": pieces, "name": filename, "link": link, "description": description, "bg_color": bg_color, "owner": user, "queue": QUEUE_NEW, "status": NEEDS_MODERATION if not current_app.config["AUTO_APPROVE_PUZZLES"] else IN_RENDER_QUEUE, "permission": permission, } cur.execute( fetch_query_string("insert_puzzle.sql"), d, ) db.commit() puzzle = rowify( cur.execute( fetch_query_string("select_puzzle_id_by_puzzle_id.sql"), { "puzzle_id": puzzle_id }, ).fetchall(), cur.description, )[0][0] puzzle = puzzle["puzzle"] cur.execute( fetch_query_string("add-puzzle-file.sql"), { "puzzle": puzzle, "name": "original", "url": f"{prefix_resources_url}/resources/{puzzle_id}/original.{original_slip}.jpg", }, ) if not is_unsplash_link: slip = uuid.uuid4().hex[:4] preview_full_slip = f"preview_full.{slip}.jpg" cur.execute( fetch_query_string("add-puzzle-file.sql"), { "puzzle": puzzle, "name": "preview_full", "url": f"{prefix_resources_url}/resources/{puzzle_id}/{preview_full_slip}", }, ) im = Image.open( os.path.join(tmp_puzzle_dir, f"original.{original_slip}.jpg")).copy() im.thumbnail(size=(384, 384)) im.save(os.path.join(tmp_puzzle_dir, preview_full_slip)) im.close() classic_variant = cur.execute( fetch_query_string("select-puzzle-variant-id-for-slug.sql"), { "slug": CLASSIC }).fetchone()[0] cur.execute( fetch_query_string("insert-puzzle-instance.sql"), { "original": puzzle, "instance": puzzle, "variant": classic_variant }, ) result = cur.execute( fetch_query_string("select-puzzle-features-enabled.sql"), { "enabled": 1 }).fetchall() if result: (puzzle_features, _) = rowify(result, cur.description) # Add puzzle features for puzzle_feature in puzzle_features: if (puzzle_feature["slug"] == "hidden-preview" and "hidden-preview" in features): cur.execute( fetch_query_string( "add-puzzle-feature-to-puzzle-by-id--hidden-preview.sql" ), { "puzzle": puzzle, "puzzle_feature": puzzle_feature["id"] }, ) elif (puzzle_feature["slug"] == "secret-message" and "secret-message" in features): cur.execute( fetch_query_string( "add-puzzle-feature-to-puzzle-by-id--secret-message.sql" ), { "puzzle": puzzle, "puzzle_feature": puzzle_feature["id"], "message": secret_message, }, ) db.commit() cur.close() pr.put(tmp_puzzle_dir) rmtree(tmp_dir) if is_unsplash_link: original_filename = f"original.{original_slip}.jpg" # Go download the unsplash image and update the db job = current_app.unsplashqueue.enqueue( "api.jobs.unsplash_image.add_photo_to_puzzle", puzzle_id, filename, description, original_filename, result_ttl=0, job_timeout="24h", ) return puzzle_id
def post(self): args = {} if request.form: args.update(request.form.to_dict(flat=True)) # Check pieces arg try: pieces = int(args.get("pieces", current_app.config["MINIMUM_PIECE_COUNT"])) except ValueError as err: abort(400) if pieces < current_app.config["MINIMUM_PIECE_COUNT"]: abort(400) bg_color = check_bg_color(args.get("bg_color", "#808080")[:50]) # Check description instance_description = args.get("instance_description", "") # Check puzzle_id source_puzzle_id = args.get("puzzle_id") if not source_puzzle_id: abort(400) # Check fork fork = int(args.get("fork", "0")) if fork not in (0, 1): abort(400) fork = bool(fork == 1) # Check permission permission = int(args.get("permission", PUBLIC)) if permission not in (PUBLIC, PRIVATE): abort(400) if fork: # All copies of puzzles are unlisted permission = PRIVATE user = int( current_app.secure_cookie.get(u"user") or user_id_from_ip(request.headers.get("X-Real-IP")) ) cur = db.cursor() # The user should have # 2400 or more dots (points) # TODO: this could be configurable per site or for other reasons. # userHasEnoughPoints = cur.execute(fetch_query_string("select-minimum-points-for-user.sql"), {'user': user, 'points': 2400}).fetchall() # if not userHasEnoughPoints: # abort(400) # An available instance slot result = cur.execute( fetch_query_string("select-available-user-puzzle-slot-for-player.sql"), {"player": user}, ).fetchone()[0] userHasAvailablePuzzleInstanceSlot = bool(result) if not userHasAvailablePuzzleInstanceSlot: cur.close() db.commit() abort(400) # Check if puzzle is valid to be a new puzzle instance if not fork: # Creating a new puzzle instance result = cur.execute( fetch_query_string("select-valid-puzzle-for-new-puzzle-instance.sql"), { "puzzle_id": source_puzzle_id, "ACTIVE": ACTIVE, "IN_QUEUE": IN_QUEUE, "COMPLETED": COMPLETED, "FROZEN": FROZEN, "REBUILD": REBUILD, "IN_RENDER_QUEUE": IN_RENDER_QUEUE, "RENDERING": RENDERING, }, ).fetchall() if not result: # Puzzle does not exist or is not a valid puzzle to create instance from. cur.close() db.commit() abort(400) else: # Creating a copy of existing puzzle pieces (forking) result = cur.execute( fetch_query_string( "select-valid-puzzle-for-new-puzzle-instance-fork.sql" ), { "puzzle_id": source_puzzle_id, "ACTIVE": ACTIVE, "IN_QUEUE": IN_QUEUE, "COMPLETED": COMPLETED, "FROZEN": FROZEN, }, ).fetchall() if not result: # Puzzle does not exist or is not a valid puzzle to create instance from. cur.close() db.commit() abort(400) (result, col_names) = rowify(result, cur.description) source_puzzle_data = result[0] puzzle_id = generate_new_puzzle_id(source_puzzle_data["name"]) # Create puzzle dir if not fork: puzzle_dir = os.path.join( current_app.config.get("PUZZLE_RESOURCES"), puzzle_id ) os.mkdir(puzzle_dir) if not fork: d = { "puzzle_id": puzzle_id, "pieces": pieces, "name": source_puzzle_data["name"], "link": source_puzzle_data["link"], "description": source_puzzle_data["description"] if not instance_description else instance_description, "bg_color": bg_color, "owner": user, "queue": QUEUE_NEW, "status": IN_RENDER_QUEUE, "permission": permission, } cur.execute( fetch_query_string("insert_puzzle.sql"), d, ) else: d = { "puzzle_id": puzzle_id, "pieces": source_puzzle_data["pieces"], "rows": source_puzzle_data["rows"], "cols": source_puzzle_data["cols"], "piece_width": source_puzzle_data["piece_width"], "mask_width": source_puzzle_data["mask_width"], "table_width": source_puzzle_data["table_width"], "table_height": source_puzzle_data["table_height"], "name": source_puzzle_data["name"], "link": source_puzzle_data["link"], "description": source_puzzle_data["description"] if not instance_description else instance_description, "bg_color": bg_color, "owner": user, "queue": QUEUE_NEW, "status": MAINTENANCE, "permission": permission, # All copies of puzzles are unlisted } cur.execute( fetch_query_string("insert_puzzle_instance_copy.sql"), d, ) db.commit() result = cur.execute( fetch_query_string("select-all-from-puzzle-by-puzzle_id.sql"), {"puzzle_id": puzzle_id}, ).fetchall() if not result: cur.close() db.commit() abort(500) (result, col_names) = rowify(result, cur.description) puzzle_data = result[0] puzzle = puzzle_data["id"] classic_variant = cur.execute( fetch_query_string("select-puzzle-variant-id-for-slug.sql"), {"slug": CLASSIC}, ).fetchone()[0] cur.execute( fetch_query_string("insert-puzzle-instance.sql"), { "original": source_puzzle_data["id"], "instance": puzzle, "variant": classic_variant, }, ) cur.execute( fetch_query_string("fill-user-puzzle-slot.sql"), {"player": user, "puzzle": puzzle}, ) db.commit() cur.close() if not fork: job = current_app.createqueue.enqueue_call( func="api.jobs.pieceRenderer.render", args=([puzzle_data]), result_ttl=0, timeout="24h", ) else: # Copy existing puzzle job = current_app.cleanupqueue.enqueue_call( func="api.jobs.piece_forker.fork_puzzle_pieces", args=([source_puzzle_data, puzzle_data]), result_ttl=0, ) return redirect("/chill/site/front/{0}/".format(puzzle_id), code=303)
def post(self): args = {} if request.form: args.update(request.form.to_dict(flat=True)) args["features"] = set(request.form.getlist("features")) # Check pieces arg try: pieces = int( args.get("pieces", current_app.config["MINIMUM_PIECE_COUNT"])) except ValueError as err: abort(400) if pieces < current_app.config["MINIMUM_PIECE_COUNT"]: abort(400) bg_color = check_bg_color(args.get("bg_color", "#808080")[:50]) # Check description instance_description = escape(args.get("instance_description", ""))[:1000] # Check secret_message secret_message = escape(args.get("secret_message", ""))[:1000] # Check puzzle_id source_puzzle_id = args.get("puzzle_id") if not source_puzzle_id: abort(400) # Check fork fork = int(args.get("fork", "0")) if fork not in (0, 1): abort(400) fork = bool(fork == 1) # Validate permission permission = int(args.get("permission", PUBLIC)) if permission not in (PUBLIC, PRIVATE): abort(400) if fork: # All copies of puzzles are unlisted permission = PRIVATE # Note that the permission value is updated to be unlisted later on if # the source puzzle is unlisted based on source puzzle data. user = int( current_app.secure_cookie.get("user") or user_id_from_ip(request.headers.get("X-Real-IP"))) cur = db.cursor() # The user should have # 2400 or more dots (points) # TODO: this could be configurable per site or for other reasons. # userHasEnoughPoints = cur.execute(fetch_query_string("select-minimum-points-for-user.sql"), {'user': user, 'points': 2400}).fetchall() # if not userHasEnoughPoints: # abort(400) # An available instance slot result = cur.execute( fetch_query_string( "select-available-user-puzzle-slot-for-player.sql"), { "player": user }, ).fetchone()[0] userHasAvailablePuzzleInstanceSlot = bool(result) if not userHasAvailablePuzzleInstanceSlot: cur.close() db.commit() abort(400) # Check if puzzle is valid to be a new puzzle instance if not fork: # Creating a new puzzle instance result = cur.execute( fetch_query_string( "select-valid-puzzle-for-new-puzzle-instance.sql"), { "puzzle_id": source_puzzle_id, "ACTIVE": ACTIVE, "IN_QUEUE": IN_QUEUE, "COMPLETED": COMPLETED, "FROZEN": FROZEN, "REBUILD": REBUILD, "IN_RENDER_QUEUE": IN_RENDER_QUEUE, "RENDERING": RENDERING, }, ).fetchall() if not result: # Puzzle does not exist or is not a valid puzzle to create instance from. cur.close() db.commit() abort(400) else: # Creating a copy of existing puzzle pieces (forking) result = cur.execute( fetch_query_string( "select-valid-puzzle-for-new-puzzle-instance-fork.sql"), { "puzzle_id": source_puzzle_id, "ACTIVE": ACTIVE, "IN_QUEUE": IN_QUEUE, "COMPLETED": COMPLETED, "FROZEN": FROZEN, }, ).fetchall() if not result: # Puzzle does not exist or is not a valid puzzle to create instance from. cur.close() db.commit() abort(400) (result, col_names) = rowify(result, cur.description) source_puzzle_data = result[0] result = cur.execute( fetch_query_string("select-puzzle-features-for-puzzle_id.sql"), { "puzzle_id": source_puzzle_id, "enabled": 1 }, ).fetchall() source_features = set() if result: (result, _) = rowify(result, cur.description) source_features = set(map(lambda x: x["slug"], result)) # Set the permission of new puzzle to be unlisted if source puzzle is # that way. The form should have this field as disabled if the source # puzzle is unlisted; which would mean it isn't sent as a parameter. if source_puzzle_data["permission"] == PRIVATE: permission = PRIVATE puzzle_id = generate_new_puzzle_id(source_puzzle_data["name"]) # Create puzzle dir if not fork: puzzle_dir = os.path.join( current_app.config.get("PUZZLE_RESOURCES"), puzzle_id) os.mkdir(puzzle_dir) if not fork: d = { "puzzle_id": puzzle_id, "pieces": pieces, "name": source_puzzle_data["name"], "link": source_puzzle_data["link"], "description": source_puzzle_data["description"] if not instance_description else instance_description, "bg_color": bg_color, "owner": user, "queue": QUEUE_NEW, "status": IN_RENDER_QUEUE, "permission": permission, } cur.execute( fetch_query_string("insert_puzzle.sql"), d, ) else: d = { "puzzle_id": puzzle_id, "pieces": source_puzzle_data["pieces"], "rows": source_puzzle_data["rows"], "cols": source_puzzle_data["cols"], "piece_width": source_puzzle_data["piece_width"], "mask_width": source_puzzle_data["mask_width"], "table_width": source_puzzle_data["table_width"], "table_height": source_puzzle_data["table_height"], "name": source_puzzle_data["name"], "link": source_puzzle_data["link"], "description": source_puzzle_data["description"] if not instance_description else instance_description, "bg_color": bg_color, "owner": user, "queue": QUEUE_NEW, "status": MAINTENANCE, "permission": permission, # All copies of puzzles are unlisted } cur.execute( fetch_query_string("insert_puzzle_instance_copy.sql"), d, ) db.commit() result = cur.execute( fetch_query_string("select_puzzle_id_by_puzzle_id.sql"), { "puzzle_id": puzzle_id }, ).fetchall() if not result: cur.close() db.commit() current_app.logger.error( f"Failed to get puzzle id from select_puzzle_id_by_puzzle_id.sql using {puzzle_id}" ) abort(500) puzzle = result[0][0] classic_variant = cur.execute( fetch_query_string("select-puzzle-variant-id-for-slug.sql"), { "slug": CLASSIC }, ).fetchone()[0] cur.execute( fetch_query_string("insert-puzzle-instance.sql"), { "original": source_puzzle_data["id"], "instance": puzzle, "variant": classic_variant, }, ) cur.execute( fetch_query_string("fill-user-puzzle-slot.sql"), { "player": user, "puzzle": puzzle }, ) features = args.get("features") result = cur.execute( fetch_query_string("select-puzzle-features-enabled.sql"), { "enabled": 1 }).fetchall() if result: (puzzle_features, _) = rowify(result, cur.description) # Add puzzle features for puzzle_feature in puzzle_features: if puzzle_feature[ "slug"] == "hidden-preview" and "hidden-preview" in features.union( source_features): # If source puzzle had hidden-preview then this puzzle will # also. cur.execute( fetch_query_string( "add-puzzle-feature-to-puzzle-by-id--hidden-preview.sql" ), { "puzzle": puzzle, "puzzle_feature": puzzle_feature["id"] }, ) elif (puzzle_feature["slug"] == "secret-message" and "secret-message" in features): cur.execute( fetch_query_string( "add-puzzle-feature-to-puzzle-by-id--secret-message.sql" ), { "puzzle": puzzle, "puzzle_feature": puzzle_feature["id"], "message": secret_message, }, ) result = cur.execute( fetch_query_string( "select-all-and-preview_full-from-puzzle-by-puzzle_id.sql"), { "puzzle_id": puzzle_id }, ).fetchall() if not result: cur.close() db.commit() current_app.logger.error( f"Failed to get result from select-all-and-preview_full-from-puzzle-by-puzzle_id.sql using {puzzle_id}" ) abort(500) (result, col_names) = rowify(result, cur.description) puzzle_data = result[0] db.commit() cur.close() if not fork: job = current_app.createqueue.enqueue( "api.jobs.pieceRenderer.render", [puzzle_data], result_ttl=0, job_timeout="24h", ) else: # Copy existing puzzle job = current_app.cleanupqueue.enqueue( "api.jobs.piece_forker.fork_puzzle_pieces", source_puzzle_data, puzzle_data, result_ttl=0, ) return redirect("/chill/site/front/{0}/".format(puzzle_id), code=303)