This commit is contained in:
Unstealable 2025-06-27 14:10:40 +02:00
parent 4626fafef7
commit 944584e28a
14 changed files with 304 additions and 126 deletions

View File

@ -1,3 +1,4 @@
CLIENT_NAME="YourAPINme (Dev)/1.0"
VRCHAT_API_BASE="https://api.vrchat.cloud/api/1"
IS_RENDER=False
IS_DISTANT=False
DISTANT_URL_CONTEXT="https://distant.vrchat.cloud/data/account.json"

3
.gitignore vendored
View File

@ -5,4 +5,5 @@ venv/
*.log
"@ | Out-File -Encoding utf8 .gitignore
.env
data/*
data/
account.json.php

View File

@ -1,24 +0,0 @@
<?php
define('ACCESS_TOKEN', 'mon_token_de_securite_ici');
if (!isset($_GET['token']) || $_GET['token'] !== ACCESS_TOKEN) {
header('HTTP/1.1 401 Unauthorized');
echo json_encode(['error' => '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;
?>

View File

@ -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"}

View File

@ -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")

49
app/api/vrchat_search.py Normal file
View File

@ -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()

View File

@ -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")

View File

@ -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")
IS_DISTANT = os.getenv("IS_DISTANT", "false").lower() in ("true", "1", "t")

View File

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

68
app/vrchat_context.py Normal file
View File

@ -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

34
distant_accounts.json.php Normal file
View File

@ -0,0 +1,34 @@
<?php
$authorized_ip = '0.0.0.0';
$allowed_origin_prefix = 'https://vrchatapi-';
$allowed_origin_suffix = '.kvs.fyi';
$client_ip = $_SERVER['REMOTE_ADDR'] ?? '';
$origin = $_SERVER['HTTP_ORIGIN'] ?? '';
function startsWith($haystack, $needle) {
return substr($haystack, 0, strlen($needle)) === $needle;
}
function endsWith($haystack, $needle) {
return substr($haystack, -strlen($needle)) === $needle;
}
$is_ip_allowed = $client_ip === $authorized_ip;
$is_origin_allowed = startsWith($origin, $allowed_origin_prefix) && endsWith($origin, $allowed_origin_suffix);
if (!$is_ip_allowed && !$is_origin_allowed) {
header("Location: https://github.com/unstealable");
exit;
}
$vrchat_token = [
"manual_username" => "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);

View File

@ -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.")

View File

@ -2,4 +2,5 @@ fastapi
uvicorn[standard]
httpx
orjson
python-dotenv
python-dotenv
email-validator

2
run.py
View File

@ -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...")