Пример #1
0
    def get(self, puzzle_id):
        ""
        ip = request.headers.get("X-Real-IP")
        user = int(
            current_app.secure_cookie.get(u"user") or user_id_from_ip(ip))
        cur = db.cursor()
        result = cur.execute(
            fetch_query_string("select_viewable_puzzle_id.sql"),
            {
                "puzzle_id": puzzle_id
            },
        ).fetchall()
        if not result:
            # 404 if puzzle does not exist
            abort(404)

        (result, col_names) = rowify(result, cur.description)
        puzzle = result[0].get("puzzle")
        status = result[0].get("status")
        now = int(time.time())

        count = (redis_connection.zcount(
            "timeline:{puzzle}".format(puzzle=puzzle), now - 5 * 60, "+inf")
                 or 0)

        player_active_count = {"now": now, "count": count}

        cur.close()
        return json.jsonify(player_active_count)
Пример #2
0
    def post(self, puzzle_id):
        "Ping and record the time in milliseconds for this player."
        now_ms = int(time.time() * 1000)
        response = {"message": "", "name": ""}

        user = current_app.secure_cookie.get(u"user") or user_id_from_ip(
            request.headers.get("X-Real-IP"), skip_generate=True)

        if user == None:
            response["message"] = "Player not currently logged in."
            response["name"] = "error"
            return make_response(json.jsonify(response), 400)

        user = int(user)

        cur = db.cursor()

        # Validate the puzzle_id
        result = cur.execute(
            fetch_query_string("select_viewable_puzzle_id.sql"),
            {
                "puzzle_id": puzzle_id
            },
        ).fetchall()
        if not result:
            response["message"] = "Invalid puzzle id."
            response["name"] = "error"
            cur.close()
            db.commit()
            return make_response(json.jsonify(response), 400)
        else:
            (result, col_names) = rowify(result, cur.description)
            puzzle = result[0].get("puzzle")
            status = result[0].get("status")
            if status != ACTIVE:
                response["message"] = "Puzzle not active"
                response["name"] = "invalid"
                cur.close()
                db.commit()
                return make_response(json.jsonify(response), 200)

        # publish to the puzzle channel the ping with the user id.  This will
        # allow that player to determine their latency.
        token = uuid.uuid4().hex[:4]
        pingtoken_key = get_pingtoken_key(puzzle, user, token)
        redis_connection.setex(pingtoken_key, 60, now_ms)

        current_app.logger.debug(
            "publish ping {puzzle_id}".format(puzzle_id=puzzle_id))
        sse.publish(
            "{user}:{token}".format(user=user, token=token),
            type="ping",
            channel="puzzle:{puzzle_id}".format(puzzle_id=puzzle_id),
        )
        response["message"] = "ping accepted"
        response["name"] = "accepted"
        response = make_response(json.jsonify(response), 202)
        cur.close()
        db.commit()
        return response
Пример #3
0
    def get(self, puzzle_id):
        """
        deletePenalty: number;
        canFreeze: boolean;
        canDelete: boolean;
        canReset: boolean;
        hasActions: boolean;
        deleteDisabledMessage: string; //Not enough dots to delete this puzzle
        isFrozen: boolean;
        status: number;
        """
        ip = request.headers.get("X-Real-IP")
        user = int(current_app.secure_cookie.get("user") or user_id_from_ip(ip))

        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]

        (delete_penalty, can_delete, delete_disabled_message) = self.get_delete_prereq(
            puzzleData
        )
        response = {
            "canDelete": can_delete,
            "canFreeze": puzzleData.get("status") in (FROZEN, BUGGY_UNLISTED, ACTIVE),
            "canReset": puzzleData.get("permission") == PRIVATE
            and not puzzleData.get("is_original")
            and puzzleData.get("status") in (FROZEN, BUGGY_UNLISTED, ACTIVE, COMPLETED),
            "hasActions": puzzleData.get("status")
            in (
                FROZEN,
                ACTIVE,
                COMPLETED,
                BUGGY_UNLISTED,
                RENDERING_FAILED,
                REBUILD,
                IN_RENDER_QUEUE,
                MAINTENANCE,
            ),
            "deleteDisabledMessage": delete_disabled_message,
            "deletePenalty": delete_penalty,
            "isFrozen": puzzleData.get("status") == FROZEN,
            "status": puzzleData.get("status", -99),
        }
        cur.close()
        return make_response(json.jsonify(response), 200)
Пример #4
0
    def get(self):
        ""
        ip = request.headers.get("X-Real-IP")
        user = current_app.secure_cookie.get(u"user") or user_id_from_ip(ip)
        if user != None:
            user = int(user)
        args = {}
        if request.args:
            args.update(request.args.to_dict(flat=True))
        count = args.get("count")
        if count == None:
            return make_response(
                encoder.encode({"msg": "missing count param"}), 400)

        count = int(count)
        if count > 45:
            return make_response(
                encoder.encode({"msg": "Count arg is too high"}), 400)

        cur = db.cursor()
        now = int(time.time())
        active_players = frozenset(
            map(
                int,
                redis_connection.zrevrangebyscore("timeline", "+inf",
                                                  now - ACTIVE_RANGE),
            ))

        rank_slice = redis_connection.zrevrange("rank", 0, -1, withscores=True)

        ranks = []
        has_user_in_ranks = False
        user_in_ranks_index = -1
        for index, item in enumerate(rank_slice):
            (player, score) = map(int, item)
            if not has_user_in_ranks and player == user:
                has_user_in_ranks = True
                user_in_ranks_index = len(ranks)
            if player in active_players or player == user:
                ranks.append({
                    "id": player,
                    "score": score,
                })

        ranks_near_user = []
        if has_user_in_ranks:
            start = max(0, user_in_ranks_index - int(count / 2))
            end = min(len(ranks), user_in_ranks_index + int(count / 2))
            ranks_near_user = ranks[start:end]

        player_ranks = {
            "rank_slice": ranks_near_user,
        }

        cur.close()
        return encoder.encode(player_ranks)
Пример #5
0
    def post(self):
        """Update shareduser to user"""

        # 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:
            abort(400)

        cur = db.cursor()

        response = make_response("", 200)

        user = user_id_from_ip(request.headers.get("X-Real-IP"))
        if user == None:
            abort(400)
        user = int(user)

        # Only set new user if enough dots
        result = cur.execute(
            fetch_query_string("select-minimum-points-for-user.sql"),
            {
                "user":
                user,
                "points":
                current_app.config["NEW_USER_STARTING_POINTS"] +
                current_app.config["POINT_COST_FOR_CHANGING_BIT"],
            },
        ).fetchone()
        if result:
            self.register_new_user(user)
            # Save as a cookie
            current_app.secure_cookie.set(u"user",
                                          str(user),
                                          response,
                                          expires_days=365)
            # Remove shareduser
            expires = datetime.datetime.utcnow() - datetime.timedelta(days=365)
            current_app.secure_cookie.set(u"shareduser",
                                          "",
                                          response,
                                          expires=expires)

            cur.execute(
                fetch_query_string("decrease-user-points.sql"),
                {
                    "user": user,
                    "points":
                    current_app.config["POINT_COST_FOR_CHANGING_BIT"],
                },
            )

        cur.close()
        db.commit()
        return response
Пример #6
0
    def post(self):
        args = {}
        if request.form:
            args.update(request.form.to_dict(flat=True))

        # Only allow valid contributor
        if args.get("contributor",
                    None) != current_app.config.get("NEW_PUZZLE_CONTRIB"):
            abort(403)

        # 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])

        user = int(
            current_app.secure_cookie.get(u"user")
            or user_id_from_ip(request.headers.get("X-Real-IP")))

        # All puzzles are public by default, but allow the player to set to
        # PRIVATE if they have the role of membership.
        permission = int(args.get("permission", PUBLIC))
        if permission not in (PUBLIC, PRIVATE):
            permission = PUBLIC

        description = escape(args.get("description", ""))[:1000]

        # Check link and validate
        link = url_fix(args.get("link", ""))[:1000]

        upload_file = request.files.get("upload_file", None)

        puzzle_id = submit_puzzle(pieces, bg_color, user, permission,
                                  description, link, upload_file)

        return redirect("/chill/site/front/{0}/".format(puzzle_id), code=303)
