Initial
This commit is contained in:
commit
5f102a7820
2
.env.example
Normal file
2
.env.example
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
CLIENT_NAME="YourAPINme (Dev)/1.0"
|
||||||
|
VRCHAT_API_BASE="https://api.vrchat.cloud/api/1"
|
8
.gitignore
vendored
Normal file
8
.gitignore
vendored
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
@"
|
||||||
|
__pycache__/
|
||||||
|
*.pyc
|
||||||
|
venv/
|
||||||
|
*.log
|
||||||
|
"@ | Out-File -Encoding utf8 .gitignore
|
||||||
|
.env
|
||||||
|
data/*
|
112
README.md
Normal file
112
README.md
Normal file
@ -0,0 +1,112 @@
|
|||||||
|
<img src="./data/static/logo.png" width="300" style="margin-top: 10px;" draggable="false">
|
||||||
|
|
||||||
|
A lightweight, fast, and secure **FastAPI** proxy server for the [VRChat API](https://vrchat.community/getting-started).
|
||||||
|
Designed to handle authentication, token management, and provide cached access to public and private VRChat data endpoints.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🚀 Features
|
||||||
|
|
||||||
|
- **Seamless VRChat account authentication** with support for 2FA (email & TOTP)
|
||||||
|
- Automated token storage and expiry management (configurable cache duration)
|
||||||
|
- Public API endpoints to fetch VRChat groups & users info
|
||||||
|
- Private API endpoints secured by VRChat auth tokens
|
||||||
|
- Written in Python with FastAPI and HTTPX for async requests
|
||||||
|
- Auto environment setup with virtual environment creation & dependency installation
|
||||||
|
- Ready to deploy on any server with Python 3.8+ (tested with YunoHost)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 💡 Why this project?
|
||||||
|
|
||||||
|
VRChat’s official API requires complex login flows and token management that can be cumbersome to implement for your apps or bots.
|
||||||
|
**K-API** simplifies this by handling authentication and caching internally, exposing easy REST endpoints for your applications.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🛠️ Getting Started
|
||||||
|
|
||||||
|
### Requirements
|
||||||
|
|
||||||
|
- Python 3.8 or higher installed globally
|
||||||
|
- Git (optional)
|
||||||
|
|
||||||
|
### Installation & Running
|
||||||
|
|
||||||
|
Clone this repository:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone https://git.kvs.fyi/kryscau/VRChatAPI.git
|
||||||
|
# You can use the mirror with Github with: https://github.com/kryscau/VRChatAPI.git
|
||||||
|
cd VRChatAPI
|
||||||
|
```
|
||||||
|
|
||||||
|
Run the included Python bootstrap script to create and activate the virtual environment, install dependencies, authenticate your VRChat account, and start the server:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python run.py
|
||||||
|
```
|
||||||
|
|
||||||
|
> This script will prompt for your VRChat username, password, and 2FA code if required.
|
||||||
|
> Tokens are stored securely and refreshed automatically every 30 days.
|
||||||
|
|
||||||
|
### Access the API
|
||||||
|
|
||||||
|
- Public endpoint example:
|
||||||
|
`GET /api/public/groups/{group_id}`
|
||||||
|
Returns info about a VRChat group.
|
||||||
|
|
||||||
|
- Private endpoint example (requires authentication or specific permission in VRChat [join group, bans perms, ...]):
|
||||||
|
`GET /api/private/users/{user_id}`
|
||||||
|
Returns private user data accessible with your token.
|
||||||
|
|
||||||
|
Explore interactive docs at:
|
||||||
|
`http://127.0.0.1:8000/docs`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔒 Security & Privacy
|
||||||
|
|
||||||
|
- Your VRChat credentials and tokens are stored **locally** in JSON files inside the `data/auth/` directory.
|
||||||
|
- No credentials or tokens are ever sent to third-party servers.
|
||||||
|
- Use HTTPS and proper firewall rules when deploying publicly.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📁 Project Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
/app
|
||||||
|
/api # FastAPI route modules (public/private)
|
||||||
|
/data/auth # Token storage (auto-generated)
|
||||||
|
/prelaunch # Authentication helper scripts
|
||||||
|
run.py # Python bootstrapper script
|
||||||
|
requirements.txt # Python dependencies
|
||||||
|
README.md # This file
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🤝 Contribution
|
||||||
|
|
||||||
|
Feel free to open issues or submit pull requests.
|
||||||
|
Feature requests and bug reports are welcome!
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ⚡ License
|
||||||
|
|
||||||
|
MIT License © 2025 Kryscau (K-API)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🚀 Next Steps
|
||||||
|
|
||||||
|
- Add WebSocket support for real-time VRChat events
|
||||||
|
- Implement caching layers with Redis or similar
|
||||||
|
- Dockerize for easy container deployments
|
||||||
|
- Add OAuth support for multi-user API proxies
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
Made with ❤️ by [Kryscau](https://kryscau.github.io).
|
162
app/api/vrchat_groups.py
Normal file
162
app/api/vrchat_groups.py
Normal file
@ -0,0 +1,162 @@
|
|||||||
|
from fastapi import APIRouter, HTTPException
|
||||||
|
import httpx
|
||||||
|
import json
|
||||||
|
from app.env import CLIENT_NAME, API_BASE, TOKEN_FILE
|
||||||
|
|
||||||
|
router = APIRouter()
|
||||||
|
|
||||||
|
def load_token():
|
||||||
|
if not TOKEN_FILE.exists():
|
||||||
|
return None
|
||||||
|
with open(TOKEN_FILE, "r") as f:
|
||||||
|
return json.load(f)
|
||||||
|
|
||||||
|
@router.get("/groups/{group_id}")
|
||||||
|
async def get_groups(group_id: str):
|
||||||
|
token = load_token()
|
||||||
|
if not token:
|
||||||
|
raise HTTPException(status_code=401, detail="Token not found, please authenticate first")
|
||||||
|
|
||||||
|
auth_cookie = token.get("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}
|
||||||
|
params = {
|
||||||
|
"includeRoles": "true",
|
||||||
|
"purpose": "group"
|
||||||
|
}
|
||||||
|
url = f"{API_BASE}/groups/{group_id}"
|
||||||
|
|
||||||
|
async with httpx.AsyncClient() as client:
|
||||||
|
r = await client.get(url, headers=headers, cookies=cookies, params=params)
|
||||||
|
|
||||||
|
if r.status_code != 200:
|
||||||
|
raise HTTPException(status_code=r.status_code, detail=f"Failed to fetch group info: {r.text}")
|
||||||
|
|
||||||
|
return r.json()
|
||||||
|
|
||||||
|
@router.get("/groups/{group_id}/instances")
|
||||||
|
async def get_groups_instances(group_id: str, user_id: str):
|
||||||
|
token = load_token()
|
||||||
|
if not token:
|
||||||
|
raise HTTPException(status_code=401, detail="Token not found, please authenticate first")
|
||||||
|
|
||||||
|
auth_cookie = token.get("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}/groups/{group_id}/instances"
|
||||||
|
|
||||||
|
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 groups instances info: {r.text}")
|
||||||
|
|
||||||
|
return r.json()
|
||||||
|
|
||||||
|
@router.get("/groups/{group_id}/posts")
|
||||||
|
async def get_groups_posts(group_id: str):
|
||||||
|
token = load_token()
|
||||||
|
if not token:
|
||||||
|
raise HTTPException(status_code=401, detail="Token not found, please authenticate first")
|
||||||
|
|
||||||
|
auth_cookie = token.get("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}
|
||||||
|
params = {
|
||||||
|
"n": "10",
|
||||||
|
"offset": "0",
|
||||||
|
"publicOnly": False
|
||||||
|
}
|
||||||
|
url = f"{API_BASE}/groups/{group_id}/posts"
|
||||||
|
|
||||||
|
async with httpx.AsyncClient() as client:
|
||||||
|
r = await client.get(url, headers=headers, cookies=cookies, params=params)
|
||||||
|
|
||||||
|
if r.status_code != 200:
|
||||||
|
raise HTTPException(status_code=r.status_code, detail=f"Failed to fetch groups posts info: {r.text}")
|
||||||
|
|
||||||
|
return r.json()
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/groups/{group_id}/bans")
|
||||||
|
async def get_groups_bans(group_id: str):
|
||||||
|
token = load_token()
|
||||||
|
if not token:
|
||||||
|
raise HTTPException(status_code=401, detail="Token not found, please authenticate first")
|
||||||
|
|
||||||
|
auth_cookie = token.get("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}
|
||||||
|
params = {
|
||||||
|
"n": "51",
|
||||||
|
"offset": "0"
|
||||||
|
}
|
||||||
|
url = f"{API_BASE}/groups/{group_id}/bans"
|
||||||
|
|
||||||
|
async with httpx.AsyncClient() as client:
|
||||||
|
r = await client.get(url, headers=headers, cookies=cookies, params=params)
|
||||||
|
|
||||||
|
if r.status_code != 200:
|
||||||
|
raise HTTPException(status_code=r.status_code, detail=f"Failed to fetch groups bans info: {r.text}")
|
||||||
|
|
||||||
|
return r.json()
|
||||||
|
|
||||||
|
@router.get("/groups/{group_id}/roles")
|
||||||
|
async def get_groups_roles(group_id: str):
|
||||||
|
token = load_token()
|
||||||
|
if not token:
|
||||||
|
raise HTTPException(status_code=401, detail="Token not found, please authenticate first")
|
||||||
|
|
||||||
|
auth_cookie = token.get("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}/groups/{group_id}/roles"
|
||||||
|
|
||||||
|
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 groups roles info: {r.text}")
|
||||||
|
|
||||||
|
return r.json()
|
||||||
|
|
||||||
|
@router.get("/groups/{group_id}/members")
|
||||||
|
async def get_groups_members(group_id: str):
|
||||||
|
token = load_token()
|
||||||
|
if not token:
|
||||||
|
raise HTTPException(status_code=401, detail="Token not found, please authenticate first")
|
||||||
|
|
||||||
|
auth_cookie = token.get("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}
|
||||||
|
params = {
|
||||||
|
"n": "25",
|
||||||
|
"offset": "0"
|
||||||
|
}
|
||||||
|
url = f"{API_BASE}/groups/{group_id}/members"
|
||||||
|
|
||||||
|
async with httpx.AsyncClient() as client:
|
||||||
|
r = await client.get(url, headers=headers, cookies=cookies, params=params)
|
||||||
|
|
||||||
|
if r.status_code != 200:
|
||||||
|
raise HTTPException(status_code=r.status_code, detail=f"Failed to fetch groups members info: {r.text}")
|
||||||
|
|
||||||
|
return r.json()
|
108
app/api/vrchat_users.py
Normal file
108
app/api/vrchat_users.py
Normal file
@ -0,0 +1,108 @@
|
|||||||
|
from fastapi import APIRouter, HTTPException
|
||||||
|
import httpx
|
||||||
|
import json
|
||||||
|
from app.env import CLIENT_NAME, API_BASE, TOKEN_FILE
|
||||||
|
|
||||||
|
router = APIRouter()
|
||||||
|
|
||||||
|
def load_token():
|
||||||
|
if not TOKEN_FILE.exists():
|
||||||
|
return None
|
||||||
|
with open(TOKEN_FILE, "r") as f:
|
||||||
|
return json.load(f)
|
||||||
|
|
||||||
|
@router.get("/users/{user_id}")
|
||||||
|
async def get_user(user_id: str):
|
||||||
|
token = load_token()
|
||||||
|
if not token:
|
||||||
|
raise HTTPException(status_code=401, detail="Token not found, please authenticate first")
|
||||||
|
|
||||||
|
auth_cookie = token.get("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/{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 info: {r.text}")
|
||||||
|
|
||||||
|
return r.json()
|
||||||
|
|
||||||
|
@router.get("/users/{user_id}/friends/status")
|
||||||
|
async def get_user_friend_status(user_id: str):
|
||||||
|
token = load_token()
|
||||||
|
if not token:
|
||||||
|
raise HTTPException(status_code=401, detail="Token not found, please authenticate first")
|
||||||
|
|
||||||
|
auth_cookie = token.get("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/{user_id}/friendStatus"
|
||||||
|
|
||||||
|
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 friend status info: {r.text}")
|
||||||
|
|
||||||
|
return r.json()
|
||||||
|
|
||||||
|
@router.get("/users/{user_id}/worlds")
|
||||||
|
async def get_user_worlds(user_id: str):
|
||||||
|
token = load_token()
|
||||||
|
if not token:
|
||||||
|
raise HTTPException(status_code=401, detail="Token not found, please authenticate first")
|
||||||
|
|
||||||
|
auth_cookie = token.get("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}
|
||||||
|
params = {
|
||||||
|
"releaseStatus": "public",
|
||||||
|
"sort": "updated",
|
||||||
|
"order": "descending",
|
||||||
|
"userId": user_id,
|
||||||
|
"n": "100",
|
||||||
|
"offset": "0"
|
||||||
|
}
|
||||||
|
url = f"{API_BASE}/worlds"
|
||||||
|
|
||||||
|
async with httpx.AsyncClient() as client:
|
||||||
|
r = await client.get(url, headers=headers, cookies=cookies, params=params)
|
||||||
|
|
||||||
|
if r.status_code != 200:
|
||||||
|
raise HTTPException(status_code=r.status_code, detail=f"Failed to fetch user worlds info: {r.text}")
|
||||||
|
|
||||||
|
return r.json()
|
||||||
|
|
||||||
|
@router.get("/users/{user_id}/groups")
|
||||||
|
async def get_user_groups(user_id: str):
|
||||||
|
token = load_token()
|
||||||
|
if not token:
|
||||||
|
raise HTTPException(status_code=401, detail="Token not found, please authenticate first")
|
||||||
|
|
||||||
|
auth_cookie = token.get("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/{user_id}/groups"
|
||||||
|
|
||||||
|
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 groups info: {r.text}")
|
||||||
|
|
||||||
|
return r.json()
|
8
app/env.py
Normal file
8
app/env.py
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
from dotenv import load_dotenv
|
||||||
|
import os
|
||||||
|
from pathlib import Path
|
||||||
|
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"))
|
32
app/main.py
Normal file
32
app/main.py
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
from fastapi import FastAPI
|
||||||
|
from app.api.vrchat_users import router as users
|
||||||
|
from app.api.vrchat_groups import router as groups
|
||||||
|
|
||||||
|
app = FastAPI(
|
||||||
|
title="K-API",
|
||||||
|
description="""
|
||||||
|
K-API is a fast, secure, and lightweight proxy API for VRChat.
|
||||||
|
It handles authentication via VRChat’s official API, including 2FA support,
|
||||||
|
and provides cached endpoints for user and group information retrieval.
|
||||||
|
|
||||||
|
This project is designed for developers who want a hassle-free way
|
||||||
|
to integrate VRChat data into their apps without managing sessions or tokens manually.
|
||||||
|
|
||||||
|
Features:
|
||||||
|
- Automatic token management with 2FA handling
|
||||||
|
- Public and private VRChat data endpoints
|
||||||
|
- Response caching for performance
|
||||||
|
- Easy deployment on self-hosted servers (YunoHost compatible)
|
||||||
|
|
||||||
|
Built with FastAPI and async HTTPX for high performance and reliability.
|
||||||
|
""",
|
||||||
|
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" }
|
||||||
|
)
|
||||||
|
prefix = "/api"
|
||||||
|
|
||||||
|
app.include_router(users, prefix=prefix, tags=["Users"])
|
||||||
|
app.include_router(groups, prefix=prefix, tags=["Groups"])
|
142
app/prelaunch/vrchat_auth.py
Normal file
142
app/prelaunch/vrchat_auth.py
Normal file
@ -0,0 +1,142 @@
|
|||||||
|
import httpx
|
||||||
|
import base64
|
||||||
|
import json
|
||||||
|
from datetime import datetime, timedelta, timezone
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
sys.path.append(str(Path(__file__).resolve().parent.parent))
|
||||||
|
from env import CLIENT_NAME, API_BASE, TOKEN_FILE
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
def verify_auth_cookie(auth_cookie):
|
||||||
|
cookies = {"auth": auth_cookie}
|
||||||
|
headers = {"User-Agent": CLIENT_NAME}
|
||||||
|
with httpx.Client(base_url=API_BASE, cookies=cookies, headers=headers) as client:
|
||||||
|
r = client.get("/auth")
|
||||||
|
return r.status_code == 200 and r.json().get("ok", False)
|
||||||
|
|
||||||
|
def login():
|
||||||
|
print("🔐 Connecting to VRChat")
|
||||||
|
manual_username = input("Username: ")
|
||||||
|
password = input("Password: ")
|
||||||
|
|
||||||
|
creds = f"{manual_username}:{password}"
|
||||||
|
b64 = base64.b64encode(creds.encode()).decode()
|
||||||
|
auth_header = f"Basic {b64}"
|
||||||
|
|
||||||
|
headers = {
|
||||||
|
"Authorization": auth_header,
|
||||||
|
"User-Agent": CLIENT_NAME
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
return None
|
||||||
|
|
||||||
|
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:
|
||||||
|
code = input("Code 2FA (TOTP): ")
|
||||||
|
verify_endpoint = "/auth/twofactorauth/verify"
|
||||||
|
elif "emailOtp" in mfa_types:
|
||||||
|
code = input("Code 2FA (email): ")
|
||||||
|
verify_endpoint = "/auth/twofactorauth/emailotp/verify"
|
||||||
|
else:
|
||||||
|
print("❌ Unknown 2FA type:", mfa_types)
|
||||||
|
return None
|
||||||
|
|
||||||
|
r2 = client.post(verify_endpoint, json={"code": code})
|
||||||
|
|
||||||
|
if r2.status_code != 200 or not r2.json().get("verified", False):
|
||||||
|
print("❌ 2FA verification failed:", r2.text)
|
||||||
|
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)
|
||||||
|
return None
|
||||||
|
|
||||||
|
data = r3.json()
|
||||||
|
|
||||||
|
# 4 - Retrieve auth cookie
|
||||||
|
auth_cookie = None
|
||||||
|
for cookie in client.cookies.jar:
|
||||||
|
if cookie.name == "auth":
|
||||||
|
auth_cookie = cookie.value
|
||||||
|
break
|
||||||
|
|
||||||
|
if not auth_cookie:
|
||||||
|
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", "")
|
||||||
|
|
||||||
|
return {
|
||||||
|
"manual_username": manual_username,
|
||||||
|
"displayName": display_name,
|
||||||
|
"user_id": user_id,
|
||||||
|
"auth": b64,
|
||||||
|
"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.")
|
||||||
|
else:
|
||||||
|
print("❌ Unable to obtain a valid token.")
|
5
requirements.txt
Normal file
5
requirements.txt
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
fastapi
|
||||||
|
uvicorn[standard]
|
||||||
|
httpx
|
||||||
|
orjson
|
||||||
|
python-dotenv
|
56
run.py
Normal file
56
run.py
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import subprocess
|
||||||
|
import venv
|
||||||
|
|
||||||
|
VENV_DIR = "venv"
|
||||||
|
REQ_FILE = "requirements.txt"
|
||||||
|
PORT = "8000"
|
||||||
|
|
||||||
|
# INFORMATIONS : This file does not require “venv” to run, but make sure you have Python installed globally (py run.py on Windows, for example).
|
||||||
|
# INFORMATIONS : This file does not require “venv” to run, but make sure you have Python installed globally (py run.py on Windows, for example).
|
||||||
|
# INFORMATIONS : This file does not require “venv” to run, but make sure you have Python installed globally (py run.py on Windows, for example).
|
||||||
|
# INFORMATIONS : This file does not require “venv” to run, but make sure you have Python installed globally (py run.py on Windows, for example).
|
||||||
|
# INFORMATIONS : This file does not require “venv” to run, but make sure you have Python installed globally (py run.py on Windows, for example).
|
||||||
|
|
||||||
|
def create_venv():
|
||||||
|
print("Creating virtual environment...")
|
||||||
|
venv.create(VENV_DIR, with_pip=True)
|
||||||
|
|
||||||
|
def run_in_venv(cmd):
|
||||||
|
if os.name == "nt":
|
||||||
|
# Windows
|
||||||
|
python_bin = os.path.join(VENV_DIR, "Scripts", "python.exe")
|
||||||
|
else:
|
||||||
|
# Unix
|
||||||
|
python_bin = os.path.join(VENV_DIR, "bin", "python")
|
||||||
|
full_cmd = [python_bin] + cmd
|
||||||
|
result = subprocess.run(full_cmd)
|
||||||
|
if result.returncode != 0:
|
||||||
|
print(f"Command {cmd} failed.")
|
||||||
|
sys.exit(result.returncode)
|
||||||
|
|
||||||
|
def install_requirements():
|
||||||
|
print("Installing dependencies...")
|
||||||
|
run_in_venv(["-m", "pip", "install", "--upgrade", "pip"])
|
||||||
|
run_in_venv(["-m", "pip", "install", "-r", REQ_FILE])
|
||||||
|
|
||||||
|
def main():
|
||||||
|
if not os.path.exists(VENV_DIR):
|
||||||
|
create_venv()
|
||||||
|
install_requirements()
|
||||||
|
else:
|
||||||
|
print("Virtual environment found.")
|
||||||
|
|
||||||
|
print("Running VRChat authentication script...")
|
||||||
|
run_in_venv(["app/prelaunch/vrchat_auth.py"])
|
||||||
|
|
||||||
|
print("Starting FastAPI server...")
|
||||||
|
|
||||||
|
HOST = "0.0.0.0"
|
||||||
|
RELOAD_FLAG = "--reload"
|
||||||
|
|
||||||
|
run_in_venv(["-m", "uvicorn", "app.main:app", "--host", HOST, "--port", PORT] + ([RELOAD_FLAG] if RELOAD_FLAG else []))
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
Loading…
x
Reference in New Issue
Block a user