Beispiel #1
0
def attempt_piece_movement(ip, user, puzzleData, piece, x, y, r, karma_change,
                           karma):
    try:
        (msg, karma_change) = translate(ip, user, puzzleData, piece, x, y, r,
                                        karma_change, karma)
    except (PieceMutateError, WatchError):
        current_app.logger.debug(sys.exc_info()[0])
        current_app.logger.warning("piece mutate error")
        err_msg = {
            "msg": "Try again",
            "type": "piecegrouperror",
            "reason": "Conflict with piece group",
            "timeout": 3,
        }
        return (err_msg, 0)
    except:
        current_app.logger.warning("other error {}".format(sys.exc_info()[0]))
        raise

    finally:
        current_app.logger.debug("bump pzq_current")
        pzq_current_key = "pzq_current:{puzzle}".format(
            puzzle=puzzleData["puzzle"])
        pzq_next_key = "pzq_next:{puzzle}".format(puzzle=puzzleData["puzzle"])
        piece_move_timeout = current_app.config["PIECE_MOVE_TIMEOUT"]
        redis_connection.incr(pzq_current_key, amount=1)
        redis_connection.expire(pzq_current_key, piece_move_timeout + 2)
        redis_connection.expire(pzq_next_key, piece_move_timeout + 2)
    return (msg, karma_change)
Beispiel #2
0
        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)
Beispiel #3
0
def bump_count(user):
    """
    Bump the count for pieces moved for the user.
    The nginx conf may also have rate limits on this route. The goal here
    is to ban user if the piece movement rate continues to max out at this rate.
    """
    timestamp_now = int(time.time())
    rounded_timestamp = timestamp_now - (timestamp_now %
                                         PIECE_TRANSLATE_RATE_TIMEOUT)
    err_msg = {}

    # TODO: optimize the timestamp used here by truncating to last digits based
    # on the expiration of the key.
    piece_translate_rate_key = "ptrate:{user}:{timestamp}".format(
        user=user, timestamp=rounded_timestamp)
    if redis_connection.setnx(piece_translate_rate_key, 1):
        redis_connection.expire(piece_translate_rate_key,
                                PIECE_TRANSLATE_RATE_TIMEOUT)
    count = redis_connection.incr(piece_translate_rate_key)
    if count > PIECE_TRANSLATE_MAX_COUNT:
        err_msg = increase_ban_time(user, PIECE_TRANSLATE_BAN_TIME_INCR)
        err_msg["reason"] = PIECE_TRANSLATE_EXCEEDED_REASON

    return err_msg
