├── 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 | [](https://www.python.org/downloads/release/python-3120/)
4 | [](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 | [](https://www.python.org/downloads/release/python-3120/)
4 | [](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/ # низкоуровневое Telegram 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 |
--------------------------------------------------------------------------------