init
This commit is contained in:
4
.env
Normal file
4
.env
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
BOT_TOKEN=8495170789:AAHyjjhHwwVtd9_ROnjHqPHRdnmyVr1aeaY
|
||||||
|
GEMINI_API_KEY=AIzaSyAHzDYhgjOqZZnvOnOFRGaSkKu4OAN3kZE
|
||||||
|
MONGO_HOST=mongodb://admin:super_secure_password@31.59.58.220:27017/
|
||||||
|
ADMIN_ID=567047
|
||||||
14
Dockerfile
Normal file
14
Dockerfile
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
FROM python:3.13-slim
|
||||||
|
|
||||||
|
# Рабочая директория внутри контейнера
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Копируем зависимости (если есть requirements.txt)
|
||||||
|
COPY requirements.txt ./
|
||||||
|
RUN pip install --no-cache-dir -r requirements.txt
|
||||||
|
|
||||||
|
# Копируем код
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
# Запуск приложения (замени app.py на свой файл)
|
||||||
|
CMD ["python", "main.py"]
|
||||||
0
adapters/__init__.py
Normal file
0
adapters/__init__.py
Normal file
99
adapters/google_adapter.py
Normal file
99
adapters/google_adapter.py
Normal file
@@ -0,0 +1,99 @@
|
|||||||
|
import os
|
||||||
|
import io
|
||||||
|
import logging
|
||||||
|
from datetime import datetime
|
||||||
|
from typing import List, Union, Dict, Any
|
||||||
|
from PIL import Image
|
||||||
|
|
||||||
|
# Импортируем из нового SDK
|
||||||
|
from google import genai
|
||||||
|
from google.genai import types
|
||||||
|
|
||||||
|
# Для настройки логгера
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class GoogleAdapter:
|
||||||
|
def __init__(self, api_key: str):
|
||||||
|
if not api_key:
|
||||||
|
raise ValueError("API Key for Gemini is missing")
|
||||||
|
self.client = genai.Client(api_key=api_key)
|
||||||
|
# Укажите актуальную модель.
|
||||||
|
# Если gemini-3-pro-image-preview недоступна, используйте gemini-2.0-flash-exp
|
||||||
|
self.model_name = "gemini-3-pro-preview"
|
||||||
|
|
||||||
|
def generate(
|
||||||
|
self,
|
||||||
|
prompt: str,
|
||||||
|
image_bytes: bytes = None,
|
||||||
|
generate_image: bool = False
|
||||||
|
) -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Универсальный метод:
|
||||||
|
- Если generate_image=True: просим модель вернуть картинку (Image Generation).
|
||||||
|
- Если image_bytes переданы + generate_image=False: это Vision (описание фото).
|
||||||
|
- Если image_bytes + generate_image=True: это Image-to-Image (редактирование).
|
||||||
|
"""
|
||||||
|
if generate_image:
|
||||||
|
self.model_name = "gemini-3-pro-image-preview"
|
||||||
|
else :
|
||||||
|
self.model_name = "gemini-3-pro-preview"
|
||||||
|
contents = [prompt]
|
||||||
|
|
||||||
|
# Если есть входное изображение (для Vision или для редактирования)
|
||||||
|
if image_bytes:
|
||||||
|
try:
|
||||||
|
image = Image.open(io.BytesIO(image_bytes))
|
||||||
|
contents.append(image)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error processing input image: {e}")
|
||||||
|
return {"error": "Не удалось обработать входящее изображение."}
|
||||||
|
|
||||||
|
# Настраиваем конфигурацию
|
||||||
|
# Для генерации картинок добавляем 'IMAGE' в response_modalities
|
||||||
|
modalities = ['TEXT']
|
||||||
|
if generate_image:
|
||||||
|
modalities.append('IMAGE')
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Вызов API (синхронный метод в обертке, но aiogram вызывает его в треде,
|
||||||
|
# либо используйте client.aio для асинхронности если поддерживается версией SDK)
|
||||||
|
# В google-genai v0.3+ есть асинхронный клиент, но для простоты здесь стандартный вызов.
|
||||||
|
# Чтобы не блокировать event loop, в main.py мы обернем это в to_thread при необходимости,
|
||||||
|
# но пока используем стандартный вызов.
|
||||||
|
|
||||||
|
response = self.client.models.generate_content(
|
||||||
|
model=self.model_name,
|
||||||
|
contents=contents,
|
||||||
|
config=types.GenerateContentConfig(
|
||||||
|
response_modalities=modalities,
|
||||||
|
temperature=0.7 if not generate_image else 1.0,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
result = {"text": "", "images": []}
|
||||||
|
|
||||||
|
# Парсим ответ (Text или Inline Data)
|
||||||
|
if response.parts:
|
||||||
|
for part in response.parts:
|
||||||
|
if part.text:
|
||||||
|
result["text"] += part.text
|
||||||
|
|
||||||
|
# Проверяем наличие сгенерированного изображения
|
||||||
|
if part.inline_data:
|
||||||
|
# ИСПРАВЛЕНИЕ: Берем "сырые" байты напрямую из ответа
|
||||||
|
# Это работает быстрее и не вызывает ошибку с PIL
|
||||||
|
|
||||||
|
# part.inline_data.data — это уже bytes
|
||||||
|
byte_arr = io.BytesIO(part.inline_data.data)
|
||||||
|
now = datetime.now()
|
||||||
|
# Имя файла для телеграма (формально)
|
||||||
|
byte_arr.name = f'{now.timestamp()}.png'
|
||||||
|
|
||||||
|
result["images"].append(byte_arr)
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Gemini API Error: {e}")
|
||||||
|
return {"error": f"Ошибка API: {str(e)}"}
|
||||||
9
docker-compose.yml
Normal file
9
docker-compose.yml
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
services:
|
||||||
|
tot-bot:
|
||||||
|
image: tot-bot:latest
|
||||||
|
container_name: tot-bot
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
network: host
|
||||||
|
network_mode: host
|
||||||
|
restart: unless-stopped
|
||||||
15
keyboards.py
Normal file
15
keyboards.py
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
from aiogram.types import InlineKeyboardMarkup, InlineKeyboardButton
|
||||||
|
|
||||||
|
def get_request_kb():
|
||||||
|
return InlineKeyboardMarkup(inline_keyboard=[
|
||||||
|
[InlineKeyboardButton(text="🔐 Запросить доступ", callback_data="req_access")]
|
||||||
|
])
|
||||||
|
|
||||||
|
def get_admin_decision_kb(user_id: int):
|
||||||
|
"""Кнопки для админа с ID пользователя в callback_data"""
|
||||||
|
return InlineKeyboardMarkup(inline_keyboard=[
|
||||||
|
[
|
||||||
|
InlineKeyboardButton(text="✅ Разрешить", callback_data=f"access_allow_{user_id}"),
|
||||||
|
InlineKeyboardButton(text="🚫 Запретить", callback_data=f"access_deny_{user_id}")
|
||||||
|
]
|
||||||
|
])
|
||||||
102
main.py
Normal file
102
main.py
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
import asyncio
|
||||||
|
import logging
|
||||||
|
import os
|
||||||
|
|
||||||
|
from aiogram import Bot, Dispatcher, Router, F
|
||||||
|
from aiogram.client.default import DefaultBotProperties
|
||||||
|
from aiogram.enums import ParseMode
|
||||||
|
from aiogram.filters import CommandStart, Command, CommandObject
|
||||||
|
from aiogram.types import Message, BufferedInputFile
|
||||||
|
from aiogram.fsm.storage.mongo import MongoStorage
|
||||||
|
from dotenv import load_dotenv
|
||||||
|
from motor.motor_asyncio import AsyncIOMotorClient
|
||||||
|
|
||||||
|
# Импорты
|
||||||
|
from adapters.google_adapter import GoogleAdapter
|
||||||
|
from middlewares.auth import AuthMiddleware
|
||||||
|
from middlewares.dao import DaoMiddleware
|
||||||
|
from repos.char_repo import CharacterRepo
|
||||||
|
from repos.dao import DAO
|
||||||
|
from repos.user_repo import UsersRepo
|
||||||
|
from routers import char_router
|
||||||
|
# ВАЖНО: Импортируем роутер с логикой кнопок, а не создаем пустой
|
||||||
|
from routers.auth_router import router as auth_router
|
||||||
|
from routers.gen_router import router as gen_router
|
||||||
|
from routers.char_router import router as char_router
|
||||||
|
|
||||||
|
|
||||||
|
load_dotenv()
|
||||||
|
|
||||||
|
# Настройки
|
||||||
|
BOT_TOKEN = os.getenv("BOT_TOKEN")
|
||||||
|
GEMINI_API_KEY = os.getenv("GEMINI_API_KEY")
|
||||||
|
MONGO_HOST = os.getenv("MONGO_HOST")
|
||||||
|
ADMIN_ID = int(os.getenv("ADMIN_ID")) # Сразу преобразуем в int
|
||||||
|
|
||||||
|
# Инициализация
|
||||||
|
bot = Bot(token=BOT_TOKEN, default=DefaultBotProperties(parse_mode=ParseMode.HTML))
|
||||||
|
|
||||||
|
# БД
|
||||||
|
mongo_client = AsyncIOMotorClient(MONGO_HOST)
|
||||||
|
users_repo = UsersRepo(mongo_client)
|
||||||
|
char_repo = CharacterRepo(mongo_client)
|
||||||
|
|
||||||
|
# Dispatcher
|
||||||
|
# Если MongoStorage пока не настроен на authSource=admin, можно временно убрать storage=...
|
||||||
|
dp = Dispatcher(storage=MongoStorage(mongo_client))
|
||||||
|
|
||||||
|
# ВНЕДРЕНИЕ ЗАВИСИМОСТЕЙ (чтобы они были доступны в хендлерах)
|
||||||
|
dp["repo"] = users_repo
|
||||||
|
dp["admin_id"] = ADMIN_ID
|
||||||
|
dp["gemini"] = GoogleAdapter(api_key=GEMINI_API_KEY) # Инициализируем тут
|
||||||
|
|
||||||
|
# РОУТИНГ
|
||||||
|
|
||||||
|
# 1. Роутер авторизации (кнопки) - ПОДКЛЮЧАЕМ ПЕРВЫМ И БЕЗ МИДЛВАРИ
|
||||||
|
dp.include_router(auth_router)
|
||||||
|
main_router = Router()
|
||||||
|
dp.include_router(main_router)
|
||||||
|
dp.include_router(char_router)
|
||||||
|
dp.include_router(gen_router)
|
||||||
|
|
||||||
|
# 2. Основной роутер (чат с ботом)
|
||||||
|
|
||||||
|
# Вешаем защиту ТОЛЬКО на основной роутер
|
||||||
|
main_router.message.middleware(AuthMiddleware(repo=users_repo, admin_id=ADMIN_ID))
|
||||||
|
gen_router.message.middleware(AuthMiddleware(repo=users_repo, admin_id=ADMIN_ID))
|
||||||
|
char_router.message.middleware(DaoMiddleware(dao=DAO(client=mongo_client)))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def setup_logging() -> None:
|
||||||
|
logging.basicConfig(level=logging.INFO,
|
||||||
|
format="%(asctime)s [%(levelname)s] %(name)s: %(message)s")
|
||||||
|
|
||||||
|
|
||||||
|
# --- ХЕНДЛЕРЫ ОСНОВНОГО РОУТЕРА ---
|
||||||
|
# Переносим их прямо сюда или в отдельный файл routers/chat_router.py
|
||||||
|
@main_router.message(Command("help"))
|
||||||
|
async def show_help(message: Message) -> None:
|
||||||
|
await message.answer("Для того, чтобы обратиться для текстовой генерации - просто отправь промпт.\n\n"
|
||||||
|
"Для генерации фото - /image {prompt}\n\n"
|
||||||
|
"Можно отправить фото и команду /image {prompt}\n\n"
|
||||||
|
"Диалоги не поддерживаются!!!! <b>Каждое новое сообщение - новый диалог</b>")
|
||||||
|
|
||||||
|
|
||||||
|
@main_router.message(CommandStart())
|
||||||
|
async def cmd_start(message: Message):
|
||||||
|
await message.answer("👋 Привет! Я готов к работе.\n\n"
|
||||||
|
"Для того, чтобы обратиться для текстовой генерации - просто отправь промпт.\n\n"
|
||||||
|
"Для генерации фото - /image {prompt}\n\n"
|
||||||
|
"Можно отправить фото и команду /image {prompt}\n\n"
|
||||||
|
"Диалоги не поддерживаются!!!! <b>Каждое новое сообщение - новый диалог</b>"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# --- ЗАПУСК ---
|
||||||
|
if __name__ == "__main__":
|
||||||
|
setup_logging()
|
||||||
|
try:
|
||||||
|
asyncio.run(dp.start_polling(bot))
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
print("Bot stopped")
|
||||||
0
middlewares/__init__.py
Normal file
0
middlewares/__init__.py
Normal file
50
middlewares/auth.py
Normal file
50
middlewares/auth.py
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
from typing import Callable, Dict, Any, Awaitable
|
||||||
|
from aiogram import BaseMiddleware
|
||||||
|
from aiogram.types import Message
|
||||||
|
from repos.user_repo import UsersRepo, UserStatus
|
||||||
|
from keyboards import get_request_kb
|
||||||
|
|
||||||
|
|
||||||
|
class AuthMiddleware(BaseMiddleware):
|
||||||
|
def __init__(self, repo: UsersRepo, admin_id: int):
|
||||||
|
self.repo = repo
|
||||||
|
self.admin_id = admin_id
|
||||||
|
|
||||||
|
async def __call__(
|
||||||
|
self,
|
||||||
|
handler: Callable[[Message, Dict[str, Any]], Awaitable[Any]],
|
||||||
|
event: Message,
|
||||||
|
data: Dict[str, Any]
|
||||||
|
) -> Any:
|
||||||
|
user = event.from_user
|
||||||
|
|
||||||
|
# Админа пускаем всегда
|
||||||
|
# if user.id == self.admin_id:
|
||||||
|
# return await handler(event, data)
|
||||||
|
|
||||||
|
# Получаем данные из БД
|
||||||
|
db_user = await self.repo.get_user(user.id)
|
||||||
|
status = db_user.get("status") if db_user else UserStatus.NONE
|
||||||
|
|
||||||
|
# 1. Если доступ уже разрешен — пропускаем к боту
|
||||||
|
if status == UserStatus.ALLOWED:
|
||||||
|
return await handler(event, data)
|
||||||
|
|
||||||
|
# 2. Если статус PENDING (ждет решения)
|
||||||
|
if status == UserStatus.PENDING:
|
||||||
|
await event.answer("⏳ Ваша заявка находится на рассмотрении администратора.")
|
||||||
|
return
|
||||||
|
|
||||||
|
# 3. Если нет в базе или ЗАПРЕЩЕН — проверяем тайминг 24 часа
|
||||||
|
can_request = await self.repo.can_request_access(user.id)
|
||||||
|
|
||||||
|
if can_request:
|
||||||
|
await event.answer(
|
||||||
|
"⛔️ У вас нет доступа к этому боту.\nВы можете отправить запрос администратору.",
|
||||||
|
reply_markup=get_request_kb()
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
# Если 24 часа еще не прошло
|
||||||
|
await event.answer("⛔️ Доступ запрещен.\nПовторный запрос можно отправить через 24 часа.")
|
||||||
|
|
||||||
|
return # Прерываем обработку, хендлеры бота не сработают
|
||||||
13
middlewares/dao.py
Normal file
13
middlewares/dao.py
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
from aiogram import BaseMiddleware
|
||||||
|
from aiogram.types import TelegramObject
|
||||||
|
|
||||||
|
from repos.dao import DAO
|
||||||
|
|
||||||
|
|
||||||
|
class DaoMiddleware(BaseMiddleware):
|
||||||
|
def __init__(self, dao: DAO):
|
||||||
|
self._dao = dao
|
||||||
|
|
||||||
|
async def __call__(self, handler, event: TelegramObject, data: dict):
|
||||||
|
data["dao"] = self._dao
|
||||||
|
return await handler(event, data)
|
||||||
8
models/Character.py
Normal file
8
models/Character.py
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
from pydantic import BaseModel
|
||||||
|
|
||||||
|
|
||||||
|
class Character(BaseModel):
|
||||||
|
id: int | None
|
||||||
|
name: str
|
||||||
|
character_image: bytes
|
||||||
|
character_bio: str
|
||||||
0
models/__init__.py
Normal file
0
models/__init__.py
Normal file
0
repos/__init__.py
Normal file
0
repos/__init__.py
Normal file
16
repos/char_repo.py
Normal file
16
repos/char_repo.py
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
from motor.motor_asyncio import AsyncIOMotorClient
|
||||||
|
|
||||||
|
from models.Character import Character
|
||||||
|
|
||||||
|
|
||||||
|
class CharacterRepo:
|
||||||
|
def __init__(self, client: AsyncIOMotorClient, db_name="bot_db"):
|
||||||
|
self.collection = client[db_name]["characters"]
|
||||||
|
|
||||||
|
async def add_character(self, character: Character) -> Character:
|
||||||
|
op = await self.collection.insert_one(character.model_dump())
|
||||||
|
character.id = op.inserted_id
|
||||||
|
return character
|
||||||
|
|
||||||
|
async def get_character(self, character_id: int) -> Character:
|
||||||
|
return await self.collection.find_one({"id": character_id})
|
||||||
9
repos/dao.py
Normal file
9
repos/dao.py
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
from motor.motor_asyncio import AsyncIOMotorClient
|
||||||
|
|
||||||
|
from repos.char_repo import CharacterRepo
|
||||||
|
from repos.user_repo import UsersRepo
|
||||||
|
|
||||||
|
|
||||||
|
class DAO:
|
||||||
|
def __init__(self, client: AsyncIOMotorClient, db_name="bot_db"):
|
||||||
|
self.chars = CharacterRepo(client, db_name)
|
||||||
65
repos/user_repo.py
Normal file
65
repos/user_repo.py
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
from datetime import datetime, timedelta
|
||||||
|
from enum import Enum
|
||||||
|
|
||||||
|
from aiogram.types import User
|
||||||
|
from motor.motor_asyncio import AsyncIOMotorClient
|
||||||
|
|
||||||
|
|
||||||
|
class UserStatus:
|
||||||
|
ALLOWED = "allowed"
|
||||||
|
DENIED = "denied"
|
||||||
|
PENDING = "pending"
|
||||||
|
NONE = "none" # Пользователя нет в базе
|
||||||
|
|
||||||
|
|
||||||
|
class UsersRepo:
|
||||||
|
def __init__(self, client: AsyncIOMotorClient, db_name="bot_db"):
|
||||||
|
self.collection = client[db_name]["users"]
|
||||||
|
|
||||||
|
async def get_user(self, user_id: int):
|
||||||
|
return await self.collection.find_one({"user_id": user_id})
|
||||||
|
|
||||||
|
async def create_or_update_request(self, user: User):
|
||||||
|
"""
|
||||||
|
Обновляет дату последнего запроса и ставит статус PENDING.
|
||||||
|
Сохраняет всю инфу о юзере.
|
||||||
|
"""
|
||||||
|
now = datetime.now()
|
||||||
|
data = {
|
||||||
|
"user_id": user.id,
|
||||||
|
"username": user.username,
|
||||||
|
"full_name": user.full_name,
|
||||||
|
"status": UserStatus.PENDING,
|
||||||
|
"last_request_date": now
|
||||||
|
}
|
||||||
|
await self.collection.update_one(
|
||||||
|
{"user_id": user.id},
|
||||||
|
{"$set": data},
|
||||||
|
upsert=True
|
||||||
|
)
|
||||||
|
|
||||||
|
async def set_status(self, user_id: int, status: str):
|
||||||
|
"""Меняет статус (разрешен/запрещен)"""
|
||||||
|
await self.collection.update_one(
|
||||||
|
{"user_id": user_id},
|
||||||
|
{"$set": {"status": status}}
|
||||||
|
)
|
||||||
|
|
||||||
|
async def can_request_access(self, user_id: int) -> bool:
|
||||||
|
"""
|
||||||
|
Проверяет, можно ли отправить запрос (прошло ли 24 часа).
|
||||||
|
Возвращает True, если пользователя нет или прошло > 24ч.
|
||||||
|
"""
|
||||||
|
user = await self.get_user(user_id)
|
||||||
|
if not user:
|
||||||
|
return True
|
||||||
|
|
||||||
|
last_date = user.get("last_request_date")
|
||||||
|
if not last_date:
|
||||||
|
return True
|
||||||
|
|
||||||
|
# Проверка на 24 часа
|
||||||
|
if datetime.now() - last_date > timedelta(hours=24):
|
||||||
|
return True
|
||||||
|
|
||||||
|
return False
|
||||||
41
requirements.txt
Normal file
41
requirements.txt
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
aiofiles==24.1.0
|
||||||
|
aiogram==3.24.0
|
||||||
|
aiohappyeyeballs==2.6.1
|
||||||
|
aiohttp==3.11.18
|
||||||
|
aiosignal==1.4.0
|
||||||
|
annotated-types==0.7.0
|
||||||
|
anyio==4.12.1
|
||||||
|
attrs==25.4.0
|
||||||
|
certifi==2026.1.4
|
||||||
|
cffi==2.0.0
|
||||||
|
charset-normalizer==3.4.4
|
||||||
|
cryptography==46.0.4
|
||||||
|
distro==1.9.0
|
||||||
|
dnspython==2.8.0
|
||||||
|
frozenlist==1.8.0
|
||||||
|
google-auth==2.48.0
|
||||||
|
google-genai==1.61.0
|
||||||
|
h11==0.16.0
|
||||||
|
httpcore==1.0.9
|
||||||
|
httpx==0.28.1
|
||||||
|
idna==3.11
|
||||||
|
magic-filter==1.0.12
|
||||||
|
motor==3.7.1
|
||||||
|
multidict==6.7.1
|
||||||
|
pillow==12.1.0
|
||||||
|
propcache==0.4.1
|
||||||
|
pyasn1==0.6.2
|
||||||
|
pyasn1_modules==0.4.2
|
||||||
|
pycparser==3.0
|
||||||
|
pydantic==2.10.6
|
||||||
|
pydantic_core==2.27.2
|
||||||
|
pymongo==4.16.0
|
||||||
|
python-dotenv==1.2.1
|
||||||
|
requests==2.32.5
|
||||||
|
rsa==4.9.1
|
||||||
|
sniffio==1.3.1
|
||||||
|
tenacity==9.1.2
|
||||||
|
typing_extensions==4.15.0
|
||||||
|
urllib3==2.6.3
|
||||||
|
websockets==15.0.1
|
||||||
|
yarl==1.22.0
|
||||||
0
routers/__init__.py
Normal file
0
routers/__init__.py
Normal file
83
routers/auth_router.py
Normal file
83
routers/auth_router.py
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
from aiogram import Router, F, Bot
|
||||||
|
from aiogram.types import CallbackQuery, Message
|
||||||
|
from repos.user_repo import UsersRepo, UserStatus
|
||||||
|
from keyboards import get_admin_decision_kb
|
||||||
|
|
||||||
|
router = Router()
|
||||||
|
|
||||||
|
|
||||||
|
# Чтобы IDE не ругалась, переменные инициализируются в main,
|
||||||
|
# но здесь мы ожидаем, что они будут переданы или доступны через DI (workflow_data)
|
||||||
|
# В этом примере я буду доставать их из data, переданной диспетчером, или глобально (для простоты примера - глобально в рамках архитектуры aiogram лучше через middleware DI).
|
||||||
|
# Для простоты доступа предположим, что repo и admin_id прокинуты.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@router.callback_query(F.data == "req_access")
|
||||||
|
async def user_request_access(callback: CallbackQuery, repo: UsersRepo, bot: Bot, admin_id: int):
|
||||||
|
"""Пользователь нажал 'Запросить доступ'"""
|
||||||
|
user = callback.from_user
|
||||||
|
|
||||||
|
# Двойная проверка на 24 часа (на случай если пользователь не обновлял сообщение)
|
||||||
|
if not await repo.can_request_access(user.id):
|
||||||
|
await callback.answer("⏳ Вы уже отправляли запрос недавно. Повторите через сутки.", show_alert=True)
|
||||||
|
return
|
||||||
|
|
||||||
|
# 1. Записываем в БД статус PENDING
|
||||||
|
await repo.create_or_update_request(user)
|
||||||
|
|
||||||
|
# 2. Уведомляем пользователя
|
||||||
|
await callback.message.edit_text("✅ Заявка отправлена администратору. Ожидайте решения.")
|
||||||
|
|
||||||
|
# 3. Отправляем уведомление Админу
|
||||||
|
# Формируем красивый текст
|
||||||
|
info = (
|
||||||
|
f"🔔 <b>Новый запрос доступа!</b>\n"
|
||||||
|
f"👤 Имя: {user.full_name}\n"
|
||||||
|
f"🆔 ID: <code>{user.id}</code>\n"
|
||||||
|
f"🔗 Username: @{user.username if user.username else 'нет'}"
|
||||||
|
)
|
||||||
|
|
||||||
|
await bot.send_message(
|
||||||
|
chat_id=admin_id,
|
||||||
|
text=info,
|
||||||
|
reply_markup=get_admin_decision_kb(user.id)
|
||||||
|
)
|
||||||
|
await callback.answer()
|
||||||
|
|
||||||
|
|
||||||
|
@router.callback_query(F.data.startswith("access_"))
|
||||||
|
async def admin_decision(callback: CallbackQuery, repo: UsersRepo, bot: Bot):
|
||||||
|
"""Админ нажал Разрешить или Запретить"""
|
||||||
|
action, user_id_str = callback.data.split("_")[1], callback.data.split("_")[2]
|
||||||
|
target_user_id = int(user_id_str)
|
||||||
|
|
||||||
|
if action == "allow":
|
||||||
|
# Обновляем БД
|
||||||
|
await repo.set_status(target_user_id, UserStatus.ALLOWED)
|
||||||
|
|
||||||
|
# Ответ админу
|
||||||
|
await callback.message.edit_text(f"✅ Доступ для {target_user_id} <b>РАЗРЕШЕН</b>.")
|
||||||
|
|
||||||
|
# Уведомление пользователю
|
||||||
|
try:
|
||||||
|
await bot.send_message(target_user_id, "🎉 <b>Вам предоставлен доступ к боту!</b>\nНажмите /start")
|
||||||
|
except:
|
||||||
|
await callback.message.answer(
|
||||||
|
f"⚠️ Не удалось уведомить пользователя {target_user_id} (он заблокировал бота?)")
|
||||||
|
|
||||||
|
elif action == "deny":
|
||||||
|
# Обновляем БД (статус DENIED, дата запроса остается старой - то есть через сутки он сможет снова попросить)
|
||||||
|
# Если хотите, чтобы при отказе таймер 24ч сбрасывался на "сейчас", нужно обновить last_request_date.
|
||||||
|
# В текущей реализации repo, мы просто меняем статус. Таймер тикает от момента подачи заявки пользователем.
|
||||||
|
|
||||||
|
await repo.set_status(target_user_id, UserStatus.DENIED)
|
||||||
|
|
||||||
|
await callback.message.edit_text(f"🚫 Доступ для {target_user_id} <b>ЗАПРЕЩЕН</b>.")
|
||||||
|
|
||||||
|
try:
|
||||||
|
await bot.send_message(target_user_id, "🚫 Администратор отклонил ваш запрос доступа.")
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
await callback.answer()
|
||||||
48
routers/char_router.py
Normal file
48
routers/char_router.py
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
from aiogram.filters import Command
|
||||||
|
from aiogram.fsm.context import FSMContext
|
||||||
|
from aiogram.fsm.state import State, StatesGroup
|
||||||
|
from aiogram.types import *
|
||||||
|
from aiogram import Router, F
|
||||||
|
|
||||||
|
from models.Character import Character
|
||||||
|
from repos.dao import DAO
|
||||||
|
|
||||||
|
router = Router()
|
||||||
|
|
||||||
|
|
||||||
|
class States(StatesGroup):
|
||||||
|
char_wait_name = State()
|
||||||
|
char_wait_bio = State()
|
||||||
|
|
||||||
|
|
||||||
|
@router.message(F.document, Command("add_char"))
|
||||||
|
async def add_char(message: Message, state: FSMContext, dao: DAO):
|
||||||
|
await state.set_data({"photo": bot.download(file=message.document.file_id)})
|
||||||
|
await state.set_state(States.char_wait_name)
|
||||||
|
await message.answer("Кайф, теперь напиши ее имя")
|
||||||
|
|
||||||
|
|
||||||
|
@router.callback_query(States.char_wait_name)
|
||||||
|
async def new_char_name(message: Message, state: FSMContext, dao: DAO):
|
||||||
|
await state.set_data({"name": message.text})
|
||||||
|
await state.set_state(States.char_wait_bio)
|
||||||
|
await message.answer("А теперь напиши био. Хоть чуть чуть.")
|
||||||
|
|
||||||
|
|
||||||
|
@router.callback_query(States.char_wait_bio)
|
||||||
|
async def new_char_bio(message: Message, state: FSMContext, dao: DAO):
|
||||||
|
data = await state.get_data()
|
||||||
|
photo = data["photo"]
|
||||||
|
name = data["name"]
|
||||||
|
char = Character(id=None, name=name, character_image=photo, character_bio=message.text)
|
||||||
|
await dao.chars.add_character(char)
|
||||||
|
await message.answer_photo(photo=BufferedInputFile(char.character_image, "img.png"), caption="Персонаж создан!\n"
|
||||||
|
f"Имя:{char.name}\n"
|
||||||
|
f"Био: {char.character_bio}\n"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@router.message(Command("add_char"))
|
||||||
|
async def add_char_cmd(message: Message):
|
||||||
|
await message.answer(
|
||||||
|
"Добавление персонажа производится через отправку документа-фото исходного изображения персонажа.")
|
||||||
55
routers/gen_router.py
Normal file
55
routers/gen_router.py
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
import asyncio
|
||||||
|
|
||||||
|
from aiogram import Router, Bot, F
|
||||||
|
from aiogram.enums import ParseMode
|
||||||
|
from aiogram.filters import *
|
||||||
|
from aiogram.types import *
|
||||||
|
|
||||||
|
from adapters.google_adapter import GoogleAdapter
|
||||||
|
|
||||||
|
router = Router()
|
||||||
|
|
||||||
|
@router.message(Command("image"))
|
||||||
|
async def cmd_image_gen(message: Message, command: CommandObject, gemini: GoogleAdapter, bot: Bot):
|
||||||
|
# ... ваш код ...
|
||||||
|
# Обратите внимание: gemini теперь прилетает аргументом, так как мы сделали dp["gemini"]
|
||||||
|
prompt = command.args
|
||||||
|
if not prompt:
|
||||||
|
await message.answer("⚠️ Напиши промпт.")
|
||||||
|
return
|
||||||
|
|
||||||
|
wait_msg = await message.answer("🎨 Генерирую...")
|
||||||
|
|
||||||
|
# Получение байтов фото (логика та же)
|
||||||
|
image_bytes = None
|
||||||
|
if message.photo:
|
||||||
|
file_io = await bot.download(message.photo[-1].file_id)
|
||||||
|
image_bytes = file_io.getvalue()
|
||||||
|
elif message.reply_to_message and message.reply_to_message.photo:
|
||||||
|
file_io = await bot.download(message.reply_to_message.photo[-1].file_id)
|
||||||
|
image_bytes = file_io.getvalue()
|
||||||
|
|
||||||
|
result = await asyncio.to_thread(
|
||||||
|
gemini.generate, prompt=prompt, image_bytes=image_bytes, generate_image=True
|
||||||
|
)
|
||||||
|
|
||||||
|
await wait_msg.delete()
|
||||||
|
|
||||||
|
if result.get("images"):
|
||||||
|
for img in result["images"]:
|
||||||
|
await message.answer_document(BufferedInputFile(img.read(), "img.png"))
|
||||||
|
elif result.get("text"):
|
||||||
|
await message.answer(result["text"])
|
||||||
|
else:
|
||||||
|
await message.answer(f"Ошибка: {result.get('error', 'Unknown')}")
|
||||||
|
|
||||||
|
|
||||||
|
@router.message(F.text)
|
||||||
|
async def handle_text(message: Message, gemini: GoogleAdapter, bot: Bot):
|
||||||
|
await bot.send_chat_action(message.chat.id, "typing")
|
||||||
|
result = await asyncio.to_thread(gemini.generate, prompt=message.text)
|
||||||
|
if result.get("text"):
|
||||||
|
await message.answer(result["text"], parse_mode=ParseMode.MARKDOWN)
|
||||||
|
else:
|
||||||
|
await message.answer("Ошибка генерации")
|
||||||
|
|
||||||
Reference in New Issue
Block a user