diff --git a/config/supervisord.conf b/config/supervisord.conf index ebb11cb12..e80bb49a3 100644 --- a/config/supervisord.conf +++ b/config/supervisord.conf @@ -16,6 +16,13 @@ environment=LOGGING_FILENAME=expiring_vips_%(ENV_SERVER_NUMBER)s.log startretries=10 autorestart=true +[program:seed_vip] +command=/code/manage.py seed_vip +environment=LOGGING_FILENAME=seed_vip_%(ENV_SERVER_NUMBER)s.log +startretries=10 +autostart=true +autorestart=true + [program:log_event_loop] command=/code/manage.py log_loop diff --git a/rcon/api_commands.py b/rcon/api_commands.py index b13bb043f..40da2782d 100644 --- a/rcon/api_commands.py +++ b/rcon/api_commands.py @@ -50,6 +50,7 @@ from rcon.user_config.rcon_server_settings import RconServerSettingsUserConfig from rcon.user_config.real_vip import RealVipUserConfig from rcon.user_config.scorebot import ScorebotUserConfig +from rcon.user_config.seed_vip import SeedVIPUserConfig from rcon.user_config.standard_messages import ( StandardBroadcastMessagesUserConfig, StandardPunishmentMessagesUserConfig, @@ -81,8 +82,9 @@ def parameter_aliases(alias_to_param: Dict[str, str]): """Specify parameter aliases of a function. This might be useful to preserve backwards compatibility or to handle parameters named after a Python reserved keyword. - + Takes a mapping of aliases to their parameter name.""" + def decorator(func): @functools.wraps(func) def wrapper(*args, **kwargs): @@ -90,11 +92,13 @@ def wrapper(*args, **kwargs): if alias in kwargs: kwargs[param] = kwargs.pop(alias) return func(*args, **kwargs) - + wrapper._parameter_aliases = alias_to_param return wrapper + return decorator + def get_rcon_api(credentials: ServerInfoType | None = None) -> "RconAPI": """Return a initialized Rcon connection to the game server @@ -578,9 +582,11 @@ def get_online_mods(self) -> list[AdminUserType]: def get_ingame_mods(self) -> list[AdminUserType]: return ingame_mods() - @parameter_aliases({ - "from": "from_", - }) + @parameter_aliases( + { + "from": "from_", + } + ) def get_historical_logs( self, player_name: str | None = None, @@ -1010,6 +1016,43 @@ def set_real_vip_config( reset_to_default=reset_to_default, ) + def get_seed_vip_config( + self, + ) -> SeedVIPUserConfig: + return SeedVIPUserConfig.load_from_db() + + def validate_seed_vip_config( + self, + by: str, + config: dict[str, Any] | BaseUserConfig | None = None, + reset_to_default: bool = False, + **kwargs, + ) -> bool: + return self._validate_user_config( + command_name=inspect.currentframe().f_code.co_name, # type: ignore + by=by, + model=SeedVIPUserConfig, + data=config or kwargs, + dry_run=True, + reset_to_default=reset_to_default, + ) + + def set_seed_vip_config( + self, + by: str, + config: dict[str, Any] | BaseUserConfig | None = None, + reset_to_default: bool = False, + **kwargs, + ) -> bool: + return self._validate_user_config( + command_name=inspect.currentframe().f_code.co_name, # type: ignore + by=by, + model=SeedVIPUserConfig, + data=config or kwargs, + dry_run=False, + reset_to_default=reset_to_default, + ) + def get_camera_notification_config(self) -> CameraNotificationUserConfig: return CameraNotificationUserConfig.load_from_db() diff --git a/rcon/automods/level_thresholds.py b/rcon/automods/level_thresholds.py index 3f6386a5f..b3b9e4550 100644 --- a/rcon/automods/level_thresholds.py +++ b/rcon/automods/level_thresholds.py @@ -22,10 +22,9 @@ WatchStatus, ) from rcon.automods.num_or_inf import num_or_inf -from rcon.types import GameState, GetDetailedPlayer +from rcon.types import GameStateType, GetDetailedPlayer from rcon.user_config.auto_mod_level import AutoModLevelUserConfig, Roles - LEVEL_THRESHOLDS_RESET_SECS = 120 AUTOMOD_USERNAME = "LevelThresholdsAutomod" @@ -260,7 +259,7 @@ def punitions_to_apply( squad_name: str, team: Literal["axis", "allies"], squad: dict, - game_state: GameState, + game_state: GameStateType, ) -> PunitionsToApply: """ Observe all squads/players diff --git a/rcon/automods/no_leader.py b/rcon/automods/no_leader.py index a8089f18b..94c7622ad 100644 --- a/rcon/automods/no_leader.py +++ b/rcon/automods/no_leader.py @@ -23,10 +23,9 @@ WatchStatus, ) from rcon.automods.num_or_inf import num_or_inf -from rcon.types import GameState +from rcon.types import GameStateType from rcon.user_config.auto_mod_no_leader import AutoModNoLeaderUserConfig - LEADER_WATCH_RESET_SECS = 120 AUTOMOD_USERNAME = "NoLeaderWatch" @@ -137,7 +136,7 @@ def punitions_to_apply( squad_name: str, team: Literal["axis", "allies"], squad: dict, - game_state: GameState, + game_state: GameStateType, ) -> PunitionsToApply: """ Observe all squads/players diff --git a/rcon/automods/no_solotank.py b/rcon/automods/no_solotank.py index 8114d5e6a..b66b9ab85 100644 --- a/rcon/automods/no_solotank.py +++ b/rcon/automods/no_solotank.py @@ -22,10 +22,9 @@ WatchStatus, ) from rcon.automods.num_or_inf import num_or_inf -from rcon.types import GameState +from rcon.types import GameStateType from rcon.user_config.auto_mod_solo_tank import AutoModNoSoloTankUserConfig - SOLO_TANK_RESET_SECS = 120 AUTOMOD_USERNAME = "NoSoloTank" @@ -134,7 +133,7 @@ def punitions_to_apply( squad_name: str, team: Literal["axis", "allies"], squad: dict, - game_state: GameState, + game_state: GameStateType, ) -> PunitionsToApply: """ Observe all squads/players diff --git a/rcon/automods/seeding_rules.py b/rcon/automods/seeding_rules.py index edf9bb8f2..49e7b7e20 100644 --- a/rcon/automods/seeding_rules.py +++ b/rcon/automods/seeding_rules.py @@ -29,10 +29,9 @@ from rcon.game_logs import on_match_start from rcon.maps import GameMode, parse_layer from rcon.rcon import StructuredLogLineType -from rcon.types import GameState, GetDetailedPlayer, Roles +from rcon.types import GameStateType, GetDetailedPlayer, Roles from rcon.user_config.auto_mod_seeding import AutoModSeedingUserConfig - SEEDING_RULES_RESET_SECS = 120 AUTOMOD_USERNAME = "SeedingRulesAutomod" SEEDING_RULE_NAMES = ["disallowed_roles", "disallowed_weapons", "enforce_cap_fight"] @@ -296,7 +295,7 @@ def punitions_to_apply( squad_name: str, team: Literal["axis", "allies"], squad: dict, - game_state: GameState, + game_state: GameStateType, ) -> PunitionsToApply: """ Observe all squads/players diff --git a/rcon/cli.py b/rcon/cli.py index e89e1f1d4..094ba9863 100644 --- a/rcon/cli.py +++ b/rcon/cli.py @@ -12,6 +12,7 @@ from sqlalchemy import select, text, update import rcon.expiring_vips.service +import rcon.seed_vip.service import rcon.user_config import rcon.user_config.utils from rcon import auto_settings, broadcast, game_logs, routines @@ -31,6 +32,7 @@ from rcon.steam_utils import enrich_db_users from rcon.user_config.auto_settings import AutoSettingsConfig from rcon.user_config.log_stream import LogStreamUserConfig +from rcon.user_config.seed_vip import SeedVIPUserConfig from rcon.user_config.webhooks import ( BaseMentionWebhookUserConfig, BaseUserConfig, @@ -126,6 +128,15 @@ def run_expiring_vips(): rcon.expiring_vips.service.run() +@cli.command(name="seed_vip") +def run_seed_vip(): + config = SeedVIPUserConfig.load_from_db() + if config.enabled: + rcon.seed_vip.service.run() + else: + logger.info("Seed VIP is not enabled") + + @cli.command(name="automod") def run_automod(): automod.run() diff --git a/rcon/rcon.py b/rcon/rcon.py index 65d6fc54e..a1b43cd23 100644 --- a/rcon/rcon.py +++ b/rcon/rcon.py @@ -22,7 +22,7 @@ AdminType, GameLayoutRandomConstraints, GameServerBanType, - GameState, + GameStateType, GetDetailedPlayer, GetDetailedPlayers, GetPlayersType, @@ -747,7 +747,7 @@ def message_player( ) return res - def get_gamestate(self) -> GameState: + def get_gamestate(self) -> GameStateType: """ Returns player counts, team scores, remaining match time and current/next map @@ -1311,21 +1311,22 @@ def set_maprotation(self, map_names: list[str]) -> list[Layer]: super().remove_map_from_rotation(current[0], 1) return self.get_map_rotation() - + @ttl_cache(ttl=10) def get_objective_row(self, row: int): return super().get_objective_row(row) - + def get_objective_rows(self) -> List[List[str]]: - return [ - self.get_objective_row(row) - for row in range(5) - ] + return [self.get_objective_row(row) for row in range(5)] - def set_game_layout(self, objectives: Sequence[str | int | None], random_constraints: GameLayoutRandomConstraints = 0): + def set_game_layout( + self, + objectives: Sequence[str | int | None], + random_constraints: GameLayoutRandomConstraints = 0, + ): if len(objectives) != 5: raise ValueError("5 objectives must be provided") - + obj_rows = self.get_objective_rows() parsed_objs: list[str] = [] for row, (obj, obj_row) in enumerate(zip(objectives, obj_rows)): @@ -1340,12 +1341,17 @@ def set_game_layout(self, objectives: Sequence[str | int | None], random_constra elif obj in ("right", "bottom"): parsed_objs.append(obj_row[2]) else: - raise ValueError("Objective %s does not exist in row %s" % (obj, row)) - + raise ValueError( + "Objective %s does not exist in row %s" % (obj, row) + ) + elif isinstance(obj, int): # Use index of the objective if not (0 <= obj <= 2): - raise ValueError("Objective index %s is out of range 0-2 in row %s" % (obj, row + 1)) + raise ValueError( + "Objective index %s is out of range 0-2 in row %s" + % (obj, row + 1) + ) parsed_objs.append(obj_row[obj]) elif obj is None: @@ -1375,7 +1381,7 @@ def set_game_layout(self, objectives: Sequence[str | int | None], random_constra neighbors.append(obj_rows[row - 1].index(parsed_objs[row - 1])) if row < 4 and parsed_objs[row + 1] is not None: neighbors.append(obj_rows[row + 1].index(parsed_objs[row + 1])) - + # Skip this row for now if neither of its neighbors had their objective determined yet if not neighbors: continue @@ -1394,7 +1400,7 @@ def set_game_layout(self, objectives: Sequence[str | int | None], random_constra if random_constraints & GameLayoutRandomConstraints.ALWAYS_DIAGONAL: # Cannot have two objectives in a straight row obj_choices[neighbor_idx] = None - + # Pick an objective. If none are viable, discard constraints. parsed_objs[row] = random.choice( [c for c in obj_choices if c is not None] or obj_row diff --git a/rcon/seed_vip/__init__.py b/rcon/seed_vip/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/rcon/seed_vip/models.py b/rcon/seed_vip/models.py new file mode 100644 index 000000000..7d71a1007 --- /dev/null +++ b/rcon/seed_vip/models.py @@ -0,0 +1,61 @@ +from datetime import datetime, timedelta +from logging import getLogger + +import pydantic + +from rcon.maps import Layer + +logger = getLogger(__name__) + + +class BaseCondition(pydantic.BaseModel): + def is_met(self): + raise NotImplementedError + + +class PlayerCountCondition(BaseCondition): + faction: str + min_players: int = pydantic.Field(ge=0, le=50) + max_players: int = pydantic.Field(ge=0, le=50) + + current_players: int = pydantic.Field(ge=0, le=50) + + def is_met(self): + return self.min_players <= self.current_players <= self.max_players + + +class PlayTimeCondition(BaseCondition): + # This is constrained on the user config side + min_time_secs: int = pydantic.Field() + # This should be constrained to ge=0 but CRCON will sometimes + # report players with negative play time + current_time_secs: int = pydantic.Field() + + def is_met(self): + return self.current_time_secs >= self.min_time_secs + + +class GameState(pydantic.BaseModel): + num_allied_players: int + num_axis_players: int + allied_score: int + axis_score: int + raw_time_remaining: str + time_remaining: timedelta + current_map: Layer + next_map: Layer + + +class Player(pydantic.BaseModel): + name: str + player_id: str + current_playtime_seconds: int + + +class VipPlayer(pydantic.BaseModel): + player: Player + expiration_date: datetime | None + + +class ServerPopulation(pydantic.BaseModel): + players: dict[str, Player] diff --git a/rcon/seed_vip/service.py b/rcon/seed_vip/service.py new file mode 100644 index 000000000..b5bec7d04 --- /dev/null +++ b/rcon/seed_vip/service.py @@ -0,0 +1,279 @@ +import sys +from collections import defaultdict +from datetime import datetime, timedelta, timezone +from logging import getLogger +from time import sleep + +import humanize + +import discord +from rcon.api_commands import get_rcon_api +from rcon.seed_vip.utils import ( + calc_vip_expiration_timestamp, + collect_steam_ids, + filter_indefinite_vip_steam_ids, + filter_online_players, + get_gamestate, + get_next_player_bucket, + get_online_players, + get_vips, + is_seeded, + make_seed_announcement_embed, + message_players, + reward_players, +) +from rcon.user_config.seed_vip import SeedVIPUserConfig + +logger = getLogger(__name__) + + +def run(): + config = SeedVIPUserConfig.load_from_db() + current_lang = config.language + + rcon_api = get_rcon_api() + + to_add_vip_steam_ids: set[str] = set() + no_reward_steam_ids: set[str] = set() + player_name_lookup: dict[str, str] = {} + prev_announced_bucket: int = 0 + player_buckets = config.player_announce_thresholds + if player_buckets: + next_player_bucket = player_buckets[0] + else: + next_player_bucket = None + last_bucket_announced = False + seeded_timestamp: datetime | None = None + + gamestate = get_gamestate(rcon=rcon_api) + is_seeding = not is_seeded(config=config, gamestate=gamestate) + + try: + while True: + # Reload the config each loop to catch changes to the config + config = SeedVIPUserConfig.load_from_db() + + if not config.enabled: + logger.info("Seed VIP is not enabled") + break + + try: + if current_lang != config.language: + logger.info(f"Deactivating language={current_lang}") + humanize.deactivate() + + if config.language: + # The language to translate to if using the `nice_time_delta` and `nice_expiration_date` settings + # Any valid language code shoud work, look here for examples: https://gist.github.com/jacobbubu/1836273 + current_lang = config.language + humanize.activate(config.language) + logger.info(f"Activated language={config.language}") + except FileNotFoundError as e: + logger.exception(e) + logger.error( + f"Unable to activate language={config.language}, defaulting to English" + ) + + online_players = get_online_players(rcon=rcon_api) + if online_players is None: + logger.debug( + f"Did not receive a usable result from `get_online_players`, sleeping {config.poll_time_seeding} seconds" + ) + sleep(config.poll_time_seeding) + continue + + gamestate = get_gamestate(rcon=rcon_api) + + if gamestate is None: + logger.debug( + f"Did not receive a usable result from `get_gamestate`, sleeping {config.poll_time_seeding} seconds" + ) + sleep(config.poll_time_seeding) + continue + + total_players = gamestate.num_allied_players + gamestate.num_axis_players + + player_name_lookup |= { + p.player_id: p.name for p in online_players.players.values() + } + + logger.debug( + f"{is_seeding=} {len(online_players.players.keys())} online players (`get_players`), {gamestate.num_allied_players} allied {gamestate.num_axis_players} axis players (gamestate)", + ) + to_add_vip_steam_ids = collect_steam_ids( + config=config, + players=online_players, + cum_steam_ids=to_add_vip_steam_ids, + ) + + # Server seeded + if is_seeding and is_seeded(config=config, gamestate=gamestate): + seeded_timestamp = datetime.now(tz=timezone.utc) + logger.info(f"Server seeded at {seeded_timestamp.isoformat()}") + current_vips = get_vips(rcon=rcon_api) + + # only include online players in the current_vips + current_vips = filter_online_players(current_vips, online_players) + + # no vip reward needed for indefinite vip holders + indefinite_vip_steam_ids = filter_indefinite_vip_steam_ids(current_vips) + to_add_vip_steam_ids -= indefinite_vip_steam_ids + + # Players who were online when we seeded but didn't meet the criteria for VIP + no_reward_steam_ids = { + p.player_id for p in online_players.players.values() + } - to_add_vip_steam_ids + + expiration_timestamps = defaultdict( + lambda: calc_vip_expiration_timestamp( + config=config, + expiration=None, + from_time=seeded_timestamp or datetime.now(tz=timezone.utc), + ) + ) + for player in current_vips.values(): + expiration_timestamps[player.player.player_id] = ( + calc_vip_expiration_timestamp( + config=config, + expiration=player.expiration_date if player else None, + from_time=seeded_timestamp, + ) + ) + + # Add or update VIP in CRCON + reward_players( + rcon=rcon_api, + config=config, + to_add_vip_steam_ids=to_add_vip_steam_ids, + current_vips=current_vips, + players_lookup=player_name_lookup, + expiration_timestamps=expiration_timestamps, + ) + + # Message those who earned VIP + message_players( + rcon=rcon_api, + config=config, + message=config.player_messages.reward_player_message, + steam_ids=to_add_vip_steam_ids, + expiration_timestamps=expiration_timestamps, + ) + + # Message those who did not earn + message_players( + rcon=rcon_api, + config=config, + message=config.player_messages.reward_player_message_no_vip, + steam_ids=no_reward_steam_ids, + expiration_timestamps=None, + ) + + # Post seeding complete Discord message + if config.hooks: + logger.debug( + f"Making embed for `{config.player_messages.seeding_complete_message}`" + ) + embed = make_seed_announcement_embed( + message=config.player_messages.seeding_complete_message, + current_map=rcon_api.current_map.pretty_name, + time_remaining=gamestate.raw_time_remaining, + player_count_message=config.player_messages.player_count_message, + num_allied_players=gamestate.num_allied_players, + num_axis_players=gamestate.num_axis_players, + ) + if embed: + for wh in config.hooks: + wh = discord.SyncWebhook.from_url(url=str(wh.url)) + wh.send(embed=embed) + + # Reset for next seed + last_bucket_announced = False + prev_announced_bucket = 0 + to_add_vip_steam_ids.clear() + is_seeding = False + elif ( + not is_seeding + and not is_seeded(config=config, gamestate=gamestate) + and total_players > 0 + ): + delta: timedelta | None = None + if seeded_timestamp: + delta = datetime.now(tz=timezone.utc) - seeded_timestamp + + if not seeded_timestamp: + logger.debug( + f"Back in seeding: seeded_timestamp={seeded_timestamp} {delta=} {config.requirements.buffer=}" + ) + is_seeding = True + elif delta and (delta > config.requirements.buffer.as_timedelta): + logger.debug( + f"Back in seeding: seeded_timestamp={seeded_timestamp.isoformat()} {delta=} delta > buffer {delta > config.requirements.buffer.as_timedelta} {config.requirements.buffer=}" + ) + is_seeding = True + else: + logger.info( + f"Delaying seeding mode due to buffer of {config.requirements.buffer} > {delta} time since seeded" + ) + + if is_seeding: + sleep_time = config.poll_time_seeding + + # When we fall back into seeding with players still on the + # server we want to announce the largest bucket possible or + # it will announce from the smallest to the largest and spam + # Discord with unneccessary announcements + next_player_bucket = get_next_player_bucket( + config.player_announce_thresholds, + total_players=total_players, + ) + + # Announce seeding progress + logger.debug( + f"whs={[wh.url for wh in config.hooks]} {config.player_announce_thresholds=} {total_players=} {prev_announced_bucket=} {next_player_bucket=} {last_bucket_announced=}" + ) + if ( + config.hooks + and next_player_bucket + and not last_bucket_announced + and prev_announced_bucket < next_player_bucket + and total_players >= next_player_bucket + ): + prev_announced_bucket = next_player_bucket + + embed = make_seed_announcement_embed( + message=config.player_messages.seeding_in_progress_message.format( + player_count=total_players + ), + current_map=rcon_api.current_map.pretty_name, + time_remaining=gamestate.raw_time_remaining, + player_count_message=config.player_messages.player_count_message, + num_allied_players=gamestate.num_allied_players, + num_axis_players=gamestate.num_axis_players, + ) + if next_player_bucket == config.player_announce_thresholds[-1]: + logger.debug(f"setting last_bucket_announced=True") + last_bucket_announced = True + + if embed: + for wh in config.hooks: + wh = discord.SyncWebhook.from_url(url=str(wh.url)) + wh.send(embed=embed) + + else: + sleep_time = config.poll_time_seeded + + logger.info(f"sleeping {sleep_time=}") + sleep(sleep_time) + except* Exception as eg: + for e in eg.exceptions: + logger.exception(e) + raise + + +if __name__ == "__main__": + try: + run() + except Exception as e: + logger.error("Seed VIP stopped") + logger.exception(e) + sys.exit(1) diff --git a/rcon/seed_vip/utils.py b/rcon/seed_vip/utils.py new file mode 100644 index 000000000..a560a1a75 --- /dev/null +++ b/rcon/seed_vip/utils.py @@ -0,0 +1,344 @@ +from collections import defaultdict +from datetime import datetime, timedelta, timezone +from logging import getLogger +from typing import Iterable, Sequence + +from humanize import naturaldelta, naturaltime + +import discord +from rcon.api_commands import RconAPI +from rcon.seed_vip.models import ( + BaseCondition, + GameState, + Player, + PlayerCountCondition, + PlayTimeCondition, + ServerPopulation, + VipPlayer, +) +from rcon.types import GameStateType, GetPlayersType, VipIdType +from rcon.user_config.seed_vip import SeedVIPUserConfig +from rcon.utils import INDEFINITE_VIP_DATE + +logger = getLogger(__name__) + + +def filter_indefinite_vip_steam_ids(current_vips: dict[str, VipPlayer]) -> set[str]: + """Return a set of steam IDs that have indefinite VIP status""" + return { + player_id + for player_id, vip_player in current_vips.items() + if has_indefinite_vip(vip_player) + } + + +def filter_online_players( + vips: dict[str, VipPlayer], players: ServerPopulation +) -> dict[str, VipPlayer]: + """Return a dictionary of players that are online""" + return { + player_id: vip_player + for player_id, vip_player in vips.items() + if player_id in players.players + } + + +def has_indefinite_vip(player: VipPlayer | None) -> bool: + """Return true if the player has an indefinite VIP status""" + if player is None or player.expiration_date is None: + return False + expiration = player.expiration_date + return expiration >= INDEFINITE_VIP_DATE + + +def all_met(conditions: Iterable[BaseCondition]) -> bool: + return all(c.is_met() for c in conditions) + + +def check_population_conditions( + config: SeedVIPUserConfig, gamestate: GameState +) -> bool: + """Return if the current player count is within min/max players for seeding""" + player_count_conditions = [ + PlayerCountCondition( + faction="allies", + min_players=config.requirements.min_allies, + max_players=config.requirements.max_allies, + current_players=gamestate.num_allied_players, + ), + PlayerCountCondition( + faction="axis", + min_players=config.requirements.min_axis, + max_players=config.requirements.max_axis, + current_players=gamestate.num_axis_players, + ), + ] + + logger.debug( + f"{player_count_conditions[0]}={player_count_conditions[0].is_met()} {player_count_conditions[1]}={player_count_conditions[1].is_met()} breaking", + ) + if not all_met(player_count_conditions): + return False + + return True + + +def check_player_conditions( + config: SeedVIPUserConfig, server_pop: ServerPopulation +) -> set[str]: + """Return a set of steam IDs that meet seeding criteria""" + return set( + player.player_id + for player in server_pop.players.values() + if PlayTimeCondition( + min_time_secs=int(config.requirements.minimum_play_time.total_seconds), + current_time_secs=player.current_playtime_seconds, + ).is_met() + ) + + +def is_seeded(config: SeedVIPUserConfig, gamestate: GameState) -> bool: + """Return if the server has enough players to be out of seeding""" + return ( + gamestate.num_allied_players >= config.requirements.max_allies + and gamestate.num_axis_players >= config.requirements.max_axis + ) + + +def calc_vip_expiration_timestamp( + config: SeedVIPUserConfig, expiration: datetime | None, from_time: datetime +) -> datetime: + """Return the players new expiration date accounting for reward/existing timestamps""" + if expiration is None: + timestamp = from_time + config.reward.timeframe.as_timedelta + return timestamp + + if config.reward.cumulative: + return expiration + config.reward.timeframe.as_timedelta + else: + # Don't step on the old expiration if it's further in the future than the new one + timestamp = from_time + config.reward.timeframe.as_timedelta + if timestamp < expiration: + return expiration + else: + return timestamp + + +def collect_steam_ids( + config: SeedVIPUserConfig, + players: ServerPopulation, + cum_steam_ids: set[str], +) -> set[str]: + player_conditions_steam_ids = check_player_conditions( + config=config, server_pop=players + ) + + if config.requirements.online_when_seeded: + cum_steam_ids = set(player_conditions_steam_ids) + else: + cum_steam_ids |= player_conditions_steam_ids + + return cum_steam_ids + + +def format_player_message( + message: str, + vip_reward: timedelta, + vip_expiration: datetime, + nice_time_delta: bool = True, + nice_expiration_date: bool = True, +) -> str: + if nice_time_delta: + delta = naturaldelta(vip_reward) + else: + delta = vip_reward + + if nice_expiration_date: + date = naturaltime(vip_expiration) + else: + date = vip_expiration.isoformat() + + return message.format(vip_reward=delta, vip_expiration=date) + + +def make_seed_announcement_embed( + message: str | None, + current_map: str, + time_remaining: str, + player_count_message: str, + num_axis_players: int, + num_allied_players: int, +) -> discord.Embed | None: + if not message: + return + + logger.debug(f"{num_allied_players=} {num_axis_players=}") + + embed = discord.Embed(title=message) + embed.timestamp = datetime.now(tz=timezone.utc) + embed.add_field(name="Current Map", value=current_map) + embed.add_field(name="Time Remaining", value=time_remaining) + embed.add_field( + name="Players Per Team", + value=player_count_message.format( + num_allied_players=num_allied_players, num_axis_players=num_axis_players + ), + ) + + return embed + + +def format_vip_reward_name(player_name: str, format_str): + return format_str.format(player_name=player_name) + + +def should_announce_seeding_progress( + player_buckets: list[int], + total_players: int, + prev_announced_bucket: int, + next_player_bucket: int, + last_bucket_announced: bool, +) -> bool: + return ( + len(player_buckets) > 0 + and total_players > prev_announced_bucket + and total_players >= next_player_bucket + and not last_bucket_announced + ) + + +def message_players( + rcon: RconAPI, + config: SeedVIPUserConfig, + message: str, + steam_ids: Iterable[str], + expiration_timestamps: defaultdict[str, datetime] | None, +): + for steam_id in steam_ids: + if expiration_timestamps: + formatted_message = format_player_message( + message=message, + vip_reward=config.reward.timeframe.as_timedelta, + vip_expiration=expiration_timestamps[steam_id], + nice_time_delta=config.nice_time_delta, + nice_expiration_date=config.nice_expiration_date, + ) + else: + formatted_message = message + + if config.dry_run: + logger.info(f"{config.dry_run=} messaging {steam_id}: {formatted_message}") + else: + rcon.message_player( + player_id=steam_id, + message=formatted_message, + ) + + +def reward_players( + rcon: RconAPI, + config: SeedVIPUserConfig, + to_add_vip_steam_ids: set[str], + current_vips: dict[str, VipPlayer], + players_lookup: dict[str, str], + expiration_timestamps: defaultdict[str, datetime], +): + logger.info(f"Rewarding players with VIP {config.dry_run=}") + logger.info(f"Total={len(to_add_vip_steam_ids)} {to_add_vip_steam_ids=}") + logger.info(f"Total={len(current_vips)=} {current_vips=}") + for player_id in to_add_vip_steam_ids: + player = current_vips.get(player_id) + expiration_date = expiration_timestamps[player_id] + + if has_indefinite_vip(player): + logger.info( + f"{config.dry_run=} Skipping! pre-existing indefinite VIP for {player_id=} {player=} {vip_name=} {expiration_date=}" + ) + continue + + vip_name = ( + player.player.name + if player + else format_vip_reward_name( + players_lookup.get(player_id, "No player name found"), + format_str=config.player_messages.reward_player_message_no_vip, + ) + ) + + if not config.dry_run: + logger.info( + f"{config.dry_run=} adding VIP to {player_id=} {player=} {vip_name=} {expiration_date=}", + ) + rcon.add_vip( + player_id=player_id, + description=vip_name, + expiration=expiration_date.isoformat(), + ) + + else: + logger.info( + f"{config.dry_run=} adding VIP to {player_id=} {player=} {vip_name=} {expiration_date=}", + ) + + +def get_next_player_bucket( + player_buckets: Sequence[int], + total_players: int, +) -> int | None: + idx = None + for idx, ele in enumerate(player_buckets): + if ele > total_players: + break + + try: + if total_players > player_buckets[-1]: + return player_buckets[-1] + elif idx: + return player_buckets[idx - 1] + except IndexError: + return None + + +def get_online_players( + rcon: RconAPI, +) -> ServerPopulation: + result: list[GetPlayersType] = rcon.get_players() + players = {} + for raw_player in result: + name = raw_player["name"] + player_id = player_id = raw_player["player_id"] + if raw_player["profile"] is None: + # Apparently CRCON will occasionally not return a player profile + logger.debug(f"No CRCON profile, skipping {raw_player}") + continue + current_playtime_seconds = raw_player["profile"]["current_playtime_seconds"] # type: ignore + p = Player( + name=name, + player_id=player_id, + current_playtime_seconds=current_playtime_seconds, + ) + players[p.player_id] = p + + return ServerPopulation(players=players) + + +def get_gamestate(rcon: RconAPI) -> GameState: + result: GameStateType = rcon.get_gamestate() + return GameState.model_validate(result) + + +def get_vips( + rcon: RconAPI, +) -> dict[str, VipPlayer]: + raw_vips: list[VipIdType] = rcon.get_vip_ids() + return { + vip["player_id"]: VipPlayer( + player=Player( + player_id=vip["player_id"], + name=vip["name"], + current_playtime_seconds=0, + ), + expiration_date=vip["vip_expiration"], + ) + for vip in raw_vips + } diff --git a/rcon/types.py b/rcon/types.py index d1b09f172..6df3aa6bd 100644 --- a/rcon/types.py +++ b/rcon/types.py @@ -637,7 +637,7 @@ class ParsedLogsType(TypedDict): logs: list[StructuredLogLineWithMetaData] -class GameState(TypedDict): +class GameStateType(TypedDict): """TypedDict for Rcon.get_gamestate""" num_allied_players: int diff --git a/rcon/user_config/seed_db.py b/rcon/user_config/seed_db.py index 61652849e..a833c3890 100644 --- a/rcon/user_config/seed_db.py +++ b/rcon/user_config/seed_db.py @@ -20,6 +20,7 @@ from rcon.user_config.rcon_server_settings import RconServerSettingsUserConfig from rcon.user_config.real_vip import RealVipUserConfig from rcon.user_config.scorebot import ScorebotUserConfig +from rcon.user_config.seed_vip import SeedVIPUserConfig from rcon.user_config.standard_messages import ( StandardBroadcastMessagesUserConfig, StandardPunishmentMessagesUserConfig, @@ -68,6 +69,7 @@ def seed_default_config(): RconServerSettingsUserConfig.seed_db(sess) RealVipUserConfig.seed_db(sess) ScorebotUserConfig.seed_db(sess) + SeedVIPUserConfig.seed_db(sess) StandardBroadcastMessagesUserConfig.seed_db(sess) StandardPunishmentMessagesUserConfig.seed_db(sess) StandardWelcomeMessagesUserConfig.seed_db(sess) diff --git a/rcon/user_config/seed_vip.py b/rcon/user_config/seed_vip.py new file mode 100644 index 000000000..1d45976f1 --- /dev/null +++ b/rcon/user_config/seed_vip.py @@ -0,0 +1,258 @@ +from datetime import timedelta +from logging import getLogger +from typing import TypedDict + +import pydantic +from pydantic import Field + +import discord + +logger = getLogger(__name__) + +from rcon.user_config.utils import BaseUserConfig, _listType, key_check, set_user_config +from rcon.user_config.webhooks import DiscordWebhook, WebhookType + +SEEDING_IN_PROGRESS_MESSAGE = "Server has reached {player_count} players" +SEEDING_COMPLETE_MESSAGE = "Server is live!" +PLAYER_COUNT_MESSAGE = "{num_allied_players} - {num_axis_players}" +REWARD_PLAYER_MESSAGE = "Thank you for helping us seed.\n\nYou've been granted {vip_reward} of VIP\n\nYour VIP currently expires: {vip_expiration}" +REWARD_PLAYER_MESSAGE_NO_VIP = "Thank you for helping us seed.\n\nThe server is now live and the regular rules apply." + +PLAYER_NAME_FORMAT_NOT_CURRENT_VIP = "{player_name} - CRCON Seed VIP" + + +class RawBufferType(TypedDict): + seconds: int + minutes: int + hours: int + + +class RawMinPlayTimeType(TypedDict): + seconds: int + minutes: int + hours: int + + +class RawRequirementsType(TypedDict): + buffer: RawBufferType + min_allies: int + min_axis: int + max_allies: int + max_axis: int + online_when_seeded: bool + minimum_play_time: RawMinPlayTimeType + + +class RawRewardTimeFrameType(TypedDict): + minutes: int + hours: int + days: int + weeks: int + + +class RawRewardType(TypedDict): + forward: bool + player_name_format_not_current_vip: str + cumulative: bool + timeframe: RawRewardTimeFrameType + + +class RawPlayerMessagesType(TypedDict): + seeding_in_progress_message: str + seeding_complete_message: str + player_count_message: str + reward_player_message: str + reward_player_message_no_vip: str + + +class SeedVIPType(TypedDict): + enabled: bool + dry_run: bool + language: str + hooks: list[WebhookType] + player_announce_thresholds: list[int] + poll_time_seeding: int + poll_time_seeded: int + requirements: RawRequirementsType + nice_time_delta: bool + nice_expiration_date: bool + player_messages: RawPlayerMessagesType + reward: RawRewardType + + +class BufferType(pydantic.BaseModel): + seconds: int = Field(default=0, ge=0) + minutes: int = Field(default=10, ge=0) + hours: int = Field(default=0, ge=0) + + @property + def as_timedelta(self): + return timedelta( + seconds=self.seconds, + minutes=self.minutes, + hours=self.hours, + ) + + +class MinPlayTime(pydantic.BaseModel): + seconds: int = Field(default=0, ge=0) + minutes: int = Field(default=5, ge=0) + hours: int = Field(default=0, ge=0) + + @property + def total_seconds(self): + return int( + timedelta( + seconds=self.seconds, minutes=self.minutes, hours=self.hours + ).total_seconds() + ) + + +class Requirements(pydantic.BaseModel): + buffer: BufferType = Field(default_factory=BufferType) + min_allies: int = Field(default=0, ge=0, le=50) + min_axis: int = Field(default=0, ge=0, le=50) + max_allies: int = Field(default=20, ge=0, le=50) + max_axis: int = Field(default=20, ge=0, le=50) + online_when_seeded: bool = Field(default=True) + minimum_play_time: MinPlayTime = Field(default_factory=MinPlayTime) + + +class PlayerMessages(pydantic.BaseModel): + seeding_in_progress_message: str = Field(default=SEEDING_IN_PROGRESS_MESSAGE) + seeding_complete_message: str = Field(default=SEEDING_COMPLETE_MESSAGE) + player_count_message: str = Field(default=PLAYER_COUNT_MESSAGE) + reward_player_message: str = Field(default=REWARD_PLAYER_MESSAGE) + reward_player_message_no_vip: str = Field(default=REWARD_PLAYER_MESSAGE_NO_VIP) + + +class RewardTimeFrame(pydantic.BaseModel): + minutes: int = Field(default=0, ge=0) + hours: int = Field(default=0, ge=0) + days: int = Field(default=1, ge=0) + weeks: int = Field(default=0, ge=0) + + @property + def as_timedelta(self): + return timedelta( + minutes=self.minutes, hours=self.hours, days=self.days, weeks=self.weeks + ) + + @property + def total_seconds(self): + return int(self.as_timedelta.total_seconds()) + + +class Reward(pydantic.BaseModel): + forward: bool = Field(default=False) + player_name_format_not_current_vip: str = Field( + default=PLAYER_NAME_FORMAT_NOT_CURRENT_VIP + ) + cumulative: bool = Field(default=True) + timeframe: RewardTimeFrame = Field(default_factory=RewardTimeFrame) + + +class SeedVIPUserConfig(BaseUserConfig): + enabled: bool = Field(default=False) + dry_run: bool = Field(default=True) + language: str | None = Field(default="en_US") + hooks: list[DiscordWebhook] = Field(default_factory=list) + player_announce_thresholds: list[int] = Field(default=[10, 20, 30]) + poll_time_seeding: int = Field(default=30, ge=0) + poll_time_seeded: int = Field(default=300, ge=0) + nice_time_delta: bool = Field(default=True) + nice_expiration_date: bool = Field(default=True) + requirements: Requirements = Field(default_factory=Requirements) + player_messages: PlayerMessages = Field(default_factory=PlayerMessages) + reward: Reward = Field(default_factory=Reward) + + @staticmethod + def save_to_db(values: SeedVIPType, dry_run=False): + # logger.info(f"{values=}") + key_check(SeedVIPType.__required_keys__, values.keys()) + logger.info(f"after key_check") + + raw_hooks: list[WebhookType] = values.get("hooks") + _listType(values=raw_hooks) + + logger.info(f"after listType check") + for obj in raw_hooks: + key_check(WebhookType.__required_keys__, obj.keys()) + + logger.info(f"after key_check for webhooks") + validated_hooks = [DiscordWebhook(url=obj.get("url")) for obj in raw_hooks] + + logger.info(f"after hooks validated") + raw_player_messages = values.get("player_messages") + raw_requirements = values.get("requirements") + raw_buffer = raw_requirements.get("buffer") + raw_min_play_time = raw_requirements.get("minimum_play_time") + raw_reward = values.get("reward") + raw_reward_time_frame = raw_reward.get("timeframe") + + validated_player_messages = PlayerMessages( + seeding_in_progress_message=raw_player_messages.get( + "seeding_in_progress_message" + ), + seeding_complete_message=raw_player_messages.get( + "seeding_complete_message" + ), + player_count_message=raw_player_messages.get("player_count_message"), + reward_player_message=raw_player_messages.get("reward_player_message"), + reward_player_message_no_vip=raw_player_messages.get( + "reward_player_message_no_vip" + ), + ) + + validated_requirements = Requirements( + buffer=BufferType( + seconds=raw_buffer.get("seconds"), + minutes=raw_buffer.get("minutes"), + hours=raw_buffer.get("hours"), + ), + min_allies=raw_requirements.get("min_allies"), + max_allies=raw_requirements.get("max_allies"), + min_axis=raw_requirements.get("min_axis"), + max_axis=raw_requirements.get("max_axis"), + online_when_seeded=raw_requirements.get("online_when_seeded"), + minimum_play_time=MinPlayTime( + seconds=raw_min_play_time.get("seconds"), + minutes=raw_min_play_time.get("minutes"), + hours=raw_min_play_time.get("hours"), + ), + ) + + validated_reward_time_frame = RewardTimeFrame( + minutes=raw_reward_time_frame.get("minutes"), + hours=raw_reward_time_frame.get("hours"), + days=raw_reward_time_frame.get("days"), + weeks=raw_reward_time_frame.get("weeks"), + ) + + validated_reward = Reward( + forward=raw_reward.get("forward"), + player_name_format_not_current_vip=raw_reward.get( + "player_name_format_not_current_vip" + ), + cumulative=raw_reward.get("cumulative"), + timeframe=validated_reward_time_frame, + ) + + validated_conf = SeedVIPUserConfig( + enabled=values.get("enabled"), + dry_run=values.get("dry_run"), + language=values.get("language"), + hooks=validated_hooks, + player_announce_thresholds=values.get("player_announce_thresholds"), + poll_time_seeding=values.get("poll_time_seeding"), + poll_time_seeded=values.get("poll_time_seeded"), + nice_time_delta=values.get("nice_time_delta"), + nice_expiration_date=values.get("nice_expiration_date"), + requirements=validated_requirements, + player_messages=validated_player_messages, + reward=validated_reward, + ) + + if not dry_run: + logger.info(f"setting {validated_conf=}") + set_user_config(SeedVIPUserConfig.KEY(), validated_conf) diff --git a/rcongui/src/App.js b/rcongui/src/App.js index 3a212e54f..79fc6a088 100644 --- a/rcongui/src/App.js +++ b/rcongui/src/App.js @@ -54,6 +54,7 @@ import { GTXNameChange, ChatCommands, LogStream, + SeedVIP, } from "./components/UserSettings/miscellaneous"; import BlacklistRecords from "./components/Blacklist/BlacklistRecords"; import BlacklistLists from "./components/Blacklist/BlacklistLists"; @@ -432,7 +433,9 @@ function App() { @@ -720,6 +723,17 @@ function App() { /> + + + + + diff --git a/rcongui/src/components/Header/header.js b/rcongui/src/components/Header/header.js index fe35722b8..6a3142b19 100644 --- a/rcongui/src/components/Header/header.js +++ b/rcongui/src/components/Header/header.js @@ -12,31 +12,33 @@ import { navMenus } from "./nav-data"; import { LoginBox } from "./login"; import { Box, createStyles, makeStyles } from "@material-ui/core"; -const useStyles = makeStyles((theme) => createStyles({ - root: { - display: "flex", - flexGrow: 1, - flexDirection: "column", - justifyContent: "center", - alignItems: "start", - padding: theme.spacing(0.25), - minHeight: 0, - gap: theme.spacing(0.25), - [theme.breakpoints.up("md")]: { +const useStyles = makeStyles((theme) => + createStyles({ + root: { + display: "flex", + flexGrow: 1, + flexDirection: "column", + justifyContent: "center", + alignItems: "start", + padding: theme.spacing(0.25), + minHeight: 0, + gap: theme.spacing(0.25), + [theme.breakpoints.up("md")]: { + flexDirection: "row", + justifyContent: "space-between", + alignItems: "center", + gap: theme.spacing(2), + padding: theme.spacing(0.5), + }, + }, + nav: { + display: "flex", flexDirection: "row", + flexGrow: 1, justifyContent: "space-between", - alignItems: "center", - gap: theme.spacing(2), - padding: theme.spacing(0.5), - } - }, - nav: { - display: "flex", - flexDirection: "row", - flexGrow: 1, - justifyContent: "space-between", - }, -})) + }, + }) +); const initialMenuState = navMenus.reduce((state, menu) => { state[menu.name] = false; @@ -47,7 +49,7 @@ const initialMenuState = navMenus.reduce((state, menu) => { const Header = ({ classes }) => { const [openedMenu, setOpenedMenu] = React.useState(initialMenuState); const [anchorEl, setAnchorEl] = React.useState(null); - const localClasses = useStyles() + const localClasses = useStyles(); const handleOpenMenu = (name) => (event) => { setOpenedMenu({ @@ -85,12 +87,18 @@ const Header = ({ classes }) => { onClose={handleCloseMenu(menu.name)} PaperProps={{ style: { - minWidth: '20ch', + minWidth: "20ch", }, }} > {menu.links.map((link) => ( - + {link.name} ))} diff --git a/rcongui/src/components/Header/nav-data.js b/rcongui/src/components/Header/nav-data.js index 9fa411637..1f9c5ffe1 100644 --- a/rcongui/src/components/Header/nav-data.js +++ b/rcongui/src/components/Header/nav-data.js @@ -46,7 +46,7 @@ export const navMenus = [ }, { name: "Map Manager", - to: "/settings/maps/change" + to: "/settings/maps/change", }, { name: "Audit Webhooks", @@ -136,6 +136,10 @@ export const navMenus = [ name: "Log Stream", to: "/settings/log-stream", }, + { + name: "Seed VIP", + to: "/settings/seed-vip", + }, ], }, { diff --git a/rcongui/src/components/UserSettings/miscellaneous.js b/rcongui/src/components/UserSettings/miscellaneous.js index b115b7644..859708f65 100644 --- a/rcongui/src/components/UserSettings/miscellaneous.js +++ b/rcongui/src/components/UserSettings/miscellaneous.js @@ -656,3 +656,100 @@ export const LogStream = ({ /> ); }; + +export const SeedVIP = ({ + description, + getEndpoint, + setEndpoint, + validateEndpoint, + describeEndpoint, +}) => { + const notes = ` + { + "enabled": true, + "dry_run": false, + + /* Any one of these language codes should work: 'ar_JO', 'ar_DZ', 'de_DE', 'ru_RU', 'sv_SE', 'uk_UA', 'ar_EG', 'ar_MA', 'ja_JP', 'sk_SK', 'ar_BH', 'fr_FR', 'iw_IL', 'sl_SI', 'bn_BD', 'eu_ES', 'ar_AE', 'id_ID', 'ar_KW', 'ca_ES', 'ar_LY', 'it_IT', 'ar_SD', 'ar_SY', 'es_ES', 'ar_SA', 'pt_PT', 'zh_HK', 'da_DK', 'tlh_QS', 'ar_IQ', 'ko_KR', 'pl_PL', 'vi_VN', 'nl_NL', 'ar_OM', 'fa_IR', 'ar_TN', 'ar_LB', 'tr_TR', 'fi_FI', 'ar_QA', 'hu_HU', 'ar_YE', 'in_ID', 'nb_NO', 'zh_CN', 'el_GR', 'he_IL', 'pt_BR' */ + "language": null, + + /* A list of webhooks for reporting seeding status*/ + "hooks": [ + {"url": "https://...."} + ], + /* Which player counts to post an announcement for if any webhooks are set */ + "player_announce_thresholds": [ + 10, + 20, + 30 + ], + /* How frequently to check when the server is below the seeding cut off */ + "poll_time_seeding": 30, + /* How frequently to check when the server is above the seeding cut off */ + "poll_time_seeded": 300, + + /* Uses humane style dates/times when enabled (in X days, hours, etc style messages) */ + "nice_time_delta": true, + "nice_expiration_date": true, + + "requirements": { + /* How long after the server has seeded to check, this prevents message/VIP spamming if you hover near your seed limit */ + "buffer": { + "seconds": 0, + "minutes": 10, + "hours": 0 + }, + /* The minimum players before you consider seeding by team */ + "min_allies": 0, + "min_axis": 0, + /* The number of players for the server to be 'seeded' */ + "max_allies": 25, + "max_axis": 25, + /* Whether a player needs to still be connected when the server seeds to get VIP */ + "online_when_seeded": true, + /* The minimum amount of time a player has to be connected during seeding to be rewarded, it is the sum of all of the fields */ + "minimum_play_time": { + "seconds": 0, + "minutes": 5, + "hours": 0 + } + }, + "player_messages": { + /* The format of the discord webhook messages */ + "seeding_in_progress_message": "Server has reached {player_count} players", + "seeding_complete_message": "Server is live!", + "player_count_message": "{num_allied_players} - {num_axis_players}", + + /* The message sent to a connected player when they are granted VIP */ + "reward_player_message": "Thank you for helping us seed.\n\nYou've been granted {vip_reward} of VIP\n\nYour VIP currently expires: {vip_expiration}", + /* The message sent to a connected player when they did not earn VIP */ + "reward_player_message_no_vip": "Thank you for helping us seed.\n\nThe server is now live and the regular rules apply." + }, + "reward": { + /* Whether to send the VIP command to the other connected game servers hosted in the same CRCON */ + "forward": false, + /* The description when adding VIP to a player who does not have VIP */ + "player_name_format_not_current_vip": "{player_name} - CRCON Seed VIP", + /* When true, it will add their VIP reward if they have VIP, false will overwrite */ + "cumulative": true, + /* The VIP time to give to the player, it is the sum of all of the fields */ + "timeframe": { + "minutes": 0, + "hours": 0, + "days": 1, + "weeks": 0 + } + } + } + `; + + return ( + + ); +}; diff --git a/rconweb/api/migrations/0003_create_default_groups.py b/rconweb/api/migrations/0003_create_default_groups.py index cf7e4d824..b302aadcf 100644 --- a/rconweb/api/migrations/0003_create_default_groups.py +++ b/rconweb/api/migrations/0003_create_default_groups.py @@ -192,6 +192,8 @@ "can_change_blacklists", "can_delete_blacklists", "can_change_game_layout", + "can_view_seed_vip_config", + "can_change_seed_vip_config", ), ), ( @@ -374,6 +376,8 @@ "can_change_blacklists", "can_delete_blacklists", "can_change_game_layout", + "can_view_seed_vip_config", + "can_change_seed_vip_config", ), ), ( diff --git a/rconweb/api/migrations/0014_alter_rconuser_options.py b/rconweb/api/migrations/0014_alter_rconuser_options.py index 2e3eeb74b..e135a3841 100644 --- a/rconweb/api/migrations/0014_alter_rconuser_options.py +++ b/rconweb/api/migrations/0014_alter_rconuser_options.py @@ -6,12 +6,541 @@ class Migration(migrations.Migration): dependencies = [ - ('api', '0013_delete_deprecated_permissions'), + ("api", "0013_delete_deprecated_permissions"), ] operations = [ migrations.AlterModelOptions( - name='rconuser', - options={'default_permissions': (), 'permissions': (('can_add_admin_roles', 'Can add HLL game server admin roles to players'), ('can_add_map_to_rotation', 'Can add a map to the rotation'), ('can_add_map_to_whitelist', 'Can add a map to the votemap whitelist'), ('can_add_maps_to_rotation', 'Can add maps to the rotation'), ('can_add_maps_to_whitelist', 'Can add multiple maps to the votemap whitelist'), ('can_add_player_comments', 'Can add comments to a players profile'), ('can_add_player_watch', 'Can add a watch to players'), ('can_add_vip', 'Can add VIP status to players'), ('can_ban_profanities', 'Can ban profanities (censored game chat)'), ('can_change_auto_broadcast_config', 'Can change the automated broadcast settings'), ('can_change_auto_settings', 'Can change auto settings'), ('can_change_autobalance_enabled', 'Can enable/disable autobalance'), ('can_change_autobalance_threshold', 'Can change the autobalance threshold'), ('can_change_broadcast_message', 'Can change the broadcast message'), ('can_change_camera_config', 'Can change camera notification settings'), ('can_change_current_map', 'Can change the current map'), ('can_change_discord_webhooks', 'Can change configured webhooks on the settings page'), ('can_change_idle_autokick_time', 'Can change the idle autokick time'), ('can_change_max_ping_autokick', 'Can change the max ping autokick'), ('can_change_profanities', 'Can add/remove profanities (censored game chat)'), ('can_change_queue_length', 'Can change the server queue size'), ('can_change_real_vip_config', 'Can change the real VIP settings'), ('can_change_server_name', 'Can change the server name'), ('can_change_shared_standard_messages', 'Can change the shared standard messages'), ('can_change_team_switch_cooldown', 'Can change the team switch cooldown'), ('can_change_vip_slots', 'Can change the number of reserved VIP slots'), ('can_change_votekick_autotoggle_config', 'Can change votekick settings'), ('can_change_votekick_enabled', 'Can enable/disable vote kicks'), ('can_change_votekick_threshold', 'Can change vote kick thresholds'), ('can_change_votemap_config', 'Can change the votemap settings'), ('can_change_welcome_message', 'Can change the welcome (rules) message'), ('can_clear_crcon_cache', 'Can clear the CRCON Redis cache'), ('can_download_vip_list', 'Can download the VIP list'), ('can_flag_player', 'Can add flags to players'), ('can_kick_players', 'Can kick players'), ('can_message_players', 'Can message players'), ('can_perma_ban_players', 'Can permanently ban players'), ('can_punish_players', 'Can punish players'), ('can_remove_admin_roles', 'Can remove HLL game server admin roles from players'), ('can_remove_all_vips', 'Can remove all VIPs'), ('can_remove_map_from_rotation', 'Can remove a map from the rotation'), ('can_remove_map_from_whitelist', 'Can remove a map from the votemap whitelist'), ('can_remove_maps_from_rotation', 'Can remove maps from the rotation'), ('can_remove_maps_from_whitelist', 'Can remove multiple maps from the votemap whitelist'), ('can_remove_perma_bans', 'Can remove permanent bans from players'), ('can_remove_player_watch', 'Can remove a watch from players'), ('can_remove_temp_bans', 'Can remove temporary bans from players'), ('can_remove_vip', 'Can remove VIP status from players'), ('can_reset_map_whitelist', 'Can reset the votemap whitelist'), ('can_reset_votekick_threshold', 'Can reset votekick thresholds'), ('can_reset_votemap_state', 'Can reset votemap selection & votes'), ('can_run_raw_commands', 'Can send raw commands to the HLL game server'), ('can_set_map_whitelist', 'Can set the votemap whitelist'), ('can_switch_players_immediately', 'Can immediately switch players'), ('can_switch_players_on_death', 'Can switch players on death'), ('can_temp_ban_players', 'Can temporarily ban players'), ('can_toggle_services', 'Can enable/disable services (automod, etc)'), ('can_unban_profanities', 'Can unban profanities (censored game chat)'), ('can_unflag_player', 'Can remove flags from players'), ('can_upload_vip_list', 'Can upload a VIP list'), ('can_view_admin_groups', 'Can view available admin roles'), ('can_view_admin_ids', 'Can view the name/steam IDs/role of everyone with a HLL game server admin role'), ('can_view_admins', 'Can view users with HLL game server admin roles'), ('can_view_all_maps', 'Can view all possible maps'), ('can_view_audit_logs', 'Can view the can_view_audit_logs endpoint'), ('can_view_audit_logs_autocomplete', 'Can view the get_audit_logs_autocomplete endpoint'), ('can_view_auto_broadcast_config', 'Can view the automated broadcast settings'), ('can_view_auto_settings', 'Can view auto settings'), ('can_view_autobalance_enabled', 'Can view if autobalance is enabled'), ('can_view_autobalance_threshold', 'Can view the autobalance threshold'), ('can_view_available_services', 'Can view services (automod, etc)'), ('can_view_broadcast_message', 'Can view the current broadcast message'), ('can_view_camera_config', 'Can view camera notification settings'), ('can_view_connection_info', "Can view CRCON's connection info"), ('can_view_current_map', 'Can view the currently playing map'), ('can_view_date_scoreboard', 'Can view the date_scoreboard endpoint'), ('can_view_detailed_player_info', 'Can view detailed player info (name, steam ID, loadout, squad, etc.)'), ('can_view_discord_webhooks', 'Can view configured webhooks on the settings page'), ('can_view_game_logs', 'Can view the get_logs endpoint (returns unparsed game logs)'), ('can_view_gamestate', 'Can view the current gamestate'), ('can_view_get_players', 'Can view get_players endpoint (name, steam ID, VIP status and sessions) for all connected players'), ('can_view_get_status', 'Can view the get_status endpoint (server name, current map, player count)'), ('can_view_historical_logs', 'Can view historical logs'), ('can_view_idle_autokick_time', 'Can view the idle autokick time'), ('can_view_ingame_admins', 'Can view admins connected to the game server'), ('can_view_map_rotation', 'Can view the current map rotation'), ('can_view_map_whitelist', 'Can view the votemap whitelist'), ('can_view_max_ping_autokick', 'Can view the max autokick ping'), ('can_view_next_map', 'Can view the next map in the rotation'), ('can_view_online_admins', 'Can view admins connected to CRCON'), ('can_view_online_console_admins', 'Can view the player name of all connected players with a HLL game server admin role'), ('can_view_other_crcon_servers', 'Can view other servers hosted in the same CRCON (forward to all servers)'), ('can_view_perma_bans', 'Can view permanently banned players'), ('can_view_player_bans', 'Can view all bans (temp/permanent) for a specific player'), ('can_view_player_comments', 'Can view comments added to a players profile'), ('can_view_player_history', 'Can view History > Players'), ('can_view_player_info', 'Can view the get_player_info endpoint (Name, steam ID, country and steam bans)'), ('can_view_player_messages', 'Can view messages sent to players'), ('can_view_player_profile', 'View the detailed player profile page'), ('can_view_player_slots', 'Can view the current/max players on the server'), ('can_view_playerids', 'Can view the get_playerids endpoint (name and steam IDs of connected players)'), ('can_view_players', 'Can view get_players endpoint for all connected players '), ('can_view_profanities', 'Can view profanities (censored game chat)'), ('can_view_queue_length', 'Can view the maximum size of the server queue'), ('can_view_real_vip_config', 'Can view the real VIP settings'), ('can_view_recent_logs', 'Can view recent logs (Live view)'), ('can_view_round_time_remaining', 'Can view the amount of time left in the round'), ('can_view_server_name', 'Can view the server name'), ('can_view_shared_standard_messages', 'Can view the shared standard messages'), ('can_view_structured_logs', 'Can view the get_structured_logs endpoint'), ('can_view_team_objective_scores', 'Can view the number of objectives held by each team'), ('can_view_team_switch_cooldown', 'Can view the team switch cooldown value'), ('can_view_detailed_players', 'Can view get_detailed_players endpoint'), ('can_view_team_view', 'Can view get_team_view endpoint (detailed player info by team for all connected players)'), ('can_view_temp_bans', 'Can view temporary banned players'), ('can_view_vip_count', 'Can view the number of connected VIPs'), ('can_view_vip_ids', 'Can view all players with VIP and their expiration timestamps'), ('can_view_vip_slots', 'Can view the number of reserved VIP slots'), ('can_view_votekick_autotoggle_config', 'Can view votekick settings'), ('can_view_votekick_enabled', 'Can view if vote kick is enabled'), ('can_view_votekick_threshold', 'Can view the vote kick thresholds'), ('can_view_votemap_config', 'Can view the votemap settings'), ('can_view_votemap_status', 'Can view the current votemap status (votes, results, etc)'), ('can_view_current_map_sequence', 'Can view the current map shuffle sequence'), ('can_view_map_shuffle_enabled', 'Can view if map shuffle is enabled'), ('can_change_map_shuffle_enabled', 'Can enable/disable map shuffle'), ('can_view_welcome_message', 'Can view the server welcome message'), ('can_view_auto_mod_level_config', 'Can view Auto Mod Level enforcement config'), ('can_change_auto_mod_level_config', 'Can change Auto Mod Level enforcement config'), ('can_view_auto_mod_no_leader_config', 'Can view Auto Mod No Leader enforcement config'), ('can_change_auto_mod_no_leader_config', 'Can change Auto Mod No Leader enforcement config'), ('can_view_auto_mod_seeding_config', 'Can view Auto Mod No Seeding enforcement config'), ('can_change_auto_mod_seeding_config', 'Can change Auto Mod No Seeding enforcement config'), ('can_view_auto_mod_solo_tank_config', 'Can view Auto Mod No Solo Tank enforcement config'), ('can_change_auto_mod_solo_tank_config', 'Can change Auto Mod No Solo Tank enforcement config'), ('can_view_tk_ban_on_connect_config', 'Can view team kill ban on connect config'), ('can_change_tk_ban_on_connect_config', 'Can change team kill ban on connect config'), ('can_view_expired_vip_config', 'Can view Expired VIP config'), ('can_change_expired_vip_config', 'Can change Expired VIP config'), ('can_view_server_name_change_config', 'Can view server name change (GSP credentials!) config'), ('can_change_server_name_change_config', 'Can change server name change (GSP credentials!) config'), ('can_view_log_line_discord_webhook_config', 'Can view log webhook (messages for log events) config'), ('can_change_log_line_discord_webhook_config', 'Can change log webhook (messages for log events) config'), ('can_view_name_kick_config', 'Can view kick players for names config'), ('can_change_name_kick_config', 'Can change kick players for names config'), ('can_view_rcon_connection_settings_config', 'Can view game server connection settings config'), ('can_change_rcon_connection_settings_config', 'Can change game server connection settings config'), ('can_view_rcon_server_settings_config', 'Can view general CRCON server settings'), ('can_change_rcon_server_settings_config', 'Can change general CRCON server settings'), ('can_view_scorebot_config', 'Can view scorebot config'), ('can_change_scorebot_config', 'Can change scorebot config'), ('can_view_standard_broadcast_messages', 'Can view shared broadcast messages'), ('can_change_standard_broadcast_messages', 'Can change shared broadcast messages'), ('can_view_standard_punishment_messages', 'Can view shared punishment messages'), ('can_change_standard_punishment_messages', 'Can change shared punishment messages'), ('can_view_standard_welcome_messages', 'Can view shared welcome messages'), ('can_change_standard_welcome_messages', 'Can change shared welcome messages'), ('can_view_steam_config', 'Can view steam API config'), ('can_change_steam_config', 'Can change steam API config'), ('can_view_vac_game_bans_config', 'Can view VAC/Gameban ban on connect config'), ('can_change_vac_game_bans_config', 'Can change VAC/Gameban ban on connect config'), ('can_view_admin_pings_discord_webhooks_config', 'Can view Discord admin ping config'), ('can_change_admin_pings_discord_webhooks_config', 'Can change Discord admin ping config'), ('can_view_audit_discord_webhooks_config', 'Can view Discord audit config'), ('can_change_audit_discord_webhooks_config', 'Can change Discord audit config'), ('can_view_camera_discord_webhooks_config', 'Can view Discord admin cam notification config'), ('can_change_camera_discord_webhooks_config', 'Can change Discord admin cam notification config'), ('can_view_chat_discord_webhooks_config', 'Can view Discord chat notification config'), ('can_change_chat_discord_webhooks_config', 'Can change Discord chat notification config'), ('can_view_kills_discord_webhooks_config', 'Can view Discord team/teamkill notification config'), ('can_change_kills_discord_webhooks_config', 'Can change Discord team/teamkill notification config'), ('can_view_watchlist_discord_webhooks_config', 'Can view Discord player watchlist notification config'), ('can_change_watchlist_discord_webhooks_config', 'Can change Discord player watchlist notification config'), ('can_restart_webserver', 'Can restart the webserver (Not a complete Docker restart)'), ('can_view_chat_commands_config', 'Can view the chat commands config'), ('can_change_chat_commands_config', 'Can change the chat commands config'), ('can_view_log_stream_config', 'Can view the Log Stream config'), ('can_change_log_stream_config', 'Can change the Log Stream config'), ('can_view_blacklists', 'Can view available blacklists'), ('can_add_blacklist_records', 'Can add players to blacklists'), ('can_change_blacklist_records', 'Can unblacklist players and edit blacklist records'), ('can_delete_blacklist_records', 'Can delete blacklist records'), ('can_create_blacklists', 'Can create blacklists'), ('can_change_blacklists', 'Can change blacklists'), ('can_delete_blacklists', 'Can delete blacklists'), ('can_change_game_layout', 'Can change game layout'))}, + name="rconuser", + options={ + "default_permissions": (), + "permissions": ( + ( + "can_add_admin_roles", + "Can add HLL game server admin roles to players", + ), + ("can_add_map_to_rotation", "Can add a map to the rotation"), + ( + "can_add_map_to_whitelist", + "Can add a map to the votemap whitelist", + ), + ("can_add_maps_to_rotation", "Can add maps to the rotation"), + ( + "can_add_maps_to_whitelist", + "Can add multiple maps to the votemap whitelist", + ), + ( + "can_add_player_comments", + "Can add comments to a players profile", + ), + ("can_add_player_watch", "Can add a watch to players"), + ("can_add_vip", "Can add VIP status to players"), + ("can_ban_profanities", "Can ban profanities (censored game chat)"), + ( + "can_change_auto_broadcast_config", + "Can change the automated broadcast settings", + ), + ("can_change_auto_settings", "Can change auto settings"), + ( + "can_change_autobalance_enabled", + "Can enable/disable autobalance", + ), + ( + "can_change_autobalance_threshold", + "Can change the autobalance threshold", + ), + ( + "can_change_broadcast_message", + "Can change the broadcast message", + ), + ( + "can_change_camera_config", + "Can change camera notification settings", + ), + ("can_change_current_map", "Can change the current map"), + ( + "can_change_discord_webhooks", + "Can change configured webhooks on the settings page", + ), + ( + "can_change_idle_autokick_time", + "Can change the idle autokick time", + ), + ( + "can_change_max_ping_autokick", + "Can change the max ping autokick", + ), + ( + "can_change_profanities", + "Can add/remove profanities (censored game chat)", + ), + ("can_change_queue_length", "Can change the server queue size"), + ("can_change_real_vip_config", "Can change the real VIP settings"), + ("can_change_server_name", "Can change the server name"), + ( + "can_change_shared_standard_messages", + "Can change the shared standard messages", + ), + ( + "can_change_team_switch_cooldown", + "Can change the team switch cooldown", + ), + ( + "can_change_vip_slots", + "Can change the number of reserved VIP slots", + ), + ( + "can_change_votekick_autotoggle_config", + "Can change votekick settings", + ), + ("can_change_votekick_enabled", "Can enable/disable vote kicks"), + ( + "can_change_votekick_threshold", + "Can change vote kick thresholds", + ), + ("can_change_votemap_config", "Can change the votemap settings"), + ( + "can_change_welcome_message", + "Can change the welcome (rules) message", + ), + ("can_clear_crcon_cache", "Can clear the CRCON Redis cache"), + ("can_download_vip_list", "Can download the VIP list"), + ("can_flag_player", "Can add flags to players"), + ("can_kick_players", "Can kick players"), + ("can_message_players", "Can message players"), + ("can_perma_ban_players", "Can permanently ban players"), + ("can_punish_players", "Can punish players"), + ( + "can_remove_admin_roles", + "Can remove HLL game server admin roles from players", + ), + ("can_remove_all_vips", "Can remove all VIPs"), + ( + "can_remove_map_from_rotation", + "Can remove a map from the rotation", + ), + ( + "can_remove_map_from_whitelist", + "Can remove a map from the votemap whitelist", + ), + ( + "can_remove_maps_from_rotation", + "Can remove maps from the rotation", + ), + ( + "can_remove_maps_from_whitelist", + "Can remove multiple maps from the votemap whitelist", + ), + ("can_remove_perma_bans", "Can remove permanent bans from players"), + ("can_remove_player_watch", "Can remove a watch from players"), + ("can_remove_temp_bans", "Can remove temporary bans from players"), + ("can_remove_vip", "Can remove VIP status from players"), + ("can_reset_map_whitelist", "Can reset the votemap whitelist"), + ("can_reset_votekick_threshold", "Can reset votekick thresholds"), + ("can_reset_votemap_state", "Can reset votemap selection & votes"), + ( + "can_run_raw_commands", + "Can send raw commands to the HLL game server", + ), + ("can_set_map_whitelist", "Can set the votemap whitelist"), + ( + "can_switch_players_immediately", + "Can immediately switch players", + ), + ("can_switch_players_on_death", "Can switch players on death"), + ("can_temp_ban_players", "Can temporarily ban players"), + ( + "can_toggle_services", + "Can enable/disable services (automod, etc)", + ), + ( + "can_unban_profanities", + "Can unban profanities (censored game chat)", + ), + ("can_unflag_player", "Can remove flags from players"), + ("can_upload_vip_list", "Can upload a VIP list"), + ("can_view_admin_groups", "Can view available admin roles"), + ( + "can_view_admin_ids", + "Can view the name/steam IDs/role of everyone with a HLL game server admin role", + ), + ( + "can_view_admins", + "Can view users with HLL game server admin roles", + ), + ("can_view_all_maps", "Can view all possible maps"), + ( + "can_view_audit_logs", + "Can view the can_view_audit_logs endpoint", + ), + ( + "can_view_audit_logs_autocomplete", + "Can view the get_audit_logs_autocomplete endpoint", + ), + ( + "can_view_auto_broadcast_config", + "Can view the automated broadcast settings", + ), + ("can_view_auto_settings", "Can view auto settings"), + ( + "can_view_autobalance_enabled", + "Can view if autobalance is enabled", + ), + ( + "can_view_autobalance_threshold", + "Can view the autobalance threshold", + ), + ("can_view_available_services", "Can view services (automod, etc)"), + ( + "can_view_broadcast_message", + "Can view the current broadcast message", + ), + ("can_view_camera_config", "Can view camera notification settings"), + ("can_view_connection_info", "Can view CRCON's connection info"), + ("can_view_current_map", "Can view the currently playing map"), + ( + "can_view_date_scoreboard", + "Can view the date_scoreboard endpoint", + ), + ( + "can_view_detailed_player_info", + "Can view detailed player info (name, steam ID, loadout, squad, etc.)", + ), + ( + "can_view_discord_webhooks", + "Can view configured webhooks on the settings page", + ), + ( + "can_view_game_logs", + "Can view the get_logs endpoint (returns unparsed game logs)", + ), + ("can_view_gamestate", "Can view the current gamestate"), + ( + "can_view_get_players", + "Can view get_players endpoint (name, steam ID, VIP status and sessions) for all connected players", + ), + ( + "can_view_get_status", + "Can view the get_status endpoint (server name, current map, player count)", + ), + ("can_view_historical_logs", "Can view historical logs"), + ("can_view_idle_autokick_time", "Can view the idle autokick time"), + ( + "can_view_ingame_admins", + "Can view admins connected to the game server", + ), + ("can_view_map_rotation", "Can view the current map rotation"), + ("can_view_map_whitelist", "Can view the votemap whitelist"), + ("can_view_max_ping_autokick", "Can view the max autokick ping"), + ("can_view_next_map", "Can view the next map in the rotation"), + ("can_view_online_admins", "Can view admins connected to CRCON"), + ( + "can_view_online_console_admins", + "Can view the player name of all connected players with a HLL game server admin role", + ), + ( + "can_view_other_crcon_servers", + "Can view other servers hosted in the same CRCON (forward to all servers)", + ), + ("can_view_perma_bans", "Can view permanently banned players"), + ( + "can_view_player_bans", + "Can view all bans (temp/permanent) for a specific player", + ), + ( + "can_view_player_comments", + "Can view comments added to a players profile", + ), + ("can_view_player_history", "Can view History > Players"), + ( + "can_view_player_info", + "Can view the get_player_info endpoint (Name, steam ID, country and steam bans)", + ), + ("can_view_player_messages", "Can view messages sent to players"), + ( + "can_view_player_profile", + "View the detailed player profile page", + ), + ( + "can_view_player_slots", + "Can view the current/max players on the server", + ), + ( + "can_view_playerids", + "Can view the get_playerids endpoint (name and steam IDs of connected players)", + ), + ( + "can_view_players", + "Can view get_players endpoint for all connected players ", + ), + ( + "can_view_profanities", + "Can view profanities (censored game chat)", + ), + ( + "can_view_queue_length", + "Can view the maximum size of the server queue", + ), + ("can_view_real_vip_config", "Can view the real VIP settings"), + ("can_view_recent_logs", "Can view recent logs (Live view)"), + ( + "can_view_round_time_remaining", + "Can view the amount of time left in the round", + ), + ("can_view_server_name", "Can view the server name"), + ( + "can_view_shared_standard_messages", + "Can view the shared standard messages", + ), + ( + "can_view_structured_logs", + "Can view the get_structured_logs endpoint", + ), + ( + "can_view_team_objective_scores", + "Can view the number of objectives held by each team", + ), + ( + "can_view_team_switch_cooldown", + "Can view the team switch cooldown value", + ), + ( + "can_view_detailed_players", + "Can view get_detailed_players endpoint", + ), + ( + "can_view_team_view", + "Can view get_team_view endpoint (detailed player info by team for all connected players)", + ), + ("can_view_temp_bans", "Can view temporary banned players"), + ("can_view_vip_count", "Can view the number of connected VIPs"), + ( + "can_view_vip_ids", + "Can view all players with VIP and their expiration timestamps", + ), + ("can_view_vip_slots", "Can view the number of reserved VIP slots"), + ( + "can_view_votekick_autotoggle_config", + "Can view votekick settings", + ), + ("can_view_votekick_enabled", "Can view if vote kick is enabled"), + ( + "can_view_votekick_threshold", + "Can view the vote kick thresholds", + ), + ("can_view_votemap_config", "Can view the votemap settings"), + ( + "can_view_votemap_status", + "Can view the current votemap status (votes, results, etc)", + ), + ( + "can_view_current_map_sequence", + "Can view the current map shuffle sequence", + ), + ( + "can_view_map_shuffle_enabled", + "Can view if map shuffle is enabled", + ), + ( + "can_change_map_shuffle_enabled", + "Can enable/disable map shuffle", + ), + ("can_view_welcome_message", "Can view the server welcome message"), + ( + "can_view_auto_mod_level_config", + "Can view Auto Mod Level enforcement config", + ), + ( + "can_change_auto_mod_level_config", + "Can change Auto Mod Level enforcement config", + ), + ( + "can_view_auto_mod_no_leader_config", + "Can view Auto Mod No Leader enforcement config", + ), + ( + "can_change_auto_mod_no_leader_config", + "Can change Auto Mod No Leader enforcement config", + ), + ( + "can_view_auto_mod_seeding_config", + "Can view Auto Mod No Seeding enforcement config", + ), + ( + "can_change_auto_mod_seeding_config", + "Can change Auto Mod No Seeding enforcement config", + ), + ( + "can_view_auto_mod_solo_tank_config", + "Can view Auto Mod No Solo Tank enforcement config", + ), + ( + "can_change_auto_mod_solo_tank_config", + "Can change Auto Mod No Solo Tank enforcement config", + ), + ( + "can_view_tk_ban_on_connect_config", + "Can view team kill ban on connect config", + ), + ( + "can_change_tk_ban_on_connect_config", + "Can change team kill ban on connect config", + ), + ("can_view_expired_vip_config", "Can view Expired VIP config"), + ("can_change_expired_vip_config", "Can change Expired VIP config"), + ( + "can_view_server_name_change_config", + "Can view server name change (GSP credentials!) config", + ), + ( + "can_change_server_name_change_config", + "Can change server name change (GSP credentials!) config", + ), + ( + "can_view_log_line_discord_webhook_config", + "Can view log webhook (messages for log events) config", + ), + ( + "can_change_log_line_discord_webhook_config", + "Can change log webhook (messages for log events) config", + ), + ( + "can_view_name_kick_config", + "Can view kick players for names config", + ), + ( + "can_change_name_kick_config", + "Can change kick players for names config", + ), + ( + "can_view_rcon_connection_settings_config", + "Can view game server connection settings config", + ), + ( + "can_change_rcon_connection_settings_config", + "Can change game server connection settings config", + ), + ( + "can_view_rcon_server_settings_config", + "Can view general CRCON server settings", + ), + ( + "can_change_rcon_server_settings_config", + "Can change general CRCON server settings", + ), + ("can_view_scorebot_config", "Can view scorebot config"), + ("can_change_scorebot_config", "Can change scorebot config"), + ( + "can_view_standard_broadcast_messages", + "Can view shared broadcast messages", + ), + ( + "can_change_standard_broadcast_messages", + "Can change shared broadcast messages", + ), + ( + "can_view_standard_punishment_messages", + "Can view shared punishment messages", + ), + ( + "can_change_standard_punishment_messages", + "Can change shared punishment messages", + ), + ( + "can_view_standard_welcome_messages", + "Can view shared welcome messages", + ), + ( + "can_change_standard_welcome_messages", + "Can change shared welcome messages", + ), + ("can_view_steam_config", "Can view steam API config"), + ("can_change_steam_config", "Can change steam API config"), + ( + "can_view_vac_game_bans_config", + "Can view VAC/Gameban ban on connect config", + ), + ( + "can_change_vac_game_bans_config", + "Can change VAC/Gameban ban on connect config", + ), + ( + "can_view_admin_pings_discord_webhooks_config", + "Can view Discord admin ping config", + ), + ( + "can_change_admin_pings_discord_webhooks_config", + "Can change Discord admin ping config", + ), + ( + "can_view_audit_discord_webhooks_config", + "Can view Discord audit config", + ), + ( + "can_change_audit_discord_webhooks_config", + "Can change Discord audit config", + ), + ( + "can_view_camera_discord_webhooks_config", + "Can view Discord admin cam notification config", + ), + ( + "can_change_camera_discord_webhooks_config", + "Can change Discord admin cam notification config", + ), + ( + "can_view_chat_discord_webhooks_config", + "Can view Discord chat notification config", + ), + ( + "can_change_chat_discord_webhooks_config", + "Can change Discord chat notification config", + ), + ( + "can_view_kills_discord_webhooks_config", + "Can view Discord team/teamkill notification config", + ), + ( + "can_change_kills_discord_webhooks_config", + "Can change Discord team/teamkill notification config", + ), + ( + "can_view_watchlist_discord_webhooks_config", + "Can view Discord player watchlist notification config", + ), + ( + "can_change_watchlist_discord_webhooks_config", + "Can change Discord player watchlist notification config", + ), + ( + "can_restart_webserver", + "Can restart the webserver (Not a complete Docker restart)", + ), + ( + "can_view_chat_commands_config", + "Can view the chat commands config", + ), + ( + "can_change_chat_commands_config", + "Can change the chat commands config", + ), + ("can_view_log_stream_config", "Can view the Log Stream config"), + ( + "can_change_log_stream_config", + "Can change the Log Stream config", + ), + ("can_view_blacklists", "Can view available blacklists"), + ("can_add_blacklist_records", "Can add players to blacklists"), + ( + "can_change_blacklist_records", + "Can unblacklist players and edit blacklist records", + ), + ("can_delete_blacklist_records", "Can delete blacklist records"), + ("can_create_blacklists", "Can create blacklists"), + ("can_change_blacklists", "Can change blacklists"), + ("can_delete_blacklists", "Can delete blacklists"), + ("can_change_game_layout", "Can change game layout"), + ), + }, ), ] diff --git a/rconweb/api/models.py b/rconweb/api/models.py index 70ca5cb3c..78ea55014 100644 --- a/rconweb/api/models.py +++ b/rconweb/api/models.py @@ -438,4 +438,6 @@ class Meta: ("can_change_blacklists", "Can change blacklists"), ("can_delete_blacklists", "Can delete blacklists"), ("can_change_game_layout", "Can change game layout"), + ("can_view_seed_vip_config", "Can view the Seed VIP config"), + ("can_change_seed_vip_config", "Can change the Seed VIP config"), ) diff --git a/rconweb/api/urls.py b/rconweb/api/urls.py index fabe0a894..d6eb4269c 100644 --- a/rconweb/api/urls.py +++ b/rconweb/api/urls.py @@ -9,13 +9,13 @@ audit_log, auth, auto_settings, + history, multi_servers, scoreboards, services, user_settings, views, vips, - history, ) from .auth import api_response from .decorators import ENDPOINT_HTTP_METHODS @@ -191,6 +191,7 @@ def get_api_documentation(request): ), ("describe_real_vip_config", user_settings.describe_real_vip_config), ("describe_log_stream_config", user_settings.describe_log_stream_config), + ("describe_seed_vip_config", user_settings.describe_seed_vip_config), ("get_all_discord_webhooks_config", user_settings.get_all_discord_webhooks_config), ("get_all_standard_message_config", user_settings.get_all_standard_message_config), ("reconnect_gameserver", views.restart_gunicorn), diff --git a/rconweb/api/user_settings.py b/rconweb/api/user_settings.py index 224ea1ad7..a032f6dce 100644 --- a/rconweb/api/user_settings.py +++ b/rconweb/api/user_settings.py @@ -21,6 +21,7 @@ from rcon.user_config.rcon_server_settings import RconServerSettingsUserConfig from rcon.user_config.real_vip import RealVipUserConfig from rcon.user_config.scorebot import ScorebotUserConfig +from rcon.user_config.seed_vip import SeedVIPUserConfig from rcon.user_config.standard_messages import ( StandardBroadcastMessagesUserConfig, StandardPunishmentMessagesUserConfig, @@ -491,3 +492,16 @@ def describe_real_vip_config(request): command=command_name, failed=False, ) + + +@csrf_exempt +@login_required() +@require_http_methods(["GET"]) +def describe_seed_vip_config(request): + command_name = "describe_seed_vip_config" + + return api_response( + result=SeedVIPUserConfig.model_json_schema(), + command=command_name, + failed=False, + ) diff --git a/rconweb/api/views.py b/rconweb/api/views.py index 835485880..4eac3a056 100644 --- a/rconweb/api/views.py +++ b/rconweb/api/views.py @@ -192,7 +192,7 @@ def expose_api_endpoint( @wraps(func) def wrapper(request: HttpRequest): parameters = inspect.signature(func).parameters - aliases = getattr(func, '_parameter_aliases', {}) + aliases = getattr(func, "_parameter_aliases", {}) arguments = {} failure = False others = None @@ -589,7 +589,10 @@ def run_raw_command(request): }, rcon_api.get_objective_row: "api.can_view_current_map", rcon_api.get_objective_rows: "api.can_view_current_map", - rcon_api.set_game_layout: "api.can_change_game_layout" + rcon_api.set_game_layout: "api.can_change_game_layout", + rcon_api.get_seed_vip_config: "api.can_view_seed_vip_config", + rcon_api.set_seed_vip_config: "api.can_change_seed_vip_config", + rcon_api.validate_seed_vip_config: "api.can_change_seed_vip_config", } PREFIXES_TO_EXPOSE = [ @@ -678,6 +681,7 @@ def run_raw_command(request): rcon_api.get_recent_logs: ["GET", "POST"], rcon_api.get_round_time_remaining: ["GET"], rcon_api.get_scorebot_config: ["GET"], + rcon_api.get_seed_vip_config: ["GET"], rcon_api.get_server_name_change_config: ["GET"], rcon_api.get_server_settings: ["GET"], rcon_api.get_slots: ["GET"], @@ -751,6 +755,7 @@ def run_raw_command(request): rcon_api.set_rcon_server_settings_config: ["POST"], rcon_api.set_real_vip_config: ["POST"], rcon_api.set_scorebot_config: ["POST"], + rcon_api.set_seed_vip_config: ["POST"], rcon_api.set_server_name_change_config: ["POST"], rcon_api.set_server_name: ["POST"], rcon_api.set_standard_broadcast_messages: ["POST"], @@ -794,6 +799,7 @@ def run_raw_command(request): rcon_api.validate_rcon_server_settings_config: ["POST"], rcon_api.validate_real_vip_config: ["POST"], rcon_api.validate_scorebot_config: ["POST"], + rcon_api.validate_seed_vip_config: ["POST"], rcon_api.validate_server_name_change_config: ["POST"], rcon_api.validate_standard_broadcast_messages: ["POST"], rcon_api.validate_standard_punishments_messages: ["POST"], @@ -866,7 +872,8 @@ def run_raw_command(request): ) except: logger.exception( - "Failed to initialized endpoint for %r - Most likely bad configuration", func + "Failed to initialized endpoint for %r - Most likely bad configuration", + func, ) raise logger.info("Done Initializing endpoints") diff --git a/requirements.txt b/requirements.txt index 75b515913..db1164123 100644 --- a/requirements.txt +++ b/requirements.txt @@ -28,3 +28,4 @@ discord.py==2.4.0 # django-dirctory is currently incompatible with Django 4.0+, PR is pending to fix django-directory upstream git+https://github.com/cemathey/django-directory@f51ee1e8dc50edf453fee4a0d9631c0e46fe9433 pre-commit +humanize==4.10.0 \ No newline at end of file diff --git a/tests/test_no_leader_watch.py b/tests/test_no_leader_watch.py index c40319cf2..3cf098f98 100644 --- a/tests/test_no_leader_watch.py +++ b/tests/test_no_leader_watch.py @@ -17,7 +17,7 @@ WatchStatus, ) from rcon.automods.no_leader import NoLeaderAutomod -from rcon.types import GameState, Roles +from rcon.types import GameStateType, Roles from rcon.user_config.auto_mod_no_leader import AutoModNoLeaderUserConfig @@ -557,7 +557,7 @@ def team_view(): } -game_state: GameState = { +game_state: GameStateType = { "allied_score": 3, "axis_score": 2, "current_map": "", diff --git a/tests/test_no_solo_tank_automod.py b/tests/test_no_solo_tank_automod.py index a76d51f58..5ef51c700 100644 --- a/tests/test_no_solo_tank_automod.py +++ b/tests/test_no_solo_tank_automod.py @@ -5,7 +5,7 @@ from rcon.automods.models import PunishPlayer, PunishStepState, WatchStatus from rcon.automods.no_solotank import NoSoloTankAutomod -from rcon.types import GameState +from rcon.types import GameStateType from rcon.user_config.auto_mod_solo_tank import AutoModNoSoloTankUserConfig @@ -377,7 +377,7 @@ def team_view(): } -game_state: GameState = { +game_state: GameStateType = { "allied_score": 3, "axis_score": 2, "current_map": { diff --git a/tests/test_seeding_rules_automod.py b/tests/test_seeding_rules_automod.py index fc4afb974..8a76350a0 100644 --- a/tests/test_seeding_rules_automod.py +++ b/tests/test_seeding_rules_automod.py @@ -15,7 +15,7 @@ WatchStatus, ) from rcon.automods.seeding_rules import SeedingRulesAutomod -from rcon.types import GameState, StructuredLogLineWithMetaData +from rcon.types import GameStateType, StructuredLogLineWithMetaData from rcon.user_config.auto_mod_seeding import ( AutoModSeedingUserConfig, DisallowedRoles, @@ -164,7 +164,7 @@ def team_view(): } -game_state: GameState = { +game_state: GameStateType = { "allied_score": 3, "axis_score": 2, "current_map": {