├── lesson_3 ├── handlers │ ├── admin_private.py │ └── user_private.py └── app.py ├── lesson_4 ├── handlers │ ├── admin_private.py │ ├── user_group.py │ └── user_private.py ├── common │ └── bot_cmds_list.py ├── filters │ └── chat_types.py └── app.py ├── lesson_5 ├── handlers │ ├── admin_private.py │ ├── user_group.py │ └── user_private.py ├── common │ └── bot_cmds_list.py ├── filters │ └── chat_types.py ├── app.py └── kbds │ └── reply.py ├── lesson_6-FSM ├── common │ ├── restricted_words.py │ └── bot_cmds_list.py ├── filters │ └── chat_types.py ├── app.py ├── kbds │ └── reply.py └── handlers │ ├── user_group.py │ ├── user_private.py │ └── admin_private.py ├── lesson_7 ├── common │ ├── restricted_words.py │ └── bot_cmds_list.py ├── requirements.txt ├── README.md ├── filters │ └── chat_types.py ├── database │ ├── models.py │ ├── engine.py │ └── orm_query.py ├── middlewares │ └── db.py ├── kbds │ ├── inline.py │ └── reply.py ├── app.py └── handlers │ ├── user_group.py │ ├── user_private.py │ └── admin_private.py ├── lesson_8 template ├── common │ ├── restricted_words.py │ ├── bot_cmds_list.py │ └── texts_for_db.py ├── requirements.txt ├── banners │ ├── about.jpg │ ├── cart.jpg │ ├── main.jpg │ ├── catalog.jpg │ ├── payment.jpg │ └── shipping.jpg ├── kbds │ ├── inline.py │ └── reply.py ├── filters │ └── chat_types.py ├── database │ ├── engine.py │ ├── models.py │ └── orm_query.py ├── middlewares │ └── db.py ├── handlers │ ├── user_private.py │ ├── user_group.py │ └── admin_private.py ├── app.py └── README.md ├── lesson_8_multi_level_inline_menu ├── common │ ├── restricted_words.py │ ├── bot_cmds_list.py │ └── texts_for_db.py ├── requirements.txt ├── README.md ├── filters │ └── chat_types.py ├── database │ ├── engine.py │ ├── models.py │ └── orm_query.py ├── middlewares │ └── db.py ├── kbds │ ├── reply.py │ └── inline.py ├── utils │ └── paginator.py ├── app.py └── handlers │ ├── user_group.py │ ├── user_private.py │ ├── menu_processing.py │ └── admin_private.py ├── lesson_6 -Prepared_template_for_review ├── common │ ├── restricted_words.py │ └── bot_cmds_list.py ├── filters │ └── chat_types.py ├── app.py ├── kbds │ └── reply.py └── handlers │ ├── user_group.py │ ├── admin_private.py │ └── user_private.py ├── README.md ├── lesson_2 └── app.py └── lesson_1 └── app.py /lesson_3/handlers/admin_private.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /lesson_4/handlers/admin_private.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /lesson_5/handlers/admin_private.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /lesson_6-FSM/common/restricted_words.py: -------------------------------------------------------------------------------- 1 | restricted_words = {"кабан", "хомяк", "выхухоль"} -------------------------------------------------------------------------------- /lesson_7/common/restricted_words.py: -------------------------------------------------------------------------------- 1 | restricted_words = {"кабан", "хомяк", "выхухоль"} -------------------------------------------------------------------------------- /lesson_8 template/common/restricted_words.py: -------------------------------------------------------------------------------- 1 | restricted_words = {"кабан", "хомяк", "выхухоль"} -------------------------------------------------------------------------------- /lesson_7/requirements.txt: -------------------------------------------------------------------------------- 1 | aiogram 2 | python-dotenv 3 | sqlalchemy 4 | asyncpg 5 | aiosqlite 6 | -------------------------------------------------------------------------------- /lesson_8 template/requirements.txt: -------------------------------------------------------------------------------- 1 | aiogram 2 | python-dotenv 3 | sqlalchemy 4 | asyncpg 5 | aiosqlite -------------------------------------------------------------------------------- /lesson_8_multi_level_inline_menu/common/restricted_words.py: -------------------------------------------------------------------------------- 1 | restricted_words = {"кабан", "хомяк", "выхухоль"} -------------------------------------------------------------------------------- /lesson_6 -Prepared_template_for_review/common/restricted_words.py: -------------------------------------------------------------------------------- 1 | restricted_words = {"кабан", "хомяк", "выхухоль"} -------------------------------------------------------------------------------- /lesson_8_multi_level_inline_menu/requirements.txt: -------------------------------------------------------------------------------- 1 | aiogram==3.3.0 2 | sqlalchemy==2.0.25 3 | python-dotenv 4 | asyncpg 5 | aiosqlite -------------------------------------------------------------------------------- /lesson_8_multi_level_inline_menu/README.md: -------------------------------------------------------------------------------- 1 | Изображения баннеров в папке lesson_8 template. 2 | Код из видео yt канала Python Hub Studio. 3 | -------------------------------------------------------------------------------- /lesson_8 template/banners/about.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PythonHubStudio/aiogram-3-course-telegram-bot/HEAD/lesson_8 template/banners/about.jpg -------------------------------------------------------------------------------- /lesson_8 template/banners/cart.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PythonHubStudio/aiogram-3-course-telegram-bot/HEAD/lesson_8 template/banners/cart.jpg -------------------------------------------------------------------------------- /lesson_8 template/banners/main.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PythonHubStudio/aiogram-3-course-telegram-bot/HEAD/lesson_8 template/banners/main.jpg -------------------------------------------------------------------------------- /lesson_8 template/banners/catalog.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PythonHubStudio/aiogram-3-course-telegram-bot/HEAD/lesson_8 template/banners/catalog.jpg -------------------------------------------------------------------------------- /lesson_8 template/banners/payment.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PythonHubStudio/aiogram-3-course-telegram-bot/HEAD/lesson_8 template/banners/payment.jpg -------------------------------------------------------------------------------- /lesson_8 template/banners/shipping.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PythonHubStudio/aiogram-3-course-telegram-bot/HEAD/lesson_8 template/banners/shipping.jpg -------------------------------------------------------------------------------- /lesson_7/README.md: -------------------------------------------------------------------------------- 1 | Для работы через PostgreSQL, нужно установить сервер этой СУБД на ПК, или указать url адрес базы данных на удаленном сервере. Иначе, использовать engine на sqlite+aiosqlite. 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Код из каждого видео плейлиста в отдельной папке lesson_1, 2, .. и так далее 2 | Код из курса по созданию бота для Telegram на python с aiogram 3, YT канала Python Hub Studio 3 | 4 | Код дополнительного примера из второго видео, по работе с API Telegram напрямую через aiohttp: 5 | https://github.com/PythonHubStudio/Telegram-bot-with-aiohttp-and-asyncio 6 | -------------------------------------------------------------------------------- /lesson_4/common/bot_cmds_list.py: -------------------------------------------------------------------------------- 1 | from aiogram.types import BotCommand 2 | 3 | 4 | private = [ 5 | BotCommand(command='menu', description='Посмотреть меню'), 6 | BotCommand(command='about', description='О нас'), 7 | BotCommand(command='payment', description='Варианты оплаты'), 8 | BotCommand(command='shipping', description='Варианты доставки'), 9 | ] -------------------------------------------------------------------------------- /lesson_5/common/bot_cmds_list.py: -------------------------------------------------------------------------------- 1 | from aiogram.types import BotCommand 2 | 3 | 4 | private = [ 5 | BotCommand(command='menu', description='Посмотреть меню'), 6 | BotCommand(command='about', description='О нас'), 7 | BotCommand(command='payment', description='Варианты оплаты'), 8 | BotCommand(command='shipping', description='Варианты доставки'), 9 | ] -------------------------------------------------------------------------------- /lesson_7/common/bot_cmds_list.py: -------------------------------------------------------------------------------- 1 | from aiogram.types import BotCommand 2 | 3 | 4 | private = [ 5 | BotCommand(command='menu', description='Посмотреть меню'), 6 | BotCommand(command='about', description='О нас'), 7 | BotCommand(command='payment', description='Варианты оплаты'), 8 | BotCommand(command='shipping', description='Варианты доставки'), 9 | ] -------------------------------------------------------------------------------- /lesson_6-FSM/common/bot_cmds_list.py: -------------------------------------------------------------------------------- 1 | from aiogram.types import BotCommand 2 | 3 | 4 | private = [ 5 | BotCommand(command='menu', description='Посмотреть меню'), 6 | BotCommand(command='about', description='О нас'), 7 | BotCommand(command='payment', description='Варианты оплаты'), 8 | BotCommand(command='shipping', description='Варианты доставки'), 9 | ] -------------------------------------------------------------------------------- /lesson_4/filters/chat_types.py: -------------------------------------------------------------------------------- 1 | from aiogram.filters import Filter 2 | from aiogram import types 3 | 4 | 5 | class ChatTypeFilter(Filter): 6 | def __init__(self, chat_types: list[str]) -> None: 7 | self.chat_types = chat_types 8 | 9 | async def __call__(self, message: types.Message) -> bool: 10 | return message.chat.type in self.chat_types -------------------------------------------------------------------------------- /lesson_5/filters/chat_types.py: -------------------------------------------------------------------------------- 1 | from aiogram.filters import Filter 2 | from aiogram import types 3 | 4 | 5 | class ChatTypeFilter(Filter): 6 | def __init__(self, chat_types: list[str]) -> None: 7 | self.chat_types = chat_types 8 | 9 | async def __call__(self, message: types.Message) -> bool: 10 | return message.chat.type in self.chat_types -------------------------------------------------------------------------------- /lesson_8 template/common/bot_cmds_list.py: -------------------------------------------------------------------------------- 1 | # from aiogram.types import BotCommand 2 | 3 | 4 | # private = [ 5 | # BotCommand(command='menu', description='Посмотреть меню'), 6 | # BotCommand(command='about', description='О нас'), 7 | # BotCommand(command='payment', description='Варианты оплаты'), 8 | # BotCommand(command='shipping', description='Варианты доставки'), 9 | # ] -------------------------------------------------------------------------------- /lesson_6 -Prepared_template_for_review/common/bot_cmds_list.py: -------------------------------------------------------------------------------- 1 | from aiogram.types import BotCommand 2 | 3 | 4 | private = [ 5 | BotCommand(command='menu', description='Посмотреть меню'), 6 | BotCommand(command='about', description='О нас'), 7 | BotCommand(command='payment', description='Варианты оплаты'), 8 | BotCommand(command='shipping', description='Варианты доставки'), 9 | ] -------------------------------------------------------------------------------- /lesson_8_multi_level_inline_menu/common/bot_cmds_list.py: -------------------------------------------------------------------------------- 1 | # from aiogram.types import BotCommand 2 | 3 | 4 | # private = [ 5 | # BotCommand(command='menu', description='Посмотреть меню'), 6 | # BotCommand(command='about', description='О нас'), 7 | # BotCommand(command='payment', description='Варианты оплаты'), 8 | # BotCommand(command='shipping', description='Варианты доставки'), 9 | # ] -------------------------------------------------------------------------------- /lesson_8 template/kbds/inline.py: -------------------------------------------------------------------------------- 1 | from aiogram.types import InlineKeyboardButton 2 | from aiogram.utils.keyboard import InlineKeyboardBuilder 3 | 4 | 5 | 6 | def get_callback_btns(*, btns: dict[str, str], sizes: tuple[int] = (2,)): 7 | keyboard = InlineKeyboardBuilder() 8 | 9 | for text, data in btns.items(): 10 | keyboard.add(InlineKeyboardButton(text=text, callback_data=data)) 11 | 12 | return keyboard.adjust(*sizes).as_markup() 13 | -------------------------------------------------------------------------------- /lesson_3/handlers/user_private.py: -------------------------------------------------------------------------------- 1 | from aiogram import types, Router 2 | from aiogram.filters import CommandStart, Command 3 | 4 | user_private_router = Router() 5 | 6 | 7 | @user_private_router.message(CommandStart()) 8 | async def start_cmd(message: types.Message): 9 | await message.answer("Привет, я виртуальный помощник") 10 | 11 | 12 | @user_private_router.message(Command('menu', 'name')) 13 | async def menu_cmd(message: types.Message): 14 | await message.answer("Вот меню:") 15 | -------------------------------------------------------------------------------- /lesson_7/filters/chat_types.py: -------------------------------------------------------------------------------- 1 | from aiogram.filters import Filter 2 | from aiogram import Bot, types 3 | 4 | 5 | class ChatTypeFilter(Filter): 6 | def __init__(self, chat_types: list[str]) -> None: 7 | self.chat_types = chat_types 8 | 9 | async def __call__(self, message: types.Message) -> bool: 10 | return message.chat.type in self.chat_types 11 | 12 | 13 | class IsAdmin(Filter): 14 | def __init__(self) -> None: 15 | pass 16 | 17 | async def __call__(self, message: types.Message, bot: Bot) -> bool: 18 | return message.from_user.id in bot.my_admins_list -------------------------------------------------------------------------------- /lesson_6-FSM/filters/chat_types.py: -------------------------------------------------------------------------------- 1 | from aiogram.filters import Filter 2 | from aiogram import Bot, types 3 | 4 | 5 | class ChatTypeFilter(Filter): 6 | def __init__(self, chat_types: list[str]) -> None: 7 | self.chat_types = chat_types 8 | 9 | async def __call__(self, message: types.Message) -> bool: 10 | return message.chat.type in self.chat_types 11 | 12 | 13 | class IsAdmin(Filter): 14 | def __init__(self) -> None: 15 | pass 16 | 17 | async def __call__(self, message: types.Message, bot: Bot) -> bool: 18 | return message.from_user.id in bot.my_admins_list -------------------------------------------------------------------------------- /lesson_8 template/filters/chat_types.py: -------------------------------------------------------------------------------- 1 | from aiogram.filters import Filter 2 | from aiogram import Bot, types 3 | 4 | 5 | class ChatTypeFilter(Filter): 6 | def __init__(self, chat_types: list[str]) -> None: 7 | self.chat_types = chat_types 8 | 9 | async def __call__(self, message: types.Message) -> bool: 10 | return message.chat.type in self.chat_types 11 | 12 | 13 | class IsAdmin(Filter): 14 | def __init__(self) -> None: 15 | pass 16 | 17 | async def __call__(self, message: types.Message, bot: Bot) -> bool: 18 | return message.from_user.id in bot.my_admins_list -------------------------------------------------------------------------------- /lesson_8_multi_level_inline_menu/filters/chat_types.py: -------------------------------------------------------------------------------- 1 | from aiogram.filters import Filter 2 | from aiogram import Bot, types 3 | 4 | 5 | class ChatTypeFilter(Filter): 6 | def __init__(self, chat_types: list[str]) -> None: 7 | self.chat_types = chat_types 8 | 9 | async def __call__(self, message: types.Message) -> bool: 10 | return message.chat.type in self.chat_types 11 | 12 | 13 | class IsAdmin(Filter): 14 | def __init__(self) -> None: 15 | pass 16 | 17 | async def __call__(self, message: types.Message, bot: Bot) -> bool: 18 | return message.from_user.id in bot.my_admins_list -------------------------------------------------------------------------------- /lesson_6 -Prepared_template_for_review/filters/chat_types.py: -------------------------------------------------------------------------------- 1 | from aiogram.filters import Filter 2 | from aiogram import Bot, types 3 | 4 | 5 | class ChatTypeFilter(Filter): 6 | def __init__(self, chat_types: list[str]) -> None: 7 | self.chat_types = chat_types 8 | 9 | async def __call__(self, message: types.Message) -> bool: 10 | return message.chat.type in self.chat_types 11 | 12 | 13 | class IsAdmin(Filter): 14 | def __init__(self) -> None: 15 | pass 16 | 17 | async def __call__(self, message: types.Message, bot: Bot) -> bool: 18 | return message.from_user.id in bot.my_admins_list -------------------------------------------------------------------------------- /lesson_2/app.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | from aiogram import Bot, Dispatcher, types 3 | from aiogram.filters import CommandStart 4 | 5 | bot = Bot(token="your TOKEN here") 6 | dp = Dispatcher() 7 | 8 | 9 | @dp.message(CommandStart()) 10 | async def start_cmd(message: types.Message): 11 | await message.answer('Это была команда старт') 12 | 13 | 14 | @dp.message() 15 | async def echo(message: types.Message): 16 | 17 | await message.answer(message.text) 18 | await message.reply(message.text) 19 | 20 | 21 | async def main(): 22 | await bot.delete_webhook(drop_pending_updates=True) 23 | await dp.start_polling(bot) 24 | 25 | asyncio.run(main()) -------------------------------------------------------------------------------- /lesson_3/app.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import os 3 | 4 | from aiogram import Bot, Dispatcher, types 5 | from aiogram.filters import CommandStart 6 | 7 | from dotenv import find_dotenv, load_dotenv 8 | load_dotenv(find_dotenv()) 9 | 10 | from handlers.user_private import user_private_router 11 | 12 | 13 | ALLOWED_UPDATES = ['message, edited_message'] 14 | 15 | bot = Bot(token=os.getenv('TOKEN')) 16 | dp = Dispatcher() 17 | 18 | dp.include_router(user_private_router) 19 | 20 | 21 | 22 | async def main(): 23 | await bot.delete_webhook(drop_pending_updates=True) 24 | await dp.start_polling(bot, allowed_updates=ALLOWED_UPDATES) 25 | 26 | asyncio.run(main()) -------------------------------------------------------------------------------- /lesson_7/database/models.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy import DateTime, Float, String, Text, func 2 | from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column 3 | 4 | 5 | class Base(DeclarativeBase): 6 | created: Mapped[DateTime] = mapped_column(DateTime, default=func.now()) 7 | updated: Mapped[DateTime] = mapped_column(DateTime, default=func.now(), onupdate=func.now()) 8 | 9 | 10 | class Product(Base): 11 | __tablename__ = 'product' 12 | 13 | id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True) 14 | name: Mapped[str] = mapped_column(String(150), nullable=False) 15 | description: Mapped[str] = mapped_column(Text) 16 | price: Mapped[float] = mapped_column(Float(asdecimal=True), nullable=False) 17 | image: Mapped[str] = mapped_column(String(150)) 18 | -------------------------------------------------------------------------------- /lesson_1/app.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | from aiogram import Bot, Dispatcher, types 3 | from aiogram.filters import CommandStart 4 | 5 | bot = Bot(token="your TOKEN here") 6 | dp = Dispatcher() 7 | 8 | 9 | @dp.message(CommandStart()) 10 | async def start_cmd(message: types.Message): 11 | await message.answer('Это была команда старт') 12 | 13 | @dp.message() 14 | async def echo(message: types.Message): 15 | text = message.text 16 | 17 | if text in ['Привет', 'привет', 'hi', 'hello']: 18 | await message.answer('И тебе привет!') 19 | elif text in ['Пока', 'пока', 'До свидания']: 20 | await message.answer('И тебе пока!') 21 | else: 22 | await message.answer(message.text) 23 | 24 | 25 | async def main(): 26 | await dp.start_polling(bot) 27 | 28 | asyncio.run(main()) -------------------------------------------------------------------------------- /lesson_7/database/engine.py: -------------------------------------------------------------------------------- 1 | import os 2 | from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine 3 | 4 | from database.models import Base 5 | 6 | #from .env file: 7 | # DB_LITE=sqlite+aiosqlite:///my_base.db 8 | # DB_URL=postgresql+asyncpg://login:password@localhost:5432/db_name 9 | 10 | # engine = create_async_engine(os.getenv('DB_LITE'), echo=True) 11 | 12 | engine = create_async_engine(os.getenv('DB_URL'), echo=True) 13 | 14 | session_maker = async_sessionmaker(bind=engine, class_=AsyncSession, expire_on_commit=False) 15 | 16 | 17 | async def create_db(): 18 | async with engine.begin() as conn: 19 | await conn.run_sync(Base.metadata.create_all) 20 | 21 | 22 | async def drop_db(): 23 | async with engine.begin() as conn: 24 | await conn.run_sync(Base.metadata.drop_all) 25 | -------------------------------------------------------------------------------- /lesson_4/handlers/user_group.py: -------------------------------------------------------------------------------- 1 | from string import punctuation 2 | 3 | from aiogram import F, types, Router 4 | 5 | from filters.chat_types import ChatTypeFilter 6 | 7 | user_group_router = Router() 8 | user_group_router.message.filter(ChatTypeFilter(['group', 'supergroup'])) 9 | user_group_router.edited_message.filter(ChatTypeFilter(['group', 'supergroup'])) 10 | 11 | 12 | restricted_words = {'кабан', 'хомяк', 'выхухоль'} 13 | 14 | def clean_text(text: str): 15 | return text.translate(str.maketrans('', '', punctuation)) 16 | 17 | 18 | @user_group_router.edited_message() 19 | @user_group_router.message() 20 | async def cleaner(message: types.Message): 21 | if restricted_words.intersection(clean_text(message.text.lower()).split()): 22 | await message.answer(f"{message.from_user.first_name}, соблюддайте порядок в чате!") 23 | await message.delete() 24 | # await message.chat.ban(message.from_user.id) -------------------------------------------------------------------------------- /lesson_5/handlers/user_group.py: -------------------------------------------------------------------------------- 1 | from string import punctuation 2 | 3 | from aiogram import F, types, Router 4 | 5 | from filters.chat_types import ChatTypeFilter 6 | 7 | user_group_router = Router() 8 | user_group_router.message.filter(ChatTypeFilter(['group', 'supergroup'])) 9 | user_group_router.edited_message.filter(ChatTypeFilter(['group', 'supergroup'])) 10 | 11 | 12 | restricted_words = {'кабан', 'хомяк', 'выхухоль'} 13 | 14 | def clean_text(text: str): 15 | return text.translate(str.maketrans('', '', punctuation)) 16 | 17 | 18 | @user_group_router.edited_message() 19 | @user_group_router.message() 20 | async def cleaner(message: types.Message): 21 | if restricted_words.intersection(clean_text(message.text.lower()).split()): 22 | await message.answer(f"{message.from_user.first_name}, соблюддайте порядок в чате!") 23 | await message.delete() 24 | # await message.chat.ban(message.from_user.id) -------------------------------------------------------------------------------- /lesson_4/app.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import os 3 | 4 | from aiogram import Bot, Dispatcher, types 5 | 6 | 7 | from dotenv import find_dotenv, load_dotenv 8 | load_dotenv(find_dotenv()) 9 | 10 | from handlers.user_private import user_private_router 11 | from handlers.user_group import user_group_router 12 | from common.bot_cmds_list import private 13 | 14 | ALLOWED_UPDATES = ['message, edited_message'] 15 | 16 | bot = Bot(token=os.getenv('TOKEN')) 17 | dp = Dispatcher() 18 | 19 | dp.include_router(user_private_router) 20 | dp.include_router(user_group_router) 21 | 22 | 23 | 24 | async def main(): 25 | await bot.delete_webhook(drop_pending_updates=True) 26 | # await bot.delete_my_commands(scope=types.BotCommandScopeAllPrivateChats()) 27 | await bot.set_my_commands(commands=private, scope=types.BotCommandScopeAllPrivateChats()) 28 | await dp.start_polling(bot, allowed_updates=ALLOWED_UPDATES) 29 | 30 | asyncio.run(main()) -------------------------------------------------------------------------------- /lesson_5/app.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import os 3 | 4 | from aiogram import Bot, Dispatcher, types 5 | from aiogram.enums import ParseMode 6 | 7 | from dotenv import find_dotenv, load_dotenv 8 | load_dotenv(find_dotenv()) 9 | 10 | from handlers.user_private import user_private_router 11 | from handlers.user_group import user_group_router 12 | 13 | from common.bot_cmds_list import private 14 | 15 | ALLOWED_UPDATES = ['message, edited_message'] 16 | 17 | bot = Bot(token=os.getenv('TOKEN'), parse_mode=ParseMode.HTML) 18 | dp = Dispatcher() 19 | 20 | dp.include_router(user_private_router) 21 | dp.include_router(user_group_router) 22 | 23 | 24 | async def main(): 25 | await bot.delete_webhook(drop_pending_updates=True) 26 | # await bot.delete_my_commands(scope=types.BotCommandScopeAllPrivateChats()) 27 | await bot.set_my_commands(commands=private, scope=types.BotCommandScopeAllPrivateChats()) 28 | await dp.start_polling(bot, allowed_updates=ALLOWED_UPDATES) 29 | 30 | asyncio.run(main()) -------------------------------------------------------------------------------- /lesson_8 template/common/texts_for_db.py: -------------------------------------------------------------------------------- 1 | from aiogram.utils.formatting import Bold, as_list, as_marked_section 2 | 3 | 4 | categories = ['Еда', 'Напитки'] 5 | 6 | description_for_info_pages = { 7 | "main": "Добро пожаловать!", 8 | "about": "Пиццерия Такая-то.\nРежим работы - круглосуточно.", 9 | "payment": as_marked_section( 10 | Bold("Варианты оплаты:"), 11 | "Картой в боте", 12 | "При получении карта/кеш", 13 | "В заведении", 14 | marker="✅ ", 15 | ).as_html(), 16 | "shipping": as_list( 17 | as_marked_section( 18 | Bold("Варианты доставки/заказа:"), 19 | "Курьер", 20 | "Самовынос (сейчас прибегу заберу)", 21 | "Покушаю у Вас (сейчас прибегу)", 22 | marker="✅ ", 23 | ), 24 | as_marked_section(Bold("Нельзя:"), "Почта", "Голуби", marker="❌ "), 25 | sep="\n----------------------\n", 26 | ).as_html(), 27 | 'catalog': 'Категории:', 28 | 'cart': 'В корзине ничего нет!' 29 | } 30 | -------------------------------------------------------------------------------- /lesson_8_multi_level_inline_menu/common/texts_for_db.py: -------------------------------------------------------------------------------- 1 | from aiogram.utils.formatting import Bold, as_list, as_marked_section 2 | 3 | 4 | categories = ['Еда', 'Напитки'] 5 | 6 | description_for_info_pages = { 7 | "main": "Добро пожаловать!", 8 | "about": "Пиццерия Такая-то.\nРежим работы - круглосуточно.", 9 | "payment": as_marked_section( 10 | Bold("Варианты оплаты:"), 11 | "Картой в боте", 12 | "При получении карта/кеш", 13 | "В заведении", 14 | marker="✅ ", 15 | ).as_html(), 16 | "shipping": as_list( 17 | as_marked_section( 18 | Bold("Варианты доставки/заказа:"), 19 | "Курьер", 20 | "Самовынос (сейчас прибегу заберу)", 21 | "Покушаю у Вас (сейчас прибегу)", 22 | marker="✅ ", 23 | ), 24 | as_marked_section(Bold("Нельзя:"), "Почта", "Голуби", marker="❌ "), 25 | sep="\n----------------------\n", 26 | ).as_html(), 27 | 'catalog': 'Категории:', 28 | 'cart': 'В корзине ничего нет!' 29 | } 30 | -------------------------------------------------------------------------------- /lesson_6 -Prepared_template_for_review/app.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import os 3 | 4 | from aiogram import Bot, Dispatcher, types 5 | from aiogram.enums import ParseMode 6 | 7 | from dotenv import find_dotenv, load_dotenv 8 | load_dotenv(find_dotenv()) 9 | 10 | from handlers.user_private import user_private_router 11 | from handlers.user_group import user_group_router 12 | from handlers.admin_private import admin_router 13 | 14 | from common.bot_cmds_list import private 15 | 16 | ALLOWED_UPDATES = ['message, edited_message'] 17 | 18 | bot = Bot(token=os.getenv('TOKEN'), parse_mode=ParseMode.HTML) 19 | bot.my_admins_list = [] 20 | 21 | dp = Dispatcher() 22 | 23 | dp.include_router(user_private_router) 24 | dp.include_router(user_group_router) 25 | dp.include_router(admin_router) 26 | 27 | 28 | async def main(): 29 | await bot.delete_webhook(drop_pending_updates=True) 30 | # await bot.delete_my_commands(scope=types.BotCommandScopeAllPrivateChats()) 31 | await bot.set_my_commands(commands=private, scope=types.BotCommandScopeAllPrivateChats()) 32 | await dp.start_polling(bot, allowed_updates=ALLOWED_UPDATES) 33 | 34 | asyncio.run(main()) -------------------------------------------------------------------------------- /lesson_6-FSM/app.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import os 3 | 4 | from aiogram import Bot, Dispatcher, types 5 | from aiogram.enums import ParseMode 6 | from aiogram.fsm.strategy import FSMStrategy 7 | 8 | from dotenv import find_dotenv, load_dotenv 9 | load_dotenv(find_dotenv()) 10 | 11 | from handlers.user_private import user_private_router 12 | from handlers.user_group import user_group_router 13 | from handlers.admin_private import admin_router 14 | 15 | from common.bot_cmds_list import private 16 | 17 | ALLOWED_UPDATES = ['message, edited_message'] 18 | 19 | bot = Bot(token=os.getenv('TOKEN'), parse_mode=ParseMode.HTML) 20 | bot.my_admins_list = [] 21 | 22 | dp = Dispatcher() 23 | 24 | dp.include_router(user_private_router) 25 | dp.include_router(user_group_router) 26 | dp.include_router(admin_router) 27 | 28 | 29 | async def main(): 30 | await bot.delete_webhook(drop_pending_updates=True) 31 | # await bot.delete_my_commands(scope=types.BotCommandScopeAllPrivateChats()) 32 | await bot.set_my_commands(commands=private, scope=types.BotCommandScopeAllPrivateChats()) 33 | await dp.start_polling(bot, allowed_updates=ALLOWED_UPDATES) 34 | 35 | asyncio.run(main()) -------------------------------------------------------------------------------- /lesson_8 template/database/engine.py: -------------------------------------------------------------------------------- 1 | import os 2 | from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine 3 | 4 | from database.models import Base 5 | from database.orm_query import orm_add_banner_description, orm_create_categories 6 | 7 | from common.texts_for_db import categories, description_for_info_pages 8 | 9 | #from .env file: 10 | # DB_LITE=sqlite+aiosqlite:///my_base.db 11 | # DB_URL=postgresql+asyncpg://login:password@localhost:5432/db_name 12 | 13 | engine = create_async_engine(os.getenv('DB_LITE'), echo=True) 14 | 15 | # engine = create_async_engine(os.getenv('DB_URL'), echo=True) 16 | 17 | session_maker = async_sessionmaker(bind=engine, class_=AsyncSession, expire_on_commit=False) 18 | 19 | 20 | 21 | async def create_db(): 22 | async with engine.begin() as conn: 23 | await conn.run_sync(Base.metadata.create_all) 24 | 25 | async with session_maker() as session: 26 | await orm_create_categories(session, categories) 27 | await orm_add_banner_description(session, description_for_info_pages) 28 | 29 | 30 | async def drop_db(): 31 | async with engine.begin() as conn: 32 | await conn.run_sync(Base.metadata.drop_all) 33 | -------------------------------------------------------------------------------- /lesson_8_multi_level_inline_menu/database/engine.py: -------------------------------------------------------------------------------- 1 | import os 2 | from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine 3 | 4 | from database.models import Base 5 | from database.orm_query import orm_add_banner_description, orm_create_categories 6 | 7 | from common.texts_for_db import categories, description_for_info_pages 8 | 9 | #from .env file: 10 | # DB_LITE=sqlite+aiosqlite:///my_base.db 11 | # DB_URL=postgresql+asyncpg://login:password@localhost:5432/db_name 12 | 13 | # engine = create_async_engine(os.getenv('DB_LITE'), echo=True) 14 | 15 | engine = create_async_engine(os.getenv('DB_URL'), echo=True) 16 | 17 | session_maker = async_sessionmaker(bind=engine, class_=AsyncSession, expire_on_commit=False) 18 | 19 | 20 | 21 | async def create_db(): 22 | async with engine.begin() as conn: 23 | await conn.run_sync(Base.metadata.create_all) 24 | 25 | async with session_maker() as session: 26 | await orm_create_categories(session, categories) 27 | await orm_add_banner_description(session, description_for_info_pages) 28 | 29 | 30 | async def drop_db(): 31 | async with engine.begin() as conn: 32 | await conn.run_sync(Base.metadata.drop_all) 33 | -------------------------------------------------------------------------------- /lesson_7/middlewares/db.py: -------------------------------------------------------------------------------- 1 | from typing import Any, Awaitable, Callable, Dict 2 | 3 | from aiogram import BaseMiddleware 4 | from aiogram.types import Message, TelegramObject 5 | 6 | from sqlalchemy.ext.asyncio import async_sessionmaker 7 | 8 | 9 | class DataBaseSession(BaseMiddleware): 10 | def __init__(self, session_pool: async_sessionmaker): 11 | self.session_pool = session_pool 12 | 13 | 14 | async def __call__( 15 | self, 16 | handler: Callable[[TelegramObject, Dict[str, Any]], Awaitable[Any]], 17 | event: TelegramObject, 18 | data: Dict[str, Any], 19 | ) -> Any: 20 | async with self.session_pool() as session: 21 | data['session'] = session 22 | return await handler(event, data) 23 | 24 | 25 | # class CounterMiddleware(BaseMiddleware): 26 | # def __init__(self) -> None: 27 | # self.counter = 0 28 | 29 | # async def __call__( 30 | # self, 31 | # handler: Callable[[TelegramObject, Dict[str, Any]], Awaitable[Any]], 32 | # event: TelegramObject, 33 | # data: Dict[str, Any] 34 | # ) -> Any: 35 | # self.counter += 1 36 | # data['counter'] = self.counter 37 | # return await handler(event, data) -------------------------------------------------------------------------------- /lesson_8 template/middlewares/db.py: -------------------------------------------------------------------------------- 1 | from typing import Any, Awaitable, Callable, Dict 2 | 3 | from aiogram import BaseMiddleware 4 | from aiogram.types import Message, TelegramObject 5 | 6 | from sqlalchemy.ext.asyncio import async_sessionmaker 7 | 8 | 9 | class DataBaseSession(BaseMiddleware): 10 | def __init__(self, session_pool: async_sessionmaker): 11 | self.session_pool = session_pool 12 | 13 | 14 | async def __call__( 15 | self, 16 | handler: Callable[[TelegramObject, Dict[str, Any]], Awaitable[Any]], 17 | event: TelegramObject, 18 | data: Dict[str, Any], 19 | ) -> Any: 20 | async with self.session_pool() as session: 21 | data['session'] = session 22 | return await handler(event, data) 23 | 24 | 25 | # class CounterMiddleware(BaseMiddleware): 26 | # def __init__(self) -> None: 27 | # self.counter = 0 28 | 29 | # async def __call__( 30 | # self, 31 | # handler: Callable[[TelegramObject, Dict[str, Any]], Awaitable[Any]], 32 | # event: TelegramObject, 33 | # data: Dict[str, Any] 34 | # ) -> Any: 35 | # self.counter += 1 36 | # data['counter'] = self.counter 37 | # return await handler(event, data) -------------------------------------------------------------------------------- /lesson_8_multi_level_inline_menu/middlewares/db.py: -------------------------------------------------------------------------------- 1 | from typing import Any, Awaitable, Callable, Dict 2 | 3 | from aiogram import BaseMiddleware 4 | from aiogram.types import Message, TelegramObject 5 | 6 | from sqlalchemy.ext.asyncio import async_sessionmaker 7 | 8 | 9 | class DataBaseSession(BaseMiddleware): 10 | def __init__(self, session_pool: async_sessionmaker): 11 | self.session_pool = session_pool 12 | 13 | 14 | async def __call__( 15 | self, 16 | handler: Callable[[TelegramObject, Dict[str, Any]], Awaitable[Any]], 17 | event: TelegramObject, 18 | data: Dict[str, Any], 19 | ) -> Any: 20 | async with self.session_pool() as session: 21 | data['session'] = session 22 | return await handler(event, data) 23 | 24 | 25 | # class CounterMiddleware(BaseMiddleware): 26 | # def __init__(self) -> None: 27 | # self.counter = 0 28 | 29 | # async def __call__( 30 | # self, 31 | # handler: Callable[[TelegramObject, Dict[str, Any]], Awaitable[Any]], 32 | # event: TelegramObject, 33 | # data: Dict[str, Any] 34 | # ) -> Any: 35 | # self.counter += 1 36 | # data['counter'] = self.counter 37 | # return await handler(event, data) -------------------------------------------------------------------------------- /lesson_7/kbds/inline.py: -------------------------------------------------------------------------------- 1 | from aiogram.types import InlineKeyboardButton 2 | from aiogram.utils.keyboard import InlineKeyboardBuilder 3 | 4 | 5 | def get_callback_btns( 6 | *, 7 | btns: dict[str, str], 8 | sizes: tuple[int] = (2,)): 9 | 10 | keyboard = InlineKeyboardBuilder() 11 | 12 | for text, data in btns.items(): 13 | 14 | keyboard.add(InlineKeyboardButton(text=text, callback_data=data)) 15 | 16 | return keyboard.adjust(*sizes).as_markup() 17 | 18 | 19 | def get_url_btns( 20 | *, 21 | btns: dict[str, str], 22 | sizes: tuple[int] = (2,)): 23 | 24 | keyboard = InlineKeyboardBuilder() 25 | 26 | for text, url in btns.items(): 27 | 28 | keyboard.add(InlineKeyboardButton(text=text, url=url)) 29 | 30 | return keyboard.adjust(*sizes).as_markup() 31 | 32 | 33 | #Создать микс из CallBack и URL кнопок 34 | def get_inlineMix_btns( 35 | *, 36 | btns: dict[str, str], 37 | sizes: tuple[int] = (2,)): 38 | 39 | keyboard = InlineKeyboardBuilder() 40 | 41 | for text, value in btns.items(): 42 | if '://' in value: 43 | keyboard.add(InlineKeyboardButton(text=text, url=value)) 44 | else: 45 | keyboard.add(InlineKeyboardButton(text=text, callback_data=value)) 46 | 47 | return keyboard.adjust(*sizes).as_markup() -------------------------------------------------------------------------------- /lesson_8 template/handlers/user_private.py: -------------------------------------------------------------------------------- 1 | from aiogram import F, types, Router 2 | from aiogram.filters import CommandStart 3 | 4 | from sqlalchemy.ext.asyncio import AsyncSession 5 | from database.orm_query import ( 6 | orm_add_to_cart, 7 | orm_add_user, 8 | ) 9 | 10 | from filters.chat_types import ChatTypeFilter 11 | from kbds.inline import get_callback_btns 12 | 13 | 14 | 15 | user_private_router = Router() 16 | user_private_router.message.filter(ChatTypeFilter(["private"])) 17 | 18 | 19 | @user_private_router.message(CommandStart()) 20 | async def start_cmd(message: types.Message): 21 | await message.answer("Привет, я виртуальный помощник", 22 | reply_markup=get_callback_btns(btns={ 23 | 'Нажми меня': 'some_1' 24 | })) 25 | 26 | 27 | @user_private_router.callback_query(F.data.startswith('some_')) 28 | async def counter(callback: types.CallbackQuery): 29 | number = int(callback.data.split('_')[-1]) 30 | 31 | await callback.message.edit_text( 32 | text=f"Нажатий - {number}", 33 | reply_markup=get_callback_btns(btns={ 34 | 'Нажми еще раз': f'some_{number+1}' 35 | })) 36 | 37 | 38 | # Пример для видео как делать не нужно: 39 | # menu_level_menuName_category_page_productID -------------------------------------------------------------------------------- /lesson_7/kbds/reply.py: -------------------------------------------------------------------------------- 1 | from aiogram.types import KeyboardButton 2 | from aiogram.utils.keyboard import ReplyKeyboardBuilder 3 | 4 | 5 | def get_keyboard( 6 | *btns: str, 7 | placeholder: str = None, 8 | request_contact: int = None, 9 | request_location: int = None, 10 | sizes: tuple[int] = (2,), 11 | ): 12 | ''' 13 | Parameters request_contact and request_location must be as indexes of btns args for buttons you need. 14 | Example: 15 | get_keyboard( 16 | "Меню", 17 | "О магазине", 18 | "Варианты оплаты", 19 | "Варианты доставки", 20 | "Отправить номер телефона", 21 | placeholder="Что вас интересует?", 22 | request_contact=4, 23 | sizes=(2, 2, 1) 24 | ) 25 | ''' 26 | keyboard = ReplyKeyboardBuilder() 27 | 28 | for index, text in enumerate(btns, start=0): 29 | 30 | if request_contact and request_contact == index: 31 | keyboard.add(KeyboardButton(text=text, request_contact=True)) 32 | 33 | elif request_location and request_location == index: 34 | keyboard.add(KeyboardButton(text=text, request_location=True)) 35 | else: 36 | keyboard.add(KeyboardButton(text=text)) 37 | 38 | return keyboard.adjust(*sizes).as_markup( 39 | resize_keyboard=True, input_field_placeholder=placeholder) 40 | 41 | 42 | -------------------------------------------------------------------------------- /lesson_6-FSM/kbds/reply.py: -------------------------------------------------------------------------------- 1 | from aiogram.types import KeyboardButton 2 | from aiogram.utils.keyboard import ReplyKeyboardBuilder 3 | 4 | 5 | def get_keyboard( 6 | *btns: str, 7 | placeholder: str = None, 8 | request_contact: int = None, 9 | request_location: int = None, 10 | sizes: tuple[int] = (2,), 11 | ): 12 | ''' 13 | Parameters request_contact and request_location must be as indexes of btns args for buttons you need. 14 | Example: 15 | get_keyboard( 16 | "Меню", 17 | "О магазине", 18 | "Варианты оплаты", 19 | "Варианты доставки", 20 | "Отправить номер телефона", 21 | placeholder="Что вас интересует?", 22 | request_contact=4, 23 | sizes=(2, 2, 1) 24 | ) 25 | ''' 26 | keyboard = ReplyKeyboardBuilder() 27 | 28 | for index, text in enumerate(btns, start=0): 29 | 30 | if request_contact and request_contact == index: 31 | keyboard.add(KeyboardButton(text=text, request_contact=True)) 32 | 33 | elif request_location and request_location == index: 34 | keyboard.add(KeyboardButton(text=text, request_location=True)) 35 | else: 36 | keyboard.add(KeyboardButton(text=text)) 37 | 38 | return keyboard.adjust(*sizes).as_markup( 39 | resize_keyboard=True, input_field_placeholder=placeholder) 40 | 41 | 42 | -------------------------------------------------------------------------------- /lesson_8 template/kbds/reply.py: -------------------------------------------------------------------------------- 1 | from aiogram.types import KeyboardButton 2 | from aiogram.utils.keyboard import ReplyKeyboardBuilder 3 | 4 | 5 | def get_keyboard( 6 | *btns: str, 7 | placeholder: str = None, 8 | request_contact: int = None, 9 | request_location: int = None, 10 | sizes: tuple[int] = (2,), 11 | ): 12 | ''' 13 | Parameters request_contact and request_location must be as indexes of btns args for buttons you need. 14 | Example: 15 | get_keyboard( 16 | "Меню", 17 | "О магазине", 18 | "Варианты оплаты", 19 | "Варианты доставки", 20 | "Отправить номер телефона", 21 | placeholder="Что вас интересует?", 22 | request_contact=4, 23 | sizes=(2, 2, 1) 24 | ) 25 | ''' 26 | keyboard = ReplyKeyboardBuilder() 27 | 28 | for index, text in enumerate(btns, start=0): 29 | 30 | if request_contact and request_contact == index: 31 | keyboard.add(KeyboardButton(text=text, request_contact=True)) 32 | 33 | elif request_location and request_location == index: 34 | keyboard.add(KeyboardButton(text=text, request_location=True)) 35 | else: 36 | keyboard.add(KeyboardButton(text=text)) 37 | 38 | return keyboard.adjust(*sizes).as_markup( 39 | resize_keyboard=True, input_field_placeholder=placeholder) 40 | 41 | 42 | -------------------------------------------------------------------------------- /lesson_4/handlers/user_private.py: -------------------------------------------------------------------------------- 1 | from aiogram import F, types, Router 2 | from aiogram.filters import CommandStart, Command, or_f 3 | from filters.chat_types import ChatTypeFilter 4 | 5 | user_private_router = Router() 6 | user_private_router.message.filter(ChatTypeFilter(['private'])) 7 | 8 | @user_private_router.message(CommandStart()) 9 | async def start_cmd(message: types.Message): 10 | await message.answer("Привет, я виртуальный помощник") 11 | 12 | 13 | # @user_private_router.message(F.text.lower() == "меню") 14 | @user_private_router.message(or_f(Command("menu"), (F.text.lower() == "меню"))) 15 | async def menu_cmd(message: types.Message): 16 | await message.answer("Вот меню:") 17 | 18 | 19 | @user_private_router.message(F.text.lower() == "о нас") 20 | @user_private_router.message(Command("about")) 21 | async def about_cmd(message: types.Message): 22 | await message.answer("О нас:") 23 | 24 | 25 | @user_private_router.message(F.text.lower() == "варианты оплаты") 26 | @user_private_router.message(Command("payment")) 27 | async def payment_cmd(message: types.Message): 28 | await message.answer("Варианты оплаты:") 29 | 30 | 31 | @user_private_router.message((F.text.lower().contains('доставк')) | (F.text.lower() == 'варианты доставки')) 32 | @user_private_router.message(Command("shipping")) 33 | async def menu_cmd(message: types.Message): 34 | await message.answer("Варианты доставки:") 35 | 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /lesson_8_multi_level_inline_menu/kbds/reply.py: -------------------------------------------------------------------------------- 1 | from aiogram.types import KeyboardButton 2 | from aiogram.utils.keyboard import ReplyKeyboardBuilder 3 | 4 | 5 | def get_keyboard( 6 | *btns: str, 7 | placeholder: str = None, 8 | request_contact: int = None, 9 | request_location: int = None, 10 | sizes: tuple[int] = (2,), 11 | ): 12 | ''' 13 | Parameters request_contact and request_location must be as indexes of btns args for buttons you need. 14 | Example: 15 | get_keyboard( 16 | "Меню", 17 | "О магазине", 18 | "Варианты оплаты", 19 | "Варианты доставки", 20 | "Отправить номер телефона", 21 | placeholder="Что вас интересует?", 22 | request_contact=4, 23 | sizes=(2, 2, 1) 24 | ) 25 | ''' 26 | keyboard = ReplyKeyboardBuilder() 27 | 28 | for index, text in enumerate(btns, start=0): 29 | 30 | if request_contact and request_contact == index: 31 | keyboard.add(KeyboardButton(text=text, request_contact=True)) 32 | 33 | elif request_location and request_location == index: 34 | keyboard.add(KeyboardButton(text=text, request_location=True)) 35 | else: 36 | keyboard.add(KeyboardButton(text=text)) 37 | 38 | return keyboard.adjust(*sizes).as_markup( 39 | resize_keyboard=True, input_field_placeholder=placeholder) 40 | 41 | 42 | -------------------------------------------------------------------------------- /lesson_6 -Prepared_template_for_review/kbds/reply.py: -------------------------------------------------------------------------------- 1 | from aiogram.types import KeyboardButton 2 | from aiogram.utils.keyboard import ReplyKeyboardBuilder 3 | 4 | 5 | def get_keyboard( 6 | *btns: str, 7 | placeholder: str = None, 8 | request_contact: int = None, 9 | request_location: int = None, 10 | sizes: tuple[int] = (2,), 11 | ): 12 | ''' 13 | Parameters request_contact and request_location must be as indexes of btns args for buttons you need. 14 | Example: 15 | get_keyboard( 16 | "Меню", 17 | "О магазине", 18 | "Варианты оплаты", 19 | "Варианты доставки", 20 | "Отправить номер телефона" 21 | placeholder="Что вас интересует?", 22 | request_contact=4, 23 | sizes=(2, 2, 1) 24 | ) 25 | ''' 26 | keyboard = ReplyKeyboardBuilder() 27 | 28 | for index, text in enumerate(btns, start=0): 29 | 30 | if request_contact and request_contact == index: 31 | keyboard.add(KeyboardButton(text=text, request_contact=True)) 32 | 33 | elif request_location and request_location == index: 34 | keyboard.add(KeyboardButton(text=text, request_location=True)) 35 | else: 36 | 37 | keyboard.add(KeyboardButton(text=text)) 38 | 39 | return keyboard.adjust(*sizes).as_markup( 40 | resize_keyboard=True, input_field_placeholder=placeholder) 41 | 42 | 43 | -------------------------------------------------------------------------------- /lesson_8_multi_level_inline_menu/utils/paginator.py: -------------------------------------------------------------------------------- 1 | import math 2 | 3 | 4 | # Простой пагинатор 5 | class Paginator: 6 | def __init__(self, array: list | tuple, page: int=1, per_page: int=1): 7 | self.array = array 8 | self.per_page = per_page 9 | self.page = page 10 | self.len = len(self.array) 11 | # math.ceil - округление в большую сторону до целого числа 12 | self.pages = math.ceil(self.len / self.per_page) 13 | 14 | def __get_slice(self): 15 | start = (self.page - 1) * self.per_page 16 | stop = start + self.per_page 17 | return self.array[start:stop] 18 | 19 | def get_page(self): 20 | page_items = self.__get_slice() 21 | return page_items 22 | 23 | def has_next(self): 24 | if self.page < self.pages: 25 | return self.page + 1 26 | return False 27 | 28 | def has_previous(self): 29 | if self.page > 1: 30 | return self.page - 1 31 | return False 32 | 33 | def get_next(self): 34 | if self.page < self.pages: 35 | self.page += 1 36 | return self.get_page() 37 | raise IndexError(f'Next page does not exist. Use has_next() to check before.') 38 | 39 | def get_previous(self): 40 | if self.page > 1: 41 | self.page -= 1 42 | return self.__get_slice() 43 | raise IndexError(f'Previous page does not exist. Use has_previous() to check before.') -------------------------------------------------------------------------------- /lesson_7/database/orm_query.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy import select, update, delete 2 | from sqlalchemy.ext.asyncio import AsyncSession 3 | 4 | from database.models import Product 5 | 6 | 7 | async def orm_add_product(session: AsyncSession, data: dict): 8 | obj = Product( 9 | name=data["name"], 10 | description=data["description"], 11 | price=float(data["price"]), 12 | image=data["image"], 13 | ) 14 | session.add(obj) 15 | await session.commit() 16 | 17 | 18 | async def orm_get_products(session: AsyncSession): 19 | query = select(Product) 20 | result = await session.execute(query) 21 | return result.scalars().all() 22 | 23 | 24 | async def orm_get_product(session: AsyncSession, product_id: int): 25 | query = select(Product).where(Product.id == product_id) 26 | result = await session.execute(query) 27 | return result.scalar() 28 | 29 | 30 | async def orm_update_product(session: AsyncSession, product_id: int, data): 31 | query = update(Product).where(Product.id == product_id).values( 32 | name=data["name"], 33 | description=data["description"], 34 | price=float(data["price"]), 35 | image=data["image"],) 36 | await session.execute(query) 37 | await session.commit() 38 | 39 | 40 | async def orm_delete_product(session: AsyncSession, product_id: int): 41 | query = delete(Product).where(Product.id == product_id) 42 | await session.execute(query) 43 | await session.commit() 44 | -------------------------------------------------------------------------------- /lesson_5/kbds/reply.py: -------------------------------------------------------------------------------- 1 | from aiogram.types import KeyboardButtonPollType, ReplyKeyboardMarkup, KeyboardButton, ReplyKeyboardRemove 2 | from aiogram.utils.keyboard import ReplyKeyboardBuilder 3 | 4 | 5 | start_kb = ReplyKeyboardMarkup( 6 | keyboard=[ 7 | [ 8 | KeyboardButton(text="Меню"), 9 | KeyboardButton(text="О магазине"), 10 | ], 11 | { 12 | KeyboardButton(text="Варианты доставки"), 13 | KeyboardButton(text="Варианты оплаты"), 14 | } 15 | ], 16 | resize_keyboard=True, 17 | input_field_placeholder='Что Вас интересует?' 18 | ) 19 | 20 | del_kbd = ReplyKeyboardRemove() 21 | 22 | 23 | start_kb2 = ReplyKeyboardBuilder() 24 | start_kb2.add( 25 | KeyboardButton(text="Меню"), 26 | KeyboardButton(text="О магазине"), 27 | KeyboardButton(text="Варианты доставки"), 28 | KeyboardButton(text="Варианты оплаты"), 29 | ) 30 | start_kb2.adjust(2, 2) 31 | 32 | 33 | start_kb3 = ReplyKeyboardBuilder() 34 | start_kb3.attach(start_kb2) 35 | start_kb3.row(KeyboardButton(text="Оставить отзыв"),) 36 | 37 | 38 | test_kb = ReplyKeyboardMarkup( 39 | keyboard=[ 40 | [ 41 | KeyboardButton(text="Создать опрос", request_poll=KeyboardButtonPollType()), 42 | ], 43 | [ 44 | KeyboardButton(text="Отправить номер ☎️", request_contact=True), 45 | KeyboardButton(text="Отправить локацию 🗺️", request_location=True), 46 | ], 47 | ], 48 | resize_keyboard=True, 49 | ) 50 | 51 | 52 | -------------------------------------------------------------------------------- /lesson_8 template/app.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import os 3 | 4 | from aiogram import Bot, Dispatcher, types 5 | from aiogram.enums import ParseMode 6 | 7 | from dotenv import find_dotenv, load_dotenv 8 | 9 | load_dotenv(find_dotenv()) 10 | 11 | from middlewares.db import DataBaseSession 12 | 13 | from database.engine import create_db, drop_db, session_maker 14 | 15 | from handlers.user_private import user_private_router 16 | from handlers.user_group import user_group_router 17 | from handlers.admin_private import admin_router 18 | 19 | # from common.bot_cmds_list import private 20 | 21 | 22 | # ALLOWED_UPDATES = ['message', 'edited_message', 'callback_query'] 23 | 24 | bot = Bot(token=os.getenv('TOKEN'), parse_mode=ParseMode.HTML) 25 | bot.my_admins_list = [] 26 | 27 | dp = Dispatcher() 28 | 29 | dp.include_router(user_private_router) 30 | dp.include_router(user_group_router) 31 | dp.include_router(admin_router) 32 | 33 | 34 | async def on_startup(bot): 35 | 36 | # await drop_db() 37 | 38 | await create_db() 39 | 40 | 41 | async def on_shutdown(bot): 42 | print('бот лег') 43 | 44 | 45 | async def main(): 46 | dp.startup.register(on_startup) 47 | dp.shutdown.register(on_shutdown) 48 | 49 | dp.update.middleware(DataBaseSession(session_pool=session_maker)) 50 | 51 | await bot.delete_webhook(drop_pending_updates=True) 52 | # await bot.delete_my_commands(scope=types.BotCommandScopeAllPrivateChats()) 53 | # await bot.set_my_commands(commands=private, scope=types.BotCommandScopeAllPrivateChats()) 54 | await dp.start_polling(bot, allowed_updates=dp.resolve_used_update_types()) 55 | 56 | asyncio.run(main()) 57 | -------------------------------------------------------------------------------- /lesson_8_multi_level_inline_menu/app.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import os 3 | 4 | from aiogram import Bot, Dispatcher, types 5 | from aiogram.enums import ParseMode 6 | 7 | from dotenv import find_dotenv, load_dotenv 8 | 9 | load_dotenv(find_dotenv()) 10 | 11 | from middlewares.db import DataBaseSession 12 | 13 | from database.engine import create_db, drop_db, session_maker 14 | 15 | from handlers.user_private import user_private_router 16 | from handlers.user_group import user_group_router 17 | from handlers.admin_private import admin_router 18 | 19 | # from common.bot_cmds_list import private 20 | 21 | 22 | # ALLOWED_UPDATES = ['message', 'edited_message', 'callback_query'] 23 | 24 | bot = Bot(token=os.getenv('TOKEN'), parse_mode=ParseMode.HTML) 25 | bot.my_admins_list = [] 26 | 27 | dp = Dispatcher() 28 | 29 | dp.include_router(user_private_router) 30 | dp.include_router(user_group_router) 31 | dp.include_router(admin_router) 32 | 33 | 34 | async def on_startup(bot): 35 | 36 | # await drop_db() 37 | 38 | await create_db() 39 | 40 | 41 | async def on_shutdown(bot): 42 | print('бот лег') 43 | 44 | 45 | async def main(): 46 | dp.startup.register(on_startup) 47 | dp.shutdown.register(on_shutdown) 48 | 49 | dp.update.middleware(DataBaseSession(session_pool=session_maker)) 50 | 51 | await bot.delete_webhook(drop_pending_updates=True) 52 | # await bot.delete_my_commands(scope=types.BotCommandScopeAllPrivateChats()) 53 | # await bot.set_my_commands(commands=private, scope=types.BotCommandScopeAllPrivateChats()) 54 | await dp.start_polling(bot, allowed_updates=dp.resolve_used_update_types()) 55 | 56 | asyncio.run(main()) 57 | -------------------------------------------------------------------------------- /lesson_7/app.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import os 3 | 4 | from aiogram import Bot, Dispatcher, types 5 | from aiogram.enums import ParseMode 6 | 7 | from dotenv import find_dotenv, load_dotenv 8 | 9 | load_dotenv(find_dotenv()) 10 | 11 | from middlewares.db import DataBaseSession 12 | 13 | from database.engine import create_db, drop_db, session_maker 14 | 15 | from handlers.user_private import user_private_router 16 | from handlers.user_group import user_group_router 17 | from handlers.admin_private import admin_router 18 | 19 | from common.bot_cmds_list import private 20 | 21 | 22 | # ALLOWED_UPDATES = ['message', 'edited_message', 'callback_query'] 23 | 24 | bot = Bot(token=os.getenv('TOKEN'), parse_mode=ParseMode.HTML) 25 | bot.my_admins_list = [] 26 | 27 | dp = Dispatcher() 28 | 29 | dp.include_router(user_private_router) 30 | dp.include_router(user_group_router) 31 | dp.include_router(admin_router) 32 | 33 | 34 | async def on_startup(bot): 35 | 36 | run_param = False 37 | if run_param: 38 | await drop_db() 39 | 40 | await create_db() 41 | 42 | 43 | async def on_shutdown(bot): 44 | print('бот лег') 45 | 46 | 47 | async def main(): 48 | dp.startup.register(on_startup) 49 | dp.shutdown.register(on_shutdown) 50 | 51 | dp.update.middleware(DataBaseSession(session_pool=session_maker)) 52 | 53 | await bot.delete_webhook(drop_pending_updates=True) 54 | # await bot.delete_my_commands(scope=types.BotCommandScopeAllPrivateChats()) 55 | await bot.set_my_commands(commands=private, scope=types.BotCommandScopeAllPrivateChats()) 56 | await dp.start_polling(bot, allowed_updates=dp.resolve_used_update_types()) 57 | 58 | asyncio.run(main()) 59 | -------------------------------------------------------------------------------- /lesson_7/handlers/user_group.py: -------------------------------------------------------------------------------- 1 | from string import punctuation 2 | 3 | from aiogram import F, Bot, types, Router 4 | from aiogram.filters import Command 5 | 6 | from filters.chat_types import ChatTypeFilter 7 | from common.restricted_words import restricted_words 8 | 9 | 10 | user_group_router = Router() 11 | user_group_router.message.filter(ChatTypeFilter(["group", "supergroup"])) 12 | user_group_router.edited_message.filter(ChatTypeFilter(["group", "supergroup"])) 13 | 14 | 15 | @user_group_router.message(Command("admin")) 16 | async def get_admins(message: types.Message, bot: Bot): 17 | chat_id = message.chat.id 18 | admins_list = await bot.get_chat_administrators(chat_id) 19 | #просмотреть все данные и свойства полученных объектов 20 | #print(admins_list) 21 | # Код ниже это генератор списка, как и этот x = [i for i in range(10)] 22 | admins_list = [ 23 | member.user.id 24 | for member in admins_list 25 | if member.status == "creator" or member.status == "administrator" 26 | ] 27 | bot.my_admins_list = admins_list 28 | if message.from_user.id in admins_list: 29 | await message.delete() 30 | #print(admins_list) 31 | 32 | 33 | def clean_text(text: str): 34 | return text.translate(str.maketrans("", "", punctuation)) 35 | 36 | 37 | @user_group_router.edited_message() 38 | @user_group_router.message() 39 | async def cleaner(message: types.Message): 40 | if restricted_words.intersection(clean_text(message.text.lower()).split()): 41 | await message.answer( 42 | f"{message.from_user.first_name}, соблюддайте порядок в чате!" 43 | ) 44 | await message.delete() 45 | # await message.chat.ban(message.from_user.id) 46 | -------------------------------------------------------------------------------- /lesson_6-FSM/handlers/user_group.py: -------------------------------------------------------------------------------- 1 | from string import punctuation 2 | 3 | from aiogram import F, Bot, types, Router 4 | from aiogram.filters import Command 5 | 6 | from filters.chat_types import ChatTypeFilter 7 | from common.restricted_words import restricted_words 8 | 9 | 10 | user_group_router = Router() 11 | user_group_router.message.filter(ChatTypeFilter(["group", "supergroup"])) 12 | user_group_router.edited_message.filter(ChatTypeFilter(["group", "supergroup"])) 13 | 14 | 15 | @user_group_router.message(Command("admin")) 16 | async def get_admins(message: types.Message, bot: Bot): 17 | chat_id = message.chat.id 18 | admins_list = await bot.get_chat_administrators(chat_id) 19 | #просмотреть все данные и свойства полученных объектов 20 | #print(admins_list) 21 | # Код ниже это генератор списка, как и этот x = [i for i in range(10)] 22 | admins_list = [ 23 | member.user.id 24 | for member in admins_list 25 | if member.status == "creator" or member.status == "administrator" 26 | ] 27 | bot.my_admins_list = admins_list 28 | if message.from_user.id in admins_list: 29 | await message.delete() 30 | #print(admins_list) 31 | 32 | 33 | def clean_text(text: str): 34 | return text.translate(str.maketrans("", "", punctuation)) 35 | 36 | 37 | @user_group_router.edited_message() 38 | @user_group_router.message() 39 | async def cleaner(message: types.Message): 40 | if restricted_words.intersection(clean_text(message.text.lower()).split()): 41 | await message.answer( 42 | f"{message.from_user.first_name}, соблюддайте порядок в чате!" 43 | ) 44 | await message.delete() 45 | # await message.chat.ban(message.from_user.id) 46 | -------------------------------------------------------------------------------- /lesson_8 template/handlers/user_group.py: -------------------------------------------------------------------------------- 1 | from string import punctuation 2 | 3 | from aiogram import F, Bot, types, Router 4 | from aiogram.filters import Command 5 | 6 | from filters.chat_types import ChatTypeFilter 7 | from common.restricted_words import restricted_words 8 | 9 | 10 | user_group_router = Router() 11 | user_group_router.message.filter(ChatTypeFilter(["group", "supergroup"])) 12 | user_group_router.edited_message.filter(ChatTypeFilter(["group", "supergroup"])) 13 | 14 | 15 | @user_group_router.message(Command("admin")) 16 | async def get_admins(message: types.Message, bot: Bot): 17 | chat_id = message.chat.id 18 | admins_list = await bot.get_chat_administrators(chat_id) 19 | #просмотреть все данные и свойства полученных объектов 20 | #print(admins_list) 21 | # Код ниже это генератор списка, как и этот x = [i for i in range(10)] 22 | admins_list = [ 23 | member.user.id 24 | for member in admins_list 25 | if member.status == "creator" or member.status == "administrator" 26 | ] 27 | bot.my_admins_list = admins_list 28 | if message.from_user.id in admins_list: 29 | await message.delete() 30 | #print(admins_list) 31 | 32 | 33 | def clean_text(text: str): 34 | return text.translate(str.maketrans("", "", punctuation)) 35 | 36 | 37 | @user_group_router.edited_message() 38 | @user_group_router.message() 39 | async def cleaner(message: types.Message): 40 | if restricted_words.intersection(clean_text(message.text.lower()).split()): 41 | await message.answer( 42 | f"{message.from_user.first_name}, соблюддайте порядок в чате!" 43 | ) 44 | await message.delete() 45 | # await message.chat.ban(message.from_user.id) 46 | -------------------------------------------------------------------------------- /lesson_8_multi_level_inline_menu/handlers/user_group.py: -------------------------------------------------------------------------------- 1 | from string import punctuation 2 | 3 | from aiogram import F, Bot, types, Router 4 | from aiogram.filters import Command 5 | 6 | from filters.chat_types import ChatTypeFilter 7 | from common.restricted_words import restricted_words 8 | 9 | 10 | user_group_router = Router() 11 | user_group_router.message.filter(ChatTypeFilter(["group", "supergroup"])) 12 | user_group_router.edited_message.filter(ChatTypeFilter(["group", "supergroup"])) 13 | 14 | 15 | @user_group_router.message(Command("admin")) 16 | async def get_admins(message: types.Message, bot: Bot): 17 | chat_id = message.chat.id 18 | admins_list = await bot.get_chat_administrators(chat_id) 19 | #просмотреть все данные и свойства полученных объектов 20 | #print(admins_list) 21 | # Код ниже это генератор списка, как и этот x = [i for i in range(10)] 22 | admins_list = [ 23 | member.user.id 24 | for member in admins_list 25 | if member.status == "creator" or member.status == "administrator" 26 | ] 27 | bot.my_admins_list = admins_list 28 | if message.from_user.id in admins_list: 29 | await message.delete() 30 | #print(admins_list) 31 | 32 | 33 | def clean_text(text: str): 34 | return text.translate(str.maketrans("", "", punctuation)) 35 | 36 | 37 | @user_group_router.edited_message() 38 | @user_group_router.message() 39 | async def cleaner(message: types.Message): 40 | if restricted_words.intersection(clean_text(message.text.lower()).split()): 41 | await message.answer( 42 | f"{message.from_user.first_name}, соблюддайте порядок в чате!" 43 | ) 44 | await message.delete() 45 | # await message.chat.ban(message.from_user.id) 46 | -------------------------------------------------------------------------------- /lesson_6 -Prepared_template_for_review/handlers/user_group.py: -------------------------------------------------------------------------------- 1 | from string import punctuation 2 | 3 | from aiogram import F, Bot, types, Router 4 | from aiogram.filters import Command 5 | 6 | from filters.chat_types import ChatTypeFilter 7 | from common.restricted_words import restricted_words 8 | 9 | 10 | user_group_router = Router() 11 | user_group_router.message.filter(ChatTypeFilter(["group", "supergroup"])) 12 | user_group_router.edited_message.filter(ChatTypeFilter(["group", "supergroup"])) 13 | 14 | 15 | @user_group_router.message(Command("admin")) 16 | async def get_admins(message: types.Message, bot: Bot): 17 | chat_id = message.chat.id 18 | admins_list = await bot.get_chat_administrators(chat_id) 19 | #просмотреть все данные и свойства полученных объектов 20 | #print(admins_list) 21 | # Код ниже это генератор списка, как и этот x = [i for i in range(10)] 22 | admins_list = [ 23 | member.user.id 24 | for member in admins_list 25 | if member.status == "creator" or member.status == "administrator" 26 | ] 27 | bot.my_admins_list = admins_list 28 | if message.from_user.id in admins_list: 29 | await message.delete() 30 | #print(admins_list) 31 | 32 | 33 | def clean_text(text: str): 34 | return text.translate(str.maketrans("", "", punctuation)) 35 | 36 | 37 | @user_group_router.edited_message() 38 | @user_group_router.message() 39 | async def cleaner(message: types.Message): 40 | if restricted_words.intersection(clean_text(message.text.lower()).split()): 41 | await message.answer( 42 | f"{message.from_user.first_name}, соблюддайте порядок в чате!" 43 | ) 44 | await message.delete() 45 | # await message.chat.ban(message.from_user.id) 46 | -------------------------------------------------------------------------------- /lesson_8 template/README.md: -------------------------------------------------------------------------------- 1 | # В видео №8 это расскажу обязательно, это легкая предподготовка, но вот изменения: 2 | В видео №8 будем работать с многоуровневым инлайн меню, каталогом, пагинацией и корзиной товаров пользователя. 3 | 4 | 1) Добавились новые таблицы БД, в том числе и категории товаров. 5 | 2) Подготовлены функции с нужными запросыми к ORM 6 | 3) В FSM админа и командах на просмотр/добавления/изменения соответственно тоже появился пункт категории 7 | 4) Исправил пару косяков в FSM по Вашим комментам 8 | 5) При старте бота автоматически наполняются таблицы: category и banner (бод баннером подразумевается пункт будущего инлайн меню не отображающий товары и прочее, а содержащий справочную информацию о about и тд... 9 | 6) Добавлена "микро" FSM для админа, для добавления изображения для отображения справочной информации о магазине, вроде about, варианты оплаты, доставки и тд. 10 | 7) Команды пользователя убраны, так как в дальнейшем не понадобятся. Чтоб убрать кнопку меню с ними в вашем боте - раскоментируйте на один запуск эту строчку в app.py -> main: 11 | ``` 12 | async def main(): 13 | dp.startup.register(on_startup) 14 | dp.shutdown.register(on_shutdown) 15 | 16 | dp.update.middleware(DataBaseSession(session_pool=session_maker)) 17 | 18 | await bot.delete_webhook(drop_pending_updates=True) 19 | 👇👇👇Эту 20 | # await bot.delete_my_commands(scope=types.BotCommandScopeAllPrivateChats()) 👈<-------------ЭТУ 21 | # await bot.set_my_commands(commands=private, scope=types.BotCommandScopeAllPrivateChats()) 22 | await dp.start_polling(bot, allowed_updates=dp.resolve_used_update_types()) 23 | ``` 24 | 8) Старая схема БД не подойдет для этого бота, поэтому при запуске этого бота в !первый раз - удалить старые таблицы (это уже предусмотрено, нужно раскоментировать на !первый запуск эту строчку кода в файле app.py: 25 | 26 | ``` 27 | async def on_startup(bot): 28 | 👇👇👇Эту 29 | # await drop_db() 👈 <------ЭТУ 30 | 31 | await create_db() 32 | ``` 33 | См. следующее видео - это все будет рассказано. 34 | -------------------------------------------------------------------------------- /lesson_8_multi_level_inline_menu/handlers/user_private.py: -------------------------------------------------------------------------------- 1 | from aiogram import F, types, Router 2 | from aiogram.filters import CommandStart 3 | 4 | from sqlalchemy.ext.asyncio import AsyncSession 5 | from database.orm_query import ( 6 | orm_add_to_cart, 7 | orm_add_user, 8 | ) 9 | 10 | from filters.chat_types import ChatTypeFilter 11 | from handlers.menu_processing import get_menu_content 12 | from kbds.inline import MenuCallBack, get_callback_btns 13 | 14 | 15 | 16 | user_private_router = Router() 17 | user_private_router.message.filter(ChatTypeFilter(["private"])) 18 | 19 | 20 | @user_private_router.message(CommandStart()) 21 | async def start_cmd(message: types.Message, session: AsyncSession): 22 | media, reply_markup = await get_menu_content(session, level=0, menu_name="main") 23 | 24 | await message.answer_photo(media.media, caption=media.caption, reply_markup=reply_markup) 25 | 26 | 27 | async def add_to_cart(callback: types.CallbackQuery, callback_data: MenuCallBack, session: AsyncSession): 28 | user = callback.from_user 29 | await orm_add_user( 30 | session, 31 | user_id=user.id, 32 | first_name=user.first_name, 33 | last_name=user.last_name, 34 | phone=None, 35 | ) 36 | await orm_add_to_cart(session, user_id=user.id, product_id=callback_data.product_id) 37 | await callback.answer("Товар добавлен в корзину.") 38 | 39 | 40 | @user_private_router.callback_query(MenuCallBack.filter()) 41 | async def user_menu(callback: types.CallbackQuery, callback_data: MenuCallBack, session: AsyncSession): 42 | 43 | if callback_data.menu_name == "add_to_cart": 44 | await add_to_cart(callback, callback_data, session) 45 | return 46 | 47 | media, reply_markup = await get_menu_content( 48 | session, 49 | level=callback_data.level, 50 | menu_name=callback_data.menu_name, 51 | category=callback_data.category, 52 | page=callback_data.page, 53 | product_id=callback_data.product_id, 54 | user_id=callback.from_user.id, 55 | ) 56 | 57 | await callback.message.edit_media(media=media, reply_markup=reply_markup) 58 | await callback.answer() -------------------------------------------------------------------------------- /lesson_8 template/database/models.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy import DateTime, ForeignKey, Numeric, String, Text, BigInteger, func 2 | from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, relationship 3 | 4 | 5 | class Base(DeclarativeBase): 6 | created: Mapped[DateTime] = mapped_column(DateTime, default=func.now()) 7 | updated: Mapped[DateTime] = mapped_column(DateTime, default=func.now(), onupdate=func.now()) 8 | 9 | 10 | class Banner(Base): 11 | __tablename__ = 'banner' 12 | 13 | id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True) 14 | name: Mapped[str] = mapped_column(String(15), unique=True) 15 | image: Mapped[str] = mapped_column(String(150), nullable=True) 16 | description: Mapped[str] = mapped_column(Text, nullable=True) 17 | 18 | 19 | class Category(Base): 20 | __tablename__ = 'category' 21 | 22 | id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True) 23 | name: Mapped[str] = mapped_column(String(150), nullable=False) 24 | 25 | 26 | class Product(Base): 27 | __tablename__ = 'product' 28 | 29 | id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True) 30 | name: Mapped[str] = mapped_column(String(150), nullable=False) 31 | description: Mapped[str] = mapped_column(Text) 32 | price: Mapped[float] = mapped_column(Numeric(5,2), nullable=False) 33 | image: Mapped[str] = mapped_column(String(150)) 34 | category_id: Mapped[int] = mapped_column(ForeignKey('category.id', ondelete='CASCADE'), nullable=False) 35 | 36 | category: Mapped['Category'] = relationship(backref='product') 37 | 38 | 39 | class User(Base): 40 | __tablename__ = 'user' 41 | 42 | id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True) 43 | user_id: Mapped[int] = mapped_column(BigInteger, unique=True) 44 | first_name: Mapped[str] = mapped_column(String(150), nullable=True) 45 | last_name: Mapped[str] = mapped_column(String(150), nullable=True) 46 | phone: Mapped[str] = mapped_column(String(13), nullable=True) 47 | 48 | 49 | class Cart(Base): 50 | __tablename__ = 'cart' 51 | 52 | id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True) 53 | user_id: Mapped[int] = mapped_column(ForeignKey('user.user_id', ondelete='CASCADE'), nullable=False) 54 | product_id: Mapped[int] = mapped_column(ForeignKey('product.id', ondelete='CASCADE'), nullable=False) 55 | quantity: Mapped[int] 56 | 57 | user: Mapped['User'] = relationship(backref='cart') 58 | product: Mapped['Product'] = relationship(backref='cart') 59 | 60 | -------------------------------------------------------------------------------- /lesson_8_multi_level_inline_menu/database/models.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy import DateTime, ForeignKey, Numeric, String, Text, BigInteger, func 2 | from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, relationship 3 | 4 | 5 | class Base(DeclarativeBase): 6 | created: Mapped[DateTime] = mapped_column(DateTime, default=func.now()) 7 | updated: Mapped[DateTime] = mapped_column(DateTime, default=func.now(), onupdate=func.now()) 8 | 9 | 10 | class Banner(Base): 11 | __tablename__ = 'banner' 12 | 13 | id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True) 14 | name: Mapped[str] = mapped_column(String(15), unique=True) 15 | image: Mapped[str] = mapped_column(String(150), nullable=True) 16 | description: Mapped[str] = mapped_column(Text, nullable=True) 17 | 18 | 19 | class Category(Base): 20 | __tablename__ = 'category' 21 | 22 | id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True) 23 | name: Mapped[str] = mapped_column(String(150), nullable=False) 24 | 25 | 26 | class Product(Base): 27 | __tablename__ = 'product' 28 | 29 | id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True) 30 | name: Mapped[str] = mapped_column(String(150), nullable=False) 31 | description: Mapped[str] = mapped_column(Text) 32 | price: Mapped[float] = mapped_column(Numeric(5,2), nullable=False) 33 | image: Mapped[str] = mapped_column(String(150)) 34 | category_id: Mapped[int] = mapped_column(ForeignKey('category.id', ondelete='CASCADE'), nullable=False) 35 | 36 | category: Mapped['Category'] = relationship(backref='product') 37 | 38 | 39 | class User(Base): 40 | __tablename__ = 'user' 41 | 42 | id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True) 43 | user_id: Mapped[int] = mapped_column(BigInteger, unique=True) 44 | first_name: Mapped[str] = mapped_column(String(150), nullable=True) 45 | last_name: Mapped[str] = mapped_column(String(150), nullable=True) 46 | phone: Mapped[str] = mapped_column(String(13), nullable=True) 47 | 48 | 49 | class Cart(Base): 50 | __tablename__ = 'cart' 51 | 52 | id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True) 53 | user_id: Mapped[int] = mapped_column(ForeignKey('user.user_id', ondelete='CASCADE'), nullable=False) 54 | product_id: Mapped[int] = mapped_column(ForeignKey('product.id', ondelete='CASCADE'), nullable=False) 55 | quantity: Mapped[int] 56 | 57 | user: Mapped['User'] = relationship(backref='cart') 58 | product: Mapped['Product'] = relationship(backref='cart') 59 | 60 | -------------------------------------------------------------------------------- /lesson_6 -Prepared_template_for_review/handlers/admin_private.py: -------------------------------------------------------------------------------- 1 | from aiogram import F, Router, types 2 | from aiogram.filters import Command 3 | 4 | from filters.chat_types import ChatTypeFilter, IsAdmin 5 | from kbds.reply import get_keyboard 6 | 7 | 8 | admin_router = Router() 9 | admin_router.message.filter(ChatTypeFilter(["private"]), IsAdmin()) 10 | 11 | 12 | ADMIN_KB = get_keyboard( 13 | "Добавить товар", 14 | "Изменить товар", 15 | "Удалить товар", 16 | "Я так, просто посмотреть зашел", 17 | placeholder="Выберите действие", 18 | sizes=(2, 1, 1), 19 | ) 20 | 21 | 22 | @admin_router.message(Command("admin")) 23 | async def add_product(message: types.Message): 24 | await message.answer("Что хотите сделать?", reply_markup=ADMIN_KB) 25 | 26 | 27 | @admin_router.message(F.text == "Я так, просто посмотреть зашел") 28 | async def starring_at_product(message: types.Message): 29 | await message.answer("ОК, вот список товаров") 30 | 31 | 32 | @admin_router.message(F.text == "Изменить товар") 33 | async def change_product(message: types.Message): 34 | await message.answer("ОК, вот список товаров") 35 | 36 | 37 | @admin_router.message(F.text == "Удалить товар") 38 | async def delete_product(message: types.Message): 39 | await message.answer("Выберите товар(ы) для удаления") 40 | 41 | 42 | #Код ниже для машины состояний (FSM) 43 | 44 | @admin_router.message(F.text == "Добавить товар") 45 | async def add_product(message: types.Message): 46 | await message.answer( 47 | "Введите название товара", reply_markup=types.ReplyKeyboardRemove() 48 | ) 49 | 50 | 51 | @admin_router.message(Command("отмена")) 52 | @admin_router.message(F.text.casefold() == "отмена") 53 | async def cancel_handler(message: types.Message) -> None: 54 | await message.answer("Действия отменены", reply_markup=ADMIN_KB) 55 | 56 | 57 | @admin_router.message(Command("назад")) 58 | @admin_router.message(F.text.casefold() == "назад") 59 | async def cancel_handler(message: types.Message) -> None: 60 | await message.answer(f"ок, вы вернулись к прошлому шагу") 61 | 62 | 63 | @admin_router.message(F.text) 64 | async def add_name(message: types.Message): 65 | await message.answer("Введите описание товара") 66 | 67 | 68 | @admin_router.message(F.text) 69 | async def add_description(message: types.Message): 70 | await message.answer("Введите стоимость товара") 71 | 72 | 73 | @admin_router.message(F.text) 74 | async def add_price(message: types.Message): 75 | await message.answer("Загрузите изображение товара") 76 | 77 | 78 | @admin_router.message(F.photo) 79 | async def add_image(message: types.Message): 80 | await message.answer("Товар добавлен", reply_markup=ADMIN_KB) 81 | -------------------------------------------------------------------------------- /lesson_5/handlers/user_private.py: -------------------------------------------------------------------------------- 1 | from aiogram import F, types, Router 2 | from aiogram.enums import ParseMode 3 | from aiogram.filters import CommandStart, Command, or_f 4 | from aiogram.utils.formatting import as_list, as_marked_section, Bold #Italic, as_numbered_list и тд 5 | 6 | from filters.chat_types import ChatTypeFilter 7 | 8 | from kbds import reply 9 | 10 | user_private_router = Router() 11 | user_private_router.message.filter(ChatTypeFilter(["private"])) 12 | 13 | 14 | @user_private_router.message(CommandStart()) 15 | async def start_cmd(message: types.Message): 16 | await message.answer("Привет, я виртуальный помощник", 17 | reply_markup=reply.start_kb3.as_markup( 18 | resize_keyboard=True, 19 | input_field_placeholder='Что Вас интересует?')) 20 | 21 | 22 | # @user_private_router.message(F.text.lower() == "меню") 23 | @user_private_router.message(or_f(Command("menu"), (F.text.lower() == "меню"))) 24 | async def menu_cmd(message: types.Message): 25 | await message.answer("Вот меню:") 26 | 27 | 28 | @user_private_router.message(F.text.lower() == "о магазине") 29 | @user_private_router.message(Command("about")) 30 | async def about_cmd(message: types.Message): 31 | await message.answer("О нас:") 32 | 33 | 34 | @user_private_router.message(F.text.lower() == "варианты оплаты") 35 | @user_private_router.message(Command("payment")) 36 | async def payment_cmd(message: types.Message): 37 | 38 | text = as_marked_section( 39 | Bold("Варианты оплаты:"), 40 | "Картой в боте", 41 | "При получении карта/кеш", 42 | "В заведении", 43 | marker='✅ ' 44 | ) 45 | await message.answer(text.as_html()) 46 | 47 | 48 | @user_private_router.message( 49 | (F.text.lower().contains("доставк")) | (F.text.lower() == "варианты доставки") 50 | ) 51 | @user_private_router.message(Command("shipping")) 52 | async def menu_cmd(message: types.Message): 53 | text = as_list( 54 | as_marked_section( 55 | Bold("Варианты доставки/заказа:"), 56 | "Курьер", 57 | "Самовынос (сейчас прибегу заберу)", 58 | "Покушаю у Вас (сейчас прибегу)", 59 | marker='✅ ' 60 | ), 61 | as_marked_section( 62 | Bold("Нельзя:"), 63 | "Почта", 64 | "Голуби", 65 | marker='❌ ' 66 | ), 67 | sep='\n----------------------\n' 68 | ) 69 | await message.answer(text.as_html()) 70 | 71 | 72 | @user_private_router.message(F.contact) 73 | async def get_contact(message: types.Message): 74 | await message.answer(f"номер получен") 75 | await message.answer(str(message.contact)) 76 | 77 | 78 | @user_private_router.message(F.location) 79 | async def get_location(message: types.Message): 80 | await message.answer(f"локация получена") 81 | await message.answer(str(message.location)) -------------------------------------------------------------------------------- /lesson_6-FSM/handlers/user_private.py: -------------------------------------------------------------------------------- 1 | from aiogram import F, types, Router 2 | from aiogram.enums import ParseMode 3 | from aiogram.filters import CommandStart, Command, or_f 4 | from aiogram.utils.formatting import ( 5 | as_list, 6 | as_marked_section, 7 | Bold, 8 | ) # Italic, as_numbered_list и тд 9 | 10 | from filters.chat_types import ChatTypeFilter 11 | 12 | from kbds.reply import get_keyboard 13 | 14 | user_private_router = Router() 15 | user_private_router.message.filter(ChatTypeFilter(["private"])) 16 | 17 | 18 | @user_private_router.message(CommandStart()) 19 | async def start_cmd(message: types.Message): 20 | await message.answer( 21 | "Привет, я виртуальный помощник", 22 | reply_markup=get_keyboard( 23 | "Меню", 24 | "О магазине", 25 | "Варианты оплаты", 26 | "Варианты доставки", 27 | placeholder="Что вас интересует?", 28 | sizes=(2, 2) 29 | ), 30 | ) 31 | 32 | 33 | # @user_private_router.message(F.text.lower() == "меню") 34 | @user_private_router.message(or_f(Command("menu"), (F.text.lower() == "меню"))) 35 | async def menu_cmd(message: types.Message): 36 | await message.answer("Вот меню:") 37 | 38 | 39 | @user_private_router.message(F.text.lower() == "о магазине") 40 | @user_private_router.message(Command("about")) 41 | async def about_cmd(message: types.Message): 42 | await message.answer("О нас:") 43 | 44 | 45 | @user_private_router.message(F.text.lower() == "варианты оплаты") 46 | @user_private_router.message(Command("payment")) 47 | async def payment_cmd(message: types.Message): 48 | text = as_marked_section( 49 | Bold("Варианты оплаты:"), 50 | "Картой в боте", 51 | "При получении карта/кеш", 52 | "В заведении", 53 | marker="✅ ", 54 | ) 55 | await message.answer(text.as_html()) 56 | 57 | 58 | @user_private_router.message( 59 | (F.text.lower().contains("доставк")) | (F.text.lower() == "варианты доставки")) 60 | @user_private_router.message(Command("shipping")) 61 | async def menu_cmd(message: types.Message): 62 | text = as_list( 63 | as_marked_section( 64 | Bold("Варианты доставки/заказа:"), 65 | "Курьер", 66 | "Самовынос (сейчас прибегу заберу)", 67 | "Покушаю у Вас (сейчас прибегу)", 68 | marker="✅ ", 69 | ), 70 | as_marked_section( 71 | Bold("Нельзя:"), 72 | "Почта", 73 | "Голуби", 74 | marker="❌ " 75 | ), 76 | sep="\n----------------------\n", 77 | ) 78 | await message.answer(text.as_html()) 79 | 80 | 81 | # @user_private_router.message(F.contact) 82 | # async def get_contact(message: types.Message): 83 | # await message.answer(f"номер получен") 84 | # await message.answer(str(message.contact)) 85 | 86 | 87 | # @user_private_router.message(F.location) 88 | # async def get_location(message: types.Message): 89 | # await message.answer(f"локация получена") 90 | # await message.answer(str(message.location)) 91 | -------------------------------------------------------------------------------- /lesson_6 -Prepared_template_for_review/handlers/user_private.py: -------------------------------------------------------------------------------- 1 | from aiogram import F, types, Router 2 | from aiogram.enums import ParseMode 3 | from aiogram.filters import CommandStart, Command, or_f 4 | from aiogram.utils.formatting import ( 5 | as_list, 6 | as_marked_section, 7 | Bold, 8 | ) # Italic, as_numbered_list и тд 9 | 10 | from filters.chat_types import ChatTypeFilter 11 | 12 | from kbds.reply import get_keyboard 13 | 14 | user_private_router = Router() 15 | user_private_router.message.filter(ChatTypeFilter(["private"])) 16 | 17 | 18 | @user_private_router.message(CommandStart()) 19 | async def start_cmd(message: types.Message): 20 | await message.answer( 21 | "Привет, я виртуальный помощник", 22 | reply_markup=get_keyboard( 23 | "Меню", 24 | "О магазине", 25 | "Варианты оплаты", 26 | "Варианты доставки", 27 | placeholder="Что вас интересует?", 28 | sizes=(2, 2) 29 | ), 30 | ) 31 | 32 | 33 | # @user_private_router.message(F.text.lower() == "меню") 34 | @user_private_router.message(or_f(Command("menu"), (F.text.lower() == "меню"))) 35 | async def menu_cmd(message: types.Message): 36 | await message.answer("Вот меню:") 37 | 38 | 39 | @user_private_router.message(F.text.lower() == "о магазине") 40 | @user_private_router.message(Command("about")) 41 | async def about_cmd(message: types.Message): 42 | await message.answer("О нас:") 43 | 44 | 45 | @user_private_router.message(F.text.lower() == "варианты оплаты") 46 | @user_private_router.message(Command("payment")) 47 | async def payment_cmd(message: types.Message): 48 | text = as_marked_section( 49 | Bold("Варианты оплаты:"), 50 | "Картой в боте", 51 | "При получении карта/кеш", 52 | "В заведении", 53 | marker="✅ ", 54 | ) 55 | await message.answer(text.as_html()) 56 | 57 | 58 | @user_private_router.message( 59 | (F.text.lower().contains("доставк")) | (F.text.lower() == "варианты доставки")) 60 | @user_private_router.message(Command("shipping")) 61 | async def menu_cmd(message: types.Message): 62 | text = as_list( 63 | as_marked_section( 64 | Bold("Варианты доставки/заказа:"), 65 | "Курьер", 66 | "Самовынос (сейчас прибегу заберу)", 67 | "Покушаю у Вас (сейчас прибегу)", 68 | marker="✅ ", 69 | ), 70 | as_marked_section( 71 | Bold("Нельзя:"), 72 | "Почта", 73 | "Голуби", 74 | marker="❌ " 75 | ), 76 | sep="\n----------------------\n", 77 | ) 78 | await message.answer(text.as_html()) 79 | 80 | 81 | # @user_private_router.message(F.contact) 82 | # async def get_contact(message: types.Message): 83 | # await message.answer(f"номер получен") 84 | # await message.answer(str(message.contact)) 85 | 86 | 87 | # @user_private_router.message(F.location) 88 | # async def get_location(message: types.Message): 89 | # await message.answer(f"локация получена") 90 | # await message.answer(str(message.location)) 91 | -------------------------------------------------------------------------------- /lesson_7/handlers/user_private.py: -------------------------------------------------------------------------------- 1 | from aiogram import F, types, Router 2 | from aiogram.enums import ParseMode 3 | from aiogram.filters import CommandStart, Command, or_f 4 | from aiogram.utils.formatting import ( 5 | as_list, 6 | as_marked_section, 7 | Bold, 8 | ) 9 | from sqlalchemy.ext.asyncio import AsyncSession 10 | from database.orm_query import orm_get_products # Italic, as_numbered_list и тд 11 | 12 | from filters.chat_types import ChatTypeFilter 13 | 14 | from kbds.reply import get_keyboard 15 | 16 | user_private_router = Router() 17 | user_private_router.message.filter(ChatTypeFilter(["private"])) 18 | 19 | 20 | @user_private_router.message(CommandStart()) 21 | async def start_cmd(message: types.Message): 22 | await message.answer( 23 | "Привет, я виртуальный помощник", 24 | reply_markup=get_keyboard( 25 | "Меню", 26 | "О магазине", 27 | "Варианты оплаты", 28 | "Варианты доставки", 29 | placeholder="Что вас интересует?", 30 | sizes=(2, 2) 31 | ), 32 | ) 33 | 34 | 35 | # @user_private_router.message(F.text.lower() == "меню") 36 | @user_private_router.message(or_f(Command("menu"), (F.text.lower() == "меню"))) 37 | async def menu_cmd(message: types.Message, session: AsyncSession): 38 | for product in await orm_get_products(session): 39 | await message.answer_photo( 40 | product.image, 41 | caption=f"{product.name}\ 42 | \n{product.description}\nСтоимость: {round(product.price, 2)}", 43 | ) 44 | await message.answer("Вот меню:") 45 | 46 | 47 | @user_private_router.message(F.text.lower() == "о магазине") 48 | @user_private_router.message(Command("about")) 49 | async def about_cmd(message: types.Message): 50 | await message.answer("О нас:") 51 | 52 | 53 | @user_private_router.message(F.text.lower() == "варианты оплаты") 54 | @user_private_router.message(Command("payment")) 55 | async def payment_cmd(message: types.Message): 56 | text = as_marked_section( 57 | Bold("Варианты оплаты:"), 58 | "Картой в боте", 59 | "При получении карта/кеш", 60 | "В заведении", 61 | marker="✅ ", 62 | ) 63 | await message.answer(text.as_html()) 64 | 65 | 66 | @user_private_router.message( 67 | (F.text.lower().contains("доставк")) | (F.text.lower() == "варианты доставки")) 68 | @user_private_router.message(Command("shipping")) 69 | async def shipping_cmd(message: types.Message): 70 | text = as_list( 71 | as_marked_section( 72 | Bold("Варианты доставки/заказа:"), 73 | "Курьер", 74 | "Самовынос (сейчас прибегу заберу)", 75 | "Покушаю у Вас (сейчас прибегу)", 76 | marker="✅ ", 77 | ), 78 | as_marked_section( 79 | Bold("Нельзя:"), 80 | "Почта", 81 | "Голуби", 82 | marker="❌ " 83 | ), 84 | sep="\n----------------------\n", 85 | ) 86 | await message.answer(text.as_html()) 87 | 88 | 89 | # @user_private_router.message(F.contact) 90 | # async def get_contact(message: types.Message): 91 | # await message.answer(f"номер получен") 92 | # await message.answer(str(message.contact)) 93 | 94 | 95 | # @user_private_router.message(F.location) 96 | # async def get_location(message: types.Message): 97 | # await message.answer(f"локация получена") 98 | # await message.answer(str(message.location)) 99 | -------------------------------------------------------------------------------- /lesson_8_multi_level_inline_menu/handlers/menu_processing.py: -------------------------------------------------------------------------------- 1 | from aiogram.types import InputMediaPhoto 2 | from sqlalchemy.ext.asyncio import AsyncSession 3 | 4 | from database.orm_query import ( 5 | orm_add_to_cart, 6 | orm_delete_from_cart, 7 | orm_get_banner, 8 | orm_get_categories, 9 | orm_get_products, 10 | orm_get_user_carts, 11 | orm_reduce_product_in_cart, 12 | ) 13 | from kbds.inline import ( 14 | get_products_btns, 15 | get_user_cart, 16 | get_user_catalog_btns, 17 | get_user_main_btns, 18 | ) 19 | 20 | from utils.paginator import Paginator 21 | 22 | 23 | async def main_menu(session, level, menu_name): 24 | banner = await orm_get_banner(session, menu_name) 25 | image = InputMediaPhoto(media=banner.image, caption=banner.description) 26 | 27 | kbds = get_user_main_btns(level=level) 28 | 29 | return image, kbds 30 | 31 | 32 | async def catalog(session, level, menu_name): 33 | banner = await orm_get_banner(session, menu_name) 34 | image = InputMediaPhoto(media=banner.image, caption=banner.description) 35 | 36 | categories = await orm_get_categories(session) 37 | kbds = get_user_catalog_btns(level=level, categories=categories) 38 | 39 | return image, kbds 40 | 41 | 42 | def pages(paginator: Paginator): 43 | btns = dict() 44 | if paginator.has_previous(): 45 | btns["◀ Пред."] = "previous" 46 | 47 | if paginator.has_next(): 48 | btns["След. ▶"] = "next" 49 | 50 | return btns 51 | 52 | 53 | async def products(session, level, category, page): 54 | products = await orm_get_products(session, category_id=category) 55 | 56 | paginator = Paginator(products, page=page) 57 | product = paginator.get_page()[0] 58 | 59 | image = InputMediaPhoto( 60 | media=product.image, 61 | caption=f"{product.name}\ 62 | \n{product.description}\nСтоимость: {round(product.price, 2)}\n\ 63 | Товар {paginator.page} из {paginator.pages}", 64 | ) 65 | 66 | pagination_btns = pages(paginator) 67 | 68 | kbds = get_products_btns( 69 | level=level, 70 | category=category, 71 | page=page, 72 | pagination_btns=pagination_btns, 73 | product_id=product.id, 74 | ) 75 | 76 | return image, kbds 77 | 78 | 79 | async def carts(session, level, menu_name, page, user_id, product_id): 80 | if menu_name == "delete": 81 | await orm_delete_from_cart(session, user_id, product_id) 82 | if page > 1: 83 | page -= 1 84 | elif menu_name == "decrement": 85 | is_cart = await orm_reduce_product_in_cart(session, user_id, product_id) 86 | if page > 1 and not is_cart: 87 | page -= 1 88 | elif menu_name == "increment": 89 | await orm_add_to_cart(session, user_id, product_id) 90 | 91 | carts = await orm_get_user_carts(session, user_id) 92 | 93 | if not carts: 94 | banner = await orm_get_banner(session, "cart") 95 | image = InputMediaPhoto( 96 | media=banner.image, caption=f"{banner.description}" 97 | ) 98 | 99 | kbds = get_user_cart( 100 | level=level, 101 | page=None, 102 | pagination_btns=None, 103 | product_id=None, 104 | ) 105 | 106 | else: 107 | paginator = Paginator(carts, page=page) 108 | 109 | cart = paginator.get_page()[0] 110 | 111 | cart_price = round(cart.quantity * cart.product.price, 2) 112 | total_price = round( 113 | sum(cart.quantity * cart.product.price for cart in carts), 2 114 | ) 115 | image = InputMediaPhoto( 116 | media=cart.product.image, 117 | caption=f"{cart.product.name}\n{cart.product.price}$ x {cart.quantity} = {cart_price}$\ 118 | \nТовар {paginator.page} из {paginator.pages} в корзине.\nОбщая стоимость товаров в корзине {total_price}", 119 | ) 120 | 121 | pagination_btns = pages(paginator) 122 | 123 | kbds = get_user_cart( 124 | level=level, 125 | page=page, 126 | pagination_btns=pagination_btns, 127 | product_id=cart.product.id, 128 | ) 129 | 130 | return image, kbds 131 | 132 | 133 | async def get_menu_content( 134 | session: AsyncSession, 135 | level: int, 136 | menu_name: str, 137 | category: int | None = None, 138 | page: int | None = None, 139 | product_id: int | None = None, 140 | user_id: int | None = None, 141 | ): 142 | if level == 0: 143 | return await main_menu(session, level, menu_name) 144 | elif level == 1: 145 | return await catalog(session, level, menu_name) 146 | elif level == 2: 147 | return await products(session, level, category, page) 148 | elif level == 3: 149 | return await carts(session, level, menu_name, page, user_id, product_id) 150 | -------------------------------------------------------------------------------- /lesson_8_multi_level_inline_menu/kbds/inline.py: -------------------------------------------------------------------------------- 1 | from aiogram.filters.callback_data import CallbackData 2 | from aiogram.types import InlineKeyboardButton 3 | from aiogram.utils.keyboard import InlineKeyboardBuilder 4 | 5 | 6 | class MenuCallBack(CallbackData, prefix="menu"): 7 | level: int 8 | menu_name: str 9 | category: int | None = None 10 | page: int = 1 11 | product_id: int | None = None 12 | 13 | 14 | def get_user_main_btns(*, level: int, sizes: tuple[int] = (2,)): 15 | keyboard = InlineKeyboardBuilder() 16 | btns = { 17 | "Товары 🍕": "catalog", 18 | "Корзина 🛒": "cart", 19 | "О нас ℹ️": "about", 20 | "Оплата 💰": "payment", 21 | "Доставка ⛵": "shipping", 22 | } 23 | for text, menu_name in btns.items(): 24 | if menu_name == 'catalog': 25 | keyboard.add(InlineKeyboardButton(text=text, 26 | callback_data=MenuCallBack(level=level+1, menu_name=menu_name).pack())) 27 | elif menu_name == 'cart': 28 | keyboard.add(InlineKeyboardButton(text=text, 29 | callback_data=MenuCallBack(level=3, menu_name=menu_name).pack())) 30 | else: 31 | keyboard.add(InlineKeyboardButton(text=text, 32 | callback_data=MenuCallBack(level=level, menu_name=menu_name).pack())) 33 | 34 | return keyboard.adjust(*sizes).as_markup() 35 | 36 | 37 | def get_user_catalog_btns(*, level: int, categories: list, sizes: tuple[int] = (2,)): 38 | keyboard = InlineKeyboardBuilder() 39 | 40 | keyboard.add(InlineKeyboardButton(text='Назад', 41 | callback_data=MenuCallBack(level=level-1, menu_name='main').pack())) 42 | keyboard.add(InlineKeyboardButton(text='Корзина 🛒', 43 | callback_data=MenuCallBack(level=3, menu_name='cart').pack())) 44 | 45 | for c in categories: 46 | keyboard.add(InlineKeyboardButton(text=c.name, 47 | callback_data=MenuCallBack(level=level+1, menu_name=c.name, category=c.id).pack())) 48 | 49 | return keyboard.adjust(*sizes).as_markup() 50 | 51 | 52 | def get_products_btns( 53 | *, 54 | level: int, 55 | category: int, 56 | page: int, 57 | pagination_btns: dict, 58 | product_id: int, 59 | sizes: tuple[int] = (2, 1) 60 | ): 61 | keyboard = InlineKeyboardBuilder() 62 | 63 | keyboard.add(InlineKeyboardButton(text='Назад', 64 | callback_data=MenuCallBack(level=level-1, menu_name='catalog').pack())) 65 | keyboard.add(InlineKeyboardButton(text='Корзина 🛒', 66 | callback_data=MenuCallBack(level=3, menu_name='cart').pack())) 67 | keyboard.add(InlineKeyboardButton(text='Купить 💵', 68 | callback_data=MenuCallBack(level=level, menu_name='add_to_cart', product_id=product_id).pack())) 69 | 70 | keyboard.adjust(*sizes) 71 | 72 | row = [] 73 | for text, menu_name in pagination_btns.items(): 74 | if menu_name == "next": 75 | row.append(InlineKeyboardButton(text=text, 76 | callback_data=MenuCallBack( 77 | level=level, 78 | menu_name=menu_name, 79 | category=category, 80 | page=page + 1).pack())) 81 | 82 | elif menu_name == "previous": 83 | row.append(InlineKeyboardButton(text=text, 84 | callback_data=MenuCallBack( 85 | level=level, 86 | menu_name=menu_name, 87 | category=category, 88 | page=page - 1).pack())) 89 | 90 | return keyboard.row(*row).as_markup() 91 | 92 | 93 | def get_user_cart( 94 | *, 95 | level: int, 96 | page: int | None, 97 | pagination_btns: dict | None, 98 | product_id: int | None, 99 | sizes: tuple[int] = (3,) 100 | ): 101 | keyboard = InlineKeyboardBuilder() 102 | if page: 103 | keyboard.add(InlineKeyboardButton(text='Удалить', 104 | callback_data=MenuCallBack(level=level, menu_name='delete', product_id=product_id, page=page).pack())) 105 | keyboard.add(InlineKeyboardButton(text='-1', 106 | callback_data=MenuCallBack(level=level, menu_name='decrement', product_id=product_id, page=page).pack())) 107 | keyboard.add(InlineKeyboardButton(text='+1', 108 | callback_data=MenuCallBack(level=level, menu_name='increment', product_id=product_id, page=page).pack())) 109 | 110 | keyboard.adjust(*sizes) 111 | 112 | row = [] 113 | for text, menu_name in pagination_btns.items(): 114 | if menu_name == "next": 115 | row.append(InlineKeyboardButton(text=text, 116 | callback_data=MenuCallBack(level=level, menu_name=menu_name, page=page + 1).pack())) 117 | elif menu_name == "previous": 118 | row.append(InlineKeyboardButton(text=text, 119 | callback_data=MenuCallBack(level=level, menu_name=menu_name, page=page - 1).pack())) 120 | 121 | keyboard.row(*row) 122 | 123 | row2 = [ 124 | InlineKeyboardButton(text='На главную 🏠', 125 | callback_data=MenuCallBack(level=0, menu_name='main').pack()), 126 | InlineKeyboardButton(text='Заказать', 127 | callback_data=MenuCallBack(level=0, menu_name='order').pack()), 128 | ] 129 | return keyboard.row(*row2).as_markup() 130 | else: 131 | keyboard.add( 132 | InlineKeyboardButton(text='На главную 🏠', 133 | callback_data=MenuCallBack(level=0, menu_name='main').pack())) 134 | 135 | return keyboard.adjust(*sizes).as_markup() 136 | 137 | 138 | def get_callback_btns(*, btns: dict[str, str], sizes: tuple[int] = (2,)): 139 | keyboard = InlineKeyboardBuilder() 140 | 141 | for text, data in btns.items(): 142 | keyboard.add(InlineKeyboardButton(text=text, callback_data=data)) 143 | 144 | return keyboard.adjust(*sizes).as_markup() 145 | -------------------------------------------------------------------------------- /lesson_8_multi_level_inline_menu/database/orm_query.py: -------------------------------------------------------------------------------- 1 | import math 2 | from sqlalchemy import select, update, delete 3 | from sqlalchemy.ext.asyncio import AsyncSession 4 | from sqlalchemy.orm import joinedload 5 | 6 | from database.models import Banner, Cart, Category, Product, User 7 | 8 | 9 | ############### Работа с баннерами (информационными страницами) ############### 10 | 11 | async def orm_add_banner_description(session: AsyncSession, data: dict): 12 | #Добавляем новый или изменяем существующий по именам 13 | #пунктов меню: main, about, cart, shipping, payment, catalog 14 | query = select(Banner) 15 | result = await session.execute(query) 16 | if result.first(): 17 | return 18 | session.add_all([Banner(name=name, description=description) for name, description in data.items()]) 19 | await session.commit() 20 | 21 | 22 | async def orm_change_banner_image(session: AsyncSession, name: str, image: str): 23 | query = update(Banner).where(Banner.name == name).values(image=image) 24 | await session.execute(query) 25 | await session.commit() 26 | 27 | 28 | async def orm_get_banner(session: AsyncSession, page: str): 29 | query = select(Banner).where(Banner.name == page) 30 | result = await session.execute(query) 31 | return result.scalar() 32 | 33 | 34 | async def orm_get_info_pages(session: AsyncSession): 35 | query = select(Banner) 36 | result = await session.execute(query) 37 | return result.scalars().all() 38 | 39 | 40 | ############################ Категории ###################################### 41 | 42 | async def orm_get_categories(session: AsyncSession): 43 | query = select(Category) 44 | result = await session.execute(query) 45 | return result.scalars().all() 46 | 47 | async def orm_create_categories(session: AsyncSession, categories: list): 48 | query = select(Category) 49 | result = await session.execute(query) 50 | if result.first(): 51 | return 52 | session.add_all([Category(name=name) for name in categories]) 53 | await session.commit() 54 | 55 | ############ Админка: добавить/изменить/удалить товар ######################## 56 | 57 | async def orm_add_product(session: AsyncSession, data: dict): 58 | obj = Product( 59 | name=data["name"], 60 | description=data["description"], 61 | price=float(data["price"]), 62 | image=data["image"], 63 | category_id=int(data["category"]), 64 | ) 65 | session.add(obj) 66 | await session.commit() 67 | 68 | 69 | async def orm_get_products(session: AsyncSession, category_id): 70 | query = select(Product).where(Product.category_id == int(category_id)) 71 | result = await session.execute(query) 72 | return result.scalars().all() 73 | 74 | 75 | async def orm_get_product(session: AsyncSession, product_id: int): 76 | query = select(Product).where(Product.id == product_id) 77 | result = await session.execute(query) 78 | return result.scalar() 79 | 80 | 81 | async def orm_update_product(session: AsyncSession, product_id: int, data): 82 | query = ( 83 | update(Product) 84 | .where(Product.id == product_id) 85 | .values( 86 | name=data["name"], 87 | description=data["description"], 88 | price=float(data["price"]), 89 | image=data["image"], 90 | category_id=int(data["category"]), 91 | ) 92 | ) 93 | await session.execute(query) 94 | await session.commit() 95 | 96 | 97 | async def orm_delete_product(session: AsyncSession, product_id: int): 98 | query = delete(Product).where(Product.id == product_id) 99 | await session.execute(query) 100 | await session.commit() 101 | 102 | ##################### Добавляем юзера в БД ##################################### 103 | 104 | async def orm_add_user( 105 | session: AsyncSession, 106 | user_id: int, 107 | first_name: str | None = None, 108 | last_name: str | None = None, 109 | phone: str | None = None, 110 | ): 111 | query = select(User).where(User.user_id == user_id) 112 | result = await session.execute(query) 113 | if result.first() is None: 114 | session.add( 115 | User(user_id=user_id, first_name=first_name, last_name=last_name, phone=phone) 116 | ) 117 | await session.commit() 118 | 119 | 120 | ######################## Работа с корзинами ####################################### 121 | 122 | async def orm_add_to_cart(session: AsyncSession, user_id: int, product_id: int): 123 | query = select(Cart).where(Cart.user_id == user_id, Cart.product_id == product_id) 124 | cart = await session.execute(query) 125 | cart = cart.scalar() 126 | if cart: 127 | cart.quantity += 1 128 | await session.commit() 129 | return cart 130 | else: 131 | session.add(Cart(user_id=user_id, product_id=product_id, quantity=1)) 132 | await session.commit() 133 | 134 | 135 | 136 | async def orm_get_user_carts(session: AsyncSession, user_id): 137 | query = select(Cart).filter(Cart.user_id == user_id).options(joinedload(Cart.product)) 138 | result = await session.execute(query) 139 | return result.scalars().all() 140 | 141 | 142 | async def orm_delete_from_cart(session: AsyncSession, user_id: int, product_id: int): 143 | query = delete(Cart).where(Cart.user_id == user_id, Cart.product_id == product_id) 144 | await session.execute(query) 145 | await session.commit() 146 | 147 | 148 | async def orm_reduce_product_in_cart(session: AsyncSession, user_id: int, product_id: int): 149 | query = select(Cart).where(Cart.user_id == user_id, Cart.product_id == product_id) 150 | cart = await session.execute(query) 151 | cart = cart.scalar() 152 | 153 | if not cart: 154 | return 155 | if cart.quantity > 1: 156 | cart.quantity -= 1 157 | await session.commit() 158 | return True 159 | else: 160 | await orm_delete_from_cart(session, user_id, product_id) 161 | await session.commit() 162 | return False 163 | 164 | 165 | 166 | -------------------------------------------------------------------------------- /lesson_6-FSM/handlers/admin_private.py: -------------------------------------------------------------------------------- 1 | from aiogram import F, Router, types 2 | from aiogram.filters import Command, StateFilter 3 | from aiogram.fsm.context import FSMContext 4 | from aiogram.fsm.state import State, StatesGroup 5 | 6 | from filters.chat_types import ChatTypeFilter, IsAdmin 7 | from kbds.reply import get_keyboard 8 | 9 | 10 | admin_router = Router() 11 | admin_router.message.filter(ChatTypeFilter(["private"]), IsAdmin()) 12 | 13 | 14 | ADMIN_KB = get_keyboard( 15 | "Добавить товар", 16 | "Изменить товар", 17 | "Удалить товар", 18 | "Я так, просто посмотреть зашел", 19 | placeholder="Выберите действие", 20 | sizes=(2, 1, 1), 21 | ) 22 | 23 | 24 | @admin_router.message(Command("admin")) 25 | async def admin_features(message: types.Message): 26 | await message.answer("Что хотите сделать?", reply_markup=ADMIN_KB) 27 | 28 | 29 | @admin_router.message(F.text == "Я так, просто посмотреть зашел") 30 | async def starring_at_product(message: types.Message): 31 | await message.answer("ОК, вот список товаров") 32 | 33 | 34 | @admin_router.message(F.text == "Изменить товар") 35 | async def change_product(message: types.Message): 36 | await message.answer("ОК, вот список товаров") 37 | 38 | 39 | @admin_router.message(F.text == "Удалить товар") 40 | async def delete_product(message: types.Message): 41 | await message.answer("Выберите товар(ы) для удаления") 42 | 43 | 44 | #Код ниже для машины состояний (FSM) 45 | 46 | class AddProduct(StatesGroup): 47 | #Шаги состояний 48 | name = State() 49 | description = State() 50 | price = State() 51 | image = State() 52 | 53 | texts = { 54 | 'AddProduct:name': 'Введите название заново:', 55 | 'AddProduct:description': 'Введите описание заново:', 56 | 'AddProduct:price': 'Введите стоимость заново:', 57 | 'AddProduct:image': 'Этот стейт последний, поэтому...', 58 | } 59 | 60 | #Становимся в состояние ожидания ввода name 61 | @admin_router.message(StateFilter(None), F.text == "Добавить товар") 62 | async def add_product(message: types.Message, state: FSMContext): 63 | await message.answer( 64 | "Введите название товара", reply_markup=types.ReplyKeyboardRemove() 65 | ) 66 | await state.set_state(AddProduct.name) 67 | 68 | 69 | #Хендлер отмены и сброса состояния должен быть всегда именно хдесь, 70 | #после того как только встали в состояние номер 1 (элементарная очередность фильтров) 71 | @admin_router.message(StateFilter('*'), Command("отмена")) 72 | @admin_router.message(StateFilter('*'), F.text.casefold() == "отмена") 73 | async def cancel_handler(message: types.Message, state: FSMContext) -> None: 74 | 75 | current_state = await state.get_state() 76 | if current_state is None: 77 | return 78 | 79 | await state.clear() 80 | await message.answer("Действия отменены", reply_markup=ADMIN_KB) 81 | 82 | #Вернутся на шаг назад (на прошлое состояние) 83 | @admin_router.message(StateFilter('*'), Command("назад")) 84 | @admin_router.message(StateFilter('*'), F.text.casefold() == "назад") 85 | async def back_step_handler(message: types.Message, state: FSMContext) -> None: 86 | 87 | current_state = await state.get_state() 88 | 89 | if current_state == AddProduct.name: 90 | await message.answer('Предидущего шага нет, или введите название товара или напишите "отмена"') 91 | return 92 | 93 | previous = None 94 | for step in AddProduct.__all_states__: 95 | if step.state == current_state: 96 | await state.set_state(previous) 97 | await message.answer(f"Ок, вы вернулись к прошлому шагу \n {AddProduct.texts[previous.state]}") 98 | return 99 | previous = step 100 | 101 | 102 | #Ловим данные для состояние name и потом меняем состояние на description 103 | @admin_router.message(AddProduct.name, F.text) 104 | async def add_name(message: types.Message, state: FSMContext): 105 | # Здесь можно сделать какую либо дополнительную проверку 106 | #и выйти из хендлера не меняя состояние с отправкой соответствующего сообщения 107 | #например: 108 | if len(message.text) >= 100: 109 | await message.answer("Название товара не должно превышать 100 символов. \n Введите заново") 110 | return 111 | 112 | await state.update_data(name=message.text) 113 | await message.answer("Введите описание товара") 114 | await state.set_state(AddProduct.description) 115 | 116 | #Хендлер для отлова некорректных вводов для состояния name 117 | @admin_router.message(AddProduct.name) 118 | async def add_name2(message: types.Message, state: FSMContext): 119 | await message.answer("Вы ввели не допустимые данные, введите текст названия товара") 120 | 121 | 122 | 123 | #Ловим данные для состояние description и потом меняем состояние на price 124 | @admin_router.message(AddProduct.description, F.text) 125 | async def add_description(message: types.Message, state: FSMContext): 126 | await state.update_data(description=message.text) 127 | await message.answer("Введите стоимость товара") 128 | await state.set_state(AddProduct.price) 129 | 130 | #Хендлер для отлова некорректных вводов для состояния description 131 | @admin_router.message(AddProduct.description) 132 | async def add_description2(message: types.Message, state: FSMContext): 133 | await message.answer("Вы ввели не допустимые данные, введите текст описания товара") 134 | 135 | 136 | 137 | #Ловим данные для состояние price и потом меняем состояние на image 138 | @admin_router.message(AddProduct.price, F.text) 139 | async def add_price(message: types.Message, state: FSMContext): 140 | try: 141 | float(message.text) 142 | except ValueError: 143 | await message.answer("Введите корректное значение цены") 144 | return 145 | 146 | await state.update_data(price=message.text) 147 | await message.answer("Загрузите изображение товара") 148 | await state.set_state(AddProduct.image) 149 | 150 | #Хендлер для отлова некорректных ввода для состояния price 151 | @admin_router.message(AddProduct.price) 152 | async def add_price2(message: types.Message, state: FSMContext): 153 | await message.answer("Вы ввели не допустимые данные, введите стоимость товара") 154 | 155 | 156 | 157 | #Ловим данные для состояние image и потом выходим из состояний 158 | @admin_router.message(AddProduct.image, F.photo) 159 | async def add_image(message: types.Message, state: FSMContext): 160 | await state.update_data(image=message.photo[-1].file_id) 161 | await message.answer("Товар добавлен", reply_markup=ADMIN_KB) 162 | data = await state.get_data() 163 | await message.answer(str(data)) 164 | await state.clear() 165 | 166 | @admin_router.message(AddProduct.image) 167 | async def add_image2(message: types.Message, state: FSMContext): 168 | await message.answer("Отправьте фото пищи") 169 | -------------------------------------------------------------------------------- /lesson_8 template/database/orm_query.py: -------------------------------------------------------------------------------- 1 | import math 2 | from sqlalchemy import select, update, delete 3 | from sqlalchemy.ext.asyncio import AsyncSession 4 | from sqlalchemy.orm import joinedload 5 | 6 | from database.models import Banner, Cart, Category, Product, User 7 | 8 | # Простой пагинатор 9 | class Paginator: 10 | def __init__(self, array: list | tuple, page: int=1, per_page: int=1): 11 | self.array = array 12 | self.per_page = per_page 13 | self.page = page 14 | self.len = len(self.array) 15 | # math.ceil - округление в большую сторону до целого числа 16 | self.pages = math.ceil(self.len / self.per_page) 17 | 18 | def __get_slice(self): 19 | start = (self.page - 1) * self.per_page 20 | stop = start + self.per_page 21 | return self.array[start:stop] 22 | 23 | def get_page(self): 24 | page_items = self.__get_slice() 25 | return page_items 26 | 27 | def has_next(self): 28 | if self.page < self.pages: 29 | return self.page + 1 30 | return False 31 | 32 | def has_previous(self): 33 | if self.page > 1: 34 | return self.page - 1 35 | return False 36 | 37 | def get_next(self): 38 | if self.page < self.pages: 39 | self.page += 1 40 | return self.get_page() 41 | raise IndexError(f'Next page does not exist. Use has_next() to check before.') 42 | 43 | def get_previous(self): 44 | if self.page > 1: 45 | self.page -= 1 46 | return self.__get_slice() 47 | raise IndexError(f'Previous page does not exist. Use has_previous() to check before.') 48 | 49 | 50 | ############### Работа с баннерами (информационными страницами) ############### 51 | 52 | async def orm_add_banner_description(session: AsyncSession, data: dict): 53 | #Добавляем новый или изменяем существующий по именам 54 | #пунктов меню: main, about, cart, shipping, payment, catalog 55 | query = select(Banner) 56 | result = await session.execute(query) 57 | if result.first(): 58 | return 59 | session.add_all([Banner(name=name, description=description) for name, description in data.items()]) 60 | await session.commit() 61 | 62 | 63 | async def orm_change_banner_image(session: AsyncSession, name: str, image: str): 64 | query = update(Banner).where(Banner.name == name).values(image=image) 65 | await session.execute(query) 66 | await session.commit() 67 | 68 | 69 | async def orm_get_banner(session: AsyncSession, page: str): 70 | query = select(Banner).where(Banner.name == page) 71 | result = await session.execute(query) 72 | return result.scalar() 73 | 74 | 75 | async def orm_get_info_pages(session: AsyncSession): 76 | query = select(Banner) 77 | result = await session.execute(query) 78 | return result.scalars().all() 79 | 80 | 81 | ############################ Категории ###################################### 82 | 83 | async def orm_get_categories(session: AsyncSession): 84 | query = select(Category) 85 | result = await session.execute(query) 86 | return result.scalars().all() 87 | 88 | async def orm_create_categories(session: AsyncSession, categories: list): 89 | query = select(Category) 90 | result = await session.execute(query) 91 | if result.first(): 92 | return 93 | session.add_all([Category(name=name) for name in categories]) 94 | await session.commit() 95 | 96 | ############ Админка: добавить/изменить/удалить товар ######################## 97 | 98 | async def orm_add_product(session: AsyncSession, data: dict): 99 | obj = Product( 100 | name=data["name"], 101 | description=data["description"], 102 | price=float(data["price"]), 103 | image=data["image"], 104 | category_id=int(data["category"]), 105 | ) 106 | session.add(obj) 107 | await session.commit() 108 | 109 | 110 | async def orm_get_products(session: AsyncSession, category_id): 111 | query = select(Product).where(Product.category_id == int(category_id)) 112 | result = await session.execute(query) 113 | return result.scalars().all() 114 | 115 | 116 | async def orm_get_product(session: AsyncSession, product_id: int): 117 | query = select(Product).where(Product.id == product_id) 118 | result = await session.execute(query) 119 | return result.scalar() 120 | 121 | 122 | async def orm_update_product(session: AsyncSession, product_id: int, data): 123 | query = ( 124 | update(Product) 125 | .where(Product.id == product_id) 126 | .values( 127 | name=data["name"], 128 | description=data["description"], 129 | price=float(data["price"]), 130 | image=data["image"], 131 | category_id=int(data["category"]), 132 | ) 133 | ) 134 | await session.execute(query) 135 | await session.commit() 136 | 137 | 138 | async def orm_delete_product(session: AsyncSession, product_id: int): 139 | query = delete(Product).where(Product.id == product_id) 140 | await session.execute(query) 141 | await session.commit() 142 | 143 | ##################### Добавляем юзера в БД ##################################### 144 | 145 | async def orm_add_user( 146 | session: AsyncSession, 147 | user_id: int, 148 | first_name: str | None = None, 149 | last_name: str | None = None, 150 | phone: str | None = None, 151 | ): 152 | query = select(User).where(User.user_id == user_id) 153 | result = await session.execute(query) 154 | if result.first() is None: 155 | session.add( 156 | User(user_id=user_id, first_name=first_name, last_name=last_name, phone=phone) 157 | ) 158 | await session.commit() 159 | 160 | 161 | ######################## Работа с корзинами ####################################### 162 | 163 | async def orm_add_to_cart(session: AsyncSession, user_id: int, product_id: int): 164 | query = select(Cart).where(Cart.user_id == user_id, Cart.product_id == product_id).options(joinedload(Cart.product)) 165 | cart = await session.execute(query) 166 | cart = cart.scalar() 167 | if cart: 168 | cart.quantity += 1 169 | await session.commit() 170 | return cart 171 | else: 172 | session.add(Cart(user_id=user_id, product_id=product_id, quantity=1)) 173 | await session.commit() 174 | 175 | 176 | 177 | async def orm_get_user_carts(session: AsyncSession, user_id): 178 | query = select(Cart).filter(Cart.user_id == user_id).options(joinedload(Cart.product)) 179 | result = await session.execute(query) 180 | return result.scalars().all() 181 | 182 | 183 | async def orm_delete_from_cart(session: AsyncSession, user_id: int, product_id: int): 184 | query = delete(Cart).where(Cart.user_id == user_id, Cart.product_id == product_id) 185 | await session.execute(query) 186 | await session.commit() 187 | 188 | 189 | async def orm_reduce_product_in_cart(session: AsyncSession, user_id: int, product_id: int): 190 | query = select(Cart).where(Cart.user_id == user_id, Cart.product_id == product_id).options(joinedload(Cart.product)) 191 | cart = await session.execute(query) 192 | cart = cart.scalar() 193 | 194 | if not cart: 195 | return 196 | if cart.quantity > 1: 197 | cart.quantity -= 1 198 | await session.commit() 199 | return True 200 | else: 201 | await orm_delete_from_cart(session, user_id, product_id) 202 | await session.commit() 203 | return False 204 | 205 | 206 | 207 | -------------------------------------------------------------------------------- /lesson_7/handlers/admin_private.py: -------------------------------------------------------------------------------- 1 | from aiogram import F, Router, types 2 | from aiogram.filters import Command, StateFilter, or_f 3 | from aiogram.fsm.context import FSMContext 4 | from aiogram.fsm.state import State, StatesGroup 5 | 6 | from sqlalchemy.ext.asyncio import AsyncSession 7 | 8 | from database.orm_query import ( 9 | orm_add_product, 10 | orm_delete_product, 11 | orm_get_product, 12 | orm_get_products, 13 | orm_update_product, 14 | ) 15 | 16 | from filters.chat_types import ChatTypeFilter, IsAdmin 17 | 18 | from kbds.inline import get_callback_btns 19 | from kbds.reply import get_keyboard 20 | 21 | 22 | admin_router = Router() 23 | admin_router.message.filter(ChatTypeFilter(["private"]), IsAdmin()) 24 | 25 | 26 | ADMIN_KB = get_keyboard( 27 | "Добавить товар", 28 | "Ассортимент", 29 | placeholder="Выберите действие", 30 | sizes=(2,), 31 | ) 32 | 33 | 34 | class AddProduct(StatesGroup): 35 | # Шаги состояний 36 | name = State() 37 | description = State() 38 | price = State() 39 | image = State() 40 | 41 | product_for_change = None 42 | 43 | texts = { 44 | "AddProduct:name": "Введите название заново:", 45 | "AddProduct:description": "Введите описание заново:", 46 | "AddProduct:price": "Введите стоимость заново:", 47 | "AddProduct:image": "Этот стейт последний, поэтому...", 48 | } 49 | 50 | 51 | @admin_router.message(Command("admin")) 52 | async def admin_features(message: types.Message): 53 | await message.answer("Что хотите сделать?", reply_markup=ADMIN_KB) 54 | 55 | 56 | @admin_router.message(F.text == "Ассортимент") 57 | async def starring_at_product(message: types.Message, session: AsyncSession): 58 | for product in await orm_get_products(session): 59 | await message.answer_photo( 60 | product.image, 61 | caption=f"{product.name}\ 62 | \n{product.description}\nСтоимость: {round(product.price, 2)}", 63 | reply_markup=get_callback_btns( 64 | btns={ 65 | "Удалить": f"delete_{product.id}", 66 | "Изменить": f"change_{product.id}", 67 | } 68 | ), 69 | ) 70 | await message.answer("ОК, вот список товаров ⏫") 71 | 72 | 73 | @admin_router.callback_query(F.data.startswith("delete_")) 74 | async def delete_product_callback(callback: types.CallbackQuery, session: AsyncSession): 75 | product_id = callback.data.split("_")[-1] 76 | await orm_delete_product(session, int(product_id)) 77 | 78 | await callback.answer("Товар удален") 79 | await callback.message.answer("Товар удален!") 80 | 81 | # FSM: 82 | 83 | # Становимся в состояние ожидания ввода name 84 | @admin_router.callback_query(StateFilter(None), F.data.startswith("change_")) 85 | async def change_product_callback( 86 | callback: types.CallbackQuery, state: FSMContext, session: AsyncSession 87 | ): 88 | product_id = callback.data.split("_")[-1] 89 | 90 | product_for_change = await orm_get_product(session, int(product_id)) 91 | 92 | AddProduct.product_for_change = product_for_change 93 | 94 | await callback.answer() 95 | await callback.message.answer( 96 | "Введите название товара", reply_markup=types.ReplyKeyboardRemove() 97 | ) 98 | await state.set_state(AddProduct.name) 99 | 100 | 101 | # Становимся в состояние ожидания ввода name 102 | @admin_router.message(StateFilter(None), F.text == "Добавить товар") 103 | async def add_product(message: types.Message, state: FSMContext): 104 | await message.answer( 105 | "Введите название товара", reply_markup=types.ReplyKeyboardRemove() 106 | ) 107 | await state.set_state(AddProduct.name) 108 | 109 | 110 | # Хендлер отмены и сброса состояния должен быть всегда именно хдесь, 111 | # после того как только встали в состояние номер 1 (элементарная очередность фильтров) 112 | @admin_router.message(StateFilter("*"), Command("отмена")) 113 | @admin_router.message(StateFilter("*"), F.text.casefold() == "отмена") 114 | async def cancel_handler(message: types.Message, state: FSMContext) -> None: 115 | current_state = await state.get_state() 116 | if current_state is None: 117 | return 118 | if AddProduct.product_for_change: 119 | AddProduct.product_for_change = None 120 | await state.clear() 121 | await message.answer("Действия отменены", reply_markup=ADMIN_KB) 122 | 123 | 124 | # Вернутся на шаг назад (на прошлое состояние) 125 | @admin_router.message(StateFilter("*"), Command("назад")) 126 | @admin_router.message(StateFilter("*"), F.text.casefold() == "назад") 127 | async def back_step_handler(message: types.Message, state: FSMContext) -> None: 128 | current_state = await state.get_state() 129 | 130 | if current_state == AddProduct.name: 131 | await message.answer( 132 | 'Предидущего шага нет, или введите название товара или напишите "отмена"' 133 | ) 134 | return 135 | 136 | previous = None 137 | for step in AddProduct.__all_states__: 138 | if step.state == current_state: 139 | await state.set_state(previous) 140 | await message.answer( 141 | f"Ок, вы вернулись к прошлому шагу \n {AddProduct.texts[previous.state]}" 142 | ) 143 | return 144 | previous = step 145 | 146 | 147 | # Ловим данные для состояние name и потом меняем состояние на description 148 | @admin_router.message(AddProduct.name, or_f(F.text, F.text == ".")) 149 | async def add_name(message: types.Message, state: FSMContext): 150 | if message.text == ".": 151 | await state.update_data(name=AddProduct.product_for_change.name) 152 | else: 153 | # Здесь можно сделать какую либо дополнительную проверку 154 | # и выйти из хендлера не меняя состояние с отправкой соответствующего сообщения 155 | # например: 156 | if len(message.text) >= 100: 157 | await message.answer( 158 | "Название товара не должно превышать 100 символов. \n Введите заново" 159 | ) 160 | return 161 | 162 | await state.update_data(name=message.text) 163 | await message.answer("Введите описание товара") 164 | await state.set_state(AddProduct.description) 165 | 166 | 167 | # Хендлер для отлова некорректных вводов для состояния name 168 | @admin_router.message(AddProduct.name) 169 | async def add_name2(message: types.Message, state: FSMContext): 170 | await message.answer("Вы ввели не допустимые данные, введите текст названия товара") 171 | 172 | 173 | # Ловим данные для состояние description и потом меняем состояние на price 174 | @admin_router.message(AddProduct.description, or_f(F.text, F.text == ".")) 175 | async def add_description(message: types.Message, state: FSMContext): 176 | if message.text == ".": 177 | await state.update_data(description=AddProduct.product_for_change.description) 178 | else: 179 | await state.update_data(description=message.text) 180 | await message.answer("Введите стоимость товара") 181 | await state.set_state(AddProduct.price) 182 | 183 | 184 | # Хендлер для отлова некорректных вводов для состояния description 185 | @admin_router.message(AddProduct.description) 186 | async def add_description2(message: types.Message, state: FSMContext): 187 | await message.answer("Вы ввели не допустимые данные, введите текст описания товара") 188 | 189 | 190 | # Ловим данные для состояние price и потом меняем состояние на image 191 | @admin_router.message(AddProduct.price, or_f(F.text, F.text == ".")) 192 | async def add_price(message: types.Message, state: FSMContext): 193 | if message.text == ".": 194 | await state.update_data(price=AddProduct.product_for_change.price) 195 | else: 196 | try: 197 | float(message.text) 198 | except ValueError: 199 | await message.answer("Введите корректное значение цены") 200 | return 201 | 202 | await state.update_data(price=message.text) 203 | await message.answer("Загрузите изображение товара") 204 | await state.set_state(AddProduct.image) 205 | 206 | 207 | # Хендлер для отлова некорректных ввода для состояния price 208 | @admin_router.message(AddProduct.price) 209 | async def add_price2(message: types.Message, state: FSMContext): 210 | await message.answer("Вы ввели не допустимые данные, введите стоимость товара") 211 | 212 | 213 | # Ловим данные для состояние image и потом выходим из состояний 214 | @admin_router.message(AddProduct.image, or_f(F.photo, F.text == ".")) 215 | async def add_image(message: types.Message, state: FSMContext, session: AsyncSession): 216 | if message.text and message.text == ".": 217 | await state.update_data(image=AddProduct.product_for_change.image) 218 | 219 | else: 220 | await state.update_data(image=message.photo[-1].file_id) 221 | data = await state.get_data() 222 | try: 223 | if AddProduct.product_for_change: 224 | await orm_update_product(session, AddProduct.product_for_change.id, data) 225 | else: 226 | await orm_add_product(session, data) 227 | await message.answer("Товар добавлен/изменен", reply_markup=ADMIN_KB) 228 | await state.clear() 229 | 230 | except Exception as e: 231 | await message.answer( 232 | f"Ошибка: \n{str(e)}\nОбратись к программеру, он опять денег хочет", 233 | reply_markup=ADMIN_KB, 234 | ) 235 | await state.clear() 236 | 237 | AddProduct.product_for_change = None 238 | 239 | 240 | @admin_router.message(AddProduct.image) 241 | async def add_image2(message: types.Message, state: FSMContext): 242 | await message.answer("Отправьте фото пищи") 243 | -------------------------------------------------------------------------------- /lesson_8 template/handlers/admin_private.py: -------------------------------------------------------------------------------- 1 | from aiogram import F, Router, types 2 | from aiogram.filters import Command, StateFilter, or_f 3 | from aiogram.fsm.context import FSMContext 4 | from aiogram.fsm.state import State, StatesGroup 5 | 6 | from sqlalchemy.ext.asyncio import AsyncSession 7 | 8 | from database.orm_query import ( 9 | orm_change_banner_image, 10 | orm_get_categories, 11 | orm_add_product, 12 | orm_delete_product, 13 | orm_get_info_pages, 14 | orm_get_product, 15 | orm_get_products, 16 | orm_update_product, 17 | ) 18 | 19 | from filters.chat_types import ChatTypeFilter, IsAdmin 20 | 21 | from kbds.inline import get_callback_btns 22 | from kbds.reply import get_keyboard 23 | 24 | 25 | admin_router = Router() 26 | admin_router.message.filter(ChatTypeFilter(["private"]), IsAdmin()) 27 | 28 | 29 | ADMIN_KB = get_keyboard( 30 | "Добавить товар", 31 | "Ассортимент", 32 | "Добавить/Изменить баннер", 33 | placeholder="Выберите действие", 34 | sizes=(2,), 35 | ) 36 | 37 | 38 | @admin_router.message(Command("admin")) 39 | async def admin_features(message: types.Message): 40 | await message.answer("Что хотите сделать?", reply_markup=ADMIN_KB) 41 | 42 | 43 | @admin_router.message(F.text == 'Ассортимент') 44 | async def admin_features(message: types.Message, session: AsyncSession): 45 | categories = await orm_get_categories(session) 46 | btns = {category.name : f'category_{category.id}' for category in categories} 47 | await message.answer("Выберите категорию", reply_markup=get_callback_btns(btns=btns)) 48 | 49 | 50 | @admin_router.callback_query(F.data.startswith('category_')) 51 | async def starring_at_product(callback: types.CallbackQuery, session: AsyncSession): 52 | category_id = callback.data.split('_')[-1] 53 | for product in await orm_get_products(session, int(category_id)): 54 | await callback.message.answer_photo( 55 | product.image, 56 | caption=f"{product.name}\ 57 | \n{product.description}\nСтоимость: {round(product.price, 2)}", 58 | reply_markup=get_callback_btns( 59 | btns={ 60 | "Удалить": f"delete_{product.id}", 61 | "Изменить": f"change_{product.id}", 62 | }, 63 | sizes=(2,) 64 | ), 65 | ) 66 | await callback.answer() 67 | await callback.message.answer("ОК, вот список товаров ⏫") 68 | 69 | 70 | @admin_router.callback_query(F.data.startswith("delete_")) 71 | async def delete_product_callback(callback: types.CallbackQuery, session: AsyncSession): 72 | product_id = callback.data.split("_")[-1] 73 | await orm_delete_product(session, int(product_id)) 74 | 75 | await callback.answer("Товар удален") 76 | await callback.message.answer("Товар удален!") 77 | 78 | 79 | ################# Микро FSM для загрузки/изменения баннеров ############################ 80 | 81 | class AddBanner(StatesGroup): 82 | image = State() 83 | 84 | # Отправляем перечень информационных страниц бота и становимся в состояние отправки photo 85 | @admin_router.message(StateFilter(None), F.text == 'Добавить/Изменить баннер') 86 | async def add_image2(message: types.Message, state: FSMContext, session: AsyncSession): 87 | pages_names = [page.name for page in await orm_get_info_pages(session)] 88 | await message.answer(f"Отправьте фото баннера.\nВ описании укажите для какой страницы:\ 89 | \n{', '.join(pages_names)}") 90 | await state.set_state(AddBanner.image) 91 | 92 | # Добавляем/изменяем изображение в таблице (там уже есть записанные страницы по именам: 93 | # main, catalog, cart(для пустой корзины), about, payment, shipping 94 | @admin_router.message(AddBanner.image, F.photo) 95 | async def add_banner(message: types.Message, state: FSMContext, session: AsyncSession): 96 | image_id = message.photo[-1].file_id 97 | for_page = message.caption.strip() 98 | pages_names = [page.name for page in await orm_get_info_pages(session)] 99 | if for_page not in pages_names: 100 | await message.answer(f"Введите нормальное название страницы, например:\ 101 | \n{', '.join(pages_names)}") 102 | return 103 | await orm_change_banner_image(session, for_page, image_id,) 104 | await message.answer("Баннер добавлен/изменен.") 105 | await state.clear() 106 | 107 | # ловим некоррекный ввод 108 | @admin_router.message(AddBanner.image) 109 | async def add_banner2(message: types.Message, state: FSMContext): 110 | await message.answer("Отправьте фото баннера или отмена") 111 | 112 | ######################################################################################### 113 | 114 | 115 | 116 | ######################### FSM для дабавления/изменения товаров админом ################### 117 | 118 | class AddProduct(StatesGroup): 119 | # Шаги состояний 120 | name = State() 121 | description = State() 122 | category = State() 123 | price = State() 124 | image = State() 125 | 126 | product_for_change = None 127 | 128 | texts = { 129 | "AddProduct:name": "Введите название заново:", 130 | "AddProduct:description": "Введите описание заново:", 131 | "AddProduct:category": "Выберите категорию заново ⬆️", 132 | "AddProduct:price": "Введите стоимость заново:", 133 | "AddProduct:image": "Этот стейт последний, поэтому...", 134 | } 135 | 136 | 137 | # Становимся в состояние ожидания ввода name 138 | @admin_router.callback_query(StateFilter(None), F.data.startswith("change_")) 139 | async def change_product_callback( 140 | callback: types.CallbackQuery, state: FSMContext, session: AsyncSession 141 | ): 142 | product_id = callback.data.split("_")[-1] 143 | 144 | product_for_change = await orm_get_product(session, int(product_id)) 145 | 146 | AddProduct.product_for_change = product_for_change 147 | 148 | await callback.answer() 149 | await callback.message.answer( 150 | "Введите название товара", reply_markup=types.ReplyKeyboardRemove() 151 | ) 152 | await state.set_state(AddProduct.name) 153 | 154 | 155 | # Становимся в состояние ожидания ввода name 156 | @admin_router.message(StateFilter(None), F.text == "Добавить товар") 157 | async def add_product(message: types.Message, state: FSMContext): 158 | await message.answer( 159 | "Введите название товара", reply_markup=types.ReplyKeyboardRemove() 160 | ) 161 | await state.set_state(AddProduct.name) 162 | 163 | 164 | # Хендлер отмены и сброса состояния должен быть всегда именно здесь, 165 | # после того, как только встали в состояние номер 1 (элементарная очередность фильтров) 166 | @admin_router.message(StateFilter("*"), Command("отмена")) 167 | @admin_router.message(StateFilter("*"), F.text.casefold() == "отмена") 168 | async def cancel_handler(message: types.Message, state: FSMContext) -> None: 169 | current_state = await state.get_state() 170 | if current_state is None: 171 | return 172 | if AddProduct.product_for_change: 173 | AddProduct.product_for_change = None 174 | await state.clear() 175 | await message.answer("Действия отменены", reply_markup=ADMIN_KB) 176 | 177 | 178 | # Вернутся на шаг назад (на прошлое состояние) 179 | @admin_router.message(StateFilter("*"), Command("назад")) 180 | @admin_router.message(StateFilter("*"), F.text.casefold() == "назад") 181 | async def back_step_handler(message: types.Message, state: FSMContext) -> None: 182 | current_state = await state.get_state() 183 | 184 | if current_state == AddProduct.name: 185 | await message.answer( 186 | 'Предидущего шага нет, или введите название товара или напишите "отмена"' 187 | ) 188 | return 189 | 190 | previous = None 191 | for step in AddProduct.__all_states__: 192 | if step.state == current_state: 193 | await state.set_state(previous) 194 | await message.answer( 195 | f"Ок, вы вернулись к прошлому шагу \n {AddProduct.texts[previous.state]}" 196 | ) 197 | return 198 | previous = step 199 | 200 | 201 | # Ловим данные для состояние name и потом меняем состояние на description 202 | @admin_router.message(AddProduct.name, F.text) 203 | async def add_name(message: types.Message, state: FSMContext): 204 | if message.text == "." and AddProduct.product_for_change: 205 | await state.update_data(name=AddProduct.product_for_change.name) 206 | else: 207 | # Здесь можно сделать какую либо дополнительную проверку 208 | # и выйти из хендлера не меняя состояние с отправкой соответствующего сообщения 209 | # например: 210 | if 4 >= len(message.text) >= 150: 211 | await message.answer( 212 | "Название товара не должно превышать 150 символов\nили быть менее 5ти символов. \n Введите заново" 213 | ) 214 | return 215 | 216 | await state.update_data(name=message.text) 217 | await message.answer("Введите описание товара") 218 | await state.set_state(AddProduct.description) 219 | 220 | # Хендлер для отлова некорректных вводов для состояния name 221 | @admin_router.message(AddProduct.name) 222 | async def add_name2(message: types.Message, state: FSMContext): 223 | await message.answer("Вы ввели не допустимые данные, введите текст названия товара") 224 | 225 | 226 | # Ловим данные для состояние description и потом меняем состояние на price 227 | @admin_router.message(AddProduct.description, F.text) 228 | async def add_description(message: types.Message, state: FSMContext, session: AsyncSession): 229 | if message.text == "." and AddProduct.product_for_change: 230 | await state.update_data(description=AddProduct.product_for_change.description) 231 | else: 232 | if 4 >= len(message.text): 233 | await message.answer( 234 | "Слишком короткое описание. \n Введите заново" 235 | ) 236 | return 237 | await state.update_data(description=message.text) 238 | 239 | categories = await orm_get_categories(session) 240 | btns = {category.name : str(category.id) for category in categories} 241 | await message.answer("Выберите категорию", reply_markup=get_callback_btns(btns=btns)) 242 | await state.set_state(AddProduct.category) 243 | 244 | # Хендлер для отлова некорректных вводов для состояния description 245 | @admin_router.message(AddProduct.description) 246 | async def add_description2(message: types.Message, state: FSMContext): 247 | await message.answer("Вы ввели не допустимые данные, введите текст описания товара") 248 | 249 | 250 | # Ловим callback выбора категории 251 | @admin_router.callback_query(AddProduct.category) 252 | async def category_choice(callback: types.CallbackQuery, state: FSMContext , session: AsyncSession): 253 | if int(callback.data) in [category.id for category in await orm_get_categories(session)]: 254 | await callback.answer() 255 | await state.update_data(category=callback.data) 256 | await callback.message.answer('Теперь введите цену товара.') 257 | await state.set_state(AddProduct.price) 258 | else: 259 | await callback.message.answer('Выберите катеорию из кнопок.') 260 | await callback.answer() 261 | 262 | #Ловим любые некорректные действия, кроме нажатия на кнопку выбора категории 263 | @admin_router.message(AddProduct.category) 264 | async def category_choice2(message: types.Message, state: FSMContext): 265 | await message.answer("'Выберите катеорию из кнопок.'") 266 | 267 | 268 | # Ловим данные для состояние price и потом меняем состояние на image 269 | @admin_router.message(AddProduct.price, F.text) 270 | async def add_price(message: types.Message, state: FSMContext): 271 | if message.text == "." and AddProduct.product_for_change: 272 | await state.update_data(price=AddProduct.product_for_change.price) 273 | else: 274 | try: 275 | float(message.text) 276 | except ValueError: 277 | await message.answer("Введите корректное значение цены") 278 | return 279 | 280 | await state.update_data(price=message.text) 281 | await message.answer("Загрузите изображение товара") 282 | await state.set_state(AddProduct.image) 283 | 284 | # Хендлер для отлова некорректных ввода для состояния price 285 | @admin_router.message(AddProduct.price) 286 | async def add_price2(message: types.Message, state: FSMContext): 287 | await message.answer("Вы ввели не допустимые данные, введите стоимость товара") 288 | 289 | 290 | # Ловим данные для состояние image и потом выходим из состояний 291 | @admin_router.message(AddProduct.image, or_f(F.photo, F.text == ".")) 292 | async def add_image(message: types.Message, state: FSMContext, session: AsyncSession): 293 | if message.text and message.text == "." and AddProduct.product_for_change: 294 | await state.update_data(image=AddProduct.product_for_change.image) 295 | 296 | elif message.photo: 297 | await state.update_data(image=message.photo[-1].file_id) 298 | else: 299 | await message.answer("Отправьте фото пищи") 300 | return 301 | data = await state.get_data() 302 | try: 303 | if AddProduct.product_for_change: 304 | await orm_update_product(session, AddProduct.product_for_change.id, data) 305 | else: 306 | await orm_add_product(session, data) 307 | await message.answer("Товар добавлен/изменен", reply_markup=ADMIN_KB) 308 | await state.clear() 309 | 310 | except Exception as e: 311 | await message.answer( 312 | f"Ошибка: \n{str(e)}\nОбратись к программеру, он опять денег хочет", 313 | reply_markup=ADMIN_KB, 314 | ) 315 | await state.clear() 316 | 317 | AddProduct.product_for_change = None 318 | 319 | # Ловим все прочее некорректное поведение для этого состояния 320 | @admin_router.message(AddProduct.image) 321 | async def add_image2(message: types.Message, state: FSMContext): 322 | await message.answer("Отправьте фото пищи") 323 | -------------------------------------------------------------------------------- /lesson_8_multi_level_inline_menu/handlers/admin_private.py: -------------------------------------------------------------------------------- 1 | from aiogram import F, Router, types 2 | from aiogram.filters import Command, StateFilter, or_f 3 | from aiogram.fsm.context import FSMContext 4 | from aiogram.fsm.state import State, StatesGroup 5 | 6 | from sqlalchemy.ext.asyncio import AsyncSession 7 | 8 | from database.orm_query import ( 9 | orm_change_banner_image, 10 | orm_get_categories, 11 | orm_add_product, 12 | orm_delete_product, 13 | orm_get_info_pages, 14 | orm_get_product, 15 | orm_get_products, 16 | orm_update_product, 17 | ) 18 | 19 | from filters.chat_types import ChatTypeFilter, IsAdmin 20 | 21 | from kbds.inline import get_callback_btns 22 | from kbds.reply import get_keyboard 23 | 24 | 25 | admin_router = Router() 26 | admin_router.message.filter(ChatTypeFilter(["private"]), IsAdmin()) 27 | 28 | 29 | ADMIN_KB = get_keyboard( 30 | "Добавить товар", 31 | "Ассортимент", 32 | "Добавить/Изменить баннер", 33 | placeholder="Выберите действие", 34 | sizes=(2,), 35 | ) 36 | 37 | 38 | @admin_router.message(Command("admin")) 39 | async def admin_features(message: types.Message): 40 | await message.answer("Что хотите сделать?", reply_markup=ADMIN_KB) 41 | 42 | 43 | @admin_router.message(F.text == 'Ассортимент') 44 | async def admin_features(message: types.Message, session: AsyncSession): 45 | categories = await orm_get_categories(session) 46 | btns = {category.name : f'category_{category.id}' for category in categories} 47 | await message.answer("Выберите категорию", reply_markup=get_callback_btns(btns=btns)) 48 | 49 | 50 | @admin_router.callback_query(F.data.startswith('category_')) 51 | async def starring_at_product(callback: types.CallbackQuery, session: AsyncSession): 52 | category_id = callback.data.split('_')[-1] 53 | for product in await orm_get_products(session, int(category_id)): 54 | await callback.message.answer_photo( 55 | product.image, 56 | caption=f"{product.name}\ 57 | \n{product.description}\nСтоимость: {round(product.price, 2)}", 58 | reply_markup=get_callback_btns( 59 | btns={ 60 | "Удалить": f"delete_{product.id}", 61 | "Изменить": f"change_{product.id}", 62 | }, 63 | sizes=(2,) 64 | ), 65 | ) 66 | await callback.answer() 67 | await callback.message.answer("ОК, вот список товаров ⏫") 68 | 69 | 70 | @admin_router.callback_query(F.data.startswith("delete_")) 71 | async def delete_product_callback(callback: types.CallbackQuery, session: AsyncSession): 72 | product_id = callback.data.split("_")[-1] 73 | await orm_delete_product(session, int(product_id)) 74 | 75 | await callback.answer("Товар удален") 76 | await callback.message.answer("Товар удален!") 77 | 78 | 79 | ################# Микро FSM для загрузки/изменения баннеров ############################ 80 | 81 | class AddBanner(StatesGroup): 82 | image = State() 83 | 84 | # Отправляем перечень информационных страниц бота и становимся в состояние отправки photo 85 | @admin_router.message(StateFilter(None), F.text == 'Добавить/Изменить баннер') 86 | async def add_image2(message: types.Message, state: FSMContext, session: AsyncSession): 87 | pages_names = [page.name for page in await orm_get_info_pages(session)] 88 | await message.answer(f"Отправьте фото баннера.\nВ описании укажите для какой страницы:\ 89 | \n{', '.join(pages_names)}") 90 | await state.set_state(AddBanner.image) 91 | 92 | # Добавляем/изменяем изображение в таблице (там уже есть записанные страницы по именам: 93 | # main, catalog, cart(для пустой корзины), about, payment, shipping 94 | @admin_router.message(AddBanner.image, F.photo) 95 | async def add_banner(message: types.Message, state: FSMContext, session: AsyncSession): 96 | image_id = message.photo[-1].file_id 97 | for_page = message.caption.strip() 98 | pages_names = [page.name for page in await orm_get_info_pages(session)] 99 | if for_page not in pages_names: 100 | await message.answer(f"Введите нормальное название страницы, например:\ 101 | \n{', '.join(pages_names)}") 102 | return 103 | await orm_change_banner_image(session, for_page, image_id,) 104 | await message.answer("Баннер добавлен/изменен.") 105 | await state.clear() 106 | 107 | # ловим некоррекный ввод 108 | @admin_router.message(AddBanner.image) 109 | async def add_banner2(message: types.Message, state: FSMContext): 110 | await message.answer("Отправьте фото баннера или отмена") 111 | 112 | ######################################################################################### 113 | 114 | 115 | 116 | ######################### FSM для дабавления/изменения товаров админом ################### 117 | 118 | class AddProduct(StatesGroup): 119 | # Шаги состояний 120 | name = State() 121 | description = State() 122 | category = State() 123 | price = State() 124 | image = State() 125 | 126 | product_for_change = None 127 | 128 | texts = { 129 | "AddProduct:name": "Введите название заново:", 130 | "AddProduct:description": "Введите описание заново:", 131 | "AddProduct:category": "Выберите категорию заново ⬆️", 132 | "AddProduct:price": "Введите стоимость заново:", 133 | "AddProduct:image": "Этот стейт последний, поэтому...", 134 | } 135 | 136 | 137 | # Становимся в состояние ожидания ввода name 138 | @admin_router.callback_query(StateFilter(None), F.data.startswith("change_")) 139 | async def change_product_callback( 140 | callback: types.CallbackQuery, state: FSMContext, session: AsyncSession 141 | ): 142 | product_id = callback.data.split("_")[-1] 143 | 144 | product_for_change = await orm_get_product(session, int(product_id)) 145 | 146 | AddProduct.product_for_change = product_for_change 147 | 148 | await callback.answer() 149 | await callback.message.answer( 150 | "Введите название товара", reply_markup=types.ReplyKeyboardRemove() 151 | ) 152 | await state.set_state(AddProduct.name) 153 | 154 | 155 | # Становимся в состояние ожидания ввода name 156 | @admin_router.message(StateFilter(None), F.text == "Добавить товар") 157 | async def add_product(message: types.Message, state: FSMContext): 158 | await message.answer( 159 | "Введите название товара", reply_markup=types.ReplyKeyboardRemove() 160 | ) 161 | await state.set_state(AddProduct.name) 162 | 163 | 164 | # Хендлер отмены и сброса состояния должен быть всегда именно здесь, 165 | # после того, как только встали в состояние номер 1 (элементарная очередность фильтров) 166 | @admin_router.message(StateFilter("*"), Command("отмена")) 167 | @admin_router.message(StateFilter("*"), F.text.casefold() == "отмена") 168 | async def cancel_handler(message: types.Message, state: FSMContext) -> None: 169 | current_state = await state.get_state() 170 | if current_state is None: 171 | return 172 | if AddProduct.product_for_change: 173 | AddProduct.product_for_change = None 174 | await state.clear() 175 | await message.answer("Действия отменены", reply_markup=ADMIN_KB) 176 | 177 | 178 | # Вернутся на шаг назад (на прошлое состояние) 179 | @admin_router.message(StateFilter("*"), Command("назад")) 180 | @admin_router.message(StateFilter("*"), F.text.casefold() == "назад") 181 | async def back_step_handler(message: types.Message, state: FSMContext) -> None: 182 | current_state = await state.get_state() 183 | 184 | if current_state == AddProduct.name: 185 | await message.answer( 186 | 'Предидущего шага нет, или введите название товара или напишите "отмена"' 187 | ) 188 | return 189 | 190 | previous = None 191 | for step in AddProduct.__all_states__: 192 | if step.state == current_state: 193 | await state.set_state(previous) 194 | await message.answer( 195 | f"Ок, вы вернулись к прошлому шагу \n {AddProduct.texts[previous.state]}" 196 | ) 197 | return 198 | previous = step 199 | 200 | 201 | # Ловим данные для состояние name и потом меняем состояние на description 202 | @admin_router.message(AddProduct.name, F.text) 203 | async def add_name(message: types.Message, state: FSMContext): 204 | if message.text == "." and AddProduct.product_for_change: 205 | await state.update_data(name=AddProduct.product_for_change.name) 206 | else: 207 | # Здесь можно сделать какую либо дополнительную проверку 208 | # и выйти из хендлера не меняя состояние с отправкой соответствующего сообщения 209 | # например: 210 | if 4 >= len(message.text) >= 150: 211 | await message.answer( 212 | "Название товара не должно превышать 150 символов\nили быть менее 5ти символов. \n Введите заново" 213 | ) 214 | return 215 | 216 | await state.update_data(name=message.text) 217 | await message.answer("Введите описание товара") 218 | await state.set_state(AddProduct.description) 219 | 220 | # Хендлер для отлова некорректных вводов для состояния name 221 | @admin_router.message(AddProduct.name) 222 | async def add_name2(message: types.Message, state: FSMContext): 223 | await message.answer("Вы ввели не допустимые данные, введите текст названия товара") 224 | 225 | 226 | # Ловим данные для состояние description и потом меняем состояние на price 227 | @admin_router.message(AddProduct.description, F.text) 228 | async def add_description(message: types.Message, state: FSMContext, session: AsyncSession): 229 | if message.text == "." and AddProduct.product_for_change: 230 | await state.update_data(description=AddProduct.product_for_change.description) 231 | else: 232 | if 4 >= len(message.text): 233 | await message.answer( 234 | "Слишком короткое описание. \n Введите заново" 235 | ) 236 | return 237 | await state.update_data(description=message.text) 238 | 239 | categories = await orm_get_categories(session) 240 | btns = {category.name : str(category.id) for category in categories} 241 | await message.answer("Выберите категорию", reply_markup=get_callback_btns(btns=btns)) 242 | await state.set_state(AddProduct.category) 243 | 244 | # Хендлер для отлова некорректных вводов для состояния description 245 | @admin_router.message(AddProduct.description) 246 | async def add_description2(message: types.Message, state: FSMContext): 247 | await message.answer("Вы ввели не допустимые данные, введите текст описания товара") 248 | 249 | 250 | # Ловим callback выбора категории 251 | @admin_router.callback_query(AddProduct.category) 252 | async def category_choice(callback: types.CallbackQuery, state: FSMContext , session: AsyncSession): 253 | if int(callback.data) in [category.id for category in await orm_get_categories(session)]: 254 | await callback.answer() 255 | await state.update_data(category=callback.data) 256 | await callback.message.answer('Теперь введите цену товара.') 257 | await state.set_state(AddProduct.price) 258 | else: 259 | await callback.message.answer('Выберите катеорию из кнопок.') 260 | await callback.answer() 261 | 262 | #Ловим любые некорректные действия, кроме нажатия на кнопку выбора категории 263 | @admin_router.message(AddProduct.category) 264 | async def category_choice2(message: types.Message, state: FSMContext): 265 | await message.answer("'Выберите катеорию из кнопок.'") 266 | 267 | 268 | # Ловим данные для состояние price и потом меняем состояние на image 269 | @admin_router.message(AddProduct.price, F.text) 270 | async def add_price(message: types.Message, state: FSMContext): 271 | if message.text == "." and AddProduct.product_for_change: 272 | await state.update_data(price=AddProduct.product_for_change.price) 273 | else: 274 | try: 275 | float(message.text) 276 | except ValueError: 277 | await message.answer("Введите корректное значение цены") 278 | return 279 | 280 | await state.update_data(price=message.text) 281 | await message.answer("Загрузите изображение товара") 282 | await state.set_state(AddProduct.image) 283 | 284 | # Хендлер для отлова некорректных ввода для состояния price 285 | @admin_router.message(AddProduct.price) 286 | async def add_price2(message: types.Message, state: FSMContext): 287 | await message.answer("Вы ввели не допустимые данные, введите стоимость товара") 288 | 289 | 290 | # Ловим данные для состояние image и потом выходим из состояний 291 | @admin_router.message(AddProduct.image, or_f(F.photo, F.text == ".")) 292 | async def add_image(message: types.Message, state: FSMContext, session: AsyncSession): 293 | if message.text and message.text == "." and AddProduct.product_for_change: 294 | await state.update_data(image=AddProduct.product_for_change.image) 295 | 296 | elif message.photo: 297 | await state.update_data(image=message.photo[-1].file_id) 298 | else: 299 | await message.answer("Отправьте фото пищи") 300 | return 301 | data = await state.get_data() 302 | try: 303 | if AddProduct.product_for_change: 304 | await orm_update_product(session, AddProduct.product_for_change.id, data) 305 | else: 306 | await orm_add_product(session, data) 307 | await message.answer("Товар добавлен/изменен", reply_markup=ADMIN_KB) 308 | await state.clear() 309 | 310 | except Exception as e: 311 | await message.answer( 312 | f"Ошибка: \n{str(e)}\nОбратись к программеру, он опять денег хочет", 313 | reply_markup=ADMIN_KB, 314 | ) 315 | await state.clear() 316 | 317 | AddProduct.product_for_change = None 318 | 319 | # Ловим все прочее некорректное поведение для этого состояния 320 | @admin_router.message(AddProduct.image) 321 | async def add_image2(message: types.Message, state: FSMContext): 322 | await message.answer("Отправьте фото пищи") 323 | --------------------------------------------------------------------------------