Zlovoblachko commited on
Commit
339f372
·
1 Parent(s): cfe3b6e

Add application file

Browse files
.dockerignore ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ __pycache__
2
+ *.pyc
3
+ .env
4
+ .git
5
+ .gitignore
6
+ README.md
.gitignore ADDED
@@ -0,0 +1,176 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ### Python ###
2
+ # Byte-compiled / optimized / DLL files
3
+ __pycache__/
4
+ *.py[cod]
5
+ *$py.class
6
+
7
+ # C extensions
8
+ *.so
9
+
10
+ # Distribution / packaging
11
+ .Python
12
+ build/
13
+ develop-eggs/
14
+ dist/
15
+ downloads/
16
+ eggs/
17
+ .eggs/
18
+ lib/
19
+ lib64/
20
+ parts/
21
+ sdist/
22
+ var/
23
+ wheels/
24
+ share/python-wheels/
25
+ *.egg-info/
26
+ .installed.cfg
27
+ *.egg
28
+ MANIFEST
29
+
30
+ # PyInstaller
31
+ # Usually these files are written by a python script from a template
32
+ # before PyInstaller builds the exe, so as to inject date/other infos into it.
33
+ *.manifest
34
+ *.spec
35
+
36
+ # Installer logs
37
+ pip-log.txt
38
+ pip-delete-this-directory.txt
39
+
40
+ # Unit test / coverage reports
41
+ htmlcov/
42
+ .tox/
43
+ .nox/
44
+ .coverage
45
+ .coverage.*
46
+ .cache
47
+ nosetests.xml
48
+ coverage.xml
49
+ *.cover
50
+ *.py,cover
51
+ .hypothesis/
52
+ .pytest_cache/
53
+ cover/
54
+
55
+ # Translations
56
+ *.mo
57
+ *.pot
58
+
59
+ # Django stuff:
60
+ *.log
61
+ local_settings.py
62
+ db.sqlite3
63
+ db.sqlite3-journal
64
+
65
+ # Flask stuff:
66
+ instance/
67
+ .webassets-cache
68
+
69
+ # Scrapy stuff:
70
+ .scrapy
71
+
72
+ # Sphinx documentation
73
+ docs/_build/
74
+
75
+ # PyBuilder
76
+ .pybuilder/
77
+ target/
78
+
79
+ # Jupyter Notebook
80
+ .ipynb_checkpoints
81
+
82
+ # IPython
83
+ profile_default/
84
+ ipython_config.py
85
+
86
+ # pyenv
87
+ # For a library or package, you might want to ignore these files since the code is
88
+ # intended to run in multiple environments; otherwise, check them in:
89
+ # .python-version
90
+
91
+ # pipenv
92
+ # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
93
+ # However, in case of collaboration, if having platform-specific dependencies or dependencies
94
+ # having no cross-platform support, pipenv may install dependencies that don't work, or not
95
+ # install all needed dependencies.
96
+ #Pipfile.lock
97
+
98
+ # poetry
99
+ # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
100
+ # This is especially recommended for binary packages to ensure reproducibility, and is more
101
+ # commonly ignored for libraries.
102
+ # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
103
+ #poetry.lock
104
+
105
+ # pdm
106
+ # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
107
+ #pdm.lock
108
+ # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
109
+ # in version control.
110
+ # https://pdm.fming.dev/#use-with-ide
111
+ .pdm.toml
112
+
113
+ # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
114
+ __pypackages__/
115
+
116
+ # Celery stuff
117
+ celerybeat-schedule
118
+ celerybeat.pid
119
+
120
+ # SageMath parsed files
121
+ *.sage.py
122
+
123
+ # Environments
124
+ .env
125
+ .venv
126
+ env/
127
+ venv/
128
+ ENV/
129
+ env.bak/
130
+ venv.bak/
131
+
132
+ # Spyder project settings
133
+ .spyderproject
134
+ .spyproject
135
+
136
+ # Rope project settings
137
+ .ropeproject
138
+
139
+ # mkdocs documentation
140
+ /site
141
+
142
+ # mypy
143
+ .mypy_cache/
144
+ .dmypy.json
145
+ dmypy.json
146
+
147
+ # Pyre type checker
148
+ .pyre/
149
+
150
+ # pytype static type analyzer
151
+ .pytype/
152
+
153
+ # Cython debug symbols
154
+ cython_debug/
155
+
156
+ # PyCharm
157
+ # JetBrains specific template is maintained in a separate JetBrains.gitignore that can
158
+ # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
159
+ # and can be added to the global gitignore or merged into this file. For a more nuclear
160
+ # option (not recommended) you can uncomment the following to ignore the entire idea folder.
161
+ #.idea/
162
+
163
+ ### Python Patch ###
164
+ # Poetry local configuration file - https://python-poetry.org/docs/configuration/#local-configuration
165
+ poetry.toml
166
+
167
+ # ruff
168
+ .ruff_cache/
169
+
170
+ # LSP config files
171
+ pyrightconfig.json
172
+ run.py
173
+ # End of https://www.toptal.com/developers/gitignore/api/python
174
+ .env
175
+ *.sqlite3
176
+ __pycache__/
Dockerfile ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.9-slim
2
+
3
+ WORKDIR /app
4
+
5
+ COPY requirements.txt .
6
+ RUN pip install --no-cache-dir -r requirements.txt
7
+
8
+ COPY . .
9
+
10
+ CMD ["python", "main.py"]
11
+
12
+ EXPOSE 7860
app/__init__.py ADDED
File without changes
app/config/config.py ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ from typing import List
3
+ from dotenv import load_dotenv
4
+
5
+ load_dotenv()
6
+
7
+ BOT_TOKEN = os.getenv('BOT_TOKEN')
8
+ DATABASE_URL = os.getenv('DATABASE_URL')
9
+ ADMIN_ID = [int(id) for id in os.getenv('ADMIN_IDS', '').split(',')]
10
+
11
+ if not BOT_TOKEN:
12
+ raise ValueError("BOT_TOKEN not found in environment")
app/database/__init__.py ADDED
File without changes
app/database/base.py ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from contextlib import asynccontextmanager
2
+ from .models import async_session
3
+
4
+ @asynccontextmanager
5
+ async def get_session():
6
+ """Provide a transactional scope around a series of operations."""
7
+ session = async_session()
8
+ try:
9
+ yield session
10
+ await session.commit()
11
+ except Exception:
12
+ await session.rollback()
13
+ raise
14
+ finally:
15
+ await session.close()
app/database/models.py ADDED
@@ -0,0 +1,142 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from datetime import datetime
2
+ from typing import Optional
3
+ from sqlalchemy import BigInteger, String, ForeignKey, Integer, DateTime, Boolean
4
+ from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, relationship
5
+ from sqlalchemy.ext.asyncio import AsyncAttrs, async_sessionmaker, create_async_engine
6
+ from app.config.config import DATABASE_URL
7
+
8
+ engine = create_async_engine(DATABASE_URL) # подключение и создание БД
9
+ async_session = async_sessionmaker(engine, expire_on_commit=False)
10
+
11
+
12
+ class Base(AsyncAttrs, DeclarativeBase):
13
+ """Base class for all models"""
14
+ pass
15
+
16
+
17
+ class User(Base):
18
+ """User model for storing telegram user data"""
19
+ __tablename__ = 'users'
20
+
21
+ id: Mapped[int] = mapped_column(primary_key=True)
22
+ tg_id: Mapped[int] = mapped_column(BigInteger, unique=True, nullable=False)
23
+ name: Mapped[str] = mapped_column(String(100), nullable=True)
24
+ login: Mapped[str] = mapped_column(String(100), nullable=True)
25
+ contact: Mapped[str] = mapped_column(String(100), nullable=True)
26
+ subscription_status: Mapped[str] = mapped_column(
27
+ String(20), default='inactive')
28
+ # Relationships
29
+ test_attempts = relationship("TestAttempt", back_populates="user")
30
+ feedback = relationship("Feedback", back_populates="user")
31
+
32
+
33
+ class Service(Base):
34
+ """Service model for storing available services"""
35
+ __tablename__ = 'services'
36
+
37
+ id: Mapped[int] = mapped_column(primary_key=True)
38
+ service_name: Mapped[str] = mapped_column(String(100), unique=True, nullable=False)
39
+ service_description: Mapped[str] = mapped_column(String(500))
40
+ service_price: Mapped[int] = mapped_column(BigInteger, nullable=False)
41
+ is_active: Mapped[bool] = mapped_column(Boolean, default=True, nullable=False)
42
+ # Relationships
43
+ feedback = relationship("Feedback", back_populates="service")
44
+
45
+
46
+ class Test(Base):
47
+ """Test model for storing quiz/test information"""
48
+ __tablename__ = 'tests'
49
+
50
+ id: Mapped[int] = mapped_column(primary_key=True)
51
+ test_name: Mapped[str] = mapped_column(String(100), unique=True, nullable=False)
52
+ test_type: Mapped[str] = mapped_column(String(20), nullable=False)
53
+ test_description: Mapped[str] = mapped_column(String(250))
54
+ is_active: Mapped[bool] = mapped_column(Boolean, default=True, nullable=False)
55
+ completion_message: Mapped[Optional[str]] = mapped_column(String(1000))
56
+ # Relationships
57
+ questions = relationship("TestQuestion", back_populates="test", cascade="all, delete-orphan")
58
+ results = relationship("TestResult", back_populates="test", cascade="all, delete-orphan")
59
+ attempts = relationship("TestAttempt", back_populates="test")
60
+
61
+
62
+ class TestQuestion(Base):
63
+ """Model for storing test questions"""
64
+ __tablename__ = 'test_questions'
65
+
66
+ id: Mapped[int] = mapped_column(primary_key=True)
67
+ test_id: Mapped[int] = mapped_column(ForeignKey('tests.id', ondelete='CASCADE'), nullable=False)
68
+ question_content: Mapped[str] = mapped_column(String(150), nullable=False)
69
+ question_variants: Mapped[str] = mapped_column(String(500), nullable=False) # JSON string
70
+ question_points: Mapped[str] = mapped_column(String(100), nullable=False) # JSON string
71
+ # Relationships
72
+ test = relationship("Test", back_populates="questions")
73
+
74
+
75
+ class TestResult(Base):
76
+ """Model for storing possible test results"""
77
+ __tablename__ = 'test_results'
78
+
79
+ id: Mapped[int] = mapped_column(primary_key=True)
80
+ test_id: Mapped[int] = mapped_column(ForeignKey('tests.id', ondelete='CASCADE'), nullable=False)
81
+ min_points: Mapped[int] = mapped_column(Integer, nullable=False)
82
+ max_points: Mapped[int] = mapped_column(Integer, nullable=False)
83
+ result_text: Mapped[str] = mapped_column(String(1000), nullable=False)
84
+ # Relationships
85
+ test = relationship("Test", back_populates="results")
86
+
87
+
88
+ class TestAttempt(Base):
89
+ """Model for storing user test attempts"""
90
+ __tablename__ = 'test_attempts'
91
+
92
+ id: Mapped[int] = mapped_column(primary_key=True)
93
+ user_id: Mapped[int] = mapped_column(ForeignKey('users.id', ondelete='CASCADE'), nullable=False)
94
+ test_id: Mapped[int] = mapped_column(ForeignKey('tests.id', ondelete='CASCADE'), nullable=False)
95
+ score: Mapped[int] = mapped_column(Integer, nullable=True)
96
+ result: Mapped[Optional[str]] = mapped_column(String(1000), nullable=True)
97
+ completed_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.now, nullable=True)
98
+ # Relationships
99
+ user = relationship("User", back_populates="test_attempts")
100
+ test = relationship("Test", back_populates="attempts")
101
+
102
+
103
+ class TestAnswer(Base):
104
+ """Stores individual answers for each question"""
105
+ __tablename__ = 'test_answers'
106
+
107
+ id: Mapped[int] = mapped_column(primary_key=True)
108
+ attempt_id: Mapped[int] = mapped_column(ForeignKey('test_attempts.id', ondelete='CASCADE'))
109
+ question_id: Mapped[int] = mapped_column(ForeignKey('test_questions.id', ondelete='CASCADE'))
110
+ answer_given: Mapped[str] = mapped_column(String(500))
111
+ points_earned: Mapped[int] = mapped_column(Integer, nullable=True)
112
+
113
+
114
+ class LeadMagnet(Base):
115
+ """Model for storing lead magnets/content triggers"""
116
+ __tablename__ = 'lead_magnets'
117
+
118
+ id: Mapped[int] = mapped_column(primary_key=True)
119
+ trigger: Mapped[str] = mapped_column(String(50), unique=True, nullable=False)
120
+ content: Mapped[str] = mapped_column(String(500), nullable=False)
121
+ is_active: Mapped[bool] = mapped_column(Boolean, default=True, nullable=False)
122
+
123
+
124
+ class Feedback(Base):
125
+ """Model for storing user feedback"""
126
+ __tablename__ = 'feedback'
127
+
128
+ id: Mapped[int] = mapped_column(primary_key=True)
129
+ user_id: Mapped[int] = mapped_column(ForeignKey('users.id', ondelete='CASCADE'), nullable=False)
130
+ service_id: Mapped[int] = mapped_column(ForeignKey('services.id', ondelete='CASCADE'), nullable=False)
131
+ rating: Mapped[int] = mapped_column(Integer, nullable=False)
132
+ review: Mapped[str] = mapped_column(String(1000))
133
+ is_new: Mapped[bool] = mapped_column(Boolean, default=True, nullable=False)
134
+ # Relationships
135
+ user = relationship("User", back_populates="feedback")
136
+ service = relationship("Service", back_populates="feedback")
137
+
138
+
139
+ async def async_main():
140
+ """Initialize database tables"""
141
+ async with engine.begin() as conn:
142
+ await conn.run_sync(Base.metadata.create_all)
app/database/requests.py ADDED
@@ -0,0 +1,708 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from typing import Optional, List, Dict, Any
2
+ from sqlalchemy import select, update, delete, func
3
+ from sqlalchemy.ext.asyncio import AsyncSession
4
+ from app.database.models import *
5
+ from app.database.base import get_session
6
+ import json
7
+ from app.utils.exceptions import DatabaseError, ValidationError
8
+
9
+
10
+ async def set_user(tg_id: int) -> Optional[User]:
11
+ """Create a new user if not exists or return existing user."""
12
+ async with get_session() as session:
13
+ try:
14
+ query = select(User).where(User.tg_id == tg_id)
15
+ user = await session.scalar(query)
16
+ if not user:
17
+ user = User(tg_id=tg_id)
18
+ session.add(user)
19
+ print("User added")
20
+ return user
21
+ except Exception as e:
22
+ raise DatabaseError(f"Error setting user: {str(e)}")
23
+
24
+
25
+ async def user_register(
26
+ tg_id: int,
27
+ name: str,
28
+ login: str,
29
+ contact: str,
30
+ subscribe: bool
31
+ ) -> None:
32
+ """Update user registration information."""
33
+ async with get_session() as session:
34
+ try:
35
+ query = update(User).where(User.tg_id == tg_id).values(
36
+ name=name,
37
+ login=login,
38
+ contact=contact,
39
+ subscription_status="active" if subscribe else "inactive"
40
+ )
41
+ await session.execute(query)
42
+ except Exception as e:
43
+ raise DatabaseError(f"Error registering user: {str(e)}")
44
+
45
+
46
+ async def check_login_unique(login: str) -> bool:
47
+ """Check if login is available"""
48
+ async with get_session() as session:
49
+ user = await session.scalar(
50
+ select(User).where(User.login == login)
51
+ )
52
+ return user is None
53
+
54
+
55
+ async def get_catalog() -> Optional[List[str]]:
56
+ """Get list of all service names."""
57
+ async with get_session() as session:
58
+ try:
59
+ query = select(Service).where(Service.is_active == True)
60
+ result = await session.execute(query)
61
+ services = result.scalars().all()
62
+ return services if services else None
63
+ except Exception as e:
64
+ raise DatabaseError(f"Error getting catalog: {str(e)}")
65
+
66
+
67
+ async def get_service_info(service_idx: str) -> Optional[Service]:
68
+ """Get detailed information about a specific service."""
69
+ async with get_session() as session:
70
+ try:
71
+ query = select(Service).where(
72
+ Service.id == service_idx,
73
+ Service.is_active == True
74
+ )
75
+ service = await session.scalar(query)
76
+ return service if service else None
77
+ except Exception as e:
78
+ raise DatabaseError(f"Error getting service info: {str(e)}")
79
+
80
+
81
+ async def add_service(name: str, desc: str, price: int, active=bool) -> None:
82
+ """Add a new service to the catalog."""
83
+ async with get_session() as session:
84
+ try:
85
+ service = Service(
86
+ service_name=name,
87
+ service_description=desc,
88
+ service_price=price,
89
+ is_active=active
90
+ )
91
+ session.add(service)
92
+ except Exception as e:
93
+ raise DatabaseError(f"Error adding service: {str(e)}")
94
+
95
+
96
+ async def edit_service(serv_id: int, param: str, change: Any, active: bool) -> None:
97
+ """Edit an existing service."""
98
+ param_mapping = {
99
+ 'name': 'service_name',
100
+ 'desc': 'service_description',
101
+ 'price': 'service_price'
102
+ }
103
+
104
+ if param not in param_mapping:
105
+ raise ValueError(f"Invalid parameter: {param}")
106
+
107
+ async with get_session() as session:
108
+ try:
109
+ query = update(Service).where(
110
+ Service.id == serv_id
111
+ ).values({param_mapping[param]: change})
112
+ await session.execute(query)
113
+ except Exception as e:
114
+ raise DatabaseError(f"Error editing service: {str(e)}")
115
+
116
+
117
+ async def delete_service(serv_id: int) -> bool:
118
+ """Delete a service from the catalog."""
119
+ async with get_session() as session:
120
+ try:
121
+ query = select(Service).where(Service.id == serv_id)
122
+ service = await session.scalar(query)
123
+ if not service:
124
+ return False
125
+ feedback_query = select(Feedback).where(Feedback.service_id == service.id)
126
+ has_feedback = await session.scalar(feedback_query)
127
+ if has_feedback:
128
+ update_query = (
129
+ update(Service)
130
+ .where(Service.id == serv_id)
131
+ .values(is_active=False)
132
+ )
133
+ await session.execute(update_query)
134
+ else:
135
+ await session.delete(service)
136
+ return True
137
+ except Exception as e:
138
+ raise DatabaseError(f"Error deleting service: {str(e)}")
139
+
140
+
141
+ async def get_leadmagnets() -> Optional[List[str]]:
142
+ """Get list of all active lead magnets."""
143
+ async with get_session() as session:
144
+ try:
145
+ query = select(LeadMagnet.trigger).where(LeadMagnet.is_active == True)
146
+ result = await session.execute(query)
147
+ magnets = result.scalars().all()
148
+ return magnets if magnets else None
149
+ except Exception as e:
150
+ raise DatabaseError(f"Error getting lead magnets: {str(e)}")
151
+
152
+
153
+ async def get_leadmagnet_info(trigger: str) -> Optional[LeadMagnet]:
154
+ """Get detailed information about a specific lead magnet."""
155
+ async with get_session() as session:
156
+ try:
157
+ query = select(LeadMagnet).where(
158
+ LeadMagnet.trigger == trigger,
159
+ LeadMagnet.is_active == True
160
+ )
161
+ magnet = await session.scalar(query)
162
+ return magnet if magnet else None
163
+ except Exception as e:
164
+ raise DatabaseError(f"Error getting lead magnet info: {str(e)}")
165
+
166
+
167
+ async def add_leadmagnet(trigger: str, content: str, active: bool) -> None:
168
+ """Add a new lead magnet."""
169
+ async with get_session() as session:
170
+ try:
171
+ magnet = LeadMagnet(
172
+ trigger=trigger,
173
+ content=content,
174
+ is_active=active
175
+ )
176
+ session.add(magnet)
177
+ except Exception as e:
178
+ raise DatabaseError(f"Error adding lead magnet: {str(e)}")
179
+
180
+
181
+ async def edit_leadmagnet(name, param, change):
182
+ async with get_session() as session:
183
+ replace_dict = {'trigger': 'trigger',
184
+ 'content': 'content',
185
+ 'status': 'is_active'}
186
+ query = select(LeadMagnet).where(LeadMagnet.trigger == name)
187
+ result = await session.execute(query)
188
+ lead = result.scalars().first()
189
+ if lead:
190
+ update_query = (
191
+ update(LeadMagnet)
192
+ .where(LeadMagnet.trigger == name)
193
+ .values({replace_dict[param]: change})
194
+ .execution_options(synchronize_session="fetch")
195
+ )
196
+ await session.execute(update_query)
197
+ await session.commit()
198
+
199
+
200
+ async def delete_leadmagnet(name: str) -> None:
201
+ """Delete a lead magnet."""
202
+ async with get_session() as session:
203
+ try:
204
+ query = delete(LeadMagnet).where(LeadMagnet.trigger == name)
205
+ await session.execute(query)
206
+ except Exception as e:
207
+ raise DatabaseError(f"Error deleting lead magnet: {str(e)}")
208
+
209
+
210
+ async def get_tests() -> Optional[List[str]]:
211
+ """Get list of all active tests."""
212
+ async with get_session() as session:
213
+ try:
214
+ query = select(Test).where(Test.is_active == True)
215
+ result = await session.execute(query)
216
+ tests = result.scalars().all()
217
+ return tests if tests else None
218
+ except Exception as e:
219
+ raise DatabaseError(f"Error getting tests: {str(e)}")
220
+
221
+
222
+ async def add_test_wo_points(
223
+ name: str,
224
+ test_type: str,
225
+ desc: str,
226
+ status: bool,
227
+ completion_message: str
228
+ ) -> None:
229
+ """Add a new test without points system."""
230
+ async with get_session() as session:
231
+ try:
232
+ test = Test(
233
+ test_name=name,
234
+ test_type=test_type,
235
+ test_description=desc,
236
+ is_active=status,
237
+ completion_message=completion_message
238
+ )
239
+ session.add(test)
240
+ except Exception as e:
241
+ raise DatabaseError(f"Error adding test: {str(e)}")
242
+
243
+
244
+ async def add_question_vars_wo_points(test_name: str, text: str) -> None:
245
+ """Add questions and variants to a test without points system."""
246
+ async with get_session() as session:
247
+ try:
248
+ # Get test ID
249
+ test = await session.scalar(
250
+ select(Test).where(Test.test_name == test_name)
251
+ )
252
+ if not test:
253
+ raise ValidationError(f"Test {test_name} not found")
254
+
255
+ # Split text into question and variants
256
+ parts = text.split('***')
257
+ if len(parts) != 2:
258
+ raise ValidationError("Invalid question format")
259
+
260
+ question = TestQuestion(
261
+ test_id=test.id,
262
+ question_content=parts[0].strip(),
263
+ question_variants=parts[1].strip(),
264
+ question_points="{}" # Empty JSON for non-pointed questions
265
+ )
266
+ session.add(question)
267
+ except Exception as e:
268
+ raise DatabaseError(f"Error adding question: {str(e)}")
269
+
270
+
271
+ async def add_test_result_w_points(test_name: str, text: str) -> None:
272
+ """Add test results with point ranges."""
273
+ async with get_session() as session:
274
+ try:
275
+ test = await session.scalar(
276
+ select(Test).where(Test.test_name == test_name)
277
+ )
278
+ if not test:
279
+ raise ValidationError(f"Test {test_name} not found")
280
+
281
+ parts = text.split('\n')
282
+ if len(parts) != 2:
283
+ raise ValidationError("Invalid result format")
284
+
285
+ point_range = parts[0].strip()
286
+ min_points, max_points = map(int, point_range.split('-'))
287
+
288
+ result = TestResult(
289
+ test_id=test.id,
290
+ min_points=min_points,
291
+ max_points=max_points,
292
+ result_text=parts[1].strip()
293
+ )
294
+ session.add(result)
295
+ except Exception as e:
296
+ raise DatabaseError(f"Error adding test result: {str(e)}")
297
+
298
+
299
+ async def delete_test(t_id: int) -> None:
300
+ """Delete a test and all related questions and results."""
301
+ async with get_session() as session:
302
+ try:
303
+ test = await session.scalar(
304
+ select(Test).where(Test.id == t_id)
305
+ )
306
+ if test:
307
+ await session.delete(test) # Cascade will handle related records
308
+ except Exception as e:
309
+ raise DatabaseError(f"Error deleting test: {str(e)}")
310
+
311
+
312
+ async def get_test(t_id: int) -> Optional[Dict[str, Any]]:
313
+ """Get complete test information including questions and results."""
314
+ async with get_session() as session:
315
+ try:
316
+ test_query = select(Test).where(
317
+ Test.id == t_id,
318
+ Test.is_active == True
319
+ )
320
+ test = await session.scalar(test_query)
321
+
322
+ if not test:
323
+ return None
324
+
325
+ questions_query = select(TestQuestion).where(
326
+ TestQuestion.test_id == test.id
327
+ )
328
+ results_query = select(TestResult).where(
329
+ TestResult.test_id == test.id
330
+ )
331
+
332
+ questions = (await session.execute(questions_query)).scalars().all()
333
+ results = (await session.execute(results_query)).scalars().all()
334
+
335
+ return {
336
+ "id": t_id,
337
+ "test": test,
338
+ "questions": questions,
339
+ "results": results
340
+ }
341
+ except Exception as e:
342
+ raise DatabaseError(f"Error getting test: {str(e)}")
343
+
344
+
345
+ async def change_test_status(t_id: int, status: bool) -> None:
346
+ """Change test active status."""
347
+ async with get_session() as session:
348
+ try:
349
+ query = update(Test).where(
350
+ Test.id == t_id
351
+ ).values(is_active=True if status == "Да" else False)
352
+ await session.execute(query)
353
+ except Exception as e:
354
+ raise DatabaseError(f"Error changing test status: {str(e)}")
355
+
356
+
357
+ async def add_feedback(
358
+ user_id: int,
359
+ service_name: str,
360
+ rating: int,
361
+ review: str
362
+ ) -> None:
363
+ """Add new feedback for a service."""
364
+ async with get_session() as session:
365
+ try:
366
+ service = await session.scalar(
367
+ select(Service).where(Service.service_name == service_name)
368
+ )
369
+ if not service:
370
+ raise ValidationError(f"Service {service_name} not found")
371
+
372
+ feedback = Feedback(
373
+ user_id=user_id,
374
+ service_id=service.id,
375
+ rating=rating,
376
+ review=review,
377
+ is_new=True
378
+ )
379
+ session.add(feedback)
380
+ except Exception as e:
381
+ raise DatabaseError(f"Error adding feedback: {str(e)}")
382
+
383
+
384
+ async def get_new_feedback() -> Optional[List[Feedback]]:
385
+ """Get all new feedback entries."""
386
+ async with get_session() as session:
387
+ try:
388
+ query = select(Feedback).where(Feedback.is_new == True)
389
+ result = await session.execute(query)
390
+ feedback = result.scalars().all()
391
+ return feedback if feedback else None
392
+ except Exception as e:
393
+ raise DatabaseError(f"Error getting new feedback: {str(e)}")
394
+
395
+
396
+ async def mark_feedback_as_read(feedback_id: int) -> None:
397
+ """Mark feedback as read."""
398
+ async with get_session() as session:
399
+ try:
400
+ query = update(Feedback).where(
401
+ Feedback.id == feedback_id
402
+ ).values(is_new=False)
403
+ await session.execute(query)
404
+ except Exception as e:
405
+ raise DatabaseError(f"Error marking feedback as read: {str(e)}")
406
+
407
+
408
+ async def get_user_info(tg_id: int) -> Optional[User]:
409
+ """Get user information by Telegram ID"""
410
+ async with get_session() as session:
411
+ try:
412
+ query = select(User).where(User.tg_id == tg_id)
413
+ user = await session.scalar(query)
414
+ return user
415
+ except Exception as e:
416
+ raise DatabaseError(f"Error getting user info: {str(e)}")
417
+
418
+
419
+ async def start_test_attempt(user_id: int, test_id: str) -> Optional[Dict[str, Any]]:
420
+ """Create new test attempt and return first question"""
421
+ async with get_session() as session:
422
+ try:
423
+ test = await session.scalar(
424
+ select(Test).where(
425
+ Test.id == test_id,
426
+ Test.is_active == True
427
+ )
428
+ )
429
+ if not test:
430
+ return None
431
+ user = await session.scalar(
432
+ select(User).where(User.tg_id == user_id)
433
+ )
434
+ if not user:
435
+ return None
436
+
437
+ # Create test attempt
438
+ attempt = TestAttempt(
439
+ user_id=user_id,
440
+ test_id=test.id
441
+ )
442
+ session.add(attempt)
443
+ await session.flush() # Get attempt ID
444
+
445
+ # Get first question
446
+ question = await session.scalar(
447
+ select(TestQuestion)
448
+ .where(TestQuestion.test_id == test.id)
449
+ .order_by(TestQuestion.id)
450
+ )
451
+ await session.commit()
452
+ return {
453
+ "attempt_id": attempt.id,
454
+ "question": question,
455
+ "total_questions": await session.scalar(
456
+ select(func.count()).select_from(TestQuestion)
457
+ .where(TestQuestion.test_id == test.id)
458
+ )
459
+ }
460
+ except Exception as e:
461
+ raise DatabaseError(f"Error starting test: {str(e)}")
462
+
463
+
464
+ async def record_answer(attempt_id: int, question_id: int, answer: str) -> Optional[Dict[str, Any]]:
465
+ """Record user's answer and return next question or result"""
466
+ async with get_session() as session:
467
+ try:
468
+ # Get the test attempt first
469
+ attempt = await session.scalar(
470
+ select(TestAttempt).where(TestAttempt.id == attempt_id)
471
+ )
472
+ if not attempt:
473
+ raise DatabaseError("Test attempt not found")
474
+
475
+ # Get question and test
476
+ question = await session.scalar(
477
+ select(TestQuestion).where(TestQuestion.id == question_id)
478
+ )
479
+ if not question:
480
+ raise DatabaseError("Question not found")
481
+
482
+ test = await session.scalar(
483
+ select(Test).where(Test.id == question.test_id)
484
+ )
485
+
486
+ # Calculate points
487
+ points = 0
488
+ if test.test_type == "С баллами":
489
+ variants_raw = question.question_variants.split('\n')
490
+ for variant in variants_raw:
491
+ if variant.strip():
492
+ try:
493
+ variant_parts = variant.strip().split('...')
494
+ if len(variant_parts) == 2:
495
+ variant_text, points_str = variant_parts
496
+ if variant_text.strip() == answer.split("...")[0].strip():
497
+ points = int(points_str.strip())
498
+ break
499
+ except ValueError:
500
+ continue
501
+
502
+ # Create and save answer record
503
+ answer_record = TestAnswer(
504
+ attempt_id=attempt_id,
505
+ question_id=question_id,
506
+ answer_given=answer,
507
+ points_earned=points
508
+ )
509
+ session.add(answer_record)
510
+ await session.flush()
511
+
512
+ # Get next question
513
+ next_question = await session.scalar(
514
+ select(TestQuestion)
515
+ .where(TestQuestion.test_id == test.id)
516
+ .where(TestQuestion.id > question_id)
517
+ .order_by(TestQuestion.id)
518
+ )
519
+
520
+ if next_question:
521
+ await session.commit()
522
+ return {"next_question": next_question}
523
+
524
+ # If no next question, test is complete
525
+ # Calculate total score
526
+ answers = await session.scalars(
527
+ select(TestAnswer)
528
+ .where(TestAnswer.attempt_id == attempt_id)
529
+ )
530
+ total_score = sum(ans.points_earned for ans in answers.all())
531
+
532
+ # Update attempt with final score
533
+ attempt.score = total_score
534
+
535
+ if test.test_type == "С баллами":
536
+ # Get appropriate result
537
+ result = await session.scalar(
538
+ select(TestResult)
539
+ .where(TestResult.test_id == test.id)
540
+ .where(TestResult.min_points <= total_score)
541
+ .where(TestResult.max_points >= total_score)
542
+ )
543
+
544
+ attempt.result = result.result_text if result else None
545
+
546
+ result_dict = {
547
+ "completed": True,
548
+ "total_points": total_score,
549
+ "result": result.result_text if result else None
550
+ }
551
+ else:
552
+ result_dict = {
553
+ "completed": True,
554
+ "result": test.completion_message
555
+ }
556
+ attempt.result = test.completion_message
557
+
558
+ await session.commit()
559
+ return result_dict
560
+
561
+ except Exception as e:
562
+ await session.rollback()
563
+ raise DatabaseError(f"Error recording answer: {str(e)}")
564
+
565
+
566
+ async def check_user_registered(user_id: int) -> bool:
567
+ """Check if user has completed registration"""
568
+ async with get_session() as session:
569
+ try:
570
+ user = await session.scalar(
571
+ select(User)
572
+ .where(User.tg_id == user_id)
573
+ )
574
+ print(f"User found: {user}") # Debug print
575
+ return bool(user.name)
576
+ except Exception as e:
577
+ raise DatabaseError(f"Error checking user registration: {str(e)}")
578
+
579
+
580
+ async def get_user_test_results(user_login: str) -> List[Dict[str, Any]]:
581
+ """Get all test results for a user"""
582
+ async with get_session() as session:
583
+ try:
584
+ user = await session.scalar(
585
+ select(User).where(User.login == user_login)
586
+ )
587
+ if not user:
588
+ return "Пользователь не найден"
589
+ attempts = await session.execute(
590
+ select(TestAttempt, Test)
591
+ .join(Test)
592
+ .where(TestAttempt.user_id == user.tg_id)
593
+ .order_by(TestAttempt.completed_at.desc())
594
+ )
595
+ if attempts:
596
+ return ([
597
+ {
598
+ "test_name": test.test_name,
599
+ "completed_at": attempt.completed_at,
600
+ "score": attempt.score,
601
+ "result": attempt.result
602
+ }
603
+ for attempt, test in attempts
604
+ ])
605
+
606
+ except Exception as e:
607
+ raise DatabaseError(f"Error getting test results: {str(e)}")
608
+
609
+
610
+ async def get_user_registration_info(user_id: int) -> str:
611
+ """Get formatted user registration information"""
612
+ async with get_session() as session:
613
+ try:
614
+ user = await session.scalar(
615
+ select(User).where(User.tg_id == user_id)
616
+ )
617
+ if not user:
618
+ return "Информация о пользователе не найдена"
619
+
620
+ return (
621
+ "📋 Ваша регистрационная информация:\n"
622
+ f"ID: {user.tg_id}\n"
623
+ f"Имя: {user.name or 'Не указано'}\n"
624
+ f"Логин: {user.login or 'Не указано'}\n"
625
+ f"Контакт: {user.contact or 'Не указано'}\n"
626
+ f"Статус подписки: {'Активна' if user.subscription_status == 'active' else 'Неактивна'}"
627
+ )
628
+ except Exception as e:
629
+ raise DatabaseError(f"Error getting user info: {str(e)}")
630
+
631
+
632
+ async def get_all_test_answers() -> List[Dict[str, Any]]:
633
+ """Fetch all test answers with related information"""
634
+ async with get_session() as session:
635
+ try:
636
+ result = await session.execute(
637
+ select(TestAnswer, TestAttempt, User, Test, TestQuestion)
638
+ .join(TestAttempt, TestAttempt.id == TestAnswer.attempt_id)
639
+ .join(User, User.id == TestAttempt.user_id)
640
+ .join(Test, Test.id == TestAttempt.test_id)
641
+ .join(TestQuestion, TestQuestion.id == TestAnswer.question_id)
642
+ .order_by(TestAttempt.completed_at.desc())
643
+ )
644
+ answers = result.fetchall()
645
+ print(answers) # Debug print
646
+
647
+ return [
648
+ {
649
+ "answer_id": answer.id,
650
+ "user_name": user.name,
651
+ "test_name": test.test_name,
652
+ "question": question.question_content,
653
+ "answer_given": answer.answer_given,
654
+ "points_earned": answer.points_earned,
655
+ "completed_at": attempt.completed_at.strftime("%d.%m.%Y %H:%M")
656
+ }
657
+ for answer, attempt, user, test, question in answers
658
+ ]
659
+
660
+ except Exception as e:
661
+ raise DatabaseError(f"Error fetching test answers: {str(e)}")
662
+
663
+
664
+ async def own_login_check(user_id: int, login: str) -> bool:
665
+ """Check if the provided login matches the user's login"""
666
+ async with get_session() as session:
667
+ try:
668
+ user = await session.scalar(
669
+ select(User).where(User.tg_id == user_id)
670
+ )
671
+ if not user:
672
+ return False
673
+ return user.login == login
674
+ except Exception as e:
675
+ raise DatabaseError(f"Error checking login: {str(e)}")
676
+
677
+
678
+ async def update_user_data(user_id: int, param: str, change: Any) -> None:
679
+ async with get_session() as session:
680
+ replace_dict = {'Имя': 'name',
681
+ 'Логин': 'login',
682
+ 'Контакт': 'contact',
683
+ 'Статус подписки на рассылку': 'subscription_status'}
684
+ query = select(User).where(User.tg_id == user_id)
685
+ result = await session.execute(query)
686
+ user = result.scalars().first()
687
+ if user:
688
+ update_query = (
689
+ update(User)
690
+ .where(User.tg_id == user_id)
691
+ .values({replace_dict[param]: change})
692
+ .execution_options(synchronize_session="fetch")
693
+ )
694
+ await session.execute(update_query)
695
+ await session.commit()
696
+
697
+
698
+ async def get_broadcast_users() -> List[int]:
699
+ """Fetch all users for broadcasting"""
700
+ async with get_session() as session:
701
+ try:
702
+ result = await session.scalars(
703
+ select(User.tg_id)
704
+ .where(User.subscription_status == 'active')
705
+ )
706
+ return result.fetchall()
707
+ except Exception as e:
708
+ raise DatabaseError(f"Error fetching broadcast users: {str(e)}")
app/handlers/__init__.py ADDED
File without changes
app/handlers/admin/__init__.py ADDED
File without changes
app/handlers/admin/broadcast.py ADDED
@@ -0,0 +1,34 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from aiogram import Router, F
2
+ from aiogram.types import Message, CallbackQuery
3
+ from aiogram.fsm.context import FSMContext
4
+ from app.database import requests as rq
5
+ from app.keyboards import admin_keyboards as kb
6
+ from app.states import Other
7
+ from app.database.requests import get_broadcast_users
8
+ from app.middleware.authentification import admin_check
9
+ from time import sleep
10
+ from random import randint
11
+
12
+ router = Router()
13
+
14
+ @router.message(F.text == 'Отправить сообщение в рассылку')
15
+ async def compose_message(message: Message, state: FSMContext):
16
+ if not await admin_check(message, {}):
17
+ await message.answer("У вас нет доступа к админ-панели")
18
+ return
19
+ await state.set_state(Other.admin_send_mailing)
20
+ await message.answer('Напишите Ваше сообщение')
21
+
22
+
23
+ @router.message(Other.admin_send_mailing)
24
+ async def send_message(message: Message, state: FSMContext):
25
+ user_ids = await get_broadcast_users()
26
+ if not user_ids:
27
+ await message.answer("Нет подписчиков для рассылки")
28
+ return
29
+ for user_id in user_ids:
30
+ await message.copy_to(user_id)
31
+ sleep(randint(1,5))
32
+ await message.answer(f"Сообщение отправлено {len(user_ids)} пользователям")
33
+ await state.clear()
34
+
app/handlers/admin/catalog.py ADDED
@@ -0,0 +1,108 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from aiogram import Router, F
2
+ from aiogram.types import Message, CallbackQuery
3
+ from aiogram.fsm.context import FSMContext
4
+ from app.database import requests as rq
5
+ from app.keyboards import admin_keyboards as kb
6
+ from app.states import AdminAddService, AdminStates
7
+ from app.middleware.authentification import admin_check
8
+
9
+ router = Router()
10
+
11
+ @router.message(F.text == 'Отредактировать каталог услуг')
12
+ async def catalog_editor_start(message: Message):
13
+ if not await admin_check(message, {}):
14
+ await message.answer("У вас нет доступа к админ-панели")
15
+ return
16
+ await message.answer(
17
+ 'Текущие услуги. Нажмите на услугу для редактирования',
18
+ reply_markup=await kb.admin_keyboard_service_catalog()
19
+ )
20
+
21
+
22
+ @router.callback_query(F.data.startswith('change_service_'))
23
+ async def edit_service(callback: CallbackQuery):
24
+ await callback.answer('')
25
+ service = await rq.get_service_info(callback.data.split('_')[2])
26
+ await callback.message.answer(f'Услуга {service.service_name} \n\n'
27
+ f'Описание: {service.service_description}\n'
28
+ f'Цена: {service.service_price} рублей \n\n'
29
+ f'Что Вы хотите изменить?',
30
+ reply_markup=await kb.admin_change_service(service.id))
31
+
32
+
33
+ @router.callback_query(F.data == 'add_service')
34
+ async def add_service(callback: CallbackQuery, state: FSMContext):
35
+ await callback.answer('Добавляем услугу')
36
+ await state.set_state(AdminAddService.admin_add_name)
37
+ await callback.message.answer('Введите название новой услуги')
38
+
39
+
40
+ @router.message(AdminAddService.admin_add_name)
41
+ async def add_name(message: Message, state: FSMContext):
42
+ await state.update_data(name=message.text)
43
+ await state.set_state(AdminAddService.admin_add_desc)
44
+ await message.answer('Введите описание новой услуги')
45
+
46
+
47
+ @router.message(AdminAddService.admin_add_desc)
48
+ async def add_desc(message: Message, state: FSMContext):
49
+ await state.update_data(desc=message.text)
50
+ await state.set_state(AdminAddService.admin_add_price)
51
+ await message.answer('Введите цену услуги в рублях')
52
+
53
+
54
+ @router.message(AdminAddService.admin_add_price)
55
+ async def add_price(message: Message, state: FSMContext):
56
+ await state.update_data(price=message.text)
57
+ data = await state.get_data()
58
+ await rq.add_service(data['name'], data['desc'], data['price'], True)
59
+ await message.answer('Услуга сохранена! Теперь можно изменить ее, выбрав услугу в меню и отредактировав',
60
+ reply_markup=kb.admin_back)
61
+ await state.clear()
62
+
63
+
64
+ @router.callback_query(F.data.startswith('editservice_'))
65
+ async def begin_service_edit(callback: CallbackQuery, state: FSMContext):
66
+ await callback.answer('')
67
+ await state.set_state(AdminStates.admin_edit_service)
68
+ await state.update_data(service_id=callback.data.split('_')[2])
69
+ await state.update_data(parameter_changed=callback.data.split("_")[1])
70
+ replace_dict = {'name': 'ое название', 'desc': 'ое описание', 'price': 'ую цену'}
71
+ await callback.message.answer(f'Введите нов{replace_dict[callback.data.split("_")[1]]} услуги')
72
+
73
+
74
+ @router.message(AdminStates.admin_edit_service)
75
+ async def finish_service_edit(message: Message, state: FSMContext):
76
+ await state.update_data(change=message.text)
77
+ data = await state.get_data()
78
+ await rq.edit_service(data['service_id'], data['parameter_changed'], data['change'], True)
79
+ await state.clear()
80
+ await message.answer(f'Информация была обновлена, можно вернуться на главную', reply_markup=kb.admin_back)
81
+
82
+
83
+ @router.callback_query(F.data.startswith('deleteservice_'))
84
+ async def confirm_service_deletion(callback: CallbackQuery, state: FSMContext):
85
+ await callback.answer('')
86
+ await state.set_state(AdminStates.admin_delete_service)
87
+ await state.update_data(serv_id=callback.data.split("_")[1])
88
+ await callback.message.answer(f'Вы уверены, что хотите удалить услугу?',
89
+ reply_markup=kb.yes_no_keyboard)
90
+
91
+
92
+ @router.message(AdminStates.admin_delete_service)
93
+ async def delete_or_not_delete(message: Message, state: FSMContext):
94
+ if message.text == 'Нет':
95
+ await state.clear()
96
+ await message.answer('Тогда вернемся в меню :)', reply_markup=kb.admin_back)
97
+
98
+ if message.text == 'Да':
99
+ data = await state.get_data()
100
+ await rq.delete_service(data['serv_id'])
101
+ await state.clear()
102
+ await message.answer('Услуга была успешно удалена! Можно вернуться в меню', reply_markup=kb.admin_back)
103
+
104
+
105
+ @router.callback_query(F.data == 'admin_to_main')
106
+ async def return_to_main(callback: CallbackQuery):
107
+ await callback.answer('Возвращаемся в меню')
108
+ await callback.message.answer('Здравствуйте! Что Вы хотите сделать?', reply_markup=kb.admin_main)
app/handlers/admin/leadmagnets.py ADDED
@@ -0,0 +1,112 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from aiogram import Router, F
2
+ from aiogram.types import Message, CallbackQuery
3
+ from aiogram.fsm.context import FSMContext
4
+ from app.database import requests as rq
5
+ from app.keyboards import admin_keyboards as kb
6
+ from app.states import AdminAddLeadmagnet, AdminStates
7
+ from app.middleware.authentification import admin_check
8
+
9
+ router = Router()
10
+
11
+ @router.message(F.text == 'Отредактировать кодовые слова')
12
+ async def view_leadmagnets(message: Message):
13
+ if not await admin_check(message, {}):
14
+ await message.answer("У вас нет доступа к админ-панели")
15
+ return
16
+ await message.answer(
17
+ 'Текущие лидмагниты. Нажмите для редактирования',
18
+ reply_markup=await kb.admin_keyboard_leadmagnets()
19
+ )
20
+
21
+
22
+ @router.callback_query(F.data == 'add_leadmanget')
23
+ async def add_leadmagnet(callback: CallbackQuery, state: FSMContext):
24
+ await callback.answer('Добавляем лидмагнит')
25
+ await state.set_state(AdminAddLeadmagnet.admin_set_trigger)
26
+ await callback.message.answer('Введите кодовое слово (пожалуйста, не используйте "_" в названии!)')
27
+
28
+
29
+ @router.message(AdminAddLeadmagnet.admin_set_trigger)
30
+ async def add_trigger(message: Message, state: FSMContext):
31
+ await state.update_data(trigger=message.text)
32
+ await state.set_state(AdminAddLeadmagnet.admin_set_content)
33
+ await message.answer('Введите содержание лидмагнита '
34
+ '(сообщение, которое получит пользователь после отправки кодового слова)')
35
+
36
+
37
+ @router.message(AdminAddLeadmagnet.admin_set_content)
38
+ async def add_content(message: Message, state: FSMContext):
39
+ await state.update_data(content=message.text)
40
+ await state.set_state(AdminAddLeadmagnet.admin_set_status)
41
+ await message.answer('Хотите ли Вы сделать данный лидмагнит активным? Активные лидмагниты будут тут же доступны для '
42
+ 'взаимодействия в пользовательском интерфейсе.', reply_markup=kb.yes_no_keyboard)
43
+
44
+
45
+ @router.message(AdminAddLeadmagnet.admin_set_status)
46
+ async def set_leadmagnet_status(message: Message, state: FSMContext):
47
+ await state.update_data(status=message.text)
48
+ data = await state.get_data()
49
+ await rq.add_leadmagnet(data['trigger'], data['content'], True if data['status'] == 'Да' else False)
50
+ await message.answer('Лидмагнит сохранен! Теперь можно отредактировать его, выбрав его в меню',
51
+ reply_markup=kb.admin_back)
52
+ await state.clear()
53
+
54
+
55
+ @router.callback_query(F.data.startswith('change_leadmagnet_'))
56
+ async def edit_leadmagnet(callback: CallbackQuery):
57
+ await callback.answer('')
58
+ leadmagnet = await rq.get_leadmagnet_info(callback.data.split('_')[2])
59
+ await callback.message.answer(f'Триггер: {leadmagnet.trigger} \n\n'
60
+ f'Содержание: {leadmagnet.content}\n'
61
+ f'Активен? {leadmagnet.is_active}\n\n'
62
+ f'Что Вы хотите изменить?',
63
+ reply_markup=await kb.admin_change_leadmagnet(leadmagnet.trigger))
64
+
65
+
66
+ @router.callback_query(F.data.startswith('editleadmagnet_'))
67
+ async def begin_leadmagnet_edit(callback: CallbackQuery, state: FSMContext):
68
+ await callback.answer('')
69
+ await state.set_state(AdminStates.admin_edit_leadmagnet)
70
+ await state.update_data(leadmagnet=callback.data.split('_')[2])
71
+ await state.update_data(parameter_changed=callback.data.split("_")[1])
72
+ replace_dict = {'trigger': 'ый триггер', 'content': 'ое содержание'}
73
+ if callback.data.split("_")[1] != 'status':
74
+ await callback.message.answer(f'Введите нов{replace_dict[callback.data.split("_")[1]]} лидмагнита')
75
+ elif callback.data.split("_")[1] == 'status':
76
+ await callback.message.answer(f'Должен ли лидмагнит быть активным?', reply_markup=kb.yes_no_keyboard)
77
+
78
+
79
+ @router.message(AdminStates.admin_edit_leadmagnet)
80
+ async def finish_leadmagnet_edit(message: Message, state: FSMContext):
81
+ await state.update_data(change=message.text)
82
+ data = await state.get_data()
83
+ await rq.edit_leadmagnet(data['leadmagnet'], data['parameter_changed'], True if data['change'] == 'Да' else False)
84
+ await message.answer(f'Информация была обновлена, можно вернуться на главную', reply_markup=kb.admin_back)
85
+
86
+
87
+ @router.callback_query(F.data.startswith('deleteleadmagnet_'))
88
+ async def confirm_leadmagnet_deletion(callback: CallbackQuery, state: FSMContext):
89
+ await callback.answer('')
90
+ await state.set_state(AdminStates.admin_delete_leadmagnet)
91
+ await state.update_data(leadmagnet=callback.data.split("_")[1])
92
+ await callback.message.answer(f'Вы уверены, что хотите удалить лидмагнит {callback.data.split("_")[1]}?',
93
+ reply_markup=kb.yes_no_keyboard)
94
+
95
+
96
+ @router.message(AdminStates.admin_delete_leadmagnet)
97
+ async def delete_or_not_delete(message: Message, state: FSMContext):
98
+ if message.text == 'Нет':
99
+ await state.clear()
100
+ await message.answer('Тогда вернемся в меню :)', reply_markup=kb.admin_back)
101
+
102
+ if message.text == 'Да':
103
+ data = await state.get_data()
104
+ await rq.delete_leadmagnet(data['leadmagnet'])
105
+ await state.clear()
106
+ await message.answer('Лидмагнит был успешно удален! Можно вернуться в меню', reply_markup=kb.admin_back)
107
+
108
+
109
+ @router.callback_query(F.data == 'admin_to_main')
110
+ async def return_to_main(callback: CallbackQuery):
111
+ await callback.answer('Возвращаемся в меню')
112
+ await callback.message.answer('Здравствуйте! Что Вы хотите сделать?', reply_markup=kb.admin_main)
app/handlers/admin/router.py ADDED
@@ -0,0 +1,31 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from aiogram import Router, F
2
+ from aiogram.types import Message
3
+ from app.middleware.authentification import admin_check
4
+ from app.keyboards import admin_keyboards as kb
5
+ from .catalog import router as catalog_router
6
+ from .leadmagnets import router as leadmagnets_router
7
+ from .tests import router as tests_router
8
+ from .view_tests import router as view_tests_router
9
+ from .broadcast import router as broadcast_router
10
+
11
+ admin_router = Router()
12
+
13
+ admin_router.include_router(catalog_router)
14
+ admin_router.include_router(leadmagnets_router)
15
+ admin_router.include_router(tests_router)
16
+ admin_router.include_router(view_tests_router)
17
+ admin_router.include_router(broadcast_router)
18
+
19
+ @admin_router.message(F.text.lower() == 'вход для админов')
20
+ async def admin_enter(message: Message):
21
+ if not await admin_check(message, {}):
22
+ await message.answer("У вас нет доступа к админ-панели")
23
+ return
24
+ await message.answer('Здравствуйте! Что Вы хотите сделать?',
25
+ reply_markup=kb.admin_main)
26
+
27
+ @admin_router.message(F.text == 'Вернуться в пользовательский интерфейс')
28
+ async def return_as_user(message: Message):
29
+ await message.answer(
30
+ 'Спасибо! Для возврата в пользовательский режим отправьте /start'
31
+ )
app/handlers/admin/tests.py ADDED
@@ -0,0 +1,203 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from aiogram import Router, F
2
+ from aiogram.types import Message, CallbackQuery
3
+ from aiogram.fsm.context import FSMContext
4
+ from app.database import requests as rq
5
+ from app.keyboards import admin_keyboards as kb
6
+ from app.states import AdminAddTest, AdminStates
7
+ from app.middleware.authentification import admin_check
8
+
9
+ router = Router()
10
+
11
+ @router.message(F.text == 'Отредактировать тесты')
12
+ async def test_editor_start(message: Message):
13
+ if not await admin_check(message, {}):
14
+ await message.answer("У вас нет доступа к админ-панели")
15
+ return
16
+ await message.answer(
17
+ 'Текущие тесты. Нажмите для редактирования',
18
+ reply_markup=await kb.admin_keyboard_tests()
19
+ )
20
+
21
+
22
+ @router.callback_query(F.data == 'add_test')
23
+ async def add_test(callback: CallbackQuery, state: FSMContext):
24
+ await callback.answer('Добавляем тест')
25
+ await state.set_state(AdminAddTest.admin_set_title)
26
+ await callback.message.answer('Введите название теста (пожалуйста, не используйте "_" в названии!)')
27
+
28
+
29
+ @router.message(AdminAddTest.admin_set_title)
30
+ async def add_test_name(message: Message, state: FSMContext):
31
+ await state.update_data(name=message.text)
32
+ await state.set_state(AdminAddTest.admin_set_desc)
33
+ await message.answer('Введите краткое описание теста')
34
+
35
+
36
+ @router.message(AdminAddTest.admin_set_desc)
37
+ async def add_test_desc(message: Message, state: FSMContext):
38
+ await state.update_data(desc=message.text)
39
+ await state.set_state(AdminAddTest.admin_set_status)
40
+ await message.answer('Хотите ли Вы сделать данный тест активным? Активные тесты будут тут же доступны для '
41
+ 'взаимодействия в пользовательском интерфейсе.', reply_markup=kb.yes_no_keyboard)
42
+
43
+
44
+ @router.message(AdminAddTest.admin_set_status)
45
+ async def add_test_status(message: Message, state: FSMContext):
46
+ await state.update_data(status=message.text)
47
+ await state.set_state(AdminAddTest.admin_set_type)
48
+ await message.answer('Введите тип теста: с баллами или без баллов. Тесты с баллами предполагают вывод разных '
49
+ 'итогов теста в зависимости от количества набранных баллов, тесты без баллов предполагают '
50
+ 'одно и то же сообщение по итогам прохождения', reply_markup=kb.test_type_keyboard)
51
+
52
+
53
+ @router.message(AdminAddTest.admin_set_type)
54
+ async def add_test_type(message: Message, state: FSMContext):
55
+ await state.update_data(type=message.text)
56
+ if message.text == 'С баллами':
57
+ await state.update_data(completion_message=None)
58
+ data = await state.get_data()
59
+ await rq.add_test_wo_points(data['name'], data['type'], data['desc'], True if data['status'] == 'Да' else False,
60
+ data['completion_message'])
61
+ await state.set_state(AdminAddTest.admin_set_completion_result_set)
62
+ await message.answer('Введите вариант сообщения, показывающегося в конце теста, и количество баллов, нужное для его '
63
+ 'достижения в следующем формате: \n'
64
+ '22-24 \n'
65
+ 'Вы получили результат 1!')
66
+ if message.text == 'Без баллов':
67
+ await state.set_state(AdminAddTest.admin_set_completion_result)
68
+ await message.answer('Введите сообщение, показывающееся в конце теста')
69
+
70
+
71
+ @router.message(AdminAddTest.admin_set_completion_result)
72
+ async def add_test_completion_wo(message: Message, state: FSMContext):
73
+ await state.update_data(completion_message=message.text)
74
+ data = await state.get_data()
75
+ await rq.add_test_wo_points(data['name'], data['type'], data['desc'], True if data['status'] == 'Да' else False, data['completion_message'])
76
+ await state.set_state(AdminAddTest.admin_add_question_vars_wo_points)
77
+ await message.answer('Введите первый вопрос, на следующих строках введите варианты ответа. Например: \n\n'
78
+ 'В каком году Юрий Гагарин полетел в космос?\n***\n'
79
+ '1963 год \n'
80
+ '1961 год \n'
81
+ '1968 год')
82
+
83
+
84
+ @router.message(AdminAddTest.admin_add_question_vars_wo_points)
85
+ async def add_question_wo(message: Message, state: FSMContext):
86
+ data = await state.get_data()
87
+ await rq.add_question_vars_wo_points(data['name'], message.text)
88
+ await state.set_state(AdminAddTest.admin_end_questions)
89
+ await message.answer('Хотите ли вы добавить еще один вопрос?', reply_markup=kb.yes_no_keyboard)
90
+
91
+
92
+ @router.message(AdminAddTest.admin_end_questions)
93
+ async def do_we_continue_q(message: Message, state: FSMContext):
94
+ if message.text == 'Да':
95
+ await state.set_state(AdminAddTest.admin_add_question_vars_wo_points)
96
+ await message.answer('Введите вопрос, придерживаясь такого же форматирования, как раньше')
97
+ elif message.text == 'Нет':
98
+ await state.clear()
99
+ await message.answer('Спасибо! Ваш тест сохранен! Можно вернуться в меню!', reply_markup=kb.admin_back)
100
+
101
+
102
+ @router.message(AdminAddTest.admin_set_completion_result_set)
103
+ async def add_test_completion_w(message: Message, state: FSMContext):
104
+ data = await state.get_data()
105
+ await rq.add_test_result_w_points(data['name'], message.text)
106
+ await state.set_state(AdminAddTest.admin_end_results)
107
+ await message.answer('Хотите ли вы добавить еще вариант?', reply_markup=kb.yes_no_keyboard)
108
+
109
+
110
+ @router.message(AdminAddTest.admin_end_results)
111
+ async def add_test_completion_choice(message: Message, state: FSMContext):
112
+ if message.text == 'Да':
113
+ await state.set_state(AdminAddTest.admin_set_completion_result_set)
114
+ await message.answer(
115
+ 'Введите вариант сообщения, показывающегося в конце теста, и количество баллов, нужное для его '
116
+ 'достижения в следующем формате: \n'
117
+ '22-24 \n'
118
+ 'Вы получили результат 1!')
119
+ if message.text == 'Нет':
120
+ await state.set_state(AdminAddTest.admin_add_question_vars_wo_points)
121
+ await message.answer('Введите первый вопрос, на следующих строках введите варианты ответа. После каждого ответа '
122
+ 'указывайте количество баллов, начисляемых за этот вариант, через .... Например: \n\n'
123
+ 'В каком году Юрий Гагарин полетел в космос?\n***\n'
124
+ '1963 год...0 \n'
125
+ '1961 год...2 \n'
126
+ '1968 год...0')
127
+
128
+
129
+ @router.callback_query(F.data.startswith('change_test_'))
130
+ async def view_test(callback: CallbackQuery):
131
+ await callback.answer('')
132
+ test_data = await rq.get_test(callback.data.split('_')[2])
133
+ results_string = ''
134
+ if not test_data['results']:
135
+ results_string += 'Отдельных результатов теста нет'
136
+ else:
137
+ for i in range(len(test_data['results'])):
138
+ results_string += f'Результат теста для количества очков {test_data["results"][i].max_points} - {test_data["results"][i].min_points}: \n' \
139
+ f'{test_data["results"][i].result_text} \n'
140
+ questions_string = ''
141
+ if not test_data['questions']:
142
+ questions_string += 'У этого теста нет вопросов'
143
+ else:
144
+ for i in range(len(test_data['questions'])):
145
+ questions_string += f'Вопрос {i+1}: {test_data["questions"][i].question_content} \n' \
146
+ f'Варианты ответа: {test_data["questions"][i].question_variants} \n'
147
+ await callback.message.answer(f'Название теста: {test_data["test"].test_name}, \n'
148
+ f'Тип теста: {test_data["test"].test_type}, \n'
149
+ f'Описание теста: {test_data["test"].test_description} \n'
150
+ f'Статус активности теста: {test_data["test"].is_active} \n'
151
+ f'Сообщение о завершении теста: {test_data["test"].completion_message} \n'
152
+ f'Возможные результаты теста: \n{results_string} \n\n'
153
+ f'Вопросы теста: \n{questions_string} \n\n'
154
+ f'На данный момент подробное редактирование тестов не поддерживается. Если Вы хотите '
155
+ f'изменить статус активности теста, нажмите на соответствующую кнопку внизу. Если Вы '
156
+ f'хотите внести в тест содержательные изменения, мы рекомендуем удалить тест и '
157
+ f'внести его заново!',
158
+ reply_markup= await kb.admin_change_test(test_data['id']))
159
+
160
+
161
+ @router.callback_query(F.data.startswith('edittest_status_'))
162
+ async def change_status(callback: CallbackQuery, state: FSMContext):
163
+ await callback.answer('')
164
+ await state.update_data(id=callback.data.split('_')[2])
165
+ await state.set_state(AdminStates.admin_edit_test_status)
166
+ await callback.message.answer(f'Должен ли тест быть активным?', reply_markup=kb.yes_no_keyboard)
167
+
168
+
169
+ @router.message(AdminStates.admin_edit_test_status)
170
+ async def set_new_status(message: Message, state: FSMContext):
171
+ data = await state.get_data()
172
+ await rq.change_test_status(data['id'], message.text)
173
+ await message.answer('Изменения были сохранены! Теперь можно вернуться в меню!',
174
+ reply_markup=kb.admin_back)
175
+ await state.clear()
176
+
177
+
178
+ @router.callback_query(F.data.startswith('deletetest_'))
179
+ async def confirm_test_deletion(callback: CallbackQuery, state: FSMContext):
180
+ await callback.answer('')
181
+ await state.set_state(AdminStates.admin_delete_test)
182
+ await state.update_data(id=callback.data.split("_")[1])
183
+ await callback.message.answer(f'Вы уверены, что хотите удалить тест?',
184
+ reply_markup=kb.yes_no_keyboard)
185
+
186
+
187
+ @router.message(AdminStates.admin_delete_test)
188
+ async def delete_or_not_delete_test(message: Message, state: FSMContext):
189
+ if message.text == 'Нет':
190
+ await state.clear()
191
+ await message.answer('Тогда вернемся в меню :)', reply_markup=kb.admin_back)
192
+
193
+ if message.text == 'Да':
194
+ data = await state.get_data()
195
+ await rq.delete_test(data['id'])
196
+ await state.clear()
197
+ await message.answer('Тест был успешно удален! Можно вернуться в меню', reply_markup=kb.admin_back)
198
+
199
+
200
+ @router.callback_query(F.data == 'admin_to_main')
201
+ async def return_to_main(callback: CallbackQuery):
202
+ await callback.answer('Возвращаемся в меню')
203
+ await callback.message.answer('Здравствуйте! Что Вы хотите сделать?', reply_markup=kb.admin_main)
app/handlers/admin/view_tests.py ADDED
@@ -0,0 +1,57 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from aiogram import Router, F
2
+ from aiogram.types import Message, CallbackQuery
3
+ from aiogram.fsm.context import FSMContext
4
+ from app.database import requests as rq
5
+ from app.keyboards import admin_keyboards as kb
6
+ from app.database.requests import get_user_test_results, get_all_test_answers
7
+ from app.states import TestStates
8
+ from app.middleware.authentification import admin_check
9
+
10
+ router = Router()
11
+
12
+ @router.message(F.text == "Просмотреть результаты тестов")
13
+ async def name_provide(message: Message, state: FSMContext):
14
+ if not await admin_check(message, {}):
15
+ await message.answer("У вас нет доступа к админ-панели")
16
+ return
17
+ await state.set_state(TestStates.name_get)
18
+ await message.answer("Введите имя пользователя, результаты тестов которого хотите посмотреть. Для просмотра результатов теста пользователь должен быть зарегистрирован!")
19
+
20
+
21
+ @router.message(TestStates.name_get)
22
+ async def show_user_results(message: Message, state: FSMContext):
23
+ results = await get_user_test_results(message.text)
24
+
25
+ if isinstance(results, str):
26
+ await message.answer(results, reply_markup=kb.admin_main)
27
+ return
28
+
29
+ if not results:
30
+ await message.answer(
31
+ "📊 У пользователя пока нет результатов тестов",
32
+ reply_markup=kb.admin_main
33
+ )
34
+ return
35
+
36
+ response = f"📋 Результаты тестов пользователя {message.text}:\n\n"
37
+
38
+ for result in results:
39
+ completed_date = result['completed_at'].strftime("%d.%m.%Y %H:%M")
40
+ response += (
41
+ f"🔷 Тест: {result['test_name']}\n"
42
+ f"📅 Дата прохождения: {completed_date}\n"
43
+ f"📊 Набрано баллов: {result['score'] if result['score'] is not None else 'Нет баллов'}\n"
44
+ f"📝 Результат: {result['result'] if result['result'] else 'Не определен'}\n"
45
+ f"{'─' * 30}\n\n"
46
+ )
47
+
48
+ await message.answer(
49
+ response,
50
+ reply_markup=kb.admin_main,
51
+ parse_mode="HTML"
52
+ )
53
+
54
+ @router.callback_query(F.data == 'admin_to_main')
55
+ async def return_to_main(callback: CallbackQuery):
56
+ await callback.answer('Возвращаемся в меню')
57
+ await callback.message.answer('Здравствуйте! Что Вы хотите сделать?', reply_markup=kb.admin_main)
app/handlers/admin_route.py ADDED
@@ -0,0 +1,392 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from aiogram.types import Message, CallbackQuery
2
+ from aiogram import F, Router
3
+ from aiogram.fsm.context import FSMContext
4
+
5
+ import app.keyboards.admin_keyboards as kb
6
+ import app.database.requests as rq
7
+ from app.states import AdminAddService, AdminAddLeadmagnet, AdminAddTest, AdminStates
8
+
9
+ admin_router = Router()
10
+
11
+
12
+ @admin_router.message((F.text == 'Вход для админов'))
13
+ async def admin_enter(message: Message):
14
+ await message.answer('Здравствуйте! Что Вы хотите сделать?', reply_markup=kb.admin_main)
15
+
16
+
17
+ @admin_router.message(F.text == 'Отредактировать каталог услуг')
18
+ async def catalog_editor_start(message: Message):
19
+ await message.answer('Сейчас у нас представлены следующие услуги. Нажмите на услугу, чтобы отредактировать ее ',
20
+ reply_markup=await kb.admin_keyboard_service_catalog())
21
+
22
+
23
+ @admin_router.callback_query(F.data.startswith('change_service_'))
24
+ async def edit_service(callback: CallbackQuery):
25
+ await callback.answer('')
26
+ service = await rq.get_service_info(callback.data.split('_')[2])
27
+ await callback.message.answer(f'Услуга {service.service_name} \n\n'
28
+ f'Описание: {service.service_description}\n'
29
+ f'Цена: {service.service_price} рублей \n\n'
30
+ f'Что Вы хотите изменить?',
31
+ reply_markup=await kb.admin_change_service(service.service_name))
32
+
33
+
34
+ @admin_router.callback_query(F.data == 'admin_to_main')
35
+ async def return_to_main(callback: CallbackQuery):
36
+ await callback.answer('Возвращаемся в меню')
37
+ await callback.message.answer('Здравствуйте! Что Вы хотите сделать?', reply_markup=kb.admin_main)
38
+
39
+
40
+ @admin_router.callback_query(F.data == 'add_service')
41
+ async def add_service(callback: CallbackQuery, state: FSMContext):
42
+ await callback.answer('Добавляем услугу')
43
+ await state.set_state(AdminAddService.admin_add_name)
44
+ await callback.message.answer('Введите название новой услуги')
45
+
46
+
47
+ @admin_router.message(AdminAddService.admin_add_name)
48
+ async def add_name(message: Message, state: FSMContext):
49
+ await state.update_data(name=message.text)
50
+ await state.set_state(AdminAddService.admin_add_desc)
51
+ await message.answer('Введите описание новой услуги')
52
+
53
+
54
+ @admin_router.message(AdminAddService.admin_add_desc)
55
+ async def add_desc(message: Message, state: FSMContext):
56
+ await state.update_data(desc=message.text)
57
+ await state.set_state(AdminAddService.admin_add_price)
58
+ await message.answer('Введите цену услуги в рублях')
59
+
60
+
61
+ @admin_router.message(AdminAddService.admin_add_price)
62
+ async def add_price(message: Message, state: FSMContext):
63
+ await state.update_data(price=message.text)
64
+ data = await state.get_data()
65
+ await rq.add_service(data['name'], data['desc'], data['price'])
66
+ await message.answer('Услуга сохранена! Теперь можно изменить ее, выбрав услугу в меню и отредактировав',
67
+ reply_markup=kb.admin_back)
68
+ await state.clear()
69
+
70
+
71
+ @admin_router.callback_query(F.data.startswith('editservice_'))
72
+ async def begin_service_edit(callback: CallbackQuery, state: FSMContext):
73
+ await callback.answer('')
74
+ await state.set_state(AdminStates.admin_edit_service)
75
+ await state.update_data(service=callback.data.split('_')[2])
76
+ await state.update_data(parameter_changed=callback.data.split("_")[1])
77
+ replace_dict = {'name': 'ое название', 'desc': 'ое описание', 'price': 'ую цену'}
78
+ await callback.message.answer(f'Введите нов{replace_dict[callback.data.split("_")[1]]} услуги')
79
+
80
+
81
+ @admin_router.message(AdminStates.admin_edit_service)
82
+ async def finish_service_edit(message: Message, state: FSMContext):
83
+ await state.update_data(change=message.text)
84
+ data = await state.get_data()
85
+ await rq.edit_service(data['service'], data['parameter_changed'], data['change'])
86
+ await state.clear()
87
+ await message.answer(f'Информация была обновлена, можно вернуться на главную', reply_markup=kb.admin_back)
88
+
89
+
90
+ @admin_router.callback_query(F.data.startswith('deleteservice_'))
91
+ async def confirm_service_deletion(callback: CallbackQuery, state: FSMContext):
92
+ await callback.answer('')
93
+ await state.set_state(AdminStates.admin_delete_service)
94
+ await state.update_data(name=callback.data.split("_")[1])
95
+ await callback.message.answer(f'Вы уверены, что хотите удалить услугу {callback.data.split("_")[1]}?',
96
+ reply_markup=kb.yes_no_keyboard)
97
+
98
+
99
+ @admin_router.message(AdminStates.admin_delete_service)
100
+ async def delete_or_not_delete(message: Message, state: FSMContext):
101
+ if message.text == 'Нет':
102
+ await state.clear()
103
+ await message.answer('Тогда вернемся в меню :)', reply_markup=kb.admin_back)
104
+
105
+ if message.text == 'Да':
106
+ data = await state.get_data()
107
+ await rq.delete_service(data['name'])
108
+ await state.clear()
109
+ await message.answer('Услуга была успешно удалена! Можно вернуться в меню', reply_markup=kb.admin_back)
110
+
111
+
112
+ @admin_router.message(F.text == 'Отредактировать кодовые слова')
113
+ async def view_leadmagnets(message: Message):
114
+ await message.answer('Вот текущие лидмагниты. Нажмите на подходящий лидмагнит, чтобы изменить его.',
115
+ reply_markup=await kb.admin_keyboard_leadmagnets())
116
+
117
+
118
+ @admin_router.callback_query(F.data == 'add_leadmanget')
119
+ async def add_leadmagnet(callback: CallbackQuery, state: FSMContext):
120
+ await callback.answer('Добавляем лидмагнит')
121
+ await state.set_state(AdminAddLeadmagnet.admin_set_trigger)
122
+ await callback.message.answer('Введите кодовое слово (пожалуйста, не используйте "_" в названии!)')
123
+
124
+
125
+ @admin_router.message(AdminAddLeadmagnet.admin_set_trigger)
126
+ async def add_trigger(message: Message, state: FSMContext):
127
+ await state.update_data(trigger=message.text)
128
+ await state.set_state(AdminAddLeadmagnet.admin_set_content)
129
+ await message.answer('Введите содержание лидмагнита '
130
+ '(сообщение, которое получит пользователь после отправки кодового слова)')
131
+
132
+
133
+ @admin_router.message(AdminAddLeadmagnet.admin_set_content)
134
+ async def add_content(message: Message, state: FSMContext):
135
+ await state.update_data(content=message.text)
136
+ await state.set_state(AdminAddLeadmagnet.admin_set_status)
137
+ await message.answer('Хотите ли Вы сделать данный лидмагнит активным? Активные лидмагниты будут тут же доступны для '
138
+ 'взаимодействия в пользовательском интерфейсе.', reply_markup=kb.yes_no_keyboard)
139
+
140
+
141
+ @admin_router.message(AdminAddLeadmagnet.admin_set_status)
142
+ async def set_leadmagnet_status(message: Message, state: FSMContext):
143
+ await state.update_data(status=message.text)
144
+ data = await state.get_data()
145
+ await rq.add_leadmagnet(data['trigger'], data['content'], data['status'])
146
+ await message.answer('Лидмагнит сохранен! Теперь можно отредактировать его, выбрав его в меню',
147
+ reply_markup=kb.admin_back)
148
+ await state.clear()
149
+
150
+
151
+ @admin_router.callback_query(F.data.startswith('change_leadmagnet_'))
152
+ async def edit_leadmagnet(callback: CallbackQuery):
153
+ await callback.answer('')
154
+ leadmagnet = await rq.get_leadmagnet_info(callback.data.split('_')[2])
155
+ await callback.message.answer(f'Триггер: {leadmagnet.trigger} \n\n'
156
+ f'Содержание: {leadmagnet.content}\n'
157
+ f'Активен? {leadmagnet.active_status}\n\n'
158
+ f'Что Вы хотите изменить?',
159
+ reply_markup=await kb.admin_change_leadmagnet(leadmagnet.trigger))
160
+
161
+
162
+ @admin_router.callback_query(F.data.startswith('editleadmagnet_'))
163
+ async def begin_leadmagnet_edit(callback: CallbackQuery, state: FSMContext):
164
+ await callback.answer('')
165
+ await state.set_state(AdminStates.admin_edit_leadmagnet)
166
+ await state.update_data(leadmagnet=callback.data.split('_')[2])
167
+ await state.update_data(parameter_changed=callback.data.split("_")[1])
168
+ replace_dict = {'trigger': 'ый триггер', 'content': 'ое содержание'}
169
+ if callback.data.split("_")[1] != 'status':
170
+ await callback.message.answer(f'Введите нов{replace_dict[callback.data.split("_")[1]]} лидмагнита')
171
+ elif callback.data.split("_")[1] == 'status':
172
+ await callback.message.answer(f'Должен ли лидмагнит быть активным?', reply_markup=kb.yes_no_keyboard)
173
+
174
+
175
+ @admin_router.message(AdminStates.admin_edit_leadmagnet)
176
+ async def finish_leadmagnet_edit(message: Message, state: FSMContext):
177
+ await state.update_data(change=message.text)
178
+ data = await state.get_data()
179
+ await rq.edit_leadmagnet(data['leadmagnet'], data['parameter_changed'], data['change'])
180
+ await message.answer(f'Информация была обновлена, можно вернуться на главную', reply_markup=kb.admin_back)
181
+
182
+
183
+ @admin_router.callback_query(F.data.startswith('deleteleadmagnet_'))
184
+ async def confirm_leadmagnet_deletion(callback: CallbackQuery, state: FSMContext):
185
+ await callback.answer('')
186
+ await state.set_state(AdminStates.admin_delete_leadmagnet)
187
+ await state.update_data(leadmagnet=callback.data.split("_")[1])
188
+ await callback.message.answer(f'Вы уверены, что хотите удалить лидмагнит {callback.data.split("_")[1]}?',
189
+ reply_markup=kb.yes_no_keyboard)
190
+
191
+
192
+ @admin_router.message(AdminStates.admin_delete_leadmagnet)
193
+ async def delete_or_not_delete(message: Message, state: FSMContext):
194
+ if message.text == 'Нет':
195
+ await state.clear()
196
+ await message.answer('Тогда вернемся в меню :)', reply_markup=kb.admin_back)
197
+
198
+ if message.text == 'Да':
199
+ data = await state.get_data()
200
+ await rq.delete_leadmagnet(data['leadmagnet'])
201
+ await state.clear()
202
+ await message.answer('Лидмагнит был успешно удален! Можно вернуться в меню', reply_markup=kb.admin_back)
203
+
204
+
205
+ @admin_router.message(F.text == 'Отредактировать тесты')
206
+ async def test_editor_start(message: Message):
207
+ await message.answer('Сейчас у нас имеются следующие тесты. Нажмите на тест, чтобы отредактировать его ',
208
+ reply_markup=await kb.admin_keyboard_tests())
209
+
210
+
211
+ @admin_router.callback_query(F.data == 'add_test')
212
+ async def add_test(callback: CallbackQuery, state: FSMContext):
213
+ await callback.answer('Добавляем тест')
214
+ await state.set_state(AdminAddTest.admin_set_title)
215
+ await callback.message.answer('Введите название теста (пожалуйста, не используйте "_" в названии!)')
216
+
217
+
218
+ @admin_router.message(AdminAddTest.admin_set_title)
219
+ async def add_test_name(message: Message, state: FSMContext):
220
+ await state.update_data(name=message.text)
221
+ await state.set_state(AdminAddTest.admin_set_desc)
222
+ await message.answer('Введите краткое описание теста')
223
+
224
+
225
+ @admin_router.message(AdminAddTest.admin_set_desc)
226
+ async def add_test_desc(message: Message, state: FSMContext):
227
+ await state.update_data(desc=message.text)
228
+ await state.set_state(AdminAddTest.admin_set_status)
229
+ await message.answer('Хотите ли Вы сделать данный тест активным? Активные тесты будут тут же доступны для '
230
+ 'взаимодействия в пользовательском интерфейсе.', reply_markup=kb.yes_no_keyboard)
231
+
232
+
233
+ @admin_router.message(AdminAddTest.admin_set_status)
234
+ async def add_test_status(message: Message, state: FSMContext):
235
+ await state.update_data(status=message.text)
236
+ await state.set_state(AdminAddTest.admin_set_type)
237
+ await message.answer('Введите тип теста: с баллами или без баллов. Тесты с баллами предполагают вывод разных '
238
+ 'итогов теста в зависимости от количества набранных баллов, тесты без баллов предполагают '
239
+ 'одно и то же сообщение по итогам прохождения', reply_markup=kb.test_type_keyboard)
240
+
241
+
242
+ @admin_router.message(AdminAddTest.admin_set_type)
243
+ async def add_test_type(message: Message, state: FSMContext):
244
+ await state.update_data(type=message.text)
245
+ if message.text == 'С баллами':
246
+ await state.update_data(completion_message=None)
247
+ data = await state.get_data()
248
+ await rq.add_test_wo_points(data['name'], data['type'], data['desc'], data['status'],
249
+ data['completion_message'])
250
+ await state.set_state(AdminAddTest.admin_set_completion_result_set)
251
+ await message.answer('Введите вариант сообщения, показывающегося в конце теста, и количество баллов, нужное для его '
252
+ 'достижения в следующем формате: \n'
253
+ '22-24 \n'
254
+ 'Вы получили результат 1!')
255
+ if message.text == 'Без баллов':
256
+ await state.set_state(AdminAddTest.admin_set_completion_result)
257
+ await message.answer('Введите сообщение, показывающееся в конце теста')
258
+
259
+
260
+ @admin_router.message(AdminAddTest.admin_set_completion_result)
261
+ async def add_test_completion_wo(message: Message, state: FSMContext):
262
+ await state.update_data(completion_message=message.text)
263
+ data = await state.get_data()
264
+ await rq.add_test_wo_points(data['name'], data['type'], data['desc'], data['status'], data['completion_message'])
265
+ await state.set_state(AdminAddTest.admin_add_question_vars_wo_points)
266
+ await message.answer('Введите первый вопрос, на следующих строках введите варианты ответа. Например: \n\n'
267
+ 'В каком году Юрий Гагарин полетел в космос?\n>>>\n'
268
+ '1963 год \n'
269
+ '1961 год \n'
270
+ '1968 год')
271
+
272
+
273
+ @admin_router.message(AdminAddTest.admin_add_question_vars_wo_points)
274
+ async def add_question_wo(message: Message, state: FSMContext):
275
+ data = await state.get_data()
276
+ await rq.add_question_vars_wo_points(data['name'], message.text)
277
+ await state.set_state(AdminAddTest.admin_end_questions)
278
+ await message.answer('Хотите ли вы добавить еще один вопрос?', reply_markup=kb.yes_no_keyboard)
279
+
280
+
281
+ @admin_router.message(AdminAddTest.admin_end_questions)
282
+ async def do_we_continue_q(message: Message, state: FSMContext):
283
+ if message.text == 'Да':
284
+ await state.set_state(AdminAddTest.admin_add_question_vars_wo_points)
285
+ await message.answer('Введите вопрос, придерживаясь такого же форматирования, как раньше')
286
+ elif message.text == 'Нет':
287
+ await state.clear()
288
+ await message.answer('Спасибо! Ваш тест сохранен! Можно вернуться в меню!', reply_markup=kb.admin_back)
289
+
290
+
291
+ @admin_router.message(AdminAddTest.admin_set_completion_result_set)
292
+ async def add_test_completion_w(message: Message, state: FSMContext):
293
+ data = await state.get_data()
294
+ await rq.add_test_result_w_points(data['name'], message.text)
295
+ await state.set_state(AdminAddTest.admin_end_results)
296
+ await message.answer('Хотите ли вы добавить еще вариант?', reply_markup=kb.yes_no_keyboard)
297
+
298
+
299
+ @admin_router.message(AdminAddTest.admin_end_results)
300
+ async def add_test_completion_choice(message: Message, state: FSMContext):
301
+ if message.text == 'Да':
302
+ await state.set_state(AdminAddTest.admin_set_completion_result_set)
303
+ await message.answer(
304
+ 'Введите вариант сообщения, показывающегося в конце теста, и количество баллов, нужное для его '
305
+ 'достижения в следующем формате: \n'
306
+ '22-24 \n'
307
+ 'Вы получили результат 1!')
308
+ if message.text == 'Нет':
309
+ await state.set_state(AdminAddTest.admin_add_question_vars_wo_points)
310
+ await message.answer('Введите первый вопрос, на следующих строках введите варианты ответа. После каждого ответа '
311
+ 'указывайте количество баллов, начисляемых за этот вариант, через .... Например: \n\n'
312
+ 'В каком году Юрий Гагарин полетел в космос?\n>>>\n'
313
+ '1963 год...0 \n'
314
+ '1961 год...2 \n'
315
+ '1968 год...0')
316
+
317
+
318
+ @admin_router.callback_query(F.data.startswith('change_test_'))
319
+ async def view_test(callback: CallbackQuery):
320
+ await callback.answer('')
321
+ test_data = await rq.get_test(callback.data.split('_')[2])
322
+ results_string = ''
323
+ if not test_data['results']:
324
+ results_string += 'Отдельных результатов теста нет'
325
+ else:
326
+ for i in range(len(test_data['results'])):
327
+ results_string += f'Результат теста для количества очков {test_data["results"][i].pointrange}: \n' \
328
+ f'{test_data["results"][i].result_text} \n'
329
+ questions_string = ''
330
+ if not test_data['questions']:
331
+ questions_string += 'У этого теста нет вопросов'
332
+ else:
333
+ for i in range(len(test_data['questions'])):
334
+ questions_string += f'Вопрос {i+1}: {test_data["questions"][i].question_content} \n' \
335
+ f'Варианты ответа: {test_data["questions"][i].question_variants} \n'
336
+ await callback.message.answer(f'Название теста: {test_data["test"].test_name}, \n'
337
+ f'Тип теста: {test_data["test"].test_type}, \n'
338
+ f'Описание теста: {test_data["test"].test_description} \n'
339
+ f'Статус активности теста: {test_data["test"].active_status} \n'
340
+ f'Сообщение о завершении теста: {test_data["test"].completion_message} \n'
341
+ f'Возможные результаты теста: \n{results_string} \n\n'
342
+ f'Вопросы теста: \n{questions_string} \n\n'
343
+ f'На данный момент подробное редактирование тестов не поддерживается. Если Вы хотите '
344
+ f'изменить статус активности теста, нажмите на соответствующую кнопку внизу. Если Вы '
345
+ f'хотите внести в тест содержательные изменения, мы рекомендуем удалить тест и '
346
+ f'внести его заново!',
347
+ reply_markup= await kb.admin_change_test(test_data['test'].test_name))
348
+
349
+
350
+ @admin_router.callback_query(F.data.startswith('edittest_status_'))
351
+ async def change_status(callback: CallbackQuery, state: FSMContext):
352
+ await callback.answer('')
353
+ await state.update_data(name=callback.data.split('_')[2])
354
+ await state.set_state(AdminStates.admin_edit_test_status)
355
+ await callback.message.answer(f'Должен ли тест быть активным?', reply_markup=kb.yes_no_keyboard)
356
+
357
+
358
+ @admin_router.message(AdminStates.admin_edit_test_status)
359
+ async def set_new_status(message: Message, state: FSMContext):
360
+ data = await state.get_data()
361
+ await rq.change_test_status(data['name'], message.text)
362
+ await message.answer('Изменения были сохранены! Теперь можно вернуться в меню!',
363
+ reply_markup=kb.admin_back)
364
+ await state.clear()
365
+
366
+
367
+ @admin_router.callback_query(F.data.startswith('deletetest_'))
368
+ async def confirm_test_deletion(callback: CallbackQuery, state: FSMContext):
369
+ await callback.answer('')
370
+ await state.set_state(AdminStates.admin_delete_test)
371
+ await state.update_data(name=callback.data.split("_")[1])
372
+ await callback.message.answer(f'Вы уверены, что хотите удалить тест {callback.data.split("_")[1]}?',
373
+ reply_markup=kb.yes_no_keyboard)
374
+
375
+
376
+ @admin_router.message(AdminStates.admin_delete_test)
377
+ async def delete_or_not_delete_test(message: Message, state: FSMContext):
378
+ if message.text == 'Нет':
379
+ await state.clear()
380
+ await message.answer('Тогда вернемся в меню :)', reply_markup=kb.admin_back)
381
+
382
+ if message.text == 'Да':
383
+ data = await state.get_data()
384
+ await rq.delete_test(data['name'])
385
+ await state.clear()
386
+ await message.answer('Тест был успешно удален! Можно вернуться в меню', reply_markup=kb.admin_back)
387
+
388
+
389
+ @admin_router.message(F.text == 'Вернуться в пользовательский интерфейс')
390
+ async def return_as_user(message: Message):
391
+ await message.answer('Спасибо! Удачного Вам дня! Отправьте в чат /start, чтобы вернуться в пользовательский режим '
392
+ 'редактирования!')
app/handlers/user/__init__.py ADDED
File without changes
app/handlers/user/catalog.py ADDED
@@ -0,0 +1,74 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from aiogram import Router, F
2
+ from aiogram.types import Message, CallbackQuery
3
+ from aiogram.fsm.context import FSMContext
4
+ from app.database import requests as rq
5
+ from app.keyboards import user_keyboards as kb
6
+ from app.states import Other
7
+ from app.database.requests import check_user_registered
8
+ from app.config.config import ADMIN_ID
9
+
10
+ router = Router()
11
+
12
+ @router.message(F.text == 'Посмотреть каталог услуг')
13
+ async def get_catalog(message: Message):
14
+ await message.answer('Спасибо за интерес! Вот наш перечень услуг. '
15
+ 'Пожалуйста, выберите самую подходящую и нажмите на соответствующую кнопку, '
16
+ 'чтобы узнать о ней подробнее:', reply_markup=await kb.user_keyboard_catalog())
17
+
18
+
19
+ @router.callback_query(F.data.startswith('service_'))
20
+ async def service_info(callback: CallbackQuery,state: FSMContext):
21
+ await callback.answer('')
22
+ service_info = await rq.get_service_info(callback.data.split('_')[1])
23
+ service_string = f"Вот информация по выбранной Вами услуге: \n" \
24
+ f"Название: {service_info.service_name} \n" \
25
+ f"Описание: {service_info.service_description} \n" \
26
+ f"Цена: {service_info.service_price}. \n\n" \
27
+ f"Хотите ли Вы воспользоваться данной услугой?"
28
+ await callback.answer('')
29
+ await state.set_state(Other.user_pre_buy_service)
30
+ await state.update_data(user_id=callback.from_user.id,
31
+ user_username=callback.from_user.username,
32
+ service=service_info.service_name)
33
+ await callback.message.answer(service_string, reply_markup=kb.service_confirm)
34
+
35
+
36
+ @router.message(F.text == 'Да, связаться с Ниной')
37
+ async def admin_prompt(message: Message, state: FSMContext):
38
+ await state.set_state(Other.user_buy_service)
39
+ await message.answer('Напишите Нине и изложите Вашу проблему - это поможет ей лучше понять Ваш конкретный случай'
40
+ ' и сделать Ваш разговор более предметным!')
41
+
42
+
43
+ @router.message(Other.user_buy_service)
44
+ async def admin_connect(message: Message, state: FSMContext):
45
+ data = await state.get_data()
46
+ await message.bot.send_message(chat_id=ADMIN_ID[0],
47
+ text=f'Здравствуйте! Поступил новый заказ на услугу {data["service"]} от пользователя'
48
+ f' @{data["user_username"]}')
49
+ await message.send_copy(ADMIN_ID[0])
50
+ await message.answer('Спасибо за обращение! Скоро Нина свяжется с Вами в личных сообщениях для обсуждения '
51
+ 'подробностей Вашего заказа!')
52
+ await state.clear()
53
+ registration_status = await check_user_registered(message.from_user.id)
54
+ if registration_status:
55
+ await message.answer('Вы можете вернуться в меню и посмотреть, какие еще услуги мы предлагаем!',
56
+ reply_markup=kb.user_back_wo_reg)
57
+ elif not registration_status:
58
+ await message.answer('Вы можете вернуться в меню и посмотреть, какие еще услуги мы предлагаем! Еще мы предлагаем '
59
+ 'зарегистрироваться, чтобы получить возможность не пропускать всё самое важное, что происходит '
60
+ 'в канале. Также регистрация позволит Вам вступить в СЕКРЕТНЫЙ КЛУБ пользователей, которые'
61
+ ' будут иметь доступ к специальным акциям и ВКУСНЫМ ЦЕНАМ для своих!',
62
+ reply_markup=kb.user_back)
63
+
64
+
65
+ @router.message(F.text == 'Нет, вернуться в меню')
66
+ async def np_service_return_to_menu(message: Message, state: FSMContext):
67
+ await state.clear()
68
+ await message.answer('Чем я могу Вам помочь?', reply_markup=kb.user_main)
69
+
70
+
71
+ @router.callback_query(F.data == "user_to_main")
72
+ async def register(callback: CallbackQuery):
73
+ await callback.answer("Возвращаемся в меню")
74
+ await callback.message.answer("Чем я могу Вам помочь?", reply_markup=kb.user_main)
app/handlers/user/info_check.py ADDED
@@ -0,0 +1,55 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from aiogram import Router, F
2
+ from aiogram.types import Message, CallbackQuery
3
+ from aiogram.fsm.context import FSMContext
4
+ from app.database import requests as rq
5
+ from app.keyboards import user_keyboards as kb
6
+ from app.database.requests import check_user_registered, update_user_data, check_login_unique, own_login_check
7
+ from app.states import Other
8
+
9
+ router = Router()
10
+
11
+ @router.message(F.text == "Посмотреть сохраненные сведения о себе")
12
+ async def get_info(message: Message):
13
+ registration_status = await check_user_registered(message.from_user.id)
14
+ if registration_status:
15
+ user_info = await rq.get_user_registration_info(message.from_user.id)
16
+ await message.answer(user_info, reply_markup=kb.user_infocheck_back)
17
+ elif not registration_status:
18
+ await message.answer("Вы не зарегистрированы. Пожалуйста, зарегистрируйтесь, чтобы увидеть сохраненные сведения о себе.", reply_markup=kb.user_back)
19
+ return
20
+
21
+
22
+ @router.callback_query(F.data == 'data_correct')
23
+ async def correction_info(callback: CallbackQuery, state: FSMContext):
24
+ await state.set_state(Other.user_data_check)
25
+ await callback.message.answer('Вы правда хотите изменить регистрационные данные?', reply_markup=kb.yes_no_keyboard)
26
+
27
+
28
+ @router.message(Other.user_data_check)
29
+ async def register_check(message: Message, state: FSMContext):
30
+ if message.text == 'Да':
31
+ await state.set_state(Other.user_data_correct)
32
+ await message.answer('Упс :( Что Вы хотите исправить?', reply_markup=kb.register_correct_keyboard)
33
+ if message.text == 'Нет':
34
+ await message.answer('Хорошо! Если что-то изменится, Вы всегда можете обновить свои данные!', reply_markup=kb.user_back_wo_reg)
35
+ await state.clear()
36
+
37
+
38
+ @router.message(Other.user_data_correct)
39
+ async def correction_info(message: Message, state: FSMContext):
40
+ await state.update_data(correction=message.text)
41
+ print("check")
42
+ await state.set_state(Other.user_data_finish)
43
+ await message.answer('Какие данные Вы хотите внести?', reply_markup=kb.yes_no_keyboard)
44
+
45
+
46
+ @router.message(Other.user_data_finish)
47
+ async def correction_info_set(message: Message, state: FSMContext):
48
+ data = await state.get_data()
49
+ if data['correction'] == 'Логин':
50
+ if not await check_login_unique(message.text) and not own_login_check(message.from_user.id, message.text):
51
+ await message.answer('Этот логин уже занят. Пожалуйста, выберите другой.')
52
+ return
53
+ await update_user_data(message.from_user.id, data['correction'], message.text)
54
+ await state.clear()
55
+ await message.answer('Изменения внесены!', reply_markup=kb.user_back_wo_reg)
app/handlers/user/leadmagnets.py ADDED
@@ -0,0 +1,71 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from aiogram import Router, F
2
+ from aiogram.types import Message
3
+ from aiogram.fsm.context import FSMContext
4
+ from app.database import requests as rq
5
+ from app.keyboards import user_keyboards as kb
6
+ from app.states import LeadMagnetStates
7
+
8
+ router = Router()
9
+
10
+ @router.message(F.text == "Ввести кодовое слово")
11
+ async def start_leadmagnet_input(message: Message, state: FSMContext):
12
+ await state.set_state(LeadMagnetStates.waiting_for_keyword)
13
+ await message.answer(
14
+ "Пожалуйста, введите кодовое слово.\n"
15
+ "Для возврата в меню напишите 'отмена'",
16
+ reply_markup=kb.user_back
17
+ )
18
+
19
+ @router.message(LeadMagnetStates.waiting_for_keyword)
20
+ async def process_leadmagnet(message: Message, state: FSMContext):
21
+ if message.text.lower() == 'отмена':
22
+ await state.clear()
23
+ await message.answer(
24
+ "Возвращаемся в главное меню",
25
+ reply_markup=kb.user_main
26
+ )
27
+ return
28
+
29
+ leadmagnet = await rq.get_leadmagnet_info(message.text)
30
+
31
+ if not leadmagnet:
32
+ await message.answer(
33
+ "К сожалению, такое кодовое слово не найдено.\n"
34
+ "Проверьте правильность написания или попробуйте другое слово.\n"
35
+ "Для возврата в меню напишите 'отмена'"
36
+ )
37
+ return
38
+
39
+ if not leadmagnet.is_active:
40
+ await message.answer(
41
+ "К сожалению, это кодовое слово больше не активно.\n"
42
+ "Попробуйте другое слово или вернитесь в меню, написав 'отмена'"
43
+ )
44
+ return
45
+
46
+ # Send leadmagnet content
47
+ await message.answer(leadmagnet.content)
48
+
49
+ # Clear state and offer registration if user is not registered
50
+ await state.clear()
51
+
52
+ user = await rq.get_user_info(message.from_user.id)
53
+ if not user or not user.contact:
54
+ await message.answer(
55
+ "Хотите получать больше полезных материалов?\n"
56
+ "Зарегистрируйтесь, чтобы не пропустить новые акции и предложения!",
57
+ reply_markup=await kb.get_registration_keyboard()
58
+ )
59
+ else:
60
+ await message.answer(
61
+ "Можете ввести другое кодовое слово или вернуться в меню",
62
+ reply_markup=kb.leadmagnet_keyboard
63
+ )
64
+
65
+ @router.message(F.text == "Вернуться в главное меню")
66
+ async def return_to_main_menu(message: Message, state: FSMContext):
67
+ await state.clear()
68
+ await message.answer(
69
+ "Возвращаемся в главное меню",
70
+ reply_markup=kb.user_main
71
+ )
app/handlers/user/messaging.py ADDED
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from aiogram import Router, F
2
+ from aiogram.types import Message
3
+ from aiogram.fsm.context import FSMContext
4
+ from app.config.config import ADMIN_ID
5
+ from app.keyboards import user_keyboards as kb
6
+ from app.states import MessageStates
7
+
8
+ router = Router()
9
+
10
+ @router.message(F.text == 'Связаться с Ниной')
11
+ async def pre_contact_admin(message: Message, state: FSMContext):
12
+ await state.set_state(MessageStates.user_contact_admin)
13
+ await message.answer('Напишите ваше сообщение для Нины')
14
+
15
+
16
+ @router.message(MessageStates.user_contact_admin)
17
+ async def contact_admin(message: Message, state: FSMContext):
18
+ await message.bot.send_message(
19
+ chat_id=ADMIN_ID[0],
20
+ text=f'Сообщение от @{message.from_user.username}:'
21
+ )
22
+ await message.forward(ADMIN_ID[0])
23
+
24
+ await message.answer(
25
+ 'Сообщение отправлено! Нина ответит вам в ближайшее время.',
26
+ reply_markup=kb.user_back_wo_reg
27
+ )
28
+ await state.clear()
app/handlers/user/registration.py ADDED
@@ -0,0 +1,96 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from aiogram import Router, F
2
+ from aiogram.types import Message, CallbackQuery
3
+ from aiogram.fsm.context import FSMContext
4
+ from app.database import requests as rq
5
+ from app.keyboards import user_keyboards as kb
6
+ from app.states import Register
7
+ from app.database.requests import check_login_unique
8
+
9
+ router = Router()
10
+
11
+ @router.callback_query(F.data == "user_register")
12
+ async def register_start(callback: CallbackQuery, state: FSMContext):
13
+ await state.set_state(Register.user_register_name)
14
+ await callback.message.answer('Как к вам обращаться?')
15
+
16
+
17
+ @router.message(Register.user_register_name)
18
+ async def register_name(message: Message, state: FSMContext):
19
+ await state.update_data(name=message.text)
20
+ await state.set_state(Register.user_register_login)
21
+ await message.answer(
22
+ 'Выберите свой логин. Он должен быть уникальным!'
23
+ )
24
+
25
+
26
+ @router.message(Register.user_register_login)
27
+ async def register_name(message: Message, state: FSMContext):
28
+ if not await check_login_unique(message.text):
29
+ await message.answer('Этот логин уже занят. Пожалуйста, выберите другой.')
30
+ return
31
+ await state.update_data(login=message.text)
32
+ await state.set_state(Register.user_register_contact)
33
+ await message.answer('Укажите предпочтительный способ связи с вами')
34
+
35
+
36
+ @router.message(Register.user_register_contact)
37
+ async def register_contact(message: Message, state: FSMContext):
38
+ await state.update_data(contact=message.text)
39
+ await state.set_state(Register.user_register_subscribe)
40
+ await message.answer(
41
+ 'Хотите получать уведомления о новинках и специальных предложениях?',
42
+ reply_markup=kb.yes_no_keyboard
43
+ )
44
+
45
+
46
+ @router.message(Register.user_register_subscribe)
47
+ async def register_subscribe(message: Message, state: FSMContext):
48
+ await state.update_data(include_in_broadcast=message.text)
49
+ if message.text == 'Да':
50
+ await message.answer('Спасибо за регистрацию! Добро пожаловать в наш тайный клуб клиентов!')
51
+ elif message.text == 'Нет':
52
+ await message.answer('Спасибо за регистрацию! Если Вы измените свое решение, Вы всегда можете подписаться '
53
+ 'на наши секретные материалы, воспользовавшись ботом!')
54
+ data = await state.get_data()
55
+ await state.set_state(Register.user_register_check)
56
+ await message.answer(f'Давайте проверим еще раз:'
57
+ f'\n - Ваше имя - {data["name"]},'
58
+ f'\n - Ваш логин - {data["login"]},'
59
+ f'\n - Ваш контакт - {data["contact"]}, '
60
+ f'\n - Подписаны ли Вы на секретную рассылку: {data["include_in_broadcast"]} '
61
+ f'\n\n Всё верно?', reply_markup=kb.yes_no_keyboard)
62
+
63
+
64
+ @router.message(Register.user_register_check)
65
+ async def register_check(message: Message, state: FSMContext):
66
+ if message.text == 'Да':
67
+ data = await state.get_data()
68
+ await rq.user_register(message.from_user.id, data["name"], data["login"], data["contact"], data["include_in_broadcast"])
69
+ await message.answer('Ура! Спасибо, что Вы с нами! Хотите ли Вы сделать что-то еще?', reply_markup=kb.user_main)
70
+ await state.clear()
71
+ if message.text == 'Нет':
72
+ await state.set_state(Register.user_register_correct)
73
+ await message.answer('Упс :( Что Вы хотите исправить?', reply_markup=kb.register_correct_keyboard)
74
+
75
+
76
+ @router.message(Register.user_register_correct)
77
+ async def choose_the_correction(message: Message, state: FSMContext):
78
+ if message.text == 'Имя':
79
+ await state.set_state(Register.user_register_name)
80
+ await message.answer('Как к Вам обращаться?')
81
+ elif message.text == 'Логин':
82
+ await state.set_state(Register.user_register_login)
83
+ await message.answer('Выберите свой логин. Он должен быть уникальным!')
84
+ elif message.text == 'Контакт':
85
+ await state.set_state(Register.user_register_contact)
86
+ await message.answer('Оставьте контакт, с которого Вам будет удобнее всего общаться с нами')
87
+ elif message.text == 'Статус подписки на рассылку':
88
+ await state.set_state(Register.user_register_subscribe)
89
+ await message.answer('Хотите ли Вы получать от нас уведомления о новинках, акциях и даже '
90
+ 'СЕКРЕТНЫХ ��АСПРОДАЖАХ ДЛЯ СВОИХ?', reply_markup=kb.yes_no_keyboard)
91
+
92
+
93
+ @router.callback_query(F.data == "user_to_main")
94
+ async def register(callback: CallbackQuery):
95
+ await callback.answer("Возвращаемся в меню")
96
+ await callback.message.answer("Чем я могу Вам помочь?", reply_markup=kb.user_main)
app/handlers/user/router.py ADDED
@@ -0,0 +1,50 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from aiogram import Router
2
+ from aiogram.filters import Command
3
+ from aiogram.types import Message
4
+ from app.database import requests as rq
5
+ from app.keyboards import user_keyboards as kb
6
+
7
+ from .catalog import router as catalog_router
8
+ from .registration import router as registration_router
9
+ from .messaging import router as messaging_router
10
+ from .leadmagnets import router as leadmagnet_router
11
+ from .tests import router as test_router
12
+ from .info_check import router as info_check_router
13
+ from .send_feedback import router as send_feedback_router
14
+
15
+ user_router = Router()
16
+
17
+ user_router.include_router(catalog_router)
18
+ user_router.include_router(leadmagnet_router)
19
+ user_router.include_router(registration_router)
20
+ user_router.include_router(messaging_router)
21
+ user_router.include_router(test_router)
22
+ user_router.include_router(info_check_router)
23
+ user_router.include_router(send_feedback_router)
24
+
25
+
26
+ @user_router.message(Command('start'))
27
+ async def cmd_start(message: Message):
28
+ await rq.set_user(message.from_user.id)
29
+ await message.answer(
30
+ 'Добро пожаловать! Я - телеграм-бот Нины Смирновой. '
31
+ 'Я могу помочь вам:\n'
32
+ '- Узнать о наших услугах\n'
33
+ '- Связать вас с Ниной\n'
34
+ '- Показать полезные материалы\n'
35
+ '- Записать ваш отзыв\n'
36
+ '- Предложить интересные тесты\n\n'
37
+ 'Чем могу помочь?',
38
+ reply_markup=kb.user_main
39
+ )
40
+
41
+ @user_router.message(Command('help'))
42
+ async def cmd_help(message: Message):
43
+ await message.answer(
44
+ 'Я могу помочь вам:\n'
45
+ '1. Просмотреть каталог услуг\n'
46
+ '2. Связаться с Ниной\n'
47
+ '3. Пройти тесты\n'
48
+ '4. Оставить отзыв\n'
49
+ '5. Зарегистрироваться для доступа к специальным предложениям'
50
+ )
app/handlers/user/send_feedback.py ADDED
@@ -0,0 +1,42 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from aiogram import Router, F
2
+ from aiogram.types import Message, CallbackQuery
3
+ from aiogram.fsm.context import FSMContext
4
+ from app.keyboards import user_keyboards as kb
5
+ from app.states import MessageStates
6
+ from app.keyboards.user_keyboards import user_keyboard_feedback
7
+ from app.database import requests as rq
8
+ from app.config.config import ADMIN_ID
9
+
10
+ router = Router()
11
+
12
+ @router.message(F.text == 'Оставить отзыв')
13
+ async def pre_contact_admin(message: Message, state: FSMContext):
14
+ await state.set_state(MessageStates.user_send_feedback)
15
+ await message.answer('Выберите услугу, на которую Вы хотите оставить отзыв', reply_markup=await user_keyboard_feedback())
16
+
17
+
18
+ @router.callback_query(F.data.startswith('feedback_'))
19
+ async def choose_service(callback: CallbackQuery, state: FSMContext):
20
+ await state.set_state(MessageStates.user_choosing_feedback_service)
21
+ await callback.answer('')
22
+ service_info = await rq.get_service_info(callback.data.split('_')[1])
23
+ await state.update_data(name=service_info.service_name)
24
+ await callback.message.answer(f"Введите ваш отзыв на услугу {service_info.service_name}")
25
+
26
+
27
+ @router.message(MessageStates.user_choosing_feedback_service)
28
+ async def contact_admin(message: Message, state: FSMContext):
29
+ await state.set_state(MessageStates.user_sending_feedback)
30
+ data = await state.get_data()
31
+ print(data)
32
+ await message.bot.send_message(
33
+ chat_id=ADMIN_ID[0],
34
+ text=f'Новый отзыв на услугу #{data["name"]}:'
35
+ )
36
+ await message.forward(ADMIN_ID[0])
37
+
38
+ await message.answer(
39
+ 'Отзыв отправлен! Спасибо за использование наших услуг!.',
40
+ reply_markup=kb.user_back_wo_reg
41
+ )
42
+ await state.clear()
app/handlers/user/tests.py ADDED
@@ -0,0 +1,74 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from aiogram import Router, F
2
+ from aiogram.types import Message, CallbackQuery
3
+ from aiogram.fsm.context import FSMContext
4
+ from app.database import requests as rq
5
+ from app.keyboards import user_keyboards as kb
6
+ from app.states import TestStates
7
+ from app.database.requests import check_user_registered
8
+
9
+ router = Router()
10
+
11
+ @router.message(F.text == "Пройти тест")
12
+ async def show_available_tests(message: Message):
13
+ registration_status = await check_user_registered(message.from_user.id)
14
+ if registration_status:
15
+ await message.answer(
16
+ "Выберите тест:",
17
+ reply_markup=await kb.get_tests_keyboard()
18
+ )
19
+ if not registration_status:
20
+ await message.answer(
21
+ "Зарегистрируйтесь, чтобы сохранить Ваши результаты и получить персональные рекомендации!",
22
+ reply_markup=kb.user_back
23
+ )
24
+
25
+ @router.callback_query(F.data.startswith("start_test_"))
26
+ async def start_test(callback: CallbackQuery, state: FSMContext):
27
+ test_id = callback.data.split("_")[2]
28
+ test_data = await rq.start_test_attempt(
29
+ callback.from_user.id,
30
+ test_id
31
+ )
32
+
33
+ if not test_data:
34
+ await callback.message.answer("Тест недоступен")
35
+ return
36
+
37
+ await state.set_state(TestStates.answering)
38
+ await state.update_data(
39
+ attempt_id=test_data["attempt_id"],
40
+ current_question=1,
41
+ total_questions=test_data["total_questions"]
42
+ )
43
+
44
+ await callback.message.answer(
45
+ f"Вопрос 1 из {test_data['total_questions']}:\n\n"
46
+ f"{test_data['question'].question_content}",
47
+ reply_markup=await kb.get_test_question_keyboard(test_data["question"])
48
+ )
49
+
50
+ @router.callback_query(F.data.startswith("test_answer_"))
51
+ async def process_answer(callback: CallbackQuery, state: FSMContext):
52
+ _, _, question_id, answer = callback.data.split("_")
53
+
54
+ data = await state.get_data()
55
+ result = await rq.record_answer(data["attempt_id"],
56
+ int(question_id),
57
+ answer
58
+ )
59
+
60
+ if result.get("completed"):
61
+ await callback.message.answer(
62
+ f"Тест завершен!\n\n"
63
+ f"Ваш результат: {result['result']}"
64
+ )
65
+ await state.clear()
66
+ else:
67
+ current_q = data["current_question"] + 1
68
+ await state.update_data(current_question=current_q)
69
+
70
+ await callback.message.answer(
71
+ f"Вопрос {current_q} из {data['total_questions']}:\n\n"
72
+ f"{result['next_question'].question_content}",
73
+ reply_markup=await kb.get_test_question_keyboard(result["next_question"])
74
+ )
app/handlers/user_route.py ADDED
@@ -0,0 +1,168 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from aiogram.filters.command import Command
2
+ from aiogram.types import Message, CallbackQuery
3
+ from aiogram import F, Router, Bot
4
+ from aiogram.fsm.context import FSMContext
5
+
6
+ import app.keyboards.user_keyboards as kb
7
+ import app.database.requests as rq
8
+ from app.states import Register, Other
9
+
10
+ user_router = Router()
11
+ bot = Bot(token='5569983238:AAFo8J083zUWOk879M7YMYfhAreeiLovGcE')
12
+
13
+
14
+ @user_router.message(Command('start'))
15
+ async def cmd_start(message: Message):
16
+ await rq.set_user(message.from_user.id)
17
+ await message.answer('Добро пожаловать! Я - телеграм-бот Нины Смирновой, и моя основная задача - взаимодействие с '
18
+ 'Вами: я могу предоставить Вам информацию о наших услугах и связать Вас с Ниной, а также '
19
+ 'показать Вам интересные и полезные материалы от Нины. Еще я могу записать Ваш отзыв, '
20
+ 'если Вы уже воспользовались нашими услугами! А еще (только это секрет) у меня есть очень'
21
+ 'интересные тесты, которые позволят Вам лучше понять себя и Вашего ребенка! \n\n '
22
+ 'Чем я могу Вам помочь?',
23
+ reply_markup=kb.user_main)
24
+
25
+
26
+ @user_router.message(Command('help'))
27
+ async def cmd_help(message: Message):
28
+ await message.answer('Это кнопка помощи')
29
+
30
+
31
+ @user_router.message(F.text == 'Посмотреть каталог услуг')
32
+ async def get_catalog(message: Message):
33
+ await message.answer('Спасибо за интерес! Вот наш перечень услуг. '
34
+ 'Пожалуйста, выберите самую подходящую и нажмите на соответствующую кнопку, '
35
+ 'чтобы узнать о ней подробнее:', reply_markup=await kb.user_keyboard_catalog())
36
+
37
+
38
+ @user_router.callback_query(F.data.startswith('service_'))
39
+ async def service_info(callback: CallbackQuery,state: FSMContext):
40
+ await callback.answer('')
41
+ service_info = await rq.get_service_info(callback.data.split('_')[1])
42
+ service_string = f"Вот информация по выбранной Вами услуге: \n" \
43
+ f"Название: {service_info.service_name} \n" \
44
+ f"Описание: {service_info.service_description} \n" \
45
+ f"Цена: {service_info.service_price}. \n\n" \
46
+ f"Хотите ли Вы воспользоваться данной услугой?"
47
+ await callback.answer('')
48
+ await state.set_state(Other.user_pre_buy_service)
49
+ await state.update_data(user_id=callback.from_user.id,
50
+ user_username=callback.from_user.username,
51
+ service=service_info.service_name)
52
+ await callback.message.answer(service_string, reply_markup=kb.service_confirm)
53
+
54
+
55
+ @user_router.message(F.text == 'Да, связаться с Ниной')
56
+ async def admin_prompt(message: Message, state: FSMContext):
57
+ await state.set_state(Other.user_buy_service)
58
+ await message.answer('Напишите Нине и изложите Вашу проблему - это поможет ей лучше понять Ваш конкретный случай'
59
+ ' и сделать Ваш разговор более предметным!')
60
+
61
+
62
+ @user_router.message(Other.user_buy_service)
63
+ async def admin_connect(message: Message, state: FSMContext):
64
+ data = await state.get_data()
65
+ await bot.send_message(chat_id=1658604792,
66
+ text=f'Здравствуйте! Поступил новый заказ на услугу {data["service"]} от пользователя'
67
+ f' @{data["user_username"]}')
68
+ await message.send_copy(1658604792)
69
+ await message.answer('Спасибо за обращение! Скоро Нина свяжется с Вами в личных сообщениях для обсуждения '
70
+ 'подробностей Вашего заказа!')
71
+ await state.clear()
72
+ await message.answer('Вы можете вернуться в меню и посмотреть, какие еще услуги мы предлагаем! Еще мы предлагаем '
73
+ 'зарегистрироваться, чтобы получить возможность не пропускать всё самое важное, что происходит '
74
+ 'в канале. Также регистрация позволит ��ам вступить в СЕКРЕТНЫЙ КЛУБ пользователей, которые'
75
+ ' будут иметь доступ к специальным акциям и ВКУСНЫМ ЦЕНАМ для своих!',
76
+ reply_markup=kb.user_back)
77
+
78
+
79
+ @user_router.message(F.text == 'Нет, вернуться в меню')
80
+ async def np_service_return_to_menu(message: Message, state: FSMContext):
81
+ await state.clear()
82
+ await message.answer('Чем я могу Вам помочь?', reply_markup=kb.user_main)
83
+
84
+
85
+ @user_router.callback_query(F.data == "user_register")
86
+ async def register(callback: CallbackQuery, state: FSMContext):
87
+ await state.set_state(Register.user_register_name)
88
+ await callback.message.answer('Как к Вам обращаться?')
89
+
90
+
91
+ @user_router.callback_query(F.data == "user_to_main")
92
+ async def register(callback: CallbackQuery):
93
+ await callback.answer("Возвращаемся в меню")
94
+ await callback.message.answer("Чем я могу Вам помочь?", reply_markup=kb.user_main)
95
+
96
+
97
+ @user_router.message(Register.user_register_name)
98
+ async def register_name(message: Message, state: FSMContext):
99
+ await state.update_data(name=message.text)
100
+ await state.set_state(Register.user_register_contact)
101
+ await message.answer('Оставьте контакт, с которого Вам будет удобнее всего общаться с нами')
102
+
103
+
104
+ @user_router.message(Register.user_register_contact)
105
+ async def register_contact(message: Message, state: FSMContext):
106
+ await state.update_data(contact=message.text)
107
+ await state.set_state(Register.user_register_subscribe)
108
+ await message.answer('Хотите ли Вы получать от нас уведомления о новинках, акциях и даже '
109
+ 'СЕКРЕТНЫХ РАСПРОДАЖАХ ДЛЯ СВОИХ?', reply_markup=kb.yes_no_keyboard)
110
+
111
+
112
+ @user_router.message(Register.user_register_subscribe)
113
+ async def register_subscribe(message: Message, state: FSMContext):
114
+ await state.update_data(include_in_broadcast=message.text)
115
+ if message.text == 'Да':
116
+ await message.answer('Спасибо за регистрацию! Добро пожаловать в наш тайный клуб клиентов!')
117
+ elif message.text == 'Нет':
118
+ await message.answer('Спасибо за регистрацию! Если Вы измените свое решение, Вы всегда можете подписаться '
119
+ 'на наши секретные материалы, воспользовавшись ботом!')
120
+ data = await state.get_data()
121
+ await state.set_state(Register.user_register_check)
122
+ await message.answer(f'Давайте проверим еще раз:'
123
+ f'\n - Ваше имя - {data["name"]},'
124
+ f'\n - Ваш контакт - {data["contact"]}, '
125
+ f'\n - Подписаны ли Вы на секретную рассылку: {data["include_in_broadcast"]} '
126
+ f'\n\n Всё верно?', reply_markup=kb.yes_no_keyboard)
127
+
128
+
129
+ @user_router.message(Register.user_register_check)
130
+ async def register_check(message: Message, state: FSMContext):
131
+ if message.text == 'Да':
132
+ data = await state.get_data()
133
+ await rq.user_register(message.from_user.id, data["name"], data["contact"], data["include_in_broadcast"])
134
+ await message.answer('Ура! Спасибо, что Вы с нами! Хотите ли Вы сделать что-то еще?', reply_markup=kb.user_main)
135
+ await state.clear()
136
+ if message.text == 'Нет':
137
+ await state.set_state(Register.user_register_correct)
138
+ await message.answer('Упс :( Что Вы хотите исправить?', reply_markup=kb.register_correct_keyboard)
139
+
140
+
141
+ @user_router.message(Register.user_register_correct)
142
+ async def choose_the_correction(message: Message, state: FSMContext):
143
+ if message.text == 'Имя':
144
+ await state.set_state(Register.user_register_name)
145
+ await message.answer('Как к Вам обращаться?')
146
+ elif message.text == 'Контакт':
147
+ await state.set_state(Register.user_register_contact)
148
+ await message.answer('Оставьте контакт, с которого Вам будет удобнее всего общаться с нами')
149
+ elif message.text == 'Статус подписки на рассылку':
150
+ await state.set_state(Register.user_register_subscribe)
151
+ await message.answer('Хотите ли Вы получать от нас уведомления о новинках, акциях и даже '
152
+ 'СЕКРЕТНЫХ РАСПРОДАЖАХ ДЛЯ СВОИХ?', reply_markup=kb.yes_no_keyboard)
153
+
154
+
155
+ @user_router.message(F.text == 'Связаться с Ниной')
156
+ async def pre_get_back_to_admin(message: Message, state: FSMContext):
157
+ await state.set_state(Other.user_contact_admin)
158
+ await message.answer('Введите Ваше сообщение - и я отправлю его Нине!')
159
+
160
+
161
+ @user_router.message(Other.user_contact_admin)
162
+ async def get_back_to_admin(message: Message, state: FSMContext):
163
+ await bot.send_message(chat_id=1658604792,
164
+ text=f'Здравствуйте! Вам поступило сообщение от пользователя'
165
+ f' @{message.from_user.username}:')
166
+ await message.send_copy(1658604792)
167
+ await message.answer('Спасибо за обращение! Нина получила Ваше сообщение и ответит Вам, как только сможет!',
168
+ reply_markup=kb.user_back_wo_reg)
app/keyboards/__init__.py ADDED
File without changes
app/keyboards/admin_keyboards.py ADDED
@@ -0,0 +1,84 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from aiogram.types import ReplyKeyboardMarkup, KeyboardButton, InlineKeyboardMarkup, InlineKeyboardButton
2
+ from aiogram.utils.keyboard import InlineKeyboardBuilder
3
+ from app.database.requests import get_catalog, get_leadmagnets, get_tests
4
+
5
+
6
+ async def admin_keyboard_service_catalog():
7
+ all_items = await get_catalog()
8
+ keyboard = InlineKeyboardBuilder()
9
+ if all_items:
10
+ for item in all_items:
11
+ keyboard.add(InlineKeyboardButton(text=item.service_name, callback_data=f'change_service_{item.id}'))
12
+ keyboard.add(InlineKeyboardButton(text='Вернуться', callback_data='admin_to_main'))
13
+ keyboard.add(InlineKeyboardButton(text='Добавить услугу', callback_data='add_service'))
14
+ return keyboard.adjust(1).as_markup()
15
+
16
+
17
+ async def admin_change_service(service_id):
18
+ keyboard = InlineKeyboardBuilder()
19
+ keyboard.add(InlineKeyboardButton(text='Название', callback_data=f'editservice_name_{service_id}'))
20
+ keyboard.add(InlineKeyboardButton(text='Описание', callback_data=f'editservice_desc_{service_id}'))
21
+ keyboard.add(InlineKeyboardButton(text='Цена', callback_data=f'editservice_price_{service_id}'))
22
+ keyboard.add(InlineKeyboardButton(text='Удалить услугу', callback_data=f'deleteservice_{service_id}'))
23
+ keyboard.add(InlineKeyboardButton(text='Вернуться', callback_data='admin_to_main'))
24
+ return keyboard.adjust(1).as_markup()
25
+
26
+
27
+ async def admin_keyboard_leadmagnets():
28
+ all_items = await get_leadmagnets()
29
+ keyboard = InlineKeyboardBuilder()
30
+ if all_items:
31
+ for item in all_items:
32
+ keyboard.add(InlineKeyboardButton(text=item, callback_data=f'change_leadmagnet_{item}'))
33
+ keyboard.add(InlineKeyboardButton(text='Вернуться', callback_data='admin_to_main'))
34
+ keyboard.add(InlineKeyboardButton(text='Добавить кодовое слово', callback_data='add_leadmanget'))
35
+ return keyboard.adjust(1).as_markup()
36
+
37
+
38
+ async def admin_change_leadmagnet(trigger):
39
+ keyboard = InlineKeyboardBuilder()
40
+ keyboard.add(InlineKeyboardButton(text='Слово-триггер', callback_data=f'editleadmagnet_trigger_{trigger}'))
41
+ keyboard.add(InlineKeyboardButton(text='Содержание лидмагнита', callback_data=f'editleadmagnet_content_{trigger}'))
42
+ keyboard.add(InlineKeyboardButton(text='Статус активности лидмагнита', callback_data=f'editleadmagnet_status_{trigger}'))
43
+ keyboard.add(InlineKeyboardButton(text='Удалить лидмагнит', callback_data=f'deleteleadmagnet_{trigger}'))
44
+ keyboard.add(InlineKeyboardButton(text='Вернуться', callback_data='admin_to_main'))
45
+ return keyboard.adjust(1).as_markup()
46
+
47
+
48
+ async def admin_change_test(t_id):
49
+ keyboard = InlineKeyboardBuilder()
50
+ keyboard.add(
51
+ InlineKeyboardButton(text='Статус активности теста', callback_data=f'edittest_status_{t_id}'))
52
+ keyboard.add(InlineKeyboardButton(text='Удалить тест', callback_data=f'deletetest_{t_id}'))
53
+ keyboard.add(InlineKeyboardButton(text='Вернуться', callback_data='admin_to_main'))
54
+ return keyboard.adjust(1).as_markup()
55
+
56
+
57
+ async def admin_keyboard_tests():
58
+ all_items = await get_tests()
59
+ keyboard = InlineKeyboardBuilder()
60
+ if all_items:
61
+ for item in all_items:
62
+ keyboard.add(InlineKeyboardButton(text=item.test_name, callback_data=f'change_test_{item.id}'))
63
+ keyboard.add(InlineKeyboardButton(text='Вернуться', callback_data='admin_to_main'))
64
+ keyboard.add(InlineKeyboardButton(text='Добавить тест', callback_data='add_test'))
65
+ return keyboard.adjust(1).as_markup()
66
+
67
+
68
+ admin_main = ReplyKeyboardMarkup(keyboard=[[KeyboardButton(text='Отредактировать каталог услуг'),
69
+ KeyboardButton(text='Отредактировать тесты')],
70
+ [KeyboardButton(text='Отредактировать кодовые слова'),
71
+ KeyboardButton(text='Отправить сообщение в рассылку'),],
72
+ [KeyboardButton(text='Просмотреть результаты тестов')],
73
+ [KeyboardButton(text='Вернуться в пользовательский интерфейс')]],
74
+ resize_keyboard=True,
75
+ input_field_placeholder='Выберите подходящий Вам вариант, нажав на кнопку внизу:')
76
+
77
+ admin_back = InlineKeyboardMarkup(inline_keyboard=[[InlineKeyboardButton(text='Вернуться в меню',
78
+ callback_data='admin_to_main')]])
79
+
80
+ yes_no_keyboard = ReplyKeyboardMarkup(keyboard=[[KeyboardButton(text='Да'), KeyboardButton(text='Нет')]],
81
+ resize_keyboard=True)
82
+
83
+ test_type_keyboard = ReplyKeyboardMarkup(keyboard=[[KeyboardButton(text='С баллами'), KeyboardButton(text='Без баллов')]],
84
+ resize_keyboard=True)
app/keyboards/user_keyboards.py ADDED
@@ -0,0 +1,98 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from aiogram.types import ReplyKeyboardMarkup, KeyboardButton, InlineKeyboardMarkup, InlineKeyboardButton
2
+ from aiogram.utils.keyboard import InlineKeyboardBuilder
3
+ from app.database.requests import get_catalog, get_tests
4
+ from app.database.requests import check_login_unique
5
+
6
+
7
+ async def user_keyboard_catalog():
8
+ all_items = await get_catalog()
9
+ keyboard = InlineKeyboardBuilder()
10
+ if all_items:
11
+ for item in all_items:
12
+ keyboard.add(InlineKeyboardButton(text=item.service_name, callback_data=f'service_{item.id}'))
13
+ keyboard.add(InlineKeyboardButton(text='Вернуться', callback_data='user_to_main'))
14
+ return keyboard.adjust(1).as_markup()
15
+
16
+
17
+ async def user_keyboard_feedback():
18
+ all_items = await get_catalog()
19
+ keyboard = InlineKeyboardBuilder()
20
+ if all_items:
21
+ for item in all_items:
22
+ keyboard.add(InlineKeyboardButton(text=item.service_name, callback_data=f'feedback_{item.id}'))
23
+ keyboard.add(InlineKeyboardButton(text='Вернуться', callback_data='user_to_main'))
24
+ return keyboard.adjust(1).as_markup()
25
+
26
+
27
+ user_main = ReplyKeyboardMarkup(keyboard=[[KeyboardButton(text='Посмотреть каталог услуг'),
28
+ KeyboardButton(text='Пройти тест')],
29
+ [KeyboardButton(text='Ввести кодовое слово'),
30
+ KeyboardButton(text='Оставить отзыв')],
31
+ [KeyboardButton(text='Посмотреть сохраненные сведения о себе'),
32
+ KeyboardButton(text='Связаться с Ниной')]],
33
+ resize_keyboard=True,
34
+ input_field_placeholder='Выберите подходящий Вам вариант, нажав на кнопку внизу:')
35
+
36
+
37
+ yes_no_keyboard = ReplyKeyboardMarkup(keyboard=[[KeyboardButton(text='Да'), KeyboardButton(text='Нет')]],
38
+ resize_keyboard=True)
39
+
40
+ register_correct_keyboard = ReplyKeyboardMarkup(keyboard=[[KeyboardButton(text='Имя'),
41
+ KeyboardButton(text='Контакт'),
42
+ KeyboardButton(text='Логин'),
43
+ KeyboardButton(text='Статус подписки на рассылку')]],
44
+ resize_keyboard=True,
45
+ input_field_placeholder='Выберите, какой параметр Вы бы хотели изменить')
46
+
47
+ service_confirm = ReplyKeyboardMarkup(keyboard=[[KeyboardButton(text='Да, связаться с Ниной'),
48
+ KeyboardButton(text='Нет, вернуться в меню')]],
49
+ resize_keyboard=True)
50
+
51
+ user_back = InlineKeyboardMarkup(inline_keyboard=[[InlineKeyboardButton(text='Зарегистрироваться',
52
+ callback_data='user_register')],
53
+ [InlineKeyboardButton(text='Вернуться в меню',
54
+ callback_data='user_to_main')]])
55
+
56
+ user_back_wo_reg = InlineKeyboardMarkup(inline_keyboard=[[InlineKeyboardButton(text='Вернуться в меню',
57
+ callback_data='user_to_main')]])
58
+
59
+
60
+ user_infocheck_back = InlineKeyboardMarkup(inline_keyboard=[[InlineKeyboardButton(text='Вернуться в меню',
61
+ callback_data='user_to_main'),
62
+ InlineKeyboardButton(text='Изменить данные',
63
+ callback_data='data_correct')]])
64
+
65
+
66
+ leadmagnet_keyboard = ReplyKeyboardMarkup(
67
+ keyboard=[
68
+ [KeyboardButton(text="Ввести кодовое слово")],
69
+ [KeyboardButton(text="Вернуться в главное меню")]
70
+ ],
71
+ resize_keyboard=True
72
+ )
73
+
74
+
75
+ async def get_test_question_keyboard(question) -> InlineKeyboardMarkup:
76
+ """Create keyboard for test question variants"""
77
+ variants = question.question_variants.split('\n')
78
+ buttons = []
79
+ for variant in variants:
80
+ variant = variant.strip()
81
+ if variant:
82
+ buttons.append([
83
+ InlineKeyboardButton(
84
+ text=variant.split('...')[0],
85
+ callback_data=f"test_answer_{question.id}_{variant}"
86
+ )
87
+ ])
88
+ return InlineKeyboardMarkup(inline_keyboard=buttons)
89
+
90
+
91
+ async def get_tests_keyboard():
92
+ all_items = await get_tests()
93
+ keyboard = InlineKeyboardBuilder()
94
+ if all_items:
95
+ for item in all_items:
96
+ keyboard.add(InlineKeyboardButton(text=f"📝 {item.test_name}", callback_data=f"start_test_{item.id}"))
97
+ keyboard.add(InlineKeyboardButton(text="◀️ Вернуться в меню", callback_data="user_to_main"))
98
+ return keyboard.adjust(1).as_markup()
app/middleware/__init__.py ADDED
File without changes
app/middleware/authentification.py ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ from typing import Dict, Any
2
+ from aiogram.types import Message
3
+ from app.config.config import ADMIN_ID
4
+ from app.database.requests import check_user_registered
5
+
6
+ async def admin_check(message: Message, data: Dict[str, Any]) -> bool:
7
+ return message.from_user.id in ADMIN_ID
8
+
9
+ async def registration_check(message: Message, data: Dict[str, Any]) -> bool:
10
+ return await check_user_registered(message.from_user.id)
app/states.py ADDED
@@ -0,0 +1,77 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from aiogram.fsm.state import State, StatesGroup
2
+
3
+
4
+ class AdminStates(StatesGroup):
5
+ admin_delete_service = State()
6
+ admin_edit_service = State()
7
+ admin_edit_leadmagnet = State()
8
+ admin_delete_leadmagnet = State()
9
+ admin_add_test = State()
10
+ admin_broadcast = State()
11
+ admin_edit_test_status = State()
12
+ admin_delete_test = State()
13
+
14
+
15
+ class AdminAddService(StatesGroup):
16
+ admin_add_name = State()
17
+ admin_add_desc = State()
18
+ admin_add_price = State()
19
+
20
+
21
+ class AdminAddLeadmagnet(StatesGroup):
22
+ admin_set_status = State()
23
+ admin_set_trigger = State()
24
+ admin_set_content = State()
25
+
26
+
27
+ class AdminAddTest(StatesGroup):
28
+ admin_set_title = State()
29
+ admin_set_type = State()
30
+ admin_set_desc = State()
31
+ admin_set_status = State()
32
+ admin_set_completion_result_set = State()
33
+ admin_set_completion_result = State()
34
+ admin_add_question_content = State()
35
+ admin_add_question_vars = State()
36
+ admin_add_question_vars_wo_points = State()
37
+ admin_end_results = State()
38
+ admin_end_questions = State()
39
+ admin_add_question_points = State()
40
+
41
+
42
+ class Register(StatesGroup):
43
+ user_register_name = State()
44
+ user_register_login = State()
45
+ user_register_contact = State()
46
+ user_register_subscribe = State()
47
+ user_register_check = State()
48
+ user_register_correct = State()
49
+
50
+
51
+ class Other(StatesGroup):
52
+ user_buy_service = State()
53
+ user_pre_buy_service = State()
54
+ user_pass_test = State()
55
+ user_receive_magnet = State()
56
+ user_leave_feedback = State()
57
+ user_contact_admin = State()
58
+ user_data_check = State()
59
+ user_data_correct = State()
60
+ user_data_finish = State()
61
+ admin_send_mailing = State()
62
+
63
+
64
+ class MessageStates(StatesGroup):
65
+ user_contact_admin = State()
66
+ user_send_feedback = State()
67
+ user_choosing_feedback_service = State()
68
+ user_sending_feedback = State()
69
+
70
+
71
+ class LeadMagnetStates(StatesGroup):
72
+ waiting_for_keyword = State()
73
+
74
+
75
+ class TestStates(StatesGroup):
76
+ answering = State()
77
+ name_get = State()
app/utils/exceptions.py ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ class DatabaseError(Exception):
2
+ """Base exception for database operations."""
3
+ pass
4
+
5
+ class ValidationError(Exception):
6
+ """Exception for data validation errors."""
7
+ pass
docker-compose.yml ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ version: '3.8'
2
+
3
+ services:
4
+ bot:
5
+ build: .
6
+ env_file:
7
+ - .env
8
+ volumes:
9
+ - ./db.sqlite3:/app/db.sqlite3
10
+ ports:
11
+ - "7860:7860"
12
+ restart: always
env.example ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ BOT_TOKEN=8787878787878787878787
2
+ DATABASE_URL=sqlite+aiosqlite:///db.sqlite3
3
+ ADMIN_IDS=ID1,ID2,ID3
main.py ADDED
@@ -0,0 +1,45 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import asyncio
2
+ import logging
3
+ from aiogram import Bot, Dispatcher
4
+ from app.database.models import async_main
5
+ from app.handlers.user.router import user_router
6
+ from app.handlers.admin.router import admin_router
7
+ from app.config.config import BOT_TOKEN
8
+
9
+
10
+ logging.basicConfig(
11
+ level=logging.INFO,
12
+ format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
13
+ )
14
+ logger = logging.getLogger(__name__)
15
+
16
+
17
+ async def shutdown(bot: Bot):
18
+ """Выключение"""
19
+ logger.info("Выключение бота...")
20
+ await bot.close()
21
+ logger.info("Бот успешно выключен")
22
+
23
+
24
+ async def main():
25
+ try:
26
+ await async_main()
27
+ bot = Bot(token=BOT_TOKEN)
28
+ dp = Dispatcher()
29
+ dp.include_router(user_router)
30
+ dp.include_router(admin_router)
31
+ logger.info("Бот запускается...")
32
+ await dp.start_polling(bot)
33
+ except Exception as e:
34
+ logger.error(f"Error in main function: {e}")
35
+ await shutdown(bot)
36
+ raise
37
+
38
+
39
+ if __name__ == '__main__':
40
+ try:
41
+ asyncio.run(main())
42
+ except (KeyboardInterrupt, RuntimeError) as e:
43
+ logger.warning(f"Бот выключен: {type(e).__name__}")
44
+ except Exception as e:
45
+ logger.error(f"Внезапная ошибка: {e}")
requirements.txt ADDED
@@ -0,0 +1,25 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ aiofiles==24.1.0
2
+ aiogram==3.17.0
3
+ aiogram_broadcaster==0.6.4
4
+ aiohappyeyeballs==2.4.4
5
+ aiohttp==3.11.11
6
+ aiosignal==1.3.2
7
+ aiosqlite==0.20.0
8
+ annotated-types==0.7.0
9
+ asyncio==3.4.3
10
+ attrs==24.3.0
11
+ certifi==2024.12.14
12
+ frozenlist==1.5.0
13
+ greenlet==3.1.1
14
+ idna==3.10
15
+ magic-filter==1.0.12
16
+ multidict==6.1.0
17
+ propcache==0.2.1
18
+ pydantic==2.10.5
19
+ pydantic_core==2.27.2
20
+ python-dotenv==1.0.1
21
+ setuptools==75.1.0
22
+ SQLAlchemy==2.0.37
23
+ typing_extensions==4.12.2
24
+ wheel==0.44.0
25
+ yarl==1.18.3