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 |
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 |
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 |
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 |
106 |
107 | **The simplest home page (HTML + Bootstrap).** with a brief description of the project.
108 |
109 | ___________
110 |
111 | ### 2. Admin Panel:
112 |
113 |
114 |
115 | ___________
116 |
117 | ### 3. Products in the Admin Panel:
118 |
119 |
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 |
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 |
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 |
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 |
156 |
157 |
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 |
--------------------------------------------------------------------------------