Пример #7
0
    def get(self, puzzle_id):
        """
        highestBid: number;
        canBump: boolean;
        hasActions: boolean;
        bumpDisabledMessage: string; //Not enough dots to delete this puzzle
        status: number;
        """
        ip = request.headers.get("X-Real-IP")
        user = int(current_app.secure_cookie.get("user") or user_id_from_ip(ip))

        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]

        (highest_bid, can_bump, bump_disabled_message) = self.get_bump_prereq(
            cur, user, puzzleData
        )
        response = {
            "canBump": can_bump,
            "hasActions": puzzleData.get("status") == IN_QUEUE,
            "bumpDisabledMessage": bump_disabled_message,
            "highestBid": highest_bid,
            "status": puzzleData.get("status", -99),
        }
        cur.close()
        return make_response(json.jsonify(response), 200)
Пример #8
0
    def get(self):
        """
        /newapi/player-puzzle-list/

        returns
        {
        puzzles: [],
        }

        """

        ip = request.headers.get("X-Real-IP")
        user = int(
            current_app.secure_cookie.get(u"user") or user_id_from_ip(ip))

        cur = db.cursor()

        puzzle_list = []
        result = cur.execute(
            fetch_query_string("select_available_player_puzzle_images.sql"),
            {
                "player": user
            },
        ).fetchall()
        if result:
            (result, col_names) = rowify(result, cur.description)
            puzzle_list = list(
                filter(lambda puzzle: puzzle["puzzle_id"], result)) + list(
                    filter(lambda puzzle: not puzzle["puzzle_id"], result))

        puzzle_list = list(map(hidden_preview, puzzle_list))
        response = {
            "puzzles": puzzle_list,
        }

        cur.close()

        return make_response(json.jsonify(response), 200)
Пример #9
0
    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 post(self):
        ""
        response = {"message": "", "name": "error"}

        is_shareduser = False
        user = current_app.secure_cookie.get(u"user")
        if not user:
            user = user_id_from_ip(request.headers.get("X-Real-IP"))
            is_shareduser = True

        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))

        email = args.get("email", "").strip().lower()
        if len(email) > EMAIL_MAXLENGTH:
            response["message"] = "E-mail is too long."
            response["name"] = "error"
            return make_response(json.jsonify(response), 400)

        cur = db.cursor()

        result = cur.execute(fetch_query_string("user-has-player-account.sql"),
                             {
                                 "player_id": user
                             }).fetchone()
        if not result or result[0] == 0:
            cur.execute(
                fetch_query_string("init-player-account-for-user.sql"),
                {"player_id": user},
            )

        result = cur.execute(
            fetch_query_string("select-player-details-for-player-id.sql"),
            {
                "player_id": user
            },
        ).fetchall()
        if not result:
            # This shouldn't happen if user-has-player-account.sql
            response["message"] = "No player account."
            response["name"] = "error"
            cur.close()
            db.commit()
            return make_response(json.jsonify(response), 400)
        (result, col_names) = rowify(result, cur.description)
        existing_player_data = result[0]

        # Prevent shareduser changing the verified email address.
        if is_shareduser and existing_player_data["is_verifying_email"]:
            response[
                "message"] = "A player on this same network has already submitted an email address. Changing it is not allowed until the account has been claimed or the verify email token expires."
            response["name"] = "error"
            return make_response(json.jsonify(response), 400)

        if existing_player_data["email"] == email:
            response[
                "message"] = "No changes made to e-mail address ({}).  The e-mail address is stored as lowercase.".format(
                    email)
            response["name"] = "error"
        else:
            if email == "":
                cur.execute(
                    fetch_query_string("remove-player-account-email.sql"),
                    {"player_id": user},
                )
                response["message"] = "Removed e-mail from player account."
                response["name"] = "success"
            else:
                # Verify that email is unique
                result = cur.execute(
                    fetch_query_string(
                        "select-player-by-verified-email-address.sql"),
                    {
                        "email": email
                    },
                ).fetchall()
                if result:
                    response[
                        "message"] = "A player has already registered this e-mail address."
                    response["name"] = "error"
                    cur.close()
                    db.commit()
                    return make_response(json.jsonify(response), 400)

                if existing_player_data["is_verifying_email"]:
                    response[
                        "message"] = "An e-mail address is already in the process of verification for this player.  Please wait."
                    response["name"] = "error"
                    cur.close()
                    db.commit()
                    return make_response(json.jsonify(response), 400)

                cur.execute(
                    fetch_query_string("update-player-account-email.sql"),
                    {
                        "player_id": user,
                        "email": email
                    },
                )
                cur.execute(
                    fetch_query_string(
                        "update-player-account-email-verified.sql"),
                    {
                        "player_id": user,
                        "email_verified": 0,
                    },
                )

                # Send verification email (silent fail if not configured)
                token = uuid.uuid4().hex
                message = """
Please verify your e-mail address with Puzzle Massive by following the link below.

http://{DOMAIN_NAME}/chill/site/claim-player/{token}/

Complete registering your e-mail address by visiting that web page and clicking
the "verify player" button.

You can ignore this message if you didn't initiate the request.
                """.format(token=token,
                           DOMAIN_NAME=current_app.config.get("DOMAIN_NAME"))
                current_app.logger.debug(message)
                if not current_app.config.get("DEBUG", True):
                    try:
                        send_message(
                            email,
                            "Puzzle Massive - verify e-mail address",
                            message,
                            current_app.config,
                        )
                    except Exception as err:
                        current_app.logger.warning(
                            "Failed to send verification message. email: {email}\n {message}\n error: {err}"
                            .format(err=err, email=email, message=message))
                        pass

                cur.execute(
                    fetch_query_string(
                        "update-player-account-email-verify-token.sql"),
                    {
                        "player_id": user,
                        "email_verify_token": token,
                        "expire_token_timeout": "+1 hour",
                    },
                )

                response[
                    "message"] = "Updated e-mail ({}) for player account.  ".format(
                        email)
                response["name"] = "success"

        db.commit()
        cur.close()
        return make_response(json.jsonify(response), 202)
