├── core ├── __init__.py ├── apps │ ├── __init__.py │ └── bot │ │ ├── __init__.py │ │ ├── keyboards │ │ ├── __init__.py │ │ ├── admin_kb.py │ │ ├── sign_inup_kb.py │ │ ├── registration_kb.py │ │ ├── default_kb.py │ │ └── catalog_ikb.py │ │ ├── management │ │ ├── __init__.py │ │ └── commands │ │ │ ├── __init__.py │ │ │ └── bot.py │ │ ├── migrations │ │ ├── __init__.py │ │ └── 0001_initial.py │ │ ├── views.py │ │ ├── states │ │ ├── __init__.py │ │ ├── signin_state.py │ │ ├── auth_state.py │ │ └── forgot_password_state.py │ │ ├── urls.py │ │ ├── apps.py │ │ ├── handlers │ │ ├── __init__.py │ │ ├── catalog.py │ │ ├── default.py │ │ └── authorization.py │ │ ├── loader.py │ │ ├── admin.py │ │ ├── models.py │ │ └── templates │ │ └── bot │ │ └── index.html └── config │ ├── __init__.py │ ├── settings │ ├── __init__.py │ ├── local.py │ └── prod.py │ ├── asgi.py │ ├── wsgi.py │ └── urls.py ├── requirements.txt ├── .gitignore ├── dc ├── db.yaml └── bot.yaml ├── .env.example ├── Dockerfile ├── entrypoint.sh ├── manage.py ├── Makefile └── README.md /core/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /core/apps/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /core/config/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /core/apps/bot/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /core/config/settings/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /core/apps/bot/keyboards/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /core/apps/bot/management/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /core/apps/bot/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /core/apps/bot/management/commands/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /core/config/settings/local.py: -------------------------------------------------------------------------------- 1 | from core.config.settings.prod import * 2 | 3 | DEBUG = True 4 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/itzddos/ecommerce-telegram-bot/HEAD/requirements.txt -------------------------------------------------------------------------------- /core/apps/bot/views.py: -------------------------------------------------------------------------------- 1 | from django.shortcuts import render 2 | 3 | 4 | def index(request): 5 | return render(request, 'bot/index.html') 6 | -------------------------------------------------------------------------------- /core/apps/bot/states/__init__.py: -------------------------------------------------------------------------------- 1 | from .auth_state import AuthState 2 | from .signin_state import SignInState 3 | from .forgot_password_state import ForgotPasswordState 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | .vscode/ 3 | 4 | __pycache__/ 5 | *.pyc 6 | *.pyo 7 | *.so 8 | *.swp 9 | *.swo 10 | *.log 11 | 12 | .venv/ 13 | venv/ 14 | 15 | .env 16 | 17 | static/ 18 | media/ 19 | -------------------------------------------------------------------------------- /core/apps/bot/states/signin_state.py: -------------------------------------------------------------------------------- 1 | from aiogram.dispatcher.filters.state import StatesGroup, State 2 | 3 | 4 | class SignInState(StatesGroup): 5 | login = State() 6 | password = State() 7 | -------------------------------------------------------------------------------- /core/apps/bot/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path, include 2 | 3 | from core.apps.bot.views import index 4 | 5 | urlpatterns = [ 6 | path('', index), 7 | path('chaining/', include('smart_selects.urls')) 8 | ] 9 | -------------------------------------------------------------------------------- /core/apps/bot/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class BotFileConfig(AppConfig): 5 | default_auto_field = 'django.db.models.BigAutoField' 6 | name = 'core.apps.bot' 7 | 8 | verbose_name = 'Product' 9 | -------------------------------------------------------------------------------- /core/apps/bot/states/auth_state.py: -------------------------------------------------------------------------------- 1 | from aiogram.dispatcher.filters.state import StatesGroup, State 2 | 3 | 4 | class AuthState(StatesGroup): 5 | user_login = State() 6 | user_password = State() 7 | user_password_2 = State() 8 | -------------------------------------------------------------------------------- /core/apps/bot/states/forgot_password_state.py: -------------------------------------------------------------------------------- 1 | from aiogram.dispatcher.filters.state import StatesGroup, State 2 | 3 | 4 | class ForgotPasswordState(StatesGroup): 5 | user_login = State() 6 | user_password = State() 7 | user_password_2 = State() 8 | -------------------------------------------------------------------------------- /core/apps/bot/handlers/__init__.py: -------------------------------------------------------------------------------- 1 | from core.apps.bot.handlers.authorization import authorization_handlers_register 2 | from core.apps.bot.handlers.catalog import catalog_handlers_register 3 | from core.apps.bot.handlers.default import default_handlers_register 4 | -------------------------------------------------------------------------------- /core/apps/bot/keyboards/admin_kb.py: -------------------------------------------------------------------------------- 1 | from aiogram.types import ReplyKeyboardMarkup, KeyboardButton 2 | 3 | markup = ReplyKeyboardMarkup(resize_keyboard=True) 4 | btn_1 = KeyboardButton("Home 🏠") 5 | btn_2 = KeyboardButton("Help 🔔") 6 | markup.add(btn_1).add(btn_2) 7 | -------------------------------------------------------------------------------- /core/apps/bot/loader.py: -------------------------------------------------------------------------------- 1 | from aiogram import Bot, Dispatcher, types 2 | from aiogram.contrib.fsm_storage.memory import MemoryStorage 3 | from django.conf import settings 4 | 5 | bot = Bot(settings.TOKEN_API, parse_mode=types.ParseMode.HTML) 6 | storage = MemoryStorage() 7 | dp = Dispatcher(bot, storage=storage) 8 | -------------------------------------------------------------------------------- /dc/db.yaml: -------------------------------------------------------------------------------- 1 | services: 2 | postgres: 3 | container_name: postgres 4 | image: postgres:16-alpine 5 | env_file: ../.env 6 | ports: 7 | - "${POSTGRES_PORT}:5432" 8 | volumes: 9 | - postgres_data:/var/lib/postgresql/data 10 | restart: always 11 | 12 | volumes: 13 | postgres_data: 14 | -------------------------------------------------------------------------------- /dc/bot.yaml: -------------------------------------------------------------------------------- 1 | services: 2 | bot: 3 | container_name: bot 4 | build: 5 | context: ../ 6 | dockerfile: Dockerfile 7 | env_file: ../.env 8 | ports: 9 | - "${BOT_PORT}:8000" 10 | volumes: 11 | - ../:/ecommerce-telegram-bot 12 | command: [ "./entrypoint.sh" ] 13 | restart: always 14 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | # Django 2 | SECRET_KEY='YOUR_DJANGO_SECRET_KEY' 3 | BOT_PORT=8000 4 | 5 | # Bot 6 | TOKEN_API=YOUR_BOT_TOKEN 7 | ADMIN_ID=YOUR_BOT_ADMIN_ID 8 | 9 | # Database 10 | POSTGRES_DB=YOUR_POSTGRES_DB 11 | POSTGRES_USER=YOUR_POSTGRES_USER 12 | POSTGRES_PASSWORD=YOUR_POSTGRES_PASSWORD 13 | POSTGRES_HOST=postgres 14 | POSTGRES_PORT=YOUR_POSTGRES_PORT 15 | -------------------------------------------------------------------------------- /core/apps/bot/keyboards/sign_inup_kb.py: -------------------------------------------------------------------------------- 1 | from aiogram.types import ReplyKeyboardMarkup, KeyboardButton 2 | 3 | markup = ReplyKeyboardMarkup(resize_keyboard=True, one_time_keyboard=False) 4 | btn_1 = KeyboardButton('Sign Up ✌️') 5 | btn_2 = KeyboardButton('Sign In 👋') 6 | btn_3 = KeyboardButton('Forgot Password? 🆘') 7 | markup.add(btn_1).insert(btn_2).add(btn_3) 8 | -------------------------------------------------------------------------------- /core/config/asgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | ASGI config for config project. 3 | 4 | It exposes the ASGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/4.1/howto/deployment/asgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.asgi import get_asgi_application 13 | 14 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'core.config.settings.prod') 15 | 16 | application = get_asgi_application() 17 | -------------------------------------------------------------------------------- /core/config/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for config project. 3 | 4 | It exposes the WSGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/4.1/howto/deployment/wsgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.wsgi import get_wsgi_application 13 | 14 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'core.config.settings.prod') 15 | 16 | application = get_wsgi_application() 17 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.9.13-slim 2 | 3 | ENV PYTHONUNBUFFERED=1 4 | ENV PYTHONDONTWRITEBYTECODE=1 5 | 6 | WORKDIR /ecommerce-telegram-bot 7 | 8 | RUN apt-get update && apt-get install -y \ 9 | python3-dev \ 10 | build-essential \ 11 | gcc \ 12 | musl-dev 13 | 14 | RUN python3 -m pip install pip==23.0.1 15 | 16 | ADD requirements.txt /ecommerce-telegram-bot 17 | 18 | RUN python3 -m pip install -r requirements.txt 19 | 20 | EXPOSE 8000 21 | 22 | COPY . /ecommerce-telegram-bot 23 | -------------------------------------------------------------------------------- /core/apps/bot/keyboards/registration_kb.py: -------------------------------------------------------------------------------- 1 | from aiogram.types import ReplyKeyboardMarkup, KeyboardButton 2 | 3 | markup = ReplyKeyboardMarkup(resize_keyboard=True, one_time_keyboard=True) 4 | btn_1 = KeyboardButton('Cancel ❌') 5 | markup.add(btn_1) 6 | 7 | markup_cancel_forgot_password = ReplyKeyboardMarkup(resize_keyboard=True, one_time_keyboard=True) 8 | btn_1 = KeyboardButton('Cancel ❌') 9 | btn_2 = KeyboardButton('Forgot Password? 🆘') 10 | markup_cancel_forgot_password.add(btn_1).add(btn_2) 11 | -------------------------------------------------------------------------------- /core/apps/bot/keyboards/default_kb.py: -------------------------------------------------------------------------------- 1 | from aiogram.types import ReplyKeyboardMarkup, KeyboardButton 2 | 3 | markup = ReplyKeyboardMarkup(resize_keyboard=True, one_time_keyboard=False) 4 | btn_1 = KeyboardButton('Help ⭐️') 5 | btn_2 = KeyboardButton('Description 📌') 6 | btn_3 = KeyboardButton('Catalog 🛒') 7 | btn_4 = KeyboardButton('Admin 👑') 8 | markup.add(btn_1).insert(btn_2).add(btn_3).insert(btn_4) 9 | 10 | only_help_markup = ReplyKeyboardMarkup(resize_keyboard=True, one_time_keyboard=True) 11 | btn_1 = KeyboardButton('Help ⭐️') 12 | only_help_markup.add(btn_1) 13 | -------------------------------------------------------------------------------- /entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! 4 | # Make sure the line endings are set to LF, not CRLF. 5 | # CRLF line endings may cause the error: 'exec ./entrypoint.sh: no such file or directory' 6 | # This can happen if the script was edited or saved in a Windows environment. 7 | # !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! 8 | 9 | python3 manage.py makemigrations 10 | python3 manage.py migrate 11 | python3 manage.py collectstatic --noinput 12 | 13 | gunicorn core.config.wsgi:application --bind 0.0.0.0:8000 & 14 | 15 | python3 manage.py bot 16 | -------------------------------------------------------------------------------- /manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """Django's command-line utility for administrative tasks.""" 3 | import os 4 | import sys 5 | 6 | 7 | def main(): 8 | """Run administrative tasks.""" 9 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'core.config.settings.prod') 10 | try: 11 | from django.core.management import execute_from_command_line 12 | except ImportError as exc: 13 | raise ImportError( 14 | "Couldn't import Django. Are you sure it's installed and " 15 | "available on your PYTHONPATH environment variable? Did you " 16 | "forget to activate a virtual environment?" 17 | ) from exc 18 | execute_from_command_line(sys.argv) 19 | 20 | 21 | if __name__ == '__main__': 22 | main() 23 | -------------------------------------------------------------------------------- /core/config/urls.py: -------------------------------------------------------------------------------- 1 | """config URL Configuration 2 | 3 | The `urlpatterns` list routes URLs to views. For more information please see: 4 | https://docs.djangoproject.com/en/4.1/topics/http/urls/ 5 | Examples: 6 | Function views 7 | 1. Add an import: from my_app import views 8 | 2. Add a URL to urlpatterns: path('', views.home, name='home') 9 | Class-based views 10 | 1. Add an import: from other_app.views import Home 11 | 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home') 12 | Including another URLconf 13 | 1. Import the include() function: from django.urls import include, path 14 | 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) 15 | """ 16 | from django.conf import settings 17 | from django.conf.urls.static import static 18 | from django.contrib import admin 19 | from django.urls import path, include 20 | 21 | urlpatterns = [ 22 | path('admin/', admin.site.urls), 23 | path('', include('core.apps.bot.urls')) 24 | ] 25 | 26 | if settings.DEBUG: 27 | urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) 28 | -------------------------------------------------------------------------------- /core/apps/bot/management/commands/bot.py: -------------------------------------------------------------------------------- 1 | from aiogram import executor, types 2 | from django.core.management import BaseCommand 3 | 4 | from core.apps.bot.handlers import default_handlers_register, catalog_handlers_register, authorization_handlers_register 5 | from core.apps.bot.keyboards import default_kb 6 | from core.apps.bot.loader import dp 7 | 8 | 9 | async def on_startup(_): 10 | print("Bot has been successfully launched!") 11 | 12 | 13 | class Command(BaseCommand): 14 | 15 | def handle(self, *args, **options): 16 | default_handlers_register() 17 | catalog_handlers_register() 18 | authorization_handlers_register() 19 | 20 | @dp.message_handler(commands=None, regexp=None) 21 | async def unknown_text(message: types.Message): 22 | await message.answer("Command not found ☹️\n\n" 23 | "Please, click the button Help ⭐️ to get assistance", 24 | reply_markup=default_kb.only_help_markup, 25 | ) 26 | 27 | executor.start_polling(dp, skip_updates=True, on_startup=on_startup) 28 | -------------------------------------------------------------------------------- /core/apps/bot/keyboards/catalog_ikb.py: -------------------------------------------------------------------------------- 1 | from aiogram.types import InlineKeyboardMarkup, InlineKeyboardButton 2 | from aiogram.utils.callback_data import CallbackData 3 | from asgiref.sync import sync_to_async 4 | 5 | from core.apps.bot.models import Category, SubCategory 6 | 7 | category_cb = CallbackData('category', 'id', 'action') 8 | subcategory_cb = CallbackData('subcategory', 'id', 'action') 9 | 10 | 11 | @sync_to_async 12 | def get_categories(): 13 | categories = Category.objects.all() 14 | markup = InlineKeyboardMarkup(row_width=1) 15 | for category in categories: 16 | markup.add(InlineKeyboardButton( 17 | text=category.name, 18 | callback_data=category_cb.new(id=category.id, action='view_categories'), 19 | )) 20 | return markup 21 | 22 | 23 | @sync_to_async 24 | def get_subcategories(cat_id): 25 | subcategories = SubCategory.objects.filter(subcategory_category_id=cat_id) 26 | subcategory_markup = InlineKeyboardMarkup(row_width=2) 27 | for subcategory in subcategories: 28 | subcategory_markup.add(InlineKeyboardButton( 29 | text=subcategory.name, 30 | callback_data=subcategory_cb.new(id=subcategory.id, action='view_subcategories'), 31 | )) 32 | return subcategory_markup 33 | -------------------------------------------------------------------------------- /core/apps/bot/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | from core.apps.bot.models import Product, Category, TelegramUser, SubCategory 4 | 5 | 6 | @admin.register(Product) 7 | class ProductAdmin(admin.ModelAdmin): 8 | list_display = ['id', 'name', 'price', 'created_at', 'product_category', 'product_subcategory', 'is_published'] 9 | list_display_links = ['id', 'name'] 10 | search_fields = ['id', 'name', 'price', 'product_category'] 11 | 12 | 13 | @admin.register(Category) 14 | class CategoryAdmin(admin.ModelAdmin): 15 | list_display = ['id', 'name', 'created_at'] 16 | list_display_links = ['id', 'name'] 17 | search_fields = ['id', 'name'] 18 | 19 | 20 | @admin.register(SubCategory) 21 | class SubcategoryAdmin(admin.ModelAdmin): 22 | list_display = ['id', 'subcategory_category', 'name', 'created_at'] 23 | list_display_links = ['id', 'name'] 24 | search_fields = ['id', 'subcategory_category', 'name'] 25 | 26 | 27 | @admin.register(TelegramUser) 28 | class TelegramUserAdmin(admin.ModelAdmin): 29 | list_display = ['id', 'user_login', 'registered_at', 'is_registered'] 30 | list_display_links = ['id', 'user_login'] 31 | search_fields = ['id', 'user_login', 'registered_at'] 32 | readonly_fields = ['chat_id', 'user_login', 'user_password', 'is_registered'] 33 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | DC = docker compose 2 | EXEC = docker exec -it 3 | 4 | ENV = --env-file .env 5 | 6 | BOT_FILE = dc/bot.yaml 7 | DB_FILE = dc/db.yaml 8 | 9 | BOT_CONTAINER = bot 10 | DB_CONTAINER = postgres 11 | 12 | # Replace with your own values 13 | POSTGRES_USER = POSTGRES_USER 14 | POSTGRES_DB = POSTGRES_DB 15 | 16 | .PHONY: bot 17 | bot: 18 | $(DC) -f $(BOT_FILE) $(ENV) up --build -d 19 | 20 | .PHONY: bot-down 21 | bot-down: 22 | $(DC) -f $(BOT_FILE) $(ENV) down --remove-orphans 23 | 24 | .PHONY: bot-exec 25 | bot-exec: 26 | $(EXEC) $(BOT_CONTAINER) bash 27 | 28 | .PHONY: bot-logs 29 | bot-logs: 30 | $(DC) -f $(BOT_FILE) $(ENV) logs -f 31 | 32 | .PHONY: create-superuser 33 | create-superuser: 34 | $(EXEC) $(BOT_CONTAINER) bash -c "python manage.py createsuperuser" 35 | 36 | .PHONY: make-migrations 37 | make-migrations: 38 | $(EXEC) $(BOT_CONTAINER) bash -c "python manage.py makemigrations" 39 | 40 | .PHONY: migrate 41 | migrate: 42 | $(EXEC) $(BOT_CONTAINER) bash -c "python manage.py migrate" 43 | 44 | .PHONY: collectstatic 45 | collectstatic: 46 | $(EXEC) $(BOT_CONTAINER) bash -c "python manage.py collectstatic --noinput" 47 | 48 | .PHONY: db 49 | db: 50 | $(DC) -f $(DB_FILE) $(ENV) up --build -d 51 | 52 | .PHONY: db-down 53 | db-down: 54 | $(DC) -f $(DB_FILE) $(ENV) down --remove-orphans 55 | 56 | .PHONY: db-exec 57 | db-exec: 58 | $(EXEC) $(DB_CONTAINER) psql -U $(POSTGRES_USER) -d $(POSTGRES_DB) 59 | 60 | .PHONY: db-logs 61 | db-logs: 62 | $(DC) -f $(DB_FILE) $(ENV) logs -f 63 | 64 | .PHONY: all 65 | all: 66 | $(DC) -f $(DB_FILE) $(ENV) up --build -d 67 | $(DC) -f $(BOT_FILE) $(ENV) up --build -d 68 | 69 | .PHONY: all-down 70 | all-down: 71 | $(DC) -f $(DB_FILE) $(ENV) down --remove-orphans 72 | $(DC) -f $(BOT_FILE) $(ENV) down --remove-orphans 73 | -------------------------------------------------------------------------------- /core/apps/bot/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | from smart_selects.db_fields import ChainedForeignKey 3 | 4 | 5 | class TelegramUser(models.Model): 6 | chat_id = models.BigIntegerField(verbose_name='User ID', unique=True, null=True) 7 | user_login = models.CharField(verbose_name='Login', max_length=255, unique=True) 8 | user_password = models.CharField(verbose_name='Password', max_length=128) 9 | is_registered = models.BooleanField(verbose_name='Is registered', default=False) 10 | registered_at = models.DateTimeField(verbose_name='Registered at', auto_now_add=True) 11 | 12 | def __str__(self): 13 | return self.user_login 14 | 15 | class Meta: 16 | verbose_name = 'Telegram User' 17 | verbose_name_plural = 'Telegram Users' 18 | db_table = 'telegram_users' 19 | ordering = ['-registered_at'] 20 | 21 | 22 | class Category(models.Model): 23 | name = models.CharField(verbose_name='Title', max_length=100) 24 | description = models.TextField(verbose_name='Description', blank=True) 25 | created_at = models.DateTimeField(verbose_name='Created at', auto_now_add=True) 26 | 27 | def __str__(self): 28 | return self.name 29 | 30 | class Meta: 31 | verbose_name = 'Category' 32 | verbose_name_plural = 'Categories' 33 | db_table = 'categories' 34 | ordering = ['-created_at'] 35 | 36 | 37 | class SubCategory(models.Model): 38 | name = models.CharField(verbose_name='Title', max_length=100) 39 | description = models.TextField(verbose_name='Description', blank=True) 40 | created_at = models.DateTimeField(verbose_name='Created at', auto_now_add=True) 41 | subcategory_category = models.ForeignKey( 42 | verbose_name='Category', 43 | to='Category', 44 | on_delete=models.PROTECT, 45 | null=True, 46 | ) 47 | 48 | def __str__(self): 49 | return self.name 50 | 51 | class Meta: 52 | verbose_name = 'SubCategory' 53 | verbose_name_plural = 'SubCategories' 54 | db_table = 'subcategories' 55 | ordering = ['-created_at'] 56 | 57 | 58 | class Product(models.Model): 59 | photo = models.ImageField(verbose_name='Image', upload_to='products/') 60 | name = models.CharField(verbose_name='Title', max_length=100) 61 | description = models.TextField(verbose_name='Description', blank=False) 62 | price = models.PositiveIntegerField(verbose_name='Price') 63 | created_at = models.DateTimeField(verbose_name='Created at', auto_now_add=True) 64 | updated_at = models.DateTimeField(verbose_name='Updated at', auto_now=True) 65 | is_published = models.BooleanField(verbose_name='Is published', default=True) 66 | product_category = models.ForeignKey(verbose_name='Category', to='Category', on_delete=models.PROTECT, null=True) 67 | 68 | product_subcategory = ChainedForeignKey( 69 | to='SubCategory', 70 | chained_field='product_category', 71 | chained_model_field='subcategory_category', 72 | show_all=False, 73 | auto_choose=True, 74 | null=True, 75 | verbose_name='SubCategory', 76 | ) 77 | 78 | def __str__(self): 79 | return self.name 80 | 81 | class Meta: 82 | verbose_name = 'Product' 83 | verbose_name_plural = 'Products' 84 | db_table = 'products' 85 | ordering = ['-created_at'] 86 | -------------------------------------------------------------------------------- /core/apps/bot/templates/bot/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Ecommerce Telegram Bot 7 | 8 | 10 | 82 | 83 | 84 |
85 |

