+ api
This commit is contained in:
0
api/__init__.py
Normal file
0
api/__init__.py
Normal file
0
api/endpoints/__init__.py
Normal file
0
api/endpoints/__init__.py
Normal file
29
api/endpoints/assets.py
Normal file
29
api/endpoints/assets.py
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
from fastapi import APIRouter
|
||||||
|
from fastapi.openapi.models import MediaType
|
||||||
|
from starlette.exceptions import HTTPException
|
||||||
|
from starlette.requests import Request
|
||||||
|
from starlette.responses import Response, JSONResponse
|
||||||
|
|
||||||
|
from repos.dao import DAO
|
||||||
|
|
||||||
|
router = APIRouter(prefix="/api/assets", tags=["Assets"])
|
||||||
|
|
||||||
|
@router.get("/{asset_id}")
|
||||||
|
async def get_asset(asset_id: str, request: Request) -> Response:
|
||||||
|
dao = request.app.state.dao
|
||||||
|
asset = await dao.assets.get_asset(asset_id)
|
||||||
|
# 2. Проверка на существование
|
||||||
|
if not asset:
|
||||||
|
raise HTTPException(status_code=404, detail="Asset not found")
|
||||||
|
return Response(content=asset.data, media_type="image/png")
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("")
|
||||||
|
async def get_assets(request: Request) -> JSONResponse:
|
||||||
|
dao: DAO = request.app.state.dao
|
||||||
|
assets = await dao.assets.get_assets()
|
||||||
|
assets_links = []
|
||||||
|
for asset in assets:
|
||||||
|
assets_links.append("/api/assets/{}".format(asset.id))
|
||||||
|
return JSONResponse(content=assets_links)
|
||||||
|
|
||||||
35
api/endpoints/character_router.py
Normal file
35
api/endpoints/character_router.py
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
from typing import List
|
||||||
|
|
||||||
|
from fastapi import APIRouter
|
||||||
|
from starlette.exceptions import HTTPException
|
||||||
|
from starlette.requests import Request
|
||||||
|
|
||||||
|
from models.Asset import Asset
|
||||||
|
from models.Character import Character
|
||||||
|
from repos.dao import DAO
|
||||||
|
|
||||||
|
router = APIRouter(prefix="/api/characters", tags=["Characters"])
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/", response_model=List[Character])
|
||||||
|
async def get_characters(request: Request) -> List[Character]:
|
||||||
|
dao: DAO = request.app.state.dao
|
||||||
|
characters = await dao.chars.get_all_characters()
|
||||||
|
return characters
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/{character_id}/assets", response_model=List[Asset])
|
||||||
|
async def get_character_assets(character_id: str, request: Request) -> List[Asset]:
|
||||||
|
dao: DAO = request.app.state.dao
|
||||||
|
character = await dao.chars.get_character(character_id)
|
||||||
|
if character is None:
|
||||||
|
raise HTTPException(status_code=404, detail="Character not found")
|
||||||
|
return await dao.assets.get_assets_by_char_id(character_id)
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/{character_id}", response_model=Character)
|
||||||
|
async def get_character_by_id(character_id: int, request: Request) -> Character:
|
||||||
|
dao: DAO = request.app.state.dao
|
||||||
|
character = await dao.chars.get_character_by_id(character_id)
|
||||||
|
return character
|
||||||
|
|
||||||
165
main.py
165
main.py
@@ -1,107 +1,194 @@
|
|||||||
import asyncio
|
import asyncio
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
|
from contextlib import asynccontextmanager
|
||||||
|
|
||||||
from aiogram import Bot, Dispatcher, Router, F
|
from aiogram import Bot, Dispatcher, Router, F
|
||||||
from aiogram.client.default import DefaultBotProperties
|
from aiogram.client.default import DefaultBotProperties
|
||||||
from aiogram.enums import ParseMode
|
from aiogram.enums import ParseMode
|
||||||
from aiogram.filters import CommandStart, Command, CommandObject
|
from aiogram.filters import CommandStart, Command
|
||||||
from aiogram.types import Message, BufferedInputFile
|
from aiogram.types import Message
|
||||||
from aiogram.fsm.storage.mongo import MongoStorage
|
from aiogram.fsm.storage.mongo import MongoStorage
|
||||||
from dotenv import load_dotenv
|
from dotenv import load_dotenv
|
||||||
|
from fastapi import FastAPI
|
||||||
from motor.motor_asyncio import AsyncIOMotorClient
|
from motor.motor_asyncio import AsyncIOMotorClient
|
||||||
|
from starlette.middleware.cors import CORSMiddleware
|
||||||
|
|
||||||
# Импорты
|
# --- ИМПОРТЫ ПРОЕКТА ---
|
||||||
from adapters.google_adapter import GoogleAdapter
|
from adapters.google_adapter import GoogleAdapter
|
||||||
from middlewares.album import AlbumMiddleware
|
from middlewares.album import AlbumMiddleware
|
||||||
from middlewares.auth import AuthMiddleware
|
from middlewares.auth import AuthMiddleware
|
||||||
from middlewares.dao import DaoMiddleware
|
from middlewares.dao import DaoMiddleware
|
||||||
|
|
||||||
|
# Репозитории и DAO
|
||||||
from repos.char_repo import CharacterRepo
|
from repos.char_repo import CharacterRepo
|
||||||
from repos.dao import DAO
|
|
||||||
from repos.user_repo import UsersRepo
|
from repos.user_repo import UsersRepo
|
||||||
from routers import char_router
|
from repos.dao import DAO
|
||||||
# ВАЖНО: Импортируем роутер с логикой кнопок, а не создаем пустой
|
# Предполагаю, что AssetsDAO лежит тут или в repos.assets_dao.
|
||||||
|
# Если нет - поправьте импорт!
|
||||||
|
|
||||||
|
|
||||||
|
# Роутеры
|
||||||
from routers.auth_router import router as auth_router
|
from routers.auth_router import router as auth_router
|
||||||
from routers.gen_router import router as gen_router
|
from routers.gen_router import router as gen_router
|
||||||
from routers.char_router import router as char_router
|
from routers.char_router import router as char_router
|
||||||
from routers.assets_router import router as assets_router
|
from routers.assets_router import router as assets_router # Роутер бота для ассетов
|
||||||
|
from api.endpoints.assets import router as api_assets_router # Роутер FastAPI
|
||||||
|
from api.endpoints.character_router import router as api_char_router # Роутер FastAPI
|
||||||
|
|
||||||
load_dotenv()
|
load_dotenv()
|
||||||
|
|
||||||
# Настройки
|
# --- КОНФИГУРАЦИЯ ---
|
||||||
BOT_TOKEN = os.getenv("BOT_TOKEN")
|
BOT_TOKEN = os.getenv("BOT_TOKEN")
|
||||||
GEMINI_API_KEY = os.getenv("GEMINI_API_KEY")
|
GEMINI_API_KEY = os.getenv("GEMINI_API_KEY")
|
||||||
MONGO_HOST = os.getenv("MONGO_HOST")
|
MONGO_HOST = os.getenv("MONGO_HOST") # Например: mongodb://localhost:27017
|
||||||
ADMIN_ID = int(os.getenv("ADMIN_ID")) # Сразу преобразуем в int
|
DB_NAME = os.getenv("DB_NAME", "my_bot_db") # Имя базы данных
|
||||||
|
ADMIN_ID = int(os.getenv("ADMIN_ID", 0))
|
||||||
|
|
||||||
# Инициализация
|
|
||||||
|
def setup_logging():
|
||||||
|
logging.basicConfig(level=logging.INFO,
|
||||||
|
format="%(asctime)s [%(levelname)s] %(name)s: %(message)s")
|
||||||
|
|
||||||
|
|
||||||
|
# --- ИНИЦИАЛИЗАЦИЯ ЗАВИСИМОСТЕЙ ---
|
||||||
bot = Bot(token=BOT_TOKEN, default=DefaultBotProperties(parse_mode=ParseMode.HTML))
|
bot = Bot(token=BOT_TOKEN, default=DefaultBotProperties(parse_mode=ParseMode.HTML))
|
||||||
|
|
||||||
# БД
|
# Клиент БД создаем глобально, чтобы он был доступен и боту (Storage), и API
|
||||||
mongo_client = AsyncIOMotorClient(MONGO_HOST)
|
mongo_client = AsyncIOMotorClient(MONGO_HOST)
|
||||||
|
|
||||||
|
# Репозитории
|
||||||
users_repo = UsersRepo(mongo_client)
|
users_repo = UsersRepo(mongo_client)
|
||||||
char_repo = CharacterRepo(mongo_client)
|
char_repo = CharacterRepo(mongo_client)
|
||||||
|
dao = DAO(mongo_client) # Главный DAO для бота
|
||||||
|
|
||||||
# Dispatcher
|
# Dispatcher
|
||||||
# Если MongoStorage пока не настроен на authSource=admin, можно временно убрать storage=...
|
dp = Dispatcher(storage=MongoStorage(mongo_client, db_name=DB_NAME))
|
||||||
dp = Dispatcher(storage=MongoStorage(mongo_client))
|
|
||||||
|
|
||||||
# ВНЕДРЕНИЕ ЗАВИСИМОСТЕЙ (чтобы они были доступны в хендлерах)
|
# Внедрение зависимостей (глобально для бота)
|
||||||
dp["repo"] = users_repo
|
dp["repo"] = users_repo
|
||||||
dp["admin_id"] = ADMIN_ID
|
dp["admin_id"] = ADMIN_ID
|
||||||
dp["gemini"] = GoogleAdapter(api_key=GEMINI_API_KEY) # Инициализируем тут
|
dp["gemini"] = GoogleAdapter(api_key=GEMINI_API_KEY)
|
||||||
|
|
||||||
# РОУТИНГ
|
# --- НАСТРОЙКА РОУТЕРОВ БОТА ---
|
||||||
|
|
||||||
# 1. Роутер авторизации (кнопки) - ПОДКЛЮЧАЕМ ПЕРВЫМ И БЕЗ МИДЛВАРИ
|
# 1. Роутеры без мидлварей (например, auth)
|
||||||
dp.include_router(auth_router)
|
dp.include_router(auth_router)
|
||||||
|
|
||||||
|
# 2. Основные роутеры
|
||||||
main_router = Router()
|
main_router = Router()
|
||||||
dp.include_router(main_router)
|
dp.include_router(main_router)
|
||||||
dp.include_router(assets_router)
|
dp.include_router(assets_router)
|
||||||
dp.include_router(char_router)
|
dp.include_router(char_router)
|
||||||
dp.include_router(gen_router)
|
dp.include_router(gen_router)
|
||||||
|
|
||||||
# 2. Основной роутер (чат с ботом)
|
# --- НАСТРОЙКА MIDDLEWARES БОТА ---
|
||||||
|
|
||||||
# Вешаем защиту ТОЛЬКО на основной роутер
|
# DaoMiddleware прокидывает объект 'dao' во все хендлеры
|
||||||
|
dp.update.middleware(DaoMiddleware(dao=dao))
|
||||||
|
|
||||||
|
# AuthMiddleware проверяет права доступа
|
||||||
main_router.message.middleware(AuthMiddleware(repo=users_repo, admin_id=ADMIN_ID))
|
main_router.message.middleware(AuthMiddleware(repo=users_repo, admin_id=ADMIN_ID))
|
||||||
gen_router.message.middleware(AuthMiddleware(repo=users_repo, admin_id=ADMIN_ID))
|
gen_router.message.middleware(AuthMiddleware(repo=users_repo, admin_id=ADMIN_ID))
|
||||||
gen_router.message.middleware(AlbumMiddleware(latency=0.8))
|
|
||||||
assets_router.message.middleware(AuthMiddleware(repo=users_repo, admin_id=ADMIN_ID))
|
assets_router.message.middleware(AuthMiddleware(repo=users_repo, admin_id=ADMIN_ID))
|
||||||
dp.update.middleware(DaoMiddleware(dao=DAO(client=mongo_client)))
|
|
||||||
|
# AlbumMiddleware для обработки групп фото
|
||||||
|
gen_router.message.middleware(AlbumMiddleware(latency=0.8))
|
||||||
|
|
||||||
|
|
||||||
|
# --- LIFESPAN (Запуск FastAPI + Bot) ---
|
||||||
|
@asynccontextmanager
|
||||||
|
async def lifespan(app: FastAPI):
|
||||||
|
# --- STARTUP ---
|
||||||
|
print("🚀 Starting up...")
|
||||||
|
|
||||||
def setup_logging() -> None:
|
# 1. Настройка DAO для FastAPI
|
||||||
logging.basicConfig(level=logging.INFO,
|
# Используем уже созданный mongo_client
|
||||||
format="%(asctime)s [%(levelname)s] %(name)s: %(message)s")
|
db = mongo_client[DB_NAME]
|
||||||
|
|
||||||
|
# Инициализируем DAO для ассетов и кладем в state приложения
|
||||||
|
# Теперь в эндпоинтах можно делать request.app.state.assets_dao
|
||||||
|
app.state.dao = dao
|
||||||
|
|
||||||
|
print("✅ DB & DAO initialized")
|
||||||
|
|
||||||
|
# 2. ЗАПУСК БОТА (в фоне)
|
||||||
|
# Важно: handle_signals=False, чтобы бот не перехватывал сигналы остановки у uvicorn
|
||||||
|
# Мы НЕ передаем сюда dao=..., так как он уже подключен через Middleware выше
|
||||||
|
polling_task = asyncio.create_task(
|
||||||
|
dp.start_polling(bot, handle_signals=False)
|
||||||
|
)
|
||||||
|
print("🤖 Bot polling started")
|
||||||
|
|
||||||
|
yield
|
||||||
|
|
||||||
|
# --- SHUTDOWN ---
|
||||||
|
print("🛑 Shutting down...")
|
||||||
|
|
||||||
|
# 3. Остановка бота
|
||||||
|
polling_task.cancel()
|
||||||
|
try:
|
||||||
|
await polling_task
|
||||||
|
except asyncio.CancelledError:
|
||||||
|
print("🤖 Bot polling stopped")
|
||||||
|
|
||||||
|
# 4. Отключение БД
|
||||||
|
# Обычно Motor закрывать не обязательно при выходе, но хорошим тоном считается
|
||||||
|
# mongo_client.close()
|
||||||
|
print("🛑 DB Connection closed")
|
||||||
|
|
||||||
|
|
||||||
# --- ХЕНДЛЕРЫ ОСНОВНОГО РОУТЕРА ---
|
# --- НАСТРОЙКА FASTAPI ---
|
||||||
# Переносим их прямо сюда или в отдельный файл routers/chat_router.py
|
app = FastAPI(title="Assets API", lifespan=lifespan)
|
||||||
|
|
||||||
|
# CORS
|
||||||
|
app.add_middleware(
|
||||||
|
CORSMiddleware,
|
||||||
|
allow_origins=["*"],
|
||||||
|
allow_methods=["*"],
|
||||||
|
allow_headers=["*"],
|
||||||
|
)
|
||||||
|
|
||||||
|
# Подключаем роутер API
|
||||||
|
app.include_router(api_assets_router)
|
||||||
|
app.include_router(api_char_router)
|
||||||
|
|
||||||
|
|
||||||
|
# --- ХЕНДЛЕРЫ БОТА (Main Router) ---
|
||||||
@main_router.message(Command("help"))
|
@main_router.message(Command("help"))
|
||||||
async def show_help(message: Message) -> None:
|
async def show_help(message: Message) -> None:
|
||||||
await message.answer("Для того, чтобы обратиться для текстовой генерации - просто отправь промпт.\n\n"
|
await message.answer("ℹ️ <b>Справка:</b>\n\n"
|
||||||
"Для генерации фото - /image {prompt}\n\n"
|
"📝 <b>Текст:</b> Просто отправь промпт.\n"
|
||||||
"Можно отправить фото и команду /image {prompt}\n\n"
|
"🎨 <b>Фото:</b> /image {промпт} (или прикрепи фото с подписью).\n\n"
|
||||||
"Диалоги не поддерживаются!!!! <b>Каждое новое сообщение - новый диалог</b>")
|
"⚠️ Диалоги не сохраняются (каждое сообщение — новый запрос).")
|
||||||
|
|
||||||
|
|
||||||
@main_router.message(CommandStart())
|
@main_router.message(CommandStart())
|
||||||
async def cmd_start(message: Message):
|
async def cmd_start(message: Message):
|
||||||
await message.answer("👋 Привет! Я готов к работе.\n\n"
|
await message.answer("👋 Привет! Я готов к работе.\n\n"
|
||||||
"Для того, чтобы обратиться для текстовой генерации - просто отправь промпт.\n\n"
|
"Напиши мне, что нужно сгенерировать, или используй /help.")
|
||||||
"Для генерации фото - /image {prompt}\n\n"
|
|
||||||
"Можно отправить фото и команду /image {prompt}\n\n"
|
|
||||||
"Диалоги не поддерживаются!!!! <b>Каждое новое сообщение - новый диалог</b>"
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
# --- ЗАПУСК ---
|
# --- ЗАПУСК ---
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
import uvicorn
|
||||||
|
|
||||||
setup_logging()
|
setup_logging()
|
||||||
|
|
||||||
|
|
||||||
|
async def main():
|
||||||
|
# Создаем конфигурацию uvicorn вручную
|
||||||
|
# loop="asyncio" заставляет использовать стандартный цикл
|
||||||
|
config = uvicorn.Config(app, host="0.0.0.0", port=8000, loop="asyncio")
|
||||||
|
server = uvicorn.Server(config)
|
||||||
|
|
||||||
|
# Запускаем сервер (lifespan запустится внутри)
|
||||||
|
await server.serve()
|
||||||
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
asyncio.run(dp.start_polling(bot))
|
# Сами запускаем цикл, контролируя аргументы
|
||||||
|
asyncio.run(main())
|
||||||
except KeyboardInterrupt:
|
except KeyboardInterrupt:
|
||||||
print("Bot stopped")
|
# Корректно обрабатываем выход
|
||||||
|
pass
|
||||||
@@ -16,7 +16,7 @@ class AssetsRepo:
|
|||||||
return asset
|
return asset
|
||||||
|
|
||||||
async def get_assets(self, limit: int = 10, offset: int = 0) -> List[Asset]:
|
async def get_assets(self, limit: int = 10, offset: int = 0) -> List[Asset]:
|
||||||
res = await self.collection.find({},{"data":0}).sort("created_at", -1).skip(offset).limit(limit).to_list(None)
|
res = await self.collection.find({}, {"data": 0}).sort("created_at", -1).skip(offset).limit(limit).to_list(None)
|
||||||
assets = []
|
assets = []
|
||||||
for doc in res:
|
for doc in res:
|
||||||
# Конвертируем ObjectId в строку и кладем в поле id
|
# Конвертируем ObjectId в строку и кладем в поле id
|
||||||
@@ -28,12 +28,26 @@ class AssetsRepo:
|
|||||||
return assets
|
return assets
|
||||||
|
|
||||||
async def get_asset(self, asset_id: str, with_data: bool = True) -> Asset:
|
async def get_asset(self, asset_id: str, with_data: bool = True) -> Asset:
|
||||||
res = await self.collection.find_one({"_id": ObjectId(asset_id)}, {"data": 0 if not with_data else 1})
|
res = await self.collection.find_one({"_id": ObjectId(asset_id)},
|
||||||
|
{"_id": 1, "name": 1, "type": 1, "tg_doc_file_id": 1,
|
||||||
|
"data": 0 if not with_data else 1})
|
||||||
res["id"] = str(res.pop("_id"))
|
res["id"] = str(res.pop("_id"))
|
||||||
return Asset(**res)
|
return Asset(**res)
|
||||||
|
|
||||||
|
|
||||||
async def update_asset(self, asset_id: str, asset: Asset):
|
async def update_asset(self, asset_id: str, asset: Asset):
|
||||||
if not asset.id:
|
if not asset.id:
|
||||||
raise Exception(f"Asset ID not found: {asset_id}")
|
raise Exception(f"Asset ID not found: {asset_id}")
|
||||||
await self.collection.update_one({"_id": ObjectId(asset_id)}, {"$set": asset.model_dump()})
|
await self.collection.update_one({"_id": ObjectId(asset_id)}, {"$set": asset.model_dump()})
|
||||||
|
|
||||||
|
async def set_tg_photo_file_id(self, asset_id: str, tg_photo_file_id: str):
|
||||||
|
await self.collection.update_one({"_id": ObjectId(asset_id)}, {"$set": {"tg_photo_file_id": tg_photo_file_id}})
|
||||||
|
|
||||||
|
async def get_assets_by_char_id(self, character_id: str, limit: int = 10, offset: int = 0) -> List[Asset]:
|
||||||
|
docs = await self.collection.find({"linked_char_id": character_id},
|
||||||
|
{"data": 0}, sort=[("created_at", -1)]).limit(limit).skip(offset).to_list(
|
||||||
|
None)
|
||||||
|
assets = []
|
||||||
|
for doc in docs:
|
||||||
|
doc["id"] = str(doc.pop("_id"))
|
||||||
|
assets.append(Asset(**doc))
|
||||||
|
return assets
|
||||||
|
|||||||
@@ -3,15 +3,18 @@ aiogram==3.24.0
|
|||||||
aiohappyeyeballs==2.6.1
|
aiohappyeyeballs==2.6.1
|
||||||
aiohttp==3.11.18
|
aiohttp==3.11.18
|
||||||
aiosignal==1.4.0
|
aiosignal==1.4.0
|
||||||
|
annotated-doc==0.0.4
|
||||||
annotated-types==0.7.0
|
annotated-types==0.7.0
|
||||||
anyio==4.12.1
|
anyio==4.12.1
|
||||||
attrs==25.4.0
|
attrs==25.4.0
|
||||||
certifi==2026.1.4
|
certifi==2026.1.4
|
||||||
cffi==2.0.0
|
cffi==2.0.0
|
||||||
charset-normalizer==3.4.4
|
charset-normalizer==3.4.4
|
||||||
|
click==8.3.1
|
||||||
cryptography==46.0.4
|
cryptography==46.0.4
|
||||||
distro==1.9.0
|
distro==1.9.0
|
||||||
dnspython==2.8.0
|
dnspython==2.8.0
|
||||||
|
fastapi==0.128.0
|
||||||
frozenlist==1.8.0
|
frozenlist==1.8.0
|
||||||
google-auth==2.48.0
|
google-auth==2.48.0
|
||||||
google-genai==1.61.0
|
google-genai==1.61.0
|
||||||
@@ -34,8 +37,10 @@ python-dotenv==1.2.1
|
|||||||
requests==2.32.5
|
requests==2.32.5
|
||||||
rsa==4.9.1
|
rsa==4.9.1
|
||||||
sniffio==1.3.1
|
sniffio==1.3.1
|
||||||
|
starlette==0.50.0
|
||||||
tenacity==9.1.2
|
tenacity==9.1.2
|
||||||
typing_extensions==4.15.0
|
typing_extensions==4.15.0
|
||||||
urllib3==2.6.3
|
urllib3==2.6.3
|
||||||
|
uvicorn==0.40.0
|
||||||
websockets==15.0.1
|
websockets==15.0.1
|
||||||
yarl==1.22.0
|
yarl==1.22.0
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ async def assets_command(msg: Message, dao: DAO):
|
|||||||
media_group.append(InputMediaPhoto(media=asset.tg_photo_file_id))
|
media_group.append(InputMediaPhoto(media=asset.tg_photo_file_id))
|
||||||
elif asset.tg_doc_file_id:
|
elif asset.tg_doc_file_id:
|
||||||
asset_full_info = await dao.assets.get_asset(asset.id)
|
asset_full_info = await dao.assets.get_asset(asset.id)
|
||||||
|
asset = asset_full_info
|
||||||
media_group.append(InputMediaPhoto(media=BufferedInputFile(asset_full_info.data, asset_full_info.name)))
|
media_group.append(InputMediaPhoto(media=BufferedInputFile(asset_full_info.data, asset_full_info.name)))
|
||||||
else:
|
else:
|
||||||
continue
|
continue
|
||||||
@@ -28,8 +29,7 @@ async def assets_command(msg: Message, dao: DAO):
|
|||||||
reply_markup=InlineKeyboardMarkup(inline_keyboard=[keyboard]))
|
reply_markup=InlineKeyboardMarkup(inline_keyboard=[keyboard]))
|
||||||
for media_index, media in enumerate(mg):
|
for media_index, media in enumerate(mg):
|
||||||
if assets[media_index].tg_photo_file_id is None:
|
if assets[media_index].tg_photo_file_id is None:
|
||||||
assets[media_index].tg_photo_file_id = media.photo[-1].file_id
|
await dao.assets.set_tg_photo_file_id(assets[media_index].id, media.photo[-1].file_id)
|
||||||
await dao.assets.update_asset(assets[media_index].id, assets[media_index])
|
|
||||||
|
|
||||||
|
|
||||||
@router.callback_query(F.data.startswith("asset_doc_"))
|
@router.callback_query(F.data.startswith("asset_doc_"))
|
||||||
|
|||||||
Reference in New Issue
Block a user