def temp_welcome_standalone(msg, seconds): from rcon.recorded_commands import RecordedRcon rcon = RecordedRcon(SERVER_INFO) prev = rcon.set_welcome_message(msg, save=False) queue = get_queue() queue.enqueue_in(timedelta(seconds), welcome, prev)
def get_prunable_vips(days_of_inactivity=30): rcon = RecordedRcon(SERVER_INFO) age_limit = datetime.datetime.now() - datetime.timedelta(days=days_of_inactivity) vips = rcon.get_vip_ids() vips_by_steam_id = {vip["steam_id_64"]: vip["name"] for vip in vips} profiles = get_profiles(list(vips_by_steam_id.keys()), 1) for player in profiles: try: last_session = player["sessions"][0] except IndexError: print(f""" VIP Name: {vips_by_steam_id[player["steam_id_64"]]} Last seen: N/A AKAs: {' || '.join(n["name"] for n in player["names"])} """) continue last_session = last_session["start"] or last_session["end"] or last_session["created"] # TODO: Handle no sessions if last_session < age_limit: print(f""" VIP Name: {vips_by_steam_id[player["steam_id_64"]]} Last seen: {last_session.isoformat()} AKAs: {' || '.join(n["name"] for n in player["names"])} """)
def ban_if_blacklisted(rcon: RecordedRcon, steam_id_64, name): with enter_session() as sess: player = get_player(sess, steam_id_64) if not player: logger.error("Can't check blacklist, player not found %s", steam_id_64) return if player.blacklist and player.blacklist.is_blacklisted: try: logger.info("Player %s was banned due blacklist, reason: %s", str(name), player.blacklist.reason) rcon.do_perma_ban(player=name, reason=player.blacklist.reason, by=f"BLACKLIST: {player.blacklist.by}") safe_save_player_action(rcon=rcon, player_name=name, action_type="PERMABAN", reason=player.blacklist.reason, by=f"BLACKLIST: {player.blacklist.by}", steam_id_64=steam_id_64) try: send_to_discord_audit( f"`BLACKLIST` -> {dict_to_discord(dict(player=name, reason=player.blacklist.reason))}", "BLACKLIST") except: logger.error("Unable to send blacklist to audit log") except: send_to_discord_audit( "Failed to apply ban on blacklisted players, please check the logs and report the error", "ERROR")
def ban_if_has_vac_bans(rcon: RecordedRcon, steam_id_64, name): try: max_days_since_ban = int(MAX_DAYS_SINCE_BAN) max_game_bans = ( float("inf") if int(MAX_GAME_BAN_THRESHOLD) <= 0 else int(MAX_GAME_BAN_THRESHOLD) ) except ValueError: # No proper value is given logger.error( "Invalid value given for environment variable BAN_ON_VAC_HISTORY_DAYS or MAX_GAME_BAN_THRESHOLD" ) return if max_days_since_ban <= 0: return # Feature is disabled with enter_session() as sess: player = get_player(sess, steam_id_64) if not player: logger.error("Can't check VAC history, player not found %s", steam_id_64) return bans = get_player_bans(steam_id_64) if not bans or not isinstance(bans, dict): logger.warning( "Can't fetch Bans for player %s, received %s", steam_id_64, bans ) # Player couldn't be fetched properly (logged by get_player_bans) return if should_ban(bans, max_game_bans, max_days_since_ban): reason = AUTO_BAN_REASON.format( DAYS_SINCE_LAST_BAN=bans.get("DaysSinceLastBan"), MAX_DAYS_SINCE_BAN=str(max_days_since_ban), ) logger.info( "Player %s was banned due VAC history, last ban: %s days ago", str(player), bans.get("DaysSinceLastBan"), ) rcon.do_perma_ban(player=name, reason=reason, by="VAC BOT") try: audit_params = dict( player=name, steam_id_64=player.steam_id_64, reason=reason, days_since_last_ban=bans.get("DaysSinceLastBan"), vac_banned=bans.get("VACBanned"), number_of_game_bans=bans.get("NumberOfGameBans"), ) send_to_discord_audit( f"`VAC/GAME BAN` -> {dict_to_discord(audit_params)}", "AUTOBAN" ) except: logger.exception("Unable to send vac ban to audit log")
def get_current_map(self): map_ = RecordedRcon(SERVER_INFO).get_map() if map_.endswith("_RESTART"): map_ = map_.replace("_RESTART", "") if map_ not in ALL_MAPS: raise ValueError("Invalid current map %s map_") return map_
def _set_real_vips(rcon: RecordedRcon, struct_log): config = RealVipConfig() if not config.get_enabled(): logger.debug("Real VIP is disabled") return desired_nb_vips = config.get_desired_total_number_vips() min_vip_slot = config.get_minimum_number_vip_slot() vip_count = rcon.get_vips_count() remaining_vip_slots = max(desired_nb_vips - vip_count, max(min_vip_slot, 0)) rcon.set_vip_slots_num(remaining_vip_slots) logger.info("Real VIP set slots to %s", remaining_vip_slots)
def __init__(self): self.rcon = Rcon(SERVER_INFO) self.rcon_2 = RecordedRcon(SERVER_INFO) self.red = get_redis_client() self.duplicate_guard_key = "unique_logs" self.log_history = self.get_log_history_list() logger.info("Registered hooks: %s", HOOKS)
def ingame_mods(): red = _red() mods = red.hgetall("moderators") or {} if not mods: return [] rcon = RecordedRcon(SERVER_INFO) players = rcon.get_players() mods_ids = set(v.decode() for v in mods.values()) ig_mods = [] for player in players: if player['steam_id_64'] in mods_ids: ig_mods.append({ "username": player["name"], 'steam_id_64': player['steam_id_64'] }) return ig_mods
def toggle_votekick(rcon: RecordedRcon): config = AutoVoteKickConfig() condition_type = config.get_condition_type().upper() min_online = config.get_min_online_mods() min_ingame = config.get_min_ingame_mods() condition = all if condition_type == 'AND' else any online_mods_ = online_mods() ingame_mods_ = ingame_mods(rcon) ok_online = len(online_mods_) >= min_online ok_ingame = len(ingame_mods_) >= min_ingame if condition([ok_ingame, ok_online]): logger.debug( f"Turning votekick off {condition_type=} {min_online=} {min_ingame=} {ok_online=} {ok_ingame=} {online_mods_=} {ingame_mods_=}" ) rcon.set_votekick_enabled(False) else: logger.debug( f"Turning votekick on {condition_type=} {min_online=} {min_ingame=} {ok_online=} {ok_ingame=} {online_mods_=} {ingame_mods_=}" ) rcon.set_votekick_enabled(True)
def apply_with_retry(self, nb_retry=2): success = False for i in range(nb_retry): try: success = self.apply_results() except: logger.exception("Applying vote map result failed.") else: break if not success: logger.warning("Falling back to adding all maps to the rotation") RecordedRcon(SERVER_INFO).set_maprotation(ALL_MAPS)
def notify_camera(rcon: RecordedRcon, struct_log): send_to_discord_audit(message=struct_log['message'], by=struct_log['player']) try: if hooks := get_prepared_discord_hooks("camera"): embeded = DiscordEmbed( title= f'{struct_log["player"]} - {struct_log["steam_id_64_1"]}', description=struct_log["sub_content"], color=242424, ) for h in hooks: h.add_embed(embeded) h.execute() except Exception: logger.exception("Unable to forward to hooks") config = CameraConfig() if config.is_broadcast(): rcon.set_broadcast(struct_log['message']) if config.is_welcome(): rcon.set_welcome_message(struct_log['message'])
def run(): max_fails = 5 from rcon.settings import SERVER_INFO rcon = RecordedRcon(SERVER_INFO) recorder = MapsRecorder(rcon) while True: try: recorder.detect_map_change() toggle_votekick(rcon) except CommandFailedError: max_fails -= 1 if max_fails <= 0: logger.exception("Routines 5 failures in a row. Stopping") raise time.sleep(30)
def bluk_vip(name_ids, mode="override"): errors = [] ctl = RecordedRcon(SERVER_INFO) vips = ctl.get_vip_ids() for vip in vips: ctl.do_remove_vip(vip["steam_id_64"]) for name, steam_id in name_ids: try: ctl.do_add_vip(name.strip(), steam_id) except CommandFailedError: error = "Failed to add {name} {steam_id}" logger.warning("Retrying once for: %s", error) time.sleep(1) try: ctl.do_add_vip(name.strip(), steam_id) except Exception: logger.exception(error) errors.append(error) if not errors: errors.append("ALL OK") return errors
class LiveStats: def __init__(self): self.rcon = RecordedRcon(SERVER_INFO) self.voted_yes_regex = re.compile(".*PV_Favour.*") self.voted_no_regex = re.compile(".*PV_Against.*") self.red = get_redis_client() def _get_player_session_time(self, player): if not player or not player.get("profile"): #logger.warning("Can't use player profile") return -1 player_time_sec = player.get("profile", {}).get("current_playtime_seconds", 0) if player["name"] == "Dr.WeeD": logger.debug("Player %s - playtime sec: %s", player["name"], player_time_sec) return player_time_sec def _is_log_from_current_session(self, now, player, log): if player["name"] == "Dr.WeeD": logger.debug( "%s %s %s %s", log["timestamp_ms"], (now.timestamp() - self._get_player_session_time(player)) * 1000, log["timestamp_ms"] >= (now.timestamp() - self._get_player_session_time(player)) * 1000, log['raw']) return ( log["timestamp_ms"] >= (now.timestamp() - self._get_player_session_time(player)) * 1000) def _get_indexed_logs_by_player_for_session(self, now, indexed_players, logs): logs_indexed = {} for l in logs: player = indexed_players.get(l["player"]) player2 = indexed_players.get(l["player2"]) try: # Only consider stats for a player from his last connection (so a disconnect reconnect should reset stats) otherwise multiple sessions could be blended into one, even if they are far apart if player and self._is_log_from_current_session( now, player, l): logs_indexed.setdefault(l["player"], []).append(l) if player2 and self._is_log_from_current_session( now, player2, l): logs_indexed.setdefault(l["player2"], []).append(l) except KeyError: logger.exception("Invalid log line %s", l) return logs_indexed def _is_player_death(self, player, log): return player["name"] == log["player2"] def _is_player_kill(self, player, log): return player["name"] == log["player"] def _add_kd(self, attacker_key, victim_key, stats, player, log): if self._is_player_kill(player, log): stats[attacker_key] += 1 elif self._is_player_death(player, log): stats[victim_key] += 1 else: logger.warning( "Log line does not belong to player '%s' line: '%s'", player["name"], log["raw"], ) def _add_kill(self, stats, player, log): self._add_kd("kills", "deaths", stats, player, log) def _add_tk(self, stats, player, log): self._add_kd("teamkills", "deaths_by_tk", stats, player, log) def _add_vote(self, stats, player, log): if self.voted_no_regex.match(log["raw"]): stats["nb_voted_no"] += 1 elif self.voted_yes_regex.match(log["raw"]): stats["nb_voted_yes"] += 1 else: logger.warning( "VOTE log line does not match either vote yes or no regex: %s", log["raw"], ) def _add_vote_started(self, stats, player, log): stats["nb_vote_started"] += 1 def _streaks_accumulator(self, player, log, stats, streaks): action = log["action"] if action == "KILL": if self._is_player_kill(player, log): streaks.kill += 1 streaks.death = 0 streaks.teamkills = 0 elif self._is_player_death(player, log): streaks.kill = 0 streaks.deaths_by_tk = 0 streaks.death += 1 if action == "TEAM KILL": if self._is_player_kill(player, log): streaks.teamkills += 1 if self._is_player_death(player, log): streaks.deaths_by_tk += 1 stats["kills_streak"] = max(streaks.kill, stats["kills_streak"]) stats["deaths_without_kill_streak"] = max( streaks.death, stats["deaths_without_kill_streak"]) stats["teamkills_streak"] = max(streaks.teamkills, stats["teamkills_streak"]) stats["deaths_by_tk_streak"] = max(streaks.deaths_by_tk, stats["deaths_by_tk_streak"]) def get_current_players_stats(self): players = self.rcon.get_players() if not players: logger.debug("No players") return {} profiles_by_id = { profile["steam_id_64"]: profile for profile in get_profiles([p["steam_id_64"] for p in players], nb_sessions=0) } logger.info("%s players, %s profiles loaded", len(players), len(profiles_by_id)) oldest_session_seconds = self._get_player_session_time( max(players, key=self._get_player_session_time)) logger.debug("Oldest session: %s", oldest_session_seconds) now = datetime.datetime.now() min_timestamp = ( now - datetime.timedelta(seconds=oldest_session_seconds)).timestamp() logger.debug("Min timestamp: %s", min_timestamp) logs = get_recent_logs(min_timestamp=min_timestamp) logger.info("%s log lines to process", len(logs["logs"])) indexed_players = {p["name"]: p for p in players} indexed_logs = self._get_indexed_logs_by_player_for_session( now, indexed_players, logs["logs"]) stats_by_player = {} actions_processors = { "KILL": self._add_kill, "TEAM KILL": self._add_tk, "VOTE STARTED": self._add_vote_started, "VOTE": self._add_vote, } for p in players: logger.debug("Crunching stats for %s", p) player_logs = indexed_logs.get(p["name"], []) stats = { "player": p["name"], "steam_id_64": p.get("steam_id_64"), "steaminfo": profiles_by_id.get(p.get("steam_id_64"), {}).get("steaminfo"), "kills": 0, "kills_streak": 0, "deaths": 0, "deaths_without_kill_streak": 0, "teamkills": 0, "teamkills_streak": 0, "deaths_by_tk": 0, "deaths_by_tk_streak": 0, "nb_vote_started": 0, "nb_voted_yes": 0, "nb_voted_no": 0, "time_seconds": self._get_player_session_time(p), } streaks = Streaks() for l in player_logs: action = l["action"] processor = actions_processors.get(action, lambda **kargs: None) processor(stats=stats, player=p, log=l) self._streaks_accumulator(p, l, stats, streaks) # stats = self._compute_stats(stats) stats_by_player[p["name"]] = self._compute_stats(stats) return stats_by_player def set_live_stats(self): snapshot_ts = datetime.datetime.now().timestamp() stats = self.get_current_players_stats() self.red.set( "LIVE_STATS", pickle.dumps(dict(snapshot_timestamp=snapshot_ts, stats=stats)), ) def get_cached_stats(self): stats = self.red.get("LIVE_STATS") if stats: stats = pickle.loads(stats) return stats def _compute_stats(self, stats): new_stats = dict(**stats) new_stats["kills_per_minute"] = round( stats["kills"] / max(stats["time_seconds"] / 60, 1), 2) new_stats["deaths_per_minute"] = round( stats["deaths"] / max(stats["time_seconds"] / 60, 1), 2) new_stats["kill_death_ratio"] = round( stats["kills"] / max(stats["deaths"], 1), 2) return new_stats
AutoVoteKickConfig, CameraConfig, InvalidConfigurationError, StandardMessages, ) from rcon.user_config import DiscordHookConfig from rcon.utils import LONG_HUMAN_MAP_NAMES, map_name from rcon.utils import MapsHistory from rcon.watchlist import PlayerWatch from rcon.workers import temporary_broadcast, temporary_welcome from .auth import login_required, api_response from .multi_servers import forward_request, forward_command from .utils import _get_data logger = logging.getLogger("rconweb") ctl = RecordedRcon(SERVER_INFO) def set_temp_msg(request, func, name): data = _get_data(request) failed = False error = None try: func(ctl, data["msg"], data["seconds"]) except Exception as e: failed = True error = repr(e) return api_response(failed=failed, error=error, result=None, command=name)
import logging import re from rcon.player_history import get_player_profile, player_has_flag from rcon.recorded_commands import RecordedRcon from rcon.discord import send_to_discord_audit from rcon.config import get_config from rcon.settings import SERVER_INFO from rcon.game_logs import on_connected logger = logging.getLogger(__name__) recorded_rcon = RecordedRcon(SERVER_INFO) @on_connected def auto_kick(_, log): try: config = get_config().get('NAME_KICKS') except KeyError: logger.error("Invalid configuration file, NAME_KICKS key is missing") return for r in config['regexps']: name = log["player"] info = recorded_rcon.get_player_info(name) try: profile = get_player_profile(info["steam_id_64"], 0) for f in config.get("whitelist_flags", []): if player_has_flag(profile, f): logger.debug(
def broadcast(msg): from rcon.recorded_commands import RecordedRcon rcon = RecordedRcon(SERVER_INFO) rcon.set_broadcast(msg)
def welcome(msg): from rcon.recorded_commands import RecordedRcon rcon = RecordedRcon(SERVER_INFO) rcon.set_welcome_message(msg)
failure = True res = None audit(func.__name__, request, arguments) #logger.debug("%s %s -> %s", func.__name__, arguments, res) return JsonResponse({ "result": res, "command": func.__name__, "arguments": data, "failed": failure }) return wrapper ctl = RecordedRcon(SERVER_INFO) def make_table(scoreboard): return '\n'.join(["Rank Name Ratio Kills Death"] + [ f"{('#'+ str(idx+1)).ljust(6)}{obj['player'].ljust(22)}{obj['ratio'].ljust(6)}{str(obj['(real) kills']).ljust(6)}{str(obj['(real) death']).ljust(5)}" for idx, obj in enumerate(scoreboard) ]) @csrf_exempt def text_scoreboard(request): try: minutes = abs(int(request.GET.get('minutes'))) except (ValueError, KeyError, TypeError): minutes = 180
h.execute() except Exception: logger.exception("Unable to forward to hooks") config = CameraConfig() if config.is_broadcast(): temporary_broadcast(rcon, struct_log["message"], 60) if config.is_welcome(): temporary_welcome(rcon, struct_log["message"], 60) if __name__ == "__main__": from rcon.settings import SERVER_INFO log = { 'version': 1, 'timestamp_ms': 1627734269000, 'relative_time_ms': 221.212, 'raw': '[543 ms (1627734269)] CONNECTED Dr.WeeD', 'line_without_time': 'CONNECTED Dr.WeeD', 'action': 'CONNECTED', 'player': 'Dr.WeeD', 'steam_id_64_1': None, 'player2': None, 'steam_id_64_2': None, 'weapon': None, 'message': 'Dr.WeeD', 'sub_content': None } real_vips(RecordedRcon(SERVER_INFO), struct_log=log)
class VoteMap: def __init__(self) -> None: self.red = get_redis_client() def is_vote(self, message): match = re.match("(\d)", message) if not match: match = re.match("!votemap\s*(.*)", message) if not match: return False if not match.groups(): raise InvalidVoteError("You must specify the number of the map") return match.groups()[0] def register_vote(self, player_name, vote_timestamp, vote_content): try: current_map = MapsHistory()[0] min_time = current_map["start"] except IndexError: logger.error("Map history is empty - Can't register vote") raise except KeyError: logger.error("Map history is corrupted - Can't register vote") raise if vote_timestamp < min_time: logger.warning( f"Vote is too old {player_name=}, {vote_timestamp=}, {vote_content=}, {current_map=}" ) selection = self.get_selection() try: vote_idx = int(vote_content) selected_map = selection[vote_idx] self.red.hset("VOTES", player_name, selected_map) except (TypeError, ValueError, IndexError): raise InvalidVoteError( f"Vote must be a number between 0 and {len(selection) - 1}") logger.info( f"Registered vote from {player_name=} for {selected_map=} - {vote_content=}" ) return selected_map def get_vote_overview(self): try: votes = self.get_votes() maps = Counter(votes.values()).most_common() return {"total_votes": len(votes), "winning_maps": maps} except Exception: logger.exception("Can't produce vote overview") def clear_votes(self): self.red.delete("VOTES") def get_votes(self): votes = self.red.hgetall("VOTES") or {} return {k.decode(): v.decode() for k, v in votes.items()} def get_current_map(self): map_ = RecordedRcon(SERVER_INFO).get_map() if map_.endswith("_RESTART"): map_ = map_.replace("_RESTART", "") if map_ not in ALL_MAPS: raise ValueError("Invalid current map %s map_") return map_ def gen_selection(self): config = VoteMapConfig() logger.debug( f"""Generating new map selection for vote map with the following criteria: {ALL_MAPS} {config.get_votemap_number_of_options()=} {config.get_votemap_ratio_of_offensives_to_offer()=} {config.get_votemap_number_of_last_played_map_to_exclude()=} {config.get_votemap_consider_offensive_as_same_map()=} {config.get_votemap_allow_consecutive_offensives()=} {config.get_votemap_allow_consecutive_offensives_of_opposite_side()=} {config.get_votemap_default_method()=} """) selection = suggest_next_maps( MapsHistory(), ALL_MAPS, selection_size=config.get_votemap_number_of_options(), exclude_last_n=config. get_votemap_number_of_last_played_map_to_exclude(), offsensive_ratio=config.get_votemap_ratio_of_offensives_to_offer(), consider_offensive_as_same_map=config. get_votemap_consider_offensive_as_same_map(), allow_consecutive_offensive=config. get_votemap_allow_consecutive_offensives(), allow_consecutive_offensives_of_opposite_side=config. get_votemap_allow_consecutive_offensives_of_opposite_side(), current_map=self.get_current_map(), ) self.red.delete("MAP_SELECTION") self.red.lpush("MAP_SELECTION", *selection) logger.info("Saved new selection: %s", selection) def get_selection(self): return [v.decode() for v in self.red.lrange("MAP_SELECTION", 0, -1)] def pick_least_played_map(self, maps): maps_history = MapsHistory() if not maps: raise ValueError("Can't pick a default. No maps to pick from") history = [obj["name"] for obj in maps_history] index = 0 for name in maps: try: idx = history.index(name) except ValueError: return name index = max(idx, index) return history[index] def pick_default_next_map(self): selection = self.get_selection() maps_history = MapsHistory() config = VoteMapConfig() all_maps = ALL_MAPS if not config.get_votemap_allow_default_to_offsensive(): logger.debug( "Not allowing default to offensive, removing all offensive maps" ) selection = [m for m in selection if not "offensive" in m] all_maps = [m for m in ALL_MAPS if not "offensive" in m] if not maps_history: raise ValueError("Map history is empty") return { DefaultMethods.least_played_suggestions: partial(self.pick_least_played_map, selection), DefaultMethods.least_played_all_maps: partial(self.pick_least_played_map, all_maps), DefaultMethods.random_all_maps: lambda: random.choice( list(set(all_maps) - set([maps_history[0]["name"]]))), DefaultMethods.random_suggestions: lambda: random.choice( list(set(selection) - set([maps_history[0]["name"]]))), }[config.get_votemap_default_method()]() def apply_results(self): config = VoteMapConfig() votes = self.get_votes() first = Counter(votes.values()).most_common(1) if not first: next_map = self.pick_default_next_map() logger.warning( "No votes recorded, defaulting with %s using default winning map %s", config.get_votemap_default_method(), next_map, ) else: logger.info(f"{votes=}") next_map = first[0][0] if next_map not in ALL_MAPS: raise ValueError( f"{next_map=} is not part of the all map list {ALL_MAPS=}") if next_map not in (selection := self.get_selection()): raise ValueError( f"{next_map=} is not part of vote select {selection=}") logger.info(f"Winning map {next_map=}") # Sanity checks below rcon = RecordedRcon(SERVER_INFO) current_map = rcon.get_map() if current_map.replace("_RESTART", "") not in ALL_MAPS: raise ValueError( f"{current_map=} is not part of the all map list {ALL_MAPS=}") try: history_current_map = MapsHistory()[0]["name"] except (IndexError, KeyError): raise ValueError("History is empty") if current_map != history_current_map: raise ValueError( f"{current_map=} does not match history map {history_current_map=}" ) # Apply rotation safely current_rotation = rcon.get_map_rotation() try: current_map_idx = current_rotation.index( current_map.replace("_RESTART", "")) except ValueError: logger.warning( f"{current_map=} is not in {current_rotation=} will try to add" ) rcon.do_add_map_to_rotation(current_map.replace("_RESTART", "")) current_rotation = rcon.get_map_rotation() try: current_map_idx = current_rotation.index( current_map.replace("_RESTART", "")) except ValueError as e: raise ValueError( f"{current_map=} is not in {current_rotation=} addint it failed" ) from e try: next_map_idx = current_rotation.index(next_map) except ValueError: logger.info( f"{next_map=} no in rotation, adding it {current_rotation=}") rcon.do_add_map_to_rotation(next_map) else: if next_map_idx < current_map_idx: logger.info( f"{next_map=} is before {current_map=} removing and re-adding" ) rcon.do_remove_map_from_rotation(next_map) rcon.do_add_map_to_rotation(next_map) current_rotation = rcon.get_map_rotation() try: next_map_idx = current_rotation.index(next_map) if next_map_idx < current_map_idx: raise ValueError(f"{next_map=} is still before {current_map=}") except ValueError as e: raise ValueError( f"{next_map=} failed to be added to {current_rotation=}") maps_to_remove = current_rotation[current_map_idx + 1:next_map_idx] logger.debug(f"{current_map=} {current_rotation=} - {maps_to_remove=}") for map in maps_to_remove: # Remove the maps that are in between the current map and the desired next map rcon.do_remove_map_from_rotation(map) for map in maps_to_remove: rcon.do_add_map_to_rotation(map) # Check that it worked current_rotation = rcon.get_map_rotation() if (not current_rotation[current_map_idx] == current_map.replace( '_RESTART', '') and current_rotation[current_map_idx + 1] == next_map): raise ValueError( f"Applying the winning map {next_map=} after the {current_map=} failed: {current_rotation=}" ) logger.info( f"Successfully applied winning mapp {next_map=}, new rotation {current_rotation=}" ) return True
rcon = RecordedRcon(SERVER_INFO) age_limit = datetime.datetime.now() - datetime.timedelta(days=days_of_inactivity) vips = rcon.get_vip_ids() vips_by_steam_id = {vip["steam_id_64"]: vip["name"] for vip in vips} profiles = get_profiles(list(vips_by_steam_id.keys()), 1) for player in profiles: try: last_session = player["sessions"][0] except IndexError: print(f""" VIP Name: {vips_by_steam_id[player["steam_id_64"]]} Last seen: N/A AKAs: {' || '.join(n["name"] for n in player["names"])} """) continue last_session = last_session["start"] or last_session["end"] or last_session["created"] # TODO: Handle no sessions if last_session < age_limit: print(f""" VIP Name: {vips_by_steam_id[player["steam_id_64"]]} Last seen: {last_session.isoformat()} AKAs: {' || '.join(n["name"] for n in player["names"])} """) if __name__ == '__main__': #print(get_prunable_vips()) light_get_vips_count(RecordedRcon(SERVER_INFO))
def auto_ban_if_tks_right_after_connection(rcon: RecordedRcon, log): config = get_config() config = config.get("BAN_TK_ON_CONNECT") if not config or not config.get("enabled"): return player_name = log["player"] player_steam_id = log["steam_id_64_1"] player_profile = None vips = {} try: player_profile = get_player_profile(player_steam_id, 0) except: logger.exception("Unable to get player profile") try: vips = set(v['steam_id_64'] for v in rcon.get_vip_ids()) except: logger.exception("Unable to get VIPS") last_logs = get_recent_logs(end=500, player_search=player_name, exact_player_match=True) logger.debug("Checking TK from %s", player_name) author = config.get("author_name", "Automation") reason = config.get("message", "No reasons provided") discord_msg = config.get("discord_webhook_message", "No message provided") webhook = config.get("discord_webhook_url") max_time_minute = config.get("max_time_after_connect_minutes", 5) excluded_weapons = [w.lower() for w in config.get("exclude_weapons", [])] ignore_after_kill = config.get("ignore_tk_after_n_kills", 1) ignore_after_death = config.get("ignore_tk_after_n_death", 1) whitelist_players = config.get("whitelist_players", {}) tk_tolerance_count = config.get("teamkill_tolerance_count", 1) if player_profile: if whitelist_players.get('is_vip') and player_steam_id in vips: logger.debug("Not checking player because he's VIP") return if whitelist_players.get('has_at_least_n_sessions') and player_profile[ 'sessions_count'] >= whitelist_players.get( 'has_at_least_n_sessions'): logger.debug("Not checking player because he has %s sessions", player_profile['sessions_count']) return flags = whitelist_players.get('has_flag', []) if not isinstance(flags, list): flags = [flags] for f in flags: if player_has_flag(player_profile, f): logger.debug("Not checking player because he has flag %s", f) return last_action_is_connect = False last_connect_time = None kill_counter = 0 death_counter = 0 tk_counter = 0 for log in reversed(last_logs["logs"]): logger.debug(log) if log["action"] == "CONNECTED": last_action_is_connect = log last_connect_time = log["timestamp_ms"] kill_counter = 0 death_counter = 0 continue if log["action"] == "TEAM KILL" and log[ 'player'] == player_name and last_action_is_connect: if excluded_weapons and log["weapon"].lower() in excluded_weapons: logger.debug( "Not counting TK as offense due to weapon exclusion") continue if log['timestamp_ms'] - last_connect_time > max_time_minute * 60 * 1000: logger.debug( "Not counting TK as offense due to elapsed time exclusion, last connection time %s, tk time %s", datetime.datetime.fromtimestamp(last_connect_time / 1000), datetime.datetime.fromtimestamp(log["timestamp_ms"])) continue logger.info("Banning player %s for TEAMKILL after connect %s", player_name, log) tk_counter += 1 if tk_counter > tk_tolerance_count: rcon.do_perma_ban( player=player_name, reason=reason, by=author, ) send_to_discord_audit(discord_msg.format(player=player_name), by=author, webhookurl=webhook) elif is_player_death(player_name, log): death_counter += 1 if death_counter >= ignore_after_death: last_action_is_connect = False elif is_player_kill(player_name, log): kill_counter += 1 if kill_counter >= ignore_after_kill: last_action_is_connect = False
def __init__(self): self.rcon = RecordedRcon(SERVER_INFO) self.voted_yes_regex = re.compile(".*PV_Favour.*") self.voted_no_regex = re.compile(".*PV_Against.*") self.red = get_redis_client()