示例#1
0
 def move_random_piece(self):
     piece_id = choice(self.movable_pieces)
     x = randint(0, self.table_width - 100)
     y = randint(0, self.table_height - 100)
     piece_token = self.user_session.get_data(
         "/puzzle/{puzzle_id}/piece/{piece_id}/token/?mark={mark}".format(
             puzzle_id=self.puzzle_id,
             piece_id=piece_id,
             mark=self.puzzle_pieces["mark"],
         ))
     if piece_token and piece_token.get("token"):
         puzzle_pieces_move = self.user_session.patch_data(
             "/puzzle/{puzzle_id}/piece/{piece_id}/move/".format(
                 puzzle_id=self.puzzle_id, piece_id=piece_id),
             payload={
                 "x": x,
                 "y": y
             },
             headers={"Token": piece_token["token"]},
         )
         if puzzle_pieces_move:
             if puzzle_pieces_move.get("msg") == "boing":
                 raise Exception("boing")
             # Reset karma:puzzle:ip redis key when it gets low
             if puzzle_pieces_move["karma"] < 2:
                 print("resetting karma for {ip}".format(
                     ip=self.user_session.ip))
                 karma_key = init_karma_key(redis_connection, self.puzzle,
                                            self.user_session.ip)
                 redis_connection.delete(karma_key)
示例#2
0
    def __init__(self, user_sessions, puzzle, puzzle_id, table_width,
                 table_height):
        self.user_sessions = user_sessions
        self.puzzle = puzzle
        self.puzzle_id = puzzle_id

        for user_session in self.user_sessions:
            karma_key = init_karma_key(redis_connection, self.puzzle,
                                       user_session.ip, current_app.config)
            redis_connection.delete(karma_key)

        self.puzzle_pieces = self.user_sessions[0].get_data(
            "/puzzle-pieces/{0}/".format(self.puzzle_id), "api")
        self.mark = uuid4().hex[:10]
        self.table_width = table_width
        self.table_height = table_height
        self.movable_pieces = [
            x["id"] for x in self.puzzle_pieces["positions"] if x["s"] != "1"
        ]
示例#3
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)
示例#4
0
    def move_random_piece(self, user_session):
        piece_id = choice(self.movable_pieces)
        x = randint(0, self.table_width - 100)
        y = randint(0, self.table_height - 100)
        start = time.perf_counter()
        piece_token = None
        try:
            piece_token = user_session.get_data(
                "/puzzle/{puzzle_id}/piece/{piece_id}/token/?mark={mark}".
                format(
                    puzzle_id=self.puzzle_id,
                    piece_id=piece_id,
                    mark=self.mark,
                ),
                "publish",
            )
        except Exception as err:
            # ("resetting karma for {ip}".format(ip=user_session.ip))
            karma_key = init_karma_key(redis_connection, self.puzzle,
                                       user_session.ip, current_app.config)
            redis_connection.delete(karma_key)
            redis_connection.zrem("bannedusers", user_session.shareduser)
            # current_app.logger.debug(f"get token error: {err}")
            if str(err) == "blockedplayer":
                blockedplayers_for_puzzle_key = "blockedplayers:{puzzle}".format(
                    puzzle=self.puzzle)
                # current_app.logger.debug("clear out {}".format(blockedplayers_for_puzzle_key))
                redis_connection.delete(blockedplayers_for_puzzle_key)
            return

        if piece_token and piece_token.get("token"):
            puzzle_pieces_move = None
            try:
                puzzle_pieces_move = user_session.patch_data(
                    "/puzzle/{puzzle_id}/piece/{piece_id}/move/".format(
                        puzzle_id=self.puzzle_id, piece_id=piece_id),
                    "publish",
                    payload={
                        "x": x,
                        "y": y,
                        "r": 0
                    },
                    headers={
                        "Token": piece_token["token"],
                        "Mark": self.mark
                    },
                )
            except Exception as err:
                if str(err) == "too_active":
                    redis_connection.incr("testdata:too_active")
                    time.sleep(30)
                else:
                    # current_app.logger.debug('move exception {}'.format(err))
                    # current_app.logger.debug("resetting karma for {ip}".format(ip=user_session.ip))
                    karma_key = init_karma_key(
                        redis_connection,
                        self.puzzle,
                        user_session.ip,
                        current_app.config,
                    )
                    redis_connection.delete(karma_key)
                    redis_connection.zrem("bannedusers",
                                          user_session.shareduser)
                return
            if puzzle_pieces_move:
                if puzzle_pieces_move.get("msg") == "boing":
                    raise Exception("boing")
                # Reset karma:puzzle:ip redis key when it gets low
                if puzzle_pieces_move["karma"] < 2:
                    # print("resetting karma for {ip}".format(ip=user_session.ip))
                    karma_key = init_karma_key(
                        redis_connection,
                        self.puzzle,
                        user_session.ip,
                        current_app.config,
                    )
                    redis_connection.delete(karma_key)
            else:
                # empty response (204) means success
                end = time.perf_counter()
                duration = end - start
                redis_connection.rpush("testdata:pa", duration)
示例#5
0
def translate(ip,
              user,
              puzzleData,
              piece,
              x,
              y,
              r,
              karma_change,
              db_file=None):
    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)

    p = ""
    points = 0
    puzzle = puzzleData["puzzle"]

    karma_key = init_karma_key(redis_connection, puzzle, ip)
    karma = int(redis_connection.get(karma_key))

    # Restrict piece to within table boundaries
    if x < 0:
        x = 0
    if x > puzzleData["table_width"]:
        x = puzzleData["table_width"]
    if y < 0:
        y = 0
    if y > puzzleData["table_height"]:
        y = puzzleData["table_height"]

    pc_puzzle_piece_key = "pc:{puzzle}:{piece}".format(puzzle=puzzle,
                                                       piece=piece)

    # Get the puzzle piece origin position
    # TODO: Handle the potential error if the hmget here gets a None value for x and y.
    (originX, originY) = list(
        map(
            int,
            redis_connection.hmget(pc_puzzle_piece_key, ["x", "y"]),
        ))
    piece_mutate_process = PieceMutateProcess(
        redis_connection,
        puzzle,
        piece,
        x,
        y,
        r,
        piece_count=puzzleData.get("pieces"))
    (msg, status) = piece_mutate_process.start()

    if status == "stacked":
        # Decrease karma since stacking
        if karma > MIN_KARMA:
            redis_connection.decr(karma_key)
        karma_change -= 1

        return publishMessage(
            msg,
            karma_change,
        )
    elif status == "moved":
        if (len(piece_mutate_process.all_other_pieces_in_piece_group) >
                PIECE_GROUP_MOVE_MAX_BEFORE_PENALTY):
            if karma > MIN_KARMA:
                redis_connection.decr(karma_key)
            karma_change -= 1
        return publishMessage(
            msg,
            karma_change,
        )
    elif status == "joined":
        return publishMessage(
            msg,
            karma_change,
            points=4,
            complete=False,
        )

    elif status == "completed":
        return publishMessage(
            msg,
            karma_change,
            points=4,
            complete=True,
        )
    else:
        pass

    # TODO: handle failed status
    return publishMessage(
        msg,
        karma_change,
    )