Merge remote-tracking branch 'origin/routing_fix'
This commit is contained in:
commit
cf1b715ab8
19
Dockerfile
19
Dockerfile
|
|
@ -2,16 +2,19 @@ FROM python:3.11-slim
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
# Установка системных зависимостей
|
# Установка системных зависимостей (только необходимые)
|
||||||
RUN apt-get update && apt-get install -y \
|
RUN apt-get update \
|
||||||
gcc \
|
&& apt-get install -y --no-install-recommends \
|
||||||
g++ \
|
gcc \
|
||||||
curl \
|
g++ \
|
||||||
|
curl \
|
||||||
|
ffmpeg \
|
||||||
|
libmagic1 \
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
# Копирование requirements и установка Python зависимостей
|
# Копирование requirements и установка Python зависимостей
|
||||||
COPY requirements.txt .
|
COPY requirements.txt .
|
||||||
RUN pip install --no-cache-dir -r requirements.txt
|
RUN python -m pip install --upgrade pip && pip install --no-cache-dir -r requirements.txt
|
||||||
|
|
||||||
# Копирование кода приложения
|
# Копирование кода приложения
|
||||||
COPY app/ ./app/
|
COPY app/ ./app/
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,326 @@
|
||||||
|
"""
|
||||||
|
Compatibility routes to preserve deprecated uploader-bot API surface (v1/system).
|
||||||
|
These endpoints mirror legacy paths so older clients continue to function,
|
||||||
|
while new v3 sync API works in parallel.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import base64
|
||||||
|
import os
|
||||||
|
from typing import Optional, List
|
||||||
|
|
||||||
|
import aiofiles
|
||||||
|
from fastapi import APIRouter, UploadFile, File, HTTPException, Query
|
||||||
|
from fastapi.responses import JSONResponse, StreamingResponse, PlainTextResponse
|
||||||
|
from sqlalchemy import select
|
||||||
|
|
||||||
|
from app.core.logging import get_logger
|
||||||
|
from app.core.config import get_settings
|
||||||
|
from app.core.database import db_manager
|
||||||
|
from app.core.models.content_models import StoredContent as Content
|
||||||
|
from app.core.storage import LocalStorageBackend
|
||||||
|
|
||||||
|
router = APIRouter(prefix="", tags=["compat-v1"])
|
||||||
|
logger = get_logger(__name__)
|
||||||
|
settings = get_settings()
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/")
|
||||||
|
async def index_root():
|
||||||
|
return PlainTextResponse("MY Network Node", status_code=200)
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/favicon.ico")
|
||||||
|
async def favicon():
|
||||||
|
return PlainTextResponse("", status_code=204)
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/api/system.version")
|
||||||
|
async def system_version():
|
||||||
|
codebase_hash = os.getenv("CODEBASE_HASH", "unknown")
|
||||||
|
codebase_branch = os.getenv("CODEBASE_BRANCH", os.getenv("GIT_BRANCH", "main"))
|
||||||
|
return {"codebase_hash": codebase_hash, "codebase_branch": codebase_branch}
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/api/system.sendStatus")
|
||||||
|
async def system_send_status(payload: dict):
|
||||||
|
try:
|
||||||
|
message_b58 = payload.get("message")
|
||||||
|
signature = payload.get("signature")
|
||||||
|
if not message_b58 or not signature:
|
||||||
|
raise HTTPException(status_code=400, detail="message and signature required")
|
||||||
|
await logger.ainfo("Compat system.sendStatus", signature=signature)
|
||||||
|
return {"ok": True}
|
||||||
|
except HTTPException:
|
||||||
|
raise
|
||||||
|
except Exception as e:
|
||||||
|
await logger.aerror("sendStatus failed", error=str(e))
|
||||||
|
raise HTTPException(status_code=500, detail="sendStatus failed")
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/api/tonconnect-manifest.json")
|
||||||
|
async def tonconnect_manifest():
|
||||||
|
host = str(getattr(settings, "PROJECT_HOST", "")) or os.getenv("PROJECT_HOST", "") or "http://localhost:8000"
|
||||||
|
return {
|
||||||
|
"url": host,
|
||||||
|
"name": "MY Network Node",
|
||||||
|
"iconUrl": f"{host}/static/icon.png",
|
||||||
|
"termsOfUseUrl": f"{host}/terms",
|
||||||
|
"privacyPolicyUrl": f"{host}/privacy",
|
||||||
|
"bridgeUrl": "https://bridge.tonapi.io/bridge",
|
||||||
|
"manifestVersion": 2
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/api/platform-metadata.json")
|
||||||
|
async def platform_metadata():
|
||||||
|
host = str(getattr(settings, "PROJECT_HOST", "")) or os.getenv("PROJECT_HOST", "") or "http://localhost:8000"
|
||||||
|
return {
|
||||||
|
"name": "MY Network Platform",
|
||||||
|
"symbol": "MYN",
|
||||||
|
"description": "Decentralized content platform (v3)",
|
||||||
|
"image": f"{host}/static/platform.png",
|
||||||
|
"external_url": host,
|
||||||
|
"version": "3.0.0"
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/api/v1/node")
|
||||||
|
async def v1_node():
|
||||||
|
from app.core.crypto import get_ed25519_manager
|
||||||
|
cm = get_ed25519_manager()
|
||||||
|
return {
|
||||||
|
"id": cm.node_id,
|
||||||
|
"node_address": "",
|
||||||
|
"master_address": "",
|
||||||
|
"indexer_height": 0,
|
||||||
|
"services": {}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/api/v1/nodeFriendly")
|
||||||
|
async def v1_node_friendly():
|
||||||
|
from app.core.crypto import get_ed25519_manager
|
||||||
|
cm = get_ed25519_manager()
|
||||||
|
return PlainTextResponse(f"Node ID: {cm.node_id}\nIndexer height: 0\nServices: none\n")
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/api/v1/auth.twa")
|
||||||
|
async def v1_auth_twa(payload: dict):
|
||||||
|
user_ref = payload.get("user") or {}
|
||||||
|
token = base64.b64encode(f"twa:{user_ref}".encode()).decode()
|
||||||
|
return {"token": token}
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/api/v1/auth.me")
|
||||||
|
async def v1_auth_me():
|
||||||
|
return {"user": None, "status": "guest"}
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/api/v1/auth.selectWallet")
|
||||||
|
async def v1_auth_select_wallet(payload: dict):
|
||||||
|
return {"ok": True}
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/api/v1/tonconnect.new")
|
||||||
|
async def v1_tonconnect_new():
|
||||||
|
return {"ok": True}
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/api/v1/tonconnect.logout")
|
||||||
|
async def v1_tonconnect_logout(payload: dict):
|
||||||
|
return {"ok": True}
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/api/v1.5/storage")
|
||||||
|
async def v1_5_storage_upload(file: UploadFile = File(...)):
|
||||||
|
return await v1_storage_upload(file)
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/api/v1.5/storage/{file_hash}")
|
||||||
|
async def v1_5_storage_get(file_hash: str):
|
||||||
|
return await v1_storage_get(file_hash)
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/api/v1/storage")
|
||||||
|
async def v1_storage_upload(file: UploadFile = File(...)):
|
||||||
|
try:
|
||||||
|
data = await file.read()
|
||||||
|
if not data:
|
||||||
|
raise HTTPException(status_code=400, detail="empty file")
|
||||||
|
|
||||||
|
backend = LocalStorageBackend()
|
||||||
|
file_hash = sha256(data).hexdigest()
|
||||||
|
file_path = os.path.join(backend.files_path, file_hash)
|
||||||
|
async with aiofiles.open(file_path, 'wb') as f:
|
||||||
|
await f.write(data)
|
||||||
|
|
||||||
|
async with db_manager.get_session() as session:
|
||||||
|
existing = await session.execute(select(Content).where(Content.hash == file_hash))
|
||||||
|
if existing.scalars().first() is None:
|
||||||
|
content = Content(
|
||||||
|
hash=file_hash,
|
||||||
|
filename=file.filename or file_hash,
|
||||||
|
file_size=len(data),
|
||||||
|
mime_type=file.content_type or "application/octet-stream",
|
||||||
|
file_path=str(file_path),
|
||||||
|
)
|
||||||
|
session.add(content)
|
||||||
|
await session.commit()
|
||||||
|
|
||||||
|
return {"hash": file_hash}
|
||||||
|
except HTTPException:
|
||||||
|
raise
|
||||||
|
except Exception as e:
|
||||||
|
await logger.aerror("v1 upload failed", error=str(e))
|
||||||
|
raise HTTPException(status_code=500, detail="upload failed")
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/api/v1/storage/{file_hash}")
|
||||||
|
async def v1_storage_get(file_hash: str):
|
||||||
|
try:
|
||||||
|
async with db_manager.get_session() as session:
|
||||||
|
result = await session.execute(select(Content).where(Content.hash == file_hash))
|
||||||
|
content = result.scalars().first()
|
||||||
|
if not content or not content.file_path:
|
||||||
|
raise HTTPException(status_code=404, detail="not found")
|
||||||
|
|
||||||
|
backend = LocalStorageBackend()
|
||||||
|
return StreamingResponse(backend.get_file_stream(content.file_path))
|
||||||
|
except HTTPException:
|
||||||
|
raise
|
||||||
|
except Exception as e:
|
||||||
|
await logger.aerror("v1 storage get failed", error=str(e))
|
||||||
|
raise HTTPException(status_code=500, detail="failed")
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/api/v1/storage.decodeContentId/{content_id}")
|
||||||
|
async def v1_decode_content_id(content_id: str):
|
||||||
|
try:
|
||||||
|
async with db_manager.get_session() as session:
|
||||||
|
result = await session.execute(select(Content).where(Content.id == content_id))
|
||||||
|
content = result.scalars().first()
|
||||||
|
if not content:
|
||||||
|
raise HTTPException(status_code=404, detail="not found")
|
||||||
|
return {
|
||||||
|
"id": content.id,
|
||||||
|
"hash": content.hash,
|
||||||
|
"filename": content.filename,
|
||||||
|
"size": content.file_size,
|
||||||
|
"mime_type": content.mime_type,
|
||||||
|
}
|
||||||
|
except HTTPException:
|
||||||
|
raise
|
||||||
|
except Exception as e:
|
||||||
|
await logger.aerror("decodeContentId failed", error=str(e))
|
||||||
|
raise HTTPException(status_code=500, detail="failed")
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/api/v1/content.list")
|
||||||
|
async def v1_content_list(limit: int = Query(50, ge=1, le=200), offset: int = Query(0, ge=0)):
|
||||||
|
try:
|
||||||
|
async with db_manager.get_session() as session:
|
||||||
|
result = await session.execute(select(Content).offset(offset).limit(limit))
|
||||||
|
items: List[Content] = result.scalars().all()
|
||||||
|
return {
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"id": it.id,
|
||||||
|
"hash": it.hash,
|
||||||
|
"filename": it.filename,
|
||||||
|
"size": it.file_size,
|
||||||
|
"mime_type": it.mime_type,
|
||||||
|
} for it in items
|
||||||
|
],
|
||||||
|
"limit": limit,
|
||||||
|
"offset": offset
|
||||||
|
}
|
||||||
|
except Exception as e:
|
||||||
|
await logger.aerror("content.list failed", error=str(e))
|
||||||
|
raise HTTPException(status_code=500, detail="failed")
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/api/v1/content.view")
|
||||||
|
async def v1_content_view(hash: Optional[str] = None, id: Optional[str] = None):
|
||||||
|
try:
|
||||||
|
if not hash and not id:
|
||||||
|
raise HTTPException(status_code=400, detail="hash or id required")
|
||||||
|
async with db_manager.get_session() as session:
|
||||||
|
stmt = select(Content)
|
||||||
|
if hash:
|
||||||
|
stmt = stmt.where(Content.hash == hash)
|
||||||
|
if id:
|
||||||
|
stmt = stmt.where(Content.id == id)
|
||||||
|
result = await session.execute(stmt)
|
||||||
|
content = result.scalars().first()
|
||||||
|
if not content:
|
||||||
|
raise HTTPException(status_code=404, detail="not found")
|
||||||
|
return {
|
||||||
|
"id": content.id,
|
||||||
|
"hash": content.hash,
|
||||||
|
"filename": content.filename,
|
||||||
|
"size": content.file_size,
|
||||||
|
"mime_type": content.mime_type,
|
||||||
|
"created_at": getattr(content, "created_at", None)
|
||||||
|
}
|
||||||
|
except HTTPException:
|
||||||
|
raise
|
||||||
|
except Exception as e:
|
||||||
|
await logger.aerror("content.view failed", error=str(e))
|
||||||
|
raise HTTPException(status_code=500, detail="failed")
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/api/v1/content.view/{content_address}")
|
||||||
|
async def v1_content_view_path(content_address: str):
|
||||||
|
try:
|
||||||
|
async with db_manager.get_session() as session:
|
||||||
|
result = await session.execute(select(Content).where((Content.id == content_address) | (Content.hash == content_address)))
|
||||||
|
content = result.scalars().first()
|
||||||
|
if not content:
|
||||||
|
raise HTTPException(status_code=404, detail="not found")
|
||||||
|
return {
|
||||||
|
"id": content.id,
|
||||||
|
"hash": content.hash,
|
||||||
|
"filename": content.filename,
|
||||||
|
"size": content.file_size,
|
||||||
|
"mime_type": content.mime_type,
|
||||||
|
"created_at": getattr(content, "created_at", None)
|
||||||
|
}
|
||||||
|
except HTTPException:
|
||||||
|
raise
|
||||||
|
except Exception as e:
|
||||||
|
await logger.aerror("content.view(path) failed", error=str(e))
|
||||||
|
raise HTTPException(status_code=500, detail="failed")
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/api/v1/content.friendlyList")
|
||||||
|
async def v1_content_friendly_list(limit: int = Query(50, ge=1, le=200), offset: int = Query(0, ge=0)):
|
||||||
|
return await v1_content_list(limit, offset)
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/api/v1.5/content.list")
|
||||||
|
async def v1_5_content_list(limit: int = Query(50, ge=1, le=200), offset: int = Query(0, ge=0)):
|
||||||
|
return await v1_content_list(limit, offset)
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/api/v1/blockchain.sendNewContentMessage")
|
||||||
|
async def v1_chain_send_new_content(payload: dict):
|
||||||
|
await logger.ainfo("compat blockchain.sendNewContentMessage", payload=payload)
|
||||||
|
return {"ok": True}
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/api/v1/blockchain.sendPurchaseContent")
|
||||||
|
async def v1_chain_send_purchase(payload: dict):
|
||||||
|
await logger.ainfo("compat blockchain.sendPurchaseContent", payload=payload)
|
||||||
|
return {"ok": True}
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/api/v1/blockchain.sendPurchaseContentMessage")
|
||||||
|
async def v1_chain_send_purchase_message(payload: dict):
|
||||||
|
await logger.ainfo("compat blockchain.sendPurchaseContentMessage", payload=payload)
|
||||||
|
return {"ok": True}
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/api/v1/account")
|
||||||
|
async def v1_account():
|
||||||
|
return {"ok": True}
|
||||||
|
|
||||||
|
|
@ -10,6 +10,7 @@ from fastapi.responses import JSONResponse
|
||||||
|
|
||||||
from app.core.crypto import get_ed25519_manager
|
from app.core.crypto import get_ed25519_manager
|
||||||
from app.core.logging import get_logger
|
from app.core.logging import get_logger
|
||||||
|
from app.core.database import get_cache_manager
|
||||||
|
|
||||||
logger = get_logger(__name__)
|
logger = get_logger(__name__)
|
||||||
|
|
||||||
|
|
@ -44,6 +45,24 @@ async def validate_node_request(request: Request) -> Dict[str, Any]:
|
||||||
|
|
||||||
try:
|
try:
|
||||||
message_data = json.loads(body.decode())
|
message_data = json.loads(body.decode())
|
||||||
|
# Anti-replay: validate timestamp and nonce
|
||||||
|
try:
|
||||||
|
ts = message_data.get("timestamp")
|
||||||
|
nonce = message_data.get("nonce")
|
||||||
|
if ts:
|
||||||
|
from datetime import datetime, timezone
|
||||||
|
now = datetime.now(timezone.utc).timestamp()
|
||||||
|
if abs(float(ts) - float(now)) > 300:
|
||||||
|
raise HTTPException(status_code=400, detail="stale timestamp")
|
||||||
|
if nonce:
|
||||||
|
cache = await get_cache_manager()
|
||||||
|
cache_key = f"replay:{node_id}:{nonce}"
|
||||||
|
if await cache.get(cache_key):
|
||||||
|
raise HTTPException(status_code=400, detail="replay detected")
|
||||||
|
await cache.set(cache_key, True, ttl=600)
|
||||||
|
except Exception:
|
||||||
|
# Backward compatible: missing fields
|
||||||
|
pass
|
||||||
except json.JSONDecodeError:
|
except json.JSONDecodeError:
|
||||||
raise HTTPException(status_code=400, detail="Invalid JSON in request body")
|
raise HTTPException(status_code=400, detail="Invalid JSON in request body")
|
||||||
|
|
||||||
|
|
@ -411,4 +430,4 @@ async def v3_network_stats():
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"V3 network stats error: {e}")
|
logger.error(f"V3 network stats error: {e}")
|
||||||
raise HTTPException(status_code=500, detail="Internal server error")
|
raise HTTPException(status_code=500, detail="Internal server error")
|
||||||
|
|
|
||||||
|
|
@ -39,6 +39,8 @@ from app.api.fastapi_content_routes import router as content_router
|
||||||
from app.api.fastapi_storage_routes import router as storage_router
|
from app.api.fastapi_storage_routes import router as storage_router
|
||||||
from app.api.fastapi_node_routes import router as node_router
|
from app.api.fastapi_node_routes import router as node_router
|
||||||
from app.api.fastapi_system_routes import router as system_router
|
from app.api.fastapi_system_routes import router as system_router
|
||||||
|
from app.api.fastapi_compat_routes import router as compat_router
|
||||||
|
from app.api.fastapi_v3_routes import router as v3_router
|
||||||
|
|
||||||
# ДОБАВЛЕНО: импорт дополнительных роутеров из app/api/routes/
|
# ДОБАВЛЕНО: импорт дополнительных роутеров из app/api/routes/
|
||||||
from app.api.routes.content_access_routes import router as content_access_router
|
from app.api.routes.content_access_routes import router as content_access_router
|
||||||
|
|
@ -226,6 +228,8 @@ def create_fastapi_app() -> FastAPI:
|
||||||
# ВНИМАНИЕ: эти роутеры уже имеют собственные префиксы (prefix=...), поэтому include без доп. prefix
|
# ВНИМАНИЕ: эти роутеры уже имеют собственные префиксы (prefix=...), поэтому include без доп. prefix
|
||||||
app.include_router(content_access_router) # /api/content/*
|
app.include_router(content_access_router) # /api/content/*
|
||||||
app.include_router(node_stats_router) # /api/node/stats/*
|
app.include_router(node_stats_router) # /api/node/stats/*
|
||||||
|
app.include_router(compat_router) # legacy v1/system
|
||||||
|
app.include_router(v3_router) # /api/v3/*
|
||||||
|
|
||||||
# Дополнительные обработчики событий
|
# Дополнительные обработчики событий
|
||||||
setup_exception_handlers(app)
|
setup_exception_handlers(app)
|
||||||
|
|
@ -310,6 +314,8 @@ def setup_exception_handlers(app: FastAPI):
|
||||||
|
|
||||||
# Увеличиваем счетчик ошибок для мониторинга
|
# Увеличиваем счетчик ошибок для мониторинга
|
||||||
from app.api.fastapi_system_routes import increment_error_counter
|
from app.api.fastapi_system_routes import increment_error_counter
|
||||||
|
from app.api.fastapi_compat_routes import router as compat_router
|
||||||
|
from app.api.fastapi_v3_routes import router as v3_router
|
||||||
await increment_error_counter()
|
await increment_error_counter()
|
||||||
|
|
||||||
return JSONResponse(
|
return JSONResponse(
|
||||||
|
|
@ -334,6 +340,8 @@ def setup_middleware_hooks(app: FastAPI):
|
||||||
|
|
||||||
# Увеличиваем счетчик запросов
|
# Увеличиваем счетчик запросов
|
||||||
from app.api.fastapi_system_routes import increment_request_counter
|
from app.api.fastapi_system_routes import increment_request_counter
|
||||||
|
from app.api.fastapi_compat_routes import router as compat_router
|
||||||
|
from app.api.fastapi_v3_routes import router as v3_router
|
||||||
await increment_request_counter()
|
await increment_request_counter()
|
||||||
|
|
||||||
# Проверяем режим обслуживания
|
# Проверяем режим обслуживания
|
||||||
|
|
@ -379,6 +387,8 @@ def setup_middleware_hooks(app: FastAPI):
|
||||||
)
|
)
|
||||||
|
|
||||||
from app.api.fastapi_system_routes import increment_error_counter
|
from app.api.fastapi_system_routes import increment_error_counter
|
||||||
|
from app.api.fastapi_compat_routes import router as compat_router
|
||||||
|
from app.api.fastapi_v3_routes import router as v3_router
|
||||||
await increment_error_counter()
|
await increment_error_counter()
|
||||||
|
|
||||||
raise
|
raise
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,14 @@
|
||||||
{
|
{
|
||||||
"version": "3.0.0",
|
"version": "3.0.0",
|
||||||
"network_id": "my-network-1754810662",
|
"network_id": "my-network-1755263533",
|
||||||
"created_at": "2025-08-10T07:24:22Z",
|
"created_at": "2025-08-15T13:12:13Z",
|
||||||
"bootstrap_nodes": [
|
"bootstrap_nodes": [
|
||||||
{
|
{
|
||||||
"id": "node-7fd144167286645c",
|
"id": "node-e3ebfd8e2444dd4f",
|
||||||
"node_id": "node-7fd144167286645c",
|
"node_id": "node-e3ebfd8e2444dd4f",
|
||||||
"address": "2a02:6b40:2000:16b1::1",
|
"address": "2a02:6b40:2000:16b1::1",
|
||||||
"port": 8000,
|
"port": 8000,
|
||||||
"public_key": "7fd144167286645c3b01cd16d87a775f57ec134dbad412e13f3016c33e936177",
|
"public_key": "e3ebfd8e2444dd4f8747472a3c753708e45a47b16f33401790caa5c5ca67534d",
|
||||||
"trusted": true,
|
"trusted": true,
|
||||||
"node_type": "bootstrap"
|
"node_type": "bootstrap"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,9 @@ asyncpg==0.29.0
|
||||||
redis==5.0.1
|
redis==5.0.1
|
||||||
aioredis==2.0.1
|
aioredis==2.0.1
|
||||||
aiofiles==23.2.1
|
aiofiles==23.2.1
|
||||||
|
aiohttp==3.12.15
|
||||||
|
yarl==1.17.1
|
||||||
|
multidict==6.0.5
|
||||||
cryptography==41.0.7
|
cryptography==41.0.7
|
||||||
python-jose[cryptography]==3.3.0
|
python-jose[cryptography]==3.3.0
|
||||||
python-multipart==0.0.6
|
python-multipart==0.0.6
|
||||||
|
|
@ -28,10 +31,16 @@ python-magic==0.4.27
|
||||||
jinja2==3.1.2
|
jinja2==3.1.2
|
||||||
starlette==0.27.0
|
starlette==0.27.0
|
||||||
structlog==23.2.0
|
structlog==23.2.0
|
||||||
aiogram==3.3.0
|
aiogram==3.21.0
|
||||||
|
magic-filter==1.0.12
|
||||||
sanic==23.12.1
|
sanic==23.12.1
|
||||||
PyJWT==2.8.0
|
PyJWT==2.8.0
|
||||||
cryptography==41.0.7
|
|
||||||
ed25519==1.5
|
ed25519==1.5
|
||||||
tonsdk==1.0.15
|
tonsdk==1.0.15
|
||||||
pytonconnect==0.3.0
|
pytonconnect==0.3.2
|
||||||
|
prometheus-client==0.22.1
|
||||||
|
pydub==0.25.1
|
||||||
|
pycryptodome==3.23.0
|
||||||
|
psycopg2-binary==2.9.10
|
||||||
|
PyNaCl==1.5.0
|
||||||
|
uvloop==0.21.0
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue