add files

This commit is contained in:
root 2025-08-16 15:08:00 +00:00
parent 4b06cd8a77
commit 8f2efee524
8 changed files with 291 additions and 92 deletions

View File

@ -81,7 +81,6 @@ async def platform_metadata():
} }
# Legacy index and favicon
@router.get("/") @router.get("/")
async def index_root(): async def index_root():
return PlainTextResponse("MY Network Node", status_code=200) return PlainTextResponse("MY Network Node", status_code=200)
@ -91,7 +90,6 @@ async def favicon():
return PlainTextResponse("", status_code=204) return PlainTextResponse("", status_code=204)
# Legacy node endpoints
@router.get("/api/v1/node") @router.get("/api/v1/node")
async def v1_node(): async def v1_node():
from app.core.crypto import get_ed25519_manager from app.core.crypto import get_ed25519_manager
@ -105,7 +103,6 @@ async def v1_node_friendly():
return PlainTextResponse(f"Node ID: {cm.node_id}\nIndexer height: 0\nServices: none\n") return PlainTextResponse(f"Node ID: {cm.node_id}\nIndexer height: 0\nServices: none\n")
# Legacy auth endpoints
@router.post("/api/v1/auth.twa") @router.post("/api/v1/auth.twa")
async def v1_auth_twa(payload: dict): async def v1_auth_twa(payload: dict):
user_ref = payload.get("user") or {} user_ref = payload.get("user") or {}
@ -141,7 +138,6 @@ async def v1_storage_upload(file: UploadFile = File(...)):
file_path = os.path.join(backend.files_path, file_hash) file_path = os.path.join(backend.files_path, file_hash)
async with aiofiles.open(file_path, 'wb') as f: async with aiofiles.open(file_path, 'wb') as f:
await f.write(data) await f.write(data)
# Возвращаем hash без записи ORM, чтобы избежать конфликтов схем
return {"hash": file_hash} return {"hash": file_hash}
except HTTPException: except HTTPException:
raise raise

View File

@ -20,12 +20,10 @@ router = APIRouter(prefix="/api/node", tags=["node-communication"])
async def validate_node_request(request: Request) -> Dict[str, Any]: async def validate_node_request(request: Request) -> Dict[str, Any]:
"""Валидация межузлового запроса с обязательной проверкой подписи""" """Валидация межузлового запроса с обязательной проверкой подписи"""
# Заголовки
required_headers = ["x-node-communication", "x-node-id", "x-node-public-key", "x-node-signature"] required_headers = ["x-node-communication", "x-node-id", "x-node-public-key", "x-node-signature"]
for header in required_headers: for header in required_headers:
if header not in request.headers: if header not in request.headers:
raise HTTPException(status_code=400, detail=f"Missing required header: {header}") raise HTTPException(status_code=400, detail=f"Missing required header: {header}")
if request.headers.get("x-node-communication") != "true": if request.headers.get("x-node-communication") != "true":
raise HTTPException(status_code=400, detail="Not a valid inter-node communication") raise HTTPException(status_code=400, detail="Not a valid inter-node communication")
@ -35,18 +33,15 @@ async def validate_node_request(request: Request) -> Dict[str, Any]:
node_id = request.headers.get("x-node-id") node_id = request.headers.get("x-node-id")
public_key = request.headers.get("x-node-public-key") public_key = request.headers.get("x-node-public-key")
# Тело запроса
body = await request.body() body = await request.body()
if not body: if not body:
raise HTTPException(status_code=400, detail="Empty message body") raise HTTPException(status_code=400, detail="Empty message body")
# JSON
try: try:
message_data = json.loads(body.decode()) message_data = json.loads(body.decode())
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")
# Anti-replay (необязательно для обратной совместимости) # Optional anti-replay
try: try:
ts = message_data.get("timestamp") ts = message_data.get("timestamp")
nonce = message_data.get("nonce") nonce = message_data.get("nonce")
@ -62,10 +57,8 @@ async def validate_node_request(request: Request) -> Dict[str, Any]:
raise HTTPException(status_code=400, detail="replay detected") raise HTTPException(status_code=400, detail="replay detected")
await cache.set(cache_key, True, ttl=600) await cache.set(cache_key, True, ttl=600)
except Exception: except Exception:
# ignore for backward compatibility
pass pass
# Подпись
is_valid = crypto_manager.verify_signature(message_data, signature, public_key) is_valid = crypto_manager.verify_signature(message_data, signature, public_key)
if not is_valid: if not is_valid:
logger.warning(f"Invalid signature from node {node_id}") logger.warning(f"Invalid signature from node {node_id}")

