Spaces:
Runtime error
Runtime error
File size: 8,829 Bytes
f974ede 687997d f974ede 52ca8dd 11ffe8b 52ca8dd 11ffe8b 52ca8dd 687997d aead0a6 52ca8dd f974ede 52ca8dd f974ede 52ca8dd df715f7 687997d df715f7 687997d df715f7 f0f8aed df715f7 f0f8aed 687997d f0f8aed 687997d f0f8aed df715f7 687997d df715f7 52ca8dd aead0a6 f0f8aed aead0a6 52ca8dd aead0a6 52ca8dd f974ede 52ca8dd f974ede 52ca8dd f974ede 52ca8dd f974ede 52ca8dd f974ede 52ca8dd f974ede 52ca8dd f974ede 52ca8dd f974ede 52ca8dd f974ede 52ca8dd f974ede 52ca8dd f974ede 52ca8dd f974ede 687997d f974ede 52ca8dd f974ede 52ca8dd df715f7 687997d df715f7 52ca8dd f974ede 52ca8dd f974ede 52ca8dd df715f7 687997d f974ede df715f7 687997d f974ede 52ca8dd 17ec854 52ca8dd f974ede | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 | import json
from fastapi import FastAPI, HTTPException, Header, BackgroundTasks, Request
from fastapi.responses import StreamingResponse
from pydantic import BaseModel
import os
import asyncio
import aiohttp
import aiofiles
from pyrogram import Client
import uuid
from dotenv import load_dotenv
# Load .env if it exists
load_dotenv()
app = FastAPI()
@app.get("/")
async def root():
return {"status": "Sonic Vault Active", "message": "Heavy Lifter Operational"}
# Environment Variables
API_ID = int(os.getenv("TELEGRAM_API_ID") or os.getenv("API_ID") or "0")
API_HASH = os.getenv("TELEGRAM_API_HASH") or os.getenv("API_HASH") or ""
BOT_TOKEN = os.getenv("TELEGRAM_BOT_TOKEN", "")
CHAT_ID = int(os.getenv("TELEGRAM_CHAT_ID", "0"))
HF_SECRET = os.getenv("HF_SECRET") or os.getenv("SECRET_TOKEN") or ""
HF_TOKEN = os.getenv("HF_TOKEN", "")
EXTERNAL_API_URL = os.getenv("EXTERNAL_API_URL", "https://api01.shrutibots.site")
YT_API_KEY = os.getenv("YT_API_KEY", "")
# Initialize Pyrogram Client (MTProto - Bot Only)
bot = Client(
"sonic_vault_bot",
api_id=API_ID,
api_hash=API_HASH,
bot_token=BOT_TOKEN,
in_memory=True
)
class VaultRequest(BaseModel):
videoId: str
type: str # 'audio' or 'video'
title: str
downloadUrl: str = None
callbackUrl: str = None
@app.on_event("startup")
async def startup():
await bot.start()
from functools import wraps
def require_auth(f):
@wraps(f)
async def decorated(*args, **kwargs):
request = kwargs.get('request')
if not request:
for arg in args:
if isinstance(arg, Request):
request = arg
break
if not request:
raise HTTPException(status_code=500, detail="Internal Server Error: Request object not found")
auth = request.headers.get("Authorization")
secret = request.headers.get("X-Secret-Token", "").strip().strip('"').strip("'")
# Extract bearer and clean it
bearer = None
if auth and auth.startswith("Bearer "):
bearer = auth.split(" ")[1].strip().strip('"').strip("'")
# Clean environment variables
clean_hf_token = HF_TOKEN.strip().strip('"').strip("'")
clean_hf_secret = HF_SECRET.strip().strip('"').strip("'")
# Dual-header authentication:
# 1. Bearer token must match either clean_hf_token or clean_hf_secret
# 2. X-Secret-Token must match clean_hf_secret
is_bearer_valid = False
if bearer:
if clean_hf_token and bearer == clean_hf_token:
is_bearer_valid = True
elif clean_hf_secret and bearer == clean_hf_secret:
is_bearer_valid = True
elif not clean_hf_token and not clean_hf_secret:
is_bearer_valid = True
is_secret_valid = (secret == clean_hf_secret) if clean_hf_secret else True
if not is_bearer_valid or not is_secret_valid:
print(f"[AUTH] Denied. Bearer: {'Match' if is_bearer_valid else 'Fail'} (len:{len(bearer or '')}), Secret: {'Match' if is_secret_valid else 'Fail'} (len:{len(secret)})")
# If Bearer failed, log if it matched the secret instead (common misconfiguration)
if not is_bearer_valid and bearer == clean_hf_secret:
print("[AUTH] Warning: Bearer token matched HF_SECRET instead of HF_TOKEN.")
raise HTTPException(status_code=401, detail="Unauthorized")
return await f(*args, **kwargs)
return decorated
async def fetch_download_url(video_id: str, media_type: str):
try:
async with aiohttp.ClientSession() as session:
url = f"{EXTERNAL_API_URL}/download"
params = {"url": video_id, "type": media_type, "api_key": YT_API_KEY}
headers = {"Authorization": f"Bearer {HF_TOKEN}"} if HF_TOKEN else {}
async with session.get(url, params=params, headers=headers) as resp:
if resp.status == 200:
return f"{EXTERNAL_API_URL}/download?url={video_id}&type={media_type}&api_key={YT_API_KEY}"
except Exception as e:
print(f"[Vault] Error fetching download URL: {str(e)}")
return None
async def process_vault(request: VaultRequest):
base_file = f"/tmp/{request.videoId}_{uuid.uuid4().hex[:8]}"
file_path = f"{base_file}.{'mp4' if request.type == 'video' else 'mp3'}"
final_path = None
try:
# 1. Resolve Download URL via External API ONLY
download_url = request.downloadUrl
if not download_url:
print(f"[Vault] Resolving {request.videoId} via {EXTERNAL_API_URL}...")
download_url = await fetch_download_url(request.videoId, request.type)
if not download_url:
raise Exception("Could not resolve download URL from external API")
print(f"[Vault] Downloading from {download_url}...")
async with aiohttp.ClientSession() as session:
async with session.get(download_url) as resp:
if resp.status == 200:
async with aiofiles.open(file_path, mode='wb') as f:
async for chunk in resp.content.iter_chunked(1024 * 1024):
await f.write(chunk)
final_path = file_path
print(f"[Vault] Downloaded: {os.path.getsize(final_path)} bytes")
else:
raise Exception(f"Download failed with status {resp.status}")
# 2. Upload to Telegram (MTProto)
print(f"[Vault] Uploading to chat {CHAT_ID}...")
caption = f"{request.title}\nID: {request.videoId}"
if request.type == 'video':
msg = await bot.send_video(
chat_id=CHAT_ID,
video=final_path,
caption=caption,
supports_streaming=True,
file_name=f"{request.title}.mp4"
)
file_id = msg.video.file_id
else:
msg = await bot.send_audio(
chat_id=CHAT_ID,
audio=final_path,
caption=caption,
title=request.title,
performer="Sonic Vault",
file_name=f"{request.title}.mp3"
)
file_id = msg.audio.file_id
print(f"[Vault] Upload success! Message ID: {msg.id}")
# 3. Callback
if request.callbackUrl:
payload = {
"status": "success",
"telegram_file_id": file_id,
"message_id": msg.id,
"videoId": request.videoId,
"type": request.type,
"title": request.title
}
async with aiohttp.ClientSession() as session:
await session.post(
request.callbackUrl,
json=payload,
headers={
"Authorization": f"Bearer {HF_SECRET}",
"X-Secret-Token": HF_SECRET
},
timeout=15
)
except Exception as e:
print(f"[Vault] Process Error: {str(e)}")
if request.callbackUrl:
try:
async with aiohttp.ClientSession() as session:
await session.post(
request.callbackUrl,
json={"status": "error", "error": str(e), "videoId": request.videoId},
headers={
"Authorization": f"Bearer {HF_SECRET}",
"X-Secret-Token": HF_SECRET
},
timeout=10
)
except: pass
finally:
if final_path and os.path.exists(final_path):
try: os.remove(final_path)
except: pass
@app.post("/vault")
@require_auth
async def vault_media(request: Request, vault_req: VaultRequest, background_tasks: BackgroundTasks):
background_tasks.add_task(process_vault, vault_req)
return {"status": "accepted", "message": "Vaulting started"}
@app.get("/proxy-stream/{file_id}")
@require_auth
async def proxy_stream(request: Request, file_id: str, type: str = "audio"):
async def stream_generator():
try:
# Pyrogram's stream_media uses MTProto (Bot Only)
async for chunk in bot.stream_media(file_id):
yield chunk
except Exception as e:
print(f"[Stream] MTProto Error: {str(e)}")
media_type = "video/mp4" if type == "video" else "audio/mpeg"
return StreamingResponse(stream_generator(), media_type=media_type)
@app.get("/health")
async def health():
return {"status": "ok", "mtproto": "active" if bot.is_connected else "inactive"}
|