Пример #11
0
    def patch(self, puzzle_id, piece):
        """
        args:
        x
        y
        r
        """
        def _blockplayer():
            timeouts = current_app.config["BLOCKEDPLAYER_EXPIRE_TIMEOUTS"]
            blocked_count_ip_key = f"blocked:{ip}"
            expire_index = max(0,
                               redis_connection.incr(blocked_count_ip_key) - 1)
            redis_connection.expire(blocked_count_ip_key, timeouts[-1])
            timeout = timeouts[min(expire_index, len(timeouts) - 1)]
            expires = now + timeout
            blockedplayers_for_puzzle_key = "blockedplayers:{puzzle}".format(
                puzzle=puzzle)
            # Add the player to the blocked players list for the puzzle and
            # extend the expiration of the key.
            redis_connection.zadd(blockedplayers_for_puzzle_key,
                                  {user: expires})
            redis_connection.expire(blockedplayers_for_puzzle_key,
                                    timeouts[-1])

            err_msg = get_blockedplayers_err_msg(expires, expires - now)
            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=puzzle_data["puzzle_id"]),
            )
            return make_response(json.jsonify(err_msg), 429)

        ip = request.headers.get("X-Real-IP")
        validate_token = (len({"all", "valid_token"}.intersection(
            current_app.config["PUZZLE_RULES"])) > 0)
        user = None
        now = int(time.time())

        # 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))

        if len(list(args.keys())) == 0:
            err_msg = {
                "msg": "invalid args",
                "type": "invalid",
                "expires": now + 5,
                "timeout": 5,
            }
            return make_response(json.jsonify(err_msg), 400)
        # check if args are only in acceptable set
        if len(self.ACCEPTABLE_ARGS.intersection(set(args.keys()))) != len(
                list(args.keys())):
            err_msg = {
                "msg": "invalid args",
                "type": "invalid",
                "expires": now + 5,
                "timeout": 5,
            }
            return make_response(json.jsonify(err_msg), 400)
        # validate that all values are int
        for key, value in list(args.items()):
            if not isinstance(value, int):
                try:
                    args[key] = int(value)
                except ValueError:
                    err_msg = {
                        "msg": "invalid args",
                        "type": "invalid",
                        "expires": now + 5,
                        "timeout": 5,
                    }
                    return make_response(json.jsonify(err_msg), 400)
        x = args.get("x")
        y = args.get("y")
        r = args.get("r")
        snapshot_id = request.headers.get("Snap")

        # Token is to make sure puzzle is still in sync.
        # validate the token
        token = request.headers.get("Token")
        if not token:
            err_msg = {
                "msg": "Missing token",
                "type": "missing",
                "expires": now + 5,
                "timeout": 5,
            }
            return make_response(json.jsonify(err_msg), 400)

        mark = request.headers.get("Mark")
        if not mark:
            err_msg = {
                "msg": "Missing mark",
                "type": "missing",
                "expires": now + 5,
                "timeout": 5,
            }
            return make_response(json.jsonify(err_msg), 400)

        # start = time.perf_counter()
        existing_token = redis_connection.get(f"t:{mark}")
        if validate_token and existing_token:
            (m_puzzle, m_piece, m_user) = existing_token.split(":")
            user = int(m_user)
        else:
            user = current_app.secure_cookie.get("user") or user_id_from_ip(
                ip, validate_shared_user=False)
            if user is None:
                err_msg = {
                    "msg": "Please reload the page.",
                    "reason": "The player login was not found.",
                    "type": "puzzlereload",
                    "timeout": 300,
                }
                return make_response(json.jsonify(err_msg), 400)
            user = int(user)

        pzq_key = "pzq:{puzzle_id}".format(puzzle_id=puzzle_id)
        pzq_fields = [
            "puzzle",
            "table_width",
            "table_height",
            "permission",
            "pieces",
        ]
        puzzle_data = dict(
            zip(pzq_fields, redis_connection.hmget(pzq_key, pzq_fields)))
        puzzle = puzzle_data.get("puzzle")
        if puzzle is None:
            req = requests.get(
                "http://{HOSTAPI}:{PORTAPI}/internal/puzzle/{puzzle_id}/details/"
                .format(
                    HOSTAPI=current_app.config["HOSTAPI"],
                    PORTAPI=current_app.config["PORTAPI"],
                    puzzle_id=puzzle_id,
                ), )
            if req.status_code >= 400:
                err_msg = {"msg": "puzzle not available", "type": "missing"}
                return make_response(json.jsonify(err_msg), req.status_code)
            try:
                result = req.json()
            except ValueError as err:
                err_msg = {"msg": "puzzle not available", "type": "missing"}
                return make_response(json.jsonify(err_msg), 500)
            if result.get("status") not in (ACTIVE, BUGGY_UNLISTED):
                err_msg = {"msg": "puzzle not available", "type": "missing"}
                return make_response(json.jsonify(err_msg), 404)
            puzzle_data = result
            puzzle_data["puzzle"] = result["id"]

            redis_connection.hmset(
                pzq_key,
                {
                    "puzzle": puzzle_data["puzzle"],
                    "table_width": puzzle_data["table_width"],
                    "table_height": puzzle_data["table_height"],
                    "permission": puzzle_data["permission"],
                    "pieces": puzzle_data["pieces"],
                },
            )
            redis_connection.expire(pzq_key, 300)
        else:
            puzzle_data["puzzle"] = int(puzzle_data["puzzle"])
            puzzle_data["table_width"] = int(puzzle_data["table_width"])
            puzzle_data["table_height"] = int(puzzle_data["table_height"])
            puzzle_data["permission"] = int(puzzle_data["permission"])
            puzzle_data["pieces"] = int(puzzle_data["pieces"])
        puzzle = int(puzzle_data["puzzle"])
        puzzle_data["puzzle_id"] = puzzle_id

        puzzle_piece_token_key = get_puzzle_piece_token_key(puzzle, piece)
        validate_token = (len({"all", "valid_token"}.intersection(
            current_app.config["PUZZLE_RULES"])) > 0)
        if validate_token:
            token_and_mark = redis_connection.get(puzzle_piece_token_key)
            if token_and_mark:
                (valid_token, other_mark) = token_and_mark.split(":")
                # other_user = int(other_user)
                if token != valid_token:
                    err_msg = increase_ban_time(user,
                                                TOKEN_INVALID_BAN_TIME_INCR)
                    err_msg["reason"] = "Token is invalid"
                    return make_response(json.jsonify(err_msg), 409)
                if mark != other_mark:
                    err_msg = increase_ban_time(user,
                                                TOKEN_INVALID_BAN_TIME_INCR)
                    err_msg["reason"] = "Player is invalid"
                    return make_response(json.jsonify(err_msg), 409)
            else:
                err_msg = {
                    "msg": "Token has expired",
                    "type": "expiredtoken",
                    "reason": "",
                }
                return make_response(json.jsonify(err_msg), 409)

        # Expire the token since it shouldn't be used again
        if validate_token:
            redis_connection.delete(puzzle_piece_token_key)
            redis_connection.delete(f"t:{mark}")

        if (len({"all", "piece_translate_rate"}.intersection(
                current_app.config["PUZZLE_RULES"])) > 0):
            err_msg = bump_count(user)
            if err_msg.get("type") == "bannedusers":
                return make_response(json.jsonify(err_msg), 429)

        # Check if piece will be moved to within boundaries
        if x and (x < 0 or x > puzzle_data["table_width"]):
            err_msg = {
                "msg": "Piece movement out of bounds",
                "type": "invalidpiecemove",
                "expires": now + 5,
                "timeout": 5,
            }
            return make_response(json.jsonify(err_msg), 400)
        if y and (y < 0 or y > puzzle_data["table_height"]):
            err_msg = {
                "msg": "Piece movement out of bounds",
                "type": "invalidpiecemove",
                "expires": now + 5,
                "timeout": 5,
            }
            return make_response(json.jsonify(err_msg), 400)

        # Check again if piece can be moved and hasn't changed since getting token
        has_y = redis_connection.hget(
            "pc:{puzzle}:{piece}".format(puzzle=puzzle, piece=piece), "y")
        if has_y is None:
            err_msg = {"msg": "piece not available", "type": "missing"}
            return make_response(json.jsonify(err_msg), 404)

        if redis_connection.sismember(f"pcfixed:{puzzle}", piece) == 1:
            # immovable
            err_msg = {
                "msg": "piece can't be moved",
                "type": "immovable",
                "expires": now + 5,
                "timeout": 5,
            }
            return make_response(json.jsonify(err_msg), 400)

        (_, _, _, origin_x, origin_y, _) = unpack_token(token)
        redis_connection.publish(
            f"enforcer_piece_translate:{puzzle}",
            f"{user}:{piece}:{origin_x}:{origin_y}:{x}:{y}",
        )

        points_key = "points:{user}".format(user=user)
        recent_points = int(redis_connection.get(points_key) or "0")

        karma_key = init_karma_key(redis_connection, puzzle, ip,
                                   current_app.config)
        karma = int(redis_connection.get(karma_key))
        karma_change = 0
        current_app.logger.debug(
            f"user: {user} ip: {ip} karma: {karma} recent_points {recent_points}"
        )

        if (len({"all", "puzzle_open_rate"}.intersection(
                current_app.config["PUZZLE_RULES"])) > 0):
            # Decrease recent points if this is a new puzzle that user hasn't moved pieces on yet in the last hour
            pzrate_key = "pzrate:{user}:{today}".format(
                user=user, today=datetime.date.today().isoformat())
            if redis_connection.sadd(pzrate_key, puzzle) == 1:
                # New puzzle that player hasn't moved a piece on in the last hour.
                redis_connection.expire(pzrate_key, HOUR)
                if recent_points > 0:
                    redis_connection.decr(points_key)

        if (len({"all", "piece_move_rate"}.intersection(
                current_app.config["PUZZLE_RULES"])) > 0):
            # Decrease karma if piece movement rate has passed threshold
            pcrate_key = f"pcrate:{puzzle}:{user}"
            moves = redis_connection.incr(pcrate_key)
            redis_connection.expire(pcrate_key, PIECE_MOVEMENT_RATE_TIMEOUT)
            if moves > PIECE_MOVEMENT_RATE_LIMIT:
                if karma > 0:
                    karma = redis_connection.decr(karma_key)
                karma_change -= 1

        if (len({"all", "hot_piece"}.intersection(
                current_app.config["PUZZLE_RULES"])) > 0):
            # Decrease karma when moving the same piece multiple times within
            # a minute.
            hotpc_key = f"hotpc:{puzzle}:{user}:{piece}"
            recent_move_count = redis_connection.incr(hotpc_key)
            redis_connection.expire(hotpc_key, HOT_PIECE_MOVEMENT_RATE_TIMEOUT)

            if recent_move_count > MOVES_BEFORE_PENALTY:
                if karma > 0:
                    karma = redis_connection.decr(karma_key)
                karma_change -= 1

        if (len({"all", "hot_spot"}.intersection(
                current_app.config["PUZZLE_RULES"])) > 0):
            # Decrease the karma for the player if the piece is in a hotspot.
            hotspot_piece_key = f"hotspot:{puzzle}:{user}:{piece}"
            hotspot_count = int(redis_connection.get(hotspot_piece_key) or "0")
            if hotspot_count > HOTSPOT_LIMIT:
                if karma > 0:
                    karma = redis_connection.decr(karma_key)
                karma_change -= 1

        if karma_change < 0:
            # Decrease recent points for a piece move that decreased karma
            if recent_points > 0 and karma_change < 0:
                recent_points = redis_connection.decr(points_key)
            if karma + recent_points <= 0:
                return _blockplayer()

        piece_move_timeout = current_app.config["PIECE_MOVE_TIMEOUT"]

        # Use a custom built and managed queue to prevent multiple processes
        # from running the attempt_piece_movement concurrently on the same
        # puzzle.
        pzq_current_key = "pzq_current:{puzzle}".format(puzzle=puzzle)
        pzq_next_key = "pzq_next:{puzzle}".format(puzzle=puzzle)
        # The attempt_piece_movement bumps the pzq_current by 1
        pzq_next = redis_connection.incr(pzq_next_key, amount=1)
        # Set the expire in case it fails to reach expire in attempt_piece_movement.
        redis_connection.expire(pzq_current_key, piece_move_timeout + 2)
        redis_connection.expire(pzq_next_key, piece_move_timeout + 2)

        attempt_count = 0
        attempt_timestamp = time.time()
        timeout = attempt_timestamp + piece_move_timeout
        while attempt_timestamp < timeout:
            pzq_current = int(redis_connection.get(pzq_current_key) or "0")
            if pzq_current == pzq_next - 1:
                try:
                    snapshot_msg = None
                    snapshot_karma_change = False
                    if snapshot_id:
                        snapshot_key = f"snap:{snapshot_id}"
                        snapshot = redis_connection.get(snapshot_key)
                        if snapshot:
                            snapshot_list = snapshot.split(":")
                            snapshot_pzq = int(snapshot_list.pop(0))
                            if snapshot_pzq != pzq_current:
                                # Check if any adjacent pieces are within range of x, y, r
                                # Within that list check if any have moved
                                # With the first one that has moved that was within range attempt piece movement on that by using adjusted x, y, r
                                snaps = list(
                                    map(lambda x: x.split("_"), snapshot_list))
                                adjacent_piece_ids = list(
                                    map(lambda x: int(x[0]), snaps))
                                adjacent_piece_props_snaps = list(
                                    map(lambda x: x[1:], snaps))
                                property_list = [
                                    "x",
                                    "y",
                                    "r",
                                    # "g"
                                ]
                                results = []
                                with redis_connection.pipeline(
                                        transaction=True) as pipe:
                                    for adjacent_piece_id in adjacent_piece_ids:
                                        pc_puzzle_adjacent_piece_key = (
                                            f"pc:{puzzle}:{adjacent_piece_id}")
                                        pipe.hmget(
                                            pc_puzzle_adjacent_piece_key,
                                            property_list,
                                        )
                                    results = pipe.execute()
                                for (
                                        a_id,
                                        snapshot_adjacent,
                                        updated_adjacent,
                                ) in zip(
                                        adjacent_piece_ids,
                                        adjacent_piece_props_snaps,
                                        results,
                                ):
                                    updated_adjacent = list(
                                        map(
                                            lambda x: x
                                            if isinstance(x, str) else "",
                                            updated_adjacent,
                                        ))
                                    adjacent_offset = snapshot_adjacent.pop()
                                    if (snapshot_adjacent != updated_adjacent
                                        ) and adjacent_offset:
                                        (a_offset_x, a_offset_y) = map(
                                            int, adjacent_offset.split(","))
                                        (a_snap_x, a_snap_y) = map(
                                            int, snapshot_adjacent[:2])
                                        # Check if the x,y is within range of the adjacent piece that has moved
                                        piece_join_tolerance = current_app.config[
                                            "PIECE_JOIN_TOLERANCE"]
                                        if (abs((a_snap_x + a_offset_x) - x) <=
                                                piece_join_tolerance and
                                                abs((a_snap_y + a_offset_y) -
                                                    y) <=
                                                piece_join_tolerance):
                                            (a_moved_x, a_moved_y) = map(
                                                int, updated_adjacent[:2])
                                            # Decrease pzq_current since it is moving an extra piece out of turn
                                            redis_connection.decr(
                                                pzq_current_key, amount=1)
                                            (
                                                snapshot_msg,
                                                snapshot_karma_change,
                                            ) = attempt_piece_movement(
                                                ip,
                                                user,
                                                puzzle_data,
                                                piece,
                                                a_moved_x + a_offset_x,
                                                a_moved_y + a_offset_y,
                                                r,
                                                karma_change,
                                                karma,
                                            )
                                            break
                except:
                    pzq_current = int(
                        redis_connection.get(pzq_current_key) or "0")
                    if pzq_current == pzq_next - 1:
                        # skip this piece move attempt
                        redis_connection.incr(pzq_current_key, amount=1)
                    current_app.logger.warning(
                        "results123 other error {}".format(sys.exc_info()[0]))
                    raise

                (msg, karma_change) = attempt_piece_movement(
                    ip,
                    user,
                    puzzle_data,
                    piece,
                    x,
                    y,
                    r,
                    karma_change or snapshot_karma_change,
                    karma,
                )
                if isinstance(snapshot_msg, str) and isinstance(msg, str):
                    msg = snapshot_msg + msg
                break
            current_app.logger.debug(f"pzq_current is {pzq_current}")
            attempt_timestamp = time.time()
            attempt_count = attempt_count + 1
            # TODO: The sleep time should be set based on an average time it
            # takes to process piece movements.
            time.sleep(0.02)

            # Decrease karma here to potentially block a player that
            # continually tries to move pieces when a puzzle is too active.
            if (len({"all", "too_active"}.intersection(
                    current_app.config["PUZZLE_RULES"])) > 0) and karma > 0:
                karma = redis_connection.decr(karma_key)
                karma_change -= 1
        current_app.logger.debug(
            f"Puzzle ({puzzle}) piece move attempts: {attempt_count}")
        if attempt_timestamp >= timeout:
            current_app.logger.warn(
                f"Puzzle {puzzle} is too active. Attempt piece move timed out after trying {attempt_count} times."
            )
            err_msg = {
                "msg": "Piece movement timed out.",
                "type": "error",
                "reason": "Puzzle is too active",
                "timeout": piece_move_timeout,
            }
            return make_response(
                json.jsonify(err_msg),
                503,
            )

        # Check msg for error or if piece can't be moved
        if not isinstance(msg, str):
            if isinstance(msg, dict):
                return make_response(json.jsonify(msg), 400)
            else:
                current_app.logger.warning("Unknown error: {}".format(msg))
                return make_response(
                    json.jsonify({
                        "msg": msg,
                        "type": "error",
                        "timeout": 3
                    }), 500)

        # publish just the bit movement so it matches what this player did
        bitmsg = formatBitMovementString(user, x, y)
        sse.publish(
            bitmsg,
            type="move",
            channel="puzzle:{puzzle_id}".format(puzzle_id=puzzle_id),
        )

        if karma_change < 0:
            if karma + recent_points <= 0:
                return _blockplayer()

        # end = time.perf_counter()
        # current_app.logger.debug("PuzzlePiecesMovePublishView {}".format(end - start))
        return make_response("", 204)