View File

@ -1,93 +1,124 @@
from app.core.logger import make_log, logger from app.core.logger import make_log, logger
from app.core.models._telegram import Wrapped_CBotChat from app.core.models._telegram import Wrapped_CBotChat
from app.core.models.user import User from app.core.models.user import User
from app.core.storage import db_session
from aiogram import BaseMiddleware, types from aiogram import BaseMiddleware, types
from app.core.models.messages import KnownTelegramMessage from app.core.models.messages import KnownTelegramMessage
from datetime import datetime from datetime import datetime
# Bot handlers historically use synchronous SQLAlchemy patterns.
# Keep a dedicated sync engine/session for bot middleware to preserve legacy behavior.
import re
from typing import Optional
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker, Session
from app.core.config import get_settings
_SYNC_ENGINE = None
_SYNC_FACTORY: Optional[sessionmaker] = None
def _to_sync_dsn(async_dsn: str) -> str:
# Convert postgresql+asyncpg:// to postgresql+psycopg2:// for synchronous engine
return re.sub(r"\+asyncpg", "+psycopg2", async_dsn)
def _ensure_sync_session() -> Session:
global _SYNC_ENGINE, _SYNC_FACTORY
if _SYNC_ENGINE is None or _SYNC_FACTORY is None:
settings = get_settings()
dsn = _to_sync_dsn(settings.DATABASE_URL)
_SYNC_ENGINE = create_engine(dsn, pool_pre_ping=True, future=True)
_SYNC_FACTORY = sessionmaker(bind=_SYNC_ENGINE, autocommit=False, autoflush=True)
return _SYNC_FACTORY()
class UserDataMiddleware(BaseMiddleware): class UserDataMiddleware(BaseMiddleware):
async def __call__(self, handler, event, data):
update_body = event.message or event.callback_query or event.inline_query or event.pre_checkout_query
if not update_body:
return
if update_body.from_user.is_bot is True: async def __call__(self, handler, event, data):
update_body = event.message or event.callback_query or getattr(event, 'inline_query', None) or getattr(event, 'pre_checkout_query', None)
if not update_body or getattr(update_body, 'from_user', None) is None:
return
if getattr(update_body.from_user, 'is_bot', False):
return return
user_id = update_body.from_user.id user_id = update_body.from_user.id
assert user_id >= 1 assert user_id >= 1
# TODO: maybe make users cache # Use sync session for bot handlers compatibility
from app.core.models.user.user import User as DbUser
from app.core.models.messages import KnownTelegramMessage as DbKnownMsg
from app.core.logging import logger as app_logger
with db_session(auto_commit=False) as session: session = _ensure_sync_session()
try:
# Load or create user
try: try:
user = session.query(User).filter_by(telegram_id=user_id).first() user = session.query(DbUser).filter(DbUser.telegram_id == user_id).first()
except BaseException as e: except Exception as e:
logger.error(f"Error when middleware getting user: {e}") await app_logger.aerror("Middleware get user failed", error=str(e))
user = None user = None
if user is None: if user is None:
logger.debug(f"User {user_id} not found. Creating new user") await app_logger.adebug("Creating new user", telegram_id=user_id)
user = User( user = DbUser(
telegram_id=user_id, telegram_id=user_id,
username=update_body.from_user.username, username=getattr(update_body.from_user, 'username', None),
lang_code='en', language_code=getattr(update_body.from_user, 'language_code', 'en'),
last_use=datetime.now(), meta={
meta=dict(first_name=update_body.from_user.first_name or '', 'first_name': getattr(update_body.from_user, 'first_name', '') or '',
last_name=update_body.from_user.last_name or '', username=update_body.from_user.username, 'last_name': getattr(update_body.from_user, 'last_name', '') or '',
language_code=update_body.from_user.language_code, 'username': getattr(update_body.from_user, 'username', None),
is_premium=update_body.from_user.is_premium), 'language_code': getattr(update_body.from_user, 'language_code', None),
created=datetime.now() 'is_premium': getattr(update_body.from_user, 'is_premium', False),
}
) )
session.add(user) session.add(user)
session.commit() session.commit()
else: else:
if user.username != update_body.from_user.username: # Update username/metadata
user.username = update_body.from_user.username changed = False
if user.username != getattr(update_body.from_user, 'username', None):
updated_meta_fields = {} user.username = getattr(update_body.from_user, 'username', None)
if user.meta.get('first_name') != update_body.from_user.first_name: changed = True
updated_meta_fields['first_name'] = update_body.from_user.first_name meta = dict(user.meta or {})
if meta.get('first_name') != getattr(update_body.from_user, 'first_name', None):
if user.meta.get('last_name') != update_body.from_user.last_name: meta['first_name'] = getattr(update_body.from_user, 'first_name', None)
updated_meta_fields['last_name'] = update_body.from_user.last_name changed = True
if meta.get('last_name') != getattr(update_body.from_user, 'last_name', None):
user.meta = { meta['last_name'] = getattr(update_body.from_user, 'last_name', None)
**user.meta, changed = True
**updated_meta_fields user.meta = meta
} user.last_activity = datetime.utcnow()
if changed:
user.last_use = datetime.now() session.commit()
session.commit()
data['user'] = user data['user'] = user
# Pass sync session for routers expecting .query()
data['db_session'] = session data['db_session'] = session
# chat_wrap can work with sync sessions too
data['chat_wrap'] = Wrapped_CBotChat(data['bot'], chat_id=user_id, db_session=session, user=user) data['chat_wrap'] = Wrapped_CBotChat(data['bot'], chat_id=user_id, db_session=session, user=user)
data['memory'] = data['dispatcher']._s_memory
# De-duplicate known messages
if getattr(update_body, 'text', None): if getattr(update_body, 'text', None):
message_type = 'common' existed = session.query(DbKnownMsg).filter(
if update_body.text.startswith('/start'): (DbKnownMsg.chat_id == update_body.chat.id) &
message_type = 'start_command' (DbKnownMsg.message_id == update_body.message_id) &
(DbKnownMsg.from_user == True)
if session.query(KnownTelegramMessage).filter_by( ).first()
chat_id=update_body.chat.id, if existed:
message_id=update_body.message_id, await app_logger.adebug("Message already processed", message_id=update_body.message_id)
from_user=True
).first():
make_log("UserDataMiddleware", f"Message {update_body.message_id} already processed", level='debug')
return return
new_message = KnownTelegramMessage( new_message = DbKnownMsg(
type=message_type, type='start_command' if str(update_body.text).startswith('/start') else 'common',
bot_id=data['chat_wrap'].bot_id, bot_id=data['chat_wrap'].bot_id,
chat_id=update_body.chat.id, chat_id=update_body.chat.id,
message_id=update_body.message_id, message_id=update_body.message_id,
from_user=True, from_user=True,
from_telegram_id=user_id, from_telegram_id=user_id,
created=datetime.now(), created=datetime.utcnow(),
meta={} meta={}
) )
session.add(new_message) session.add(new_message)
@ -95,3 +126,8 @@ class UserDataMiddleware(BaseMiddleware):
result = await handler(event, data) result = await handler(event, data)
return result return result
finally:
try:
session.close()
except Exception:
pass