Beispiel #4
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
Beispiel #5
0
    def publishMessage(msg, karma_change, karma, points=0, complete=False):
        # print(topic)
        # print(msg)
        if current_app.config.get("PUZZLE_PIECES_CACHE_TTL"):
            stamp = redis_connection.get(f"pzstamp:{puzzle}")
            if stamp:
                pcu_key = f"pcu:{stamp}"
                redis_connection.rpushx(pcu_key, msg)
        sse.publish(
            msg,
            type="move",
            channel="puzzle:{puzzle_id}".format(
                puzzle_id=puzzleData["puzzle_id"]),
        )

        if user != ANONYMOUS_USER_ID:
            points_key = "points:{user}".format(user=user)
            recent_points = int(redis_connection.get(points_key) or 0)
            if karma_change < 0 and karma <= 0 and recent_points > 0:
                redis_connection.decr(points_key)

        redis_connection.zadd("pcupdates", {puzzle: now})

        if user != ANONYMOUS_USER_ID:
            # bump the m_date for this player on the puzzle and timeline
            redis_connection.zadd("timeline:{puzzle}".format(puzzle=puzzle),
                                  {user: now})
            redis_connection.zadd("timeline", {user: now})

        # Update player points
        if points != 0 and user is not None and user != ANONYMOUS_USER_ID:
            redis_connection.zincrby("score:{puzzle}".format(puzzle=puzzle),
                                     amount=1,
                                     value=user)
            redis_connection.sadd("batchuser", user)
            redis_connection.sadd("batchpuzzle", puzzle)
            redis_connection.incr("batchscore:{user}".format(user=user),
                                  amount=1)
            redis_connection.incr(
                "batchpoints:{puzzle}:{user}".format(puzzle=puzzle, user=user),
                amount=points,
            )
            redis_connection.zincrby("rank", amount=1, value=user)
            pieces = int(puzzleData["pieces"])
            # Skip increasing dots if puzzle is private
            earns = get_earned_points(pieces,
                                      permission=puzzleData.get("permission"))

            # karma = int(redis_connection.get(karma_key))
            ## Max out recent points
            if (earns != 0 and karma >= current_app.config["MAX_KARMA"] and
                    recent_points < current_app.config["MAX_RECENT_POINTS"]):
                recent_points = redis_connection.incr(points_key)
            # Doing small puzzles doesn't increase recent points, just extends points expiration.
            redis_connection.expire(points_key,
                                    current_app.config["RECENT_POINTS_EXPIRE"])

            # Extend the karma points expiration since it has increased
            redis_connection.expire(karma_key,
                                    current_app.config["KARMA_POINTS_EXPIRE"])
            # Max out karma
            if karma < current_app.config["MAX_KARMA"]:
                karma = redis_connection.incr(karma_key)
            karma_change += 1

            redis_connection.incr("batchpoints:{user}".format(user=user),
                                  amount=earns)

        if complete:
            current_app.logger.info("puzzle {puzzle_id} is complete".format(
                puzzle_id=puzzleData["puzzle_id"]))

            sse.publish(
                "status:{}".format(COMPLETED),
                channel="puzzle:{puzzle_id}".format(
                    puzzle_id=puzzleData["puzzle_id"]),
            )

            r = requests.patch(
                "http://{HOSTAPI}:{PORTAPI}/internal/puzzle/{puzzle_id}/details/"
                .format(
                    HOSTAPI=current_app.config["HOSTAPI"],
                    PORTAPI=current_app.config["PORTAPI"],
                    puzzle_id=puzzleData["puzzle_id"],
                ),
                json={
                    "status": COMPLETED,
                    "m_date": time.strftime("%Y-%m-%d %H:%M:%S",
                                            time.gmtime()),
                    "queue": QUEUE_END_OF_LINE,
                },
            )
            if r.status_code != 200:
                raise Exception(
                    "Puzzle details api error when updating puzzle to be complete"
                )
            # Delaying helps avoid issues for players that are moving the last
            # piece of the puzzle as someone else completes it.
            delay = (current_app.config["MAX_PAUSE_PIECES_TIMEOUT"] +
                     current_app.config["PIECE_MOVE_TIMEOUT"] + 2)
            current_app.logger.info(
                f"Delaying puzzle transfer on completed puzzle ({puzzleData['puzzle_id']}) for {delay} seconds"
            )
            job = current_app.cleanupqueue.enqueue_in(
                timedelta(seconds=delay),
                "api.jobs.convertPiecesToDB.transfer",
                puzzle,
                result_ttl=0,
            )

            purge_route_from_nginx_cache(
                "/chill/site/front/{puzzle_id}/".format(
                    puzzle_id=puzzleData["puzzle_id"]),
                current_app.config.get("PURGEURLLIST"),
            )

        if karma_change and user != ANONYMOUS_USER_ID:
            sse.publish(
                "{user}:{piece}:{karma}:{karma_change}".format(
                    user=user,
                    piece=piece,
                    karma=karma + recent_points,
                    karma_change=karma_change,
                ),
                type="karma",
                channel="puzzle:{puzzle_id}".format(
                    puzzle_id=puzzleData["puzzle_id"]),
            )

        # end = time.perf_counter()
        # duration = end - start
        # redis_connection.rpush("testdata:translate", duration)
        # return topic and msg mostly for testing
        return (msg, karma_change)
Beispiel #6
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)
Beispiel #7
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)
Beispiel #8
0
    def patch(self, puzzle_id, piece):
        """
        args:
        x
        y
        r
        """
        ip = "0"  # No ip is used here for karma
        # Ignore publish of user data when anonymous user
        user = ANONYMOUS_USER_ID
        piece_move_timeout = current_app.config["PIECE_MOVE_TIMEOUT"]

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

        x = args.get("x")
        y = args.get("y")
        r = args.get("r")
        current_app.logger.debug("Test internal piece move")

        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:
            err_msg = {
                "msg": "No puzzle",
            }
            return make_response(json.jsonify(err_msg), 400)

        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_data["puzzle_id"] = puzzle_id
        puzzle = puzzle_data["puzzle"]
        puzzle = int(puzzle_data["puzzle"])

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

        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)

        karma = 1
        karma_change = 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:
                (_, _) = attempt_piece_movement(
                    ip,
                    user,
                    puzzle_data,
                    piece,
                    x,
                    y,
                    r,
                    karma_change,
                    karma,
                )
                break
        return make_response("", 204)