Пример #12
0
    def get(self, puzzle_id, piece):
        ip = request.headers.get("X-Real-IP")
        user = current_app.secure_cookie.get("user") or user_id_from_ip(
            ip, validate_shared_user=False)
        if user is None:
            err_msg = {
                "msg": "Please reload the page.",
                "reason": "The player login was not found.",
                "type": "puzzlereload",
                "timeout": 300,
            }
            response = make_response(json.jsonify(err_msg), 400)
            expires = datetime.datetime.utcnow() - datetime.timedelta(days=365)
            current_app.secure_cookie.set("user",
                                          "",
                                          response,
                                          expires=expires)
            current_app.secure_cookie.set("shareduser",
                                          "",
                                          response,
                                          expires=expires)
            return response

        user = int(user)
        mark = request.args.get("mark")
        if not isinstance(mark, str) or len(mark) != 10:
            return make_response(
                json.jsonify({
                    "msg": "invalid args",
                    "type": "invalid",
                }),
                400,
            )
        now = int(time.time())

        # start = time.perf_counter()
        pzq_key = "pzq:{puzzle_id}".format(puzzle_id=puzzle_id)
        puzzle = redis_connection.hget(pzq_key, "puzzle")
        if not puzzle:
            current_app.logger.debug("no puzzle; fetch puzzle")
            r = requests.get(
                "http://{HOSTAPI}:{PORTAPI}/internal/puzzle/{puzzle_id}/details/"
                .format(
                    HOSTAPI=current_app.config["HOSTAPI"],
                    PORTAPI=current_app.config["PORTAPI"],
                    puzzle_id=puzzle_id,
                ), )
            if r.status_code >= 400:
                # 400 if puzzle does not exist
                err_msg = {
                    "msg":
                    "puzzle is not ready at this time. Please reload the page.",
                    "type": "puzzleimmutable",
                }
                return make_response(json.jsonify(err_msg), r.status_code)
            try:
                result = r.json()
            except ValueError as err:
                err_msg = {
                    "msg":
                    "puzzle is not ready at this time. Please reload the page.",
                    "type": "puzzleimmutable",
                }
                return make_response(json.jsonify(err_msg), 500)
            if result.get("status") not in (ACTIVE, BUGGY_UNLISTED):
                err_msg = {
                    "msg":
                    "puzzle is not ready at this time. Please reload the page.",
                    "type": "puzzleimmutable",
                }
                return make_response(json.jsonify(err_msg), 400)
            puzzle = result["id"]

        puzzle = int(puzzle)
        pc_puzzle_piece_key = "pc:{puzzle}:{piece}".format(puzzle=puzzle,
                                                           piece=piece)
        piece_properties = _int_piece_properties(
            redis_connection.hgetall(pc_puzzle_piece_key))
        pcfixed = set(redis_connection.smembers(f"pcfixed:{puzzle}"))

        if piece_properties.get("y") is None:
            # 400 if puzzle does not exist or piece is not found
            # Only puzzles in ACTIVE state can be mutated
            err_msg = {
                "msg":
                "puzzle pieces can't be moved at this time. Please reload the page.",
                "type": "puzzleimmutable",
            }
            return make_response(json.jsonify(err_msg), 400)

        if piece in pcfixed:
            # immovable
            err_msg = {
                "msg": "piece can't be moved",
                "type": "immovable",
                "expires": now + 5,
                "timeout": 5,
            }
            return make_response(json.jsonify(err_msg), 400)

        # TODO: remove old entries in blockedplayers:{puzzle}
        blockedplayers_for_puzzle_key = "blockedplayers:{puzzle}".format(
            puzzle=puzzle)
        blockedplayers_expires = redis_connection.zscore(
            blockedplayers_for_puzzle_key, user)
        if blockedplayers_expires and blockedplayers_expires > now:
            err_msg = get_blockedplayers_err_msg(blockedplayers_expires,
                                                 blockedplayers_expires - now)
            return make_response(json.jsonify(err_msg), 429)

        token = pack_token(nanoid.generate(size=8), puzzle, user, piece,
                           piece_properties)
        redis_connection.publish(
            f"enforcer_token_request:{puzzle}",
            token,
        )

        def move_bit_icon_to_piece(x, y):
            # Claim the piece by showing the bit icon next to it.
            msg = formatBitMovementString(user, x, y)
            sse.publish(
                msg,
                type="move",
                channel="puzzle:{puzzle_id}".format(puzzle_id=puzzle_id),
            )

        # Snapshot of adjacent pieces at time of token request
        snapshot_id = None
        adjacent_pieces_list = _get_adjacent_pieces_list(piece_properties)
        adjacent_property_list = ["x", "y", "r", "g", str(piece)]
        pzq_current_key = "pzq_current:{puzzle}".format(puzzle=puzzle)
        results = []
        with redis_connection.pipeline(transaction=False) as pipe:
            for adjacent_piece in adjacent_pieces_list:
                pc_puzzle_adjacent_piece_key = "pc:{puzzle}:{adjacent_piece}".format(
                    puzzle=puzzle, adjacent_piece=adjacent_piece)
                pipe.hmget(pc_puzzle_adjacent_piece_key,
                           adjacent_property_list)
            pipe.get(pzq_current_key)
            results = pipe.execute()
        pzq_current = "0"
        if not isinstance(results[-1], list):
            pzq_current = results.pop() or pzq_current
        adjacent_properties = dict(
            zip(
                adjacent_pieces_list,
                map(lambda x: dict(zip(adjacent_property_list, x)), results),
            ))
        snapshot = []
        for a_piece, a_props in adjacent_properties.items():
            # skip any that are immovable
            if a_piece in pcfixed:
                continue
            # skip any that are in the same group
            if a_props.get("g") is not None and a_props.get(
                    "g") == piece_properties.get("g"):
                continue
            # skip any that don't have offsets (adjacent edge piece)
            if not a_props.get(str(piece)):
                continue
            if a_props.get("g") is None:
                a_props["g"] = ""
            snapshot.append("_".join([
                str(a_piece),
                a_props.get("x", ""),
                a_props.get("y", ""),
                a_props.get("r", ""),
                a_props.get(str(piece), ""),
                # a_props.get("g")
            ]))

        if len(snapshot):
            snapshot_id = nanoid.generate(size=8)
            snapshot_key = f"snap:{snapshot_id}"
            snapshot.insert(0, pzq_current)
            redis_connection.set(snapshot_key, ":".join(snapshot))
            redis_connection.expire(
                snapshot_key,
                current_app.config["MAX_PAUSE_PIECES_TIMEOUT"] +
                (current_app.config["PIECE_MOVE_TIMEOUT"] + 2),
            )

        validate_token = (len({"all", "valid_token"}.intersection(
            current_app.config["PUZZLE_RULES"])) > 0)

        TOKEN_LOCK_TIMEOUT = current_app.config["TOKEN_LOCK_TIMEOUT"]
        TOKEN_EXPIRE_TIMEOUT = current_app.config["TOKEN_EXPIRE_TIMEOUT"]
        if not validate_token:

            move_bit_icon_to_piece(piece_properties.get("x"),
                                   piece_properties.get("y"))
            response = {
                "token": token,
                "lock": now + TOKEN_LOCK_TIMEOUT,
                "expires": now + TOKEN_EXPIRE_TIMEOUT,
            }
            if snapshot_id:
                response["snap"] = snapshot_id
            return make_response(json.jsonify(response), 200)

        # Check if user already has a token for this puzzle. This would mean
        # that the user tried moving another piece before the locked piece
        # finished moving.
        existing_token = redis_connection.get(f"t:{mark}")
        if existing_token:
            # Temporary ban the player when clicking a piece and not
            # dropping it before clicking another piece.
            # Ban the user for a few seconds
            err_msg = increase_ban_time(user, TOKEN_LOCK_TIMEOUT)
            err_msg[
                "reason"] = "Concurrent piece movements on this puzzle from the same player are not allowed."
            return make_response(json.jsonify(err_msg), 429)

        piece_token_queue_key = get_puzzle_piece_token_queue_key(puzzle, piece)
        with redis_connection.pipeline(transaction=False) as pipe:
            pipe.zrank(piece_token_queue_key, mark)
            pipe.expire(piece_token_queue_key, TOKEN_LOCK_TIMEOUT + 5)
            (queue_rank, _) = pipe.execute()

        if queue_rank is None:
            # Append this player to a queue for getting the next token. This
            # will prevent the player with the lock from continually locking the
            # same piece.
            with redis_connection.pipeline(transaction=False) as pipe:
                pipe.zadd(piece_token_queue_key, {mark: now})
                pipe.zrank(piece_token_queue_key, mark)
                (_, queue_rank) = pipe.execute()

        # Check if token on piece is in a queue and if the player requesting it
        # is the player that is next. Show an error message if not.
        if queue_rank > 0:
            err_msg = {
                "msg": "Another player is waiting to move this piece",
                "type": "piecequeue",
                "reason": "Piece queue {}".format(queue_rank),
                "expires": now + TOKEN_LOCK_TIMEOUT,
                "timeout": TOKEN_LOCK_TIMEOUT,
            }
            return make_response(json.jsonify(err_msg), 409)

        # Check if token on piece is still owned by another user
        puzzle_piece_token_key = get_puzzle_piece_token_key(puzzle, piece)
        existing_token_and_mark = redis_connection.get(puzzle_piece_token_key)
        if existing_token_and_mark:
            (other_token, other_mark) = existing_token_and_mark.split(":")
            puzzle_and_piece_and_user = redis_connection.get(f"t:{other_mark}")
            # Check if there is a lock on this piece by other user
            if puzzle_and_piece_and_user:
                (
                    other_puzzle,
                    other_piece,
                    other_user,
                ) = puzzle_and_piece_and_user.split(":")
                other_puzzle = int(other_puzzle)
                other_piece = int(other_piece)
                other_user = int(other_user)
                if other_puzzle == puzzle and other_piece == piece:
                    # Other user has a lock on this piece
                    err_msg = {
                        "msg": "Another player is moving this piece",
                        "type": "piecelock",
                        "reason": "Piece locked",
                    }
                    return make_response(json.jsonify(err_msg), 409)

        # This piece is up for grabs since it has been more then 5 seconds since
        # another player has grabbed it.
        with redis_connection.pipeline(transaction=False) as pipe:
            # Remove player from the piece token queue
            pipe.zrem(piece_token_queue_key, mark)

            pipe.set(
                puzzle_piece_token_key,
                f"{token}:{mark}",
                ex=TOKEN_EXPIRE_TIMEOUT,
            )
            pipe.set(
                f"t:{mark}",
                f"{puzzle}:{piece}:{user}",
                ex=TOKEN_LOCK_TIMEOUT,
            )
            pipe.execute()

        move_bit_icon_to_piece(piece_properties.get("x"),
                               piece_properties.get("y"))

        response = {
            "token": token,
            "lock": now + TOKEN_LOCK_TIMEOUT,
            "expires": now + TOKEN_EXPIRE_TIMEOUT,
        }
        if snapshot_id:
            response["snap"] = snapshot_id
        # end = time.perf_counter()
        # current_app.logger.debug("PuzzlePieceTokenView {}".format(end - start))
        return make_response(json.jsonify(response), 200)