Ecommerce Telegram Bot

86 |

Project Description

87 | 96 |

97 | 98 | Click here to access Django Admin, or add /admin/ to the end of the URL. 99 | 100 |

101 |
102 | 103 | 104 | -------------------------------------------------------------------------------- /core/config/settings/prod.py: -------------------------------------------------------------------------------- 1 | """ 2 | Django settings for config project. 3 | 4 | Generated by 'django-admin startproject' using Django 4.1.7. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/4.1/topics/settings/ 8 | 9 | For the full list of settings and their values, see 10 | https://docs.djangoproject.com/en/4.1/ref/settings/ 11 | """ 12 | import os.path 13 | from pathlib import Path 14 | 15 | from environs import Env 16 | 17 | env = Env() 18 | env.read_env() 19 | 20 | TOKEN_API = env.str('TOKEN_API') 21 | ADMIN_ID = env.int('ADMIN_ID') 22 | SECRET_KEY = env.str('SECRET_KEY') 23 | 24 | # Build paths inside the project like this: BASE_DIR / 'subdir'. 25 | BASE_DIR = Path(__file__).resolve().parent.parent.parent.parent 26 | 27 | # Quick-start development settings - unsuitable for production 28 | # See https://docs.djangoproject.com/en/4.1/howto/deployment/checklist/ 29 | 30 | # SECURITY WARNING: don't run with debug turned on in production! 31 | DEBUG = True 32 | 33 | ALLOWED_HOSTS = ['*'] 34 | 35 | # Application definition 36 | INSTALLED_APPS = [ 37 | 'django.contrib.admin', 38 | 'django.contrib.auth', 39 | 'django.contrib.contenttypes', 40 | 'django.contrib.sessions', 41 | 'django.contrib.messages', 42 | 'django.contrib.staticfiles', 43 | 44 | 'core.apps.bot.apps.BotFileConfig', 45 | 'smart_selects', 46 | ] 47 | 48 | MIDDLEWARE = [ 49 | 'django.middleware.security.SecurityMiddleware', 50 | 'django.contrib.sessions.middleware.SessionMiddleware', 51 | 'django.middleware.common.CommonMiddleware', 52 | 'django.middleware.csrf.CsrfViewMiddleware', 53 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 54 | 'django.contrib.messages.middleware.MessageMiddleware', 55 | 'django.middleware.clickjacking.XFrameOptionsMiddleware', 56 | 57 | 'whitenoise.middleware.WhiteNoiseMiddleware', 58 | ] 59 | 60 | ROOT_URLCONF = 'core.config.urls' 61 | 62 | TEMPLATES = [ 63 | { 64 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 65 | 'DIRS': [BASE_DIR / 'apps' / 'bot' / 'templates'], 66 | 'APP_DIRS': True, 67 | 'OPTIONS': { 68 | 'context_processors': [ 69 | 'django.template.context_processors.debug', 70 | 'django.template.context_processors.request', 71 | 'django.contrib.auth.context_processors.auth', 72 | 'django.contrib.messages.context_processors.messages', 73 | ], 74 | }, 75 | }, 76 | ] 77 | 78 | WSGI_APPLICATION = 'config.wsgi.application' 79 | 80 | # Database 81 | # https://docs.djangoproject.com/en/4.1/ref/settings/#databases 82 | DATABASES = { 83 | 'default': { 84 | 'ENGINE': 'django.db.backends.postgresql', 85 | 'NAME': env.str('POSTGRES_DB'), 86 | 'USER': env.str('POSTGRES_USER'), 87 | 'PASSWORD': env.str('POSTGRES_PASSWORD'), 88 | 'HOST': env.str('POSTGRES_HOST', default='localhost'), 89 | 'PORT': env.str('POSTGRES_PORT', default=5432), 90 | } 91 | } 92 | 93 | # Password validation 94 | # https://docs.djangoproject.com/en/4.1/ref/settings/#auth-password-validators 95 | 96 | AUTH_PASSWORD_VALIDATORS = [ 97 | { 98 | 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', 99 | }, 100 | { 101 | 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', 102 | }, 103 | { 104 | 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', 105 | }, 106 | { 107 | 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', 108 | }, 109 | ] 110 | 111 | # Internationalization 112 | # https://docs.djangoproject.com/en/4.1/topics/i18n/ 113 | 114 | LANGUAGE_CODE = 'en-us' 115 | 116 | TIME_ZONE = 'UTC' 117 | 118 | USE_I18N = True 119 | 120 | USE_TZ = True 121 | 122 | # Static files (CSS, JavaScript, Images) 123 | # https://docs.djangoproject.com/en/4.1/howto/static-files/ 124 | 125 | STATIC_URL = '/static/' 126 | STATIC_ROOT = BASE_DIR / 'static' 127 | 128 | STATICFILES_STORAGE = 'whitenoise.storage.CompressedManifestStaticFilesStorage' 129 | 130 | MEDIA_URL = '/media/' 131 | MEDIA_ROOT = os.path.join(BASE_DIR, 'media/') 132 | 133 | # Default primary key field type 134 | # https://docs.djangoproject.com/en/4.1/ref/settings/#default-auto-field 135 | 136 | DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' 137 | -------------------------------------------------------------------------------- /core/apps/bot/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.2 on 2025-02-05 06:55 2 | 3 | from django.db import migrations, models 4 | import django.db.models.deletion 5 | import smart_selects.db_fields 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | initial = True 11 | 12 | dependencies = [ 13 | ] 14 | 15 | operations = [ 16 | migrations.CreateModel( 17 | name='Category', 18 | fields=[ 19 | ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 20 | ('name', models.CharField(max_length=100, verbose_name='Title')), 21 | ('description', models.TextField(blank=True, verbose_name='Description')), 22 | ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created at')), 23 | ], 24 | options={ 25 | 'verbose_name': 'Category', 26 | 'verbose_name_plural': 'Categories', 27 | 'db_table': 'categories', 28 | 'ordering': ['-created_at'], 29 | }, 30 | ), 31 | migrations.CreateModel( 32 | name='TelegramUser', 33 | fields=[ 34 | ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 35 | ('chat_id', models.BigIntegerField(null=True, unique=True, verbose_name='User ID')), 36 | ('user_login', models.CharField(max_length=255, unique=True, verbose_name='Login')), 37 | ('user_password', models.CharField(max_length=128, verbose_name='Password')), 38 | ('is_registered', models.BooleanField(default=False, verbose_name='Is registered')), 39 | ('registered_at', models.DateTimeField(auto_now_add=True, verbose_name='Registered at')), 40 | ], 41 | options={ 42 | 'verbose_name': 'Telegram User', 43 | 'verbose_name_plural': 'Telegram Users', 44 | 'db_table': 'telegram_users', 45 | 'ordering': ['-registered_at'], 46 | }, 47 | ), 48 | migrations.CreateModel( 49 | name='SubCategory', 50 | fields=[ 51 | ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 52 | ('name', models.CharField(max_length=100, verbose_name='Title')), 53 | ('description', models.TextField(blank=True, verbose_name='Description')), 54 | ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created at')), 55 | ('subcategory_category', models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, to='bot.category', verbose_name='Category')), 56 | ], 57 | options={ 58 | 'verbose_name': 'SubCategory', 59 | 'verbose_name_plural': 'SubCategories', 60 | 'db_table': 'subcategories', 61 | 'ordering': ['-created_at'], 62 | }, 63 | ), 64 | migrations.CreateModel( 65 | name='Product', 66 | fields=[ 67 | ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 68 | ('photo', models.ImageField(upload_to='products/', verbose_name='Image')), 69 | ('name', models.CharField(max_length=100, verbose_name='Title')), 70 | ('description', models.TextField(verbose_name='Description')), 71 | ('price', models.PositiveIntegerField(verbose_name='Price')), 72 | ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created at')), 73 | ('updated_at', models.DateTimeField(auto_now=True, verbose_name='Updated at')), 74 | ('is_published', models.BooleanField(default=True, verbose_name='Is published')), 75 | ('product_category', models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, to='bot.category', verbose_name='Category')), 76 | ('product_subcategory', smart_selects.db_fields.ChainedForeignKey(auto_choose=True, chained_field='product_category', chained_model_field='subcategory_category', null=True, on_delete=django.db.models.deletion.CASCADE, to='bot.subcategory', verbose_name='SubCategory')), 77 | ], 78 | options={ 79 | 'verbose_name': 'Product', 80 | 'verbose_name_plural': 'Products', 81 | 'db_table': 'products', 82 | 'ordering': ['-created_at'], 83 | }, 84 | ), 85 | ] 86 | -------------------------------------------------------------------------------- /core/apps/bot/handlers/catalog.py: -------------------------------------------------------------------------------- 1 | from aiogram import types 2 | from aiogram.dispatcher.filters import Text 3 | from asgiref.sync import sync_to_async 4 | 5 | from core.apps.bot.handlers.authorization import sign_in 6 | from core.apps.bot.keyboards import sign_inup_kb 7 | from core.apps.bot.keyboards.catalog_ikb import get_categories, get_subcategories, category_cb, subcategory_cb 8 | from core.apps.bot.keyboards.default_kb import markup 9 | from core.apps.bot.loader import bot, dp 10 | from core.apps.bot.models import Product, SubCategory, Category 11 | 12 | 13 | async def show_categories(message: types.Message): 14 | if sign_in['current_state']: 15 | if await category_exists(): 16 | await bot.send_message( 17 | chat_id=message.chat.id, text="Please choose a category from the list 📂", 18 | reply_markup=await get_categories(), 19 | ) 20 | else: 21 | await bot.send_message( 22 | chat_id=message.chat.id, text="Unfortunately, the administrator hasn't added any categories yet ☹️", 23 | reply_markup=markup, 24 | ) 25 | else: 26 | await message.answer( 27 | "You are not logged in, please try logging into your profile ‼️", 28 | reply_markup=sign_inup_kb.markup, 29 | ) 30 | 31 | 32 | async def get_products(query): 33 | elem = query.data.split(':') 34 | if await subcategory_products_exists(product_subcategory_id=elem[1]): 35 | await bot.send_message( 36 | chat_id=query.message.chat.id, 37 | text="Here is the list of products available in this subcategor 👇", 38 | ) 39 | async for product in Product.objects.filter(product_subcategory_id=elem[1]): 40 | photo_id = product.photo.open('rb').read() 41 | text = f"Product 🚀: {product.name}\n\n" \ 42 | f"Description 💬: {product.description}\n\n" \ 43 | f"Price 💰: {product.price} USD" 44 | await bot.send_photo(chat_id=query.message.chat.id, photo=photo_id, caption=text) 45 | else: 46 | await bot.send_message( 47 | query.message.chat.id, 48 | text="Unfortunately, there are no products in this subcategory 🙁", 49 | reply_markup=markup, 50 | ) 51 | 52 | 53 | async def show_subcategories(query: types.CallbackQuery): 54 | if sign_in['current_state']: 55 | elem = query.data.split(':') 56 | if await category_subcategory_exists(subcategory_category_id=elem[1]): 57 | await query.answer(text="SubCategories") 58 | await bot.send_message( 59 | chat_id=query.message.chat.id, 60 | text="Please choose a subcategory from the list ☺️", 61 | reply_markup=await get_subcategories(elem[1]), 62 | ) 63 | else: 64 | await bot.send_message( 65 | chat_id=query.message.chat.id, 66 | text="Sorry, there are no products in this category 😔", 67 | reply_markup=markup, 68 | ) 69 | else: 70 | await bot.send_message( 71 | chat_id=query.message.chat.id, 72 | text="You are not logged in, please try logging into your profile ‼️", 73 | reply_markup=sign_inup_kb.markup, 74 | ) 75 | 76 | 77 | async def show_products(query: types.CallbackQuery): 78 | if sign_in['current_state']: 79 | await query.answer("Product Catalog") 80 | await get_products(query) 81 | else: 82 | await bot.send_message( 83 | chat_id=query.message.chat.id, 84 | text="You are not logged in, please try logging into your profile ‼️", 85 | reply_markup=sign_inup_kb.markup, 86 | ) 87 | 88 | 89 | @sync_to_async 90 | def subcategory_products_exists(product_subcategory_id): 91 | return Product.objects.filter(product_subcategory=product_subcategory_id).exists() 92 | 93 | 94 | @sync_to_async 95 | def category_subcategory_exists(subcategory_category_id): 96 | return SubCategory.objects.filter(subcategory_category_id=subcategory_category_id).exists() 97 | 98 | 99 | @sync_to_async 100 | def category_exists(): 101 | return Category.objects.all().exists() 102 | 103 | 104 | def catalog_handlers_register(): 105 | dp.register_message_handler(show_categories, Text(equals='Catalog 🛒')) 106 | dp.register_callback_query_handler(show_subcategories, category_cb.filter(action='view_categories')) 107 | dp.register_callback_query_handler(show_products, subcategory_cb.filter(action='view_subcategories')) 108 | -------------------------------------------------------------------------------- /core/apps/bot/handlers/default.py: -------------------------------------------------------------------------------- 1 | from aiogram import types 2 | from aiogram.dispatcher.filters import Text 3 | from django.conf import settings 4 | from random import randrange 5 | 6 | from core.apps.bot.handlers.authorization import sign_in 7 | from core.apps.bot.keyboards import admin_kb, default_kb 8 | from core.apps.bot.keyboards import sign_inup_kb 9 | from core.apps.bot.loader import bot, dp 10 | from core.apps.bot.models import TelegramUser 11 | 12 | HELP_TEXT = """ 13 | Hello 👋, I’m a bot for selling various products! We have the following commands: 14 | 15 | Help ⭐️ - help with bot commands 16 | Description 📌 -address, contact details, working hours 17 | Catalog 🛒 - list of products you can buy 18 | Admin 👑 - admin menu 19 | 20 | But before starting, you need to register or log in to your profile. 21 | Click on the Sign Up ✌️ or Sign In 👋 command. 22 | If you don't do this, some commands will be unavailable 🔴 23 | 24 | We’re glad you’re using this bot ❤️ 25 | """ 26 | 27 | 28 | async def cmd_start(message: types.Message): 29 | try: 30 | await bot.send_message( 31 | chat_id=message.chat.id, 32 | text="Hello ✋, I’m a bot for selling various products!\n\n" \ 33 | "You can buy anything you want here. To see the list of " \ 34 | "products I have, just click on the 'Catalog 🛒' command below.\n\n" \ 35 | "But first, you need to register, " \ 36 | "otherwise, other commands will be unavailable!\n\n" \ 37 | "Click on the Sign Up ✌️ or Sign In 👋 command.", 38 | reply_markup=sign_inup_kb.markup, 39 | ) 40 | except: 41 | await message.reply( 42 | text="To be able to communicate with the bot, " 43 | "you can send me a direct message: " 44 | "https://t.me/yourbot", 45 | ) 46 | 47 | 48 | async def cmd_help(message: types.Message): 49 | await bot.send_message(chat_id=message.chat.id, text=HELP_TEXT, reply_markup=default_kb.markup) 50 | 51 | 52 | async def cmd_description(message: types.Message): 53 | await bot.send_message( 54 | chat_id=message.chat.id, 55 | text="Hello ✋, we are a company that sells various products! " 56 | "We’re very glad you’re using our service ❤️. We work from Monday to " 57 | "Friday.\n9:00 AM - 9:00 PM", 58 | ) 59 | await bot.send_location( 60 | chat_id=message.chat.id, 61 | latitude=randrange(1, 100), 62 | longitude=randrange(1, 100), 63 | ) 64 | 65 | 66 | async def send_all(message: types.Message): 67 | if sign_in['current_state']: 68 | if message.chat.id == settings.ADMIN_ID: 69 | await message.answer(f"Message: {message.text[message.text.find(' '):]} is being sent to all users!") 70 | async for user in TelegramUser.objects.filter(is_registered=True): 71 | await bot.send_message(chat_id=user.chat_id, text=message.text[message.text.find(' '):]) 72 | await message.answer("All sent successfully!") 73 | else: 74 | await message.answer("You are not an administrator, and you cannot send a broadcast!") 75 | else: 76 | await message.answer( 77 | "You are not logged in, please try logging into your profile ‼️", 78 | reply_markup=sign_inup_kb.markup, 79 | ) 80 | 81 | 82 | async def cmd_admin(message: types.Message): 83 | if sign_in['current_state']: 84 | if message.chat.id == settings.ADMIN_ID: 85 | await message.answer( 86 | "You have entered the admin menu 🤴\n\n" 87 | "Below are the commands available to you 💭", 88 | reply_markup=admin_kb.markup, 89 | ) 90 | else: 91 | await message.answer("You are not an administrator, and you cannot send a broadcast!") 92 | else: 93 | await message.answer( 94 | "You are not logged in, please try logging into your profile ‼️", 95 | reply_markup=sign_inup_kb.markup, 96 | ) 97 | 98 | 99 | async def cmd_home(message: types.Message): 100 | if sign_in['current_state']: 101 | if message.chat.id == settings.ADMIN_ID: 102 | await message.answer("You have entered the admin menu 🤴", reply_markup=default_kb.markup) 103 | else: 104 | await message.answer("You are not an administrator, and you cannot send a broadcast!") 105 | else: 106 | await message.answer( 107 | "You are not logged in, please try logging into your profile ‼️", 108 | reply_markup=sign_inup_kb.markup, 109 | ) 110 | 111 | 112 | HELP_ADMIN_TEXT = ''' 113 | Hello Administrator 🙋\n\n 114 | Currently, you have the following commands: 115 | - Broadcast: - with this command, you can send a message to all users of this bot. 116 | Example usage: Broadcast: 'BROADCAST TEXT' 117 | ''' 118 | 119 | 120 | async def cmd_help_admin(message: types.Message): 121 | if sign_in['current_state']: 122 | if message.chat.id == settings.ADMIN_ID: 123 | await message.answer(text=HELP_ADMIN_TEXT, reply_markup=admin_kb.markup) 124 | else: 125 | await message.answer("You are not an administrator, and you cannot send a broadcast!") 126 | else: 127 | await message.answer( 128 | "You are not logged in, please try logging into your profile ‼️", 129 | reply_markup=sign_inup_kb.markup, 130 | ) 131 | 132 | 133 | def default_handlers_register(): 134 | dp.register_message_handler(cmd_start, commands='start') 135 | dp.register_message_handler(cmd_help, Text(equals='Help ⭐️')) 136 | dp.register_message_handler(cmd_description, Text(equals='Description 📌')) 137 | dp.register_message_handler(send_all, Text(contains='Broadcast:')) 138 | dp.register_message_handler(cmd_admin, Text(equals='Admin 👑')) 139 | dp.register_message_handler(cmd_home, Text(equals='Home 🏠')) 140 | dp.register_message_handler(cmd_help_admin, Text(equals='Help 🔔')) 141 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## About the project: 2 | 3 | The Telegram bot store is written in Python using frameworks such as Django and Aiogram. It features a Django admin 4 | panel that allows creating/editing/deleting categories, subcategories, products, and users. The bot itself includes a 5 | registration system, login functionality, and a password reset option. User passwords are hashed and cannot be modified. 6 | The administrator will not be able to change a user's username or password; only the user can reset and change their 7 | password to a new one. After logging in, the user will have access to commands such as help, description, and catalog. 8 | When the catalog command is selected, an inline keyboard with product categories will appear, and after choosing a 9 | category, another inline keyboard with subcategories will show up. Then, the user will see the products. There are also 10 | commands for the administrator, such as broadcasting messages to users of the Telegram bot. Additionally, handlers for 11 | unknown or unclear commands and messages have been created for the bot. PostgreSQL is used as the database. 12 | ___________ 13 | 14 | ## Installation instructions 15 | 16 | ### 1. Clone the repository 17 | 18 | Copy the repository: `git clone https://github.com/ddosmukhambetov/ecommerce-telegram-bot` 19 | 20 | ### 2. Update keys, tokens and other settings 21 | 22 | Rename the file: `.env.example` to `.env` 23 | Modify the variables in the `.env` file 24 | 25 | ### 3. Run the project 26 | 27 | To start the project, you can use the following commands from the Makefile: 28 | 29 | - `make all` - Starts both the database and the application containers. It builds and runs them in detached mode (-d). 30 | - `make create-superuser` - Creates a superuser for the Django admin panel by running python manage.py createsuperuser 31 | - `make bot-logs` - Shows the logs for the application container (bot) in real-time. 32 | 33 | ### 4. Available commands 34 | 35 | Here is a list of the available commands from the Makefile. Each command can be run manually or through make: 36 | 37 | - `make all` - Starts both the database and the application containers. It builds and runs them in detached mode (-d). 38 | - `make all-down` - Stops and removes both the database and the application containers. 39 | - `make bot` - Builds and starts the application container (bot) with Docker Compose. 40 | - `make bot-down` - Stops and removes the application container (bot), including orphaned containers. 41 | - `make bot-exec` - Accesses the application container and opens a bash shell inside it. 42 | - `make bot-logs` - Shows the logs for the application container (bot) in real-time. 43 | - `make create-superuser` - Creates a superuser for the Django admin panel by running python manage.py createsuperuser 44 | inside the application container. 45 | - `make make-migrations` - Creates migrations for the Django application by running python manage.py makemigrations 46 | inside the application container. 47 | - `make migrate` - Applies the migrations to the database by running python manage.py migrate inside the application 48 | container. 49 | - `make collectstatic` - Collects static files for the Django application by running python manage.py collectstatic 50 | --noinput inside the application container. 51 | - `make db` - Builds and starts the database container (postgres) with Docker Compose. 52 | - `make db-down` - Stops and removes the database container (postgres), including orphaned containers. 53 | - `make db-exec` - Accesses the database container and opens a bash shell inside it. 54 | - `make db-logs` - Shows the logs for the database container (postgres) in real-time. 55 | 56 | ## Telegram Bot Functionality 57 | 58 | - The bot has commands such as Sign Up, Sign In, Forgot Password, Help, Description, Catalog, Admin Menu, etc. 59 | Below is an example of how the bot works ⬇️ 60 | 61 | ### 1. Authorization Commands (Sign Up, Sign In, Forgot Password) 62 | 63 |