Beispiel #9
0
    def do_task(self):
        super().do_task()
        made_change = False

        cur = db.cursor()

        puzzle = redis_connection.spop("batchpuzzle")
        while puzzle:
            last_batch = redis_connection.zrangebyscore(
                "timeline:{puzzle}".format(puzzle=puzzle),
                self.last_run,
                "+inf",
                withscores=True,
            )
            for (user, update_timestamp) in last_batch:
                current_app.logger.debug(
                    "user: {user}, {update_timestamp}".format(
                        user=user, update_timestamp=update_timestamp))
                user = int(user)
                points = int(
                    redis_connection.getset(
                        "batchpoints:{puzzle}:{user}".format(puzzle=puzzle,
                                                             user=user),
                        value=0,
                    ) or "0")
                redis_connection.expire(
                    "batchpoints:{puzzle}:{user}".format(puzzle=puzzle,
                                                         user=user), DAY)
                if points != 0:
                    result = cur.execute(
                        fetch_query_string("select-all-from-puzzle-by-id.sql"),
                        {
                            "puzzle": puzzle
                        },
                    ).fetchall()
                    if not result:
                        current_app.logger.warn(
                            "no puzzle details found for puzzle {}".format(
                                puzzle))
                        continue
                    (result, col_names) = rowify(result, cur.description)
                    puzzle_data = result[0]
                    puzzle_id = puzzle_data["puzzle_id"]

                    timestamp = strftime("%Y-%m-%d %H:%M:%S",
                                         gmtime(update_timestamp))
                    current_app.logger.debug(
                        "{timestamp} - bumping {points} points on {puzzle} ({puzzle_id}) for player: {player}"
                        .format(
                            puzzle=puzzle,
                            puzzle_id=puzzle_id,
                            player=user,
                            points=points,
                            timestamp=timestamp,
                        ))

                    r = requests.post(
                        "http://{HOSTAPI}:{PORTAPI}/internal/puzzle/{puzzle_id}/timeline/"
                        .format(
                            HOSTAPI=current_app.config["HOSTAPI"],
                            PORTAPI=current_app.config["PORTAPI"],
                            puzzle_id=puzzle_id,
                        ),
                        json={
                            "player": user,
                            "points": points,
                            "timestamp": timestamp
                        },
                    )
                    if r.status_code != 200:
                        current_app.logger.warning(
                            "Puzzle timeline api error. Could not add batchpoints. Skipping {puzzle_id}"
                            .format(puzzle_id=puzzle_id, ))
                        continue

                made_change = True
            puzzle = redis_connection.spop("batchpuzzle")

        if self.first_run:
            result = cur.execute(
                read_query_file(
                    "get_list_of_puzzles_in_timeline.sql")).fetchall()
            if result and len(result):
                puzzle_list = list(map(lambda x: x[0], result))
                for puzzle in puzzle_list:
                    result = cur.execute(
                        read_query_file(
                            "select_user_score_and_timestamp_per_puzzle.sql"),
                        {
                            "puzzle": puzzle
                        },
                    ).fetchall()
                    if result and len(result):
                        current_app.logger.info(
                            "Set puzzle ({0}) score and puzzle timeline on {1} players"
                            .format(puzzle, len(result)))
                        user_score = dict(map(lambda x: [x[0], x[1]], result))
                        user_timestamps = dict(
                            map(lambda x: [x[0], int(x[2])], result))
                        redis_connection.zadd(
                            "timeline:{puzzle}".format(puzzle=puzzle),
                            user_timestamps)
                        redis_connection.zadd(
                            "score:{puzzle}".format(puzzle=puzzle), user_score)
                made_change = True

            self.first_run = False

        self.last_run = int(time())

        if made_change:
            self.log_task()

        cur.close()
Beispiel #10
0
    def do_task(self):
        super().do_task()
        made_change = False

        cur = db.cursor()

        user = redis_connection.spop("batchuser")
        while user:
            user = int(user)
            score = redis_connection.getset(
                "batchscore:{user}".format(user=user), value=0)
            redis_connection.expire("batchscore:{user}".format(user=user), DAY)
            points = redis_connection.getset(
                "batchpoints:{user}".format(user=user), value=0)
            redis_connection.expire("batchpoints:{user}".format(user=user),
                                    DAY)

            current_app.logger.debug(
                "update user {id} with {points} points and score of {score}".
                format(**{
                    "id": user,
                    "points": points,
                    "score": score
                }))

            r = requests.post(
                "http://{HOSTAPI}:{PORTAPI}/internal/tasks/{task_name}/start/".
                format(
                    HOSTAPI=current_app.config["HOSTAPI"],
                    PORTAPI=current_app.config["PORTAPI"],
                    task_name="update_user_points_and_m_date",
                ),
                json={
                    "player": user,
                    "points": points,
                    "score": score,
                },
            )
            if r.status_code != 200:
                current_app.logger.warning(
                    "Internal tasks api error. Could not run task update_user_points_and_m_date for player {}"
                    .format(user))

            r = requests.post(
                "http://{HOSTAPI}:{PORTAPI}/internal/tasks/{task_name}/start/".
                format(
                    HOSTAPI=current_app.config["HOSTAPI"],
                    PORTAPI=current_app.config["PORTAPI"],
                    task_name="update_bit_icon_expiration",
                ),
                json={
                    "player": user,
                },
            )
            if r.status_code != 200:
                current_app.logger.warning(
                    "Internal tasks api error. Could not run task update_bit_icon_expiration for player {}"
                    .format(user))

            user = redis_connection.spop("batchuser")
            made_change = True

        if self.first_run:
            result = cur.execute(
                read_query_file(
                    "select_user_score_and_timestamp.sql")).fetchall()
            if result and len(result):
                current_app.logger.info(
                    "Set rank and timeline on {0} players".format(len(result)))
                user_scores = dict(map(lambda x: [x[0], x[1]], result))
                user_timestamps = dict(map(lambda x: [x[0], int(x[2])],
                                           result))
                redis_connection.zadd("rank", user_scores)
                redis_connection.zadd("timeline", user_timestamps)
                made_change = True
            self.first_run = False

        if made_change:
            self.log_task()

        cur.close()