Пример #13
0
    def post(self):
        "Route is protected by basic auth in nginx"
        args = {}
        batch = []

        user = int(
            current_app.secure_cookie.get("user")
            or user_id_from_ip(request.headers.get("X-Real-IP")))
        cur = db.cursor()
        result = cur.execute(
            fetch_query_string("select-user-details-by-id.sql"),
            {
                "id": user
            },
        ).fetchone()
        if not result:
            # Safe guard against when the user is not in the database. Usually
            # happens in development environments when switching and dropping
            # databases happens often.
            user = ANONYMOUS_USER_ID
        cur.close()

        if request.form:
            args.update(request.form.to_dict(flat=True))
            labels = [
                "unlisted", "hidden_preview", "link", "bg_color", "pieces"
            ]
            batch = list(
                map(
                    lambda x: dict(zip(labels, x)),
                    list(zip(*list(map(request.form.getlist, labels)))),
                ))

        for item in batch:
            try:
                pieces = int(
                    item.get("pieces",
                             current_app.config["MINIMUM_PIECE_COUNT"]))
            except ValueError as err:
                abort(400)
            if (not current_app.config["MINIMUM_PIECE_COUNT"] <= pieces <=
                    current_app.config["MAXIMUM_PIECE_COUNT"]):
                abort(400)
            bg_color = check_bg_color(item.get("bg_color", "#808080")[:50])
            permission = PUBLIC if item.get("unlisted",
                                            "false") == "false" else PRIVATE
            description = ""
            link = url_fix(item.get("link", ""))[:1000]
            secret_message = escape(item.get("secret_message", ""))[:1000]
            features = set()
            if item.get("hidden_preview", "false") != "false":
                features.add("hidden-preview")

            puzzle_id = submit_puzzle(
                pieces,
                bg_color,
                user,
                permission,
                description,
                link,
                upload_file=None,
                secret_message=secret_message,
                features=features,
            )

        return redirect("/chill/site/player-puzzle-list/", code=303)
