def patch(self, puzzle_id): data = request.get_json(silent=True) response_msg = update_puzzle_details(puzzle_id, data) if ( response_msg.get("rowcount") and response_msg.get("status_code") == 200 and data.get("status") ): purge_route_from_nginx_cache( "/chill/site/front/{puzzle_id}/".format(puzzle_id=puzzle_id), current_app.config.get("PURGEURLLIST"), ) return make_response(json.jsonify(response_msg), response_msg["status_code"])
def post(self): "Route is protected by basic auth in nginx" args = {} if request.form: args.update(request.form.to_dict(flat=True)) # Verify args action = args.get("action") if action not in ACTIONS: abort(400) name_register_ids = request.form.getlist("name_register_id") if len(name_register_ids) == 0: abort(400) if not isinstance(name_register_ids, list): name_register_ids = [name_register_ids] name_register_users = request.form.getlist("name_register_user") if len(name_register_users) == 0: abort(400) if not isinstance(name_register_users, list): name_register_users = [name_register_users] cur = db.cursor() if action == "reject": def each(name_register_ids): for id in name_register_ids: yield {"id": id} cur.executemany( fetch_query_string("reject-name-on-name-register-for-id.sql"), each(name_register_ids), ) db.commit() cur.close() routes_to_purge = [] for user in name_register_users: routes_to_purge.append( "/chill/site/internal/player-bit/{}/".format(user)) purge_route_from_nginx_cache( "\n".join(routes_to_purge), current_app.config.get("PURGEURLLIST"), ) return redirect("/chill/site/admin/name-register-review/")
def post(self): "Route is protected by basic auth in nginx" args = {} if request.form: args.update(request.form.to_dict(flat=True)) # TODO: Check user to see if role matches? # user = current_app.secure_cookie.get(u'user') # if not user: # abort(403) # Verify args action = args.get("action") if action not in ACTIONS: abort(400) reject = args.get("reject") if action == "reject" and reject not in ("license", "attribution"): abort(400) delete = args.get("delete") if action == "delete" and delete not in ("license", "inapt", "old", "request"): abort(400) # abort if tag value not set tag = args.get("tag") if action == "tag" and not tag: abort(400) puzzle_ids = request.form.getlist("montage_puzzle_id") if len(puzzle_ids) == 0 or len(puzzle_ids) > 20: abort(400) if not isinstance(puzzle_ids, list): puzzle_ids = [puzzle_ids] cur = db.cursor() status = None if action == "approve": # TODO: May need to be set to REBUILD if it is an existing puzzle, # otherwise the preview_full.jpg will be recreated. Use new # "rebuild" action instead of just "approve". status = IN_RENDER_QUEUE if action == "reject": if reject == "license": status = FAILED_LICENSE elif reject == "attribution": status = NO_ATTRIBUTION if action == "delete": if delete == "license": status = DELETED_LICENSE elif delete == "inapt": status = DELETED_INAPT elif delete == "old": status = DELETED_OLD elif delete == "request": status = DELETED_REQUEST for puzzle_id in puzzle_ids: delete_puzzle_resources(puzzle_id) id = cur.execute( fetch_query_string("select_puzzle_id_by_puzzle_id.sql"), { "puzzle_id": puzzle_id }, ).fetchone()[0] # current_app.logger.info('deleting puzzle resources for id {}'.format(id)) cur.execute( fetch_query_string("delete_puzzle_file_for_puzzle.sql"), {"puzzle": id}, ) cur.execute(fetch_query_string("delete_piece_for_puzzle.sql"), {"puzzle": id}) cur.execute(fetch_query_string("delete_puzzle_timeline.sql"), {"puzzle": id}) redis_connection.delete("timeline:{puzzle}".format(puzzle=id)) redis_connection.delete("score:{puzzle}".format(puzzle=id)) db.commit() def each(puzzle_ids): for puzzle_id in puzzle_ids: yield {"puzzle_id": puzzle_id, "status": status} cur.executemany( fetch_query_string("update_puzzle_status_for_puzzle_id.sql"), each(puzzle_ids), ) db.commit() for puzzle_id in puzzle_ids: purge_route_from_nginx_cache( "/chill/site/front/{puzzle_id}/".format(puzzle_id=puzzle_id), current_app.config.get("PURGEURLLIST"), ) if action == "approve": puzzles = rowify( cur.execute( fetch_query_string("select-puzzles-in-render-queue.sql"), { "IN_RENDER_QUEUE": IN_RENDER_QUEUE, "REBUILD": REBUILD }, ).fetchall(), cur.description, )[0] print("found {0} puzzles to render".format(len(puzzles))) # push each puzzle to artist job queue for puzzle in puzzles: job = current_app.createqueue.enqueue_call( func="api.jobs.pieceRenderer.render", args=([puzzle]), result_ttl=0, timeout="24h", ) # TODO: if action in ('reject', 'delete'): #Also apply to any puzzle instances cur.close() return make_response("204", 204)
def patch(self, puzzle_id): ip = request.headers.get("X-Real-IP") user = int(current_app.secure_cookie.get("user") or user_id_from_ip(ip)) # validate the args and headers args = {} xhr_data = request.get_json() if xhr_data: args.update(xhr_data) if request.form: args.update(request.form.to_dict(flat=True)) # Verify args action = args.get("action") if action not in INSTANCE_ACTIONS: abort(400) cur = db.cursor() # validate the puzzle_id result = cur.execute( fetch_query_string("select-puzzle-details-for-puzzle_id.sql"), {"puzzle_id": puzzle_id}, ).fetchall() if not result: # 400 if puzzle does not exist err_msg = { "msg": "No puzzle found", } cur.close() return make_response(json.jsonify(err_msg), 400) (result, col_names) = rowify(result, cur.description) puzzleData = result[0] if puzzleData["owner"] != user or puzzleData["is_original"]: cur.close() abort(400) if puzzleData["status"] not in ( FROZEN, ACTIVE, COMPLETED, BUGGY_UNLISTED, RENDERING_FAILED, REBUILD, IN_RENDER_QUEUE, MAINTENANCE, ): cur.close() abort(400) if action in ("freeze", "unfreeze") and puzzleData["status"] not in ( FROZEN, BUGGY_UNLISTED, ACTIVE, ): cur.close() abort(400) response = {} if action == "delete": ( delete_penalty, can_delete, delete_disabled_message, ) = self.get_delete_prereq(puzzleData) if not can_delete: response = {"msg": delete_disabled_message} cur.close() return make_response(json.jsonify(response), 400) if delete_penalty > 0: cur.execute( fetch_query_string("decrease-user-points.sql"), {"user": user, "points": delete_penalty}, ) delete_puzzle_resources(puzzle_id) cur.execute( fetch_query_string("delete_puzzle_file_for_puzzle.sql"), {"puzzle": puzzleData["id"]}, ) cur.execute( fetch_query_string("delete_piece_for_puzzle.sql"), {"puzzle": puzzleData["id"]}, ) msg = delete_puzzle_timeline(puzzle_id) if msg.get("status_code") >= 400: current_app.logger.error(msg.get("msg")) current_app.logger.error( f"Failed delete of puzzle timeline for puzzle_id {puzzle_id}" ) cur.execute( fetch_query_string("update_puzzle_status_for_puzzle.sql"), {"status": DELETED_REQUEST, "puzzle": puzzleData["id"]}, ) cur.execute( fetch_query_string("empty-user-puzzle-slot.sql"), {"player": user, "puzzle": puzzleData["id"]}, ) db.commit() sse.publish( "status:{}".format(DELETED_REQUEST), channel="puzzle:{puzzle_id}".format(puzzle_id=puzzle_id), ) response = { "status": DELETED_REQUEST, } elif action == "freeze": cur.execute( fetch_query_string("update_puzzle_status_for_puzzle.sql"), {"status": FROZEN, "puzzle": puzzleData["id"]}, ) db.commit() sse.publish( "status:{}".format(FROZEN), channel="puzzle:{puzzle_id}".format(puzzle_id=puzzle_id), ) response = { "status": FROZEN, } elif action == "unfreeze": # TODO: set status to COMPLETE if puzzle has been completed instead of ACTIVE cur.execute( fetch_query_string("update_puzzle_status_for_puzzle.sql"), {"status": ACTIVE, "puzzle": puzzleData["id"]}, ) db.commit() sse.publish( "status:{}".format(ACTIVE), channel="puzzle:{puzzle_id}".format(puzzle_id=puzzle_id), ) response = { "status": ACTIVE, } elif action == "reset": if not ( puzzleData.get("permission") == PRIVATE and not puzzleData.get("is_original") and puzzleData.get("status") in (FROZEN, BUGGY_UNLISTED, ACTIVE, COMPLETED) ): response = {"msg": "Only unlisted puzzle instances can be reset"} cur.close() return make_response(json.jsonify(response), 400) if puzzleData.get("status") not in ( ACTIVE, COMPLETED, FROZEN, BUGGY_UNLISTED, ): response = { "msg": "Puzzle is not in acceptable state in order to be reset" } cur.close() return make_response(json.jsonify(response), 400) if puzzleData.get("status") != ACTIVE: # Only update the response status if puzzle status is changing. # This way any response will trigger the "Reload" button since # the active puzzle status will have an empty response status. response = {"status": ACTIVE} job = current_app.cleanupqueue.enqueue( "api.jobs.piece_reset.reset_puzzle_pieces_and_handle_errors", puzzleData.get("id"), result_ttl=0, ) cur.close() purge_route_from_nginx_cache( "/chill/site/front/{puzzle_id}/".format(puzzle_id=puzzle_id), current_app.config.get("PURGEURLLIST"), ) return make_response(json.jsonify(response), 202)
def patch(self, puzzle_id): ip = request.headers.get("X-Real-IP") user = int(current_app.secure_cookie.get("user") or user_id_from_ip(ip)) # validate the args and headers args = {} xhr_data = request.get_json() if xhr_data: args.update(xhr_data) if request.form: args.update(request.form.to_dict(flat=True)) # Verify args action = args.get("action") if action not in ORIGINAL_ACTIONS: abort(400) cur = db.cursor() # validate the puzzle_id result = cur.execute( fetch_query_string("select-puzzle-details-for-puzzle_id.sql"), {"puzzle_id": puzzle_id}, ).fetchall() if not result: # 400 if puzzle does not exist err_msg = { "msg": "No puzzle found", } cur.close() return make_response(json.jsonify(err_msg), 400) (result, col_names) = rowify(result, cur.description) puzzleData = result[0] if not puzzleData["is_original"]: cur.close() abort(400) response = {} if action == "bump": bid_amount = self.get_bid_amount(cur, puzzleData) low, high = next( filter( lambda x: x[0] <= puzzleData["pieces"] and x[1] > puzzleData["pieces"], current_app.config["SKILL_LEVEL_RANGES"], ) ) # check if player has enough dots for bumping puzzle up in queue player_points_result = cur.execute( fetch_query_string("select-minimum-points-for-user.sql"), {"points": bid_amount, "user": user}, ).fetchone() if not player_points_result: cur.close() return make_response(json.jsonify({}), 400) # bump any puzzle that is currently at QUEUE_WINNING_BID to be QUEUE_BUMPED_BID cur.execute( fetch_query_string("bump-puzzle-winning-bid-to-bumped-bid.sql"), {"low": low, "high": high}, ) # bump this puzzle to be the winning bid and decrease player dots cur.execute( fetch_query_string("bump-puzzle-queue-winning-bid-for-puzzle.sql"), {"puzzle": puzzleData["id"]}, ) cur.execute( fetch_query_string("decrease-user-points.sql"), {"points": bid_amount, "user": user}, ) db.commit() else: cur.close() return make_response(json.jsonify({}), 400) cur.close() purge_route_from_nginx_cache( "/chill/site/front/{puzzle_id}/".format(puzzle_id=puzzle_id), current_app.config.get("PURGEURLLIST"), ) return make_response(json.jsonify(response), 202)
def publishMessage(msg, karma_change, karma, points=0, complete=False): # print(topic) # print(msg) if current_app.config.get("PUZZLE_PIECES_CACHE_TTL"): stamp = redis_connection.get(f"pzstamp:{puzzle}") if stamp: pcu_key = f"pcu:{stamp}" redis_connection.rpushx(pcu_key, msg) sse.publish( msg, type="move", channel="puzzle:{puzzle_id}".format( puzzle_id=puzzleData["puzzle_id"]), ) if user != ANONYMOUS_USER_ID: points_key = "points:{user}".format(user=user) recent_points = int(redis_connection.get(points_key) or 0) if karma_change < 0 and karma <= 0 and recent_points > 0: redis_connection.decr(points_key) redis_connection.zadd("pcupdates", {puzzle: now}) if user != ANONYMOUS_USER_ID: # bump the m_date for this player on the puzzle and timeline redis_connection.zadd("timeline:{puzzle}".format(puzzle=puzzle), {user: now}) redis_connection.zadd("timeline", {user: now}) # Update player points if points != 0 and user is not None and user != ANONYMOUS_USER_ID: redis_connection.zincrby("score:{puzzle}".format(puzzle=puzzle), amount=1, value=user) redis_connection.sadd("batchuser", user) redis_connection.sadd("batchpuzzle", puzzle) redis_connection.incr("batchscore:{user}".format(user=user), amount=1) redis_connection.incr( "batchpoints:{puzzle}:{user}".format(puzzle=puzzle, user=user), amount=points, ) redis_connection.zincrby("rank", amount=1, value=user) pieces = int(puzzleData["pieces"]) # Skip increasing dots if puzzle is private earns = get_earned_points(pieces, permission=puzzleData.get("permission")) # karma = int(redis_connection.get(karma_key)) ## Max out recent points if (earns != 0 and karma >= current_app.config["MAX_KARMA"] and recent_points < current_app.config["MAX_RECENT_POINTS"]): recent_points = redis_connection.incr(points_key) # Doing small puzzles doesn't increase recent points, just extends points expiration. redis_connection.expire(points_key, current_app.config["RECENT_POINTS_EXPIRE"]) # Extend the karma points expiration since it has increased redis_connection.expire(karma_key, current_app.config["KARMA_POINTS_EXPIRE"]) # Max out karma if karma < current_app.config["MAX_KARMA"]: karma = redis_connection.incr(karma_key) karma_change += 1 redis_connection.incr("batchpoints:{user}".format(user=user), amount=earns) if complete: current_app.logger.info("puzzle {puzzle_id} is complete".format( puzzle_id=puzzleData["puzzle_id"])) sse.publish( "status:{}".format(COMPLETED), channel="puzzle:{puzzle_id}".format( puzzle_id=puzzleData["puzzle_id"]), ) r = requests.patch( "http://{HOSTAPI}:{PORTAPI}/internal/puzzle/{puzzle_id}/details/" .format( HOSTAPI=current_app.config["HOSTAPI"], PORTAPI=current_app.config["PORTAPI"], puzzle_id=puzzleData["puzzle_id"], ), json={ "status": COMPLETED, "m_date": time.strftime("%Y-%m-%d %H:%M:%S", time.gmtime()), "queue": QUEUE_END_OF_LINE, }, ) if r.status_code != 200: raise Exception( "Puzzle details api error when updating puzzle to be complete" ) # Delaying helps avoid issues for players that are moving the last # piece of the puzzle as someone else completes it. delay = (current_app.config["MAX_PAUSE_PIECES_TIMEOUT"] + current_app.config["PIECE_MOVE_TIMEOUT"] + 2) current_app.logger.info( f"Delaying puzzle transfer on completed puzzle ({puzzleData['puzzle_id']}) for {delay} seconds" ) job = current_app.cleanupqueue.enqueue_in( timedelta(seconds=delay), "api.jobs.convertPiecesToDB.transfer", puzzle, result_ttl=0, ) purge_route_from_nginx_cache( "/chill/site/front/{puzzle_id}/".format( puzzle_id=puzzleData["puzzle_id"]), current_app.config.get("PURGEURLLIST"), ) if karma_change and user != ANONYMOUS_USER_ID: sse.publish( "{user}:{piece}:{karma}:{karma_change}".format( user=user, piece=piece, karma=karma + recent_points, karma_change=karma_change, ), type="karma", channel="puzzle:{puzzle_id}".format( puzzle_id=puzzleData["puzzle_id"]), ) # end = time.perf_counter() # duration = end - start # redis_connection.rpush("testdata:translate", duration) # return topic and msg mostly for testing return (msg, karma_change)
def post(self): "" response = {"message": "", "name": "error"} user = int( current_app.secure_cookie.get(u"user") or user_id_from_ip(request.headers.get("X-Real-IP"))) if user == None: response["message"] = "User not signed in." response["name"] = "error" return make_response(json.jsonify(response), 400) user = int(user) args = {} if request.form: args.update(request.form.to_dict(flat=True)) display_name = args.get("name", "").strip() name = normalize_name_from_display_name(display_name) # name is always converted to lowercase and display_name preserves # original case. display_name = args.get("name", "").strip() name = normalize_name_from_display_name(display_name) if len(display_name) > USER_NAME_MAXLENGTH: response["message"] = "Submitted name is too long." response["name"] = "error" return make_response(json.jsonify(response), 400) cur = db.cursor() result = cur.execute( fetch_query_string("select-minimum-points-for-user.sql"), { "user": user, "points": current_app.config["POINT_COST_FOR_CHANGING_NAME"], }, ).fetchone() if not result: response["message"] = "Not enough points to change name." response["name"] = "error" cur.close() db.commit() return make_response(json.jsonify(response), 400) else: if name == "": cur.execute( fetch_query_string( "remove-user-name-on-name-register-for-player.sql"), { "player_id": user, }, ) cur.execute( fetch_query_string("decrease-user-points.sql"), { "points": current_app.config["POINT_COST_FOR_CHANGING_NAME"], "user": user, }, ) response["message"] = "Removed name." response["name"] = "success" else: result = cur.execute( fetch_query_string( "check-status-of-name-on-name-register.sql"), { "name": name }, ).fetchall() if result: (result, col_names) = rowify(result, cur.description) name_status = result[0] if name_status["rejected"] == 1: response[ "message"] = "Submitted name has been rejected before." response["name"] = "rejected" elif name_status["claimed"] == 0 or name_status[ "user"] == user: # The name is available and can be claimed. If owned by the # user the casing of the letters can be modified. cur.execute( fetch_query_string( "remove-user-name-on-name-register-for-player.sql" ), { "player_id": user, }, ) # Also updates the display_name if casing has changed cur.execute( fetch_query_string( "claim-user-name-on-name-register-for-player.sql" ), { "player_id": user, "name": name, "display_name": display_name, "time": "+5 minutes", }, ) cur.execute( fetch_query_string("decrease-user-points.sql"), { "points": current_app. config["POINT_COST_FOR_CHANGING_NAME"], "user": user, }, ) response[ "message"] = "Submitted name ({}) reclaimed.".format( display_name) response["name"] = "success" else: response[ "message"] = "Submitted name ({}) is currently used by another player. Please try a different name.".format( display_name) response["name"] = "rejected" else: # name is new cur.execute( fetch_query_string( "remove-user-name-on-name-register-for-player.sql" ), { "player_id": user, }, ) cur.execute( fetch_query_string( "add-user-name-on-name-register-for-player-to-be-reviewed.sql" ), { "player_id": user, "name": name, "display_name": display_name, "time": "+5 minutes", }, ) cur.execute( fetch_query_string("decrease-user-points.sql"), { "points": current_app.config["POINT_COST_FOR_CHANGING_NAME"], "user": user, }, ) response[ "message"] = 'Thank you for submitting a new name. "{}" will be shown next to your bit icon in about 10 minutes.'.format( display_name) response["name"] = "success" db.commit() cur.close() purge_route_from_nginx_cache( "/chill/site/internal/player-bit/{}/".format(user), current_app.config.get("PURGEURLLIST"), ) return make_response(json.jsonify(response), 202)
def post(self): "Route is protected by basic auth in nginx" args = {} if request.form: args.update(request.form.to_dict(flat=True)) # TODO: Check user to see if role matches? # user = current_app.secure_cookie.get(u'user') # if not user: # abort(403) # Verify args action = args.get("action") if action not in ACTIONS: abort(400) reject = args.get("reject") if action == "reject" and reject not in ("license", "attribution"): abort(400) delete = args.get("delete") if action == "delete" and delete not in ("license", "inapt", "old", "request"): abort(400) edit = args.get("edit") if action == "edit" and edit not in ("private", ): abort(400) # abort if tag value not set tag = args.get("tag") if action == "tag" and not tag: abort(400) puzzle_ids = request.form.getlist("montage_puzzle_id") if len(puzzle_ids) == 0: abort(400) if not isinstance(puzzle_ids, list): puzzle_ids = [puzzle_ids] cur = db.cursor() status = None if action == "approve": status = IN_RENDER_QUEUE if action == "rebuild": status = REBUILD if action == "reject": if reject == "license": status = FAILED_LICENSE elif reject == "attribution": status = NO_ATTRIBUTION if action == "delete": if delete == "license": status = DELETED_LICENSE elif delete == "inapt": status = DELETED_INAPT elif delete == "old": status = DELETED_OLD elif delete == "request": status = DELETED_REQUEST for puzzle_id in puzzle_ids: delete_puzzle_resources(puzzle_id) id = cur.execute( fetch_query_string("select_puzzle_id_by_puzzle_id.sql"), { "puzzle_id": puzzle_id }, ).fetchone()[0] # current_app.logger.info('deleting puzzle resources for id {}'.format(id)) cur.execute( fetch_query_string("delete_puzzle_file_for_puzzle.sql"), {"puzzle": id}, ) cur.execute(fetch_query_string("delete_piece_for_puzzle.sql"), {"puzzle": id}) msg = delete_puzzle_timeline(puzzle_id) if msg.get("status_code") >= 400: current_app.logger.error(msg.get("msg")) current_app.logger.error( f"Failed delete of puzzle timeline for puzzle_id {puzzle_id}" ) cur.execute( fetch_query_string( "remove-puzzle-from-all-user-puzzle-slots.sql"), {"puzzle": id}, ) db.commit() if action == "edit": if edit == "private": def each(puzzle_ids): for puzzle_id in puzzle_ids: yield {"puzzle_id": puzzle_id, "permission": PRIVATE} cur.executemany( fetch_query_string( "update_puzzle_permission_for_puzzle_id.sql"), each(puzzle_ids), ) db.commit() def each(puzzle_ids): for puzzle_id in puzzle_ids: yield {"puzzle_id": puzzle_id, "status": status} if status is not None: cur.executemany( fetch_query_string("update_puzzle_status_for_puzzle_id.sql"), each(puzzle_ids), ) db.commit() for puzzle_id in puzzle_ids: purge_route_from_nginx_cache( "/chill/site/front/{puzzle_id}/".format(puzzle_id=puzzle_id), current_app.config.get("PURGEURLLIST"), ) if action in ("approve", "rebuild"): puzzles = rowify( cur.execute( fetch_query_string("select-puzzles-in-render-queue.sql"), { "IN_RENDER_QUEUE": IN_RENDER_QUEUE, "REBUILD": REBUILD }, ).fetchall(), cur.description, )[0] print("found {0} puzzles to render or rebuild".format( len(puzzles))) # push each puzzle to artist job queue for puzzle in puzzles: job = current_app.createqueue.enqueue( "api.jobs.pieceRenderer.render", [puzzle], result_ttl=0, job_timeout="24h", ) # TODO: if action in ('reject', 'delete'): #Also apply to any puzzle instances cur.close() return redirect("/chill/site/admin/puzzle/", code=303)
def post(self): "Route is protected by basic auth in nginx" args = {} if request.form: args.update(request.form.to_dict(flat=True)) player = args.get("player") if not player: abort(400) email_verified = int(args.get("email_verified", "0")) if not email_verified in (0, 1): abort(400) name_approved = int(args.get("name_approved", "0")) if not name_approved in (0, 1): abort(400) # name is always converted to lowercase and display_name preserves # original case. display_name = args.get("name", "").strip() name = normalize_name_from_display_name(display_name) if len(display_name) > USER_NAME_MAXLENGTH: abort(400) email = args.get("email", "").strip().lower() if len(email) > EMAIL_MAXLENGTH: abort(400) cur = db.cursor() result = cur.execute(fetch_query_string("user-has-player-account.sql"), { "player_id": player }).fetchone() if not result or result[0] == 0: cur.execute( fetch_query_string("init-player-account-for-user.sql"), {"player_id": player}, ) db.commit() result = cur.execute( fetch_query_string( "select-admin-player-details-for-player-id.sql"), { "player_id": player }, ).fetchall() if not result: cur.close() db.commit() abort(400) (result, col_names) = rowify(result, cur.description) existing_player_data = result[0] if email == "": cur.execute( fetch_query_string("remove-player-account-email.sql"), {"player_id": player}, ) else: cur.execute( fetch_query_string("update-player-account-email.sql"), { "player_id": player, "email": email }, ) cur.execute( fetch_query_string("update-player-account-email-verified.sql"), { "player_id": player, "email_verified": email_verified, }, ) cur.execute( fetch_query_string("update-user-points.sql"), { "player_id": player, "points": int(args.get("dots", existing_player_data["dots"])), "POINTS_CAP": current_app.config["POINTS_CAP"], }, ) if name == "": cur.execute( fetch_query_string( "remove-user-name-on-name-register-for-player.sql"), { "player_id": player, }, ) else: if existing_player_data["name"] != name: result = cur.execute( fetch_query_string( "select-unclaimed-name-on-name-register.sql"), { "name": name, }, ).fetchall() if result: (result, col_names) = rowify(result, cur.description) unclaimed_name_data = result[0] if unclaimed_name_data["approved_date"] == None: # name has been rejected if name_approved == 1: # override the rejected name and let player claim it cur.execute( fetch_query_string( "remove-user-name-on-name-register-for-player.sql" ), { "player_id": player, }, ) cur.execute( fetch_query_string( "claim-rejected-user-name-on-name-register-for-player.sql" ), { "player_id": player, "name": name, }, ) else: # name can be claimed cur.execute( fetch_query_string( "remove-user-name-on-name-register-for-player.sql" ), { "player_id": player, }, ) cur.execute( fetch_query_string( "claim-user-name-on-name-register-for-player.sql" ), { "player_id": player, "display_name": display_name, "name": name, "time": "+1 second", }, ) else: # The name is new and not in the NameRegister. Add it and # mark it for auto-approval. cur.execute( fetch_query_string( "remove-user-name-on-name-register-for-player.sql" ), { "player_id": player, }, ) cur.execute( fetch_query_string( "add-user-name-on-name-register-for-player.sql"), { "player_id": player, "name": name, "display_name": display_name, "time": "+1 second", }, ) if existing_player_data["name_approved"] == 1 and name_approved == 0: # Place this name on reject list cur.execute(fetch_query_string("reject-name-on-name-register.sql"), { "name": name, }) if existing_player_data["name_approved"] == 0 and name_approved == 1: cur.execute( fetch_query_string("update-user-name-approved.sql"), { "player_id": player, "name_approved": name_approved, }, ) cur.close() db.commit() purge_route_from_nginx_cache( "/chill/site/internal/player-bit/{}/".format(player), current_app.config.get("PURGEURLLIST"), ) return redirect( "/chill/site/admin/player/details/{player}/".format(player=player))
def post(self): "Route is protected by basic auth in nginx" args = {} if request.form: args.update(request.form.to_dict(flat=True)) # TODO: Check user to see if role matches? # user = current_app.secure_cookie.get(u'user') # if not user: # abort(403) # Verify args action = args.get("action") if action not in ACTIONS: abort(400) reject = args.get("reject") if action == "reject" and reject not in ("license", "attribution"): abort(400) delete = args.get("delete") if action == "delete" and delete not in ("license", "inapt", "old", "request"): abort(400) redo = args.get("redo") if action == "redo" and redo not in ("delete_and_redo", ): abort(400) edit = args.get("edit") if action == "edit" and edit not in ("private", "status_active"): abort(400) # abort if tag value not set tag = args.get("tag") if action == "tag" and not tag: abort(400) puzzle_ids = request.form.getlist("montage_puzzle_id") if len(puzzle_ids) == 0: abort(400) if not isinstance(puzzle_ids, list): puzzle_ids = [puzzle_ids] cur = db.cursor() status = None if action == "approve": status = IN_RENDER_QUEUE if action == "rebuild": status = REBUILD if action == "buggy_unlisted": status = BUGGY_UNLISTED if action == "reject": if reject == "license": status = FAILED_LICENSE elif reject == "attribution": status = NO_ATTRIBUTION if action == "delete" or action == "redo": if action == "delete": if delete == "license": status = DELETED_LICENSE elif delete == "inapt": status = DELETED_INAPT elif delete == "old": status = DELETED_OLD elif delete == "request": status = DELETED_REQUEST elif action == "redo": if redo == "delete_and_redo": # Set it back to SUGGESTED since that way a new puzzle_id # will be created. status = SUGGESTED for puzzle_id in puzzle_ids: result = cur.execute( fetch_query_string( "select-puzzle-details-for-puzzle_id.sql"), { "puzzle_id": puzzle_id }, ).fetchall() if not result: # Could be a puzzle that failed to render and doesn't have # any resources. continue (result, col_names) = rowify(result, cur.description) puzzle_details = result[0] delete_puzzle_resources( puzzle_id, is_local_resource=not puzzle_details["url"].startswith( "http") and not puzzle_details["url"].startswith("//")) id = puzzle_details["id"] # current_app.logger.info('deleting puzzle resources for id {}'.format(id)) cur.execute( fetch_query_string("delete_puzzle_file_for_puzzle.sql"), {"puzzle": id}, ) cur.execute(fetch_query_string("delete_piece_for_puzzle.sql"), {"puzzle": id}) msg = delete_puzzle_timeline(puzzle_id) if msg.get("status_code") >= 400: current_app.logger.error(msg.get("msg")) current_app.logger.error( f"Failed delete of puzzle timeline for puzzle_id {puzzle_id}" ) cur.execute( fetch_query_string( "remove-puzzle-from-all-user-puzzle-slots.sql"), {"puzzle": id}, ) db.commit() if action == "edit": if edit == "private": def each(puzzle_ids): for puzzle_id in puzzle_ids: yield {"puzzle_id": puzzle_id, "permission": PRIVATE} cur.executemany( fetch_query_string( "update_puzzle_permission_for_puzzle_id.sql"), each(puzzle_ids), ) db.commit() elif edit == "status_active": m_date_now = strftime("%Y-%m-%d %H:%M:%S", gmtime(time())) for puzzle_id in puzzle_ids: data = {"status": ACTIVE, "m_date": m_date_now} response_msg = update_puzzle_details(puzzle_id, data) if (response_msg.get("rowcount") and response_msg.get("status_code") >= 300): current_app.logger.warning( f"Failed to update puzzle details {puzzle_id} {data}" ) def each(puzzle_ids): for puzzle_id in puzzle_ids: yield {"puzzle_id": puzzle_id, "status": status} if status is not None: cur.executemany( fetch_query_string("update_puzzle_status_for_puzzle_id.sql"), each(puzzle_ids), ) db.commit() for puzzle_id in puzzle_ids: purge_route_from_nginx_cache( "/chill/site/front/{puzzle_id}/".format(puzzle_id=puzzle_id), current_app.config.get("PURGEURLLIST"), ) if action in ("approve", "rebuild"): puzzles = rowify( cur.execute( fetch_query_string("select-puzzles-in-render-queue.sql"), { "IN_RENDER_QUEUE": IN_RENDER_QUEUE, "REBUILD": REBUILD }, ).fetchall(), cur.description, )[0] print("found {0} puzzles to render or rebuild".format( len(puzzles))) # push each puzzle to artist job queue for puzzle in puzzles: job = current_app.createqueue.enqueue( "api.jobs.pieceRenderer.render", [puzzle], result_ttl=0, job_timeout="24h", ) # TODO: if action in ('reject', 'delete'): #Also apply to any puzzle instances cur.close() return redirect("/chill/site/admin/puzzle/", code=303)
def publishMessage(msg, karma_change, points=0, complete=False): # print(topic) # print(msg) sse.publish( msg, type="move", channel="puzzle:{puzzle_id}".format( puzzle_id=puzzleData["puzzle_id"]), ) now = int(time.time()) redis_connection.zadd("pcupdates", {puzzle: now}) # TODO: # return (topic, msg) # bump the m_date for this player on the puzzle and timeline redis_connection.zadd("timeline:{puzzle}".format(puzzle=puzzle), {user: now}) redis_connection.zadd("timeline", {user: now}) # Update player points if points != 0 and user != None: redis_connection.zincrby("score:{puzzle}".format(puzzle=puzzle), amount=1, value=user) redis_connection.sadd("batchuser", user) redis_connection.sadd("batchpuzzle", puzzle) redis_connection.incr("batchscore:{user}".format(user=user), amount=1) redis_connection.incr( "batchpoints:{puzzle}:{user}".format(puzzle=puzzle, user=user), amount=points, ) redis_connection.zincrby("rank", amount=1, value=user) points_key = "points:{user}".format(user=user) pieces = int(puzzleData["pieces"]) # Skip increasing dots if puzzle is private earns = get_earned_points(pieces, permission=puzzleData.get("permission")) karma = int(redis_connection.get(karma_key)) ## Max out recent points # if earns != 0: # recent_points = int(redis_connection.get(points_key) or 0) # if karma + 1 + recent_points + earns < MAX_KARMA: # redis_connection.incr(points_key, amount=earns) # Doing small puzzles doesn't increase recent points, just extends points expiration. redis_connection.expire(points_key, RECENT_POINTS_EXPIRE) karma_change += 1 # Extend the karma points expiration since it has increased redis_connection.expire(karma_key, KARMA_POINTS_EXPIRE) # Max out karma if karma < MAX_KARMA: redis_connection.incr(karma_key) else: # Max out points if earns != 0: recent_points = int(redis_connection.get(points_key) or 0) if recent_points + earns <= MAX_RECENT_POINTS: redis_connection.incr(points_key, amount=earns) redis_connection.incr("batchpoints:{user}".format(user=user), amount=earns) # TODO: Optimize by using redis for puzzle status if complete: current_app.logger.info("puzzle {puzzle_id} is complete".format( puzzle_id=puzzleData["puzzle_id"])) cur = db.cursor() cur.execute( fetch_query_string("update_puzzle_status_for_puzzle.sql"), { "puzzle": puzzle, "status": COMPLETED }, ) cur.execute( fetch_query_string("update_puzzle_m_date_to_now.sql"), { "puzzle": puzzle, "modified": now }, ) cur.execute( fetch_query_string("update_puzzle_queue_for_puzzle.sql"), { "puzzle": puzzle, "queue": QUEUE_END_OF_LINE }, ) db.commit() sse.publish( "status:{}".format(COMPLETED), channel="puzzle:{puzzle_id}".format( puzzle_id=puzzleData["puzzle_id"]), ) job = current_app.cleanupqueue.enqueue_call( func="api.jobs.convertPiecesToDB.transfer", args=(puzzle, ), result_ttl=0) purge_route_from_nginx_cache( "/chill/site/front/{puzzle_id}/".format( puzzle_id=puzzleData["puzzle_id"]), current_app.config.get("PURGEURLLIST"), ) db.commit() cur.close() # return topic and msg mostly for testing return (msg, karma_change)
def post(self): """If the bit icon is available; claim it for the user.""" data = {"message": "", "name": "error"} icon = request.args.get("icon") if not icon: data["message"] = "No icon param passed" data["name"] = "error" return make_response(json.jsonify(data), 400) # Prevent creating a new user if no support for cookies. Player should # have 'ot' already set by viewing the page. uses_cookies = current_app.secure_cookie.get(u"ot") if not uses_cookies: data["message"] = "No ot cookie present" data["name"] = "error" return make_response(json.jsonify(data), 400) cur = db.cursor() # Check if bit icon is available result = cur.execute( fetch_query_string("select_available_bit_icon.sql"), {"icon": icon} ).fetchone() if not result: cur.close() db.commit() data["message"] = "That bit icon is no longer available." data["name"] = "error" return make_response(json.jsonify(data), 400) user = current_app.secure_cookie.get(u"user") if not user: user = user_id_from_ip(request.headers.get("X-Real-IP")) if user == None: data["message"] = "Not logged in." data["name"] = "error" cur.close() db.commit() return make_response(json.jsonify(data), 400) user = int(user) else: user = int(user) data["message"] = "Bit icon claimed by using {} of your dots.".format( current_app.config["POINT_COST_FOR_CHANGING_BIT"] ) data["name"] = "success" response = make_response(json.jsonify(data), 200) # Unclaim any bit icon that the player already has cur.execute(fetch_query_string("unclaim_bit_icon.sql"), {"user": user}) # Claim the bit icon cur.execute( fetch_query_string("update_bit_icon_user.sql"), {"user": user, "icon": icon} ) cur.execute( fetch_query_string("decrease-user-points.sql"), { "points": current_app.config["POINT_COST_FOR_CHANGING_BIT"], "user": user, }, ) cur.close() db.commit() purge_route_from_nginx_cache( "/chill/site/internal/player-bit/{}/".format(user), current_app.config.get("PURGEURLLIST"), ) return response
def post(self): args = {} if request.form: args.update(request.form.to_dict(flat=True)) puzzle_id = args.get("puzzle_id") if not puzzle_id: abort(400) # 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) user = int( current_app.secure_cookie.get(u"user") or user_id_from_ip(request.headers.get("X-Real-IP"))) cur = db.cursor() result = cur.execute( fetch_query_string( "select_puzzle_for_puzzle_id_and_status_and_not_recent.sql"), { "puzzle_id": puzzle_id, "status": COMPLETED }, ).fetchall() if not result: # Puzzle does not exist or is not completed status. # Reload the page as the status may have been changed. cur.close() return redirect( "/chill/site/front/{puzzle_id}/".format(puzzle_id=puzzle_id)) (result, col_names) = rowify(result, cur.description) puzzleData = result[0] puzzle = puzzleData["id"] userCanRebuildPuzzle = cur.execute( fetch_query_string("select-user-rebuild-puzzle-prereq.sql"), { "user": user, "puzzle": puzzle, "pieces": pieces }, ).fetchall() if not userCanRebuildPuzzle: cur.close() abort(400) original_puzzle_id = puzzleData["original_puzzle_id"] # Get the adjusted piece count depending on the size of the original and # the minimum piece size. original_puzzle_dir = os.path.join( current_app.config["PUZZLE_RESOURCES"], original_puzzle_id) # TODO: get path of original.jpg via the PuzzleFile query # TODO: use requests.get to get original.jpg and run in another thread imagefile = os.path.join(original_puzzle_dir, "original.jpg") im = Image.open(imagefile) (width, height) = im.size im.close() max_pieces_that_will_fit = int((old_div(width, MIN_PIECE_SIZE)) * (old_div(height, MIN_PIECE_SIZE))) # The user points for rebuilding the puzzle is decreased by the piece # count for the puzzle. Use at least minimum piece count (20) points for # smaller puzzles. Players that own a puzzle instance do not decrease # any points (dots) if the puzzle is complete. point_cost = max( current_app.config["MINIMUM_PIECE_COUNT"], min( max_pieces_that_will_fit, pieces, current_app.config["MAX_POINT_COST_FOR_REBUILDING"], ), ) if not (puzzleData["owner"] == user and puzzleData["puzzle_id"] == puzzleData["original_puzzle_id"]): cur.execute( fetch_query_string("decrease-user-points.sql"), { "user": user, "points": point_cost }, ) # Update puzzle status to be REBUILD and change the piece count cur.execute( fetch_query_string("update_status_puzzle_for_puzzle_id.sql"), { "puzzle_id": puzzle_id, "status": REBUILD, "pieces": pieces, "queue": QUEUE_REBUILD, }, ) puzzleData["status"] = REBUILD puzzleData["pieces"] = pieces db.commit() # Delete any piece data from redis since it is no longer needed. (all_pieces, col_names) = rowify( cur.execute( fetch_query_string("select_all_piece_ids_for_puzzle.sql"), { "puzzle": puzzle }, ).fetchall(), cur.description, ) cur.close() deletePieceDataFromRedis(redis_connection, puzzle, all_pieces) job = current_app.createqueue.enqueue_call( func="api.jobs.pieceRenderer.render", args=([puzzleData]), result_ttl=0, timeout="24h", ) job = current_app.cleanupqueue.enqueue_call( func="api.jobs.timeline_archive.archive_and_clear", kwargs=({ "puzzle": puzzle }), result_ttl=0, timeout="24h", ) purge_route_from_nginx_cache( "/chill/site/front/{puzzle_id}/".format(puzzle_id=puzzle_id), current_app.config.get("PURGEURLLIST"), ) return redirect( "/chill/site/front/{puzzle_id}/".format(puzzle_id=puzzle_id))
def post(self): args = {} if request.form: args.update(request.form.to_dict(flat=True)) puzzle_id = args.get("puzzle_id") if not puzzle_id: abort(400) # 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) user = int( current_app.secure_cookie.get(u"user") or user_id_from_ip(request.headers.get("X-Real-IP"))) cur = db.cursor() result = cur.execute( fetch_query_string( "select_puzzle_for_puzzle_id_and_status_and_not_recent.sql"), { "puzzle_id": puzzle_id, "status": COMPLETED }, ).fetchall() if not result: # Puzzle does not exist or is not completed status. # Reload the page as the status may have been changed. cur.close() return redirect( "/chill/site/front/{puzzle_id}/".format(puzzle_id=puzzle_id)) (result, col_names) = rowify(result, cur.description) puzzleData = result[0] puzzle = puzzleData["id"] userCanRebuildPuzzle = cur.execute( fetch_query_string("select-user-rebuild-puzzle-prereq.sql"), { "user": user, "puzzle": puzzle, "pieces": pieces }, ).fetchall() if not userCanRebuildPuzzle: cur.close() abort(400) if (puzzleData["permission"] == PRIVATE and puzzleData["original_puzzle_id"] == puzzleData["puzzle_id"]): current_app.logger.warning( "Original puzzles that are private can not be rebuilt") cur.close() abort(400) # The user points for rebuilding the puzzle is decreased by the piece # count for the puzzle. Use at least minimum piece count (20) points for # smaller puzzles. Players that own a puzzle instance do not decrease # any points (dots) if the puzzle is complete. point_cost = max( current_app.config["MINIMUM_PIECE_COUNT"], min( pieces, current_app.config["MAX_POINT_COST_FOR_REBUILDING"], ), ) if not (puzzleData["owner"] == user and puzzleData["puzzle_id"] == puzzleData["original_puzzle_id"]): cur.execute( fetch_query_string("decrease-user-points.sql"), { "user": user, "points": point_cost }, ) # Update puzzle status to be REBUILD and change the piece count cur.execute( fetch_query_string("update_status_puzzle_for_puzzle_id.sql"), { "puzzle_id": puzzle_id, "status": REBUILD, "pieces": pieces, "queue": QUEUE_REBUILD, }, ) puzzleData["status"] = REBUILD puzzleData["pieces"] = pieces db.commit() # Delete any piece data from redis since it is no longer needed. (all_pieces, col_names) = rowify( cur.execute( fetch_query_string("select_all_piece_ids_for_puzzle.sql"), { "puzzle": puzzle }, ).fetchall(), cur.description, ) cur.close() deletePieceDataFromRedis(redis_connection, puzzle, all_pieces) delete_puzzle_resources( puzzle_id, is_local_resource=not puzzleData["preview_full"].startswith("http") and not puzzleData["preview_full"].startswith("//"), exclude_regex=r"(original|preview_full).([^.]+\.)?jpg") job = current_app.createqueue.enqueue( "api.jobs.pieceRenderer.render", [puzzleData], result_ttl=0, job_timeout="24h", ) job = current_app.cleanupqueue.enqueue( "api.jobs.timeline_archive.archive_and_clear", puzzle, result_ttl=0, job_timeout="24h", ) purge_route_from_nginx_cache( "/chill/site/front/{puzzle_id}/".format(puzzle_id=puzzle_id), current_app.config.get("PURGEURLLIST"), ) return redirect( "/chill/site/front/{puzzle_id}/".format(puzzle_id=puzzle_id))