Beispiel #11
0
    def publishMessage(msg, karma_change, points=0, complete=False):
        # print(topic)
        # print(msg)
        sse.publish(
            msg,
            type="move",
            channel="puzzle:{puzzle_id}".format(
                puzzle_id=puzzleData["puzzle_id"]),
        )

        now = int(time.time())

        redis_connection.zadd("pcupdates", {puzzle: now})

        # TODO:
        # return (topic, msg)

        # bump the m_date for this player on the puzzle and timeline
        redis_connection.zadd("timeline:{puzzle}".format(puzzle=puzzle),
                              {user: now})
        redis_connection.zadd("timeline", {user: now})

        # Update player points
        if points != 0 and user != None:
            redis_connection.zincrby("score:{puzzle}".format(puzzle=puzzle),
                                     amount=1,
                                     value=user)
            redis_connection.sadd("batchuser", user)
            redis_connection.sadd("batchpuzzle", puzzle)
            redis_connection.incr("batchscore:{user}".format(user=user),
                                  amount=1)
            redis_connection.incr(
                "batchpoints:{puzzle}:{user}".format(puzzle=puzzle, user=user),
                amount=points,
            )
            redis_connection.zincrby("rank", amount=1, value=user)
            points_key = "points:{user}".format(user=user)
            pieces = int(puzzleData["pieces"])
            # Skip increasing dots if puzzle is private
            earns = get_earned_points(pieces,
                                      permission=puzzleData.get("permission"))

            karma = int(redis_connection.get(karma_key))
            ## Max out recent points
            # if earns != 0:
            #    recent_points = int(redis_connection.get(points_key) or 0)
            #    if karma + 1 + recent_points + earns < MAX_KARMA:
            #        redis_connection.incr(points_key, amount=earns)
            # Doing small puzzles doesn't increase recent points, just extends points expiration.
            redis_connection.expire(points_key, RECENT_POINTS_EXPIRE)

            karma_change += 1
            # Extend the karma points expiration since it has increased
            redis_connection.expire(karma_key, KARMA_POINTS_EXPIRE)
            # Max out karma
            if karma < MAX_KARMA:
                redis_connection.incr(karma_key)
            else:
                # Max out points
                if earns != 0:
                    recent_points = int(redis_connection.get(points_key) or 0)
                    if recent_points + earns <= MAX_RECENT_POINTS:
                        redis_connection.incr(points_key, amount=earns)

            redis_connection.incr("batchpoints:{user}".format(user=user),
                                  amount=earns)

        # TODO: Optimize by using redis for puzzle status
        if complete:
            current_app.logger.info("puzzle {puzzle_id} is complete".format(
                puzzle_id=puzzleData["puzzle_id"]))
            cur = db.cursor()

            cur.execute(
                fetch_query_string("update_puzzle_status_for_puzzle.sql"),
                {
                    "puzzle": puzzle,
                    "status": COMPLETED
                },
            )
            cur.execute(
                fetch_query_string("update_puzzle_m_date_to_now.sql"),
                {
                    "puzzle": puzzle,
                    "modified": now
                },
            )
            cur.execute(
                fetch_query_string("update_puzzle_queue_for_puzzle.sql"),
                {
                    "puzzle": puzzle,
                    "queue": QUEUE_END_OF_LINE
                },
            )
            db.commit()
            sse.publish(
                "status:{}".format(COMPLETED),
                channel="puzzle:{puzzle_id}".format(
                    puzzle_id=puzzleData["puzzle_id"]),
            )
            job = current_app.cleanupqueue.enqueue_call(
                func="api.jobs.convertPiecesToDB.transfer",
                args=(puzzle, ),
                result_ttl=0)

            purge_route_from_nginx_cache(
                "/chill/site/front/{puzzle_id}/".format(
                    puzzle_id=puzzleData["puzzle_id"]),
                current_app.config.get("PURGEURLLIST"),
            )

            db.commit()
            cur.close()

        # return topic and msg mostly for testing
        return (msg, karma_change)
Beispiel #12
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