Пример #14
0
    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)
Пример #15
0
    def patch(self, puzzle_id):
        "Pong. Determine the latency for this player."
        response = {"message": "", "name": "", "data": {"latency": 0}}

        args = {}
        xhr_data = request.get_json()
        if xhr_data:
            args.update(xhr_data)
        if request.form:
            args.update(request.form.to_dict(flat=True))

        token = args.get("token")
        if token is None:
            response["message"] = "No token"
            response["name"] = "error"
            return make_response(json.jsonify(response), 400)

        user = current_app.secure_cookie.get(u"user") or user_id_from_ip(
            request.headers.get("X-Real-IP"),
            skip_generate=True,
            validate_shared_user=False,
        )

        if user is None:
            response["message"] = "Player not currently logged in."
            response["name"] = "error"
            return make_response(json.jsonify(response), 400)

        user = int(user)

        cur = db.cursor()

        # Validate the puzzle_id
        result = cur.execute(
            fetch_query_string("select-id-status-from-puzzle-by-puzzle_id.sql"),
            {"puzzle_id": puzzle_id},
        ).fetchall()
        if not result:
            response["message"] = "Puzzle not available"
            response["name"] = "invalid"
            cur.close()
            return make_response(json.jsonify(response), 400)
        else:
            (result, col_names) = rowify(result, cur.description)
            puzzle = result[0].get("id")
            status = result[0].get("status")
            if status not in (
                ACTIVE,
                IN_QUEUE,
                COMPLETED,
                FROZEN,
                BUGGY_UNLISTED,
                NEEDS_MODERATION,
                REBUILD,
                IN_RENDER_QUEUE,
                RENDERING,
                RENDERING_FAILED,
                MAINTENANCE,
            ):
                response["message"] = "Puzzle no longer valid"
                response["name"] = "invalid"
                cur.close()
                sse.publish(
                    "Puzzle no longer valid",
                    type="invalid",
                    channel="puzzle:{puzzle_id}".format(puzzle_id=puzzle_id),
                )
                return make_response(json.jsonify(response), 200)

        cur.close()
        # Determine latency for the player and record timestamp in sorted set.
        pingtoken_key = get_pingtoken_key(puzzle, user, token)
        ping_start = redis_connection.get(pingtoken_key)
        redis_connection.delete(pingtoken_key)
        ping_end = int(time.time() * 1000)
        if not ping_start:
            response["message"] = "Ignoring error when determining latency."
            response["name"] = "ignored"
            return make_response(json.jsonify(response), 200)
        ping_start = int(ping_start)
        ping_key = get_ping_key(puzzle)
        redis_connection.zadd(ping_key, {user: ping_end})
        redis_connection.expire(ping_key, PING_EXPIRE)

        latency = ping_end - ping_start

        # Record the latency for the player
        redis_connection.lpush(
            "latency",
            "{user}:{timestamp}:{latency}".format(
                user=user, timestamp=ping_end, latency=latency
            ),
        )
        # Keep only the last 1000 entries to latency
        redis_connection.ltrim("latency", 0, 999)

        response["message"] = "Latency"
        response["data"]["latency"] = latency
        response["name"] = "success"
        response = make_response(json.jsonify(response), 200)
        return response
