├── .gitignore ├── Dockerfile ├── LICENSE ├── README.md ├── app.py ├── data ├── __init__.py ├── assets │ ├── 1.png │ ├── 2.png │ ├── 3.png │ ├── 4.png │ ├── 5.png │ ├── 6.png │ ├── 7.png │ ├── logo.png │ └── logo_mini.png └── config.py ├── filters ├── __init__.py ├── is_admin.py └── is_user.py ├── handlers ├── __init__.py ├── admin │ ├── __init__.py │ ├── add.py │ ├── orders.py │ └── questions.py └── user │ ├── __init__.py │ ├── cart.py │ ├── catalog.py │ ├── delivery_status.py │ ├── menu.py │ ├── sos.py │ └── wallet.py ├── keyboards ├── __init__.py ├── default │ ├── __init__.py │ └── markups.py └── inline │ ├── __init__.py │ ├── categories.py │ ├── products_from_cart.py │ └── products_from_catalog.py ├── loader.py ├── requirements.txt ├── states ├── __init__.py ├── checkout_state.py ├── product_state.py └── sos_state.py └── utils ├── __init__.py └── db ├── __init__.py └── storage.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Cache files 2 | __pycache__/ 3 | 4 | # Project files 5 | .idea/ 6 | *.log 7 | .env 8 | venv 9 | .venv/ 10 | *.session 11 | data/config.py 12 | data/database.db 13 | 14 | # vscode 15 | .vscode/ 16 | .history/ -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.7-slim 2 | 3 | WORKDIR /botname 4 | 5 | COPY requirements.txt /botname/ 6 | RUN pip install -r /botname/requirements.txt 7 | COPY . /botname/ 8 | 9 | CMD python3 /botname/app.py -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Hagai 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | ShopBot 3 |