67
app/bot/poller.py Normal file
View File

@ -0,0 +1,67 @@
"""
Lightweight Telegram bot poller to ensure the bot responds.
Does not depend on legacy sync DB middleware. Provides minimal /start handler.
"""
import asyncio
import logging
from typing import Optional
from aiogram import Bot, Dispatcher, Router, types
from aiogram.filters import Command
from app.core.config import get_settings
logger = logging.getLogger(__name__)
def build_router(name: str) -> Router:
r = Router(name=name)
@r.message(Command("start"))
async def cmd_start(message: types.Message):
await message.answer(
"MY Network bot online. Use /help to get options.")
@r.message(Command("help"))
async def cmd_help(message: types.Message):
await message.answer("Available: /start, /help")
return r
async def _run_single_bot(token: str, name: str):
bot = Bot(token)
dp = Dispatcher()
dp.include_router(build_router(name))
logger.info("Starting Telegram bot polling", extra={"name": name})
try:
await dp.start_polling(bot, allowed_updates=dp.resolve_used_update_types())
except Exception as e:
logger.error("Bot polling failed", extra={"name": name, "error": str(e)})
await bot.session.close()
async def start_bots_if_configured() -> list[asyncio.Task]:
settings = get_settings()
tasks: list[asyncio.Task] = []
# Prefer TELEGRAM_WEBHOOK_ENABLED to avoid double-processing
webhook_enabled = bool(getattr(settings, 'TELEGRAM_WEBHOOK_ENABLED', False))
if webhook_enabled:
logger.info("Telegram webhook enabled; skipping polling start")
return tasks
# Main bot
token: Optional[str] = getattr(settings, 'TELEGRAM_API_KEY', None)
if token:
tasks.append(asyncio.create_task(_run_single_bot(token, 'main')))
# Client bot
ct: Optional[str] = getattr(settings, 'CLIENT_TELEGRAM_API_KEY', None)
if ct:
tasks.append(asyncio.create_task(_run_single_bot(ct, 'client')))
if tasks:
logger.info("Telegram bots polling started", extra={"count": len(tasks)})
else:
logger.info("No Telegram tokens configured; polling not started")
return tasks

