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)
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" ]
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)
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)
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, )