diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5dee43b --- /dev/null +++ b/.gitignore @@ -0,0 +1,164 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST +.DS_Store +.vscode + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# poetry +# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control +#poetry.lock + +# pdm +# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. +#pdm.lock +# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it +# in version control. +# https://pdm.fming.dev/latest/usage/project/#working-with-version-control +.pdm.toml +.pdm-python +.pdm-build/ + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# PyCharm +# JetBrains specific template is maintained in a separate JetBrains.gitignore that can +# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# and can be added to the global gitignore or merged into this file. For a more nuclear +# option (not recommended) you can uncomment the following to ignore the entire idea folder. +#.idea/ diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..3fc04b7 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,100 @@ +version: '3.9' + +services: + tgbot_app: + build: + context: ./tgbot_app + dockerfile: Dockerfile + restart: always + environment: + - REDIS_HOST=redis + - DB_HOST=db + depends_on: + - redis + ports: + - "222:222" + networks: + - test-mission_default + + + redis: + image: redis + container_name: redis + restart: always + command: redis-server --requirepass ${R_PASSWORD} + environment: + - R_PASSWORD=${R_PASSWORD} + ports: + - "6379:6379" + networks: + - test-mission_default + + + db: + image: postgres + container_name: db + shm_size: 128mb + restart: always + volumes: + - some_volume:/var/lib/postgresql/data + environment: + - POSTGRES_USER=${POSTGRES_USER} + - POSTGRES_PASSWORD=${POSTGRES_PASSWORD} + - POSTGRES_DB=${POSTGRES_DB} + ports: + - "5432:5432" + networks: + - test-mission_default + + + fastapi_app: + build: ./fastapi_app + container_name: fastapi_app + restart: always + depends_on: + - db + ports: + - "8000:8000" + networks: + - test-mission_default + + + nginx: + image: nginx + container_name: my-nginx + restart: always + volumes: + - ./nginx/nginx.conf:/etc/nginx/nginx.conf + - ./nginx/robots.txt:/var/www/html/robots.txt + - etc-letsencrypt:/etc/letsencrypt + - www-html:/var/www/html + ports: + - "80:80" + - "443:443" + networks: + - test-mission_default + + + certbot: + image: certbot/certbot + container_name: certbot + volumes: + - etc-letsencrypt:/etc/letsencrypt + - www-html:/var/www/html + depends_on: + - nginx + command: certonly --webroot -w /var/www/html --email anakinnikitaa@gmail.com -d anakinnikita.ru --cert-name=certfolder --key-type rsa --agree-tos + ports: + - "6000:80" + networks: + - test-mission_default + + +volumes: + some_volume: + www-html: + etc-letsencrypt: + +networks: + test-mission_default: + external: true diff --git a/fastapi_app/Dockerfile b/fastapi_app/Dockerfile new file mode 100644 index 0000000..3ee5720 --- /dev/null +++ b/fastapi_app/Dockerfile @@ -0,0 +1,11 @@ +FROM python:3.11.4 + +COPY . /fastapi_app + +WORKDIR /fastapi_app + +EXPOSE 8000 + +RUN pip3 install --no-cache-dir -r requirements.txt + +CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"] diff --git a/fastapi_app/config.py b/fastapi_app/config.py new file mode 100644 index 0000000..d576000 --- /dev/null +++ b/fastapi_app/config.py @@ -0,0 +1,12 @@ +from pydantic_settings import BaseSettings, SettingsConfigDict + + +class Settings(BaseSettings): + POSTGRES_USER: str + POSTGRES_DB: str + POSTGRES_PASSWORD: str + + model_config = SettingsConfigDict(env_file='.env') + + +settings = Settings() diff --git a/fastapi_app/database/engine.py b/fastapi_app/database/engine.py new file mode 100644 index 0000000..8186774 --- /dev/null +++ b/fastapi_app/database/engine.py @@ -0,0 +1,41 @@ +from typing import AsyncGenerator + +from sqlalchemy.ext.asyncio import (AsyncSession, async_sessionmaker, + create_async_engine) + +from config import settings + +URL = f'postgresql+asyncpg://{settings.POSTGRES_USER}:{settings.POSTGRES_PASSWORD}@db:5432/{settings.POSTGRES_DB}' + + +class DataBase: + def __init__(self, url, pool_size=5): + self.engine = create_async_engine( + url=url, + echo=True, + pool_size=pool_size + ) + self.session_maker = async_sessionmaker( + bind=self.engine, + class_=AsyncSession, + autoflush=False, + expire_on_commit=False + ) + + async def dispose(self): + await self.engine.dispose() + + async def get_session(self) -> AsyncGenerator[AsyncSession, None]: + async with self.session_maker() as session: + yield session + + async def create_db(self, base): + async with self.engine.begin() as conn: + await conn.run_sync(base.metadata.create_all) + + async def drop_db(self, base): + async with self.engine.begin() as conn: + await conn.run_sync(base.metadata.drop_all) + + +database = DataBase(url=URL) diff --git a/fastapi_app/database/models.py b/fastapi_app/database/models.py new file mode 100644 index 0000000..6dd4a3f --- /dev/null +++ b/fastapi_app/database/models.py @@ -0,0 +1,28 @@ +from sqlalchemy import DateTime, ForeignKey, String, Text, func +from sqlalchemy.ext.asyncio import AsyncAttrs +from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column + + +class Base(AsyncAttrs, DeclarativeBase): + __abstract__ = True + + id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True) + created: Mapped[DateTime] = mapped_column(DateTime, default=func.now()) + updated: Mapped[DateTime] = mapped_column( + DateTime, + default=func.now(), + onupdate=func.now() + ) + + +class Users(Base): + __tablename__ = 'users' + + username: Mapped[str] = mapped_column(String(150), unique=True) + + +class UserComments(Base): + __tablename__ = 'usercomments' + + username: Mapped[int] = mapped_column(ForeignKey(Users.username)) + comment: Mapped[str] = mapped_column(Text) diff --git a/fastapi_app/database/orm.py b/fastapi_app/database/orm.py new file mode 100644 index 0000000..b464d73 --- /dev/null +++ b/fastapi_app/database/orm.py @@ -0,0 +1,55 @@ +from typing import Sequence + +from sqlalchemy import select +from sqlalchemy.ext.asyncio import AsyncSession + +from database.models import UserComments, Users +from schemas.comments import CommentsCreate +from schemas.users import UsersCreate + + +async def orm_get_users( + session: AsyncSession +) -> Sequence[Users]: + users = await session.execute(select(Users)) + result = users.scalars() + return result.all() + + +async def orm_add_users( + session: AsyncSession, + users_create: UsersCreate +) -> Users: + users = Users(**users_create.model_dump()) + user = await session.execute( + select(Users) + .where(Users.username == users.username) + ) + if not user.first(): + session.add(users) + await session.commit() + return users + + +async def orm_get_comments( + session: AsyncSession, +) -> Sequence[UserComments]: + comments = await session.execute(select(UserComments)) + result = comments.scalars() + return result + + +async def orm_add_comments( + session: AsyncSession, + comments_create: CommentsCreate +) -> UserComments: + comments = UserComments(**comments_create.model_dump()) + users_check = await session.execute( + select(Users) + .where(Users.username == comments.username) + ) + if not users_check.first(): + return + session.add(comments) + await session.commit() + return comments diff --git a/fastapi_app/handlers/router.py b/fastapi_app/handlers/router.py new file mode 100644 index 0000000..e427efa --- /dev/null +++ b/fastapi_app/handlers/router.py @@ -0,0 +1,49 @@ +from typing import List + +from fastapi import APIRouter, Depends +from sqlalchemy.ext.asyncio import AsyncSession + +from database.engine import database +from database.orm import (orm_add_comments, orm_add_users, orm_get_comments, + orm_get_users) +from schemas.comments import CommentsCreate, CommentsRead +from schemas.users import UsersCreate, UsersRead + +router = APIRouter() + + +@router.post('/users-add/') +async def add_users( + users_create: UsersCreate, + session: AsyncSession = Depends(database.get_session), +): + users = await orm_add_users(session=session, users_create=users_create) + return users + + +@router.get('/users/', response_model=List[UsersRead]) +async def get_users( + session: AsyncSession = Depends(database.get_session) +): + users = await orm_get_users(session=session) + return users + + +@router.get('/comments/', response_model=List[CommentsRead]) +async def get_comments( + session: AsyncSession = Depends(database.get_session) +): + comments = await orm_get_comments(session=session) + return comments + + +@router.post('/comments-add/') +async def add_comments( + comments_create: CommentsCreate, + session: AsyncSession = Depends(database.get_session) +): + comments = await orm_add_comments( + session=session, + comments_create=comments_create + ) + return comments diff --git a/fastapi_app/main.py b/fastapi_app/main.py new file mode 100644 index 0000000..666ea81 --- /dev/null +++ b/fastapi_app/main.py @@ -0,0 +1,33 @@ +import logging +from contextlib import asynccontextmanager + +from fastapi import FastAPI +from fastapi.responses import ORJSONResponse + +from database.engine import database +from database.models import Base +from handlers.router import router + +logging.basicConfig( + level=logging.INFO, + filename='bot_log.log', + format="%(asctime)s %(levelname)s %(message)s" +) + + +@asynccontextmanager +async def lifespan(app: FastAPI): + await database.create_db(base=Base) + yield + await database.dispose() + + +app = FastAPI( + lifespan=lifespan, + default_response_class=ORJSONResponse +) + +app.include_router( + router=router, + prefix='/api' +) diff --git a/fastapi_app/requirements.txt b/fastapi_app/requirements.txt new file mode 100644 index 0000000..3a32bb4 --- /dev/null +++ b/fastapi_app/requirements.txt @@ -0,0 +1,4 @@ +fastapi==0.111.0 +pydantic-settings==2.3.4 +SQLAlchemy==2.0.31 +asyncpg==0.29.0 \ No newline at end of file diff --git a/fastapi_app/schemas/comments.py b/fastapi_app/schemas/comments.py new file mode 100644 index 0000000..e3e2550 --- /dev/null +++ b/fastapi_app/schemas/comments.py @@ -0,0 +1,14 @@ +from pydantic import BaseModel + + +class UserComments(BaseModel): + username: str + comment: str + + +class CommentsCreate(UserComments): + pass + + +class CommentsRead(UserComments): + id: int diff --git a/fastapi_app/schemas/users.py b/fastapi_app/schemas/users.py new file mode 100644 index 0000000..8a77387 --- /dev/null +++ b/fastapi_app/schemas/users.py @@ -0,0 +1,13 @@ +from pydantic import BaseModel + + +class UsersBase(BaseModel): + username: str + + +class UsersCreate(UsersBase): + pass + + +class UsersRead(UsersBase): + id: int diff --git a/nginx/nginx.conf b/nginx/nginx.conf new file mode 100644 index 0000000..a7dae34 --- /dev/null +++ b/nginx/nginx.conf @@ -0,0 +1,90 @@ +user www-data; +worker_processes auto; +pid /run/nginx.pid; +include /etc/nginx/modules-enabled/*.conf; + +events { + worker_connections 1024; +} + +http { + + sendfile on; + tcp_nopush on; + types_hash_max_size 2048; + include /etc/nginx/mime.types; + default_type application/octet-stream; + + server { + listen 80; + server_name anakinnikita.ru; + location /.well-known/acme-challenge { + allow all; + root /var/www/html; + } + location / { + return 301 https://$host$request_uri; + } + + } + server { + listen 443 ssl; + server_name anakinnikita.ru; + + root /var/www/html; + + ssl_certificate /etc/letsencrypt/live/certfolder/fullchain.pem; + ssl_certificate_key /etc/letsencrypt/live/certfolder/privkey.pem; + + location / { + proxy_set_header Host $http_host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_pass http://fastapi_app:8000; + } + location /.well-known/acme-challenge { + allow all; + root /var/www/html; + } + location /robots.txt { + root /var/www/html; + } + } + server { + listen 80; + server_name denis.anakinnikita.ru; + location /.well-known/acme-challenge { + allow all; + root /var/www/html; + } + location / { + return 301 https://$host$request_uri; + } + + } + server { + listen 443 ssl; + server_name denis.anakinnikita.ru; + + root /var/www/html; + + ssl_certificate /etc/letsencrypt/live/denis/fullchain.pem; + ssl_certificate_key /etc/letsencrypt/live/denis/privkey.pem; + + location / { + proxy_set_header Host $http_host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_pass http://xls:8080; + } + location /.well-known/acme-challenge { + allow all; + root /var/www/html; + } + location /robots.txt { + root /var/www/html; + } + } +} diff --git a/nginx/robots.txt b/nginx/robots.txt new file mode 100644 index 0000000..77470cb --- /dev/null +++ b/nginx/robots.txt @@ -0,0 +1,2 @@ +User-agent: * +Disallow: / \ No newline at end of file diff --git a/tgbot_app/Dockerfile b/tgbot_app/Dockerfile new file mode 100644 index 0000000..fef5fc5 --- /dev/null +++ b/tgbot_app/Dockerfile @@ -0,0 +1,11 @@ +FROM python:3.11.4 + +COPY . /tg_bot + +WORKDIR /tg_bot + +EXPOSE 222 + +RUN pip3 install --no-cache-dir -r requirements.txt + +CMD ["python3", "main.py"] \ No newline at end of file diff --git a/tgbot_app/api/api_client.py b/tgbot_app/api/api_client.py new file mode 100644 index 0000000..9ea75b5 --- /dev/null +++ b/tgbot_app/api/api_client.py @@ -0,0 +1,42 @@ +import logging + +import aiohttp + + +class APIClient: + headers = headers = {"Content-Type": "application/json"} + + async def get_query(self, url, prompt=None): + async with aiohttp.ClientSession() as session: + try: + async with session.get( + url=url, + headers=self.headers, + json=prompt, + ssl=False + ) as response: + response.raise_for_status() + data = await response.json() + return data + except aiohttp.ClientError as e: + logging.error(e) + return 'Произошла ошибка' + + async def post_query(self, url, prompt): + async with aiohttp.ClientSession() as session: + try: + async with session.post( + url=url, + headers=self.headers, + json=prompt, + ssl=False + ) as response: + response.raise_for_status() + data = await response.text() + return data + except aiohttp.ClientError as e: + logging.error(e) + return 'Произошла ошибка' + + +api_client = APIClient() diff --git a/tgbot_app/config.py b/tgbot_app/config.py new file mode 100644 index 0000000..d6ad3da --- /dev/null +++ b/tgbot_app/config.py @@ -0,0 +1,11 @@ +from pydantic_settings import BaseSettings, SettingsConfigDict + + +class Settings(BaseSettings): + T_TOKEN: str + R_PASSWORD: str + + model_config = SettingsConfigDict(env_file='.env') + + +settings = Settings() diff --git a/tgbot_app/filters/chat_types.py b/tgbot_app/filters/chat_types.py new file mode 100644 index 0000000..b386c4c --- /dev/null +++ b/tgbot_app/filters/chat_types.py @@ -0,0 +1,10 @@ +from aiogram import types +from aiogram.filters import Filter + + +class ChatTypeFilter(Filter): + def __init__(self, chat_types: tuple[str]) -> None: + self.chat_types = chat_types + + async def __call__(self, message: types.Message) -> bool: + return message.chat.type in self.chat_types \ No newline at end of file diff --git a/tgbot_app/handlers/user_private.py b/tgbot_app/handlers/user_private.py new file mode 100644 index 0000000..4fd8375 --- /dev/null +++ b/tgbot_app/handlers/user_private.py @@ -0,0 +1,134 @@ +import logging + +from aiogram import F, Router, types +from aiogram.filters import CommandStart +from aiogram.filters.logic import or_f +from aiogram.fsm.context import FSMContext +from aiogram.fsm.state import State, StatesGroup + +from api.api_client import api_client +from filters.chat_types import ChatTypeFilter +from kb.reply_kb import ADD_KB, START_KB + +user_private_router = Router() +user_private_router.message.filter(ChatTypeFilter(('private'))) + + +class AddInfo(StatesGroup): + text = State() + + +@user_private_router.message(or_f(CommandStart(), (F.text == '↩️ Назад'))) +async def start(message: types.Message): + hello_msg = 'Выбери, чем хочешь заняться🐶' + if message.text == '/start': + hello_msg = (f'Добро пожаловать, *{message.from_user.full_name}*!\n\n' + 'Я тестовый *Бот*, ') + hello_msg + + await message.answer( + hello_msg, + reply_markup=START_KB, + ) + + +@user_private_router.message(F.text.lower() == 'добавить комментарий.') +async def create(message: types.Message, state: FSMContext): + await message.answer( + 'Введите информацию', + reply_markup=ADD_KB + ) + await state.set_state(AddInfo.text) + + +@user_private_router.message( + AddInfo.text, + F.text.lower() == 'выйти без сохранения' +) +async def post_comment_quit( + message: types.Message, + state: FSMContext, +): + await state.clear() + text = 'Вы вышли.' + await message.answer( + text, + reply_markup=START_KB + ) + + +@user_private_router.message(AddInfo.text, F.text) +async def post_comment( + message: types.Message, + state: FSMContext, +): + await state.set_data({ + 'comment': message.text, + 'username': message.from_user.username + }) + data = await state.get_data() + await state.clear() + logging.info(data) + response = await api_client.post_query( + url='https://my-nginx/api/comments-add/', + prompt=data + ) + text = 'Успешно' + if response == 'Произошла ошибка': + text = 'Ошибка' + await message.answer( + text, + reply_markup=START_KB + ) + + +@user_private_router.message(F.text.lower() == 'зарегистрироваться.') +async def post_user( + message: types.Message, +): + data = {'username': message.from_user.username} + response = await api_client.post_query( + url='https://my-nginx/api/users-add/', + prompt=data + ) + text = 'Успешно' + if response == 'Произошла ошибка': + text = 'Ошибка' + await message.answer(text) + + +@user_private_router.message( + F.text.lower() == 'посмотреть всех пользователей.' +) +async def get_users( + message: types.Message, +): + response = await api_client.get_query( + url='https://my-nginx/api/users/', + ) + text = '' + if isinstance(response, str): + text = 'Пусто' + await message.answer(text) + return + for elem in response: + text += f'{elem.get("id")}. *{elem.get("username")}*\n' + logging.debug(text) + await message.answer(text) + + +@user_private_router.message(F.text.lower() == 'посмотреть все комментарии.') +async def get_comments( + message: types.Message, +): + response = await api_client.get_query( + url='https://my-nginx/api/comments/', + ) + text = '' + if isinstance(response, str): + text = 'Пусто' + await message.answer(text) + return + for elem in response: + text += f'*{elem.get("username")}*:\n{elem.get("comment")}\n' + logging.debug(text) + await message.answer(text) diff --git a/tgbot_app/kb/reply_kb.py b/tgbot_app/kb/reply_kb.py new file mode 100644 index 0000000..7ca7d2a --- /dev/null +++ b/tgbot_app/kb/reply_kb.py @@ -0,0 +1,36 @@ +from aiogram.types import KeyboardButton +from aiogram.utils.keyboard import ReplyKeyboardBuilder + + +def get_kb( + *buttons: str, + placeholder: str | None = None, + request_contact: int | None = None, + request_location: int | None = None, + sizes: tuple[int] = (2,) + ): + keyboard = ReplyKeyboardBuilder() + for idx, text in enumerate(buttons): + if request_contact and request_contact == idx + 1: + keyboard.add(KeyboardButton(text=text, request_contact=True)) + elif request_location and request_location == idx + 1: + keyboard.add(KeyboardButton(text=text, request_location=True)) + else: + keyboard.add(KeyboardButton(text=text)) + return keyboard.adjust(*sizes).as_markup( + resize_keyboard=True, input_field_placeholder=placeholder + ) + + +START_KB = get_kb( + 'Зарегистрироваться.', + 'Добавить комментарий.', + 'Посмотреть всех пользователей.', + 'Посмотреть все комментарии.', + placeholder='Что вас интересует?', + sizes=(1,) +) + +ADD_KB = get_kb( + 'Выйти без сохранения' +) diff --git a/tgbot_app/main.py b/tgbot_app/main.py new file mode 100644 index 0000000..682b776 --- /dev/null +++ b/tgbot_app/main.py @@ -0,0 +1,56 @@ +import asyncio +import logging + +from aiogram import Bot, Dispatcher, types +from aiogram.client.bot import DefaultBotProperties +from aiogram.enums import ParseMode +from aiogram.fsm.storage.redis import DefaultKeyBuilder, RedisStorage +from aiogram.fsm.strategy import FSMStrategy + +from config import settings +from handlers.user_private import user_private_router + +BOT = Bot( + token=settings.T_TOKEN, + default=DefaultBotProperties(parse_mode=ParseMode.MARKDOWN) +) + +REDIS_URL_1 = f'redis://:{settings.R_PASSWORD}@redis:6379/0?decode_responses=True&protocol=3' + +redis_fsm_storage = RedisStorage.from_url( + url=REDIS_URL_1, + key_builder=DefaultKeyBuilder( + with_bot_id=True, + with_destiny=True + ) +) + +logging.basicConfig( + level=logging.INFO, + filename='bot_log.log', + format="%(asctime)s %(levelname)s %(message)s" +) + +ALLOWED_UPDATES = ['message', 'edited_message', 'callback_query'] + +dp = Dispatcher( + fsm_strategy=FSMStrategy.GLOBAL_USER, + storage=redis_fsm_storage +) + +dp.include_router(user_private_router) + + +@dp.shutdown() +async def on_shutdown(): + """Закрытие сессии redis перед выключением бота""" + await redis_fsm_storage.close() + + +async def main(): + await BOT.delete_webhook(drop_pending_updates=True) + await dp.start_polling(BOT, allowed_updates=ALLOWED_UPDATES) + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/tgbot_app/requirements.txt b/tgbot_app/requirements.txt new file mode 100644 index 0000000..eb632f1 --- /dev/null +++ b/tgbot_app/requirements.txt @@ -0,0 +1,3 @@ +aiogram==3.6.0 +pydantic-settings==2.3.4 +redis==5.0.7 \ No newline at end of file