authentification

64 | 65 | This section implements a registration and login system. It also includes a "Forgot Password" function. When creating a 66 | password, it is hashed. The user can only create one profile, as their user ID will be assigned to the profile during 67 | registration. The "Sign Up" command first asks for a username, then checks if that username is already taken by 68 | another user. If it is, the bot will ask for a new, unique username. If the username is available, the bot proceeds to 69 | the password creation step. The password must contain at least one digit and only consist of Latin characters. If the 70 | user creates an incorrect password, they will be informed about the required password format. Once the password is 71 | successfully created, it is hashed, and the user is saved in the database, appearing in the Django admin panel. The 72 | administrator does not have the ability to edit user data. 73 | 74 | ### 2. Catalog Command (Categories, Subcategories, Products) 75 | 76 |

view products

77 | 78 | The "Catalog" command is responsible for displaying categories, subcategories, and products. This command is only 79 | accessible once the user has logged in (authenticated). The image shows how this command works. There may be cases where 80 | a category or subcategory has no products; in such cases, the bot will inform the user that there are no products in 81 | that category/subcategory. Categories, subcategories, and products are sorted in the order they were added. These 82 | objects can be added, edited, and deleted in the Django admin panel. Additionally, the category model can be easily 83 | modified and made recursive, allowing for the creation of multiple subcategories under each category. This makes it 84 | flexible and scalable for organizing products in a hierarchical structure. 85 | 86 | ### 3. Default Commands (Help, Description, Admin -> Broadcast) 87 | 88 |