Пример #16
0
    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
Пример #17
0
    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)
Пример #18
0
    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))
Пример #19
0
    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 (not current_app.config["MINIMUM_PIECE_COUNT"] <= pieces <=
                current_app.config["MAXIMUM_PIECE_COUNT"]):
            abort(400)

        bg_color = check_bg_color(args.get("bg_color", "#808080")[:50])

        user = int(
            current_app.secure_cookie.get(u"user")
            or user_id_from_ip(request.headers.get("X-Real-IP")))

        permission = int(args.get("permission", PUBLIC))
        if permission not in (PUBLIC, PRIVATE):
            permission = PUBLIC

        description = escape(args.get("description", "").strip())[:1000]

        # Check secret_message
        secret_message = escape(args.get("secret_message", ""))[:1000]

        # Check link and validate
        link = url_fix(args.get("link", "").strip())[:100]

        if not link and not description:
            abort(400)

        puzzle_id = uuid.uuid1().hex

        features = set(args.get("features", []))

        cur = db.cursor()
        d = {
            "puzzle_id": puzzle_id,
            "pieces": pieces,
            "link": link,
            "description": description,
            "bg_color": bg_color,
            "owner": user,
            "status": SUGGESTED,
            "permission": permission,
        }
        cur.execute(
            """insert into Puzzle (
        puzzle_id,
        pieces,
        link,
        description,
        bg_color,
        owner,
        status,
        permission) values
        (:puzzle_id,
        :pieces,
        :link,
        :description,
        :bg_color,
        :owner,
        :status,
        :permission);
        """,
            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"]

        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()

        # Send a notification email (silent fail if not configured)
        message = """
http://{DOMAIN_NAME}/chill/site/admin/puzzle/suggested/{puzzle_id}/

pieces: {pieces}
bg_color: {bg_color}
owner: {owner}

link: {link}
description: {description}
        """.format(DOMAIN_NAME=current_app.config.get("DOMAIN_NAME"), **d)
        current_app.logger.debug(message)
        if not current_app.config.get("DEBUG", True):
            try:
                send_message(
                    current_app.config.get("EMAIL_MODERATOR"),
                    "Suggested Image",
                    message,
                    current_app.config,
                )
            except Exception as err:
                current_app.logger.warning(
                    "Failed to send notification message for suggested image. email: {email}\n {message}\n error: {err}"
                    .format(
                        err=err,
                        email=current_app.config.get("EMAIL_MODERATOR"),
                        message=message,
                    ))
                pass

        # Redirect to a thank you page (not revealing the puzzle_id)
        return redirect("/chill/site/suggested-puzzle-thank-you/", code=303)
Пример #20
0
    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 post(self):
        ""
        response = {"message": "", "name": "error"}

        user = current_app.secure_cookie.get(u"user")
        if user:
            response[
                "message"] = "User currently logged in.  No need to reset the login by e-mail."
            response["name"] = "error"
            return make_response(json.jsonify(response), 400)

        user = user_id_from_ip(request.headers.get("X-Real-IP"))
        if user == None:
            response["message"] = "Shared user not currently logged in."
            response["name"] = "error"
            return make_response(json.jsonify(response), 400)

        args = {}
        if request.form:
            args.update(request.form.to_dict(flat=True))

        email = args.get("email", "").strip().lower()
        if len(email) > EMAIL_MAXLENGTH:
            response["message"] = "E-mail is too long."
            response["name"] = "error"
            return make_response(json.jsonify(response), 400)

        cur = db.cursor()

        # Get user by their verified email address
        result = cur.execute(
            fetch_query_string("get-user-by-verified-email-address.sql"),
            {
                "email": email
            },
        ).fetchone()
        if not result or result[0] == 0:
            response[
                "message"] = "Sorry, that e-mail address has not been verified."
            response["name"] = "error"
            cur.close()
            db.commit()
            return make_response(json.jsonify(response), 400)
        else:
            user = result[0]

        result = cur.execute(
            fetch_query_string("select-player-details-for-player-id.sql"),
            {
                "player_id": user
            },
        ).fetchall()
        if not result:
            # This shouldn't happen if user-has-player-account.sql
            response["message"] = "No player account."
            response["name"] = "error"
            cur.close()
            db.commit()
            return make_response(json.jsonify(response), 400)
        (result, col_names) = rowify(result, cur.description)
        existing_player_data = result[0]

        if existing_player_data["has_active_reset_login_token"]:
            response[
                "message"] = "Please check your e-mail for a reset login link.  The reset login link that was sent earlier has not expired."
            response["name"] = "error"
            cur.close()
            db.commit()
            return make_response(json.jsonify(response), 400)

        # Send a link to reset the login (silent fail if not configured)
        token = uuid.uuid4().hex
        message = """
The link below is only valid for a short time.  Use it to reset your login link
for Puzzle Massive.

http://{DOMAIN_NAME}/chill/site/reset-login/{token}/

After visiting that web page and clicking the reset login button; a new login
link will be created. Any older ones will no longer be valid.

You can ignore this message if you didn't initiate the request.
        """.format(token=token,
                   DOMAIN_NAME=current_app.config.get("DOMAIN_NAME"))
        current_app.logger.debug(message)
        email_sent = False
        if not current_app.config.get("DEBUG", True):
            try:
                send_message(email, "Puzzle Massive - reset login", message,
                             current_app.config)
                email_sent = True
            except Exception as err:
                current_app.logger.warning(
                    "Failed to send reset login message. email: {email}\n {message}\n error: {err}"
                    .format(err=err, email=email, message=message))
                email_sent = False

        cur.execute(
            fetch_query_string("update-player-account-login-token.sql"),
            {
                "player_id": user,
                "reset_login_token": token,
                "expire_token_timeout": "+1 day",
            },
        )

        if current_app.config.get("DEBUG", True):
            response[
                "message"] = "A reset login link has been sent to your e-mail. DEBUG is True, so did not really send the email. Check the logs for the login link."
            response["name"] = "success"
        elif not email_sent:
            response[
                "message"] = "Failed to send reset login link to your e-mail.".format(
                    email)
            response["name"] = "error"
        else:
            response[
                "message"] = "A reset login link has been sent to your e-mail.".format(
                    email)
            response["name"] = "success"

        db.commit()
        cur.close()
        return make_response(json.jsonify(response), 202)
Пример #22
0
    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)