View File

@ -5,6 +5,8 @@ from sqlalchemy import and_
from app.core.logger import make_log from app.core.logger import make_log
from app.core.models.messages import KnownTelegramMessage from app.core.models.messages import KnownTelegramMessage
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select
from app.core._config import TELEGRAM_API_KEY, CLIENT_TELEGRAM_API_KEY from app.core._config import TELEGRAM_API_KEY, CLIENT_TELEGRAM_API_KEY
from app.core.models._telegram.templates.player import PlayerTemplates from app.core.models._telegram.templates.player import PlayerTemplates
@ -56,25 +58,36 @@ class Wrapped_CBotChat(T, PlayerTemplates):
if self.db_session: if self.db_session:
if message_type == 'common': if message_type == 'common':
ci = 0 ci = 0
for oc_msg in self.db_session.query(KnownTelegramMessage).filter( if isinstance(self.db_session, AsyncSession):
and_( res = await self.db_session.execute(select(KnownTelegramMessage).where(
KnownTelegramMessage.type == 'common', and_(
KnownTelegramMessage.bot_id == self.bot_id, KnownTelegramMessage.type == 'common',
KnownTelegramMessage.chat_id == self._chat_id, KnownTelegramMessage.bot_id == self.bot_id,
KnownTelegramMessage.deleted == False KnownTelegramMessage.chat_id == self._chat_id,
) KnownTelegramMessage.deleted == False
).all(): )
))
old = res.scalars().all()
else:
old = self.db_session.query(KnownTelegramMessage).filter(
and_(
KnownTelegramMessage.type == 'common',
KnownTelegramMessage.bot_id == self.bot_id,
KnownTelegramMessage.chat_id == self._chat_id,
KnownTelegramMessage.deleted == False
)
).all()
for oc_msg in old:
make_log(self, f"Delete old message {oc_msg.message_id} {oc_msg.type} {oc_msg.bot_id} {oc_msg.chat_id}") make_log(self, f"Delete old message {oc_msg.message_id} {oc_msg.type} {oc_msg.bot_id} {oc_msg.chat_id}")
await self.delete_message(oc_msg.message_id) await self.delete_message(oc_msg.message_id)
ci += 1 ci += 1
make_log(self, f"Deleted {ci} old messages", level='debug') make_log(self, f"Deleted {ci} old messages", level='debug')
if isinstance(result, types.Message): if isinstance(result, types.Message):
message_id = getattr(result, 'message_id', None) message_id = getattr(result, 'message_id', None)
assert message_id, "No message_id" assert message_id, "No message_id"
self.db_session.add( if isinstance(self.db_session, AsyncSession):
KnownTelegramMessage( self.db_session.add(KnownTelegramMessage(
type=message_type, type=message_type,
bot_id=self.bot_id, bot_id=self.bot_id,
chat_id=self._chat_id, chat_id=self._chat_id,
@ -83,9 +96,20 @@ class Wrapped_CBotChat(T, PlayerTemplates):
created=datetime.now(), created=datetime.now(),
meta=message_meta or {}, meta=message_meta or {},
content_id=content_id content_id=content_id
) ))
) await self.db_session.commit()
self.db_session.commit() else:
self.db_session.add(KnownTelegramMessage(
type=message_type,
bot_id=self.bot_id,
chat_id=self._chat_id,
message_id=message_id,
from_user=False,
created=datetime.now(),
meta=message_meta or {},
content_id=content_id
))
self.db_session.commit()
else: else:
make_log(self, f"Unknown result type: {type(result)}", level='warning') make_log(self, f"Unknown result type: {type(result)}", level='warning')
@ -137,14 +161,28 @@ class Wrapped_CBotChat(T, PlayerTemplates):
message_id message_id
)): )):
if self.db_session: if self.db_session:
known_message = self.db_session.query(KnownTelegramMessage).filter( known_message = None
KnownTelegramMessage.bot_id == self.bot_id, if isinstance(self.db_session, AsyncSession):
KnownTelegramMessage.chat_id == self._chat_id, res = await self.db_session.execute(select(KnownTelegramMessage).where(
KnownTelegramMessage.message_id == message_id and_(
).first() KnownTelegramMessage.bot_id == self.bot_id,
KnownTelegramMessage.chat_id == self._chat_id,
KnownTelegramMessage.message_id == message_id
)
))
known_message = res.scalars().first()
else:
known_message = self.db_session.query(KnownTelegramMessage).filter(
KnownTelegramMessage.bot_id == self.bot_id,
KnownTelegramMessage.chat_id == self._chat_id,
KnownTelegramMessage.message_id == message_id
).first()
if known_message: if known_message:
known_message.deleted = True known_message.deleted = True
self.db_session.commit() if isinstance(self.db_session, AsyncSession):
await self.db_session.commit()
else:
self.db_session.commit()
except Exception as e: except Exception as e:
make_log(self, f"Error deleting message {self._chat_id}/{message_id}. Error: {e}", level='warning') make_log(self, f"Error deleting message {self._chat_id}/{message_id}. Error: {e}", level='warning')
return None return None