default commands

89 | 90 | The "Default" commands section includes commands such as "Help," which provides assistance regarding the bot. There is 91 | also the "Description" command, which gives an overview of the Telegram store/bot. Additionally, there is an interesting 92 | command called "Admin." To use this command, the user must be on the list of Telegram administrators. Once the "Admin" 93 | button is pressed, the user is redirected to the admin menu. Currently, this menu contains one command: "Broadcast," 94 | along with buttons for "Home" and "Help." The "Help" button provides instructions for the administrator, including 95 | available commands and their descriptions. The "Home" button simply returns the user to the main menu. Thanks to the " 96 | Broadcast" command, the administrator can send messages to all registered users of the Telegram bot. 97 | ___________ 98 | 99 | ## Django Admin Panel: 100 | 101 | - The project uses Django to handle models, the admin panel, relationships between models, and more. 102 | 103 | ### 1. Simple Home Page 104 | 105 |

simple main page

106 | 107 | **The simplest home page (HTML + Bootstrap).** with a brief description of the project. 108 | 109 | ___________ 110 | 111 | ### 2. Admin Panel: 112 | 113 |

admin panel

114 | 115 | ___________ 116 | 117 | ### 3. Products in the Admin Panel: 118 | 119 |

creating_product

120 | 121 | The product accepts a photo, title, description, price, whether it is published, as well as category and subcategory. 122 | The subcategory is linked to the category. All created products are displayed in the Django admin panel. 123 | 124 | ___________ 125 | 126 | ### 4. Categories in the Admin Panel: 127 | 128 |