Пример #23
0
    def patch(self, puzzle_id):
        "Pong. Determine the latency for this player."
        response = {"message": "", "name": "", "data": {"latency": 0}}

        args = {}
        xhr_data = request.get_json()
        if xhr_data:
            args.update(xhr_data)
        if request.form:
            args.update(request.form.to_dict(flat=True))

        token = args.get("token")
        if token == None:
            response["message"] = "No token"
            response["name"] = "error"
            return make_response(json.jsonify(response), 400)

        user = current_app.secure_cookie.get(u"user") or user_id_from_ip(
            request.headers.get("X-Real-IP"), skip_generate=True)

        if user == None:
            response["message"] = "Player not currently logged in."
            response["name"] = "error"
            return make_response(json.jsonify(response), 400)

        user = int(user)

        cur = db.cursor()

        # Validate the puzzle_id
        result = cur.execute(
            fetch_query_string("select_viewable_puzzle_id.sql"),
            {
                "puzzle_id": puzzle_id
            },
        ).fetchall()
        if not result:
            response["message"] = "Invalid puzzle id."
            response["name"] = "error"
            cur.close()
            db.commit()
            return make_response(json.jsonify(response), 400)
        else:
            (result, col_names) = rowify(result, cur.description)
            puzzle = result[0].get("puzzle")
            status = result[0].get("status")
            if status != ACTIVE:
                response["message"] = "Puzzle not active"
                response["name"] = "invalid"
                cur.close()
                db.commit()
                return make_response(json.jsonify(response), 200)

        # Determine latency for the player and record timestamp in sorted set.
        pingtoken_key = get_pingtoken_key(puzzle, user, token)
        ping_start = redis_connection.get(pingtoken_key)
        redis_connection.delete(pingtoken_key)
        ping_end = int(time.time() * 1000)
        if not ping_start:
            response["message"] = "Ignoring error when determining latency."
            response["name"] = "ignored"
            return make_response(json.jsonify(response), 200)
        ping_start = int(ping_start)
        ping_key = get_ping_key(puzzle)
        redis_connection.zadd(ping_key, {user: ping_end})
        redis_connection.expire(ping_key, PING_EXPIRE)

        latency = ping_end - ping_start

        # Record the latency for the player
        redis_connection.lpush(
            "latency",
            "{user}:{timestamp}:{latency}".format(user=user,
                                                  timestamp=ping_end,
                                                  latency=latency),
        )
        # Keep only the last 1000 entries to latency
        redis_connection.ltrim("latency", 0, 999)

        response["message"] = "Latency"
        response["data"]["latency"] = latency
        response["name"] = "success"
        response = make_response(json.jsonify(response), 200)
        return response
Пример #24
0
    def post(self):
        args = {}
        if request.form:
            args.update(request.form.to_dict(flat=True))
            args["features"] = set(request.form.getlist("features"))

        # Only allow valid contributor
        if args.get("contributor",
                    None) != current_app.config.get("NEW_PUZZLE_CONTRIB"):
            abort(403)

        # Check pieces arg
        try:
            pieces = int(
                args.get("pieces", current_app.config["MINIMUM_PIECE_COUNT"]))
        except ValueError as err:
            abort(400)
        if (not current_app.config["MINIMUM_PIECE_COUNT"] <= pieces <=
                current_app.config["MAXIMUM_PIECE_COUNT"]):
            abort(400)

        bg_color = check_bg_color(args.get("bg_color", "#808080")[:50])

        user = int(
            current_app.secure_cookie.get("user")
            or user_id_from_ip(request.headers.get("X-Real-IP")))
        cur = db.cursor()
        result = cur.execute(
            fetch_query_string("select-user-details-by-id.sql"),
            {
                "id": user
            },
        ).fetchone()
        if not result:
            # Safe guard against when the user is not in the database. Usually
            # happens in development environments when switching and dropping
            # databases happens often.
            user = ANONYMOUS_USER_ID
        cur.close()

        permission = int(args.get("permission", PUBLIC))
        if permission not in (PUBLIC, PRIVATE):
            permission = PUBLIC

        description = escape(args.get("description", ""))[:1000]

        # Check secret_message
        secret_message = escape(args.get("secret_message", ""))[:1000]

        # Check link and validate
        link = url_fix(args.get("link", ""))[:1000]

        upload_file = request.files.get("upload_file", None)

        features = set(args.get("features", []))

        puzzle_id = submit_puzzle(
            pieces,
            bg_color,
            user,
            permission,
            description,
            link,
            upload_file,
            secret_message=secret_message,
            features=features,
        )

        # TODO AUTO_APPROVE_PUZZLES only works for non Unsplash photos at the moment.
        if current_app.config["AUTO_APPROVE_PUZZLES"] and not re.search(
                unsplash_url_regex, link):
            cur = db.cursor()
            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]
            cur.close()
            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",
                )

        return redirect("/chill/site/front/{0}/".format(puzzle_id), code=303)
Пример #25
0
    def post(self, puzzle_id):
        "Ping and record the time in milliseconds for this player."
        now_ms = int(time.time() * 1000)
        response = {"message": "", "name": ""}

        user = current_app.secure_cookie.get(u"user") or user_id_from_ip(
            request.headers.get("X-Real-IP"),
            skip_generate=True,
            validate_shared_user=False,
        )

        if user is None:
            response["message"] = "Player not currently logged in."
            response["name"] = "error"
            return make_response(json.jsonify(response), 400)

        user = int(user)

        cur = db.cursor()

        # Validate the puzzle_id
        result = cur.execute(
            fetch_query_string("select-id-status-from-puzzle-by-puzzle_id.sql"),
            {"puzzle_id": puzzle_id},
        ).fetchall()
        if not result:
            response["message"] = "Puzzle not available"
            response["name"] = "invalid"
            cur.close()
            return make_response(json.jsonify(response), 400)
        else:
            (result, col_names) = rowify(result, cur.description)
            puzzle = result[0].get("id")
            status = result[0].get("status")
            if status not in (
                ACTIVE,
                IN_QUEUE,
                COMPLETED,
                FROZEN,
                BUGGY_UNLISTED,
                NEEDS_MODERATION,
                REBUILD,
                IN_RENDER_QUEUE,
                RENDERING,
                RENDERING_FAILED,
                MAINTENANCE,
            ):
                response["message"] = "Puzzle no longer valid"
                response["name"] = "invalid"
                cur.close()
                sse.publish(
                    "Puzzle no longer valid",
                    type="invalid",
                    channel="puzzle:{puzzle_id}".format(puzzle_id=puzzle_id),
                )
                return make_response(json.jsonify(response), 200)

        cur.close()
        # publish to the puzzle channel the ping with the user id.  This will
        # allow that player to determine their latency.
        token = uuid.uuid4().hex[:4]
        pingtoken_key = get_pingtoken_key(puzzle, user, token)
        redis_connection.setex(pingtoken_key, 60, now_ms)

        current_app.logger.debug("publish ping {puzzle_id}".format(puzzle_id=puzzle_id))
        sse.publish(
            "{user}:{token}".format(user=user, token=token),
            type="ping",
            channel="puzzle:{puzzle_id}".format(puzzle_id=puzzle_id),
        )
        response["message"] = "ping accepted"
        response["name"] = "accepted"
        response = make_response(json.jsonify(response), 202)
        return response
Пример #26
0
    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))