View File

@ -122,6 +122,13 @@ async def lifespan(app: FastAPI):
# Проверка готовности системы # Проверка готовности системы
await logger.ainfo("System initialization completed successfully") await logger.ainfo("System initialization completed successfully")
# Start Telegram bot polling (non-blocking) if configured
try:
from app.services.telegram_poller import start_bots_if_configured
app.state._bot_tasks = await start_bots_if_configured()
except Exception as _e:
await logger.awarning('Telegram bot polling not started', error=str(_e))
yield yield
except Exception as e: except Exception as e:
@ -150,6 +157,16 @@ async def lifespan(app: FastAPI):
except Exception as e: except Exception as e:
await logger.aerror(f"Error closing cache: {e}") await logger.aerror(f"Error closing cache: {e}")
# Stop Telegram bot polling tasks if any
try:
tasks = getattr(app.state, '_bot_tasks', []) or []
for t in tasks:
t.cancel()
if tasks:
await logger.ainfo('Telegram bot polling tasks cancelled', count=len(tasks))
except Exception as _e:
await logger.awarning('Failed to cancel bot tasks', error=str(_e))
await logger.ainfo("Application shutdown completed") await logger.ainfo("Application shutdown completed")

View File

@ -0,0 +1,52 @@
"""
Project Telegram polling startup: launches both main and client bots with full routers and middleware.
Cleans webhooks before polling to ensure updates delivery.
"""
import asyncio
import logging
from typing import Optional
from aiogram import Bot, Dispatcher
from aiogram.fsm.storage.memory import MemoryStorage
from app.core.config import get_settings
from app.bot.middleware import UserDataMiddleware
from app.bot.routers.index import main_router as main_bot_router
from app.client_bot.routers.index import main_router as client_bot_router
logger = logging.getLogger(__name__)
async def _run_bot(token: str, name: str, router) -> None:
bot = Bot(token)
dp = Dispatcher(storage=MemoryStorage())
dp.update.outer_middleware(UserDataMiddleware())
dp.include_router(router)
logger.info("Preparing Telegram bot for polling", extra={"name": name})
# Ensure webhook is removed so polling works
await bot.delete_webhook(drop_pending_updates=False)
logger.info("Webhook cleared", extra={"name": name})
logger.info("Starting Telegram bot polling", extra={"name": name})
await dp.start_polling(bot, allowed_updates=dp.resolve_used_update_types())
async def start_bots_if_configured() -> list[asyncio.Task]:
settings = get_settings()
tasks: list[asyncio.Task] = []
if bool(getattr(settings, 'TELEGRAM_WEBHOOK_ENABLED', False)):
logger.info("Telegram webhook enabled; skipping polling start")
return tasks
main_token: Optional[str] = getattr(settings, 'TELEGRAM_API_KEY', None)
if main_token:
tasks.append(asyncio.create_task(_run_bot(main_token, 'main', main_bot_router)))
client_token: Optional[str] = getattr(settings, 'CLIENT_TELEGRAM_API_KEY', None)
if client_token:
tasks.append(asyncio.create_task(_run_bot(client_token, 'client', client_bot_router)))
if tasks:
logger.info("Telegram bots polling started", extra={"count": len(tasks)})
else:
logger.info("No Telegram tokens configured; polling not started")
return tasks

View File

@ -1,14 +1,14 @@
{ {
"version": "3.0.0", "version": "3.0.0",
"network_id": "my-network-1755317385", "network_id": "my-network-1755348796",
"created_at": "2025-08-16T04:09:45Z", "created_at": "2025-08-16T12:53:16Z",
"bootstrap_nodes": [ "bootstrap_nodes": [
{ {
"id": "node-3a2c6a21e3401fce", "id": "node-bea1d09bc687311b",
"node_id": "node-3a2c6a21e3401fce", "node_id": "node-bea1d09bc687311b",
"address": "2a02:6b40:2000:16b1::1", "address": "2a02:6b40:2000:16b1::1",
"port": 8000, "port": 8000,
"public_key": "3a2c6a21e3401fceed1fb63c45d068f20e21b48159db3a961a2c43e8701071d4", "public_key": "bea1d09bc687311b17b048789be8d8950b88904aa68a0d9992a83cb3851e8bd6",
"trusted": true, "trusted": true,
"node_type": "bootstrap" "node_type": "bootstrap"
} }