├── .dockerignore ├── .gitignore ├── Dockerfile ├── alembic.ini ├── alembic ├── env.py ├── script.py.mako └── versions │ └── ee68bbe1c956_create_tables.py ├── app.py ├── docker-compose.yml ├── pyproject.toml ├── src ├── __init__.py ├── api │ ├── __init__.py │ ├── decorators │ │ └── __init__.py │ └── routes │ │ ├── __init__.py │ │ ├── demo_manager.py │ │ ├── demo_upload.py │ │ └── grant_token.py ├── config.py ├── db │ ├── __init__.py │ └── models.py └── utils.py └── start_app.sh /.dockerignore: -------------------------------------------------------------------------------- 1 | venv 2 | .venv 3 | .DS_Store 4 | *.pyc 5 | .eggs 6 | *.egg-info 7 | data -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | venv 2 | .venv 3 | .DS_Store 4 | *.pyc 5 | .eggs 6 | *.egg-info 7 | data -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.10-slim-buster 2 | 3 | WORKDIR /app 4 | 5 | ADD . /app 6 | 7 | RUN pip install -e . 8 | 9 | RUN apt-get update && apt-get install -y --no-install-recommends \ 10 | apt-utils \ 11 | postgresql-client 12 | 13 | ENV DATABASE_URL=postgresql://postgres:@db:5432/demos 14 | EXPOSE 80 15 | -------------------------------------------------------------------------------- /alembic.ini: -------------------------------------------------------------------------------- 1 | [alembic] 2 | # path to migration scripts 3 | script_location = alembic 4 | 5 | [DEFAULT] 6 | prepend_sys_path = . 7 | version_path_separator = os 8 | 9 | [demos] 10 | script_location = alembic 11 | 12 | [loggers] 13 | keys = root,sqlalchemy,alembic 14 | 15 | [handlers] 16 | keys = console 17 | 18 | [formatters] 19 | keys = generic 20 | 21 | [logger_root] 22 | level = WARN 23 | handlers = console 24 | qualname = 25 | 26 | [logger_sqlalchemy] 27 | level = WARN 28 | handlers = 29 | qualname = sqlalchemy.engine 30 | 31 | [logger_alembic] 32 | level = INFO 33 | handlers = 34 | qualname = alembic 35 | 36 | [handler_console] 37 | class = StreamHandler 38 | args = (sys.stderr,) 39 | level = NOTSET 40 | formatter = generic 41 | 42 | [formatter_generic] 43 | format = %(levelname)-5.5s [%(name)s] %(message)s 44 | datefmt = %H:%M:%S 45 | -------------------------------------------------------------------------------- /alembic/env.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | from logging.config import fileConfig 4 | 5 | from sqlalchemy import engine_from_config, pool 6 | 7 | from alembic import context 8 | 9 | config = context.config 10 | 11 | fileConfig(config.config_file_name) 12 | 13 | project_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), "..")) 14 | sys.path.append(project_dir) 15 | 16 | from src.db.models import DemoMetadata, PollingSession, Report, Review 17 | 18 | target_metadata = context.config.get_main_option("sqlalchemy.url") 19 | 20 | 21 | def run_migrations_offline(): 22 | """Run migrations in 'offline' mode. 23 | 24 | This configures the context with just a URL 25 | and not an Engine, though an Engine is acceptable 26 | here as well. By skipping the Engine creation 27 | we don't even need a DBAPI to be available. 28 | 29 | Calls to context.execute() here emit the given string to the 30 | script output. 31 | """ 32 | url = config.get_main_option("sqlalchemy.url") 33 | context.configure( 34 | url=url, 35 | target_metadata=target_metadata, 36 | literal_binds=True, 37 | dialect_opts={"implicit_returning": False}, 38 | ) 39 | 40 | with context.begin_transaction(): 41 | context.run_migrations() 42 | 43 | 44 | def run_migrations_online(): 45 | """Run migrations in 'online' mode. 46 | 47 | In this scenario we need to create an Engine 48 | and associate a connection with the context. 49 | """ 50 | 51 | configuration = config.get_section(config.config_ini_section) 52 | configuration["sqlalchemy.url"] = os.getenv("DATABASE_URL") 53 | 54 | connectable = engine_from_config( 55 | configuration, 56 | prefix="sqlalchemy.", 57 | poolclass=pool.NullPool, 58 | ) 59 | 60 | with connectable.connect() as connection: 61 | context.configure(connection=connection, target_metadata=target_metadata) 62 | 63 | with context.begin_transaction(): 64 | context.run_migrations() 65 | 66 | 67 | if context.is_offline_mode(): 68 | run_migrations_offline() 69 | else: 70 | run_migrations_online() 71 | -------------------------------------------------------------------------------- /alembic/script.py.mako: -------------------------------------------------------------------------------- 1 | """${message} 2 | 3 | Revision ID: ${up_revision} 4 | Revises: ${down_revision | comma,n} 5 | Create Date: ${create_date} 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | ${imports if imports else ""} 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = ${repr(up_revision)} 14 | down_revision = ${repr(down_revision)} 15 | branch_labels = ${repr(branch_labels)} 16 | depends_on = ${repr(depends_on)} 17 | 18 | 19 | def upgrade() -> None: 20 | ${upgrades if upgrades else "pass"} 21 | 22 | 23 | def downgrade() -> None: 24 | ${downgrades if downgrades else "pass"} 25 | -------------------------------------------------------------------------------- /alembic/versions/ee68bbe1c956_create_tables.py: -------------------------------------------------------------------------------- 1 | """create tables~ 2 | 3 | Revision ID: ee68bbe1c956 4 | Revises: 5 | Create Date: 2023-06-19 19:41:05.442416 6 | 7 | """ 8 | import sqlalchemy as sa 9 | from sqlalchemy.dialects.postgresql import JSON 10 | 11 | from alembic import op 12 | 13 | # revision identifiers, used by Alembic. 14 | revision = "ee68bbe1c956" 15 | down_revision = None 16 | branch_labels = None 17 | depends_on = None 18 | 19 | 20 | def upgrade() -> None: 21 | op.create_table( 22 | "demo_metadata", 23 | sa.Column("demo_id", sa.String(), primary_key=True), 24 | sa.Column("file_path", sa.Text(), nullable=False), 25 | sa.Column("timeseries_data", JSON), 26 | ) 27 | 28 | op.create_table( 29 | "reports", 30 | sa.Column("upload_time", sa.DateTime(timezone=True), primary_key=True), 31 | sa.Column("reporter_steam_id", sa.String(), primary_key=True), 32 | sa.Column("suspected_cheater_steam_id", sa.String(), nullable=False), 33 | sa.Column("demo_id", sa.String(), sa.ForeignKey("demo_metadata.demo_id"), nullable=False), 34 | ) 35 | 36 | op.create_table( 37 | "reviews", 38 | sa.Column("reviewer_steam_id", sa.String(), primary_key=True), 39 | sa.Column("demo_id", sa.String(), sa.ForeignKey("demo_metadata.demo_id"), primary_key=True), 40 | sa.Column("cheat_types", sa.Text(), nullable=False), 41 | ) 42 | 43 | op.create_table( 44 | "polling_sessions", 45 | sa.Column("polling_session_id", sa.String(), nullable=False, primary_key=True), 46 | sa.Column("megabase_user_key", sa.String(), nullable=False), 47 | sa.Column("server_steamid64", sa.String(), nullable=False), 48 | sa.Column("is_active", sa.Boolean(), nullable=False), 49 | sa.Column("start_time", sa.DateTime(timezone=True), nullable=False), 50 | sa.Column("end_time", sa.DateTime(timezone=True)), 51 | ) 52 | 53 | 54 | def downgrade() -> None: 55 | op.drop_table("reports") 56 | op.drop_table("reviews") 57 | op.drop_table("demo_metadata") 58 | op.drop_table("polling_sessions") 59 | -------------------------------------------------------------------------------- /app.py: -------------------------------------------------------------------------------- 1 | from src import create_app 2 | 3 | app = create_app() 4 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.8' 2 | 3 | services: 4 | db: 5 | image: postgres:15.2 6 | restart: always 7 | environment: 8 | POSTGRES_USER: postgres 9 | POSTGRES_DB: demos 10 | POSTGRES_HOST_AUTH_METHOD: trust 11 | volumes: 12 | - ./data/db:/var/lib/postgresql/data 13 | networks: 14 | - backend 15 | healthcheck: 16 | test: [ "CMD-SHELL", "pg_isready -U postgres" ] 17 | interval: 5s 18 | timeout: 5s 19 | retries: 5 20 | 21 | app: 22 | build: 23 | context: . 24 | dockerfile: Dockerfile 25 | volumes: 26 | - .:/code 27 | depends_on: 28 | - db 29 | networks: 30 | - backend 31 | 32 | networks: 33 | backend: 34 | driver: bridge 35 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["setuptools>=63.0.0", "wheel"] 3 | build-backend = "setuptools.build_meta" 4 | 5 | [tool.setuptools] 6 | packages = ["src"] 7 | 8 | [project] 9 | name = "mock-backend" 10 | 11 | version = "0.0.0" 12 | 13 | description = "Repo For Prototyping the Backend" 14 | 15 | requires-python = ">=3.10" 16 | 17 | dependencies = [ 18 | "flask", 19 | "flask-cors", 20 | "gunicorn", 21 | "click", 22 | "sqlalchemy", 23 | "alembic", 24 | "psycopg2-binary", 25 | "requests", 26 | ] 27 | 28 | [project.scripts] 29 | workers = "src.cli:workers" 30 | 31 | [project.optional-dependencies] 32 | dev = [ 33 | "flake8>=4.0.1", 34 | "pep8-naming>=0.13.0", 35 | "flake8-docstrings>=1.6.0", 36 | "pytest>=7.1.2", 37 | "pytest-cov>=3.0.0", 38 | "isort>=5.10.1", 39 | "black>=22.8.0", 40 | ] 41 | 42 | [tool.isort] 43 | profile = "black" 44 | 45 | [tool.black] 46 | line-length = 120 -------------------------------------------------------------------------------- /src/__init__.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import os 4 | 5 | from flask import Flask 6 | from flask_cors import CORS 7 | 8 | from src.api import api 9 | from src.config import Config, DevelopmentConfig 10 | 11 | 12 | def create_app(config: Config | None = None): 13 | app = Flask(__name__) 14 | CORS(app) 15 | 16 | stage = os.environ["stage"] 17 | 18 | if config is None: 19 | config = DevelopmentConfig if stage == "dev" else Config 20 | 21 | app.config.from_object(config) 22 | 23 | app.logger.info(f"{stage=}") 24 | 25 | app.register_blueprint(api) 26 | 27 | def health_view(): 28 | """Health check endpoint.""" 29 | return "OK", 200 30 | 31 | app.add_url_rule("/health", endpoint="health", view_func=health_view) 32 | 33 | return app 34 | -------------------------------------------------------------------------------- /src/api/__init__.py: -------------------------------------------------------------------------------- 1 | from flask import Blueprint 2 | 3 | from src.api.routes.demo_manager import DemoManagerResource 4 | 5 | api = Blueprint("api", __name__, url_prefix="/api") 6 | 7 | 8 | api.add_url_rule( 9 | "/demomanager", 10 | methods=["POST"], 11 | view_func=DemoManagerResource.as_view("demomanager"), 12 | ) 13 | -------------------------------------------------------------------------------- /src/api/decorators/__init__.py: -------------------------------------------------------------------------------- 1 | from functools import wraps 2 | 3 | from flask import jsonify, request 4 | 5 | 6 | def is_valid_megabase_key(key: str) -> bool: 7 | """Perform some bs lookup eventually to validate this.""" 8 | return True 9 | 10 | 11 | def validate_token(f): 12 | @wraps(f) 13 | def wrapper(*args, **kwargs): 14 | """Verifies megabase api key header.""" 15 | for k in request.args.keys(): 16 | if k.lower() == "megabase_user_key": 17 | key = request.args.get("megabase_user_key") 18 | break 19 | else: 20 | return jsonify("Unauthorized"), 401 21 | 22 | if is_valid_megabase_key(key): # TODO implement 23 | return f(*args, **kwargs) 24 | 25 | else: 26 | return jsonify("Invalid megabase key"), 400 27 | 28 | return wrapper 29 | -------------------------------------------------------------------------------- /src/api/routes/__init__.py: -------------------------------------------------------------------------------- 1 | from flask import jsonify 2 | 3 | 4 | def _validate_args(request: dict[str, str], required_args: list[str], valid_args: list[str]) -> None: 5 | if missing_fields := set(required_args).difference(set(request.keys())): 6 | return jsonify(f"Bad request: missing required fields: {missing_fields}"), 400 7 | for k in request.keys(): 8 | if k not in valid_args: 9 | return jsonify(f"Bad request: '{k}' is an invalid field"), 400 10 | -------------------------------------------------------------------------------- /src/api/routes/demo_manager.py: -------------------------------------------------------------------------------- 1 | """Endpoint to manage start and stopping of demo requests.""" 2 | 3 | from enum import Enum 4 | 5 | from flask import jsonify, request 6 | from flask.views import MethodView 7 | 8 | from src import utils 9 | from src.api.decorators import validate_token 10 | from src.api.routes import _validate_args 11 | 12 | 13 | class RequestType(Enum): 14 | START = 1 15 | END = 2 16 | 17 | @classmethod 18 | def valid_request_types(cls) -> set: 19 | return (cls.START.value, cls.END.value) 20 | 21 | 22 | REQUIRED_GET_ARGS = ["megabase_user_key", "server_steam_id", "request_type"] 23 | VALID_GET_ARGS = REQUIRED_GET_ARGS 24 | 25 | 26 | class DemoManagerResource(MethodView): 27 | """API resource for managing demo start and end times from the client.""" 28 | 29 | @validate_token 30 | def post(self): 31 | """Get channels for a given system and dataset.""" 32 | kwargs = request.args.to_dict() 33 | validated = _validate_args(kwargs, REQUIRED_GET_ARGS, VALID_GET_ARGS) 34 | if validated is not None: 35 | return validated 36 | 37 | try: 38 | request_type = int(kwargs["request_type"]) 39 | except ValueError: 40 | return jsonify("Expected an integer for request_type!"), 400 41 | if request_type not in RequestType.valid_request_types(): 42 | return jsonify(f"Expected request type to be one of {RequestType.valid_request_types()}"), 400 43 | 44 | # check if there is already an instance from this user (use key as this is 1-1 with steam id) 45 | megabase_user_key = kwargs["megabase_user_key"] 46 | server_steam_id = kwargs["server_steam_id"] 47 | is_active_session = utils.is_active_session(megabase_user_key) 48 | 49 | if request_type == RequestType.START.value: 50 | if is_active_session: 51 | # fmt: off 52 | return jsonify(f"User is already actively in a session, please close it out before requesting a new session!"), 200 53 | # fmt: on 54 | polling_session_id = utils.start_demo_session(megabase_user_key, server_steam_id) 55 | return jsonify(f"User started a session with {polling_session_id}!"), 200 56 | elif request_type == RequestType.END.value: 57 | if not is_active_session: 58 | return jsonify(f"User is not in a session, cannot close session out!"), 400 59 | polling_session_id = utils.stop_demo_session(megabase_user_key, server_steam_id) 60 | return jsonify(f"User ended a session with {polling_session_id}!"), 200 61 | -------------------------------------------------------------------------------- /src/api/routes/demo_upload.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MegaAntiCheat/mock-backend/8c30dd3413ef5bb0e4fd6d14daa5188c0ff5db1f/src/api/routes/demo_upload.py -------------------------------------------------------------------------------- /src/api/routes/grant_token.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MegaAntiCheat/mock-backend/8c30dd3413ef5bb0e4fd6d14daa5188c0ff5db1f/src/api/routes/grant_token.py -------------------------------------------------------------------------------- /src/config.py: -------------------------------------------------------------------------------- 1 | class Config: 2 | ENVIRONMENT = "production" 3 | DEBUG = False 4 | TESTING = False 5 | 6 | # File uploads 7 | # MAX_CONTENT_LENGTH sets maximum file size for non-multipart uploads. Flask will raise a RequestEntityTooLarge 8 | # exception if uploaded file exceeds this limit. 9 | MAX_CONTENT_LENGTH = 2 * 1000 * 1000 * 1000 # 2GB 10 | MULTIPART_UPLOAD_CHUNK_SIZE_MB = 1_000 # 1GB 11 | 12 | 13 | class DevelopmentConfig(Config): 14 | ENVIRONMENT = "development" 15 | DEBUG = True 16 | -------------------------------------------------------------------------------- /src/db/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MegaAntiCheat/mock-backend/8c30dd3413ef5bb0e4fd6d14daa5188c0ff5db1f/src/db/__init__.py -------------------------------------------------------------------------------- /src/db/models.py: -------------------------------------------------------------------------------- 1 | from uuid import uuid4 2 | 3 | import sqlalchemy as sa 4 | from sqlalchemy.dialects.postgresql import JSON 5 | from sqlalchemy.orm import DeclarativeBase 6 | 7 | 8 | class Base(DeclarativeBase): 9 | pass 10 | 11 | 12 | class Report(Base): 13 | __tablename__ = "reports" 14 | 15 | upload_time = sa.Column("upload_time", sa.types.TIMESTAMP(timezone=True), primary_key=True) 16 | reporter_steam_id = sa.Column("reporter_steam_id", sa.String, primary_key=True) 17 | suspected_cheater_steam_id = sa.Column("suspected_cheater_steam_id", sa.String, nullable=False) 18 | demo_id = sa.Column("demo_id", sa.String, sa.ForeignKey("demo_metadata.demo_id"), nullable=False) 19 | 20 | 21 | class Review(Base): 22 | __tablename__ = "reviews" 23 | 24 | reviewer_steam_id = sa.Column("reviewer_steam_id", sa.String, primary_key=True) 25 | demo_id = sa.Column("demo_id", sa.String, sa.ForeignKey("demo_metadata.demo_id"), primary_key=True) 26 | cheat_types = sa.Column("cheat_types", sa.Text, nullable=False) 27 | 28 | 29 | class DemoMetadata(Base): 30 | __tablename__ = "demo_metadata" 31 | 32 | demo_id = sa.Column("demo_id", sa.String, primary_key=True) 33 | file_path = sa.Column("file_path", sa.Text, nullable=False) 34 | timeseries_data = sa.Column("timeseries_data", JSON) 35 | 36 | 37 | class PollingSession(Base): 38 | __tablename__ = "polling_sessions" 39 | 40 | polling_session_id = sa.Column("polling_session_id", sa.String, nullable=False, primary_key=True) 41 | megabase_user_key = sa.Column("megabase_user_key", sa.String, nullable=False) 42 | server_steamid64 = sa.Column("server_steamid64", sa.String, nullable=False) 43 | is_active = sa.Column("is_active", sa.Boolean, nullable=False) 44 | start_time = sa.Column("start_time", sa.types.TIMESTAMP(timezone=True), nullable=False) 45 | end_time = sa.Column("end_time", sa.types.TIMESTAMP(timezone=True)) 46 | -------------------------------------------------------------------------------- /src/utils.py: -------------------------------------------------------------------------------- 1 | """Utils for the API.""" 2 | 3 | import os 4 | from datetime import datetime, timezone 5 | from threading import Thread 6 | from uuid import uuid4 7 | 8 | import sqlalchemy as sa 9 | 10 | from src.db.models import PollingSession 11 | 12 | DATABASE_URL = os.getenv("DATABASE_URL", None) 13 | if DATABASE_URL is not None: 14 | engine = sa.create_engine(DATABASE_URL) 15 | Session = sa.orm.sessionmaker(engine) 16 | else: 17 | engine = None 18 | Session = None 19 | 20 | 21 | def is_active_session(megabase_user_key: str) -> bool: 22 | """Return if the current user is in a session or not. 23 | 24 | Args: 25 | megabase_user_key: API key associated with the user 26 | 27 | Returns: 28 | truth value of if the user is in a polling session 29 | """ 30 | with Session() as session: 31 | sql = sa.select(PollingSession).where( 32 | PollingSession.megabase_user_key == megabase_user_key, PollingSession.is_active == True 33 | ) 34 | _is_active_session = session.execute(sql).scalar_one_or_none() 35 | 36 | if _is_active_session is None: 37 | return False 38 | 39 | return True 40 | 41 | 42 | def start_demo_session(megabase_user_key: str, server_steam_id: str): 43 | """Start a demo session in the database from a request. 44 | 45 | Args: 46 | megabase_user_key: API key associated with the user 47 | server_steam_id: Steam ID of the server 48 | 49 | Returns: 50 | the ID of the polling session started. 51 | """ 52 | try: 53 | server_steamid64 = int(server_steam_id) 54 | except ValueError: 55 | server_steamid64 = convert_server_id3_id_to_64_id(server_steam_id) 56 | 57 | polling_session_id = uuid4().hex 58 | start_time = datetime.now().astimezone(timezone.utc) 59 | with Session() as session: 60 | polling_session = PollingSession( 61 | polling_session_id=polling_session_id, 62 | megabase_user_key=megabase_user_key, 63 | server_steamid64=server_steamid64, 64 | is_active=True, 65 | start_time=start_time, 66 | ) 67 | session.add(polling_session) 68 | session.commit() 69 | 70 | return polling_session_id 71 | 72 | 73 | def stop_demo_session(megabase_user_key: str, server_steam_id: str) -> str: 74 | """Stop a demo session in the database from a request. 75 | 76 | Args: 77 | megabase_user_key: API key associated with the user 78 | server_steam_id: Steam ID of the server 79 | 80 | Returns: 81 | the ID of the polling session stopped. 82 | """ 83 | try: 84 | server_steamid64 = int(server_steam_id) 85 | except ValueError: 86 | server_steamid64 = convert_server_id3_id_to_64_id(server_steam_id) 87 | 88 | with Session() as session: 89 | sql = ( 90 | sa.select(PollingSession) 91 | .where( 92 | PollingSession.megabase_user_key == megabase_user_key, 93 | PollingSession.server_steamid64 == server_steamid64, 94 | PollingSession.is_active == True, 95 | ) 96 | .order_by(PollingSession.start_time.desc()).limit(1) 97 | ) 98 | polling_session = session.execute(sql).scalar_one() 99 | polling_session.end_time = datetime.now().astimezone(timezone.utc) 100 | session.commit() 101 | 102 | stopped_session_id = polling_session.polling_session_id 103 | return stopped_session_id 104 | 105 | 106 | def poll_server(steam_server_id: str) -> None: 107 | """Poll a server and send telemetry to the DB. 108 | 109 | Args: 110 | steam_server_id: ID of the server. 111 | """ 112 | pass 113 | 114 | 115 | class ServerPollingManager: 116 | """Class that handles polling servers with demo requests for server telemetry.""" 117 | 118 | def __init__(self): 119 | self.active_sessions = {} 120 | self.id = uuid4().hex # keep track in DB 121 | 122 | def start_session(self, steam_server_id: str) -> None: 123 | pass 124 | 125 | def close_session(self, steam_server_id: str) -> None: 126 | pass 127 | 128 | def poll_connections(self): 129 | """Poll the active connections table in the DB to make sure this instance is polling the correct servers.""" 130 | pass 131 | 132 | 133 | SERVER_STEAM_ID_MAPPING = { 134 | "I": 0, # Invalid 135 | "i": 0, # Invalid 136 | "U": 1, # Individual 137 | "M": 2, # Multiseat 138 | "G": 3, # GameServer 139 | "A": 4, # AnonGameServer 140 | "P": 5, # Pending 141 | "C": 6, # ContentServer 142 | "g": 7, # Clan 143 | "T": 8, # Chat 144 | "L": 8, # Chat 145 | "c": 8, # Chat 146 | "a": 10, # AnonUser 147 | } 148 | 149 | 150 | def convert_server_id3_id_to_64_id(id3: str) -> str: 151 | """Convert a server id3 id to 64 id. 152 | 153 | Args: 154 | id3: server id3 str like [A:1:658635804:23591] 155 | 156 | Returns: 157 | converted id as a string 158 | """ 159 | 160 | def _num_bin(num, size): 161 | if num >= pow(2, size): 162 | raise ValueError("'num' is out of bounds!") 163 | return bin(num)[2:].zfill(size) 164 | 165 | if id3[0].startswith("["): 166 | id3 = id3[1:] 167 | 168 | if id3[-1].endswith("]"): 169 | id3 = id3[:-1] 170 | 171 | parts = id3.split(":") 172 | 173 | type_id = SERVER_STEAM_ID_MAPPING[parts[0]] 174 | universe = int(parts[1]) 175 | account_id = int(parts[2]) 176 | if len(parts) >= 4: 177 | instance_id = int(parts[3]) 178 | else: 179 | raise ValueError("'convert_server_id3_id_to_64_id' can only convert server ids!") 180 | 181 | id_bin = f"{_num_bin(universe, 8)}{_num_bin(type_id, 4)}{_num_bin(instance_id, 20)}{_num_bin(account_id, 32)}" 182 | 183 | steam64_id = int(id_bin, 2) 184 | 185 | return str(steam64_id) 186 | -------------------------------------------------------------------------------- /start_app.sh: -------------------------------------------------------------------------------- 1 | exec gunicorn -b 0.0.0.0:80 --workers 8 --access-logfile - --error-logfile - app:app --------------------------------------------------------------------------------