├── app ├── core │ ├── __init__.py │ ├── config.py │ └── logger.py ├── domain │ ├── __init__.py │ └── entities │ │ ├── __init__.py │ │ ├── gift.py │ │ ├── auto_buy_setting.py │ │ ├── user.py │ │ └── transaction.py ├── interfaces │ ├── __init__.py │ ├── services │ │ ├── __init__.py │ │ └── gifts_service.py │ ├── telegram │ │ ├── states │ │ │ ├── __init__.py │ │ │ ├── gift_state.py │ │ │ ├── deposit_state.py │ │ │ └── auto_buy_state.py │ │ ├── keyboards │ │ │ ├── __init__.py │ │ │ ├── inline.py │ │ │ └── default.py │ │ ├── handlers │ │ │ ├── auto_buy │ │ │ │ ├── __init__.py │ │ │ │ └── auto_buy.py │ │ │ ├── buy_gift │ │ │ │ ├── __init__.py │ │ │ │ └── buy_gift.py │ │ │ ├── payment │ │ │ │ ├── __init__.py │ │ │ │ ├── payment_handler.py │ │ │ │ ├── deposit_payment.py │ │ │ │ └── gift_payment.py │ │ │ ├── start.py │ │ │ ├── profile │ │ │ │ ├── __init__.py │ │ │ │ ├── balance.py │ │ │ │ ├── notifications.py │ │ │ │ ├── language.py │ │ │ │ ├── deposit.py │ │ │ │ ├── refund.py │ │ │ │ └── history.py │ │ │ ├── help.py │ │ │ └── __init__.py │ │ ├── middlewares │ │ │ ├── private_chat_only.py │ │ │ ├── error_handler.py │ │ │ └── back_button_middleware.py │ │ └── messages.py │ └── repositories │ │ ├── auto_buy_setting_repo.py │ │ ├── __init__.py │ │ ├── gift_repo.py │ │ ├── transaction_repo.py │ │ └── user_repo.py ├── application │ ├── __init__.py │ └── use_cases │ │ ├── gifts │ │ ├── __init__.py │ │ ├── sync_gifts.py │ │ ├── purchase_gift.py │ │ └── auto_buy_gifts.py │ │ ├── auto_buy_setting │ │ ├── __init__.py │ │ ├── update_auto_buy_setting.py │ │ └── get_or_create_auto_buy_setting.py │ │ ├── transaction │ │ ├── __init__.py │ │ ├── change_transaction_status.py │ │ ├── create_transaction.py │ │ └── refund_transaction.py │ │ ├── user │ │ ├── __init__.py │ │ ├── get_user_by_telegram_id.py │ │ ├── debit_user_balance.py │ │ ├── credit_user_balance.py │ │ └── register_user.py │ │ └── __init__.py ├── infrastructure │ ├── __init__.py │ ├── db │ │ ├── __init__.py │ │ ├── enums.py │ │ ├── models │ │ │ ├── __init__.py │ │ │ ├── gift.py │ │ │ ├── auto_buy_setting.py │ │ │ ├── user.py │ │ │ └── transaction.py │ │ ├── repositories │ │ │ ├── __init__.py │ │ │ ├── auto_buy_setting_repo_impl.py │ │ │ ├── gift_repo_impl.py │ │ │ ├── transaction_repo_impl.py │ │ │ └── user_repo_impl.py │ │ ├── database.py │ │ └── session.py │ ├── scheduler │ │ ├── __init__.py │ │ └── jobs.py │ ├── telegram │ │ ├── __init__.py │ │ └── bot.py │ └── services │ │ ├── __init__.py │ │ └── gifts_api_impl.py └── main.py ├── requirements.txt ├── .env ├── .gitignore ├── setup_and_run_windows.bat ├── setup_and_run_linux.sh ├── ReadmeEN.md └── Readme.md /app/core/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/domain/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/interfaces/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/application/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /app/infrastructure/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/infrastructure/db/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/interfaces/services/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/infrastructure/scheduler/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/infrastructure/telegram/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/interfaces/telegram/states/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/interfaces/telegram/keyboards/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/neverwasbored/TgGiftBuyerBot/HEAD/requirements.txt -------------------------------------------------------------------------------- /app/infrastructure/services/__init__.py: -------------------------------------------------------------------------------- 1 | from .gifts_api_impl import TelegramGiftsApi 2 | 3 | __all__ = ["TelegramGiftsApi"] 4 | -------------------------------------------------------------------------------- /.env: -------------------------------------------------------------------------------- 1 | BOT_TOKEN=7955856771:AAHPzYrKmjDVqqUvpcyQ1mGWut7NmjIqffg 2 | DATABASE_URL=sqlite+aiosqlite:///user_data.db 3 | CHECK_GIFTS_DELAY_SECONDS=3 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .venv/ 2 | venv/ 3 | .pytest_cache/ 4 | logs/ 5 | user_data.db 6 | .cursorignore 7 | **/__pycache__/ 8 | .idea/ 9 | .env 10 | -------------------------------------------------------------------------------- /app/interfaces/telegram/handlers/auto_buy/__init__.py: -------------------------------------------------------------------------------- 1 | from .auto_buy import router as auto_buy_router 2 | 3 | 4 | __all__ = ["auto_buy_router"] 5 | -------------------------------------------------------------------------------- /app/interfaces/telegram/handlers/buy_gift/__init__.py: -------------------------------------------------------------------------------- 1 | from .buy_gift import router as buy_gift_router 2 | 3 | 4 | __all__ = ["buy_gift_router"] 5 | -------------------------------------------------------------------------------- /app/main.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | 3 | from app.infrastructure.telegram.bot import main 4 | 5 | 6 | if __name__ == "__main__": 7 | asyncio.run(main()) 8 | -------------------------------------------------------------------------------- /app/interfaces/telegram/handlers/payment/__init__.py: -------------------------------------------------------------------------------- 1 | from .payment_handler import router as payment_handler_router 2 | 3 | 4 | __all__ = ["payment_handler_router"] 5 | -------------------------------------------------------------------------------- /app/interfaces/telegram/states/gift_state.py: -------------------------------------------------------------------------------- 1 | from aiogram.fsm.state import State, StatesGroup 2 | 3 | 4 | class GiftStates(StatesGroup): 5 | waiting_for_gift_id = State() 6 | -------------------------------------------------------------------------------- /setup_and_run_windows.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | IF NOT EXIST venv ( 3 | python -m venv venv 4 | ) 5 | call venv\Scripts\activate 6 | pip install -r requirements.txt 7 | python -m app.main 8 | -------------------------------------------------------------------------------- /app/interfaces/telegram/states/deposit_state.py: -------------------------------------------------------------------------------- 1 | from aiogram.fsm.state import State, StatesGroup 2 | 3 | 4 | class DepositStates(StatesGroup): 5 | waiting_for_amount_deposit = State() 6 | -------------------------------------------------------------------------------- /app/application/use_cases/gifts/__init__.py: -------------------------------------------------------------------------------- 1 | from .auto_buy_gifts import AutoBuyGiftsForAllUsers 2 | from .purchase_gift import PurchaseGift 3 | from .sync_gifts import SyncGifts 4 | 5 | __all__ = ["AutoBuyGiftsForAllUsers", "PurchaseGift", "SyncGifts"] 6 | -------------------------------------------------------------------------------- /app/infrastructure/db/enums.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | 3 | 4 | class TransactionStatus(Enum): 5 | COMPLETED = "completed" 6 | REFUNDED = "refunded" 7 | 8 | 9 | class UserRole(Enum): 10 | USER = "user" 11 | ADMIN = "admin" 12 | -------------------------------------------------------------------------------- /app/interfaces/telegram/states/auto_buy_state.py: -------------------------------------------------------------------------------- 1 | from aiogram.fsm.state import State, StatesGroup 2 | 3 | 4 | class AutoBuyStates(StatesGroup): 5 | menu = State() 6 | set_price = State() 7 | set_supply = State() 8 | set_cycles = State() 9 | -------------------------------------------------------------------------------- /app/application/use_cases/auto_buy_setting/__init__.py: -------------------------------------------------------------------------------- 1 | from .get_or_create_auto_buy_setting import GetOrCreateAutoBuySetting 2 | from .update_auto_buy_setting import UpdateAutoBuySetting 3 | 4 | __all__ = ["GetOrCreateAutoBuySetting", "UpdateAutoBuySetting"] 5 | -------------------------------------------------------------------------------- /setup_and_run_linux.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | if [ ! -d "venv" ]; then 3 | python3.12 -m venv venv 4 | fi 5 | 6 | # активируем виртуальное окружение 7 | source venv/bin/activate 8 | 9 | pip install -r requirements.txt 10 | python -m app.main 11 | -------------------------------------------------------------------------------- /app/domain/entities/__init__.py: -------------------------------------------------------------------------------- 1 | from .auto_buy_setting import AutoBuySettingDTO 2 | from .gift import GiftDTO 3 | from .transaction import TransactionDTO 4 | from .user import UserDTO 5 | 6 | __all__ = ["UserDTO", "GiftDTO", "TransactionDTO", "AutoBuySettingDTO"] 7 | -------------------------------------------------------------------------------- /app/infrastructure/db/models/__init__.py: -------------------------------------------------------------------------------- 1 | from .auto_buy_setting import AutoBuySettingModel 2 | from .gift import GiftModel 3 | from .transaction import TransactionModel 4 | from .user import UserModel 5 | 6 | __all__ = ["AutoBuySettingModel", "GiftModel", "TransactionModel", "UserModel"] 7 | -------------------------------------------------------------------------------- /app/domain/entities/gift.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | 3 | 4 | @dataclass(frozen=True) 5 | class GiftDTO: 6 | id: str 7 | gift_id: int 8 | emoji: str 9 | star_count: int 10 | remaining_count: int 11 | total_count: int 12 | is_new: bool = True 13 | -------------------------------------------------------------------------------- /app/application/use_cases/transaction/__init__.py: -------------------------------------------------------------------------------- 1 | from .change_transaction_status import ChangeTransactionStatus 2 | from .create_transaction import CreateTransaction 3 | from .refund_transaction import RefundTransaction 4 | 5 | __all__ = ["ChangeTransactionStatus", "CreateTransaction", "RefundTransaction"] 6 | -------------------------------------------------------------------------------- /app/interfaces/repositories/auto_buy_setting_repo.py: -------------------------------------------------------------------------------- 1 | from abc import ABC, abstractmethod 2 | 3 | from app.domain.entities import AutoBuySettingDTO 4 | 5 | 6 | class IAutoBuySettingRepository(ABC): 7 | @abstractmethod 8 | async def get_auto_buy_setting(self, telegram_id: int) -> AutoBuySettingDTO: ... 9 | -------------------------------------------------------------------------------- /app/domain/entities/auto_buy_setting.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | 3 | 4 | @dataclass(frozen=True) 5 | class AutoBuySettingDTO: 6 | id: int 7 | user_id: int 8 | status: bool 9 | price_limit_from: int 10 | price_limit_to: int 11 | supply_limit: int 12 | cycles: int 13 | -------------------------------------------------------------------------------- /app/core/config.py: -------------------------------------------------------------------------------- 1 | from pydantic_settings import BaseSettings, SettingsConfigDict 2 | 3 | 4 | class Settings(BaseSettings): 5 | bot_token: str 6 | database_url: str 7 | check_gifts_delay_seconds: int 8 | 9 | model_config = SettingsConfigDict(env_file=".env", case_sensitive=False) 10 | 11 | 12 | settings = Settings() 13 | -------------------------------------------------------------------------------- /app/domain/entities/user.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | 3 | from app.infrastructure.db.enums import UserRole 4 | 5 | 6 | @dataclass(frozen=True) 7 | class UserDTO: 8 | id: int 9 | telegram_id: int 10 | username: str 11 | balance: int 12 | role: UserRole 13 | notifications_enabled: bool 14 | language: str 15 | -------------------------------------------------------------------------------- /app/application/use_cases/user/__init__.py: -------------------------------------------------------------------------------- 1 | from .credit_user_balance import CreditUserBalance 2 | from .debit_user_balance import DebitUserBalance 3 | from .get_user_by_telegram_id import GetUserByTelegramId 4 | from .register_user import RegisterUser 5 | 6 | __all__ = [ 7 | "CreditUserBalance", 8 | "DebitUserBalance", 9 | "GetUserByTelegramId", 10 | "RegisterUser", 11 | ] 12 | -------------------------------------------------------------------------------- /app/interfaces/repositories/__init__.py: -------------------------------------------------------------------------------- 1 | from .auto_buy_setting_repo import IAutoBuySettingRepository 2 | from .gift_repo import IGiftRepository 3 | from .transaction_repo import ITransactionRepository 4 | from .user_repo import IUserRepository 5 | 6 | __all__ = [ 7 | "IUserRepository", 8 | "IGiftRepository", 9 | "IAutoBuySettingRepository", 10 | "ITransactionRepository", 11 | ] 12 | -------------------------------------------------------------------------------- /app/infrastructure/db/repositories/__init__.py: -------------------------------------------------------------------------------- 1 | from .auto_buy_setting_repo_impl import AutoBuySettingReporistory 2 | from .gift_repo_impl import GiftRepository 3 | from .transaction_repo_impl import TransactionRepository 4 | from .user_repo_impl import UserRepository 5 | 6 | __all__ = [ 7 | "UserRepository", 8 | "GiftRepository", 9 | "TransactionRepository", 10 | "AutoBuySettingReporistory", 11 | ] 12 | -------------------------------------------------------------------------------- /app/application/use_cases/user/get_user_by_telegram_id.py: -------------------------------------------------------------------------------- 1 | from app.domain.entities import UserDTO 2 | from app.interfaces.repositories import IUserRepository 3 | 4 | 5 | class GetUserByTelegramId: 6 | def __init__(self, repo: IUserRepository): 7 | self.repo = repo 8 | 9 | async def execute(self, telegram_id: int) -> UserDTO | None: 10 | return await self.repo.get_user_by_telegram_id(telegram_id=telegram_id) 11 | -------------------------------------------------------------------------------- /app/domain/entities/transaction.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | from datetime import datetime 3 | 4 | from app.infrastructure.db.enums import TransactionStatus 5 | 6 | 7 | @dataclass(frozen=True) 8 | class TransactionDTO: 9 | id: int 10 | user_id: int 11 | amount: int 12 | telegram_payment_charge_id: str 13 | payload: str 14 | status: TransactionStatus 15 | created_at: datetime 16 | updated_at: datetime 17 | -------------------------------------------------------------------------------- /app/interfaces/repositories/gift_repo.py: -------------------------------------------------------------------------------- 1 | from abc import ABC, abstractmethod 2 | from typing import Sequence 3 | 4 | from app.domain.entities import GiftDTO 5 | 6 | 7 | class IGiftRepository(ABC): 8 | @abstractmethod 9 | async def save_all(self, gifts: Sequence[GiftDTO]) -> None: ... 10 | 11 | @abstractmethod 12 | async def get_new_gifts(self) -> list[GiftDTO]: ... 13 | 14 | @abstractmethod 15 | async def reset_new_gifts(self): ... 16 | -------------------------------------------------------------------------------- /app/application/use_cases/user/debit_user_balance.py: -------------------------------------------------------------------------------- 1 | from app.domain.entities import UserDTO 2 | from app.interfaces.repositories import IUserRepository 3 | 4 | 5 | class DebitUserBalance: 6 | def __init__(self, repo: IUserRepository): 7 | self.repo = repo 8 | 9 | async def execute(self, telegram_id: int, amount: int) -> UserDTO: 10 | return await self.repo.debit_user_balance( 11 | telegram_id=telegram_id, amount=amount 12 | ) 13 | -------------------------------------------------------------------------------- /app/application/use_cases/user/credit_user_balance.py: -------------------------------------------------------------------------------- 1 | from app.domain.entities import UserDTO 2 | from app.interfaces.repositories import IUserRepository 3 | 4 | 5 | class CreditUserBalance: 6 | def __init__(self, repo: IUserRepository): 7 | self.repo = repo 8 | 9 | async def execute(self, telegram_id: int, amount: int) -> UserDTO: 10 | return await self.repo.credit_user_balance( 11 | telegram_id=telegram_id, amount=amount 12 | ) 13 | -------------------------------------------------------------------------------- /app/application/use_cases/auto_buy_setting/update_auto_buy_setting.py: -------------------------------------------------------------------------------- 1 | from app.domain.entities import AutoBuySettingDTO 2 | from app.interfaces.repositories import IAutoBuySettingRepository 3 | 4 | 5 | class UpdateAutoBuySetting: 6 | def __init__(self, repo: IAutoBuySettingRepository): 7 | self.repo = repo 8 | 9 | async def execute(self, telegram_id: int, **kwargs) -> AutoBuySettingDTO: 10 | return await self.repo.update_auto_buy_setting(telegram_id, **kwargs) 11 | -------------------------------------------------------------------------------- /app/interfaces/telegram/handlers/start.py: -------------------------------------------------------------------------------- 1 | from aiogram import Router, types 2 | from aiogram.filters import CommandStart 3 | 4 | from app.core.logger import logger 5 | from app.interfaces.telegram.keyboards.inline import language_keyboard 6 | 7 | router = Router() 8 | 9 | 10 | @logger.catch 11 | @router.message(CommandStart()) 12 | async def start_handler(message: types.Message): 13 | await message.answer( 14 | "Выберите язык / Choose language:", reply_markup=language_keyboard() 15 | ) 16 | -------------------------------------------------------------------------------- /app/infrastructure/db/database.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy.ext.asyncio import ( 2 | AsyncEngine, 3 | AsyncSession, 4 | async_sessionmaker, 5 | create_async_engine, 6 | ) 7 | from sqlalchemy.orm import DeclarativeBase 8 | 9 | from app.core.config import settings 10 | 11 | 12 | class Base(DeclarativeBase): 13 | pass 14 | 15 | 16 | async_engine: AsyncEngine = create_async_engine(url=settings.database_url) 17 | 18 | AsyncSessionLocal: AsyncSession = async_sessionmaker( 19 | bind=async_engine, expire_on_commit=False, autoflush=False 20 | ) 21 | -------------------------------------------------------------------------------- /app/infrastructure/db/session.py: -------------------------------------------------------------------------------- 1 | from contextlib import asynccontextmanager 2 | from typing import AsyncGenerator 3 | 4 | from sqlalchemy.ext.asyncio import AsyncSession 5 | 6 | from .database import AsyncSessionLocal 7 | 8 | 9 | @asynccontextmanager 10 | async def get_db() -> AsyncGenerator[AsyncSession, None]: 11 | async with AsyncSessionLocal() as session: 12 | try: 13 | yield session 14 | await session.commit() 15 | except Exception as e: 16 | await session.rollback() 17 | raise e 18 | -------------------------------------------------------------------------------- /app/interfaces/telegram/handlers/profile/__init__.py: -------------------------------------------------------------------------------- 1 | from .balance import router as balance_router 2 | from .deposit import router as deposit_router 3 | from .refund import router as refund_router 4 | from .history import router as history_router 5 | from .notifications import router as notifications_router 6 | from .language import router as language_router 7 | 8 | 9 | __all__ = [ 10 | "balance_router", 11 | "deposit_router", 12 | "refund_router", 13 | "history_router", 14 | "notifications_router", 15 | "language_router", 16 | ] 17 | -------------------------------------------------------------------------------- /app/application/use_cases/transaction/change_transaction_status.py: -------------------------------------------------------------------------------- 1 | from app.infrastructure.db.enums import TransactionStatus 2 | from app.interfaces.repositories import ITransactionRepository 3 | 4 | 5 | class ChangeTransactionStatus: 6 | def __init__(self, repo: ITransactionRepository): 7 | self.repo = repo 8 | 9 | async def execute(self, telegram_payment_charge_id: str, status: TransactionStatus): 10 | return self.repo.change_status( 11 | telegram_payment_charge_id=telegram_payment_charge_id, status=status 12 | ) 13 | -------------------------------------------------------------------------------- /app/interfaces/services/gifts_service.py: -------------------------------------------------------------------------------- 1 | from abc import ABC, abstractmethod 2 | 3 | from app.domain.entities import GiftDTO 4 | 5 | 6 | class IGiftsService(ABC): 7 | @abstractmethod 8 | async def get_available_gifts(self) -> list[GiftDTO] | None: ... 9 | 10 | @abstractmethod 11 | async def send_gift( 12 | self, user_id: int, gift_id: str, pay_for_upgrade: bool = False 13 | ) -> bool: ... 14 | 15 | @abstractmethod 16 | async def refund_payment( 17 | self, telegram_payment_charge_id: str, user_id: int 18 | ) -> bool: ... 19 | -------------------------------------------------------------------------------- /app/application/use_cases/user/register_user.py: -------------------------------------------------------------------------------- 1 | from app.domain.entities import UserDTO 2 | from app.interfaces.repositories import IUserRepository 3 | 4 | 5 | class RegisterUser: 6 | def __init__(self, repo: IUserRepository): 7 | self.repo = repo 8 | 9 | async def execute(self, telegram_id: int, username: str) -> UserDTO: 10 | existing_user = await self.repo.get_by_telegram_id(telegram_id) 11 | 12 | if existing_user: 13 | return existing_user 14 | 15 | return await self.repo.create(telegram_id=telegram_id, username=username) 16 | -------------------------------------------------------------------------------- /app/application/use_cases/auto_buy_setting/get_or_create_auto_buy_setting.py: -------------------------------------------------------------------------------- 1 | from app.domain.entities import AutoBuySettingDTO 2 | from app.interfaces.repositories import IAutoBuySettingRepository 3 | 4 | 5 | class GetOrCreateAutoBuySetting: 6 | def __init__(self, repo: IAutoBuySettingRepository): 7 | self.repo = repo 8 | 9 | async def execute(self, telegram_id: int, user_id: int) -> AutoBuySettingDTO: 10 | setting = await self.repo.get_auto_buy_setting(telegram_id) 11 | if setting: 12 | return setting 13 | return await self.repo.create_auto_buy_setting(user_id) 14 | -------------------------------------------------------------------------------- /app/core/logger.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | 4 | from loguru import logger 5 | 6 | logger.remove() 7 | logger.add( 8 | sys.stdout, 9 | format="{time:YYYY-MM-DD HH:mm:ss} | " 10 | "{level: <8} | " 11 | "{name}:{function}:{line} - " 12 | "{message}", 13 | level="INFO", 14 | enqueue=True, 15 | ) 16 | 17 | os.makedirs("logs", exist_ok=True) 18 | 19 | logger.add( 20 | "logs/bot_{time:YYYY-MM}.log", 21 | format="{time:YYYY-MM-DD HH:mm:ss} | {level} | {module}:{function}:{line} - {message}", 22 | level="DEBUG", 23 | rotation="1 week", 24 | compression="zip", 25 | enqueue=True, 26 | ) 27 | -------------------------------------------------------------------------------- /app/application/use_cases/__init__.py: -------------------------------------------------------------------------------- 1 | from .auto_buy_setting import GetOrCreateAutoBuySetting, UpdateAutoBuySetting 2 | from .gifts import AutoBuyGiftsForAllUsers, PurchaseGift, SyncGifts 3 | from .transaction import ChangeTransactionStatus, CreateTransaction, RefundTransaction 4 | from .user import CreditUserBalance, DebitUserBalance, GetUserByTelegramId, RegisterUser 5 | 6 | __all__ = [ 7 | # Gifts 8 | "AutoBuyGiftsForAllUsers", 9 | "PurchaseGift", 10 | "SyncGifts", 11 | # AutoBuySettings 12 | "GetOrCreateAutoBuySetting", 13 | "UpdateAutoBuySetting", 14 | # Transaction 15 | "ChangeTransactionStatus", 16 | "CreateTransaction", 17 | "RefundTransaction", 18 | # User 19 | "CreditUserBalance", 20 | "DebitUserBalance", 21 | "GetUserByTelegramId", 22 | "RegisterUser", 23 | ] 24 | -------------------------------------------------------------------------------- /app/interfaces/telegram/handlers/help.py: -------------------------------------------------------------------------------- 1 | from aiogram import Router, types 2 | from aiogram.filters import Command 3 | 4 | from app.infrastructure.db.repositories import UserRepository 5 | from app.infrastructure.db.session import get_db 6 | from app.interfaces.telegram.keyboards.default import main_menu_keyboard 7 | from app.interfaces.telegram.messages import MESSAGES 8 | 9 | router = Router() 10 | 11 | 12 | @router.message(Command(commands=["help"])) 13 | async def help_command(message: types.Message): 14 | async with get_db() as session: 15 | repo = UserRepository(session) 16 | user = await repo.get_user_by_telegram_id(message.from_user.id) 17 | lang = user.language if user else "ru" 18 | await message.reply( 19 | text=MESSAGES[lang]["help"], reply_markup=main_menu_keyboard() 20 | ) 21 | -------------------------------------------------------------------------------- /app/application/use_cases/transaction/create_transaction.py: -------------------------------------------------------------------------------- 1 | from app.domain.entities import TransactionDTO 2 | from app.infrastructure.db.enums import TransactionStatus 3 | from app.interfaces.repositories import ITransactionRepository 4 | 5 | 6 | class CreateTransaction: 7 | def __init__(self, repo: ITransactionRepository): 8 | self.repo = repo 9 | 10 | async def execute( 11 | self, 12 | user_id: int, 13 | amount: int, 14 | telegram_payment_charge_id: str, 15 | status: TransactionStatus, 16 | payload: str, 17 | ) -> TransactionDTO | None: 18 | return await self.repo.create( 19 | user_id=user_id, 20 | amount=amount, 21 | telegram_payment_charge_id=telegram_payment_charge_id, 22 | status=status, 23 | payload=payload, 24 | ) 25 | -------------------------------------------------------------------------------- /app/application/use_cases/gifts/sync_gifts.py: -------------------------------------------------------------------------------- 1 | from app.core.logger import logger 2 | from app.interfaces.repositories import IGiftRepository 3 | from app.interfaces.services.gifts_service import IGiftsService 4 | 5 | 6 | class SyncGifts: 7 | def __init__(self, api: IGiftsService, repo: IGiftRepository): 8 | self.api = api 9 | self.repo = repo 10 | 11 | async def execute(self): 12 | logger.info("[UseCase:SyncGifts] Старт синхронизации подарков") 13 | gifts = await self.api.get_available_gifts() or None 14 | if not gifts: 15 | logger.warning("[UseCase:SyncGifts] Не удалось получить подарки или их нет") 16 | return 17 | logger.info(f"[UseCase:SyncGifts] Получено подарков: {len(gifts)}") 18 | await self.repo.save_all(gifts=gifts) 19 | logger.info("[UseCase:SyncGifts] Завершено") 20 | -------------------------------------------------------------------------------- /app/infrastructure/db/models/gift.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy import BigInteger, Integer, String 2 | from sqlalchemy.orm import Mapped, mapped_column 3 | 4 | from ..database import Base 5 | 6 | 7 | class GiftModel(Base): 8 | __tablename__ = "gifts" 9 | 10 | id: Mapped[int] = mapped_column( 11 | Integer, primary_key=True, autoincrement=True) 12 | 13 | gift_id: Mapped[int] = mapped_column(BigInteger, index=True, unique=True) 14 | 15 | emoji: Mapped[str] = mapped_column(String, nullable=False) 16 | 17 | star_count: Mapped[int] = mapped_column(Integer, nullable=False) 18 | 19 | remaining_count: Mapped[int] = mapped_column( 20 | Integer, nullable=True, default=None) 21 | 22 | total_count: Mapped[int] = mapped_column( 23 | Integer, nullable=True, default=None) 24 | 25 | is_new: Mapped[bool] = mapped_column(default=True, nullable=False) 26 | -------------------------------------------------------------------------------- /app/interfaces/telegram/handlers/__init__.py: -------------------------------------------------------------------------------- 1 | from aiogram import Dispatcher 2 | 3 | from .help import router as help_router 4 | from .start import router as start_router 5 | from .auto_buy import auto_buy_router 6 | from .buy_gift import buy_gift_router 7 | from .payment import payment_handler_router 8 | from .profile import ( 9 | balance_router, 10 | deposit_router, 11 | refund_router, 12 | history_router, 13 | notifications_router, 14 | language_router, 15 | ) 16 | 17 | 18 | def register_handlers(dp: Dispatcher): 19 | dp.include_router(start_router) 20 | dp.include_router(help_router) 21 | dp.include_router(auto_buy_router) 22 | dp.include_router(buy_gift_router) 23 | dp.include_router(payment_handler_router) 24 | dp.include_router(balance_router) 25 | dp.include_router(deposit_router) 26 | dp.include_router(refund_router) 27 | dp.include_router(history_router) 28 | dp.include_router(notifications_router) 29 | dp.include_router(language_router) 30 | -------------------------------------------------------------------------------- /app/interfaces/telegram/middlewares/private_chat_only.py: -------------------------------------------------------------------------------- 1 | from aiogram import BaseMiddleware 2 | 3 | 4 | class PrivateChatOnlyMiddleware(BaseMiddleware): 5 | async def __call__(self, handler, event, data): 6 | message = ( 7 | data.get("event_update").message 8 | if hasattr(data.get("event_update"), "message") 9 | else None 10 | ) 11 | if message and getattr(message.chat, "type", None) != "private": 12 | await message.answer("Бот работает только в личных сообщениях.") 13 | return 14 | callback_query = ( 15 | data.get("event_update").callback_query 16 | if hasattr(data.get("event_update"), "callback_query") 17 | else None 18 | ) 19 | if ( 20 | callback_query 21 | and getattr(callback_query.message.chat, "type", None) != "private" 22 | ): 23 | await callback_query.answer( 24 | "Бот работает только в личных сообщениях.", show_alert=True 25 | ) 26 | return 27 | return await handler(event, data) 28 | -------------------------------------------------------------------------------- /app/interfaces/telegram/keyboards/inline.py: -------------------------------------------------------------------------------- 1 | from aiogram.types import InlineKeyboardButton, InlineKeyboardMarkup 2 | from aiogram.utils.keyboard import InlineKeyboardBuilder 3 | 4 | 5 | def payment_keyboard(price): 6 | builder = InlineKeyboardBuilder() 7 | builder.button(text=f"Оплатить {price}⭐️") 8 | 9 | 10 | def language_keyboard(): 11 | return InlineKeyboardMarkup( 12 | inline_keyboard=[ 13 | [ 14 | InlineKeyboardButton(text="🇷🇺 Русский", callback_data="lang_ru"), 15 | InlineKeyboardButton(text="🇬🇧 English", callback_data="lang_en"), 16 | ] 17 | ] 18 | ) 19 | 20 | 21 | def history_pagination_keyboard( 22 | has_prev: bool, 23 | has_next: bool, 24 | current_page: int, 25 | total_pages: int, 26 | lang: str = "ru", 27 | ): 28 | builder = InlineKeyboardBuilder() 29 | 30 | if has_prev: 31 | builder.button(text="◀️ Назад", callback_data=f"history_prev_{current_page}") 32 | if has_next: 33 | builder.button(text="Вперёд ▶️", callback_data=f"history_next_{current_page}") 34 | 35 | return builder.as_markup() 36 | -------------------------------------------------------------------------------- /app/infrastructure/db/models/auto_buy_setting.py: -------------------------------------------------------------------------------- 1 | from typing import TYPE_CHECKING 2 | 3 | from sqlalchemy import Boolean, ForeignKey, Integer 4 | from sqlalchemy.orm import Mapped, mapped_column, relationship 5 | 6 | from ..database import Base 7 | 8 | if TYPE_CHECKING: 9 | from . import UserModel 10 | 11 | 12 | class AutoBuySettingModel(Base): 13 | __tablename__ = "auto_buy_settings" 14 | 15 | id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) 16 | 17 | user_id: Mapped[int] = mapped_column( 18 | ForeignKey("users.id"), unique=True, index=True, nullable=False 19 | ) 20 | 21 | status: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False) 22 | 23 | price_limit_from: Mapped[int] = mapped_column(Integer, default=0, nullable=False) 24 | 25 | price_limit_to: Mapped[int] = mapped_column(Integer, default=10**9, nullable=False) 26 | 27 | supply_limit: Mapped[int] = mapped_column(Integer, default=10**9) 28 | 29 | cycles: Mapped[int] = mapped_column(Integer, default=1, nullable=False) 30 | 31 | user: Mapped["UserModel"] = relationship(back_populates="auto_buy_setting") 32 | -------------------------------------------------------------------------------- /app/interfaces/telegram/handlers/profile/balance.py: -------------------------------------------------------------------------------- 1 | from aiogram import Router, F 2 | from aiogram.types import Message 3 | 4 | from app.application.use_cases import GetUserByTelegramId 5 | from app.core.logger import logger 6 | from app.infrastructure.db.repositories import UserRepository 7 | from app.infrastructure.db.session import get_db 8 | from app.interfaces.telegram.keyboards.default import main_menu_keyboard 9 | from app.interfaces.telegram.messages import BUTTONS, MESSAGES 10 | 11 | 12 | router = Router() 13 | 14 | 15 | @logger.catch 16 | @router.message( 17 | F.text.in_( 18 | [ 19 | BUTTONS["ru"]["balance"], 20 | BUTTONS["en"]["balance"], 21 | ] 22 | ) 23 | ) 24 | async def get_balance_command(message: Message) -> None: 25 | async with get_db() as session: 26 | repo = UserRepository(session) 27 | user = await GetUserByTelegramId(repo).execute(telegram_id=message.from_user.id) 28 | lang = user.language if user else "ru" 29 | await message.answer( 30 | MESSAGES[lang]["main_menu_balance"](user.username, user.balance), 31 | reply_markup=main_menu_keyboard( 32 | lang=lang, notifications_enabled=user.notifications_enabled 33 | ), 34 | ) 35 | -------------------------------------------------------------------------------- /app/interfaces/repositories/transaction_repo.py: -------------------------------------------------------------------------------- 1 | from abc import ABC, abstractmethod 2 | 3 | from app.domain.entities import TransactionDTO 4 | from app.infrastructure.db.enums import TransactionStatus 5 | 6 | 7 | class ITransactionRepository(ABC): 8 | @abstractmethod 9 | async def create( 10 | self, 11 | user_id: int, 12 | amount: int, 13 | telegram_payment_charge_id: str, 14 | status: TransactionStatus, 15 | payload: str, 16 | ) -> TransactionDTO | None: ... 17 | 18 | @abstractmethod 19 | async def change_status( 20 | self, telegram_payment_charge_id: str, status: TransactionStatus 21 | ) -> TransactionDTO | None: ... 22 | 23 | @abstractmethod 24 | async def get_by_payment_charge_id( 25 | self, telegram_payment_charge_id: str 26 | ) -> TransactionDTO | None: ... 27 | 28 | @abstractmethod 29 | async def get_last_user_transactions( 30 | self, user_id: int, limit: int = 5 31 | ) -> list[TransactionDTO]: ... 32 | 33 | @abstractmethod 34 | async def get_user_transactions_paginated( 35 | self, user_id: int, offset: int, limit: int 36 | ) -> list[TransactionDTO]: ... 37 | 38 | @abstractmethod 39 | async def get_user_transactions_count(self, user_id: int) -> int: ... 40 | -------------------------------------------------------------------------------- /app/infrastructure/db/models/user.py: -------------------------------------------------------------------------------- 1 | from typing import TYPE_CHECKING 2 | 3 | from sqlalchemy import BigInteger, Enum, Integer, String 4 | from sqlalchemy.orm import Mapped, mapped_column, relationship 5 | 6 | from ..database import Base 7 | from ..enums import UserRole 8 | 9 | if TYPE_CHECKING: 10 | from .auto_buy_setting import AutoBuySettingModel 11 | from .transaction import TransactionModel 12 | 13 | 14 | class UserModel(Base): 15 | __tablename__ = "users" 16 | 17 | id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) 18 | 19 | telegram_id: Mapped[int] = mapped_column( 20 | BigInteger, nullable=False, unique=True, index=True 21 | ) 22 | 23 | username: Mapped[str] = mapped_column(String, nullable=False) 24 | 25 | balance: Mapped[int] = mapped_column(Integer, default=0) 26 | 27 | role: Mapped[UserRole] = mapped_column( 28 | Enum(UserRole), default=UserRole.USER, nullable=False 29 | ) 30 | 31 | notifications_enabled: Mapped[bool] = mapped_column(default=True, nullable=False) 32 | 33 | language: Mapped[str] = mapped_column(String, default="ru", nullable=False) 34 | 35 | auto_buy_setting: Mapped["AutoBuySettingModel"] = relationship( 36 | back_populates="user", cascade="all, delete-orphan", uselist=False 37 | ) 38 | 39 | transactions: Mapped[list["TransactionModel"]] = relationship( 40 | back_populates="user", cascade="all, delete-orphan" 41 | ) 42 | -------------------------------------------------------------------------------- /app/infrastructure/db/models/transaction.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | from typing import TYPE_CHECKING 3 | 4 | from sqlalchemy import DateTime, Enum as SQLEnum, ForeignKey, Integer, String, func 5 | from sqlalchemy.orm import Mapped, mapped_column, relationship 6 | 7 | from ..database import Base 8 | from ..enums import TransactionStatus 9 | 10 | if TYPE_CHECKING: 11 | from .user import UserModel 12 | 13 | 14 | class TransactionModel(Base): 15 | __tablename__ = "transactions" 16 | 17 | id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) 18 | 19 | user_id: Mapped[int] = mapped_column( 20 | ForeignKey("users.id"), nullable=False, index=True 21 | ) 22 | 23 | telegram_payment_charge_id: Mapped[str] = mapped_column(String, nullable=False) 24 | 25 | payload: Mapped[str] = mapped_column(String, nullable=True) 26 | 27 | amount: Mapped[int] = mapped_column(Integer, nullable=False) 28 | 29 | status: Mapped[TransactionStatus] = mapped_column( 30 | SQLEnum(TransactionStatus), default=TransactionStatus.COMPLETED, nullable=False 31 | ) 32 | 33 | created_at: Mapped[datetime] = mapped_column( 34 | DateTime, default=func.now(), nullable=False 35 | ) 36 | 37 | updated_at: Mapped[datetime] = mapped_column( 38 | DateTime, default=func.now(), onupdate=func.now(), nullable=False 39 | ) 40 | 41 | user: Mapped["UserModel"] = relationship(back_populates="transactions") 42 | -------------------------------------------------------------------------------- /app/interfaces/repositories/user_repo.py: -------------------------------------------------------------------------------- 1 | from abc import ABC, abstractmethod 2 | 3 | from app.domain.entities import UserDTO, AutoBuySettingDTO 4 | 5 | 6 | class IUserRepository(ABC): 7 | @abstractmethod 8 | async def create(self, telegram_id: int, username: str) -> UserDTO: ... 9 | 10 | @abstractmethod 11 | async def get_user_by_telegram_id( 12 | self, telegram_id: int) -> UserDTO | None: ... 13 | 14 | @abstractmethod 15 | async def get_user_by_id(self, user_id: int) -> UserDTO | None: ... 16 | 17 | @abstractmethod 18 | async def credit_user_balance( 19 | self, telegram_id: int, amount: int 20 | ) -> UserDTO | None: ... 21 | 22 | @abstractmethod 23 | async def debit_user_balance( 24 | self, telegram_id: int, amount: int 25 | ) -> UserDTO | None: ... 26 | 27 | @abstractmethod 28 | async def get_all_with_auto_buy_enabled(self) -> list[UserDTO]: ... 29 | 30 | @abstractmethod 31 | async def get_notifications_enabled(self, telegram_id: int) -> bool: ... 32 | 33 | @abstractmethod 34 | async def set_notifications_enabled( 35 | self, telegram_id: int, enabled: bool 36 | ) -> None: ... 37 | 38 | @abstractmethod 39 | async def get_language(self, telegram_id: int) -> str: ... 40 | 41 | @abstractmethod 42 | async def set_language(self, telegram_id: int, lang: str) -> None: ... 43 | 44 | @abstractmethod 45 | async def get_all_with_auto_buy_enabled_and_settings( 46 | self, 47 | ) -> list[tuple[UserDTO, AutoBuySettingDTO]]: ... 48 | -------------------------------------------------------------------------------- /app/interfaces/telegram/handlers/payment/payment_handler.py: -------------------------------------------------------------------------------- 1 | from aiogram import F, Router 2 | from aiogram.types import Message 3 | 4 | from app.core.logger import logger 5 | from app.infrastructure.db.session import get_db 6 | from app.interfaces.telegram.handlers.payment.deposit_payment import ( 7 | process_deposit_payment, 8 | ) 9 | from app.interfaces.telegram.handlers.payment.gift_payment import process_gift_payment 10 | 11 | router = Router() 12 | 13 | 14 | @logger.catch 15 | @router.message(F.successful_payment) 16 | async def handle_successful_payment(message: Message): 17 | payment_info = message.successful_payment 18 | 19 | logger.info(f"Успешная транзакция: {payment_info}") 20 | 21 | payload = payment_info.invoice_payload 22 | async with get_db() as session: 23 | if payload.startswith("deposit_"): 24 | try: 25 | await process_deposit_payment(message, payment_info, session) 26 | except ValueError: 27 | await message.answer( 28 | "Произошла ошибка при депозите!\nПопробуйте позже." 29 | ) 30 | 31 | elif payload.startswith("gift_"): 32 | try: 33 | await process_gift_payment(message, payment_info, session) 34 | except ValueError: 35 | await message.answer( 36 | "Произошла ошибка при покупке подарков!\nПопробуйте позже." 37 | ) 38 | 39 | else: 40 | logger.error(f"Неизвестный тип транзакции: {payload}") 41 | await message.answer( 42 | "Непредвиденная ошибка! Пожалуйста, свяжитесь с разработчиком!" 43 | ) 44 | -------------------------------------------------------------------------------- /app/interfaces/telegram/handlers/payment/deposit_payment.py: -------------------------------------------------------------------------------- 1 | from aiogram.types import Message, SuccessfulPayment 2 | 3 | from app.application.use_cases import CreateTransaction, CreditUserBalance 4 | from app.core.logger import logger 5 | from app.infrastructure.db.enums import TransactionStatus 6 | from app.infrastructure.db.repositories import TransactionRepository, UserRepository 7 | from app.interfaces.telegram.keyboards.default import main_menu_keyboard 8 | from app.interfaces.telegram.messages import ERRORS, MESSAGES 9 | 10 | 11 | @logger.catch 12 | async def process_deposit_payment( 13 | message: Message, payment_info: SuccessfulPayment, session 14 | ) -> None: 15 | payload = payment_info.invoice_payload 16 | parts = payload.split("_") 17 | amount = int(parts[1]) 18 | user_id = int(parts[3]) 19 | 20 | user_repo = UserRepository(session) 21 | transaction_repo = TransactionRepository(session) 22 | user = await CreditUserBalance(user_repo).execute( 23 | telegram_id=user_id, amount=amount 24 | ) 25 | lang = user.language if user else "ru" 26 | 27 | if not user: 28 | raise ValueError(ERRORS[lang]["user_not_found"]) 29 | 30 | transaction = await CreateTransaction(transaction_repo).execute( 31 | user_id=user.id, 32 | amount=amount, 33 | telegram_payment_charge_id=payment_info.telegram_payment_charge_id, 34 | status=TransactionStatus.COMPLETED, 35 | payload=payload, 36 | ) 37 | 38 | if not transaction: 39 | raise ValueError(ERRORS[lang]["transaction_failed"]) 40 | 41 | if user.notifications_enabled: 42 | await message.answer( 43 | MESSAGES[lang]["deposit_success"](amount), reply_markup=main_menu_keyboard() 44 | ) 45 | -------------------------------------------------------------------------------- /app/interfaces/telegram/handlers/profile/notifications.py: -------------------------------------------------------------------------------- 1 | from aiogram import Router, F 2 | from aiogram.types import Message 3 | 4 | from app.application.use_cases import GetUserByTelegramId 5 | from app.core.logger import logger 6 | from app.infrastructure.db.repositories import UserRepository 7 | from app.infrastructure.db.session import get_db 8 | from app.interfaces.telegram.keyboards.default import main_menu_keyboard 9 | from app.interfaces.telegram.messages import BUTTONS, MESSAGES 10 | 11 | 12 | router = Router() 13 | 14 | 15 | @logger.catch 16 | @router.message( 17 | F.text.in_( 18 | [ 19 | BUTTONS["ru"]["notifications_on"], 20 | BUTTONS["en"]["notifications_on"], 21 | BUTTONS["ru"]["notifications_off"], 22 | BUTTONS["en"]["notifications_off"], 23 | ] 24 | ) 25 | ) 26 | async def toggle_notifications(message: Message): 27 | async with get_db() as session: 28 | repo = UserRepository(session) 29 | user = await GetUserByTelegramId(repo).execute(telegram_id=message.from_user.id) 30 | lang = user.language if user else "ru" 31 | enabled = await repo.get_notifications_enabled(message.from_user.id) 32 | await repo.set_notifications_enabled(message.from_user.id, not enabled) 33 | toggled_msg = MESSAGES[lang].get("notifications_toggled") 34 | if toggled_msg is None: 35 | toggled_msg = ( 36 | "Уведомления включены." 37 | if not enabled 38 | else ( 39 | "Уведомления отключены." 40 | if lang == "ru" 41 | else ( 42 | "Notifications enabled." 43 | if not enabled 44 | else "Notifications disabled." 45 | ) 46 | ) 47 | ) 48 | else: 49 | toggled_msg = toggled_msg( 50 | "Уведомления включены." if not enabled else "Уведомления отключены." 51 | ) 52 | await message.answer( 53 | toggled_msg, 54 | reply_markup=main_menu_keyboard( 55 | lang=lang, notifications_enabled=not enabled 56 | ), 57 | ) 58 | -------------------------------------------------------------------------------- /app/interfaces/telegram/keyboards/default.py: -------------------------------------------------------------------------------- 1 | from aiogram.types import KeyboardButton, ReplyKeyboardMarkup 2 | 3 | from app.interfaces.telegram.messages import BUTTONS 4 | 5 | 6 | def main_menu_keyboard(lang="ru", notifications_enabled: bool = True): 7 | notif_btn = ( 8 | BUTTONS[lang]["notifications_on"] 9 | if notifications_enabled 10 | else BUTTONS[lang]["notifications_off"] 11 | ) 12 | return ReplyKeyboardMarkup( 13 | keyboard=[ 14 | [ 15 | KeyboardButton(text=BUTTONS[lang]["balance"]), 16 | KeyboardButton(text=BUTTONS[lang]["buy_gift"]), 17 | KeyboardButton(text=BUTTONS[lang]["deposit"]), 18 | ], 19 | [ 20 | KeyboardButton(text=BUTTONS[lang]["auto_buy"]), 21 | KeyboardButton(text=BUTTONS[lang]["history"]), 22 | ], 23 | [ 24 | KeyboardButton(text=notif_btn), 25 | KeyboardButton(text=BUTTONS[lang]["language"]), 26 | ], 27 | ], 28 | resize_keyboard=True, 29 | ) 30 | 31 | 32 | def back_keyboard(lang="ru"): 33 | return ReplyKeyboardMarkup( 34 | keyboard=[[KeyboardButton(text=BUTTONS[lang]["back"])]], resize_keyboard=True 35 | ) 36 | 37 | 38 | def cancel_keyboard(lang="ru"): 39 | return ReplyKeyboardMarkup( 40 | keyboard=[[KeyboardButton(text=BUTTONS[lang]["cancel"])]], resize_keyboard=True 41 | ) 42 | 43 | 44 | def history_keyboard(has_prev, has_next, lang="ru"): 45 | if lang not in BUTTONS: 46 | lang = "ru" 47 | row = [] 48 | if has_prev: 49 | row.append(KeyboardButton(text=BUTTONS[lang]["prev"])) 50 | if has_next: 51 | row.append(KeyboardButton(text=BUTTONS[lang]["next"])) 52 | keyboard = [row] if row else [] 53 | keyboard.append([KeyboardButton(text=BUTTONS[lang]["back"])]) 54 | return ReplyKeyboardMarkup(keyboard=keyboard, resize_keyboard=True) 55 | 56 | 57 | def auto_buy_keyboard(lang="ru"): 58 | return ReplyKeyboardMarkup( 59 | keyboard=[ 60 | [ 61 | KeyboardButton(text=BUTTONS[lang]["auto_buy_toggle"]), 62 | ], 63 | [ 64 | KeyboardButton(text=BUTTONS[lang]["auto_buy_price"]), 65 | KeyboardButton(text=BUTTONS[lang]["auto_buy_supply"]), 66 | KeyboardButton(text=BUTTONS[lang]["auto_buy_cycles"]), 67 | ], 68 | [ 69 | KeyboardButton(text=BUTTONS[lang]["back"]), 70 | ], 71 | ], 72 | resize_keyboard=True, 73 | ) 74 | -------------------------------------------------------------------------------- /app/infrastructure/telegram/bot.py: -------------------------------------------------------------------------------- 1 | from aiogram import Bot, Dispatcher 2 | from aiogram.fsm.storage.memory import MemoryStorage 3 | from aiogram.client.default import DefaultBotProperties 4 | from aiogram.enums import ParseMode 5 | from aiogram.exceptions import TelegramForbiddenError 6 | from sqlalchemy import create_engine 7 | 8 | from app.core.config import settings 9 | from app.core.logger import logger 10 | from app.infrastructure.db.database import Base 11 | from app.infrastructure.scheduler.jobs import schedule_sync_job 12 | from app.interfaces.telegram.handlers import register_handlers 13 | from app.interfaces.telegram.middlewares.private_chat_only import ( 14 | PrivateChatOnlyMiddleware, 15 | ) 16 | from app.interfaces.telegram.middlewares.error_handler import ( 17 | ErrorHandlerMiddleware, 18 | ) 19 | from app.interfaces.telegram.middlewares.back_button_middleware import ( 20 | BackButtonMiddleware, 21 | ) 22 | 23 | bot = Bot( 24 | token=settings.bot_token, default=DefaultBotProperties(parse_mode=ParseMode.HTML) 25 | ) 26 | dp = Dispatcher(storage=MemoryStorage()) 27 | 28 | dp.message.middleware(PrivateChatOnlyMiddleware()) 29 | dp.callback_query.middleware(PrivateChatOnlyMiddleware()) 30 | dp.message.middleware(ErrorHandlerMiddleware()) 31 | dp.callback_query.middleware(ErrorHandlerMiddleware()) 32 | dp.message.middleware(BackButtonMiddleware()) 33 | 34 | 35 | @dp.errors() 36 | async def errors_handler(update, exception): 37 | if isinstance(exception, TelegramForbiddenError): 38 | logger.warning(f"Bot was blocked by user: {exception}") 39 | return 40 | logger.error(f"Unhandled exception: {exception}") 41 | 42 | 43 | # Закомментировать если используете не sqlite / Comment if using another db not sqlite 44 | # + нужно использовать alembic для миграций / + need use alembic for migrations 45 | def init_db(): 46 | url = settings.database_url.replace("+aiosqlite", "") 47 | engine = create_engine(url) 48 | Base.metadata.create_all(engine) 49 | 50 | 51 | @logger.catch 52 | async def on_startup(): 53 | # Закомментировать если используете не sqlite / Comment if using another db not sqlite 54 | logger.info("Initializing database...") 55 | init_db() 56 | logger.info("Database initialized successfully") 57 | # //////////////////////////////////////////////////////////////////////////////////// 58 | 59 | logger.info("Starting scheduler...") 60 | schedule_sync_job(bot) 61 | 62 | 63 | @logger.catch 64 | async def main(): 65 | logger.info("Starting bot...") 66 | 67 | await on_startup() 68 | 69 | register_handlers(dp) 70 | 71 | await dp.start_polling(bot) 72 | -------------------------------------------------------------------------------- /app/interfaces/telegram/handlers/profile/language.py: -------------------------------------------------------------------------------- 1 | from aiogram import Router, F 2 | from aiogram.types import CallbackQuery, Message 3 | 4 | from app.application.use_cases import GetUserByTelegramId 5 | from app.core.logger import logger 6 | from app.infrastructure.db.repositories import UserRepository 7 | from app.infrastructure.db.session import get_db 8 | from app.interfaces.telegram.keyboards.default import main_menu_keyboard 9 | from app.interfaces.telegram.keyboards.inline import language_keyboard 10 | from app.interfaces.telegram.messages import BUTTONS, MESSAGES 11 | 12 | 13 | router = Router() 14 | 15 | 16 | @logger.catch 17 | @router.message( 18 | F.text.in_( 19 | [ 20 | BUTTONS["ru"]["language"], 21 | BUTTONS["en"]["language"], 22 | ] 23 | ) 24 | ) 25 | async def change_language(message: Message): 26 | async with get_db() as session: 27 | repo = UserRepository(session) 28 | user = await GetUserByTelegramId(repo).execute(telegram_id=message.from_user.id) 29 | lang = user.language if user else "ru" 30 | prompt = MESSAGES[lang].get("change_language_prompt") 31 | if prompt is None: 32 | prompt = "Выберите язык:" if lang == "ru" else "Choose language:" 33 | await message.answer( 34 | prompt, 35 | reply_markup=language_keyboard(), 36 | ) 37 | 38 | 39 | @router.callback_query(lambda c: c.data in ["lang_ru", "lang_en"]) 40 | async def set_language_callback(call: CallbackQuery): 41 | lang = "ru" if call.data == "lang_ru" else "en" 42 | async with get_db() as session: 43 | repo = UserRepository(session) 44 | user = await repo.get_user_by_telegram_id(call.from_user.id) 45 | if user: 46 | await repo.set_language(call.from_user.id, lang) 47 | else: 48 | user = await repo.create( 49 | call.from_user.id, call.from_user.username or "user", language=lang 50 | ) 51 | onboarding_text = MESSAGES[lang]["onboarding"](user.username, user.balance) 52 | 53 | # Если это выбор языка при старте, удаляем сообщение и отправляем новое 54 | if call.message.text == "Выберите язык / Choose language:": 55 | await call.message.delete() 56 | await call.message.answer( 57 | onboarding_text, 58 | reply_markup=main_menu_keyboard( 59 | lang=lang, notifications_enabled=user.notifications_enabled 60 | ), 61 | ) 62 | else: 63 | # Если это смена языка из меню, редактируем текущее сообщение 64 | await call.message.edit_text( 65 | onboarding_text, reply_markup=language_keyboard() 66 | ) 67 | 68 | await call.answer() 69 | -------------------------------------------------------------------------------- /app/interfaces/telegram/middlewares/error_handler.py: -------------------------------------------------------------------------------- 1 | from aiogram import BaseMiddleware 2 | from aiogram.exceptions import ( 3 | TelegramForbiddenError, 4 | TelegramBadRequest, 5 | TelegramRetryAfter, 6 | TelegramAPIError, 7 | TelegramNetworkError, 8 | TelegramConflictError, 9 | TelegramUnauthorizedError, 10 | TelegramMigrateToChat, 11 | ) 12 | from app.core.logger import logger 13 | 14 | 15 | class ErrorHandlerMiddleware(BaseMiddleware): 16 | async def __call__(self, handler, event, data): 17 | try: 18 | return await handler(event, data) 19 | except TelegramForbiddenError as e: 20 | user_id = self._get_user_id(event) 21 | logger.warning(f"Bot was blocked by user {user_id}: {e}") 22 | return 23 | except TelegramBadRequest as e: 24 | user_id = self._get_user_id(event) 25 | logger.warning(f"Bad request for user {user_id}: {e}") 26 | return 27 | except TelegramRetryAfter as e: 28 | user_id = self._get_user_id(event) 29 | logger.warning( 30 | f"Rate limit hit for user {user_id}, retry after {e.retry_after}s: {e}" 31 | ) 32 | return 33 | except TelegramUnauthorizedError as e: 34 | logger.error(f"Bot token is invalid or bot was deleted: {e}") 35 | return 36 | except TelegramMigrateToChat as e: 37 | logger.warning(f"Chat migrated to {e.migrate_to_chat_id}: {e}") 38 | return 39 | except TelegramConflictError as e: 40 | user_id = self._get_user_id(event) 41 | logger.warning(f"Conflict error for user {user_id}: {e}") 42 | return 43 | except TelegramNetworkError as e: 44 | logger.error(f"Network error: {e}") 45 | return 46 | except TelegramAPIError as e: 47 | user_id = self._get_user_id(event) 48 | logger.error(f"Telegram API error for user {user_id}: {e}") 49 | return 50 | except Exception as e: 51 | user_id = self._get_user_id(event) 52 | logger.error( 53 | f"Unhandled exception for user {user_id} in {handler.__name__}: {e}" 54 | ) 55 | return 56 | 57 | def _get_user_id(self, event): 58 | if hasattr(event, "from_user") and event.from_user: 59 | return event.from_user.id 60 | elif hasattr(event, "message") and event.message and event.message.from_user: 61 | return event.message.from_user.id 62 | elif ( 63 | hasattr(event, "callback_query") 64 | and event.callback_query 65 | and event.callback_query.from_user 66 | ): 67 | return event.callback_query.from_user.id 68 | elif hasattr(event, "chat") and event.chat: 69 | return event.chat.id 70 | return None 71 | -------------------------------------------------------------------------------- /app/interfaces/telegram/handlers/payment/gift_payment.py: -------------------------------------------------------------------------------- 1 | import aiohttp 2 | 3 | from app.core.logger import logger 4 | from app.application.use_cases import PurchaseGift 5 | from app.infrastructure.db.repositories import TransactionRepository, UserRepository 6 | from app.infrastructure.services import TelegramGiftsApi 7 | from app.interfaces.telegram.messages import ERRORS, MESSAGES 8 | 9 | 10 | @logger.catch 11 | async def process_gift_payment(message, payment_info, session, from_balance=False): 12 | if not from_balance: 13 | payload = payment_info.invoice_payload 14 | parts = payload.split("_") 15 | gift_id = int(parts[1]) 16 | user_id = int(parts[3]) 17 | gifts_count = int(parts[5]) 18 | provider_charge_id = payment_info.telegram_payment_charge_id 19 | else: 20 | parts = message.text.split() 21 | gift_id = int(parts[0]) 22 | user_id = int(parts[1]) 23 | gifts_count = int(parts[2]) 24 | payload = f"gift_{gift_id}_to_{user_id}_count_{gifts_count}" 25 | provider_charge_id = "buy_gift_transaction" 26 | 27 | async with aiohttp.ClientSession() as http_session: 28 | user_repo = UserRepository(session) 29 | transaction_repo = TransactionRepository(session) 30 | 31 | gifts_api = TelegramGiftsApi(http_session) 32 | use_case = PurchaseGift(user_repo, transaction_repo, gifts_api) 33 | 34 | result = await use_case.execute( 35 | buyer_telegram_id=message.from_user.id, 36 | recipient_id=user_id, 37 | gift_id=gift_id, 38 | gifts_count=gifts_count, 39 | payload=payload, 40 | provider_charge_id=provider_charge_id, 41 | ) 42 | 43 | user = result["user"] if result.get("user") else None 44 | lang = user.language if user else "ru" 45 | 46 | if not result["ok"]: 47 | err = result["error"] 48 | if err == "user_not_found": 49 | await message.reply(ERRORS[lang]["user_not_found"]) 50 | 51 | elif err == "gift_not_found": 52 | await message.reply(ERRORS[lang]["gift_not_found"]) 53 | 54 | elif err == "not_enough_balance": 55 | await message.reply(ERRORS[lang]["not_enough_balance"]) 56 | 57 | elif err == "debit_failed": 58 | await message.reply(ERRORS[lang]["debit_failed"]) 59 | 60 | elif err == "gift_send_failed": 61 | await message.reply(ERRORS[lang]["gift_send_failed"]) 62 | 63 | elif err == "transaction_failed": 64 | await message.reply(ERRORS[lang]["transaction_failed"]) 65 | 66 | else: 67 | await message.reply(ERRORS[lang]["unknown"]) 68 | 69 | return 70 | if user and user.notifications_enabled: 71 | await message.reply(MESSAGES[lang]["buy_gift_success"](user.balance)) 72 | -------------------------------------------------------------------------------- /app/interfaces/telegram/handlers/profile/deposit.py: -------------------------------------------------------------------------------- 1 | from aiogram import Router, F 2 | from aiogram.filters import StateFilter 3 | from aiogram.fsm.context import FSMContext 4 | from aiogram.types import LabeledPrice, Message, PreCheckoutQuery 5 | 6 | from app.application.use_cases import GetUserByTelegramId 7 | from app.core.logger import logger 8 | from app.infrastructure.db.repositories import UserRepository 9 | from app.infrastructure.db.session import get_db 10 | from app.interfaces.telegram.keyboards.default import back_keyboard, main_menu_keyboard 11 | from app.interfaces.telegram.messages import BUTTONS, ERRORS, MESSAGES 12 | from app.interfaces.telegram.states.deposit_state import DepositStates 13 | 14 | router = Router() 15 | 16 | 17 | @logger.catch 18 | @router.message( 19 | F.text.in_( 20 | [ 21 | BUTTONS["ru"]["deposit"], 22 | BUTTONS["en"]["deposit"], 23 | ] 24 | ) 25 | ) 26 | async def deposit_command(message: Message, state: FSMContext) -> None: 27 | async with get_db() as session: 28 | repo = UserRepository(session) 29 | user = await GetUserByTelegramId(repo).execute(telegram_id=message.from_user.id) 30 | lang = user.language if user else "ru" 31 | if not user: 32 | await message.reply( 33 | ERRORS[lang]["user_not_found"], 34 | reply_markup=main_menu_keyboard(lang=lang), 35 | ) 36 | return 37 | await message.answer( 38 | text=MESSAGES[lang]["deposit_prompt"](user.username, user.balance), 39 | reply_markup=back_keyboard(lang=lang), 40 | ) 41 | await state.set_state(DepositStates.waiting_for_amount_deposit) 42 | await state.update_data(prev_state=None) 43 | 44 | 45 | @logger.catch 46 | @router.message(StateFilter(DepositStates.waiting_for_amount_deposit)) 47 | async def process_deposit_input(message: Message, state: FSMContext) -> None: 48 | async with get_db() as session: 49 | repo = UserRepository(session) 50 | user = await GetUserByTelegramId(repo).execute(telegram_id=message.from_user.id) 51 | lang = user.language if user else "ru" 52 | try: 53 | amount = int(message.text) 54 | if amount <= 0: 55 | raise ValueError("Amount must be positive.") 56 | except ValueError: 57 | await message.answer( 58 | text=MESSAGES[lang]["deposit_error"], reply_markup=back_keyboard(lang=lang) 59 | ) 60 | return 61 | payload = f"deposit_{amount}_to_{message.from_user.id}" 62 | logger.info( 63 | f"Создаю депозит на {amount} звёзд от пользователя: {message.from_user.id}" 64 | ) 65 | prices = [ 66 | LabeledPrice(label=MESSAGES[lang]["deposit_invoice_title"], amount=amount) 67 | ] 68 | await message.answer_invoice( 69 | title=MESSAGES[lang]["deposit_invoice_title"], 70 | description=MESSAGES[lang]["deposit_invoice_description"](amount), 71 | payload=payload, 72 | currency="XTR", 73 | prices=prices, 74 | provider_token="", 75 | reply_markup=None, 76 | ) 77 | await state.clear() 78 | 79 | 80 | @logger.catch 81 | @router.pre_checkout_query() 82 | async def pre_checkout_handler(pre_checkout_query: PreCheckoutQuery) -> None: 83 | await pre_checkout_query.answer(ok=True) 84 | -------------------------------------------------------------------------------- /app/application/use_cases/transaction/refund_transaction.py: -------------------------------------------------------------------------------- 1 | from app.core.logger import logger 2 | from app.domain.entities import UserDTO 3 | from app.infrastructure.db.enums import TransactionStatus, UserRole 4 | from app.interfaces.repositories import ITransactionRepository, IUserRepository 5 | from app.interfaces.services.gifts_service import IGiftsService 6 | 7 | 8 | class RefundTransaction: 9 | def __init__( 10 | self, 11 | user_repo: IUserRepository, 12 | transaction_repo: ITransactionRepository, 13 | gifts_service: IGiftsService, 14 | ): 15 | self.user_repo = user_repo 16 | self.transaction_repo = transaction_repo 17 | self.gifts_service = gifts_service 18 | 19 | async def execute( 20 | self, admin_telegram_id: int, telegram_payment_charge_id: str 21 | ) -> dict: 22 | logger.info( 23 | f"[UseCase:RefundTransaction] Старт: admin={admin_telegram_id}, tx_id={telegram_payment_charge_id}" 24 | ) 25 | admin: UserDTO = await self.user_repo.get_user_by_telegram_id(admin_telegram_id) 26 | 27 | if not admin or admin.role != UserRole.ADMIN: 28 | logger.error("[UseCase:RefundTransaction] Ошибка: not_admin") 29 | return {"ok": False, "error": "not_admin"} 30 | 31 | transaction = await self.transaction_repo.get_by_payment_charge_id( 32 | telegram_payment_charge_id 33 | ) 34 | if not transaction: 35 | logger.error("[UseCase:RefundTransaction] Ошибка: transaction_not_found") 36 | return {"ok": False, "error": "transaction_not_found"} 37 | 38 | if transaction.status == TransactionStatus.REFUNDED: 39 | logger.error("[UseCase:RefundTransaction] Ошибка: already_refunded") 40 | return {"ok": False, "error": "already_refunded"} 41 | 42 | user: UserDTO = await self.user_repo.get_user_by_id(transaction.user_id) 43 | if not user: 44 | logger.error("[UseCase:RefundTransaction] Ошибка: user_not_found") 45 | return {"ok": False, "error": "user_not_found"} 46 | 47 | logger.info(f"[UseCase:RefundTransaction] Баланс до: {user.balance}") 48 | refund_amount = transaction.amount 49 | 50 | telegram_refund_success = await self.gifts_service.refund_payment( 51 | telegram_payment_charge_id, user.telegram_id 52 | ) 53 | if not telegram_refund_success: 54 | logger.error("[UseCase:RefundTransaction] Ошибка: telegram_refund_failed") 55 | return {"ok": False, "error": "telegram_refund_failed"} 56 | 57 | user = await self.user_repo.debit_user_balance(user.telegram_id, refund_amount) 58 | if not user: 59 | logger.error("[UseCase:RefundTransaction] Ошибка: debit_failed") 60 | return {"ok": False, "error": "debit_failed"} 61 | 62 | logger.info(f"[UseCase:RefundTransaction] Баланс после: {user.balance}") 63 | 64 | logger.info( 65 | f"[UseCase:RefundTransaction] Транзакция до: id={transaction.id}, status={transaction.status}" 66 | ) 67 | await self.transaction_repo.change_status( 68 | telegram_payment_charge_id, TransactionStatus.REFUNDED 69 | ) 70 | logger.info( 71 | f"[UseCase:RefundTransaction] Транзакция после: id={transaction.id}, status=refunded" 72 | ) 73 | logger.info( 74 | f"[UseCase:RefundTransaction] Успех: tx_id={telegram_payment_charge_id}, amount={refund_amount}" 75 | ) 76 | return {"ok": True, "refund_amount": refund_amount} 77 | -------------------------------------------------------------------------------- /app/infrastructure/db/repositories/auto_buy_setting_repo_impl.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy import select 2 | from sqlalchemy.ext.asyncio import AsyncSession 3 | 4 | from app.core.logger import logger 5 | from app.domain.entities import AutoBuySettingDTO 6 | from app.infrastructure.db.models import AutoBuySettingModel, UserModel 7 | from app.interfaces.repositories import IAutoBuySettingRepository 8 | 9 | 10 | class AutoBuySettingReporistory(IAutoBuySettingRepository): 11 | def __init__(self, session: AsyncSession): 12 | self.session = session 13 | 14 | @logger.catch 15 | async def get_auto_buy_setting(self, telegram_id: int) -> AutoBuySettingDTO | None: 16 | setting = await self.session.scalar( 17 | select(AutoBuySettingModel) 18 | .join(UserModel, AutoBuySettingModel.user_id == UserModel.id) 19 | .where(UserModel.telegram_id == telegram_id) 20 | ) 21 | if not setting: 22 | logger.info( 23 | f"[AutoBuySettingRepo] Нет настроек автопокупки для telegram_id={telegram_id}" 24 | ) 25 | return None 26 | 27 | logger.info( 28 | f"[AutoBuySettingRepo] Получены настройки автопокупки для user_id={setting.user_id}" 29 | ) 30 | 31 | return AutoBuySettingDTO( 32 | id=setting.id, 33 | user_id=setting.user_id, 34 | status=setting.status, 35 | price_limit_from=setting.price_limit_from, 36 | price_limit_to=setting.price_limit_to, 37 | supply_limit=setting.supply_limit, 38 | cycles=setting.cycles, 39 | ) 40 | 41 | @logger.catch 42 | async def create_auto_buy_setting(self, user_id: int) -> AutoBuySettingDTO: 43 | setting = AutoBuySettingModel(user_id=user_id) 44 | self.session.add(setting) 45 | await self.session.flush() 46 | 47 | logger.info( 48 | f"[AutoBuySettingRepo] Созданы настройки автопокупки для user_id={user_id}" 49 | ) 50 | 51 | return AutoBuySettingDTO( 52 | id=setting.id, 53 | user_id=setting.user_id, 54 | status=setting.status, 55 | price_limit_from=setting.price_limit_from, 56 | price_limit_to=setting.price_limit_to, 57 | supply_limit=setting.supply_limit, 58 | cycles=setting.cycles, 59 | ) 60 | 61 | @logger.catch 62 | async def update_auto_buy_setting( 63 | self, telegram_id: int, **kwargs 64 | ) -> AutoBuySettingDTO | None: 65 | setting = await self.session.scalar( 66 | select(AutoBuySettingModel) 67 | .join(UserModel, AutoBuySettingModel.user_id == UserModel.id) 68 | .where(UserModel.telegram_id == telegram_id) 69 | ) 70 | if not setting: 71 | logger.info( 72 | f"[AutoBuySettingRepo] Не найдено настроек для обновления telegram_id={telegram_id}" 73 | ) 74 | return None 75 | for key, value in kwargs.items(): 76 | if hasattr(setting, key): 77 | setattr(setting, key, value) 78 | await self.session.flush() 79 | 80 | logger.info( 81 | f"[AutoBuySettingRepo] Обновлены настройки автопокупки для user_id={setting.user_id}: {kwargs}" 82 | ) 83 | 84 | return AutoBuySettingDTO( 85 | id=setting.id, 86 | user_id=setting.user_id, 87 | status=setting.status, 88 | price_limit_from=setting.price_limit_from, 89 | price_limit_to=setting.price_limit_to, 90 | supply_limit=setting.supply_limit, 91 | cycles=setting.cycles, 92 | ) 93 | -------------------------------------------------------------------------------- /app/infrastructure/db/repositories/gift_repo_impl.py: -------------------------------------------------------------------------------- 1 | from typing import Sequence 2 | 3 | from sqlalchemy import select 4 | from sqlalchemy.ext.asyncio import AsyncSession 5 | 6 | from app.core.logger import logger 7 | from app.domain.entities import GiftDTO 8 | from app.infrastructure.db.models import GiftModel 9 | from app.interfaces.repositories import IGiftRepository 10 | 11 | 12 | class GiftRepository(IGiftRepository): 13 | def __init__(self, session: AsyncSession): 14 | self.session = session 15 | 16 | @logger.catch 17 | async def save_all(self, gifts: Sequence[GiftDTO]) -> bool: 18 | added = 0 19 | updated_count = 0 20 | for gift in gifts: 21 | exists = await self.session.scalar( 22 | select(GiftModel).where(GiftModel.gift_id == gift.gift_id) 23 | ) 24 | if not exists: 25 | self.session.add( 26 | GiftModel( 27 | gift_id=gift.gift_id, 28 | emoji=gift.emoji, 29 | star_count=gift.star_count, 30 | remaining_count=gift.remaining_count or None, 31 | total_count=gift.total_count or None, 32 | is_new=True, 33 | ) 34 | ) 35 | added += 1 36 | else: 37 | updated = False 38 | if exists.star_count != gift.star_count: 39 | exists.star_count = gift.star_count 40 | updated = True 41 | if exists.remaining_count != gift.remaining_count: 42 | exists.remaining_count = gift.remaining_count 43 | updated = True 44 | if exists.total_count != gift.total_count: 45 | exists.total_count = gift.total_count 46 | updated = True 47 | if updated: 48 | exists.is_new = True 49 | updated_count += 1 50 | else: 51 | exists.is_new = False 52 | await self.session.flush() 53 | logger.info( 54 | f"[GiftRepo] Добавлено новых подарков: {added}, обновлено: {updated_count}" 55 | ) 56 | return True 57 | 58 | async def get_new_gifts(self) -> list[GiftDTO]: 59 | result = await self.session.scalars(select(GiftModel).where(GiftModel.is_new)) 60 | gifts = result.all() 61 | logger.info(f"[GiftRepo] Найдено новых подарков: {len(gifts)}") 62 | return [ 63 | GiftDTO( 64 | id=str(g.id), 65 | gift_id=g.gift_id, 66 | emoji=g.emoji, 67 | star_count=g.star_count, 68 | remaining_count=g.remaining_count, 69 | total_count=g.total_count, 70 | is_new=g.is_new, 71 | ) 72 | for g in gifts 73 | ] 74 | 75 | async def reset_new_gifts(self): 76 | result = await self.session.scalars(select(GiftModel).where(GiftModel.is_new)) 77 | gifts = result.all() 78 | for g in gifts: 79 | g.is_new = False 80 | await self.session.flush() 81 | logger.info(f"[GiftRepo] Сброшено новых подарков: {len(gifts)}") 82 | 83 | async def reset_new_gift(self, gift_id: int): 84 | gift = await self.session.scalar( 85 | select(GiftModel).where( 86 | GiftModel.gift_id == gift_id, GiftModel.is_new) 87 | ) 88 | if gift: 89 | gift.is_new = False 90 | await self.session.flush() 91 | logger.info(f"[GiftRepo] Сброшен новый подарок: {gift_id}") 92 | -------------------------------------------------------------------------------- /app/interfaces/telegram/handlers/profile/refund.py: -------------------------------------------------------------------------------- 1 | import aiohttp 2 | from aiogram import Router 3 | from aiogram.types import Message 4 | from aiogram.filters import Command 5 | 6 | from app.application.use_cases import RefundTransaction 7 | from app.core.logger import logger 8 | from app.infrastructure.db.repositories import TransactionRepository, UserRepository 9 | from app.infrastructure.db.session import get_db 10 | from app.infrastructure.services import TelegramGiftsApi 11 | from app.interfaces.telegram.keyboards.default import back_keyboard, main_menu_keyboard 12 | from app.interfaces.telegram.messages import ERRORS, MESSAGES 13 | 14 | router = Router(name="Refund router") 15 | 16 | 17 | @logger.catch 18 | @router.message(Command(commands=["refund"])) 19 | async def command_refund_handler(message: Message) -> None: 20 | parts = message.text.strip().split() 21 | 22 | async with get_db() as session: 23 | user_repo = UserRepository(session) 24 | transaction_repo = TransactionRepository(session) 25 | 26 | user = await user_repo.get_user_by_telegram_id(message.from_user.id) 27 | lang = user.language if user else "ru" 28 | if len(parts) != 2: 29 | await message.reply( 30 | MESSAGES[lang]["input_error"], reply_markup=back_keyboard(lang=lang) 31 | ) 32 | return 33 | 34 | transaction_id = parts[1] 35 | 36 | async with aiohttp.ClientSession() as http_session: 37 | gifts_api = TelegramGiftsApi(http_session) 38 | use_case = RefundTransaction(user_repo, transaction_repo, gifts_api) 39 | result = await use_case.execute(message.from_user.id, transaction_id) 40 | 41 | if not result["ok"]: 42 | err = result["error"] 43 | if err == "not_admin": 44 | await message.reply( 45 | ERRORS[lang]["refund_not_admin"], 46 | reply_markup=back_keyboard(lang=lang), 47 | ) 48 | 49 | elif err == "transaction_not_found": 50 | await message.reply( 51 | ERRORS[lang]["refund_not_found"], 52 | reply_markup=back_keyboard(lang=lang), 53 | ) 54 | 55 | elif err == "already_refunded": 56 | await message.reply( 57 | ERRORS[lang]["refund_already"], 58 | reply_markup=back_keyboard(lang=lang), 59 | ) 60 | 61 | elif err == "user_not_found": 62 | await message.reply( 63 | ERRORS[lang]["refund_user_not_found"], 64 | reply_markup=back_keyboard(lang=lang), 65 | ) 66 | 67 | elif err == "debit_failed": 68 | await message.reply( 69 | ERRORS[lang]["refund_debit_failed"], 70 | reply_markup=back_keyboard(lang=lang), 71 | ) 72 | 73 | elif err == "telegram_refund_failed": 74 | await message.reply( 75 | ERRORS[lang]["refund_telegram_failed"], 76 | reply_markup=back_keyboard(lang=lang), 77 | ) 78 | 79 | else: 80 | await message.reply( 81 | f"{MESSAGES[lang]['operation_failed']} {err}", 82 | reply_markup=back_keyboard(lang=lang), 83 | ) 84 | 85 | return 86 | if user.notifications_enabled: 87 | await message.reply( 88 | MESSAGES[lang]["refund_success"]( 89 | transaction_id, result["refund_amount"] 90 | ), 91 | reply_markup=main_menu_keyboard(lang=lang), 92 | ) 93 | -------------------------------------------------------------------------------- /app/infrastructure/services/gifts_api_impl.py: -------------------------------------------------------------------------------- 1 | import aiohttp 2 | 3 | from app.core.config import settings 4 | from app.core.logger import logger 5 | from app.domain.entities import GiftDTO 6 | 7 | 8 | class TelegramGiftsApi: 9 | def __init__(self, session: aiohttp.ClientSession): 10 | self._session = session 11 | self._base_url = f"https://api.telegram.org/bot{settings.bot_token}" 12 | 13 | @logger.catch 14 | async def get_available_gifts(self) -> list[GiftDTO] | None: 15 | url = f"{self._base_url}/getAvailableGifts" 16 | try: 17 | async with self._session.get(url) as resp: 18 | data = await resp.json() 19 | 20 | if not data.get("ok"): 21 | logger.error(f"[GiftsApi] Телеграм вернул ошибку: {data}") 22 | return None 23 | 24 | gifts = data["result"]["gifts"] 25 | logger.info(f"[GiftsApi] Получены подарки: {gifts}") 26 | logger.info(f"[GiftsApi] Получено подарков: {len(gifts)}") 27 | return [ 28 | GiftDTO( 29 | id=gift["id"], 30 | gift_id=int(gift["id"]), 31 | emoji=gift["sticker"]["emoji"], 32 | star_count=gift["star_count"], 33 | remaining_count=gift.get("remaining_count", None), 34 | total_count=gift.get("total_count", None), 35 | ) 36 | for gift in gifts 37 | ] 38 | except Exception as e: 39 | logger.error(f"[GiftsApi] Ошибка getAvailableGifts: {e}") 40 | return None 41 | 42 | @logger.catch 43 | async def send_gift( 44 | self, user_id: int, gift_id: str, pay_for_upgrade: bool = False 45 | ) -> bool: 46 | url = f"{self._base_url}/sendGift" 47 | payload = { 48 | "user_id": user_id, 49 | "gift_id": gift_id, 50 | "pay_for_upgrade": pay_for_upgrade, 51 | } 52 | 53 | try: 54 | async with self._session.post(url, json=payload) as resp: 55 | data = await resp.json() 56 | if data.get("ok"): 57 | logger.info( 58 | f"[GiftsApi] Подарок {gift_id} успешно отправлен пользователю {user_id}" 59 | ) 60 | return True 61 | else: 62 | logger.error( 63 | f"[GiftsApi] Ошибка отправки подарка: {data.get('description')}" 64 | ) 65 | return False 66 | except Exception as e: 67 | logger.error(f"[GiftsApi] Ошибка при отправке подарка: {e}") 68 | return False 69 | 70 | @logger.catch 71 | async def refund_payment( 72 | self, telegram_payment_charge_id: str, user_id: int 73 | ) -> bool: 74 | url = f"{self._base_url}/refundStarPayment" 75 | payload = { 76 | "telegram_payment_charge_id": telegram_payment_charge_id, 77 | "user_id": user_id, 78 | } 79 | 80 | try: 81 | async with self._session.post(url, json=payload) as resp: 82 | data = await resp.json() 83 | if data.get("ok"): 84 | logger.info( 85 | f"[GiftsApi] Платеж {telegram_payment_charge_id} успешно возвращен" 86 | ) 87 | return True 88 | else: 89 | logger.error( 90 | f"[GiftsApi] Ошибка возврата платежа: {data.get('description')}" 91 | ) 92 | return False 93 | except Exception as e: 94 | logger.error(f"[GiftsApi] Ошибка при возврате платежа: {e}") 95 | return False 96 | -------------------------------------------------------------------------------- /app/infrastructure/scheduler/jobs.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | 3 | import aiohttp 4 | from aiogram import Bot 5 | from apscheduler.schedulers.asyncio import AsyncIOScheduler 6 | from apscheduler.triggers.interval import IntervalTrigger 7 | 8 | from app.application.use_cases import AutoBuyGiftsForAllUsers, PurchaseGift, SyncGifts 9 | from app.core.config import settings 10 | from app.core.logger import logger 11 | from app.infrastructure.db.repositories import ( 12 | AutoBuySettingReporistory, 13 | GiftRepository, 14 | UserRepository, 15 | TransactionRepository, 16 | ) 17 | from app.infrastructure.db.session import get_db 18 | from app.infrastructure.services import TelegramGiftsApi 19 | 20 | 21 | sync_lock = asyncio.Lock() 22 | pending_sync = False 23 | 24 | 25 | def schedule_sync_job(bot: Bot): 26 | scheduler = AsyncIOScheduler() 27 | 28 | async def sync_and_auto_buy_job(): 29 | global pending_sync 30 | if sync_lock.locked(): 31 | pending_sync = True 32 | logger.info("[Scheduler] Пропущен запуск: предыдущий цикл ещё не завершён.") 33 | return 34 | async with sync_lock: 35 | try: 36 | async with get_db() as session: 37 | async with aiohttp.ClientSession() as http_session: 38 | gifts_api = TelegramGiftsApi(http_session) 39 | gift_repo = GiftRepository(session) 40 | user_repo = UserRepository(session) 41 | auto_buy_repo = AutoBuySettingReporistory(session) 42 | transaction_repo = TransactionRepository(session) 43 | logger.info("[Scheduler] Запуск синхронизации подарков...") 44 | sync_uc = SyncGifts(gifts_api, gift_repo) 45 | await sync_uc.execute() 46 | logger.info("[Scheduler] Синхронизация подарков завершена.") 47 | new_gifts = await gift_repo.get_new_gifts() 48 | if new_gifts: 49 | logger.info( 50 | f"[Scheduler] Найдено новых подарков: {len(new_gifts)}. Запуск автопокупки..." 51 | ) 52 | purchase_uc = PurchaseGift( 53 | user_repo, transaction_repo, gifts_api 54 | ) 55 | auto_buy_uc = AutoBuyGiftsForAllUsers( 56 | user_repo, 57 | auto_buy_repo, 58 | gift_repo, 59 | gifts_api, 60 | purchase_uc, 61 | transaction_repo, 62 | bot, 63 | ) 64 | await auto_buy_uc.execute() 65 | logger.info("[Scheduler] Автопокупка завершена.") 66 | else: 67 | logger.info( 68 | "[Scheduler] Новых подарков нет. Автопокупка не требуется." 69 | ) 70 | except Exception as e: 71 | logger.error(f"[Scheduler] Ошибка в job: {e}") 72 | 73 | if pending_sync: 74 | pending_sync = False 75 | logger.info("[Scheduler] Запуск пропущенного sync_and_auto_buy_job...") 76 | await sync_and_auto_buy_job() 77 | 78 | scheduler.add_job( 79 | func=sync_and_auto_buy_job, 80 | trigger=IntervalTrigger(seconds=settings.check_gifts_delay_seconds), 81 | id="sync_and_auto_buy", 82 | replace_existing=True, 83 | ) 84 | logger.info("[Scheduler] Планировщик запущен.") 85 | scheduler.start() 86 | return scheduler 87 | -------------------------------------------------------------------------------- /app/application/use_cases/gifts/purchase_gift.py: -------------------------------------------------------------------------------- 1 | from app.core.logger import logger 2 | from app.domain.entities import GiftDTO, UserDTO 3 | from app.infrastructure.db.enums import TransactionStatus 4 | from app.interfaces.repositories import ITransactionRepository, IUserRepository 5 | from app.interfaces.services.gifts_service import IGiftsService 6 | 7 | 8 | class PurchaseGift: 9 | def __init__( 10 | self, 11 | user_repo: IUserRepository, 12 | transaction_repo: ITransactionRepository, 13 | gifts_service: IGiftsService, 14 | ): 15 | self.user_repo = user_repo 16 | self.transaction_repo = transaction_repo 17 | self.gifts_service = gifts_service 18 | 19 | async def execute( 20 | self, 21 | buyer_telegram_id: int, 22 | recipient_id: int, 23 | gift_id: int, 24 | gifts_count: int, 25 | payload: str, 26 | provider_charge_id: str = None, 27 | ) -> dict: 28 | if provider_charge_id is None: 29 | provider_charge_id = payload 30 | logger.info( 31 | f"[UseCase:PurchaseGift] Старт: buyer={buyer_telegram_id}, recipient={recipient_id}, gift={gift_id}, count={gifts_count}" 32 | ) 33 | 34 | if str(recipient_id).startswith("-100"): 35 | logger.error("[UseCase:PurchaseGift] Ошибка: recipient_is_channel") 36 | return {"ok": False, "error": "recipient_is_channel"} 37 | 38 | user: UserDTO = await self.user_repo.get_user_by_telegram_id(buyer_telegram_id) 39 | if not user: 40 | logger.error("[UseCase:PurchaseGift] Ошибка: user_not_found") 41 | return {"ok": False, "error": "user_not_found"} 42 | 43 | logger.info(f"[UseCase:PurchaseGift] Баланс до: {user.balance}") 44 | gifts: list[GiftDTO] = await self.gifts_service.get_available_gifts() 45 | gift = next((g for g in gifts if int(g.gift_id) == int(gift_id)), None) 46 | if not gift: 47 | logger.error("[UseCase:PurchaseGift] Ошибка: gift_not_found") 48 | return {"ok": False, "error": "gift_not_found"} 49 | logger.info( 50 | f"[UseCase:PurchaseGift] Параметры подарка: id={gift.gift_id}, price={gift.star_count}, total={gift.total_count}" 51 | ) 52 | 53 | amount = gift.star_count * gifts_count 54 | if user.balance < amount: 55 | logger.error("[UseCase:PurchaseGift] Ошибка: not_enough_balance") 56 | return { 57 | "ok": False, 58 | "error": "not_enough_balance", 59 | "required": amount - user.balance, 60 | "gift_price": gift.star_count, 61 | } 62 | 63 | for _ in range(gifts_count): 64 | sent = await self.gifts_service.send_gift( 65 | user_id=recipient_id, gift_id=gift_id 66 | ) 67 | if not sent: 68 | error_code = None 69 | if ( 70 | isinstance(sent, dict) 71 | and sent.get("error_code") == "STARGIFT_USAGE_LIMITED" 72 | ): 73 | error_code = "STARGIFT_USAGE_LIMITED" 74 | logger.error("[UseCase:PurchaseGift] Ошибка: gift_send_failed") 75 | return { 76 | "ok": False, 77 | "error": "gift_send_failed", 78 | "error_code": error_code, 79 | } 80 | 81 | user = await self.user_repo.debit_user_balance(buyer_telegram_id, amount) 82 | if not user: 83 | logger.error("[UseCase:PurchaseGift] Ошибка: debit_failed") 84 | return {"ok": False, "error": "debit_failed"} 85 | logger.info(f"[UseCase:PurchaseGift] Баланс после: {user.balance}") 86 | 87 | transaction = await self.transaction_repo.create( 88 | user_id=user.id, 89 | amount=amount, 90 | telegram_payment_charge_id=provider_charge_id, 91 | status=TransactionStatus.COMPLETED, 92 | payload=payload, 93 | ) 94 | if not transaction: 95 | logger.error("[UseCase:PurchaseGift] Ошибка: transaction_failed") 96 | return {"ok": False, "error": "transaction_failed"} 97 | logger.info( 98 | f"[UseCase:PurchaseGift] Транзакция: id={transaction.id}, amount={amount}, status={transaction.status}" 99 | ) 100 | 101 | logger.info( 102 | f"[UseCase:PurchaseGift] Успех: buyer={buyer_telegram_id}, recipient={recipient_id}, gift={gift_id}, count={gifts_count}" 103 | ) 104 | return { 105 | "ok": True, 106 | "user": user, 107 | "transaction": transaction, 108 | "gift": gift, 109 | "amount": amount, 110 | } 111 | -------------------------------------------------------------------------------- /app/interfaces/telegram/handlers/profile/history.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | from aiogram import Router, F 3 | from aiogram.fsm.context import FSMContext 4 | from aiogram.types import Message, CallbackQuery 5 | 6 | from app.application.use_cases import GetUserByTelegramId 7 | from app.core.logger import logger 8 | from app.infrastructure.db.repositories import TransactionRepository, UserRepository 9 | from app.infrastructure.db.session import get_db 10 | from app.interfaces.telegram.keyboards.default import ( 11 | back_keyboard, 12 | main_menu_keyboard, 13 | ) 14 | from app.interfaces.telegram.keyboards.inline import history_pagination_keyboard 15 | from app.interfaces.telegram.messages import BUTTONS, MESSAGES 16 | 17 | 18 | router = Router() 19 | 20 | 21 | @logger.catch 22 | @router.message( 23 | F.text.in_( 24 | [ 25 | BUTTONS["ru"]["history"], 26 | BUTTONS["en"]["history"], 27 | ] 28 | ) 29 | ) 30 | async def history_handler(message: Message, state: FSMContext) -> None: 31 | await state.update_data(history_page=0) 32 | await send_history_page(message, state) 33 | 34 | 35 | @logger.catch 36 | @router.callback_query(F.data.startswith("history_prev_")) 37 | async def history_prev_callback(callback: CallbackQuery, state: FSMContext) -> None: 38 | data = await state.get_data() 39 | page = max(data.get("history_page", 0) - 1, 0) 40 | await state.update_data(history_page=page) 41 | await send_history_page(callback.message, state, callback.from_user.id) 42 | await callback.answer() 43 | 44 | 45 | @logger.catch 46 | @router.callback_query(F.data.startswith("history_next_")) 47 | async def history_next_callback(callback: CallbackQuery, state: FSMContext) -> None: 48 | data = await state.get_data() 49 | page = data.get("history_page", 0) + 1 50 | await state.update_data(history_page=page) 51 | await send_history_page(callback.message, state, callback.from_user.id) 52 | await callback.answer() 53 | 54 | 55 | async def send_history_page(message: Message, state: FSMContext, user_id: int = None): 56 | if user_id is None: 57 | user_id = message.from_user.id 58 | 59 | async with get_db() as session: 60 | repo = UserRepository(session) 61 | tx_repo = TransactionRepository(session) 62 | user = await GetUserByTelegramId(repo).execute(telegram_id=user_id) 63 | lang = user.language if user else "ru" 64 | data = await state.get_data() 65 | page = data.get("history_page", 0) 66 | limit = 5 67 | offset = page * limit 68 | 69 | total_transactions = await tx_repo.get_user_transactions_count(user.id) 70 | transactions = await tx_repo.get_user_transactions_paginated( 71 | user.id, offset, limit 72 | ) 73 | 74 | total_pages = max(1, (total_transactions + limit - 1) // limit) 75 | has_next = page < total_pages - 1 76 | has_prev = page > 0 77 | 78 | if not transactions: 79 | await message.answer( 80 | MESSAGES[lang]["history_empty"], 81 | reply_markup=main_menu_keyboard( 82 | lang=lang, notifications_enabled=user.notifications_enabled 83 | ), 84 | ) 85 | return 86 | 87 | lines = [] 88 | for tx in transactions: 89 | payload = tx.payload 90 | status = tx.status.name 91 | amount = tx.amount 92 | if payload and payload.startswith("deposit_"): 93 | op = MESSAGES[lang]["history_line_deposit_op"] 94 | emoji = "💳" 95 | elif payload and payload.startswith("gift_"): 96 | op = MESSAGES[lang]["history_line_gift_op"] 97 | emoji = "🎁" 98 | elif payload and payload.startswith("autobuy_"): 99 | op = MESSAGES[lang]["history_line_autobuy_op"] 100 | emoji = "🤖" 101 | elif status == "REFUNDED": 102 | op = MESSAGES[lang]["history_line_refund_op"] 103 | emoji = "↩️" 104 | else: 105 | op = MESSAGES[lang]["history_line_operation_op"] 106 | emoji = "💸" 107 | 108 | if status == "REFUNDED": 109 | tx_time = ( 110 | tx.updated_at.strftime("%Y-%m-%d %H:%M:%S") if tx.updated_at else "" 111 | ) 112 | else: 113 | tx_time = ( 114 | tx.created_at.strftime("%Y-%m-%d %H:%M:%S") if tx.created_at else "" 115 | ) 116 | 117 | line = f"{tx_time} | " + MESSAGES[lang]["history_line"]( 118 | emoji, op, amount, status 119 | ) 120 | lines.append(line) 121 | 122 | now = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S") 123 | header = f"🕓 История\nВремя: {now}\nСтраница: {page+1}/{total_pages}\n" 124 | 125 | inline_markup = history_pagination_keyboard( 126 | has_prev, has_next, page, total_pages, lang 127 | ) 128 | 129 | try: 130 | await message.edit_text( 131 | header + "\n" + "\n".join(lines), 132 | reply_markup=inline_markup, 133 | ) 134 | except: 135 | await message.answer( 136 | header + "\n" + "\n".join(lines), 137 | reply_markup=inline_markup, 138 | ) 139 | -------------------------------------------------------------------------------- /app/interfaces/telegram/middlewares/back_button_middleware.py: -------------------------------------------------------------------------------- 1 | from aiogram import BaseMiddleware 2 | from aiogram.types import Message 3 | from aiogram.fsm.context import FSMContext 4 | 5 | from app.core.logger import logger 6 | from app.interfaces.telegram.messages import BUTTONS, MESSAGES 7 | from app.infrastructure.db.repositories import UserRepository, AutoBuySettingReporistory 8 | from app.infrastructure.db.session import get_db 9 | from app.interfaces.telegram.keyboards.default import ( 10 | main_menu_keyboard, 11 | auto_buy_keyboard, 12 | ) 13 | from app.interfaces.telegram.states.auto_buy_state import AutoBuyStates 14 | 15 | 16 | class BackButtonMiddleware(BaseMiddleware): 17 | async def __call__(self, handler, event, data): 18 | if isinstance(event, Message): 19 | logger.info( 20 | f"BackButtonMiddleware: Processing message with text: '{event.text}'" 21 | ) 22 | 23 | if event.text in [BUTTONS["ru"]["back"], BUTTONS["en"]["back"]]: 24 | logger.info(f"Back button detected: '{event.text}'") 25 | 26 | try: 27 | state: FSMContext = data.get("state") 28 | if not state: 29 | logger.info("No state found, returning to main menu") 30 | await self._return_to_main_menu(event) 31 | return 32 | 33 | current_state = await state.get_state() 34 | data_state = await state.get_data() 35 | prev_state = data_state.get("prev_state") 36 | 37 | logger.info( 38 | f"Current state: {current_state}, prev_state: {prev_state}" 39 | ) 40 | 41 | if not current_state: 42 | logger.info("No current state, returning to main menu") 43 | await state.clear() 44 | await self._return_to_main_menu(event) 45 | return 46 | elif current_state and current_state.startswith("AutoBuyStates"): 47 | await self._handle_auto_buy_back( 48 | event, state, current_state, prev_state 49 | ) 50 | return 51 | elif current_state and current_state.startswith("GiftStates"): 52 | await state.clear() 53 | await self._return_to_main_menu(event) 54 | return 55 | elif current_state and current_state.startswith("DepositStates"): 56 | await state.clear() 57 | await self._return_to_main_menu(event) 58 | return 59 | else: 60 | if prev_state: 61 | await state.set_state(prev_state) 62 | if prev_state == AutoBuyStates.menu.state: 63 | await self._return_to_auto_buy_menu(event) 64 | return 65 | else: 66 | await state.clear() 67 | await self._return_to_main_menu(event) 68 | return 69 | except Exception as e: 70 | logger.error(f"Error in back button middleware: {e}") 71 | await self._return_to_main_menu(event) 72 | return 73 | 74 | return await handler(event, data) 75 | 76 | async def _return_to_main_menu(self, event): 77 | async with get_db() as session: 78 | user_repo = UserRepository(session) 79 | user = await user_repo.get_user_by_telegram_id(event.from_user.id) 80 | lang = user.language if user else "ru" 81 | text = MESSAGES[lang]["onboarding"]( 82 | user.username if user else "user", user.balance if user else 0 83 | ) 84 | await event.answer( 85 | text, 86 | reply_markup=main_menu_keyboard( 87 | lang=lang, 88 | notifications_enabled=( 89 | user.notifications_enabled if user else True 90 | ), 91 | ), 92 | ) 93 | 94 | async def _return_to_auto_buy_menu(self, event): 95 | async with get_db() as session: 96 | user_repo = UserRepository(session) 97 | auto_buy_repo = AutoBuySettingReporistory(session) 98 | user = await user_repo.get_user_by_telegram_id(event.from_user.id) 99 | settings = await auto_buy_repo.get_auto_buy_setting(event.from_user.id) 100 | lang = user.language if user else "ru" 101 | await event.answer( 102 | MESSAGES[lang]["auto_buy_settings"](user, settings), 103 | reply_markup=auto_buy_keyboard(lang=lang), 104 | parse_mode="HTML", 105 | ) 106 | 107 | async def _handle_auto_buy_back(self, event, state, current_state, prev_state): 108 | if current_state == AutoBuyStates.menu.state: 109 | await state.clear() 110 | await self._return_to_main_menu(event) 111 | elif prev_state == AutoBuyStates.menu.state: 112 | await state.set_state(AutoBuyStates.menu) 113 | await state.update_data(prev_state=None) 114 | await self._return_to_auto_buy_menu(event) 115 | else: 116 | await state.clear() 117 | await self._return_to_main_menu(event) 118 | -------------------------------------------------------------------------------- /app/infrastructure/db/repositories/transaction_repo_impl.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy import desc, select, func 2 | from sqlalchemy.ext.asyncio import AsyncSession 3 | 4 | from app.core.logger import logger 5 | from app.domain.entities import TransactionDTO 6 | from app.infrastructure.db.enums import TransactionStatus 7 | from app.infrastructure.db.models import TransactionModel 8 | from app.interfaces.repositories import ITransactionRepository 9 | 10 | 11 | class TransactionRepository(ITransactionRepository): 12 | def __init__(self, session: AsyncSession): 13 | self.session = session 14 | 15 | async def create( 16 | self, 17 | user_id: int, 18 | amount: int, 19 | telegram_payment_charge_id: str, 20 | status: TransactionStatus, 21 | payload: str, 22 | ) -> TransactionDTO | None: 23 | existing_transaction = await self.session.scalar( 24 | select(TransactionModel).where( 25 | TransactionModel.telegram_payment_charge_id 26 | == telegram_payment_charge_id 27 | ) 28 | ) 29 | if existing_transaction: 30 | return None 31 | 32 | transaction = TransactionModel( 33 | user_id=user_id, 34 | amount=amount, 35 | telegram_payment_charge_id=telegram_payment_charge_id, 36 | status=status, 37 | payload=payload, 38 | ) 39 | self.session.add(transaction) 40 | await self.session.flush() 41 | 42 | logger.info( 43 | f"[TransactionRepo] Создана транзакция: id={transaction.id}, user_id={user_id}, amount={amount}, status={status}" 44 | ) 45 | 46 | return TransactionDTO( 47 | id=transaction.id, 48 | user_id=transaction.user_id, 49 | amount=transaction.amount, 50 | telegram_payment_charge_id=transaction.telegram_payment_charge_id, 51 | status=transaction.status, 52 | payload=transaction.payload, 53 | created_at=transaction.created_at, 54 | updated_at=transaction.updated_at, 55 | ) 56 | 57 | async def change_status( 58 | self, telegram_payment_charge_id: str, status: TransactionStatus 59 | ) -> TransactionDTO | None: 60 | existing_transaction = await self.session.scalar( 61 | select(TransactionModel).where( 62 | TransactionModel.telegram_payment_charge_id 63 | == telegram_payment_charge_id 64 | ) 65 | ) 66 | 67 | if not existing_transaction: 68 | return None 69 | 70 | existing_transaction.status = status 71 | 72 | logger.info( 73 | f"[TransactionRepo] Изменён статус транзакции: id={existing_transaction.id}, user_id={existing_transaction.user_id}, status={status}" 74 | ) 75 | 76 | return TransactionDTO( 77 | id=existing_transaction.id, 78 | user_id=existing_transaction.user_id, 79 | amount=existing_transaction.amount, 80 | telegram_payment_charge_id=existing_transaction.telegram_payment_charge_id, 81 | status=existing_transaction.status, 82 | payload=existing_transaction.payload, 83 | created_at=existing_transaction.created_at, 84 | updated_at=existing_transaction.updated_at, 85 | ) 86 | 87 | async def get_by_payment_charge_id( 88 | self, telegram_payment_charge_id: str 89 | ) -> TransactionDTO | None: 90 | transaction = await self.session.scalar( 91 | select(TransactionModel).where( 92 | TransactionModel.telegram_payment_charge_id 93 | == telegram_payment_charge_id 94 | ) 95 | ) 96 | 97 | if not transaction: 98 | return None 99 | 100 | return TransactionDTO( 101 | id=transaction.id, 102 | user_id=transaction.user_id, 103 | amount=transaction.amount, 104 | telegram_payment_charge_id=transaction.telegram_payment_charge_id, 105 | status=transaction.status, 106 | payload=transaction.payload, 107 | created_at=transaction.created_at, 108 | updated_at=transaction.updated_at, 109 | ) 110 | 111 | async def get_last_user_transactions( 112 | self, user_id: int, limit: int = 5 113 | ) -> list[TransactionDTO]: 114 | result = await self.session.execute( 115 | select(TransactionModel) 116 | .where(TransactionModel.user_id == user_id) 117 | .order_by(desc(TransactionModel.created_at)) 118 | .limit(limit) 119 | ) 120 | transactions = result.scalars().all() 121 | return [ 122 | TransactionDTO( 123 | id=tx.id, 124 | user_id=tx.user_id, 125 | amount=tx.amount, 126 | telegram_payment_charge_id=tx.telegram_payment_charge_id, 127 | status=tx.status, 128 | payload=tx.payload, 129 | created_at=tx.created_at, 130 | updated_at=tx.updated_at, 131 | ) 132 | for tx in transactions 133 | ] 134 | 135 | async def get_user_transactions_paginated( 136 | self, user_id: int, offset: int, limit: int 137 | ) -> list[TransactionDTO]: 138 | result = await self.session.scalars( 139 | select(TransactionModel) 140 | .where(TransactionModel.user_id == user_id) 141 | .order_by(desc(TransactionModel.created_at)) 142 | .offset(offset) 143 | .limit(limit) 144 | ) 145 | transactions = result.all() 146 | return [ 147 | TransactionDTO( 148 | id=tx.id, 149 | user_id=tx.user_id, 150 | amount=tx.amount, 151 | telegram_payment_charge_id=tx.telegram_payment_charge_id, 152 | status=tx.status, 153 | payload=tx.payload, 154 | created_at=tx.created_at, 155 | updated_at=tx.updated_at, 156 | ) 157 | for tx in transactions 158 | ] 159 | 160 | async def get_user_transactions_count(self, user_id: int) -> int: 161 | result = await self.session.scalar( 162 | select(func.count(TransactionModel.id)).where( 163 | TransactionModel.user_id == user_id 164 | ) 165 | ) 166 | return result if result else 0 167 | -------------------------------------------------------------------------------- /app/interfaces/telegram/handlers/buy_gift/buy_gift.py: -------------------------------------------------------------------------------- 1 | import aiohttp 2 | from aiogram import Router, types, F 3 | from aiogram.filters import StateFilter 4 | from aiogram.fsm.context import FSMContext 5 | 6 | from app.core.logger import logger 7 | from app.application.use_cases import PurchaseGift 8 | from app.infrastructure.db.repositories import TransactionRepository, UserRepository 9 | from app.infrastructure.db.session import get_db 10 | from app.infrastructure.services import TelegramGiftsApi 11 | from app.interfaces.telegram.keyboards.default import back_keyboard, main_menu_keyboard 12 | from app.interfaces.telegram.messages import BUTTONS, ERRORS, MESSAGES 13 | from app.interfaces.telegram.states.gift_state import GiftStates 14 | 15 | router = Router() 16 | 17 | 18 | @logger.catch 19 | @router.message( 20 | F.text.in_( 21 | [ 22 | BUTTONS["ru"]["buy_gift"], 23 | BUTTONS["en"]["buy_gift"], 24 | ] 25 | ) 26 | ) 27 | async def buy_gift_command(message: types.Message, state: FSMContext): 28 | async with aiohttp.ClientSession() as session: 29 | gifts_api = TelegramGiftsApi(session) 30 | gifts = await gifts_api.get_available_gifts() 31 | 32 | async with get_db() as db_session: 33 | user_repo = UserRepository(db_session) 34 | user = await user_repo.get_user_by_telegram_id(message.from_user.id) 35 | lang = user.language if user else "ru" 36 | 37 | if not gifts: 38 | await message.answer( 39 | MESSAGES[lang]["history_empty"], reply_markup=main_menu_keyboard(lang=lang) 40 | ) 41 | return 42 | 43 | gifts_sorted = sorted(gifts, key=lambda g: g.star_count, reverse=True) 44 | gift_descriptions = [ 45 | f'Подарок: {gift.emoji}\nID: {gift.gift_id}\nЦена: {gift.star_count}⭐️\nДоступно: {gift.remaining_count or "∞"}/{gift.total_count or "∞"}' 46 | for gift in gifts_sorted 47 | ] 48 | 49 | await message.answer("\n\n".join(gift_descriptions), parse_mode="HTML") 50 | await message.answer( 51 | text=MESSAGES[lang]["buy_gift_prompt"](message.from_user.id), 52 | reply_markup=back_keyboard(lang=lang), 53 | ) 54 | 55 | await state.set_state(GiftStates.waiting_for_gift_id) 56 | await state.update_data(prev_state=None) 57 | 58 | 59 | @logger.catch 60 | @router.message(StateFilter(GiftStates.waiting_for_gift_id)) 61 | async def process_gift_id_input(message: types.Message, state: FSMContext): 62 | async with get_db() as db_session: 63 | user_repo = UserRepository(db_session) 64 | user = await user_repo.get_user_by_telegram_id(message.from_user.id) 65 | lang = user.language if user else "ru" 66 | 67 | try: 68 | parts = message.text.split() 69 | if len(parts) != 3: 70 | await message.reply( 71 | MESSAGES[lang]["buy_gift_error_format"], 72 | reply_markup=back_keyboard(lang=lang), 73 | ) 74 | return 75 | gift_id, user_id, gifts_count = map(int, parts) 76 | except Exception: 77 | await message.reply( 78 | MESSAGES[lang]["buy_gift_error_numbers"], 79 | reply_markup=back_keyboard(lang=lang), 80 | ) 81 | return 82 | 83 | payload = f"gift_{gift_id}_to_{user_id}_count_{gifts_count}" 84 | 85 | async with get_db() as session, aiohttp.ClientSession() as http_session: 86 | user_repo = UserRepository(session) 87 | transaction_repo = TransactionRepository(session) 88 | gifts_api = TelegramGiftsApi(http_session) 89 | use_case = PurchaseGift(user_repo, transaction_repo, gifts_api) 90 | result = await use_case.execute( 91 | buyer_telegram_id=message.from_user.id, 92 | recipient_id=user_id, 93 | gift_id=gift_id, 94 | gifts_count=gifts_count, 95 | payload=payload, 96 | ) 97 | 98 | if not result["ok"]: 99 | err = result["error"] 100 | if err == "user_not_found": 101 | await message.reply( 102 | ERRORS[lang]["user_not_found"], 103 | reply_markup=back_keyboard(lang=lang), 104 | ) 105 | elif err == "gift_not_found": 106 | await message.reply( 107 | ERRORS[lang]["gift_not_found"], 108 | reply_markup=back_keyboard(lang=lang), 109 | ) 110 | elif err == "not_enough_balance": 111 | required = result["required"] 112 | price = result["gift_price"] 113 | prices = [ 114 | types.LabeledPrice( 115 | label=MESSAGES[lang]["deposit_success"](required), 116 | amount=required, 117 | ) 118 | ] 119 | await message.answer_invoice( 120 | title=MESSAGES[lang]["deposit_success"](required), 121 | description=f"Для покупки нужно {price * gifts_count}⭐️, у тебя {price * gifts_count - required}⭐️.", 122 | payload=payload, 123 | currency="XTR", 124 | prices=prices, 125 | provider_token="", 126 | reply_markup=None, 127 | ) 128 | await state.clear() 129 | 130 | elif err == "debit_failed": 131 | await message.reply( 132 | ERRORS[lang]["debit_failed"], reply_markup=back_keyboard(lang=lang) 133 | ) 134 | 135 | elif err == "gift_send_failed": 136 | await message.reply( 137 | ERRORS[lang]["gift_send_failed"], 138 | reply_markup=back_keyboard(lang=lang), 139 | ) 140 | 141 | elif err == "transaction_failed": 142 | await message.reply( 143 | ERRORS[lang]["transaction_failed"], 144 | reply_markup=back_keyboard(lang=lang), 145 | ) 146 | 147 | else: 148 | await message.reply( 149 | ERRORS[lang]["unknown"], reply_markup=back_keyboard(lang=lang) 150 | ) 151 | return 152 | 153 | user = result["user"] 154 | await message.reply( 155 | MESSAGES[lang]["buy_gift_success"](user.balance), 156 | reply_markup=main_menu_keyboard(lang=lang), 157 | ) 158 | await state.clear() 159 | -------------------------------------------------------------------------------- /app/application/use_cases/gifts/auto_buy_gifts.py: -------------------------------------------------------------------------------- 1 | import uuid 2 | import time 3 | import asyncio 4 | from typing import TYPE_CHECKING 5 | 6 | from aiogram import Bot 7 | 8 | from app.core.logger import logger 9 | from app.interfaces.repositories import ( 10 | IAutoBuySettingRepository, 11 | IGiftRepository, 12 | IUserRepository, 13 | ) 14 | from app.interfaces.services.gifts_service import IGiftsService 15 | 16 | from app.interfaces.telegram.messages import MESSAGES 17 | 18 | if TYPE_CHECKING: 19 | from app.application.use_cases.gifts.purchase_gift import PurchaseGift 20 | 21 | 22 | class AutoBuyGiftsForAllUsers: 23 | def __init__( 24 | self, 25 | user_repo: IUserRepository, 26 | auto_buy_repo: IAutoBuySettingRepository, 27 | gift_repo: IGiftRepository, 28 | gifts_service: IGiftsService, 29 | purchase_gift_uc: "PurchaseGift", 30 | transaction_repo, 31 | bot: Bot, 32 | ): 33 | self.user_repo = user_repo 34 | self.auto_buy_repo = auto_buy_repo 35 | self.gift_repo = gift_repo 36 | self.gifts_service = gifts_service 37 | self.purchase_gift_uc = purchase_gift_uc 38 | self.transaction_repo = transaction_repo 39 | self.bot = bot 40 | 41 | async def execute(self): 42 | logger.info("[UseCase:AutoBuyGifts] Старт автопокупки") 43 | start_time = time.monotonic() 44 | users_settings = ( 45 | await self.user_repo.get_all_with_auto_buy_enabled_and_settings() 46 | ) 47 | 48 | if not users_settings: 49 | logger.info( 50 | "[UseCase:AutoBuyGifts] Нет пользователей с включенной автопокупкой") 51 | await self.gift_repo.reset_new_gifts() 52 | return 53 | 54 | gifts = await self.gift_repo.get_new_gifts() 55 | 56 | await self.gift_repo.reset_new_gifts() 57 | 58 | logger.info( 59 | f"[UseCase:AutoBuyGifts] Пользователей: {len(users_settings)}, новых подарков: {len(gifts)}" 60 | ) 61 | 62 | if not gifts: 63 | logger.info("[UseCase:AutoBuyGifts] Нет новых подарков") 64 | return 65 | 66 | async def process_user(user, settings): 67 | suitable = [] 68 | not_suitable = [] 69 | not_enough_balance = [] 70 | 71 | for gift in gifts: 72 | if not ( 73 | settings.price_limit_from 74 | <= gift.star_count 75 | <= settings.price_limit_to 76 | ): 77 | not_suitable.append(gift) 78 | continue 79 | if ( 80 | settings.supply_limit 81 | and gift.total_count 82 | and gift.total_count > settings.supply_limit 83 | ): 84 | not_suitable.append(gift) 85 | continue 86 | if user.balance < gift.star_count: 87 | not_enough_balance.append(gift) 88 | continue 89 | suitable.append(gift) 90 | 91 | lang = getattr(user, "language", "ru") 92 | 93 | msg = f"🎁 Новые подарки! Всего: {len(gifts)}\n" 94 | 95 | if suitable: 96 | msg += f"{MESSAGES[lang].get('autobuy_suitable', 'Подходят по фильтрам')}: {len(suitable)}\n" 97 | for gift in suitable: 98 | msg += f"• {gift.emoji} ID: {gift.gift_id}, Цена: {gift.star_count}⭐️\n" 99 | 100 | if not_enough_balance: 101 | msg += f"{MESSAGES[lang].get('autobuy_no_balance', 'Недостаточно баланса')}: {len(not_enough_balance)}\n" 102 | for gift in not_enough_balance: 103 | msg += f"• {gift.emoji} ID: {gift.gift_id}, Цена: {gift.star_count}⭐️\n" 104 | 105 | if not_suitable: 106 | msg += f"{MESSAGES[lang].get('autobuy_not_suitable', 'Не подходят по фильтрам')}: {len(not_suitable)}\n" 107 | for gift in not_suitable: 108 | msg += f"• {gift.emoji} ID: {gift.gift_id}, Цена: {gift.star_count}⭐️\n" 109 | 110 | if suitable: 111 | msg += f"\n{MESSAGES[lang].get('autobuy_try_buy', 'Пробую купить подходящие подарки...')}" 112 | 113 | else: 114 | msg += f"\n{MESSAGES[lang].get('autobuy_no_suitable', 'Нет подходящих подарков для автопокупки.')}" 115 | 116 | try: 117 | await self.bot.send_message(user.telegram_id, msg) 118 | except Exception as e: 119 | logger.error( 120 | f"[AutoBuyGifts] Не удалось отправить уведомление {user.telegram_id}: {e}" 121 | ) 122 | 123 | for cycle in range(settings.cycles): 124 | for gift in suitable: 125 | logger.info( 126 | f"[AutoBuyGifts] Попытка купить подарок {gift.gift_id} для пользователя {user.telegram_id}, цикл {cycle}, баланс {user.balance}" 127 | ) 128 | payload = ( 129 | f"autobuy_{gift.gift_id}_to_{user.telegram_id}_cycle{cycle}" 130 | ) 131 | provider_charge_id = f"{payload}_{uuid.uuid4().hex}" 132 | result = await self.purchase_gift_uc.execute( 133 | buyer_telegram_id=user.telegram_id, 134 | recipient_id=user.telegram_id, 135 | gift_id=gift.gift_id, 136 | gifts_count=1, 137 | payload=payload, 138 | provider_charge_id=provider_charge_id, 139 | ) 140 | if result["ok"]: 141 | user = result["user"] 142 | try: 143 | await self.bot.send_message( 144 | user.telegram_id, 145 | f"✅ Куплен подарок {gift.emoji} (ID: {gift.gift_id}) за {gift.star_count}⭐️. Остаток: {result['user'].balance}⭐️.", 146 | ) 147 | except Exception as e: 148 | logger.error( 149 | f"[AutoBuyGifts] Не удалось отправить подтверждение покупки {user.telegram_id}: {e}" 150 | ) 151 | elif result.get("error_code") == "STARGIFT_USAGE_LIMITED": 152 | try: 153 | await self.bot.send_message( 154 | user.telegram_id, 155 | f"❌ Подарок {gift.emoji} (ID: {gift.gift_id}) временно недоступен для покупки (лимит использования). Будет предпринята попытка позже.", 156 | ) 157 | except Exception as e: 158 | logger.error( 159 | f"[AutoBuyGifts] Не удалось отправить ошибку лимита {user.telegram_id}: {e}" 160 | ) 161 | break 162 | else: 163 | try: 164 | await self.bot.send_message( 165 | user.telegram_id, 166 | f"❌ Не удалось купить подарок {gift.emoji} (ID: {gift.gift_id}): {result['error']}", 167 | ) 168 | except Exception as e: 169 | logger.error( 170 | f"[AutoBuyGifts] Не удалось отправить ошибку покупки {user.telegram_id}: {e}" 171 | ) 172 | 173 | await asyncio.gather( 174 | *(process_user(user, settings) for user, settings in users_settings) 175 | ) 176 | elapsed = time.monotonic() - start_time 177 | logger.info(f"[UseCase:AutoBuyGifts] Завершено за {elapsed:.2f} сек") 178 | -------------------------------------------------------------------------------- /app/infrastructure/db/repositories/user_repo_impl.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy import select 2 | from sqlalchemy.ext.asyncio import AsyncSession 3 | 4 | from app.core.logger import logger 5 | from app.domain.entities import UserDTO, AutoBuySettingDTO 6 | from app.infrastructure.db.models import UserModel 7 | from app.interfaces.repositories import IUserRepository 8 | 9 | 10 | class UserRepository(IUserRepository): 11 | def __init__(self, session: AsyncSession): 12 | self.session = session 13 | 14 | async def create( 15 | self, telegram_id: int, username: str, language: str = "ru" 16 | ) -> UserDTO: 17 | existing_user = await self.session.scalar( 18 | select(UserModel).where(UserModel.telegram_id == telegram_id) 19 | ) 20 | if existing_user: 21 | return UserDTO( 22 | id=existing_user.id, 23 | telegram_id=existing_user.telegram_id, 24 | username=existing_user.username, 25 | balance=existing_user.balance, 26 | role=existing_user.role, 27 | notifications_enabled=existing_user.notifications_enabled, 28 | language=existing_user.language, 29 | ) 30 | db_user = UserModel( 31 | telegram_id=telegram_id, username=username, language=language 32 | ) 33 | self.session.add(db_user) 34 | await self.session.flush() 35 | return UserDTO( 36 | id=db_user.id, 37 | telegram_id=db_user.telegram_id, 38 | username=db_user.username, 39 | balance=db_user.balance, 40 | role=db_user.role, 41 | notifications_enabled=db_user.notifications_enabled, 42 | language=db_user.language, 43 | ) 44 | 45 | async def get_user_by_telegram_id(self, telegram_id: int) -> UserDTO | None: 46 | existing_user = await self.session.scalar( 47 | select(UserModel).where(UserModel.telegram_id == telegram_id) 48 | ) 49 | if not existing_user: 50 | return None 51 | return UserDTO( 52 | id=existing_user.id, 53 | telegram_id=existing_user.telegram_id, 54 | username=existing_user.username, 55 | balance=existing_user.balance, 56 | role=existing_user.role, 57 | notifications_enabled=existing_user.notifications_enabled, 58 | language=existing_user.language, 59 | ) 60 | 61 | async def get_user_by_id(self, user_id: int) -> UserDTO | None: 62 | existing_user = await self.session.scalar( 63 | select(UserModel).where(UserModel.id == user_id) 64 | ) 65 | if not existing_user: 66 | return None 67 | return UserDTO( 68 | id=existing_user.id, 69 | telegram_id=existing_user.telegram_id, 70 | username=existing_user.username, 71 | balance=existing_user.balance, 72 | role=existing_user.role, 73 | notifications_enabled=existing_user.notifications_enabled, 74 | language=existing_user.language, 75 | ) 76 | 77 | async def credit_user_balance( 78 | self, telegram_id: int, amount: int 79 | ) -> UserDTO | None: 80 | existing_user = await self.session.scalar( 81 | select(UserModel).where(UserModel.telegram_id == telegram_id) 82 | ) 83 | 84 | if not existing_user: 85 | return None 86 | 87 | existing_user.balance += amount 88 | return UserDTO( 89 | id=existing_user.id, 90 | telegram_id=existing_user.telegram_id, 91 | username=existing_user.username, 92 | balance=existing_user.balance, 93 | role=existing_user.role, 94 | notifications_enabled=existing_user.notifications_enabled, 95 | language=existing_user.language, 96 | ) 97 | 98 | async def debit_user_balance(self, telegram_id: int, amount: int) -> UserDTO | None: 99 | existing_user = await self.session.scalar( 100 | select(UserModel).where(UserModel.telegram_id == telegram_id) 101 | ) 102 | 103 | if not existing_user: 104 | return None 105 | 106 | if existing_user.balance < amount: 107 | return None 108 | 109 | existing_user.balance -= amount 110 | return UserDTO( 111 | id=existing_user.id, 112 | telegram_id=existing_user.telegram_id, 113 | username=existing_user.username, 114 | balance=existing_user.balance, 115 | role=existing_user.role, 116 | notifications_enabled=existing_user.notifications_enabled, 117 | language=existing_user.language, 118 | ) 119 | 120 | async def get_all_with_auto_buy_enabled(self) -> list[UserDTO]: 121 | from app.infrastructure.db.models.auto_buy_setting import AutoBuySettingModel 122 | 123 | result = await self.session.scalars( 124 | select(UserModel) 125 | .join(AutoBuySettingModel, UserModel.id == AutoBuySettingModel.user_id) 126 | .where(AutoBuySettingModel.status) 127 | ) 128 | users = result.all() 129 | logger.info( 130 | f"[UserRepo] Пользователей с автопокупкой (всего): {len(users)}") 131 | return [ 132 | UserDTO( 133 | id=u.id, 134 | telegram_id=u.telegram_id, 135 | username=u.username, 136 | balance=u.balance, 137 | role=u.role, 138 | notifications_enabled=u.notifications_enabled, 139 | language=u.language, 140 | ) 141 | for u in users 142 | ] 143 | 144 | async def get_all_with_auto_buy_enabled_and_settings( 145 | self, 146 | ) -> list[tuple[UserDTO, AutoBuySettingDTO]]: 147 | from app.infrastructure.db.models.auto_buy_setting import AutoBuySettingModel 148 | from app.domain.entities.auto_buy_setting import AutoBuySettingDTO 149 | 150 | result = await self.session.execute( 151 | select(UserModel, AutoBuySettingModel) 152 | .join(AutoBuySettingModel, UserModel.id == AutoBuySettingModel.user_id) 153 | .where(AutoBuySettingModel.status) 154 | ) 155 | rows = result.all() 156 | users_settings = [] 157 | for user, setting in rows: 158 | users_settings.append( 159 | ( 160 | UserDTO( 161 | id=user.id, 162 | telegram_id=user.telegram_id, 163 | username=user.username, 164 | balance=user.balance, 165 | role=user.role, 166 | notifications_enabled=user.notifications_enabled, 167 | language=user.language, 168 | ), 169 | AutoBuySettingDTO( 170 | id=setting.id, 171 | user_id=setting.user_id, 172 | status=setting.status, 173 | price_limit_from=setting.price_limit_from, 174 | price_limit_to=setting.price_limit_to, 175 | supply_limit=setting.supply_limit, 176 | cycles=setting.cycles, 177 | ), 178 | ) 179 | ) 180 | return users_settings 181 | 182 | async def get_notifications_enabled(self, telegram_id: int) -> bool: 183 | user = await self.session.scalar( 184 | select(UserModel).where(UserModel.telegram_id == telegram_id) 185 | ) 186 | if not user: 187 | return True 188 | return user.notifications_enabled 189 | 190 | async def set_notifications_enabled(self, telegram_id: int, enabled: bool) -> None: 191 | user = await self.session.scalar( 192 | select(UserModel).where(UserModel.telegram_id == telegram_id) 193 | ) 194 | if user: 195 | user.notifications_enabled = enabled 196 | 197 | async def set_language(self, telegram_id: int, lang: str) -> None: 198 | user = await self.session.scalar( 199 | select(UserModel).where(UserModel.telegram_id == telegram_id) 200 | ) 201 | if user: 202 | user.language = lang 203 | 204 | async def get_language(self, telegram_id: int) -> str: 205 | user = await self.session.scalar( 206 | select(UserModel).where(UserModel.telegram_id == telegram_id) 207 | ) 208 | return user.language if user else "ru" 209 | -------------------------------------------------------------------------------- /app/interfaces/telegram/handlers/auto_buy/auto_buy.py: -------------------------------------------------------------------------------- 1 | from aiogram import Router, types, F 2 | from aiogram.filters import StateFilter 3 | from aiogram.fsm.context import FSMContext 4 | 5 | from app.application.use_cases import ( 6 | GetOrCreateAutoBuySetting, 7 | GetUserByTelegramId, 8 | UpdateAutoBuySetting, 9 | ) 10 | from app.core.logger import logger 11 | from app.infrastructure.db.repositories import AutoBuySettingReporistory, UserRepository 12 | from app.infrastructure.db.session import get_db 13 | from app.interfaces.telegram.keyboards.default import ( 14 | auto_buy_keyboard, 15 | back_keyboard, 16 | main_menu_keyboard, 17 | ) 18 | from app.interfaces.telegram.messages import BUTTONS, ERRORS, MESSAGES 19 | from app.interfaces.telegram.states.auto_buy_state import AutoBuyStates 20 | 21 | router = Router() 22 | 23 | 24 | @logger.catch 25 | @router.message( 26 | F.text.in_( 27 | [ 28 | BUTTONS["ru"]["auto_buy"], 29 | BUTTONS["en"]["auto_buy"], 30 | ] 31 | ) 32 | ) 33 | async def auto_buy_command(message: types.Message, state: FSMContext): 34 | async with get_db() as session: 35 | user_repo = UserRepository(session) 36 | auto_buy_repo = AutoBuySettingReporistory(session) 37 | user = await GetUserByTelegramId(user_repo).execute(message.from_user.id) 38 | lang = user.language if user else "ru" 39 | if not user: 40 | await message.answer( 41 | ERRORS[lang]["user_not_found"], 42 | reply_markup=main_menu_keyboard(lang=lang), 43 | ) 44 | return 45 | settings = await GetOrCreateAutoBuySetting(auto_buy_repo).execute( 46 | message.from_user.id, user.id 47 | ) 48 | await message.answer( 49 | text=MESSAGES[lang]["auto_buy_settings"](user, settings), 50 | reply_markup=auto_buy_keyboard(lang=lang), 51 | parse_mode="HTML", 52 | ) 53 | await state.set_state(AutoBuyStates.menu) 54 | await state.update_data(prev_state=None) 55 | 56 | 57 | @logger.catch 58 | @router.message(StateFilter(AutoBuyStates.menu)) 59 | async def auto_buy_menu_handler(message: types.Message, state: FSMContext): 60 | async with get_db() as session: 61 | user_repo = UserRepository(session) 62 | auto_buy_repo = AutoBuySettingReporistory(session) 63 | user = await GetUserByTelegramId(user_repo).execute(message.from_user.id) 64 | lang = user.language if user else "ru" 65 | if not user: 66 | await message.answer( 67 | ERRORS[lang]["user_not_found"], 68 | reply_markup=main_menu_keyboard(lang=lang), 69 | ) 70 | await state.clear() 71 | return 72 | settings = await GetOrCreateAutoBuySetting(auto_buy_repo).execute( 73 | message.from_user.id, user.id 74 | ) 75 | if message.text in ["🔄 Вкл/Выкл автопокупку", "🔄 Toggle auto-buy"]: 76 | updated = await UpdateAutoBuySetting(auto_buy_repo).execute( 77 | message.from_user.id, status=not settings.status 78 | ) 79 | await message.answer( 80 | f"{MESSAGES[lang]['auto_buy_status'](updated.status)}\n\n{MESSAGES[lang]['auto_buy_settings'](user, updated)}", 81 | reply_markup=auto_buy_keyboard(lang=lang), 82 | parse_mode="HTML", 83 | ) 84 | await state.update_data(prev_state=None) 85 | return 86 | elif message.text in ["✏️ Лимит цены", "✏️ Price limit"]: 87 | await message.answer( 88 | text=MESSAGES[lang]["auto_buy_price_prompt"], 89 | reply_markup=back_keyboard(lang=lang), 90 | parse_mode="HTML", 91 | ) 92 | await state.update_data(prev_state=AutoBuyStates.menu.state) 93 | await state.set_state(AutoBuyStates.set_price) 94 | return 95 | elif message.text in ["✏️ Лимит количества", "✏️ Supply limit"]: 96 | await message.answer( 97 | text=MESSAGES[lang]["auto_buy_supply_prompt"], 98 | reply_markup=back_keyboard(lang=lang), 99 | parse_mode="HTML", 100 | ) 101 | await state.update_data(prev_state=AutoBuyStates.menu.state) 102 | await state.set_state(AutoBuyStates.set_supply) 103 | return 104 | elif message.text in ["✏️ Кол-во циклов", "✏️ Cycles"]: 105 | await message.answer( 106 | text=MESSAGES[lang]["auto_buy_cycles_prompt"], 107 | reply_markup=back_keyboard(lang=lang), 108 | parse_mode="HTML", 109 | ) 110 | await state.update_data(prev_state=AutoBuyStates.menu.state) 111 | await state.set_state(AutoBuyStates.set_cycles) 112 | return 113 | await message.answer( 114 | text=MESSAGES[lang]["auto_buy_settings"](user, settings), 115 | reply_markup=auto_buy_keyboard(lang=lang), 116 | parse_mode="HTML", 117 | ) 118 | await state.update_data(prev_state=None) 119 | 120 | 121 | @logger.catch 122 | @router.message(StateFilter(AutoBuyStates.set_price)) 123 | async def auto_buy_set_price_handler(message: types.Message, state: FSMContext): 124 | async with get_db() as session: 125 | user_repo = UserRepository(session) 126 | user = await GetUserByTelegramId(user_repo).execute(message.from_user.id) 127 | lang = user.language if user else "ru" 128 | try: 129 | price_limits = message.text.split() 130 | if len(price_limits) != 2: 131 | raise ValueError 132 | price_from, price_to = map(int, price_limits) 133 | except Exception: 134 | await message.answer( 135 | text=MESSAGES[lang]["auto_buy_price_error"], 136 | reply_markup=back_keyboard(lang=lang), 137 | ) 138 | return 139 | async with get_db() as session: 140 | auto_buy_repo = AutoBuySettingReporistory(session) 141 | updated = await UpdateAutoBuySetting(auto_buy_repo).execute( 142 | message.from_user.id, price_limit_from=price_from, price_limit_to=price_to 143 | ) 144 | await message.answer( 145 | f"{MESSAGES[lang]['auto_buy_price_set'](price_from, price_to)}\n\n{MESSAGES[lang]['auto_buy_settings'](user, updated)}", 146 | reply_markup=auto_buy_keyboard(lang=lang), 147 | parse_mode="HTML", 148 | ) 149 | await state.set_state(AutoBuyStates.menu) 150 | await state.update_data(prev_state=None) 151 | 152 | 153 | @logger.catch 154 | @router.message(StateFilter(AutoBuyStates.set_supply)) 155 | async def auto_buy_set_supply_handler(message: types.Message, state: FSMContext): 156 | async with get_db() as session: 157 | user_repo = UserRepository(session) 158 | user = await GetUserByTelegramId(user_repo).execute(message.from_user.id) 159 | lang = user.language if user else "ru" 160 | try: 161 | supply_limit = int(message.text) 162 | if supply_limit <= 0: 163 | raise ValueError 164 | except Exception: 165 | await message.answer( 166 | text=MESSAGES[lang]["auto_buy_supply_error"], 167 | reply_markup=back_keyboard(lang=lang), 168 | ) 169 | return 170 | async with get_db() as session: 171 | auto_buy_repo = AutoBuySettingReporistory(session) 172 | updated = await UpdateAutoBuySetting(auto_buy_repo).execute( 173 | message.from_user.id, supply_limit=supply_limit 174 | ) 175 | await message.answer( 176 | f"{MESSAGES[lang]['auto_buy_supply_set'](supply_limit)}\n\n{MESSAGES[lang]['auto_buy_settings'](user, updated)}", 177 | reply_markup=auto_buy_keyboard(lang=lang), 178 | parse_mode="HTML", 179 | ) 180 | await state.set_state(AutoBuyStates.menu) 181 | await state.update_data(prev_state=None) 182 | 183 | 184 | @logger.catch 185 | @router.message(StateFilter(AutoBuyStates.set_cycles)) 186 | async def auto_buy_set_cycles_handler(message: types.Message, state: FSMContext): 187 | async with get_db() as session: 188 | user_repo = UserRepository(session) 189 | user = await GetUserByTelegramId(user_repo).execute(message.from_user.id) 190 | lang = user.language if user else "ru" 191 | try: 192 | cycles = int(message.text) 193 | if cycles <= 0: 194 | raise ValueError 195 | except Exception: 196 | await message.answer( 197 | text=MESSAGES[lang]["auto_buy_cycles_error"], 198 | reply_markup=back_keyboard(lang=lang), 199 | ) 200 | return 201 | async with get_db() as session: 202 | auto_buy_repo = AutoBuySettingReporistory(session) 203 | updated = await UpdateAutoBuySetting(auto_buy_repo).execute( 204 | message.from_user.id, cycles=cycles 205 | ) 206 | await message.answer( 207 | f"{MESSAGES[lang]['auto_buy_cycles_set'](cycles)}\n\n{MESSAGES[lang]['auto_buy_settings'](user, updated)}", 208 | reply_markup=auto_buy_keyboard(lang=lang), 209 | parse_mode="HTML", 210 | ) 211 | await state.set_state(AutoBuyStates.menu) 212 | await state.update_data(prev_state=None) 213 | -------------------------------------------------------------------------------- /ReadmeEN.md: -------------------------------------------------------------------------------- 1 | # 🚀 Telegram GiftBuyer Bot 2 | 3 | [![Python 3.12](https://img.shields.io/badge/python-3.12.10-blue.svg)](https://www.python.org/downloads/release/python-3120/) 4 | [![License: MIT](https://img.shields.io/badge/license-MIT-green.svg)](LICENSE) 5 | 6 | > Для использованя RU документации, откройте Readme.md 7 | > Automatic and manual purchasing of Telegram gifts based on **Aiogram**. 8 | > Implemented using **Clean Architecture** principles. 9 | 10 | --- 11 | 12 | ## ❗️ Disclaimer 13 | 14 | The **official bot** for this repository is **not ready yet**. Any bot using this codebase **is not official**. Use at your own risk. 15 | 16 | **Sending gifts to Telegram *channels* is not supported.** 17 | 18 | Because Telegram keeps throttling the Bot API whenever new gifts are released, this project’s capabilities may be limited in some situations. Unfortunately, user‑bot solutions are also blocked from time to time. There is no universal alternative at the moment — you will need to search and test on your own. 19 | 20 | --- 21 | 22 | ## 💖 Support / Donations 23 | 24 | | **Network** | **Token** | **Address / Tag** | 25 | |------------------------|-----------|-----------------------------------------------------------------------------------------------------| 26 | | 🟥 **TRON (TRC20)** | USDT | `TPoRfLVf4bYhZhcqLaY1UXneWD7FsP8n9U` | 27 | | 🌐 **TON** | ANY_TOKEN | `EQBVXzBT4lcTA3S7gxrg4hnl5fnsDKj4oNEzNp09aQxkwj1f`
**TAG**: `845065` | 28 | | 🪐 **Solana** | USDT | `6Y5Ke1iudDqSVFMD6iRw6rVNaG61oeiMhuNfpz4tXWb8` | 29 | | 🐬 **BSC (BEP20)** | USDT | `0xe2ea80596e8d2cca8353c0c54753b15f03d11a4b` | 30 | | 🏗️ **Polygon** | USDT | `0xe2ea80596e8d2cca8353c0c54753b15f03d11a4b` | 31 | | 🔥 **Ethereum (ERC20)** | USDT | `0xe2ea80596e8d2cca8353c0c54753b15f03d11a4b` | 32 | 33 | 🙏 **Thank you for your support!** 34 | 35 | --- 36 | 37 | ## 📋 Table of Contents 38 | 39 | - [✨ Features](#-features) 40 | - [🛠 Tech Stack](#-tech-stack) 41 | - [📂 Project Structure](#-project-structure) 42 | - [⚙️ Environment Setup](#️-environment-setup) 43 | - [⬇️ Installation](#️-installation) 44 | - [🔧 Configuration](#-configuration) 45 | - [⚙️ Auto‑Buy Settings](#️-auto-buy-settings) 46 | - [▶️ Running](#️-running) 47 | - [📱 Commands & Buttons](#-commands--buttons) 48 | - [💡 Usage Examples](#-usage-examples) 49 | - [🌐 Localisation](#-localisation) 50 | - [👤 Author](#-author) 51 | - [📝 Licence](#-licence) 52 | 53 | --- 54 | 55 | ## ✨ Features 56 | 57 | - **💳 Deposit** — top‑up your balance 58 | - **🎁 Manual purchase** — specify Gift ID, Telegram ID and quantity 59 | - **🤖 Auto‑buy** — automatically scan and buy gifts using filters 60 | - **🕓 History** — full list of transactions 61 | - **🔕 Do Not Disturb** — enable/disable new‑gift notifications 62 | 63 | --- 64 | 65 | ## 🛠 Tech Stack 66 | 67 | - **Python** 3.12.10 68 | - **Aiogram** 3.21.0 69 | - **aiohttp** 3.12.14 70 | - **aiosqlite** 0.21.0 71 | - **APScheduler** 3.11.0 72 | - **Loguru** 0.7.3 73 | - **Pydantic** 2.11.7 + **pydantic‑settings** 2.10.1 74 | - **SQLAlchemy** 2.0.41 75 | 76 | --- 77 | 78 | ## 📂 Project Structure 79 | 80 | ```text 81 | app/ 82 | ├── application/ # use_cases: business logic 83 | │ └── use_cases/ 84 | ├── core/ # configuration & logging 85 | │ ├── config.py 86 | │ └── logger.py 87 | ├── domain/ # domain entities & DTOs 88 | │ └── entities/ 89 | ├── infrastructure/ # integrations & storage 90 | │ ├── db/ # SQLAlchemy + SQLite 91 | │ ├── scheduler/ # APScheduler 92 | │ ├── services/ # HTTP clients, external APIs 93 | │ └── telegram/ # low‑level Telegram API 94 | ├── interfaces/ # repository & service abstractions 95 | │ ├── repositories/ 96 | │ ├── services/ 97 | │ └── telegram/ # bot logic 98 | └── main.py # entry point (python -m app.main) 99 | ``` 100 | 101 | --- 102 | 103 | ## ⚙️ Environment Setup 104 | 105 | 1. Install **Python 3.12.10** 106 | 2. Clone the repository and `cd` into it 107 | 3. Create a `.env` file in the root: 108 | 109 | | Variable | Description | 110 | |-------------------------------|-----------------------------------------------| 111 | | `BOT_TOKEN` | Your bot token (no quotes) | 112 | | `DATABASE_URL` | `sqlite+aiosqlite:///user_data.db` | 113 | | `CHECK_GIFTS_DELAY_SECONDS` | Gift‑scan interval (seconds) | 114 | 115 | ## EXAMPLE: 116 | BOT_TOKEN=**YOUR_TOKEN** 117 | DATABASE_URL=**sqlite+aiosqlite:///user_data.db** 118 | CHECK_GIFTS_DELAY_SECONDS=**3** 119 | 120 | --- 121 | 122 | ## ⬇️ Installation 123 | 124 | **Youtube video, how to install repository to your pc RU: [Click](https://www.youtube.com/watch?v=30aBa2Z8fnA)** 125 | 126 | ```bash 127 | # If you are not familiar with Git: 128 | # 1. Download the ZIP archive: 129 | # https://github.com/neverwasbored/TgGiftBuyerBot/archive/refs/heads/main.zip 130 | # 2. Extract it: 131 | # Windows: Right click → “Extract All…” 132 | # Linux/macOS: unzip main.zip 133 | # 3. Move into the folder: 134 | # cd TgGiftBuyerBot-main 135 | 136 | # Or clone the repository: 137 | git clone https://github.com/neverwasbored/TgGiftBuyerBot.git 138 | cd TgGiftBuyerBot 139 | 140 | # Quick Start! 141 | Run `setup_and_run` for your system! 142 | 143 | # Create virtual environment (Windows) 144 | python -m venv venv 145 | venv\Scripts\activate 146 | 147 | # Or (Linux/macOS) 148 | python3.12 -m venv venv 149 | source venv/bin/activate 150 | 151 | # Install dependencies 152 | pip install -r requirements.txt 153 | ``` 154 | 155 | --- 156 | 157 | ## 🔧 Configuration 158 | 159 | - **No migrations are used** — tables are created automatically on first run. 160 | - In `.env` provide **only** your `BOT_TOKEN`; the defaults are fine for the rest. 161 | 162 | --- 163 | 164 | ## 🖥️ Local Run Recommended 165 | 166 | For security, privacy and easier debugging **it is recommended to run the bot locally** on your own computer. 167 | 168 | --- 169 | 170 | ## ⚙️ Auto‑Buy Settings 171 | 172 | Auto‑buy lets the bot find and purchase new gifts that match your criteria — no extra commands needed! 173 | 174 | ### Parameters 175 | 176 | - **Price (from / to)** 177 | Define a price range in stars. The bot will consider only gifts whose price falls within this range. 178 | 179 | - **Supply** 180 | Total supply of the gift. The bot will consider only gifts with the specified supply. 181 | 182 | - **Auto‑buy cycles** 183 | Number of passes through the list of new gifts. 184 | Example: with 3 new gifts, where 2 fit the filters, and 2 cycles, the bot will make 4 purchases (2x2), as long as there is enough balance. 185 | 186 | --- 187 | 188 | ## ▶️ Running 189 | 190 | ```bash 191 | # With the virtual environment activated 192 | python -m app.main 193 | ``` 194 | 195 | --- 196 | 197 | ## 📱 Commands & Buttons 198 | 199 | ### Commands 200 | 201 | | Command | Description | 202 | |-----------|------------------| 203 | | `/start` | Start the bot | 204 | | `/help` | Help & FAQ | 205 | 206 | ### Menu Buttons 207 | 208 | | Item | Action | 209 | |---------------------------|-------------------------------------------------------------------------------------------------------------| 210 | | 💳 **Deposit** | Top‑up balance | 211 | | 🎁 **Buy Gift** | Manual purchase (input: `GIFT_ID, Telegram_ID, quantity`) | 212 | | 🤖 **Auto‑Buy** | Enable/disable auto‑buy; set filters:
– Price from/to
– Supply
– Auto‑buy cycles | 213 | | 🕓 **History** | Show all transactions | 214 | | 🔕 **Do Not Disturb** | Toggle new‑gift notifications | 215 | 216 | --- 217 | 218 | ## 💡 Usage Examples 219 | 220 | 1. **Start** the bot: 221 | ```text 222 | /start 223 | ``` 224 | 2. **Manual purchase**: press 🎁 and enter, for example: 225 | ``` 226 | 12345, 67890, 2 227 | ``` 228 | 3. **Auto‑buy**: press 🤖, set: 229 | - Price: 50 – 200 stars 230 | - Supply: 10 231 | - Cycles: 3 232 | 4. **View history**: press 🕓 233 | 5. **Notification control**: 🔕 “Do Not Disturb” 234 | 235 | --- 236 | 237 | ## 🌐 Localisation 238 | 239 | Supported languages: **Russian** and **English**. 240 | The language is detected automatically from your Telegram settings or via `/help`. 241 | 242 | --- 243 | 244 | ## 🔄 `/refund` Command 245 | 246 | To use the `/refund` command: 247 | 248 | 1. Open the database (`user_data.db` in the project root). My choice is Sqlite Browser (A database editing program) 249 | 2. In the `users` table change your account’s `status` from `USER` to `ADMIN`. 250 | 3. Open the `transactions` table and copy the `telegram_payment_charge_id` of the desired transaction. 251 | 4. Return to the bot and execute: 252 | ``` 253 | /refund 254 | ``` 255 | 256 | ⚠️ **Important:** the refund **will fail** if the bot’s balance in stars is less than the amount in that transaction. 257 | 258 | Alternative: withdraw funds via the Telegram bot. You need **at least 1000 stars** on balance to withdraw. 259 | 260 | --- 261 | 262 | ## 👤 Author 263 | 264 | [neverwasbored](https://github.com/neverwasbored) 265 | 266 | --- 267 | 268 | ## 📝 Licence 269 | 270 | This project is licensed under the **MIT License**. 271 | -------------------------------------------------------------------------------- /Readme.md: -------------------------------------------------------------------------------- 1 | # 🚀 Telegram GiftBuyer Bot 2 | 3 | [![Python 3.12](https://img.shields.io/badge/python-3.12.10-blue.svg)](https://www.python.org/downloads/release/python-3120/) 4 | [![License: MIT](https://img.shields.io/badge/license-MIT-green.svg)](LICENSE) 5 | 6 | > For use EN docs, open ReadmeEN.md 7 | > Автоматическая и ручная покупка подарков в Telegram на основе Aiogram. 8 | > Реализовано по принципам **Clean Architecture**. 9 | 10 | --- 11 | 12 | ## ❗️ Дисклеймер 13 | 14 | Официальный бот репозитория **ещё не готов**. Все боты, использующие этот репозиторий, **не являются официальными**. Используйте на свой страх и риск. 15 | 16 | **Отправка подарков в Telegram‑каналы не поддерживается.** 17 | 18 | Из-за новых попыток Telegram ограничивать работу Bot API при выпуске новых подарков, возможности этого репозитория могут быть ограничены в некоторых ситуациях. К сожалению, user-bot решения также периодически блокируются. Универсальных альтернатив у меня на данный момент нет — придётся искать и тестировать их самостоятельно. 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 | | 🟥 **TRON (TRC20)** | USDT | `TPoRfLVf4bYhZhcqLaY1UXneWD7FsP8n9U` | 45 | | 🌐 **TON** | ANY_TOKEN | `EQBVXzBT4lcTA3S7gxrg4hnl5fnsDKj4oNEzNp09aQxkwj1f`
**ТЕГ**: `845065` | 46 | | 🪐 **Solana** | USDT | `6Y5Ke1iudDqSVFMD6iRw6rVNaG61oeiMhuNfpz4tXWb8` | 47 | | 🐬 **BSC (BEP20)** | USDT | `0xe2ea80596e8d2cca8353c0c54753b15f03d11a4b` | 48 | | 🏗️ **Polygon** | USDT | `0xe2ea80596e8d2cca8353c0c54753b15f03d11a4b` | 49 | | 🔥 **Ethereum (ERC20)** | USDT | `0xe2ea80596e8d2cca8353c0c54753b15f03d11a4b` | 50 | 51 | 🙏 **Спасибо за вашу поддержку!** 52 | 53 | --- 54 | 55 | ## ✨ Особенности 56 | 57 | - **💳 Депозит** — пополнение баланса 58 | - **🎁 Ручная покупка** — указать ID подарка, Telegram ID и количество 59 | - **🤖 Автопокупка** — автоматический скан и покупка по фильтрам 60 | - **🕓 История** — полный список транзакций 61 | - **🔕 Не беспокоить** — вкл/выкл уведомления о новых подарках 62 | 63 | --- 64 | 65 | ## 🛠 Технологии 66 | 67 | - **Python** 3.12.10 68 | - **Aiogram** 3.21.0 69 | - **aiohttp** 3.12.14 70 | - **aiosqlite** 0.21.0 71 | - **APScheduler** 3.11.0 72 | - **Loguru** 0.7.3 73 | - **Pydantic** 2.11.7 + **pydantic‑settings** 2.10.1 74 | - **SQLAlchemy** 2.0.41 75 | 76 | --- 77 | 78 | ## 📂 Структура проекта 79 | 80 | ```text 81 | app/ 82 | ├── application/ # use_cases: реализация бизнес-логики 83 | │ └── use_cases/ 84 | ├── core/ # конфигурация и логирование 85 | │ ├── config.py 86 | │ └── logger.py 87 | ├── domain/ # доменные сущности и DTO 88 | │ └── entities/ 89 | ├── infrastructure/ # интеграции и хранилища 90 | │ ├── db/ # SQLAlchemy + SQLite 91 | │ ├── scheduler/ # APScheduler 92 | │ ├── services/ # HTTP‑клиенты, внешние API 93 | │ └── telegram/ # низкоуровневое Tele­gram API 94 | ├── interfaces/ # абстракции репозиториев и сервисов 95 | │ ├── repositories/ 96 | │ ├── services/ 97 | │ └── telegram/ # логика бота 98 | └── main.py # точка входа (python -m app.main) 99 | ``` 100 | 101 | --- 102 | 103 | ## ⚙️ Настройка окружения 104 | 105 | 1. Установите **Python 3.12.10** 106 | 2. Клонируйте репозиторий и перейдите в него 107 | 3. Создайте файл `.env` в корне: 108 | 109 | | Переменная | Описание | 110 | |------------------------------|-------------------------------------| 111 | | `BOT_TOKEN` | Токен вашего бота (без кавычек) | 112 | | `DATABASE_URL` | `sqlite+aiosqlite:///user_data.db` | 113 | | `CHECK_GIFTS_DELAY_SECONDS` | Интервал проверки (в секундах) | 114 | 115 | ## ПРИМЕР: 116 | BOT_TOKEN=**YOUR_TOKEN** 117 | DATABASE_URL=**sqlite+aiosqlite:///user_data.db** 118 | CHECK_GIFTS_DELAY_SECONDS=**3** 119 | 120 | --- 121 | 122 | ## ⬇️ Установка 123 | 124 | **Видео по установке репозитория: [Клик](https://www.youtube.com/watch?v=30aBa2Z8fnA)** 125 | 126 | ```bash 127 | # Если не знакомы с Git: 128 | # 1. Скачайте ZIP-архив: 129 | # https://github.com/neverwasbored/TgGiftBuyerBot/archive/refs/heads/main.zip 130 | # 2. Распакуйте: 131 | # Windows: ПКМ → "Извлечь все" 132 | # Linux/macOS: unzip main.zip 133 | # 3. Перейдите в папку: 134 | # cd TgGiftBuyerBot-main 135 | 136 | # Или клонировать репозиторий: 137 | git clone https://github.com/neverwasbored/TgGiftBuyerBot.git 138 | cd TgGiftBuyerBot 139 | 140 | # Быстрый старт! 141 | Запустите setup_and_run для вашей системы! 142 | 143 | # Создать виртуальное окружение (Windows) 144 | python -m venv venv 145 | venv\Scripts\activate 146 | 147 | # Или (Linux/macOS) 148 | python3.12 -m venv venv 149 | source venv/bin/activate 150 | 151 | # Установить зависимости 152 | pip install -r requirements.txt 153 | ``` 154 | 155 | --- 156 | 157 | ## 🔧 Конфигурация 158 | 159 | - Миграции **не используются** — таблицы создаются автоматически при первом запуске. 160 | - В `.env` указывайте **только** свой `BOT_TOKEN`; остальные параметры подходят по умолчанию. 161 | 162 | --- 163 | 164 | ## 🖥️ Рекомендуется запускать локально 165 | 166 | Для обеспечения безопасности, конфиденциальности и удобства отладки **рекомендуется запускать бота локально** на вашем компьютере. 167 | 168 | ## ⚙️ Настройки автопокупки 169 | 170 | Автопокупка позволяет боту самостоятельно искать и приобретать новые подарки по вашим критериям — без лишних запросов! 171 | 172 | ### Параметры автопокупки 173 | 174 | - **Цена (от / до)** 175 | Задайте диапазон цены в звёздах. Бот будет рассматривать только подарки, цена которых попадает в этот интервал. 176 | 177 | - **Supply** 178 | Общее количество выпущенных подарков (supply). Бот будет учитывать только подарки с указанным supply. 179 | 180 | - **Циклы автопокупки** 181 | Количество «проходов» по списку новых подарков. 182 | Например, при 3 новых подарках, где 2 подходят под фильтры, и 2 циклах бот совершит 4 покупки (2×2), пока хватает баланса. 183 | 184 | --- 185 | 186 | ## ▶️ Запуск 187 | 188 | ```bash 189 | # При активном виртуальном окружении 190 | python -m app.main 191 | ``` 192 | 193 | --- 194 | 195 | ## 📱 Команды и кнопки 196 | 197 | ### Команды 198 | 199 | | Команда | Описание | 200 | |----------|------------------| 201 | | `/start` | Запустить бота | 202 | | `/help` | Справка и помощь | 203 | 204 | ### Кнопки меню 205 | 206 | | Элемент | Действие | 207 | |--------------------------|--------------------------------------------------------------------------------------------------------------| 208 | | 💳 **Депозит** | Пополнить баланс | 209 | | 🎁 **Купить подарок** | Ручная покупка (ввод: `ID_подарка, Telegram_ID, количество`) | 210 | | 🤖 **Автопокупка** | Вкл/выкл автопокупку; задаются фильтры:
– Цена от/до
– Supply
– Количество циклов автопокупки | 211 | | 🕓 **История** | Показать список всех транзакций | 212 | | 🔕 **Не беспокоить** | Отключить/включить уведомления о новых подарках | 213 | 214 | --- 215 | 216 | ## 💡 Примеры использования 217 | 218 | 1. **Запуск** бота: 219 | ```text 220 | /start 221 | ``` 222 | 2. **Ручная покупка**: нажмите 🎁 и введите, например: 223 | ``` 224 | 12345, 67890, 2 225 | ``` 226 | 3. **Автопокупка**: нажмите 🤖, установите: 227 | - Цена: от 50 до 200 звёзд 228 | - Supply: 10 229 | - Циклы: 3 230 | 4. **Просмотр истории**: нажмите 🕓 231 | 5. **Управление уведомлениями**: 🔕 «Не беспокоить» 232 | 233 | --- 234 | 235 | ## 🌐 Локализация 236 | 237 | --- 238 | 239 | ## 🔄 Команда `/refund` 240 | 241 | Чтобы воспользоваться командой `/refund`: 242 | 243 | 1. Откройте базу данных (файл `user_data.db` в корне проекта). Мой выбор - Sqlite Browser (Программа для редактирования бд) 244 | 2. В таблице `users` измените поле `status` вашего аккаунта с `USER` на `ADMIN`. 245 | 3. Перейдите в таблицу `transactions` и скопируйте `telegram_payment_charge_id` нужной транзакции. 246 | 4. Вернитесь в бота и выполните команду: 247 | ``` 248 | /refund 249 | ``` 250 | 251 | ⚠️ **Важно:** если на балансе бота меньше звёзд, чем в указанной транзакции, рефанд **не выполнится**. 252 | 253 | Альтернатива: вывод средств через Telegram‑бота. Требуется не менее **1000 звёзд** на балансе для вывода. 254 | 255 | 256 | Поддерживаются **русский** и **английский** языки. 257 | Язык определяется автоматически по языковым настройкам Telegram или через `/help`. 258 | 259 | --- 260 | 261 | ## 👤 Автор 262 | 263 | [neverwasbored](https://github.com/neverwasbored) 264 | 265 | --- 266 | 267 | ## 📝 Лицензия 268 | 269 | Этот проект лицензируется под **MIT License**. 270 | -------------------------------------------------------------------------------- /app/interfaces/telegram/messages.py: -------------------------------------------------------------------------------- 1 | BUTTONS = { 2 | "ru": { 3 | "balance": "💰 Баланс", 4 | "buy_gift": "🎁 Купить подарок", 5 | "deposit": "💳 Депозит", 6 | "auto_buy": "🤖 Автопокупка", 7 | "history": "🕓 История", 8 | "notifications_on": "🔕 Не беспокоить", 9 | "notifications_off": "🔔 Включить уведомления", 10 | "language": "🌐 Язык", 11 | "back": "🔙 Назад", 12 | "cancel": "❌ Отмена", 13 | "next": "➡️ Вперёд", 14 | "prev": "⬅️ Назад", 15 | "auto_buy_toggle": "🔄 Вкл/Выкл автопокупку", 16 | "auto_buy_price": "✏️ Лимит цены", 17 | "auto_buy_supply": "✏️ Лимит количества", 18 | "auto_buy_cycles": "✏️ Кол-во циклов", 19 | }, 20 | "en": { 21 | "balance": "💰 Balance", 22 | "buy_gift": "🎁 Buy gift", 23 | "deposit": "💳 Deposit", 24 | "auto_buy": "🤖 Auto-buy", 25 | "history": "🕓 History", 26 | "notifications_on": "🔕 Do not disturb", 27 | "notifications_off": "🔔 Enable notifications", 28 | "language": "🌐 Language", 29 | "back": "🔙 Back", 30 | "cancel": "❌ Cancel", 31 | "next": "➡️ Next", 32 | "prev": "⬅️ Back", 33 | "auto_buy_toggle": "🔄 Toggle auto-buy", 34 | "auto_buy_price": "✏️ Price limit", 35 | "auto_buy_supply": "✏️ Supply limit", 36 | "auto_buy_cycles": "✏️ Cycles", 37 | }, 38 | } 39 | 40 | MESSAGES = { 41 | "ru": { 42 | "auto_buy_status": lambda status: f"Статус автопокупки: {'🟢 Включена' if status else '🔴 Выключена'}.", 43 | "auto_buy_settings": lambda user, settings: ( 44 | f"{user.username}, твой баланс: {user.balance}⭐️\n\n" 45 | f"⚙️ Автопокупка\n" 46 | f"Статус: {'🟢 Включена' if settings.status else '🔴 Выключена'}\n\n" 47 | f"Лимит цены: от {settings.price_limit_from} до {settings.price_limit_to}⭐️\n" 48 | f"Лимит количества: {settings.supply_limit or 'не задан'}\n" 49 | f"Циклов: {settings.cycles}\n" 50 | ), 51 | "auto_buy_price_set": lambda price_from, price_to: f"✅ Лимит цены установлен: от {price_from} до {price_to}⭐️", 52 | "auto_buy_supply_set": lambda supply_limit: f"✅ Лимит количества установлен: {supply_limit}", 53 | "auto_buy_cycles_set": lambda cycles: f"✅ Количество циклов установлено: {cycles}", 54 | "auto_buy_price_prompt": "Введи лимит цены через пробел: ОТ ДО (например, 10 100)", 55 | "auto_buy_supply_prompt": "Введи лимит количества подарков (например, 50)", 56 | "auto_buy_cycles_prompt": "Введи количество циклов (например, 2). Каждый цикл — это попытка купить подарки.", 57 | "auto_buy_price_error": "Ошибка! Введи лимит цены через пробел: ОТ ДО (например, 10 100)", 58 | "auto_buy_supply_error": "Ошибка! Введи положительное число для лимита.", 59 | "auto_buy_cycles_error": "Ошибка! Введи положительное число для циклов.", 60 | "main_menu_balance": lambda username, balance: f"{username}, твой баланс: {balance}⭐️", 61 | "history_empty": "Нет операций за последнее время.", 62 | "history_line": lambda emoji, op, amount, status: f"{emoji} {op} | {amount}⭐️ | {status}", 63 | "history_line_autobuy_op": "Автопокупка", 64 | "history_line_deposit_op": "Депозит", 65 | "history_line_gift_op": "Подарок", 66 | "history_line_refund_op": "Возврат", 67 | "history_line_operation_op": "Операция", 68 | "deposit_prompt": lambda username, balance: f"{username}, твой баланс: {balance}⭐️\nВведи сумму депозита (только целые числа!). Пример: 15", 69 | "deposit_success": lambda amount: f"Депозит на {amount}⭐️ успешно пополнил ваш баланс.", 70 | "deposit_error": "Введи положительную цифру или число. Пример: 15", 71 | "buy_gift_prompt": lambda user_id: ( 72 | f"🎁 Покупка подарка\n" 73 | f"\n" 74 | f"1️⃣ Введи ID подарка (скопируй из списка выше)\n" 75 | f"2️⃣ Введи ID получателя (или свой)\n" 76 | f"3️⃣ Введи количество (целое число)\n" 77 | f"\n" 78 | f"ℹ️ Твой user_id: {user_id}\n" 79 | f"\n" 80 | f"Пример: 12345678 {user_id} 10\n" 81 | f"\n" 82 | f"Каждое значение через пробел." 83 | ), 84 | "buy_gift_error_format": "Введи ID подарка, ID получателя и количество через пробел.", 85 | "buy_gift_error_numbers": "Все значения должны быть числами.", 86 | "buy_gift_success": lambda balance: f"Покупка успешна! Твой новый баланс: {balance}⭐️.", 87 | "refund_success": lambda tx_id, amount: f"Возврат по транзакции {tx_id} успешно обработан. Сумма возврата: {amount}⭐️.", 88 | "notifications_on": "Уведомления включены.", 89 | "notifications_off": "Уведомления отключены.", 90 | "onboarding": lambda username, balance: ( 91 | f"Привет, {username}! 👋\n" 92 | f"Твой баланс: {balance}⭐️\n\n" 93 | f"Я помогу тебе покупать подарки за звёзды. Вот что я умею:\n" 94 | f"• 💳 Пополнить баланс — кнопка 'Депозит'\n" 95 | f"• 🎁 Купить подарок — кнопка 'Купить подарок'\n" 96 | f"• 🤖 Автопокупка — бот сам купит подходящие подарки по фильтрам\n" 97 | f"• 🕓 История — посмотреть все свои операции\n" 98 | f"• 🔕 Не беспокоить — отключить уведомления\n\n" 99 | f"Попробуй прямо сейчас: выбери действие в меню или нажми кнопку!" 100 | ), 101 | "help": ( 102 | "Напиши /start для запуска бота.\n" 103 | "Разработчик не занимается хостингом бота!\n" 104 | "По вопросам пользования чужим ботом не писать!\n\n" 105 | "TG: @neverbeentoxic\n" 106 | "Github: https://github.com/neverwasbored/TgGiftBuyerBot" 107 | ), 108 | "unknown_command": "Неизвестная команда.", 109 | "cancelled": "Действие отменено.", 110 | "back_to_menu": "Главное меню.", 111 | "input_error": "Ошибка ввода. Попробуйте ещё раз.", 112 | "not_admin": "У вас нет прав для этой команды.", 113 | "operation_success": "Операция успешно выполнена.", 114 | "operation_failed": "Операция не выполнена.", 115 | "new_gifts_notification": lambda total, suitable, not_suitable: ( 116 | f"🎁 Новые подарки! Всего: {total}\n" 117 | f"Подходят по фильтрам: {suitable}\n" 118 | f"Не подходят по фильтрам: {not_suitable}" 119 | ), 120 | "autobuy_purchase_success": lambda gift, balance: f"✅ Куплен подарок {gift} Остаток: {balance}⭐️.", 121 | "autobuy_purchase_fail": lambda gift, error: f"❌ Не удалось купить подарок {gift}: {error}", 122 | "deposit_invoice_title": "Пополнение баланса", 123 | "deposit_invoice_description": lambda amount: f"Пополнение баланса на {amount}⭐️", 124 | "autobuy_suitable": "Подходят по фильтрам", 125 | "autobuy_no_balance": "Недостаточно баланса", 126 | "autobuy_not_suitable": "Не подходят по фильтрам", 127 | "autobuy_try_buy": "Пробую купить подходящие подарки...", 128 | "autobuy_no_suitable": "Нет подходящих подарков для автопокупки.", 129 | }, 130 | "en": { 131 | "auto_buy_status": lambda status: f"Auto-buy status: {'🟢 Enabled' if status else '🔴 Disabled'}.", 132 | "auto_buy_settings": lambda user, settings: ( 133 | f"{user.username}, your balance: {user.balance}⭐️\n\n" 134 | f"⚙️ Auto-buy\n" 135 | f"Status: {'🟢 Enabled' if settings.status else '🔴 Disabled'}\n\n" 136 | f"Price limit: from {settings.price_limit_from} to {settings.price_limit_to}⭐️\n" 137 | f"Supply limit: {settings.supply_limit or 'not set'}\n" 138 | f"Cycles: {settings.cycles}\n" 139 | ), 140 | "auto_buy_price_set": lambda price_from, price_to: f"✅ Price limit set: from {price_from} to {price_to}⭐️", 141 | "auto_buy_supply_set": lambda supply_limit: f"✅ Supply limit set: {supply_limit}", 142 | "auto_buy_cycles_set": lambda cycles: f"✅ Number of cycles set: {cycles}", 143 | "auto_buy_price_prompt": "Enter price limit: FROM TO (e.g. 10 100)", 144 | "auto_buy_supply_prompt": "Enter supply limit (e.g. 50)", 145 | "auto_buy_cycles_prompt": "Enter number of cycles (e.g. 2). Each cycle is a purchase attempt.", 146 | "auto_buy_price_error": "Error! Enter price limit: FROM TO (e.g. 10 100)", 147 | "auto_buy_supply_error": "Error! Enter a positive number for supply limit.", 148 | "auto_buy_cycles_error": "Error! Enter a positive number for cycles.", 149 | "main_menu_balance": lambda username, balance: f"{username}, your balance: {balance}⭐️", 150 | "history_empty": "No recent transactions.", 151 | "history_line": lambda emoji, op, amount, status: f"{emoji} {op} | {amount}⭐️ | {status}", 152 | "history_line_autobuy_op": "Auto-buy", 153 | "history_line_deposit_op": "Deposit", 154 | "history_line_gift_op": "Gift", 155 | "history_line_refund_op": "Refund", 156 | "history_line_operation_op": "Operation", 157 | "deposit_prompt": lambda username, balance: f"{username}, your balance: {balance}⭐️\nEnter deposit amount (integers only!). Example: 15", 158 | "deposit_success": lambda amount: f"Deposit of {amount}⭐️ successfully added to your balance.", 159 | "deposit_error": "Enter a positive digit or number. Example: 15", 160 | "buy_gift_prompt": lambda user_id: ( 161 | f"🎁 Buy a gift\n" 162 | f"\n" 163 | f"1️⃣ Enter Gift ID (copy from the list below)\n" 164 | f"2️⃣ Enter Recipient ID (or your own)\n" 165 | f"3️⃣ Enter Amount (integer)\n" 166 | f"\n" 167 | f"ℹ️ Your user_id: {user_id}\n" 168 | f"\n" 169 | f"Example: 12345678 {user_id} 10\n" 170 | f"\n" 171 | f"Each value separated by space." 172 | ), 173 | "buy_gift_error_format": "Enter gift ID, recipient ID and amount separated by space.", 174 | "buy_gift_error_numbers": "All values must be numbers.", 175 | "buy_gift_success": lambda balance: f"Purchase successful! Your new balance: {balance}⭐️.", 176 | "refund_success": lambda tx_id, amount: f"Refund for transaction {tx_id} processed. Amount: {amount}⭐️.", 177 | "notifications_on": "Notifications enabled.", 178 | "notifications_off": "Notifications disabled.", 179 | "onboarding": lambda username, balance: ( 180 | f"Hi, {username}! 👋\n" 181 | f"Your balance: {balance}⭐️\n\n" 182 | f"I will help you buy gifts for stars. Here's what I can do:\n" 183 | f"• 💳 Top up balance — 'Deposit' button\n" 184 | f"• 🎁 Buy gift — 'Buy gift' button\n" 185 | f"• 🤖 Auto-buy — bot will buy suitable gifts by filters\n" 186 | f"• 🕓 History — view all your transactions\n" 187 | f"• 🔕 Do not disturb — disable notifications\n\n" 188 | f"Try now: choose an action in the menu or press a button!" 189 | ), 190 | "help": ( 191 | "Type /start to launch the bot.\n" 192 | "The developer does not provide bot hosting!\n" 193 | "Do not write about using someone else's bot!\n\n" 194 | "TG: @neverbeentoxic\n" 195 | "Github: https://github.com/neverwasbored/TgGiftBuyerBot" 196 | ), 197 | "unknown_command": "Unknown command.", 198 | "cancelled": "Action cancelled.", 199 | "back_to_menu": "Main menu.", 200 | "input_error": "Input error. Try again.", 201 | "not_admin": "You do not have permission for this command.", 202 | "operation_success": "Operation successful.", 203 | "operation_failed": "Operation failed.", 204 | "new_gifts_notification": lambda total, suitable, not_suitable: ( 205 | f"🎁 New gifts! Total: {total}\n" 206 | f"Suitable by filters: {suitable}\n" 207 | f"Not suitable by filters: {not_suitable}" 208 | ), 209 | "autobuy_purchase_success": lambda gift, balance: f"✅ Bought gift {gift} Balance: {balance}⭐️.", 210 | "autobuy_purchase_fail": lambda gift, error: f"❌ Failed to buy gift {gift}: {error}", 211 | "deposit_invoice_title": "Balance top-up", 212 | "deposit_invoice_description": lambda amount: f"Top-up for {amount}⭐️", 213 | "autobuy_suitable": "Suitable by filters", 214 | "autobuy_no_balance": "Not enough balance", 215 | "autobuy_not_suitable": "Not suitable by filters", 216 | "autobuy_try_buy": "Trying to buy suitable gifts...", 217 | "autobuy_no_suitable": "No suitable gifts for auto-buy.", 218 | }, 219 | } 220 | 221 | ERRORS = { 222 | "ru": { 223 | "user_not_found": "Пользователь не найден.", 224 | "gift_not_found": "Подарок с таким ID не найден.", 225 | "not_enough_balance": "Недостаточно средств для покупки подарка.", 226 | "debit_failed": "Ошибка списания баланса.", 227 | "gift_send_failed": "Ошибка отправки подарка. Звёзды сохранены.", 228 | "transaction_failed": "Ошибка создания транзакции.", 229 | "unknown": "Неизвестная ошибка.", 230 | "refund_not_admin": "У вас нет прав для выполнения этой команды.", 231 | "refund_not_found": "Транзакция не найдена. Проверьте ID и попробуйте снова.", 232 | "refund_already": "Средства по этой транзакции уже были возвращены.", 233 | "refund_user_not_found": "Пользователь, связанный с транзакцией, не найден.", 234 | "refund_debit_failed": "Ошибка возврата средств пользователю.", 235 | "refund_telegram_failed": "Ошибка возврата платежа через Telegram. Попробуйте позже.", 236 | }, 237 | "en": { 238 | "user_not_found": "User not found.", 239 | "gift_not_found": "Gift with this ID not found.", 240 | "not_enough_balance": "Not enough funds to buy the gift.", 241 | "debit_failed": "Failed to debit balance.", 242 | "gift_send_failed": "Failed to send gift. Stars are safe.", 243 | "transaction_failed": "Failed to create transaction.", 244 | "unknown": "Unknown error.", 245 | "refund_not_admin": "You do not have permission for this command.", 246 | "refund_not_found": "Transaction not found. Check the ID and try again.", 247 | "refund_already": "Funds for this transaction have already been refunded.", 248 | "refund_user_not_found": "User associated with the transaction not found.", 249 | "refund_debit_failed": "Failed to refund user.", 250 | "refund_telegram_failed": "Failed to refund payment via Telegram. Try again later.", 251 | }, 252 | } 253 | --------------------------------------------------------------------------------