diff --git a/.env.example b/.env.example index 276d126..e4bd12b 100644 --- a/.env.example +++ b/.env.example @@ -1,3 +1,4 @@ CLIENT_NAME="YourAPINme (Dev)/1.0" VRCHAT_API_BASE="https://api.vrchat.cloud/api/1" -IS_RENDER=False \ No newline at end of file +IS_DISTANT=False +DISTANT_URL_CONTEXT="https://distant.vrchat.cloud/data/account.json" \ No newline at end of file diff --git a/.gitignore b/.gitignore index 739c436..d398c7f 100644 --- a/.gitignore +++ b/.gitignore @@ -5,4 +5,5 @@ venv/ *.log "@ | Out-File -Encoding utf8 .gitignore .env -data/* \ No newline at end of file +data/ +account.json.php \ No newline at end of file diff --git a/account.json.php b/account.json.php deleted file mode 100644 index c414ae1..0000000 --- a/account.json.php +++ /dev/null @@ -1,24 +0,0 @@ - 'Unauthorized']); - exit; -} - -$jsonContent = <<<'JSON' -{ - "manual_username": "user123", - "displayName": "User Display Name", - "user_id": "abcdef123456", - "auth": "xxxxxxxxxxxxxxx==", - "auth_cookie": "yyyyyyyyyyyyyy", - "created_at": "2025-06-22T14:00:00+00:00" -} -JSON; - -header('Content-Type: application/json'); -echo $jsonContent; - -?> \ No newline at end of file diff --git a/app/api/system.py b/app/api/system.py index eb46a86..7b9fe2d 100644 --- a/app/api/system.py +++ b/app/api/system.py @@ -1,9 +1,16 @@ -from fastapi import APIRouter, HTTPException -import httpx -import json +from fastapi import APIRouter +from app.vrchat_context import VRChatContext + +def load_context(): + VRChatContext.load() +vrchat = VRChatContext.get() router = APIRouter() -@router.get("/health") -def health_check(): - return {"status": "ok"} +@router.get("/ping") +def ping(): + return {"message": "pong"} + +@router.get("/status") +def status_check(): + return {"status": "ok" if vrchat.auth_cookie else "not authenticated"} diff --git a/app/api/vrchat_groups.py b/app/api/vrchat_groups.py index 94f20ba..58924d4 100644 --- a/app/api/vrchat_groups.py +++ b/app/api/vrchat_groups.py @@ -1,23 +1,22 @@ from fastapi import APIRouter, HTTPException import httpx import json -from app.env import CLIENT_NAME, API_BASE, TOKEN_FILE +from app.env import CLIENT_NAME, API_BASE +from app.vrchat_context import VRChatContext router = APIRouter() -def load_token(): - if not TOKEN_FILE.exists(): - return None - with open(TOKEN_FILE, "r") as f: - return json.load(f) +def load_context(): + VRChatContext.load() @router.get("/groups/{group_id}") async def get_groups(group_id: str): - token = load_token() - if not token: + load_context() + vrchat = VRChatContext.get() + if not vrchat: raise HTTPException(status_code=401, detail="Token not found, please authenticate first") - auth_cookie = token.get("auth_cookie") + auth_cookie = vrchat.auth_cookie if not auth_cookie: raise HTTPException(status_code=401, detail="Auth cookie missing in token") @@ -39,11 +38,12 @@ async def get_groups(group_id: str): @router.get("/groups/{group_id}/instances") async def get_groups_instances(group_id: str, user_id: str): - token = load_token() - if not token: + load_context() + vrchat = VRChatContext.get() + if not vrchat: raise HTTPException(status_code=401, detail="Token not found, please authenticate first") - auth_cookie = token.get("auth_cookie") + auth_cookie = vrchat.auth_cookie if not auth_cookie: raise HTTPException(status_code=401, detail="Auth cookie missing in token") @@ -61,11 +61,12 @@ async def get_groups_instances(group_id: str, user_id: str): @router.get("/groups/{group_id}/posts") async def get_groups_posts(group_id: str): - token = load_token() - if not token: + load_context() + vrchat = VRChatContext.get() + if not vrchat: raise HTTPException(status_code=401, detail="Token not found, please authenticate first") - auth_cookie = token.get("auth_cookie") + auth_cookie = vrchat.auth_cookie if not auth_cookie: raise HTTPException(status_code=401, detail="Auth cookie missing in token") @@ -89,11 +90,12 @@ async def get_groups_posts(group_id: str): @router.get("/groups/{group_id}/bans") async def get_groups_bans(group_id: str): - token = load_token() - if not token: + load_context() + vrchat = VRChatContext.get() + if not vrchat: raise HTTPException(status_code=401, detail="Token not found, please authenticate first") - auth_cookie = token.get("auth_cookie") + auth_cookie = vrchat.auth_cookie if not auth_cookie: raise HTTPException(status_code=401, detail="Auth cookie missing in token") @@ -115,11 +117,12 @@ async def get_groups_bans(group_id: str): @router.get("/groups/{group_id}/roles") async def get_groups_roles(group_id: str): - token = load_token() - if not token: + load_context() + vrchat = VRChatContext.get() + if not vrchat: raise HTTPException(status_code=401, detail="Token not found, please authenticate first") - auth_cookie = token.get("auth_cookie") + auth_cookie = vrchat.auth_cookie if not auth_cookie: raise HTTPException(status_code=401, detail="Auth cookie missing in token") @@ -137,11 +140,12 @@ async def get_groups_roles(group_id: str): @router.get("/groups/{group_id}/members") async def get_groups_members(group_id: str): - token = load_token() - if not token: + load_context() + vrchat = VRChatContext.get() + if not vrchat: raise HTTPException(status_code=401, detail="Token not found, please authenticate first") - auth_cookie = token.get("auth_cookie") + auth_cookie = vrchat.auth_cookie if not auth_cookie: raise HTTPException(status_code=401, detail="Auth cookie missing in token") diff --git a/app/api/vrchat_search.py b/app/api/vrchat_search.py new file mode 100644 index 0000000..802c351 --- /dev/null +++ b/app/api/vrchat_search.py @@ -0,0 +1,49 @@ +from fastapi import APIRouter, HTTPException +import httpx +import json +from app.env import CLIENT_NAME, API_BASE, TOKEN_FILE +from app.vrchat_context import VRChatContext + +router = APIRouter() + +def load_context(): + VRChatContext.load() + +def load_token(): + if not TOKEN_FILE.exists(): + return None + with open(TOKEN_FILE, "r") as f: + return json.load(f) + +@router.get("/auth/exists/{type}/{text}") +async def get_if_exists_per_type(type: str, text: str): + load_context() + vrchat = VRChatContext.get() + + if type not in ["username", "email"]: + raise HTTPException(status_code=400, detail="Invalid type, must be 'username' or 'email'") + + if not text: + raise HTTPException(status_code=400, detail="Text cannot be empty") + + if not text.startswith("usr_") or text.startswith("group_"): + raise HTTPException(status_code=400, detail="Invalid text format, must start with 'usr_' or 'group_'") + + if not vrchat: + raise HTTPException(status_code=401, detail="Token not found, please authenticate first") + + auth_cookie = vrchat.auth_cookie + if not auth_cookie: + raise HTTPException(status_code=401, detail="Auth cookie missing in token") + + headers = {"User-Agent": CLIENT_NAME} + cookies = {"auth": auth_cookie} + url = f"{API_BASE}/auth/exists?{type}={text}{'&displayName=' + text if type == 'username' else ''}" + + async with httpx.AsyncClient() as client: + r = await client.get(url, headers=headers, cookies=cookies) + + if r.status_code != 200: + raise HTTPException(status_code=r.status_code, detail=f"Failed to fetch if {type} exists: {r.text}") + + return r.json() \ No newline at end of file diff --git a/app/api/vrchat_users.py b/app/api/vrchat_users.py index bf4f5af..e74d583 100644 --- a/app/api/vrchat_users.py +++ b/app/api/vrchat_users.py @@ -1,23 +1,45 @@ from fastapi import APIRouter, HTTPException import httpx import json -from app.env import CLIENT_NAME, API_BASE, TOKEN_FILE +from app.env import CLIENT_NAME, API_BASE +from app.vrchat_context import VRChatContext router = APIRouter() -def load_token(): - if not TOKEN_FILE.exists(): - return None - with open(TOKEN_FILE, "r") as f: - return json.load(f) +def load_context(): + VRChatContext.load() + +@router.get("/users/me") +async def get_bot_users_profile(): + load_context() + vrchat = VRChatContext.get() + if not vrchat: + raise HTTPException(status_code=401, detail="Token not found, please authenticate first") + + auth_cookie = vrchat.auth_cookie + if not auth_cookie: + raise HTTPException(status_code=401, detail="Auth cookie missing in token") + + headers = {"User-Agent": CLIENT_NAME} + cookies = {"auth": auth_cookie} + url = f"{API_BASE}/users/{vrchat.user_id}" + + async with httpx.AsyncClient() as client: + r = await client.get(url, headers=headers, cookies=cookies) + + if r.status_code != 200: + raise HTTPException(status_code=r.status_code, detail=f"Failed to fetch user (current bot) info: {r.text}") + + return r.json() @router.get("/users/{user_id}") async def get_user(user_id: str): - token = load_token() - if not token: + load_context() + vrchat = VRChatContext.get() + if not vrchat: raise HTTPException(status_code=401, detail="Token not found, please authenticate first") - auth_cookie = token.get("auth_cookie") + auth_cookie = vrchat.auth_cookie if not auth_cookie: raise HTTPException(status_code=401, detail="Auth cookie missing in token") @@ -35,11 +57,12 @@ async def get_user(user_id: str): @router.get("/users/{user_id}/friends/status") async def get_user_friend_status(user_id: str): - token = load_token() - if not token: + load_context() + vrchat = VRChatContext.get() + if not vrchat: raise HTTPException(status_code=401, detail="Token not found, please authenticate first") - auth_cookie = token.get("auth_cookie") + auth_cookie = vrchat.auth_cookie if not auth_cookie: raise HTTPException(status_code=401, detail="Auth cookie missing in token") @@ -57,11 +80,12 @@ async def get_user_friend_status(user_id: str): @router.get("/users/{user_id}/worlds") async def get_user_worlds(user_id: str): - token = load_token() - if not token: + load_context() + vrchat = VRChatContext.get() + if not vrchat: raise HTTPException(status_code=401, detail="Token not found, please authenticate first") - auth_cookie = token.get("auth_cookie") + auth_cookie = vrchat.auth_cookie if not auth_cookie: raise HTTPException(status_code=401, detail="Auth cookie missing in token") @@ -87,11 +111,12 @@ async def get_user_worlds(user_id: str): @router.get("/users/{user_id}/groups") async def get_user_groups(user_id: str): - token = load_token() - if not token: + load_context() + vrchat = VRChatContext.get() + if not vrchat: raise HTTPException(status_code=401, detail="Token not found, please authenticate first") - auth_cookie = token.get("auth_cookie") + auth_cookie = vrchat.auth_cookie if not auth_cookie: raise HTTPException(status_code=401, detail="Auth cookie missing in token") diff --git a/app/env.py b/app/env.py index 8afeac1..7b5ddbf 100644 --- a/app/env.py +++ b/app/env.py @@ -6,4 +6,4 @@ load_dotenv(dotenv_path=Path(__file__).parent.parent / ".env") CLIENT_NAME = os.getenv("CLIENT_NAME", "default-client-name") API_BASE = os.getenv("VRCHAT_API_BASE", "https://api.vrchat.cloud/api/1") TOKEN_FILE = Path(os.getenv("TOKEN_FILE", "data/auth/account.json")) -IS_RENDER = os.getenv("IS_RENDER", "false").lower() in ("true", "1", "t") \ No newline at end of file +IS_DISTANT = os.getenv("IS_DISTANT", "false").lower() in ("true", "1", "t") \ No newline at end of file diff --git a/app/main.py b/app/main.py index 929bd02..0ce2a2c 100644 --- a/app/main.py +++ b/app/main.py @@ -1,7 +1,13 @@ from fastapi import FastAPI +from app.api.vrchat_search import router as search from app.api.vrchat_users import router as users from app.api.vrchat_groups import router as groups from app.api.system import router as system +from app.vrchat_context import VRChatContext + +def load_context(): + VRChatContext.load() +vrchat = VRChatContext.get() app = FastAPI( title="K-API", @@ -24,11 +30,13 @@ app = FastAPI( swagger_ui_parameters={"defaultModelsExpandDepth": -1}, docs_url="/docs", redoc_url=None, - openapi_url="/openapi.json", - contact={"name": "Kryscau", "url": "https://vrchat.com/home/user/usr_323befe7-edbc-46fe-af9d-560f7e6b290c", "email": "kryscau@kvs.fyi" } + openapi_url="/api.json", + contact={"name": "Unstealable", "url": "https://vrchat.com/home/user/usr_3e354294-5925-42bb-a5e6-511c39a390eb"} ) prefix = "/api" -app.include_router(users, prefix=prefix, tags=["Users"]) -app.include_router(groups, prefix=prefix, tags=["Groups"]) +if vrchat.auth_cookie: + app.include_router(search, prefix=prefix, tags=["Search"]) + app.include_router(users, prefix=prefix, tags=["Users"]) + app.include_router(groups, prefix=prefix, tags=["Groups"]) app.include_router(system, prefix=prefix, tags=["System"]) \ No newline at end of file diff --git a/app/vrchat_context.py b/app/vrchat_context.py new file mode 100644 index 0000000..3ed7493 --- /dev/null +++ b/app/vrchat_context.py @@ -0,0 +1,68 @@ +import json +from dataclasses import dataclass +from pathlib import Path +from typing import Optional +import os +import httpx + +from app.env import IS_DISTANT + +@dataclass +class VRChatData: + display_name: str + user_id: str + auth_cookie: str + auth_header: str + manual_username: str + +class VRChatContext: + _instance: Optional["VRChatContext"] = None + + def __init__(self): + self._token: Optional[VRChatData] = None + + @classmethod + def load(cls): + if IS_DISTANT: + cls._load_from_remote() + else: + cls._load_from_local() + + @classmethod + def _load_from_local(cls, path: Path = Path("data/account.json")): + if not path.exists(): + raise FileNotFoundError(f"account.json file not found: {path}") + with open(path, "r", encoding="utf-8") as f: + data = json.load(f) + cls._set_instance(data) + + @classmethod + def _load_from_remote(cls): + remote_url = os.getenv("DISTANT_TOKEN_URL") + if not remote_url: + raise EnvironmentError("DISTANT_TOKEN_URL is not defined in environment") + + try: + response = httpx.get(remote_url, timeout=5.0) + response.raise_for_status() + data = response.json() + cls._set_instance(data) + except httpx.RequestError as e: + raise ConnectionError(f"Could not fetch remote VRChat Data: {e}") + + @classmethod + def _set_instance(cls, data: dict): + cls._instance = cls() + cls._instance._token = VRChatData( + display_name=data.get("displayName", ""), + user_id=data.get("user_id", ""), + auth_cookie=data.get("auth_cookie", ""), + auth_header=data.get("auth", ""), + manual_username=data.get("manual_username", "") + ) + + @classmethod + def get(cls) -> VRChatData: + if not cls._instance or not cls._instance._token: + raise RuntimeError("VRChatContext not initialized. Call VRChatContext.load() first.") + return cls._instance._token diff --git a/distant_accounts.json.php b/distant_accounts.json.php new file mode 100644 index 0000000..2e8cc87 --- /dev/null +++ b/distant_accounts.json.php @@ -0,0 +1,34 @@ + "TonUsername", + "displayName" => "TonDisplayName", + "user_id" => "usr_abcdef1234567890", + "auth" => "ZGF0YQ==", + "auth_cookie" => "auth_cookie_valeur" +]; + +header('Content-Type: application/json'); +echo json_encode($vrchat_token, JSON_PRETTY_PRINT); diff --git a/app/prelaunch/vrchat_auth.py b/python/vrchat_auth.py similarity index 76% rename from app/prelaunch/vrchat_auth.py rename to python/vrchat_auth.py index e20b0dc..911f302 100644 --- a/app/prelaunch/vrchat_auth.py +++ b/python/vrchat_auth.py @@ -6,28 +6,8 @@ import sys from pathlib import Path sys.path.append(str(Path(__file__).resolve().parent.parent)) -from env import CLIENT_NAME, API_BASE, TOKEN_FILE, IS_RENDER - -if IS_RENDER: - print("⚠️ Running in Render environment, skipping VRChat auth.") - sys.exit(0) - -def save_token(data): - data["created_at"] = datetime.now(timezone.utc).isoformat() - TOKEN_FILE.parent.mkdir(parents=True, exist_ok=True) - with open(TOKEN_FILE, "w") as f: - json.dump(data, f, indent=4) - -def load_token(): - if not TOKEN_FILE.exists(): - return None - with open(TOKEN_FILE, "r") as f: - data = json.load(f) - created = datetime.fromisoformat(data.get("created_at", "2000-01-01T00:00:00+00:00")) - if datetime.now(timezone.utc) - created > timedelta(days=30): - print("⚠️ Token expired. Reconnection required.") - return None - return data +from app.env import CLIENT_NAME, API_BASE, TOKEN_FILE, IS_DISTANT +from app.vrchat_context import VRChatContext def verify_auth_cookie(auth_cookie): cookies = {"auth": auth_cookie} @@ -36,6 +16,51 @@ def verify_auth_cookie(auth_cookie): r = client.get("/auth") return r.status_code == 200 and r.json().get("ok", False) +def get_or_create_token(): + if IS_DISTANT: + print("⚠️ Running in distant environment, using VRChatContext.") + try: + VRChatContext.load() + token = VRChatContext.get() + if not verify_auth_cookie(token.auth_cookie): + print("❌ Remote token invalid. Please regenerate the token and update the distant source.") + return None + print("🔓 Auth ready from distant environment.") + return { + "manual_username": token.manual_username, + "displayName": token.display_name, + "user_id": token.user_id, + "auth": token.auth_header, + "auth_cookie": token.auth_cookie + } + except Exception as e: + print("❌ Failed to initialize VRChatContext:", e) + return None + + if not TOKEN_FILE.exists(): + return None + + with open(TOKEN_FILE, "r") as f: + data = json.load(f) + + created = datetime.fromisoformat(data.get("created_at", "2000-01-01T00:00:00+00:00")) + if datetime.now(timezone.utc) - created > timedelta(days=30): + print("⚠️ Token expired. Reconnection required.") + return None + + if not verify_auth_cookie(data.get("auth_cookie", "")): + print("❌ Local token invalid. Please log in again.") + return None + + print("🟢 Local token valid.") + return data + +def save_token(data): + data["created_at"] = datetime.now(timezone.utc).isoformat() + TOKEN_FILE.parent.mkdir(parents=True, exist_ok=True) + with open(TOKEN_FILE, "w") as f: + json.dump(data, f, indent=4) + def login(): print("🔐 Connecting to VRChat") manual_username = input("Username: ") @@ -51,7 +76,6 @@ def login(): } with httpx.Client(base_url=API_BASE, headers=headers) as client: - # 1 - First GET call to /auth/user with Basic Auth r = client.get("/auth/user") if r.status_code != 200: print("❌ Connection failed:", r.text) @@ -59,12 +83,10 @@ def login(): data = r.json() - # 2 - Check 2FA if "requiresTwoFactorAuth" in data: mfa_types = data["requiresTwoFactorAuth"] print(f"🔐 2FA required: {mfa_types}") - # The Authorization header is removed for the following request client.headers.pop("Authorization", None) if "otp" in mfa_types: @@ -84,7 +106,6 @@ def login(): return None print("✅ 2FA verified!") - # 3 - Repeat a GET /auth/user without Basic Auth to confirm the session r3 = client.get("/auth/user") if r3.status_code != 200: print("❌ Failed to fetch user data after 2FA:", r3.text) @@ -92,7 +113,6 @@ def login(): data = r3.json() - # 4 - Retrieve auth cookie auth_cookie = None for cookie in client.cookies.jar: if cookie.name == "auth": @@ -103,14 +123,12 @@ def login(): print("❌ Auth cookie not found after login.") return None - # 5 - Check cookie if not verify_auth_cookie(auth_cookie): print("❌ Auth cookie invalid.") return None print("✅ Connected and verified.") - # 6 - Prepare the information to be backed up display_name = data.get("displayName", manual_username) user_id = data.get("id", "") @@ -122,25 +140,11 @@ def login(): "auth_cookie": auth_cookie } -def get_or_create_token(): - token = load_token() - if token: - print("🔑 Found saved token, verifying...") - if verify_auth_cookie(token.get("auth_cookie", "")): - print("🟢 Token already valid.") - return token - else: - print("⚠️ Saved token invalid, need to login again.") - - new_token = login() - if new_token: - save_token(new_token) - return new_token - return None - if __name__ == "__main__": token_data = get_or_create_token() if token_data: print("🔓 Auth ready. Token stored.") + if not IS_DISTANT: + save_token(token_data) else: print("❌ Unable to obtain a valid token.") diff --git a/requirements.txt b/requirements.txt index c3c1028..de3ba1b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,4 +2,5 @@ fastapi uvicorn[standard] httpx orjson -python-dotenv \ No newline at end of file +python-dotenv +email-validator \ No newline at end of file diff --git a/run.py b/run.py index 4e5f8d6..b8600ce 100644 --- a/run.py +++ b/run.py @@ -43,7 +43,7 @@ def main(): print("Virtual environment found.") print("Running VRChat authentication script...") - run_in_venv(["app/prelaunch/vrchat_auth.py"]) + run_in_venv(["python/vrchat_auth.py"]) print("Starting FastAPI server...")