Skip to content

Commit

Permalink
CCS-98955 refactor nfs project
Browse files Browse the repository at this point in the history
  • Loading branch information
Ivan Kudinov committed Jun 11, 2024
1 parent 49e18f4 commit 0e609f0
Show file tree
Hide file tree
Showing 165 changed files with 48,524 additions and 36 deletions.
49 changes: 27 additions & 22 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,39 +1,44 @@
# next-feature-smartapp

Бот для тестирования функционала BotX
## Отладка Backend

### Установка
#### Конфигурирование
Необходимо скопировать `./backend/example.env -> ./backend/.env`. Затем заполнить переменную BOT_CREDENTIALS в файле `./backend/.env` в формате `<host>@<secret_key>@<bot_id>`.

1. Воспользуйтесь инструкцией [Руководство администратора](https://express.ms/admin_guide.pdf)
`-> Эксплуатация корпоративного сервера -> Управление контактами -> Чат-боты`
2. Создайте бота в панели администратора eXpress.
3. Получите `secret_key` (Секретный ключ) и `bot_id` (ID) нажав на имя созданного бота.
4. Получите `cts_host` имя хоста, на котором находится панель администратора.
5. Установите флаг "Включено" и заполните имя для SmartApp.

6. Скачайте репозиторий на сервер:
Для получения параметров `secret_key` и `bot_id` необходимо создать бота в панели администратора (см. *[Руководство администратора](https://express.ms/admin_guide.pdf) -> Эксплуатация корпоративного сервера -> Управление контактами -> Чат-боты*).

#### Запуск в режиме отладки
```bash
git clone --recursive https://github.com/ExpressApp/next-feature-smartapp
docker-compose up -d
```

флаг `--recursive` нужен для клонирования подмодуля botx-smartapp-rpc
## Отладка Frontend

7. Заполните переменные окружения в `docker-compose.yml` реальными значениями:
#### Установка зависимостей
```
cd frontend
npm i
```

#### Запуск в режиме отладки
```
- BOT_CREDENTIALS=
./scripts/run-front
```

8. Запустите контейнеры командой:
## Деплой на сервер

```bash
docker-compose up -d
#### Сборка контейнера
```
9. Убедитесь, что в логах нет ошибок.

```bash
docker-compose logs
./scripts/build
```

10. Выберите текущий SmartApp через меню SmartApp, проверьте что он открылся.
#### Запуск
```
docker run -d \
-e BOT_CREDENTIALS="<creds>" \
-e DEBUG=True \
<tag>
```
гдe
`<tag>` - название тега, собранного на предыдущем шаге.
`<creds>` - учетные данные бота в формате `cts_host@secret_key@bot_id`
1 change: 1 addition & 0 deletions backend/.dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
**/__pycache__
110 changes: 110 additions & 0 deletions backend/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
# 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/
*.egg-info/
.installed.cfg
*.egg
MANIFEST

# 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/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
.hypothesis/
.pytest_cache/

# Translations
*.mo
*.pot

# Django stuff:
*.log
local_settings.py
db.sqlite3

# Flask stuff:
instance/
.webassets-cache

# Scrapy stuff:
.scrapy

# Sphinx documentation
docs/_build/

# PyBuilder
target/

# Jupyter Notebook
.ipynb_checkpoints

# pyenv
.python-version

# celery beat schedule file
celerybeat-schedule

# 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/

.idea/
.vscode/

# frontend build
app/resources/static
42 changes: 42 additions & 0 deletions backend/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
FROM python:3.10-slim

ENV PYTHONUNBUFFERED 1
ENV UVICORN_CMD_ARGS ""

ARG NON_ROOT_USER=express_bot
RUN useradd --create-home ${NON_ROOT_USER}

EXPOSE 8000
WORKDIR /app

COPY poetry.lock pyproject.toml ./

ARG CI_JOB_TOKEN=""
ARG GIT_HOST=""
ARG GIT_PASSWORD=${CI_JOB_TOKEN}
ARG GIT_LOGIN="gitlab-ci-token"

RUN apt-get update && \
apt-get install -y sudo git build-essential && \
echo "${NON_ROOT_USER} ALL = NOPASSWD: /usr/sbin/update-ca-certificates" > /etc/sudoers.d/express_bot && \
apt-get clean autoclean && \
apt-get autoremove --yes && \
rm -rf /var/lib/{apt,dpkg,cache,log}/

# Poetry can't read password to download private repos
RUN echo -e "machine ${GIT_HOST}\nlogin ${GIT_LOGIN}\npassword ${GIT_PASSWORD}" > ~/.netrc && \
pip install poetry==1.5.1 --no-cache-dir && \
poetry config virtualenvs.create false && \
poetry install --no-dev && \
rm -rf /root/.cache/pypoetry && \
rm -rf ~/.netrc

COPY app app

ARG CI_COMMIT_SHA=""
ENV GIT_COMMIT_SHA=${CI_COMMIT_SHA}

USER ${NON_ROOT_USER}

CMD sudo update-ca-certificates && \
uvicorn --host=0.0.0.0 $UVICORN_CMD_ARGS app.main:app
Empty file added backend/app/__init__.py
Empty file.
Empty file added backend/app/api/__init__.py
Empty file.
Empty file.
13 changes: 13 additions & 0 deletions backend/app/api/dependencies/bot.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
"""Bot dependency for FastAPI."""

from fastapi import Depends, Request
from pybotx import Bot


def get_bot(request: Request) -> Bot:
assert isinstance(request.app.state.bot, Bot)

return request.app.state.bot


bot_dependency = Depends(get_bot)
Empty file.
132 changes: 132 additions & 0 deletions backend/app/api/endpoints/botx.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
"""Endpoints for communication with botx."""

from http import HTTPStatus

from fastapi import APIRouter, Request
from fastapi.responses import JSONResponse
from pybotx import (
Bot,
BotXMethodCallbackNotFoundError,
UnknownBotAccountError,
UnsupportedBotAPIVersionError,
UnverifiedRequestError,
build_bot_disabled_response,
build_command_accepted_response,
build_unverified_request_response,
)
from pybotx.constants import BOT_API_VERSION

from app.api.dependencies.bot import bot_dependency
from app.logger import logger
from app.settings import settings

router = APIRouter()


@router.post("/command")
async def command_handler(request: Request, bot: Bot = bot_dependency) -> JSONResponse:
"""Receive commands from users. Max timeout - 5 seconds."""
try: # noqa: WPS225
bot.async_execute_raw_bot_command(
await request.json(),
request_headers=request.headers,
)
except ValueError:
error_label = "Bot command validation error"

if settings.DEBUG:
logger.exception(error_label)
else:
logger.warning(error_label)

return JSONResponse(
build_bot_disabled_response(error_label),
status_code=HTTPStatus.SERVICE_UNAVAILABLE,
)
except UnknownBotAccountError as exc:
error_label = f"No credentials for bot {exc.bot_id}"
logger.warning(error_label)

return JSONResponse(
build_bot_disabled_response(error_label),
status_code=HTTPStatus.SERVICE_UNAVAILABLE,
)
except UnsupportedBotAPIVersionError as exc:
error_label = (
f"Unsupported Bot API version: `{exc.version}`. "
f"Set protocol version to `{BOT_API_VERSION}` in Admin panel."
)
logger.warning(error_label)

return JSONResponse(
build_bot_disabled_response(error_label),
status_code=HTTPStatus.SERVICE_UNAVAILABLE,
)
except UnverifiedRequestError as exc:
logger.warning(f"UnverifiedRequestError: {exc.args[0]}")
return JSONResponse(
content=build_unverified_request_response(
status_message=exc.args[0],
),
status_code=HTTPStatus.UNAUTHORIZED,
)

return JSONResponse(
build_command_accepted_response(), status_code=HTTPStatus.ACCEPTED
)


@router.get("/status")
async def status_handler(request: Request, bot: Bot = bot_dependency) -> JSONResponse:
"""Show bot status and commands list."""
try:
status = await bot.raw_get_status(
dict(request.query_params),
request_headers=request.headers,
)
except UnknownBotAccountError as exc:
error_label = f"Unknown bot_id: {exc.bot_id}"
logger.warning(exc)
return JSONResponse(
build_bot_disabled_response(error_label),
status_code=HTTPStatus.SERVICE_UNAVAILABLE,
)
except ValueError:
error_label = "Invalid params"
logger.warning(error_label)
return JSONResponse(
build_bot_disabled_response(error_label), status_code=HTTPStatus.BAD_REQUEST
)
except UnverifiedRequestError as exc:
logger.warning(f"UnverifiedRequestError: {exc.args[0]}")
return JSONResponse(
content=build_unverified_request_response(
status_message=exc.args[0],
),
status_code=HTTPStatus.UNAUTHORIZED,
)

return JSONResponse(status)


@router.post("/notification/callback")
async def callback_handler(request: Request, bot: Bot = bot_dependency) -> JSONResponse:
"""Process BotX methods callbacks."""
try:
await bot.set_raw_botx_method_result(
await request.json(),
verify_request=False,
)
except BotXMethodCallbackNotFoundError as exc:
error_label = f"Unexpected callback with sync_id: {exc.sync_id}"
logger.warning(error_label)

return JSONResponse(
build_bot_disabled_response(error_label),
status_code=HTTPStatus.SERVICE_UNAVAILABLE,
)

return JSONResponse(
build_command_accepted_response(),
status_code=HTTPStatus.ACCEPTED,
)
8 changes: 8 additions & 0 deletions backend/app/api/routers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
"""Configuration of routers for all endpoints."""
from fastapi import APIRouter

from app.api.endpoints.botx import router as bot_router

router = APIRouter()

router.include_router(bot_router, tags=["botx"])
Empty file added backend/app/bot/__init__.py
Empty file.
Loading

0 comments on commit 0e609f0

Please sign in to comment.