4 | 5 | This is an example Telegram shop bot. It's a simple and, most importantly, efficient way to place an order without leaving your favorite messenger. 6 | 7 | ## What can it do? 8 | 9 | 1. `/start` - needed to start the bot and choose the mode (user/admin). 10 | 11 | 2. `/menu` - go to the menu. 12 | 13 | 3. `/sos` - ask the administrator a question. 14 | 15 | ## Menu 16 | 17 | The user menu looks like this: 18 | 19 | ![User Menu](data/assets/4.png) 20 | 21 | ## Catalog 22 | 23 | The catalog consists of products sorted by categories. Users can add items to their cart, and the admin has full control over catalog management (addition/removal). 24 | 25 | ## Cart 26 | 27 | The ordering process looks like this: the user goes to the `🛍️ Catalog`, selects the desired category, chooses products, and clicks the `🛒 Cart` button. 28 | 29 | ![cart](data/assets/5.png) 30 | 31 | --- 32 | 33 | Then, after making sure everything is in place, proceed to checkout by clicking `📦 Place Order`. 34 | 35 | ![checkout](data/assets/6.png) 36 | 37 | ## Add a Product 38 | 39 | To add a product, select a category and click the `➕ Add Product` button. Then, fill out the "name-description-image-price" form and confirm. 40 | 41 | ![add_product](data/assets/1.png) 42 | 43 | ## Contacting Administration 44 | 45 | To ask the admin a question, simply select the `/sos` command. There is a limit on the number of questions. 46 | 47 | ![sos](data/assets/7.png) 48 | 49 | ## Get started 50 | 51 | 1. Clone this repository. 52 | 53 | 2. Create and activate virtual enviroment: 54 | 55 | Windows: 56 | 57 | ```powershell 58 | python -m venv venv 59 | & venv/scripts/activate.ps1 60 | ``` 61 | 62 | UNIX: 63 | 64 | ```bash 65 | python3 -m venv venv 66 | source venv/scripts/activate 67 | ``` 68 | 69 | 3. Install the requirements: 70 | 71 | ```bash 72 | pip install -r requirements.txt 73 | ``` 74 | 75 | 4. Create and populate `.env` file in the root directory. Here are the required keys (_\*_ - always required; _\*\*_ - required only in production): 76 | 77 | | Key | Value | 78 | | ------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------- | 79 | | BOT*TOKEN (*\*\_) | To get bot token, you need create a bot via [BotFather](https://t.me/BotFather/). | 80 | | PROJECT*NAME (*\*\*\_) | Name of your project on Heroku (required if you want to deploy bot on Heroku). | 81 | | WEBHOOK*HOST, WEBHOOK_PATH (*\*\*\_) | Webhook host and path. | 82 | | ADMINS (_\*\*_) | A comma-separated string of admins IDs (e.g., 000000000,123456789). To get your Telegram ID, use [Get My ID bot](https://t.me/getmyid_bot). | 83 | 84 | Example: 85 | 86 | ```properties 87 | BOT_TOKEN=YOUR_BOT_TOKEN 88 | ADMINS=123456789,000000000 89 | ``` 90 | 91 | 5. Run `app.py`: 92 | 93 | ```bash 94 | python3 app.py 95 | ``` 96 | -------------------------------------------------------------------------------- /app.py: -------------------------------------------------------------------------------- 1 | 2 | import os 3 | import handlers 4 | from aiogram import executor, types 5 | from aiogram.types import ReplyKeyboardMarkup, ReplyKeyboardRemove 6 | from data import config 7 | from loader import dp, db, bot 8 | import filters 9 | import logging 10 | 11 | filters.setup(dp) 12 | 13 | WEBAPP_HOST = "0.0.0.0" 14 | WEBAPP_PORT = int(os.environ.get("PORT", 5000)) 15 | user_message = 'Пользователь' 16 | admin_message = 'Админ' 17 | 18 | 19 | @dp.message_handler(commands='start') 20 | async def cmd_start(message: types.Message): 21 | 22 | markup = ReplyKeyboardMarkup(resize_keyboard=True) 23 | 24 | markup.row(user_message, admin_message) 25 | 26 | await message.answer('''Привет! 👋 27 | 28 | 🤖 Я бот-магазин по подаже товаров любой категории. 29 | 30 | 🛍️ Чтобы перейти в каталог и выбрать приглянувшиеся товары возпользуйтесь командой /menu. 31 | 32 | 💰 Пополнить счет можно через Яндекс.кассу, Сбербанк или Qiwi. 33 | 34 | ❓ Возникли вопросы? Не проблема! Команда /sos поможет связаться с админами, которые постараются как можно быстрее откликнуться. 35 | 36 | 🤝 Заказать похожего бота? Свяжитесь с разработчиком Nikolay Simakov, он не кусается))) 37 | ''', reply_markup=markup) 38 | 39 | 40 | @dp.message_handler(text=user_message) 41 | async def user_mode(message: types.Message): 42 | 43 | cid = message.chat.id 44 | if cid in config.ADMINS: 45 | config.ADMINS.remove(cid) 46 | 47 | await message.answer('Включен пользовательский режим.', reply_markup=ReplyKeyboardRemove()) 48 | 49 | 50 | @dp.message_handler(text=admin_message) 51 | async def admin_mode(message: types.Message): 52 | 53 | cid = message.chat.id 54 | if cid not in config.ADMINS: 55 | config.ADMINS.append(cid) 56 | 57 | await message.answer('Включен админский режим.', reply_markup=ReplyKeyboardRemove()) 58 | 59 | 60 | async def on_startup(dp): 61 | logging.basicConfig(level=logging.INFO) 62 | db.create_tables() 63 | 64 | await bot.delete_webhook() 65 | if config.WEBHOOK_URL: 66 | await bot.set_webhook(config.WEBHOOK_URL) 67 | 68 | 69 | async def on_shutdown(): 70 | logging.warning("Shutting down..") 71 | await bot.delete_webhook() 72 | await dp.storage.close() 73 | await dp.storage.wait_closed() 74 | logging.warning("Bot down") 75 | 76 | 77 | if __name__ == '__main__': 78 | 79 | if (("HEROKU_APP_NAME" in list(os.environ.keys())) or 80 | ("RAILWAY_PUBLIC_DOMAIN" in list(os.environ.keys()))): 81 | 82 | executor.start_webhook( 83 | dispatcher=dp, 84 | webhook_path=config.WEBHOOK_PATH, 85 | on_startup=on_startup, 86 | on_shutdown=on_shutdown, 87 | skip_updates=True, 88 | host=WEBAPP_HOST, 89 | port=WEBAPP_PORT, 90 | ) 91 | 92 | else: 93 | 94 | executor.start_polling(dp, on_startup=on_startup, skip_updates=False) 95 | -------------------------------------------------------------------------------- /data/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NikolaySimakov/Shop-bot/ce2c8003ac6f23fd0b5ea2ad49bf617c785ada36/data/__init__.py -------------------------------------------------------------------------------- /data/assets/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NikolaySimakov/Shop-bot/ce2c8003ac6f23fd0b5ea2ad49bf617c785ada36/data/assets/1.png -------------------------------------------------------------------------------- /data/assets/2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NikolaySimakov/Shop-bot/ce2c8003ac6f23fd0b5ea2ad49bf617c785ada36/data/assets/2.png -------------------------------------------------------------------------------- /data/assets/3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NikolaySimakov/Shop-bot/ce2c8003ac6f23fd0b5ea2ad49bf617c785ada36/data/assets/3.png -------------------------------------------------------------------------------- /data/assets/4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NikolaySimakov/Shop-bot/ce2c8003ac6f23fd0b5ea2ad49bf617c785ada36/data/assets/4.png -------------------------------------------------------------------------------- /data/assets/5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NikolaySimakov/Shop-bot/ce2c8003ac6f23fd0b5ea2ad49bf617c785ada36/data/assets/5.png -------------------------------------------------------------------------------- /data/assets/6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NikolaySimakov/Shop-bot/ce2c8003ac6f23fd0b5ea2ad49bf617c785ada36/data/assets/6.png -------------------------------------------------------------------------------- /data/assets/7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NikolaySimakov/Shop-bot/ce2c8003ac6f23fd0b5ea2ad49bf617c785ada36/data/assets/7.png -------------------------------------------------------------------------------- /data/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NikolaySimakov/Shop-bot/ce2c8003ac6f23fd0b5ea2ad49bf617c785ada36/data/assets/logo.png -------------------------------------------------------------------------------- /data/assets/logo_mini.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NikolaySimakov/Shop-bot/ce2c8003ac6f23fd0b5ea2ad49bf617c785ada36/data/assets/logo_mini.png -------------------------------------------------------------------------------- /data/config.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from dotenv import load_dotenv 4 | 5 | load_dotenv() 6 | 7 | BOT_TOKEN = os.getenv("BOT_TOKEN") 8 | 9 | PROJECT_NAME = os.getenv("PROJECT_NAME") 10 | 11 | # Heroku webhook 12 | # WEBHOOK_HOST = f"https://{PROJECT_NAME}.herokuapp.com" 13 | # WEBHOOK_PATH = '/webhook/' + BOT_TOKEN 14 | 15 | # Railway webhook 16 | WEBHOOK_HOST = os.getenv("RAILWAY_PUBLIC_DOMAIN") 17 | WEBHOOK_PATH = os.getenv("WEBHOOK_PATH") 18 | 19 | if WEBHOOK_HOST and WEBHOOK_PATH: 20 | WEBHOOK_URL = f"{WEBHOOK_HOST}{WEBHOOK_PATH}" 21 | else: 22 | WEBHOOK_URL = None 23 | 24 | ADMINS = list(map(int, os.getenv("ADMINS", "").split(","))) 25 | -------------------------------------------------------------------------------- /filters/__init__.py: -------------------------------------------------------------------------------- 1 | from aiogram import Dispatcher 2 | from .is_admin import IsAdmin 3 | from .is_user import IsUser 4 | 5 | def setup(dp: Dispatcher): 6 | dp.filters_factory.bind(IsAdmin, event_handlers=[dp.message_handlers]) 7 | dp.filters_factory.bind(IsUser, event_handlers=[dp.message_handlers]) 8 | -------------------------------------------------------------------------------- /filters/is_admin.py: -------------------------------------------------------------------------------- 1 | 2 | from aiogram.types import Message 3 | from aiogram.dispatcher.filters import BoundFilter 4 | from data.config import ADMINS 5 | 6 | class IsAdmin(BoundFilter): 7 | 8 | async def check(self, message: Message): 9 | return message.from_user.id in ADMINS 10 | -------------------------------------------------------------------------------- /filters/is_user.py: -------------------------------------------------------------------------------- 1 | 2 | from aiogram.types import Message 3 | from aiogram.dispatcher.filters import BoundFilter 4 | from data.config import ADMINS 5 | 6 | class IsUser(BoundFilter): 7 | 8 | async def check(self, message: Message): 9 | return message.from_user.id not in ADMINS 10 | -------------------------------------------------------------------------------- /handlers/__init__.py: -------------------------------------------------------------------------------- 1 | from .admin import dp 2 | from .user import dp 3 | 4 | __all__ = ['dp'] -------------------------------------------------------------------------------- /handlers/admin/__init__.py: -------------------------------------------------------------------------------- 1 | from .add import dp 2 | from .questions import dp 3 | from .orders import dp 4 | 5 | __all__ = ['dp'] -------------------------------------------------------------------------------- /handlers/admin/add.py: -------------------------------------------------------------------------------- 1 | 2 | from aiogram.dispatcher import FSMContext 3 | from aiogram.types import Message, CallbackQuery, InlineKeyboardMarkup, InlineKeyboardButton, ContentType, ReplyKeyboardMarkup, ReplyKeyboardRemove 4 | from aiogram.utils.callback_data import CallbackData 5 | from keyboards.default.markups import * 6 | from states import ProductState, CategoryState 7 | from aiogram.types.chat import ChatActions 8 | from handlers.user.menu import settings 9 | from loader import dp, db, bot 10 | from filters import IsAdmin 11 | from hashlib import md5 12 | 13 | 14 | category_cb = CallbackData('category', 'id', 'action') 15 | product_cb = CallbackData('product', 'id', 'action') 16 | 17 | add_product = '➕ Добавить товар' 18 | delete_category = '🗑️ Удалить категорию' 19 | 20 | 21 | @dp.message_handler(IsAdmin(), text=settings) 22 | async def process_settings(message: Message): 23 | 24 | markup = InlineKeyboardMarkup() 25 | 26 | for idx, title in db.fetchall('SELECT * FROM categories'): 27 | 28 | markup.add(InlineKeyboardButton( 29 | title, callback_data=category_cb.new(id=idx, action='view'))) 30 | 31 | markup.add(InlineKeyboardButton( 32 | '+ Добавить категорию', callback_data='add_category')) 33 | 34 | await message.answer('Настройка категорий:', reply_markup=markup) 35 | 36 | 37 | @dp.callback_query_handler(IsAdmin(), category_cb.filter(action='view')) 38 | async def category_callback_handler(query: CallbackQuery, callback_data: dict, state: FSMContext): 39 | 40 | category_idx = callback_data['id'] 41 | 42 | products = db.fetchall('''SELECT * FROM products product 43 | WHERE product.tag = (SELECT title FROM categories WHERE idx=?)''', 44 | (category_idx,)) 45 | 46 | await query.message.delete() 47 | await query.answer('Все добавленные товары в эту категорию.') 48 | await state.update_data(category_index=category_idx) 49 | await show_products(query.message, products, category_idx) 50 | 51 | 52 | # category 53 | 54 | 55 | @dp.callback_query_handler(IsAdmin(), text='add_category') 56 | async def add_category_callback_handler(query: CallbackQuery): 57 | await query.message.delete() 58 | await query.message.answer('Название категории?') 59 | await CategoryState.title.set() 60 | 61 | 62 | @dp.message_handler(IsAdmin(), state=CategoryState.title) 63 | async def set_category_title_handler(message: Message, state: FSMContext): 64 | 65 | category = message.text 66 | idx = md5(category.encode('utf-8')).hexdigest() 67 | db.query('INSERT INTO categories VALUES (?, ?)', (idx, category)) 68 | 69 | await state.finish() 70 | await process_settings(message) 71 | 72 | 73 | @dp.message_handler(IsAdmin(), text=delete_category) 74 | async def delete_category_handler(message: Message, state: FSMContext): 75 | 76 | async with state.proxy() as data: 77 | 78 | if 'category_index' in data.keys(): 79 | 80 | idx = data['category_index'] 81 | 82 | db.query( 83 | 'DELETE FROM products WHERE tag IN (SELECT title FROM categories WHERE idx=?)', (idx,)) 84 | db.query('DELETE FROM categories WHERE idx=?', (idx,)) 85 | 86 | await message.answer('Готово!', reply_markup=ReplyKeyboardRemove()) 87 | await process_settings(message) 88 | 89 | 90 | # add product 91 | 92 | 93 | @dp.message_handler(IsAdmin(), text=add_product) 94 | async def process_add_product(message: Message): 95 | 96 | await ProductState.title.set() 97 | 98 | markup = ReplyKeyboardMarkup(resize_keyboard=True) 99 | markup.add(cancel_message) 100 | 101 | await message.answer('Название?', reply_markup=markup) 102 | 103 | 104 | @dp.message_handler(IsAdmin(), text=cancel_message, state=ProductState.title) 105 | async def process_cancel(message: Message, state: FSMContext): 106 | 107 | await message.answer('Ок, отменено!', reply_markup=ReplyKeyboardRemove()) 108 | await state.finish() 109 | 110 | await process_settings(message) 111 | 112 | 113 | @dp.message_handler(IsAdmin(), text=back_message, state=ProductState.title) 114 | async def process_title_back(message: Message, state: FSMContext): 115 | await process_add_product(message) 116 | 117 | 118 | @dp.message_handler(IsAdmin(), state=ProductState.title) 119 | async def process_title(message: Message, state: FSMContext): 120 | 121 | async with state.proxy() as data: 122 | data['title'] = message.text 123 | 124 | await ProductState.next() 125 | await message.answer('Описание?', reply_markup=back_markup()) 126 | 127 | 128 | @dp.message_handler(IsAdmin(), text=back_message, state=ProductState.body) 129 | async def process_body_back(message: Message, state: FSMContext): 130 | 131 | await ProductState.title.set() 132 | 133 | async with state.proxy() as data: 134 | 135 | await message.answer(f"Изменить название с {data['title']}?", reply_markup=back_markup()) 136 | 137 | 138 | @dp.message_handler(IsAdmin(), state=ProductState.body) 139 | async def process_body(message: Message, state: FSMContext): 140 | 141 | async with state.proxy() as data: 142 | data['body'] = message.text 143 | 144 | await ProductState.next() 145 | await message.answer('Фото?', reply_markup=back_markup()) 146 | 147 | 148 | @dp.message_handler(IsAdmin(), content_types=ContentType.PHOTO, state=ProductState.image) 149 | async def process_image_photo(message: Message, state: FSMContext): 150 | 151 | fileID = message.photo[-1].file_id 152 | file_info = await bot.get_file(fileID) 153 | downloaded_file = (await bot.download_file(file_info.file_path)).read() 154 | 155 | async with state.proxy() as data: 156 | data['image'] = downloaded_file 157 | 158 | await ProductState.next() 159 | await message.answer('Цена?', reply_markup=back_markup()) 160 | 161 | 162 | @dp.message_handler(IsAdmin(), content_types=ContentType.TEXT, state=ProductState.image) 163 | async def process_image_url(message: Message, state: FSMContext): 164 | 165 | if message.text == back_message: 166 | 167 | await ProductState.body.set() 168 | 169 | async with state.proxy() as data: 170 | 171 | await message.answer(f"Изменить описание с {data['body']}?", reply_markup=back_markup()) 172 | 173 | else: 174 | 175 | await message.answer('Вам нужно прислать фото товара.') 176 | 177 | 178 | @dp.message_handler(IsAdmin(), lambda message: not message.text.isdigit(), state=ProductState.price) 179 | async def process_price_invalid(message: Message, state: FSMContext): 180 | 181 | if message.text == back_message: 182 | 183 | await ProductState.image.set() 184 | 185 | async with state.proxy() as data: 186 | 187 | await message.answer("Другое изображение?", reply_markup=back_markup()) 188 | 189 | else: 190 | 191 | await message.answer('Укажите цену в виде числа!') 192 | 193 | 194 | @dp.message_handler(IsAdmin(), lambda message: message.text.isdigit(), state=ProductState.price) 195 | async def process_price(message: Message, state: FSMContext): 196 | 197 | async with state.proxy() as data: 198 | 199 | data['price'] = message.text 200 | 201 | title = data['title'] 202 | body = data['body'] 203 | price = data['price'] 204 | 205 | await ProductState.next() 206 | text = f'{title}\n\n{body}\n\nЦена: {price} рублей.' 207 | 208 | markup = check_markup() 209 | 210 | await message.answer_photo(photo=data['image'], 211 | caption=text, 212 | reply_markup=markup) 213 | 214 | 215 | @dp.message_handler(IsAdmin(), lambda message: message.text not in [back_message, all_right_message], state=ProductState.confirm) 216 | async def process_confirm_invalid(message: Message, state: FSMContext): 217 | await message.answer('Такого варианта не было.') 218 | 219 | 220 | @dp.message_handler(IsAdmin(), text=back_message, state=ProductState.confirm) 221 | async def process_confirm_back(message: Message, state: FSMContext): 222 | 223 | await ProductState.price.set() 224 | 225 | async with state.proxy() as data: 226 | 227 | await message.answer(f"Изменить цену с {data['price']}?", reply_markup=back_markup()) 228 | 229 | 230 | @dp.message_handler(IsAdmin(), text=all_right_message, state=ProductState.confirm) 231 | async def process_confirm(message: Message, state: FSMContext): 232 | 233 | async with state.proxy() as data: 234 | 235 | title = data['title'] 236 | body = data['body'] 237 | image = data['image'] 238 | price = data['price'] 239 | 240 | tag = db.fetchone( 241 | 'SELECT title FROM categories WHERE idx=?', (data['category_index'],))[0] 242 | idx = md5(' '.join([title, body, price, tag] 243 | ).encode('utf-8')).hexdigest() 244 | 245 | db.query('INSERT INTO products VALUES (?, ?, ?, ?, ?, ?)', 246 | (idx, title, body, image, int(price), tag)) 247 | 248 | await state.finish() 249 | await message.answer('Готово!', reply_markup=ReplyKeyboardRemove()) 250 | await process_settings(message) 251 | 252 | 253 | # delete product 254 | 255 | 256 | @dp.callback_query_handler(IsAdmin(), product_cb.filter(action='delete')) 257 | async def delete_product_callback_handler(query: CallbackQuery, callback_data: dict): 258 | 259 | product_idx = callback_data['id'] 260 | db.query('DELETE FROM products WHERE idx=?', (product_idx,)) 261 | await query.answer('Удалено!') 262 | await query.message.delete() 263 | 264 | 265 | async def show_products(m, products, category_idx): 266 | 267 | await bot.send_chat_action(m.chat.id, ChatActions.TYPING) 268 | 269 | for idx, title, body, image, price, tag in products: 270 | 271 | text = f'{title}\n\n{body}\n\nЦена: {price} рублей.' 272 | 273 | markup = InlineKeyboardMarkup() 274 | markup.add(InlineKeyboardButton( 275 | '🗑️ Удалить', callback_data=product_cb.new(id=idx, action='delete'))) 276 | 277 | await m.answer_photo(photo=image, 278 | caption=text, 279 | reply_markup=markup) 280 | 281 | markup = ReplyKeyboardMarkup() 282 | markup.add(add_product) 283 | markup.add(delete_category) 284 | 285 | await m.answer('Хотите что-нибудь добавить или удалить?', reply_markup=markup) 286 | -------------------------------------------------------------------------------- /handlers/admin/orders.py: -------------------------------------------------------------------------------- 1 | 2 | from aiogram.types import Message 3 | from loader import dp, db 4 | from handlers.user.menu import orders 5 | from filters import IsAdmin 6 | 7 | @dp.message_handler(IsAdmin(), text=orders) 8 | async def process_orders(message: Message): 9 | 10 | orders = db.fetchall('SELECT * FROM orders') 11 | 12 | if len(orders) == 0: await message.answer('У вас нет заказов.') 13 | else: await order_answer(message, orders) 14 | 15 | async def order_answer(message, orders): 16 | 17 | res = '' 18 | 19 | for order in orders: 20 | res += f'Заказ №{order[3]}\n\n' 21 | 22 | await message.answer(res) -------------------------------------------------------------------------------- /handlers/admin/questions.py: -------------------------------------------------------------------------------- 1 | 2 | from handlers.user.menu import questions 3 | from aiogram.dispatcher import FSMContext 4 | from aiogram.utils.callback_data import CallbackData 5 | from keyboards.default.markups import all_right_message, cancel_message, submit_markup 6 | from aiogram.types import Message, CallbackQuery, InlineKeyboardMarkup, InlineKeyboardButton, ReplyKeyboardRemove 7 | from aiogram.types.chat import ChatActions 8 | from states import AnswerState 9 | from loader import dp, db, bot 10 | from filters import IsAdmin 11 | 12 | question_cb = CallbackData('question', 'cid', 'action') 13 | 14 | 15 | @dp.message_handler(IsAdmin(), text=questions) 16 | async def process_questions(message: Message): 17 | 18 | await bot.send_chat_action(message.chat.id, ChatActions.TYPING) 19 | questions = db.fetchall('SELECT * FROM questions') 20 | 21 | if len(questions) == 0: 22 | 23 | await message.answer('Нет вопросов.') 24 | 25 | else: 26 | 27 | for cid, question in questions: 28 | 29 | markup = InlineKeyboardMarkup() 30 | markup.add(InlineKeyboardButton( 31 | 'Ответить', callback_data=question_cb.new(cid=cid, action='answer'))) 32 | 33 | await message.answer(question, reply_markup=markup) 34 | 35 | 36 | @dp.callback_query_handler(IsAdmin(), question_cb.filter(action='answer')) 37 | async def process_answer(query: CallbackQuery, callback_data: dict, state: FSMContext): 38 | 39 | async with state.proxy() as data: 40 | data['cid'] = callback_data['cid'] 41 | 42 | await query.message.answer('Напиши ответ.', reply_markup=ReplyKeyboardRemove()) 43 | await AnswerState.answer.set() 44 | 45 | 46 | @dp.message_handler(IsAdmin(), state=AnswerState.answer) 47 | async def process_submit(message: Message, state: FSMContext): 48 | 49 | async with state.proxy() as data: 50 | data['answer'] = message.text 51 | 52 | await AnswerState.next() 53 | await message.answer('Убедитесь, что не ошиблись в ответе.', reply_markup=submit_markup()) 54 | 55 | 56 | @dp.message_handler(IsAdmin(), text=cancel_message, state=AnswerState.submit) 57 | async def process_send_answer(message: Message, state: FSMContext): 58 | await message.answer('Отменено!', reply_markup=ReplyKeyboardRemove()) 59 | await state.finish() 60 | 61 | 62 | @dp.message_handler(IsAdmin(), text=all_right_message, state=AnswerState.submit) 63 | async def process_send_answer(message: Message, state: FSMContext): 64 | 65 | async with state.proxy() as data: 66 | 67 | answer = data['answer'] 68 | cid = data['cid'] 69 | 70 | question = db.fetchone( 71 | 'SELECT question FROM questions WHERE cid=?', (cid,))[0] 72 | db.query('DELETE FROM questions WHERE cid=?', (cid,)) 73 | text = f'Вопрос: {question}\n\nОтвет: {answer}' 74 | 75 | await message.answer('Отправлено!', reply_markup=ReplyKeyboardRemove()) 76 | await bot.send_message(cid, text) 77 | 78 | await state.finish() 79 | -------------------------------------------------------------------------------- /handlers/user/__init__.py: -------------------------------------------------------------------------------- 1 | from .menu import dp 2 | from .cart import dp 3 | from .wallet import dp 4 | from .catalog import dp 5 | from .delivery_status import dp 6 | from .sos import dp 7 | 8 | __all__ = ['dp'] -------------------------------------------------------------------------------- /handlers/user/cart.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from aiogram.dispatcher import FSMContext 3 | from aiogram.types import Message, CallbackQuery, ReplyKeyboardMarkup, ReplyKeyboardRemove, InlineKeyboardMarkup, InlineKeyboardButton 4 | from keyboards.inline.products_from_cart import product_markup, product_cb 5 | from aiogram.utils.callback_data import CallbackData 6 | from keyboards.default.markups import * 7 | from aiogram.types.chat import ChatActions 8 | from states import CheckoutState 9 | from loader import dp, db, bot 10 | from filters import IsUser 11 | from .menu import cart 12 | 13 | 14 | @dp.message_handler(IsUser(), text=cart) 15 | async def process_cart(message: Message, state: FSMContext): 16 | 17 | cart_data = db.fetchall( 18 | 'SELECT * FROM cart WHERE cid=?', (message.chat.id,)) 19 | 20 | if len(cart_data) == 0: 21 | 22 | await message.answer('Ваша корзина пуста.') 23 | 24 | else: 25 | 26 | await bot.send_chat_action(message.chat.id, ChatActions.TYPING) 27 | async with state.proxy() as data: 28 | data['products'] = {} 29 | 30 | order_cost = 0 31 | 32 | for _, idx, count_in_cart in cart_data: 33 | 34 | product = db.fetchone('SELECT * FROM products WHERE idx=?', (idx,)) 35 | 36 | if product == None: 37 | 38 | db.query('DELETE FROM cart WHERE idx=?', (idx,)) 39 | 40 | else: 41 | _, title, body, image, price, _ = product 42 | order_cost += price 43 | 44 | async with state.proxy() as data: 45 | data['products'][idx] = [title, price, count_in_cart] 46 | 47 | markup = product_markup(idx, count_in_cart) 48 | text = f'{title}\n\n{body}\n\nЦена: {price}₽.' 49 | 50 | await message.answer_photo(photo=image, 51 | caption=text, 52 | reply_markup=markup) 53 | 54 | if order_cost != 0: 55 | markup = ReplyKeyboardMarkup(resize_keyboard=True, selective=True) 56 | markup.add('📦 Оформить заказ') 57 | 58 | await message.answer('Перейти к оформлению?', 59 | reply_markup=markup) 60 | 61 | 62 | @dp.callback_query_handler(IsUser(), product_cb.filter(action='count')) 63 | @dp.callback_query_handler(IsUser(), product_cb.filter(action='increase')) 64 | @dp.callback_query_handler(IsUser(), product_cb.filter(action='decrease')) 65 | async def product_callback_handler(query: CallbackQuery, callback_data: dict, state: FSMContext): 66 | 67 | idx = callback_data['id'] 68 | action = callback_data['action'] 69 | 70 | if 'count' == action: 71 | 72 | async with state.proxy() as data: 73 | 74 | if 'products' not in data.keys(): 75 | 76 | await process_cart(query.message, state) 77 | 78 | else: 79 | 80 | await query.answer('Количество - ' + data['products'][idx][2]) 81 | 82 | else: 83 | 84 | async with state.proxy() as data: 85 | 86 | if 'products' not in data.keys(): 87 | 88 | await process_cart(query.message, state) 89 | 90 | else: 91 | 92 | data['products'][idx][2] += 1 if 'increase' == action else -1 93 | count_in_cart = data['products'][idx][2] 94 | 95 | if count_in_cart == 0: 96 | 97 | db.query('''DELETE FROM cart 98 | WHERE cid = ? AND idx = ?''', (query.message.chat.id, idx)) 99 | 100 | await query.message.delete() 101 | else: 102 | 103 | db.query('''UPDATE cart 104 | SET quantity = ? 105 | WHERE cid = ? AND idx = ?''', (count_in_cart, query.message.chat.id, idx)) 106 | 107 | await query.message.edit_reply_markup(product_markup(idx, count_in_cart)) 108 | 109 | 110 | @dp.message_handler(IsUser(), text='📦 Оформить заказ') 111 | async def process_checkout(message: Message, state: FSMContext): 112 | 113 | await CheckoutState.check_cart.set() 114 | await checkout(message, state) 115 | 116 | 117 | async def checkout(message, state): 118 | answer = '' 119 | total_price = 0 120 | 121 | async with state.proxy() as data: 122 | 123 | for title, price, count_in_cart in data['products'].values(): 124 | 125 | tp = count_in_cart * price 126 | answer += f'{title} * {count_in_cart}шт. = {tp}₽\n' 127 | total_price += tp 128 | 129 | await message.answer(f'{answer}\nОбщая сумма заказа: {total_price}₽.', 130 | reply_markup=check_markup()) 131 | 132 | 133 | @dp.message_handler(IsUser(), lambda message: message.text not in [all_right_message, back_message], state=CheckoutState.check_cart) 134 | async def process_check_cart_invalid(message: Message): 135 | await message.reply('Такого варианта не было.') 136 | 137 | 138 | @dp.message_handler(IsUser(), text=back_message, state=CheckoutState.check_cart) 139 | async def process_check_cart_back(message: Message, state: FSMContext): 140 | await state.finish() 141 | await process_cart(message, state) 142 | 143 | 144 | @dp.message_handler(IsUser(), text=all_right_message, state=CheckoutState.check_cart) 145 | async def process_check_cart_all_right(message: Message, state: FSMContext): 146 | await CheckoutState.next() 147 | await message.answer('Укажите свое имя.', 148 | reply_markup=back_markup()) 149 | 150 | 151 | @dp.message_handler(IsUser(), text=back_message, state=CheckoutState.name) 152 | async def process_name_back(message: Message, state: FSMContext): 153 | await CheckoutState.check_cart.set() 154 | await checkout(message, state) 155 | 156 | 157 | @dp.message_handler(IsUser(), state=CheckoutState.name) 158 | async def process_name(message: Message, state: FSMContext): 159 | 160 | async with state.proxy() as data: 161 | 162 | data['name'] = message.text 163 | 164 | if 'address' in data.keys(): 165 | 166 | await confirm(message) 167 | await CheckoutState.confirm.set() 168 | 169 | else: 170 | 171 | await CheckoutState.next() 172 | await message.answer('Укажите свой адрес места жительства.', 173 | reply_markup=back_markup()) 174 | 175 | 176 | @dp.message_handler(IsUser(), text=back_message, state=CheckoutState.address) 177 | async def process_address_back(message: Message, state: FSMContext): 178 | 179 | async with state.proxy() as data: 180 | 181 | await message.answer('Изменить имя с ' + data['name'] + '?', 182 | reply_markup=back_markup()) 183 | 184 | await CheckoutState.name.set() 185 | 186 | 187 | @dp.message_handler(IsUser(), state=CheckoutState.address) 188 | async def process_address(message: Message, state: FSMContext): 189 | 190 | async with state.proxy() as data: 191 | data['address'] = message.text 192 | 193 | await confirm(message) 194 | await CheckoutState.next() 195 | 196 | 197 | async def confirm(message): 198 | 199 | await message.answer('Убедитесь, что все правильно оформлено и подтвердите заказ.', 200 | reply_markup=confirm_markup()) 201 | 202 | 203 | @dp.message_handler(IsUser(), lambda message: message.text not in [confirm_message, back_message], state=CheckoutState.confirm) 204 | async def process_confirm_invalid(message: Message): 205 | await message.reply('Такого варианта не было.') 206 | 207 | 208 | @dp.message_handler(IsUser(), text=back_message, state=CheckoutState.confirm) 209 | async def process_confirm(message: Message, state: FSMContext): 210 | 211 | await CheckoutState.address.set() 212 | 213 | async with state.proxy() as data: 214 | await message.answer('Изменить адрес с ' + data['address'] + '?', 215 | reply_markup=back_markup()) 216 | 217 | 218 | @dp.message_handler(IsUser(), text=confirm_message, state=CheckoutState.confirm) 219 | async def process_confirm(message: Message, state: FSMContext): 220 | 221 | enough_money = True # enough money on the balance sheet 222 | markup = ReplyKeyboardRemove() 223 | 224 | if enough_money: 225 | 226 | logging.info('Deal was made.') 227 | 228 | async with state.proxy() as data: 229 | 230 | cid = message.chat.id 231 | products = [idx + '=' + str(quantity) 232 | for idx, quantity in db.fetchall('''SELECT idx, quantity FROM cart 233 | WHERE cid=?''', (cid,))] # idx=quantity 234 | 235 | db.query('INSERT INTO orders VALUES (?, ?, ?, ?)', 236 | (cid, data['name'], data['address'], ' '.join(products))) 237 | 238 | db.query('DELETE FROM cart WHERE cid=?', (cid,)) 239 | 240 | await message.answer('Ок! Ваш заказ уже в пути 🚀\nИмя: ' + data['name'] + '\nАдрес: ' + data['address'] + '', 241 | reply_markup=markup) 242 | else: 243 | 244 | await message.answer('У вас недостаточно денег на счете. Пополните баланс!', 245 | reply_markup=markup) 246 | 247 | await state.finish() 248 | -------------------------------------------------------------------------------- /handlers/user/catalog.py: -------------------------------------------------------------------------------- 1 | 2 | import logging 3 | from aiogram.types import Message, CallbackQuery 4 | from keyboards.inline.categories import categories_markup, category_cb 5 | from keyboards.inline.products_from_catalog import product_markup, product_cb 6 | from aiogram.utils.callback_data import CallbackData 7 | from aiogram.types.chat import ChatActions 8 | from loader import dp, db, bot 9 | from .menu import catalog 10 | from filters import IsUser 11 | 12 | 13 | @dp.message_handler(IsUser(), text=catalog) 14 | async def process_catalog(message: Message): 15 | await message.answer('Выберите раздел, чтобы вывести список товаров:', 16 | reply_markup=categories_markup()) 17 | 18 | 19 | @dp.callback_query_handler(IsUser(), category_cb.filter(action='view')) 20 | async def category_callback_handler(query: CallbackQuery, callback_data: dict): 21 | 22 | products = db.fetchall('''SELECT * FROM products product 23 | WHERE product.tag = (SELECT title FROM categories WHERE idx=?) 24 | AND product.idx NOT IN (SELECT idx FROM cart WHERE cid = ?)''', 25 | (callback_data['id'], query.message.chat.id)) 26 | 27 | await query.answer('Все доступные товары.') 28 | await show_products(query.message, products) 29 | 30 | 31 | @dp.callback_query_handler(IsUser(), product_cb.filter(action='add')) 32 | async def add_product_callback_handler(query: CallbackQuery, callback_data: dict): 33 | 34 | db.query('INSERT INTO cart VALUES (?, ?, 1)', 35 | (query.message.chat.id, callback_data['id'])) 36 | 37 | await query.answer('Товар добавлен в корзину!') 38 | await query.message.delete() 39 | 40 | 41 | async def show_products(m, products): 42 | 43 | if len(products) == 0: 44 | 45 | await m.answer('Здесь ничего нет 😢') 46 | 47 | else: 48 | 49 | await bot.send_chat_action(m.chat.id, ChatActions.TYPING) 50 | 51 | for idx, title, body, image, price, _ in products: 52 | 53 | markup = product_markup(idx, price) 54 | text = f'{title}\n\n{body}' 55 | 56 | await m.answer_photo(photo=image, 57 | caption=text, 58 | reply_markup=markup) 59 | -------------------------------------------------------------------------------- /handlers/user/delivery_status.py: -------------------------------------------------------------------------------- 1 | 2 | from aiogram.types import Message 3 | from loader import dp, db 4 | from .menu import delivery_status 5 | from filters import IsUser 6 | 7 | @dp.message_handler(IsUser(), text=delivery_status) 8 | async def process_delivery_status(message: Message): 9 | 10 | orders = db.fetchall('SELECT * FROM orders WHERE cid=?', (message.chat.id,)) 11 | 12 | if len(orders) == 0: await message.answer('У вас нет активных заказов.') 13 | else: await delivery_status_answer(message, orders) 14 | 15 | async def delivery_status_answer(message, orders): 16 | 17 | res = '' 18 | 19 | for order in orders: 20 | 21 | res += f'Заказ №{order[3]}' 22 | answer = [ 23 | ' лежит на складе.', 24 | ' уже в пути!', 25 | ' прибыл и ждет вас на почте!' 26 | ] 27 | 28 | res += answer[0] 29 | res += '\n\n' 30 | 31 | await message.answer(res) -------------------------------------------------------------------------------- /handlers/user/menu.py: -------------------------------------------------------------------------------- 1 | 2 | from aiogram.types import Message, CallbackQuery, ReplyKeyboardMarkup 3 | from loader import dp 4 | from filters import IsAdmin, IsUser 5 | 6 | catalog = '🛍️ Каталог' 7 | balance = '💰 Баланс' 8 | cart = '🛒 Корзина' 9 | delivery_status = '🚚 Статус заказа' 10 | 11 | settings = '⚙️ Настройка каталога' 12 | orders = '🚚 Заказы' 13 | questions = '❓ Вопросы' 14 | 15 | @dp.message_handler(IsAdmin(), commands='menu') 16 | async def admin_menu(message: Message): 17 | markup = ReplyKeyboardMarkup(selective=True) 18 | markup.add(settings) 19 | markup.add(questions, orders) 20 | 21 | await message.answer('Меню', reply_markup=markup) 22 | 23 | @dp.message_handler(IsUser(), commands='menu') 24 | async def user_menu(message: Message): 25 | markup = ReplyKeyboardMarkup(selective=True) 26 | markup.add(catalog) 27 | markup.add(balance, cart) 28 | markup.add(delivery_status) 29 | 30 | await message.answer('Меню', reply_markup=markup) 31 | -------------------------------------------------------------------------------- /handlers/user/sos.py: -------------------------------------------------------------------------------- 1 | 2 | from aiogram.dispatcher import FSMContext 3 | from aiogram.types import ReplyKeyboardMarkup, ReplyKeyboardRemove 4 | from keyboards.default.markups import all_right_message, cancel_message, submit_markup 5 | from aiogram.types import Message 6 | from states import SosState 7 | from filters import IsUser 8 | from loader import dp, db 9 | 10 | 11 | @dp.message_handler(commands='sos') 12 | async def cmd_sos(message: Message): 13 | await SosState.question.set() 14 | await message.answer('В чем суть проблемы? Опишите как можно детальнее и администратор обязательно вам ответит.', reply_markup=ReplyKeyboardRemove()) 15 | 16 | 17 | @dp.message_handler(state=SosState.question) 18 | async def process_question(message: Message, state: FSMContext): 19 | async with state.proxy() as data: 20 | data['question'] = message.text 21 | 22 | await message.answer('Убедитесь, что все верно.', reply_markup=submit_markup()) 23 | await SosState.next() 24 | 25 | 26 | @dp.message_handler(lambda message: message.text not in [cancel_message, all_right_message], state=SosState.submit) 27 | async def process_price_invalid(message: Message): 28 | await message.answer('Такого варианта не было.') 29 | 30 | 31 | @dp.message_handler(text=cancel_message, state=SosState.submit) 32 | async def process_cancel(message: Message, state: FSMContext): 33 | await message.answer('Отменено!', reply_markup=ReplyKeyboardRemove()) 34 | await state.finish() 35 | 36 | 37 | @dp.message_handler(text=all_right_message, state=SosState.submit) 38 | async def process_submit(message: Message, state: FSMContext): 39 | 40 | cid = message.chat.id 41 | 42 | if db.fetchone('SELECT * FROM questions WHERE cid=?', (cid,)) == None: 43 | 44 | async with state.proxy() as data: 45 | db.query('INSERT INTO questions VALUES (?, ?)', 46 | (cid, data['question'])) 47 | 48 | await message.answer('Отправлено!', reply_markup=ReplyKeyboardRemove()) 49 | 50 | else: 51 | 52 | await message.answer('Превышен лимит на количество задаваемых вопросов.', reply_markup=ReplyKeyboardRemove()) 53 | 54 | await state.finish() 55 | -------------------------------------------------------------------------------- /handlers/user/wallet.py: -------------------------------------------------------------------------------- 1 | 2 | from loader import dp 3 | from aiogram.dispatcher import FSMContext 4 | from aiogram.types import Message 5 | from filters import IsUser 6 | from .menu import balance 7 | 8 | # test card ==> 1111 1111 1111 1026, 12/22, CVC 000 9 | 10 | # shopId 506751 11 | 12 | # shopArticleId 538350 13 | 14 | 15 | @dp.message_handler(IsUser(), text=balance) 16 | async def process_balance(message: Message, state: FSMContext): 17 | await message.answer('Ваш кошелек пуст! Чтобы его пополнить нужно...') 18 | 19 | -------------------------------------------------------------------------------- /keyboards/__init__.py: -------------------------------------------------------------------------------- 1 | from . import inline 2 | from . import default -------------------------------------------------------------------------------- /keyboards/default/__init__.py: -------------------------------------------------------------------------------- 1 | from . import markups -------------------------------------------------------------------------------- /keyboards/default/markups.py: -------------------------------------------------------------------------------- 1 | from aiogram.types import ReplyKeyboardMarkup 2 | 3 | back_message = '👈 Назад' 4 | confirm_message = '✅ Подтвердить заказ' 5 | all_right_message = '✅ Все верно' 6 | cancel_message = '🚫 Отменить' 7 | 8 | def confirm_markup(): 9 | markup = ReplyKeyboardMarkup(resize_keyboard=True, selective=True) 10 | markup.add(confirm_message) 11 | markup.add(back_message) 12 | 13 | return markup 14 | 15 | def back_markup(): 16 | markup = ReplyKeyboardMarkup(resize_keyboard=True, selective=True) 17 | markup.add(back_message) 18 | 19 | return markup 20 | 21 | def check_markup(): 22 | markup = ReplyKeyboardMarkup(resize_keyboard=True, selective=True) 23 | markup.row(back_message, all_right_message) 24 | 25 | return markup 26 | 27 | def submit_markup(): 28 | markup = ReplyKeyboardMarkup(resize_keyboard=True, selective=True) 29 | markup.row(cancel_message, all_right_message) 30 | 31 | return markup 32 | -------------------------------------------------------------------------------- /keyboards/inline/__init__.py: -------------------------------------------------------------------------------- 1 | from . import products_from_catalog 2 | from . import products_from_cart 3 | from . import categories 4 | -------------------------------------------------------------------------------- /keyboards/inline/categories.py: -------------------------------------------------------------------------------- 1 | from aiogram.types import InlineKeyboardMarkup, InlineKeyboardButton 2 | from aiogram.utils.callback_data import CallbackData 3 | from loader import db 4 | 5 | category_cb = CallbackData('category', 'id', 'action') 6 | 7 | 8 | def categories_markup(): 9 | 10 | global category_cb 11 | 12 | markup = InlineKeyboardMarkup() 13 | for idx, title in db.fetchall('SELECT * FROM categories'): 14 | markup.add(InlineKeyboardButton(title, callback_data=category_cb.new(id=idx, action='view'))) 15 | 16 | return markup 17 | -------------------------------------------------------------------------------- /keyboards/inline/products_from_cart.py: -------------------------------------------------------------------------------- 1 | from aiogram.types import InlineKeyboardMarkup, InlineKeyboardButton 2 | from aiogram.utils.callback_data import CallbackData 3 | 4 | product_cb = CallbackData('product', 'id', 'action') 5 | 6 | def product_markup(idx, count): 7 | 8 | global product_cb 9 | 10 | markup = InlineKeyboardMarkup() 11 | back_btn = InlineKeyboardButton('⬅️', callback_data=product_cb.new(id=idx, action='decrease')) 12 | count_btn = InlineKeyboardButton(count, callback_data=product_cb.new(id=idx, action='count')) 13 | next_btn = InlineKeyboardButton('➡️', callback_data=product_cb.new(id=idx, action='increase')) 14 | markup.row(back_btn, count_btn, next_btn) 15 | 16 | return markup -------------------------------------------------------------------------------- /keyboards/inline/products_from_catalog.py: -------------------------------------------------------------------------------- 1 | from aiogram.types import InlineKeyboardMarkup, InlineKeyboardButton 2 | from aiogram.utils.callback_data import CallbackData 3 | from loader import db 4 | 5 | product_cb = CallbackData('product', 'id', 'action') 6 | 7 | 8 | def product_markup(idx='', price=0): 9 | 10 | global product_cb 11 | 12 | markup = InlineKeyboardMarkup() 13 | markup.add(InlineKeyboardButton(f'Добавить в корзину - {price}₽', callback_data=product_cb.new(id=idx, action='add'))) 14 | 15 | return markup -------------------------------------------------------------------------------- /loader.py: -------------------------------------------------------------------------------- 1 | from aiogram import Bot, Dispatcher, types 2 | from aiogram.contrib.fsm_storage.memory import MemoryStorage 3 | from utils.db.storage import DatabaseManager 4 | 5 | from data import config 6 | 7 | bot = Bot(token=config.BOT_TOKEN, parse_mode=types.ParseMode.HTML) 8 | storage = MemoryStorage() 9 | dp = Dispatcher(bot, storage=storage) 10 | db = DatabaseManager('data/database.db') -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | aiogram==2.9.2 2 | python-dotenv==1.0.1 3 | -------------------------------------------------------------------------------- /states/__init__.py: -------------------------------------------------------------------------------- 1 | from .checkout_state import CheckoutState 2 | from .product_state import ProductState, CategoryState 3 | from .sos_state import SosState, AnswerState -------------------------------------------------------------------------------- /states/checkout_state.py: -------------------------------------------------------------------------------- 1 | from aiogram.dispatcher.filters.state import StatesGroup, State 2 | 3 | class CheckoutState(StatesGroup): 4 | check_cart = State() 5 | name = State() 6 | address = State() 7 | confirm = State() -------------------------------------------------------------------------------- /states/product_state.py: -------------------------------------------------------------------------------- 1 | from aiogram.dispatcher.filters.state import StatesGroup, State 2 | 3 | class ProductState(StatesGroup): 4 | title = State() 5 | body = State() 6 | image = State() 7 | price = State() 8 | confirm = State() 9 | 10 | class CategoryState(StatesGroup): 11 | title = State() -------------------------------------------------------------------------------- /states/sos_state.py: -------------------------------------------------------------------------------- 1 | from aiogram.dispatcher.filters.state import StatesGroup, State 2 | 3 | class SosState(StatesGroup): 4 | question = State() 5 | submit = State() 6 | 7 | class AnswerState(StatesGroup): 8 | answer = State() 9 | submit = State() -------------------------------------------------------------------------------- /utils/__init__.py: -------------------------------------------------------------------------------- 1 | from . import db -------------------------------------------------------------------------------- /utils/db/__init__.py: -------------------------------------------------------------------------------- 1 | from .storage import DatabaseManager -------------------------------------------------------------------------------- /utils/db/storage.py: -------------------------------------------------------------------------------- 1 | 2 | import sqlite3 as lite 3 | 4 | class DatabaseManager(object): 5 | 6 | def __init__(self, path): 7 | self.conn = lite.connect(path) 8 | self.conn.execute('pragma foreign_keys = on') 9 | self.conn.commit() 10 | self.cur = self.conn.cursor() 11 | 12 | def create_tables(self): 13 | self.query('CREATE TABLE IF NOT EXISTS products (idx text, title text, body text, photo blob, price int, tag text)') 14 | self.query('CREATE TABLE IF NOT EXISTS orders (cid int, usr_name text, usr_address text, products text)') 15 | self.query('CREATE TABLE IF NOT EXISTS cart (cid int, idx text, quantity int)') 16 | self.query('CREATE TABLE IF NOT EXISTS categories (idx text, title text)') 17 | self.query('CREATE TABLE IF NOT EXISTS wallet (cid int, balance real)') 18 | self.query('CREATE TABLE IF NOT EXISTS questions (cid int, question text)') 19 | 20 | def query(self, arg, values=None): 21 | if values == None: 22 | self.cur.execute(arg) 23 | else: 24 | self.cur.execute(arg, values) 25 | self.conn.commit() 26 | 27 | def fetchone(self, arg, values=None): 28 | if values == None: 29 | self.cur.execute(arg) 30 | else: 31 | self.cur.execute(arg, values) 32 | return self.cur.fetchone() 33 | 34 | def fetchall(self, arg, values=None): 35 | if values == None: 36 | self.cur.execute(arg) 37 | else: 38 | self.cur.execute(arg, values) 39 | return self.cur.fetchall() 40 | 41 | def __del__(self): 42 | self.conn.close() 43 | 44 | 45 | ''' 46 | 47 | products: idx text, title text, body text, photo blob, price int, tag text 48 | 49 | orders: cid int, usr_name text, usr_address text, products text 50 | 51 | cart: cid int, idx text, quantity int ==> product_idx 52 | 53 | categories: idx text, title text 54 | 55 | wallet: cid int, balance real 56 | 57 | questions: cid int, question text 58 | 59 | ''' 60 | --------------------------------------------------------------------------------