creating_category

129 | 130 | The category accepts a name and description. All created categories are displayed in the Django admin panel. 131 | 132 | ___________ 133 | 134 | ### 5. SubCategories in the Admin Panel: 135 | 136 |

creating_subcategory

137 | 138 | The subcategory accepts the subcategory name, description, and also the category. All created subcategories are 139 | displayed in the Django admin panel. 140 | 141 | ___________ 142 | 143 | ### 6. Telegram Bot Users in the Admin Panel: 144 | 145 |

user

146 | 147 | The user accepts the user ID, login, password, and whether they are registered. Users are created within the Telegram 148 | bot, and their data, such as the user ID and registration status, are automatically obtained. All users registered in 149 | the Telegram bot are displayed in the Django admin panel. The administrator does not have the ability to edit user data. 150 | User passwords are hashed. 151 | 152 | ___________ 153 | 154 |

155 | Python Version 156 | Aiogram Version 157 | Django Version 158 |

159 | 160 | ___________ -------------------------------------------------------------------------------- /core/apps/bot/handlers/authorization.py: -------------------------------------------------------------------------------- 1 | import re 2 | 3 | from aiogram import types 4 | from aiogram.dispatcher import FSMContext 5 | from aiogram.dispatcher.filters import Text 6 | from asgiref.sync import sync_to_async 7 | from django.contrib.auth.hashers import make_password, check_password 8 | 9 | from core.apps.bot.keyboards import default_kb 10 | from core.apps.bot.keyboards import sign_inup_kb 11 | from core.apps.bot.keyboards.registration_kb import markup, markup_cancel_forgot_password 12 | from core.apps.bot.loader import dp 13 | from core.apps.bot.models import TelegramUser 14 | from core.apps.bot.states import AuthState, SignInState, ForgotPasswordState 15 | 16 | new_user = {} 17 | sign_in = {'current_state': False} 18 | update_data = {} 19 | 20 | REGISTRATION_TEXT = """ 21 | To register, first write your username! 22 | 23 | What should the username consist of? 24 | - The username should only contain Latin letters! 25 | - The username must be longer than 3 characters (letters and numbers) 26 | - The username must be unique and non-repetitive 27 | 28 | Before submitting your username, double-check it! 29 | """ 30 | 31 | 32 | async def command_cancel(message: types.Message, state: FSMContext): 33 | current_state = await state.get_state() 34 | if current_state is None: 35 | return 36 | await state.finish() 37 | await message.answer(text="The operation was successfully canceled 🙅‍", reply_markup=sign_inup_kb.markup) 38 | 39 | 40 | async def process_registration(message: types.Message): 41 | await message.answer(REGISTRATION_TEXT, reply_markup=markup) 42 | await AuthState.user_login.set() 43 | 44 | 45 | async def process_login(message: types.Message, state: FSMContext): 46 | login = message.text 47 | if not await check_users_chat_id(chat_id=message.chat.id): 48 | if not await check_user(login=login): 49 | if re.match('^[A-Za-z]+$', login) and len(login) > 3: 50 | async with state.proxy() as data: 51 | data['login'] = login 52 | new_user['user_login'] = data['login'] 53 | await message.answer("Now, please enter your password ✍️", reply_markup=markup) 54 | await AuthState.user_password.set() 55 | else: 56 | await message.answer( 57 | "The username should only consist of Latin letters and must be more than 3 characters 🔡\n\n" 58 | "Please try again ↩️!", 59 | reply_markup=markup, 60 | ) 61 | await AuthState.user_login.set() 62 | else: 63 | await message.answer( 64 | "A user with this username already exists, please try again ↩️", 65 | reply_markup=markup, 66 | ) 67 | await AuthState.user_login.set() 68 | else: 69 | await message.answer( 70 | "A user with the same ID as yours already exists, please log into your account 🫡", 71 | reply_markup=sign_inup_kb.markup, 72 | ) 73 | 74 | 75 | async def process_password(message: types.Message, state: FSMContext): 76 | if len(message.text) > 5 and re.match('^[a-zA-Z0-9]+$', message.text) and \ 77 | any(digit.isdigit() for digit in message.text): 78 | async with state.proxy() as data: 79 | data['password'] = message.text 80 | await message.answer("Please enter the password again 🔄", reply_markup=markup) 81 | await AuthState.user_password_2.set() 82 | else: 83 | await message.answer( 84 | "The password must only consist of Latin letters " 85 | "and contain at least one digit\n\n" 86 | "Please try again 🔄", 87 | reply_markup=markup, 88 | ) 89 | await AuthState.user_password.set() 90 | 91 | 92 | async def process_password_2(message: types.Message, state: FSMContext): 93 | async with state.proxy() as data: 94 | data['password_2'] = message.text 95 | new_user['user_password'] = data['password_2'] 96 | if data['password'] == data['password_2']: 97 | new_user['chat_id'] = message.chat.id 98 | await save_user() 99 | await state.finish() 100 | await message.answer( 101 | "Registration was successful ✅\n\n" 102 | "Now, please log into your profile 💝", 103 | reply_markup=sign_inup_kb.markup, 104 | ) 105 | else: 106 | await message.answer( 107 | "You entered the password incorrectly ❌\n\n" 108 | "Please try again 🔄", 109 | reply_markup=markup, 110 | ) 111 | await AuthState.user_password.set() 112 | 113 | 114 | async def command_sign_in(message: types.Message): 115 | await message.answer("Please enter your username ✨", reply_markup=markup) 116 | await SignInState.login.set() 117 | 118 | 119 | async def process_sign_in(message: types.Message, state: FSMContext): 120 | if await check_user(message.text): 121 | async with state.proxy() as sign_in_data: 122 | sign_in_data['login'] = message.text 123 | sign_in['login'] = sign_in_data['login'] 124 | await message.answer("Now, you need to enter your password 🔐", reply_markup=markup_cancel_forgot_password) 125 | await SignInState.password.set() 126 | else: 127 | await message.answer("This username does not exist, please try again ❌", reply_markup=markup) 128 | await SignInState.login.set() 129 | 130 | 131 | async def process_pass(message: types.Message, state: FSMContext): 132 | async with state.proxy() as sign_in_data: 133 | sign_in_data['password'] = message.text 134 | sign_in['password'] = sign_in_data['password'] 135 | sign_in['current_state'] = True 136 | if await get_password(username=sign_in['login'], password=sign_in['password']): 137 | await message.answer("Login was successful ⭐️", reply_markup=default_kb.markup) 138 | await state.finish() 139 | else: 140 | await message.answer( 141 | "The password is incorrect, please try again 🔄", 142 | reply_markup=markup_cancel_forgot_password, 143 | ) 144 | await SignInState.password.set() 145 | 146 | 147 | async def forgot_password(message: types.Message): 148 | await message.answer("To change your password, first enter your username 🫡", reply_markup=markup) 149 | await ForgotPasswordState.user_login.set() 150 | 151 | 152 | async def process_forgot_password_login(message: types.Message, state: FSMContext): 153 | if await check_login_chat_id(login=message.text, chat_id=message.chat.id): 154 | await message.answer( 155 | "The username was successfully found, " 156 | "and the user ID matches the username 🌟\n\n" 157 | "Now, you can change your password ✅\n\n" 158 | "Please enter your new password ✍️", 159 | reply_markup=markup, 160 | ) 161 | update_data['user_login'] = message.text 162 | await ForgotPasswordState.user_password.set() 163 | else: 164 | await message.answer( 165 | "You did not pass the check ❌\n\n" 166 | "There could be two reasons for this:\n" 167 | "1. This username does not exist\n" 168 | "2. Your user ID does not match the username you provided\n\n" 169 | "You can try again 🔄", 170 | reply_markup=sign_inup_kb.markup, 171 | ) 172 | await state.finish() 173 | 174 | 175 | async def process_forgot_password_password(message: types.Message, state: FSMContext): 176 | if len(message.text) > 5 and re.match('^[a-zA-Z0-9]+$', message.text) and \ 177 | any(digit.isdigit() for digit in message.text): 178 | async with state.proxy() as forgot_password_data: 179 | forgot_password_data['user_password'] = message.text 180 | update_data['user_password'] = forgot_password_data['user_password'] 181 | await message.answer("Please enter the password again 🔄", reply_markup=markup) 182 | await ForgotPasswordState.user_password_2.set() 183 | else: 184 | await message.answer( 185 | "The password must only consist of Latin letters " 186 | "and contain at least one digit\n\n" 187 | "Please try again 🔄", 188 | reply_markup=markup, 189 | ) 190 | await ForgotPasswordState.user_password.set() 191 | 192 | 193 | async def process_forgot_password_password_2(message: types.Message, state: FSMContext): 194 | async with state.proxy() as forgot_password_data: 195 | forgot_password_data['user_password_2'] = message.text 196 | update_data['user_password'] = forgot_password_data['user_password_2'] 197 | if forgot_password_data['user_password'] == forgot_password_data['user_password_2']: 198 | await update_user_password(login=update_data['user_login'], password=update_data['user_password']) 199 | await state.finish() 200 | await message.answer( 201 | "Password change was successful ✅\n\n" 202 | "Now, please log into your profile 💝", 203 | reply_markup=sign_inup_kb.markup, 204 | ) 205 | else: 206 | await message.answer( 207 | "You entered the password incorrectly ❌\n\n" 208 | "Please try again 🔄", 209 | reply_markup=markup, 210 | ) 211 | await ForgotPasswordState.user_password.set() 212 | 213 | 214 | @sync_to_async 215 | def save_user(): 216 | user = TelegramUser.objects.create( 217 | user_login=new_user['user_login'], 218 | user_password=make_password(new_user['user_password']), 219 | is_registered=True, 220 | chat_id=new_user['chat_id'], 221 | ) 222 | return user 223 | 224 | 225 | @sync_to_async 226 | def update_user_password(login, password): 227 | user = TelegramUser.objects.filter(user_login=login).update(user_password=make_password(password)) 228 | return user 229 | 230 | 231 | @sync_to_async 232 | def get_password(username, password): 233 | user = TelegramUser.objects.get(user_login=username) 234 | if check_password(password, user.user_password): 235 | return True 236 | else: 237 | return False 238 | 239 | 240 | @sync_to_async 241 | def check_user(login): 242 | return TelegramUser.objects.filter(user_login=login).exists() 243 | 244 | 245 | @sync_to_async 246 | def check_login_chat_id(login, chat_id): 247 | return TelegramUser.objects.filter(user_login=login, chat_id=chat_id).exists() 248 | 249 | 250 | @sync_to_async 251 | def check_users_chat_id(chat_id): 252 | return TelegramUser.objects.filter(chat_id=chat_id).exists() 253 | 254 | 255 | def authorization_handlers_register(): 256 | dp.register_message_handler(command_cancel, Text(equals='Cancel ❌', ignore_case=True), state='*') 257 | dp.register_message_handler(process_registration, Text(equals='Sign Up ✌️'), state='*') 258 | dp.register_message_handler(process_login, state=AuthState.user_login) 259 | dp.register_message_handler(process_password, state=AuthState.user_password) 260 | dp.register_message_handler(process_password_2, state=AuthState.user_password_2) 261 | dp.register_message_handler(forgot_password, Text(equals='Forgot Password? 🆘'), state='*') 262 | dp.register_message_handler(process_forgot_password_login, state=ForgotPasswordState.user_login) 263 | dp.register_message_handler(process_forgot_password_password, state=ForgotPasswordState.user_password) 264 | dp.register_message_handler(process_forgot_password_password_2, state=ForgotPasswordState.user_password_2) 265 | dp.register_message_handler(command_sign_in, Text(equals='Sign In 👋')) 266 | dp.register_message_handler(process_sign_in, state=SignInState.login) 267 | dp.register_message_handler(process_pass, state=SignInState.password) 268 | --------------------------------------------------------------------------------