MAX bot + n8n: webhook, нормализация, меню, доки, схемы БД
- register_max_webhook.py, fetch_schema.py - n8n-code-node-max-normalize.js (max_id, callback из callback.user, contact из vcf_info) - n8n-code-add-menu-buttons.js (меню с callback, request_contact, Главное меню) - docs: max-webhook, max-curl-http-request, max-api (форматы, кнопки, контакт), clpr vs sprf - README, SITUATION, схемы sprf_ и clpr_, .gitignore Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
7
.gitignore
vendored
Normal file
7
.gitignore
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
.env
|
||||
*.pyc
|
||||
__pycache__/
|
||||
.venv/
|
||||
venv/
|
||||
*.log
|
||||
.DS_Store
|
||||
72
README.md
Normal file
72
README.md
Normal file
@@ -0,0 +1,72 @@
|
||||
# MAX Bot + n8n (СПРФ / Клиент)
|
||||
|
||||
Интеграция бота в мессенджере **MAX** с **n8n**: webhook, нормализация входящих, отправка сообщений и кнопок, работа с БД (PostgreSQL, схемы sprf_ / clpr_).
|
||||
|
||||
## Содержимое репозитория
|
||||
|
||||
### Скрипты
|
||||
|
||||
| Файл | Назначение |
|
||||
|------|------------|
|
||||
| **register_max_webhook.py** | Регистрация webhook бота MAX на URL n8n (читает .env: MAX_BOT_TOKEN, N8N_MAX_WORKFLOW, MAX_WEBHOOK_SECRET). |
|
||||
| **fetch_schema.py** | Выгрузка структуры таблиц `sprf_*` из PostgreSQL в `sprf_tables_schema.md`. |
|
||||
|
||||
### Ноды для n8n (Code node)
|
||||
|
||||
| Файл | Назначение |
|
||||
|------|------------|
|
||||
| **n8n-code-node-max-normalize.js** | Нормализация входящего Webhook MAX: один объект с `max_id`, `max_chat_id`, `answer_text`, `answer_type` (text, command, callback, contact, voice, photo, file и т.д.), `callback_id`, `callback_message_text`, `contact_payload` и др. Личные чаты (dialog); при callback пользователь берётся из `callback.user`. |
|
||||
| **n8n-code-add-menu-buttons.js** | Формирование тела сообщения с меню: текст + inline_keyboard (callback-кнопки, request_contact, кнопка «Главное меню» type message). Выход: `message_body` для POST /messages. |
|
||||
|
||||
### Документация
|
||||
|
||||
| Путь | Описание |
|
||||
|------|----------|
|
||||
| **docs/max-webhook.md** | Настройка Webhook в n8n, регистрация в MAX, отправка ответа (POST /messages), ответ на callback (POST /answers), удаление кнопок. |
|
||||
| **docs/max-curl-http-request.md** | Примеры curl и настройка HTTP Request в n8n: отправка сообщения, кнопки, ответ на callback, удаление кнопок. |
|
||||
| **docs/max-api/** | Локальная копия/выжимка MAX Bot API: обзор, методы (messages, updates, subscriptions, answers), объекты (Update, Message, MessageBody, NewMessageBody), форматы текста (markdown/html), кнопки (inline_keyboard: callback, message, link, request_contact и др.), контакт (vcf_info, max_info). |
|
||||
| **docs/clpr-vs-sprf-schema-diff.md** | Сравнение структуры таблиц БД с префиксами clpr_ и sprf_. |
|
||||
| **SITUATION.md** | Текущая ситуация: что настроено, команды, файлы. |
|
||||
|
||||
### Схемы БД
|
||||
|
||||
| Файл | Описание |
|
||||
|------|----------|
|
||||
| **sprf_tables_schema.md** | Структура таблиц с префиксом `sprf_` (public). |
|
||||
| **clpr_tables_schema.md** | Структура таблиц с префиксом `clpr_` (public). |
|
||||
|
||||
## Требования
|
||||
|
||||
- Python 3, зависимости: `psycopg2-binary` (для fetch_schema).
|
||||
- В корне файл **.env** (не коммитить): `MAX_BOT_TOKEN`, `N8N_MAX_WORKFLOW`, `MAX_WEBHOOK_SECRET`, при необходимости `MAX_API_BASE`; для выгрузки схемы: `PGHOST`, `PGPORT`, `PGDATABASE`, `PGUSER`, `PGPASSWORD`.
|
||||
|
||||
## Быстрый старт
|
||||
|
||||
1. Настроить Webhook в n8n (path = `sprf_max`, POST), включить воркфлоу.
|
||||
2. Выполнить: `python3 register_max_webhook.py`.
|
||||
3. В воркфлоу после Webhook вставить Code node с содержимым `n8n-code-node-max-normalize.js`.
|
||||
4. Ответ пользователю: HTTP Request — POST `https://platform-api.max.ru/messages?user_id={{ $json.max_id }}`, body из `message_body` или свой JSON (текст, кнопки — см. docs).
|
||||
|
||||
Подробнее: **docs/max-webhook.md**, **docs/max-curl-http-request.md**, **docs/max-api/04-formats-and-buttons.md**.
|
||||
|
||||
## Ограничения MAX API
|
||||
|
||||
- Редактирование (PUT /messages) и удаление (DELETE /messages) — только для сообщений **моложе 24 часов**.
|
||||
- POST /answers (обновление сообщения с кнопками) — по факту тоже редактирование; при старше 24 ч может не сработать.
|
||||
|
||||
## Git
|
||||
|
||||
Репозиторий инициализирован, первый коммит на ветке `main`. Remote: `origin` → Gitea (при необходимости измените URL).
|
||||
|
||||
Чтобы отправить код на сервер (после создания репозитория MAX в Gitea при необходимости):
|
||||
|
||||
```bash
|
||||
cd /dev/MAX
|
||||
git push -u origin main
|
||||
```
|
||||
|
||||
Логин/пароль Gitea запросит при первом push.
|
||||
|
||||
## Лицензия / конфиденциальность
|
||||
|
||||
Скрипты и доки — для внутреннего использования. Не коммитить .env и токены.
|
||||
60
SITUATION.md
Normal file
60
SITUATION.md
Normal file
@@ -0,0 +1,60 @@
|
||||
# Текущая ситуация — MAX / СПРФ (актуализировано 13.02.2025)
|
||||
|
||||
## Что это за проект
|
||||
|
||||
- **Бот MAX** (мессенджер) получает события по **Webhook** на n8n.
|
||||
- n8n обрабатывает сообщения и коллбэки, при необходимости ходит в **PostgreSQL** (таблицы `sprf_*`) и в **MAX API** (ответы, кнопки).
|
||||
- Документация MAX API и инструкции по webhook лежат в `docs/`.
|
||||
|
||||
## Что уже настроено и работает
|
||||
|
||||
| Компонент | Статус |
|
||||
|-----------|--------|
|
||||
| **.env** | Есть: `MAX_BOT_TOKEN`, `N8N_MAX_WORKFLOW`, `MAX_WEBHOOK_SECRET`, параметры PostgreSQL |
|
||||
| **Webhook в MAX** | Зарегистрирован на `https://n8n.clientright.pro/webhook/sprf_max` (события: `message_created`, `message_callback`, `bot_started`) |
|
||||
| **Секрет** | Задан в .env; в n8n проверяй заголовок `X-Max-Bot-Api-Secret` |
|
||||
| **Схема БД** | Выгружена в `sprf_tables_schema.md` (таблицы sprf_claims, sprf_chat_messages, sprf_conversation_state и др.) |
|
||||
|
||||
## Что нужно проверить вручную
|
||||
|
||||
1. **n8n** (https://n8n.clientright.pro): воркфлоу с нодой **Webhook** включён (Production), путь = `sprf_max`, метод POST.
|
||||
2. **Проверка доставки**: написать боту в MAX — во входящих данных Webhook в n8n должен появиться объект с `update_type`, `message` и т.д.
|
||||
|
||||
## Полезные команды
|
||||
|
||||
```bash
|
||||
# Заново зарегистрировать webhook (если меняли URL или пересоздавали воркфлоу)
|
||||
python3 register_max_webhook.py
|
||||
|
||||
# Проверить, какие подписки зарегистрированы в MAX
|
||||
python3 -c "
|
||||
import os, json, urllib.request
|
||||
from pathlib import Path
|
||||
for line in Path('.env').read_text().splitlines():
|
||||
s = line.strip()
|
||||
if s and not s.startswith('#') and '=' in s:
|
||||
k, v = s.split('=', 1)
|
||||
os.environ[k.strip()] = v.strip()
|
||||
r = urllib.request.urlopen(urllib.request.Request(
|
||||
os.environ.get('MAX_API_BASE','https://platform-api.max.ru').rstrip('/') + '/subscriptions',
|
||||
headers={'Authorization': os.environ['MAX_BOT_TOKEN']}, method='GET'))
|
||||
print(r.read().decode())
|
||||
"
|
||||
|
||||
# Обновить схему таблиц sprf_ из PostgreSQL (нужны PGHOST, PGUSER, PGPASSWORD, PGDATABASE в .env)
|
||||
python3 fetch_schema.py
|
||||
```
|
||||
|
||||
## Файлы в проекте
|
||||
|
||||
- `register_max_webhook.py` — регистрация webhook в MAX
|
||||
- `fetch_schema.py` — выгрузка схемы таблиц sprf_ в `sprf_tables_schema.md`
|
||||
- `docs/max-webhook.md` — пошаговая настройка webhook в n8n и в MAX
|
||||
- `docs/max-api/` — обзор API, методы, объекты (01–03)
|
||||
- `sprf_tables_schema.md` — структура таблиц БД
|
||||
- `.env` — токены и URL (не коммитить)
|
||||
|
||||
## Дальше
|
||||
|
||||
- Дорабатывать воркфлоу в n8n под логику бота (ответы, кнопки, запись в БД).
|
||||
- При смене URL webhook — обновить `N8N_MAX_WORKFLOW` в .env и снова запустить `python3 register_max_webhook.py`.
|
||||
191
clpr_tables_schema.md
Normal file
191
clpr_tables_schema.md
Normal file
@@ -0,0 +1,191 @@
|
||||
# Структура таблиц clpr_ (public, default_db)
|
||||
## clpr_chat_messages
|
||||
| Колонка | Тип | Размер | NULL | Default |
|
||||
|---------|-----|--------|------|--------|
|
||||
| id | integer | | NO | nextval('clpr_chat_messages_id_seq'::reg... |
|
||||
| claim_id | uuid | | YES | |
|
||||
| from_user | boolean | | YES | |
|
||||
| message_text | text | | YES | |
|
||||
| file_id | text | | YES | |
|
||||
| sent_at | timestamp with time zone | | YES | now() |
|
||||
|
||||
## clpr_claim_documents
|
||||
| Колонка | Тип | Размер | NULL | Default |
|
||||
|---------|-----|--------|------|--------|
|
||||
| id | uuid | | NO | gen_random_uuid() |
|
||||
| claim_id | character varying | | YES | |
|
||||
| field_name | text | | YES | |
|
||||
| file_id | text | | YES | |
|
||||
| uploaded_at | timestamp with time zone | | YES | now() |
|
||||
| file_name | text | | YES | |
|
||||
| original_file_name | text | | YES | |
|
||||
| file_hash | character varying | 64 | YES | |
|
||||
| ocr_status | character varying | 20 | YES | 'pending'::character varying |
|
||||
| ocr_processed_at | timestamp with time zone | | YES | |
|
||||
| ocr_error | text | | YES | |
|
||||
| document_type | character varying | 50 | YES | |
|
||||
| document_label | character varying | 255 | YES | |
|
||||
| match_score | integer | | YES | |
|
||||
| match_status | character varying | 20 | YES | 'pending'::character varying |
|
||||
| match_reason | text | | YES | |
|
||||
| match_checked_at | timestamp without time zone | | YES | |
|
||||
| document_summary | text | | YES | |
|
||||
|
||||
## clpr_claim_statuses
|
||||
| Колонка | Тип | Размер | NULL | Default |
|
||||
|---------|-----|--------|------|--------|
|
||||
| code | text | | NO | |
|
||||
| description | text | | YES | |
|
||||
|
||||
## clpr_claim_types
|
||||
| Колонка | Тип | Размер | NULL | Default |
|
||||
|---------|-----|--------|------|--------|
|
||||
| code | text | | NO | |
|
||||
| description | text | | YES | |
|
||||
|
||||
## clpr_claims
|
||||
| Колонка | Тип | Размер | NULL | Default |
|
||||
|---------|-----|--------|------|--------|
|
||||
| id | uuid | | NO | gen_random_uuid() |
|
||||
| session_token | character varying | | YES | |
|
||||
| unified_id | character varying | | YES | |
|
||||
| telegram_id | bigint | | YES | |
|
||||
| channel | text | | YES | |
|
||||
| user_id | integer | | YES | |
|
||||
| type_code | text | | YES | |
|
||||
| status_code | text | | YES | |
|
||||
| policy_number | text | | YES | |
|
||||
| payload | jsonb | | YES | |
|
||||
| is_confirmed | boolean | | YES | false |
|
||||
| created_at | timestamp with time zone | | YES | now() |
|
||||
| updated_at | timestamp with time zone | | YES | now() |
|
||||
| expires_at | timestamp with time zone | | YES | |
|
||||
| contact_id | text | | YES | |
|
||||
| phone | text | | YES | |
|
||||
|
||||
## clpr_conversation_state
|
||||
| Колонка | Тип | Размер | NULL | Default |
|
||||
|---------|-----|--------|------|--------|
|
||||
| user_id | integer | | NO | |
|
||||
| current_step | text | | YES | |
|
||||
| data | jsonb | | YES | |
|
||||
| updated_at | timestamp with time zone | | YES | |
|
||||
|
||||
## clpr_dialog_history_tg
|
||||
| Колонка | Тип | Размер | NULL | Default |
|
||||
|---------|-----|--------|------|--------|
|
||||
| id | integer | | NO | nextval('clpr_dialog_history_tg_id_seq':... |
|
||||
| telegram_id | bigint | | YES | |
|
||||
| role | character varying | | YES | |
|
||||
| message | text | | YES | |
|
||||
| created_at | timestamp with time zone | | YES | now() |
|
||||
| session_token | character varying | | YES | |
|
||||
| claim_id | uuid | | YES | |
|
||||
| message_type | text | | YES | |
|
||||
| payload | jsonb | | YES | |
|
||||
| tg_message_id | bigint | | YES | |
|
||||
| tg_update_id | bigint | | YES | |
|
||||
|
||||
## clpr_document_embeddings
|
||||
| Колонка | Тип | Размер | NULL | Default |
|
||||
|---------|-----|--------|------|--------|
|
||||
| id | uuid | | YES | |
|
||||
| embedding | USER-DEFINED | | YES | |
|
||||
| text | text | | YES | |
|
||||
| metadata | jsonb | | YES | |
|
||||
|
||||
## clpr_documents
|
||||
| Колонка | Тип | Размер | NULL | Default |
|
||||
|---------|-----|--------|------|--------|
|
||||
| id | uuid | | NO | gen_random_uuid() |
|
||||
| source | text | | YES | |
|
||||
| content | text | | YES | |
|
||||
| metadata | jsonb | | YES | |
|
||||
| created_at | timestamp with time zone | | YES | now() |
|
||||
|
||||
## clpr_menu_commands
|
||||
| Колонка | Тип | Размер | NULL | Default |
|
||||
|---------|-----|--------|------|--------|
|
||||
| id | bigint | | NO | nextval('clpr_menu_commands_id_seq'::reg... |
|
||||
| command | text | | NO | |
|
||||
| description | text | | YES | |
|
||||
| action | text | | YES | |
|
||||
| reply_text | text | | YES | |
|
||||
| menu_id | text | | NO | 'main'::text |
|
||||
| menu_version | integer | | NO | 1 |
|
||||
| created_at | timestamp with time zone | | NO | now() |
|
||||
| updated_at | timestamp with time zone | | YES | |
|
||||
|
||||
## clpr_operators
|
||||
| Колонка | Тип | Размер | NULL | Default |
|
||||
|---------|-----|--------|------|--------|
|
||||
| id | integer | | NO | nextval('clpr_operators_id_seq'::regclas... |
|
||||
| telegram_id | bigint | | YES | |
|
||||
| name | text | | YES | |
|
||||
| is_active | boolean | | YES | |
|
||||
| created_at | timestamp with time zone | | YES | now() |
|
||||
|
||||
## clpr_sessions
|
||||
| Колонка | Тип | Размер | NULL | Default |
|
||||
|---------|-----|--------|------|--------|
|
||||
| id | uuid | | NO | gen_random_uuid() |
|
||||
| user_id | integer | | YES | |
|
||||
| session_token | character varying | | YES | |
|
||||
| created_at | timestamp with time zone | | YES | now() |
|
||||
| last_activity | timestamp with time zone | | YES | |
|
||||
| expires_at | timestamp with time zone | | YES | |
|
||||
|
||||
## clpr_user_accounts
|
||||
| Колонка | Тип | Размер | NULL | Default |
|
||||
|---------|-----|--------|------|--------|
|
||||
| id | integer | | NO | nextval('clpr_user_accounts_id_seq'::reg... |
|
||||
| user_id | integer | | YES | |
|
||||
| channel | text | | YES | |
|
||||
| channel_user_id | text | | YES | |
|
||||
|
||||
## clpr_users
|
||||
| Колонка | Тип | Размер | NULL | Default |
|
||||
|---------|-----|--------|------|--------|
|
||||
| id | integer | | NO | nextval('clpr_users_id_seq'::regclass) |
|
||||
| universal_id | uuid | | YES | |
|
||||
| unified_id | character varying | | YES | |
|
||||
| phone | character varying | | YES | |
|
||||
| created_at | timestamp with time zone | | YES | now() |
|
||||
| updated_at | timestamp with time zone | | YES | now() |
|
||||
| contact_data_confirmed_at | timestamp with time zone | | YES | |
|
||||
|
||||
## clpr_users_tg
|
||||
| Колонка | Тип | Размер | NULL | Default |
|
||||
|---------|-----|--------|------|--------|
|
||||
| telegram_id | bigint | | NO | |
|
||||
| phone_number | character varying | | YES | |
|
||||
| first_name_tg | character varying | | YES | |
|
||||
| last_name_tg | character varying | | YES | |
|
||||
| username | character varying | | YES | |
|
||||
| language_code | character varying | | YES | |
|
||||
| is_premium | boolean | | YES | |
|
||||
| unified_id | character varying | | YES | |
|
||||
| first_name | character varying | | YES | |
|
||||
| last_name | character varying | | YES | |
|
||||
| middle_name | character varying | | YES | |
|
||||
| birth_date | character varying | | YES | |
|
||||
| birth_place | character varying | | YES | |
|
||||
| inn | character varying | | YES | |
|
||||
| address | character varying | | YES | |
|
||||
| email | character varying | | YES | |
|
||||
| is_confirmed | boolean | | YES | false |
|
||||
| created_at | timestamp with time zone | | YES | now() |
|
||||
| updated_at | timestamp with time zone | | YES | now() |
|
||||
|
||||
## clpr_wizard_questions
|
||||
| Колонка | Тип | Размер | NULL | Default |
|
||||
|---------|-----|--------|------|--------|
|
||||
| id | integer | | NO | nextval('clpr_wizard_questions_id_seq'::... |
|
||||
| claim_type | text | | YES | |
|
||||
| step_key | text | | YES | |
|
||||
| question_text | text | | YES | |
|
||||
| answer_type | text | | YES | |
|
||||
| step_order | integer | | YES | |
|
||||
| options | jsonb | | YES | |
|
||||
| is_required | boolean | | YES | |
|
||||
|
||||
354
docs/clpr-vs-sprf-schema-diff.md
Normal file
354
docs/clpr-vs-sprf-schema-diff.md
Normal file
@@ -0,0 +1,354 @@
|
||||
# Сравнение структуры таблиц clpr_ и sprf_
|
||||
|
||||
## 1. Состав: что только в одной схеме
|
||||
|
||||
| Только в **sprf_** | Только в **clpr_** |
|
||||
|-------------------|--------------------|
|
||||
| sprf_court_decisions (файлы судебных решений, OCR, вектор, nsfw, CRM) | clpr_menu_commands (команды меню: command, action, reply_text, menu_id) |
|
||||
| sprf_court_decisions_view (view поверх court_decisions) | — |
|
||||
|
||||
Остальные таблицы есть в обеих схемах (с разными именами префикса).
|
||||
|
||||
---
|
||||
|
||||
## 2. Общие таблицы — отличия по колонкам и типам
|
||||
|
||||
### chat_messages
|
||||
| Аспект | sprf | clpr |
|
||||
|--------|------|------|
|
||||
| sent_at | без default | default `now()` |
|
||||
|
||||
Остальные колонки совпадают.
|
||||
|
||||
---
|
||||
|
||||
### claim_documents
|
||||
| Аспект | sprf | clpr |
|
||||
|--------|------|------|
|
||||
| Колонки | Базовый набор: id, claim_id, field_name, file_id, uploaded_at | Расширенный: + file_name, original_file_name, file_hash, ocr_status, ocr_processed_at, ocr_error, document_type, document_label, match_score, match_status, match_reason, match_checked_at, document_summary |
|
||||
| id | uuid, NO, без default в схеме | uuid, NO, gen_random_uuid() |
|
||||
| uploaded_at | без default | default now() |
|
||||
|
||||
**Итог:** в clpr заложена полноценная модель документов с OCR, матчингом и саммари; в sprf — минимальный набор под «файл к заявке».
|
||||
|
||||
---
|
||||
|
||||
### claim_statuses, claim_types
|
||||
Структура совпадает (code, description).
|
||||
|
||||
---
|
||||
|
||||
### claims
|
||||
| Аспект | sprf | clpr |
|
||||
|--------|------|------|
|
||||
| id | character varying, NO | uuid, NO, gen_random_uuid() |
|
||||
| Доп. поля | — | expires_at, contact_id, phone |
|
||||
| created_at / updated_at | без default в схеме | default now() |
|
||||
|
||||
Остальные поля (user_id, type_code, status_code, payload, session_token, unified_id, telegram_id, channel, is_confirmed) совпадают.
|
||||
|
||||
---
|
||||
|
||||
### conversation_state
|
||||
Структура совпадает (user_id, current_step, data, updated_at).
|
||||
|
||||
---
|
||||
|
||||
### dialog_history_tg
|
||||
| Аспект | sprf | clpr |
|
||||
|--------|------|------|
|
||||
| claim_id | character varying | uuid |
|
||||
| created_at | default now() | default now() |
|
||||
|
||||
Остальные колонки те же.
|
||||
|
||||
---
|
||||
|
||||
### document_embeddings
|
||||
| Аспект | sprf | clpr |
|
||||
|--------|------|------|
|
||||
| Модель | Чанки документа: document_id, chunk_index, embedding | Один объект на запись: id, embedding, text, metadata |
|
||||
| Колонки | document_id, chunk_index, embedding | id, embedding, text, metadata |
|
||||
|
||||
Разная семантика: sprf — эмбеддинги чанков с привязкой к документу; clpr — эмбеддинг + текст + метаданные без явного document_id/chunk_index.
|
||||
|
||||
---
|
||||
|
||||
### documents
|
||||
| Аспект | sprf | clpr |
|
||||
|--------|------|------|
|
||||
| id | uuid, NO, без default в схеме | uuid, NO, gen_random_uuid() |
|
||||
| created_at | без default | default now() |
|
||||
|
||||
Остальное совпадает (source, content, metadata).
|
||||
|
||||
---
|
||||
|
||||
### operators
|
||||
Структура совпадает. В clpr у created_at указан default now(), в sprf в схеме default не показан.
|
||||
|
||||
---
|
||||
|
||||
### sessions
|
||||
Структура совпадает (id uuid, user_id, session_token, created_at, last_activity, expires_at). В clpr created_at = now().
|
||||
|
||||
---
|
||||
|
||||
### user_accounts
|
||||
Структура совпадает (user_id, channel, channel_user_id).
|
||||
|
||||
---
|
||||
|
||||
### users
|
||||
| Аспект | sprf | clpr |
|
||||
|--------|------|------|
|
||||
| Доп. поле | — | contact_data_confirmed_at |
|
||||
| created_at / updated_at | без default в схеме | default now() |
|
||||
|
||||
Остальное совпадает (universal_id, unified_id, phone).
|
||||
|
||||
---
|
||||
|
||||
### users_tg
|
||||
Набор полей совпадает. В clpr у created_at и updated_at default now(); в sprf в схеме default не показан.
|
||||
|
||||
---
|
||||
|
||||
### wizard_questions
|
||||
Структура совпадает (claim_type, step_key, question_text, answer_type, step_order, options, is_required).
|
||||
|
||||
---
|
||||
|
||||
## 3. Краткая сводка
|
||||
|
||||
- **Только sprf:** судебные решения (court_decisions + view) — загрузка файлов, OCR, вектор, nsfw, CRM.
|
||||
- **Только clpr:** меню команд (menu_commands).
|
||||
- **clpr в среднем «богаче»:** в claims — uuid, expires_at, contact_id, phone; в claim_documents — OCR, матчинг, саммари; в users — contact_data_confirmed_at; чаще default now() на датах.
|
||||
- **sprf.claims.id** — varchar, **clpr.claims.id** — uuid.
|
||||
- **document_embeddings** устроены по-разному: sprf — по чанкам (document_id, chunk_index), clpr — id + text + metadata.
|
||||
|
||||
Если делать общий слой поверх двух схем (например, для MAX/Telegram), маппинг по именам таблиц 1:1, но нужно учитывать типы (claim_id, id в claims) и наличие/отсутствие полей (contact_id, phone, expires_at, OCR/match в claim_documents).
|
||||
|
||||
---
|
||||
|
||||
## 4. Детальное сравнение по полям (тип, NULL, default)
|
||||
|
||||
Для каждой общей таблицы: колонка | sprf тип / NULL / default | clpr тип / NULL / default | примечание.
|
||||
|
||||
### chat_messages
|
||||
|
||||
| Колонка | sprf (тип / NULL / default) | clpr (тип / NULL / default) | Примечание |
|
||||
|-------------|---------------------------------|----------------------------------|--------------|
|
||||
| id | integer / NO / nextval | integer / NO / nextval | одинаково |
|
||||
| claim_id | uuid / YES / — | uuid / YES / — | одинаково |
|
||||
| from_user | boolean / YES / — | boolean / YES / — | одинаково |
|
||||
| message_text| text / YES / — | text / YES / — | одинаково |
|
||||
| file_id | text / YES / — | text / YES / — | одинаково |
|
||||
| sent_at | timestamptz / YES / — | timestamptz / YES / **now()** | разный default |
|
||||
|
||||
---
|
||||
|
||||
### claim_documents
|
||||
|
||||
| Колонка | sprf (тип / NULL / default) | clpr (тип / NULL / default) | Примечание |
|
||||
|--------------------|-----------------------------|-----------------------------|-----------------|
|
||||
| id | uuid / NO / — | uuid / NO / gen_random_uuid() | разный default |
|
||||
| claim_id | character varying / YES / — | character varying / YES / — | одинаково |
|
||||
| field_name | text / YES / — | text / YES / — | одинаково |
|
||||
| file_id | text / YES / — | text / YES / — | одинаково |
|
||||
| uploaded_at | timestamptz / YES / — | timestamptz / YES / now() | разный default |
|
||||
| file_name | — | text / YES / — | только clpr |
|
||||
| original_file_name | — | text / YES / — | только clpr |
|
||||
| file_hash | — | varchar(64) / YES / — | только clpr |
|
||||
| ocr_status | — | varchar(20) / YES / 'pending'| только clpr |
|
||||
| ocr_processed_at | — | timestamptz / YES / — | только clpr |
|
||||
| ocr_error | — | text / YES / — | только clpr |
|
||||
| document_type | — | varchar(50) / YES / — | только clpr |
|
||||
| document_label | — | varchar(255) / YES / — | только clpr |
|
||||
| match_score | — | integer / YES / — | только clpr |
|
||||
| match_status | — | varchar(20) / YES / 'pending'| только clpr |
|
||||
| match_reason | — | text / YES / — | только clpr |
|
||||
| match_checked_at | — | timestamp / YES / — | только clpr |
|
||||
| document_summary | — | text / YES / — | только clpr |
|
||||
|
||||
---
|
||||
|
||||
### claim_statuses
|
||||
|
||||
| Колонка | sprf | clpr | Примечание |
|
||||
|-------------|------|------|------------|
|
||||
| code | text / NO / — | text / NO / — | одинаково |
|
||||
| description | text / YES / — | text / YES / — | одинаково |
|
||||
|
||||
---
|
||||
|
||||
### claim_types
|
||||
|
||||
| Колонка | sprf | clpr | Примечание |
|
||||
|-------------|------|------|------------|
|
||||
| code | text / NO / — | text / NO / — | одинаково |
|
||||
| description | text / YES / — | text / YES / — | одинаково |
|
||||
|
||||
---
|
||||
|
||||
### claims
|
||||
|
||||
| Колонка | sprf (тип / NULL / default) | clpr (тип / NULL / default) | Примечание |
|
||||
|---------------|-----------------------------|-----------------------------|-----------------|
|
||||
| id | **character varying** / NO / — | **uuid** / NO / gen_random_uuid() | разный тип и default |
|
||||
| session_token | varchar / YES / — | varchar / YES / — | одинаково |
|
||||
| unified_id | varchar / YES / — | varchar / YES / — | одинаково |
|
||||
| telegram_id | bigint / YES / — | bigint / YES / — | одинаково |
|
||||
| channel | text / YES / — | text / YES / — | одинаково |
|
||||
| user_id | integer / YES / — | integer / YES / — | одинаково |
|
||||
| type_code | text / YES / — | text / YES / — | одинаково |
|
||||
| status_code | text / YES / — | text / YES / — | одинаково |
|
||||
| policy_number | text / YES / — | text / YES / — | одинаково |
|
||||
| payload | jsonb / YES / — | jsonb / YES / — | одинаково |
|
||||
| is_confirmed | boolean / YES / false | boolean / YES / false | одинаково |
|
||||
| created_at | timestamptz / YES / — | timestamptz / YES / **now()** | разный default |
|
||||
| updated_at | timestamptz / YES / — | timestamptz / YES / **now()** | разный default |
|
||||
| expires_at | — | timestamptz / YES / — | только clpr |
|
||||
| contact_id | — | text / YES / — | только clpr |
|
||||
| phone | — | text / YES / — | только clpr |
|
||||
|
||||
---
|
||||
|
||||
### conversation_state
|
||||
|
||||
| Колонка | sprf | clpr | Примечание |
|
||||
|--------------|------|------|------------|
|
||||
| user_id | integer / NO / — | integer / NO / — | одинаково |
|
||||
| current_step | text / YES / — | text / YES / — | одинаково |
|
||||
| data | jsonb / YES / — | jsonb / YES / — | одинаково |
|
||||
| updated_at | timestamptz / YES / — | timestamptz / YES / — | одинаково |
|
||||
|
||||
---
|
||||
|
||||
### dialog_history_tg
|
||||
|
||||
| Колонка | sprf (тип / NULL / default) | clpr (тип / NULL / default) | Примечание |
|
||||
|---------------|-----------------------------|-----------------------------|----------------|
|
||||
| id | integer / NO / nextval | integer / NO / nextval | одинаково |
|
||||
| telegram_id | bigint / YES / — | bigint / YES / — | одинаково |
|
||||
| role | varchar / YES / — | varchar / YES / — | одинаково |
|
||||
| message | text / YES / — | text / YES / — | одинаково |
|
||||
| created_at | timestamptz / YES / now() | timestamptz / YES / now() | одинаково |
|
||||
| session_token | varchar / YES / — | varchar / YES / — | одинаково |
|
||||
| claim_id | **character varying** / YES / — | **uuid** / YES / — | разный тип |
|
||||
| message_type | text / YES / — | text / YES / — | одинаково |
|
||||
| payload | jsonb / YES / — | jsonb / YES / — | одинаково |
|
||||
| tg_message_id | bigint / YES / — | bigint / YES / — | одинаково |
|
||||
| tg_update_id | bigint / YES / — | bigint / YES / — | одинаково |
|
||||
|
||||
---
|
||||
|
||||
### document_embeddings
|
||||
|
||||
| Колонка | sprf | clpr | Примечание |
|
||||
|--------------|------|------|------------|
|
||||
| document_id | uuid / YES / — | — | только sprf (модель по чанкам) |
|
||||
| chunk_index | integer / YES / — | — | только sprf |
|
||||
| embedding | USER-DEFINED / YES / — | USER-DEFINED / YES / — | одинаково |
|
||||
| id | — | uuid / YES / — | только clpr |
|
||||
| text | — | text / YES / — | только clpr |
|
||||
| metadata | — | jsonb / YES / — | только clpr |
|
||||
|
||||
Разная модель: sprf — чанки документа; clpr — запись с id, text, metadata.
|
||||
|
||||
---
|
||||
|
||||
### documents
|
||||
|
||||
| Колонка | sprf (тип / NULL / default) | clpr (тип / NULL / default) | Примечание |
|
||||
|------------|-----------------------------|-----------------------------|--------------|
|
||||
| id | uuid / NO / — | uuid / NO / gen_random_uuid() | разный default |
|
||||
| source | text / YES / — | text / YES / — | одинаково |
|
||||
| content | text / YES / — | text / YES / — | одинаково |
|
||||
| metadata | jsonb / YES / — | jsonb / YES / — | одинаково |
|
||||
| created_at | timestamptz / YES / — | timestamptz / YES / **now()** | разный default |
|
||||
|
||||
---
|
||||
|
||||
### operators
|
||||
|
||||
| Колонка | sprf (тип / NULL / default) | clpr (тип / NULL / default) | Примечание |
|
||||
|------------|-----------------------------|-----------------------------|--------------|
|
||||
| id | integer / NO / nextval | integer / NO / nextval | одинаково |
|
||||
| telegram_id| bigint / YES / — | bigint / YES / — | одинаково |
|
||||
| name | text / YES / — | text / YES / — | одинаково |
|
||||
| is_active | boolean / YES / — | boolean / YES / — | одинаково |
|
||||
| created_at | timestamptz / YES / — | timestamptz / YES / **now()** | разный default |
|
||||
|
||||
---
|
||||
|
||||
### sessions
|
||||
|
||||
| Колонка | sprf (тип / NULL / default) | clpr (тип / NULL / default) | Примечание |
|
||||
|----------------|-----------------------------|-----------------------------|--------------|
|
||||
| id | uuid / NO / gen_random_uuid() | uuid / NO / gen_random_uuid() | одинаково |
|
||||
| user_id | integer / YES / — | integer / YES / — | одинаково |
|
||||
| session_token | varchar / YES / — | varchar / YES / — | одинаково |
|
||||
| created_at | timestamptz / YES / now() | timestamptz / YES / now() | одинаково |
|
||||
| last_activity | timestamptz / YES / — | timestamptz / YES / — | одинаково |
|
||||
| expires_at | timestamptz / YES / — | timestamptz / YES / — | одинаково |
|
||||
|
||||
---
|
||||
|
||||
### user_accounts
|
||||
|
||||
| Колонка | sprf | clpr | Примечание |
|
||||
|----------------|------|------|------------|
|
||||
| id | integer / NO / nextval | integer / NO / nextval | одинаково |
|
||||
| user_id | integer / YES / — | integer / YES / — | одинаково |
|
||||
| channel | text / YES / — | text / YES / — | одинаково |
|
||||
| channel_user_id| text / YES / — | text / YES / — | одинаково |
|
||||
|
||||
---
|
||||
|
||||
### users
|
||||
|
||||
| Колонка | sprf (тип / NULL / default) | clpr (тип / NULL / default) | Примечание |
|
||||
|-----------|-----------------------------|-----------------------------|--------------|
|
||||
| id | integer / NO / nextval | integer / NO / nextval | одинаково |
|
||||
| universal_id | uuid / YES / — | uuid / YES / — | одинаково |
|
||||
| unified_id| varchar / YES / — | varchar / YES / — | одинаково |
|
||||
| phone | varchar / YES / — | varchar / YES / — | одинаково |
|
||||
| created_at| timestamptz / YES / — | timestamptz / YES / **now()** | разный default |
|
||||
| updated_at| timestamptz / YES / — | timestamptz / YES / **now()** | разный default |
|
||||
| contact_data_confirmed_at | — | timestamptz / YES / — | только clpr |
|
||||
|
||||
---
|
||||
|
||||
### users_tg
|
||||
|
||||
| Колонка | sprf (тип / NULL / default) | clpr (тип / NULL / default) | Примечание |
|
||||
|---------------|-----------------------------|-----------------------------|--------------|
|
||||
| telegram_id | bigint / NO / — | bigint / NO / — | одинаково |
|
||||
| phone_number | varchar / YES / — | varchar / YES / — | одинаково |
|
||||
| first_name_tg | varchar / YES / — | varchar / YES / — | одинаково |
|
||||
| last_name_tg | varchar / YES / — | varchar / YES / — | одинаково |
|
||||
| username | varchar / YES / — | varchar / YES / — | одинаково |
|
||||
| language_code | varchar / YES / — | varchar / YES / — | одинаково |
|
||||
| is_premium | boolean / YES / — | boolean / YES / — | одинаково |
|
||||
| unified_id | varchar / YES / — | varchar / YES / — | одинаково |
|
||||
| first_name, last_name, middle_name | varchar / YES / — | varchar / YES / — | одинаково |
|
||||
| birth_date, birth_place, inn, address, email | varchar / YES / — | varchar / YES / — | одинаково |
|
||||
| is_confirmed | boolean / YES / false | boolean / YES / false | одинаково |
|
||||
| created_at | timestamptz / YES / — | timestamptz / YES / **now()** | разный default |
|
||||
| updated_at | timestamptz / YES / — | timestamptz / YES / **now()** | разный default |
|
||||
|
||||
---
|
||||
|
||||
### wizard_questions
|
||||
|
||||
| Колонка | sprf | clpr | Примечание |
|
||||
|---------------|------|------|------------|
|
||||
| id | integer / NO / nextval | integer / NO / nextval | одинаково |
|
||||
| claim_type | text / YES / — | text / YES / — | одинаково |
|
||||
| step_key | text / YES / — | text / YES / — | одинаково |
|
||||
| question_text | text / YES / — | text / YES / — | одинаково |
|
||||
| answer_type | text / YES / — | text / YES / — | одинаково |
|
||||
| step_order | integer / YES / — | integer / YES / — | одинаково |
|
||||
| options | jsonb / YES / — | jsonb / YES / — | одинаково |
|
||||
| is_required | boolean / YES / — | boolean / YES / — | одинаково |
|
||||
11
docs/max-api/01-overview.md
Normal file
11
docs/max-api/01-overview.md
Normal file
@@ -0,0 +1,11 @@
|
||||
# Обзор Max Bot API
|
||||
|
||||
Базовый URL: **https://platform-api.max.ru**. Авторизация: заголовок `Authorization: <token>`. Токен через query не поддерживается. Токен берётся в платформе MAX для партнёров: Интеграция → Получить токен.
|
||||
|
||||
Коды ответов: 200 — успех; 400 — неверный запрос; 401 — ошибка аутентификации; 404 — не найден; 405 — метод не разрешён; 429 — лимит запросов; 503 — сервис недоступен.
|
||||
|
||||
Рекомендации: для разработки — Long Polling (GET /updates), для production — только Webhook. Не более 30 запросов в секунду.
|
||||
|
||||
Клавиатура (inline_keyboard): до 210 кнопок, до 30 рядов, до 7 кнопок в ряду. Типы: callback (событие message_callback), link, request_contact, request_geo_location, open_app, message. Для Webhook поддерживается только HTTPS.
|
||||
|
||||
Форматирование: в NewMessageBody поле format: markdown или html. Markdown: *курсив*, **жирный**, `код`, [ссылка](url). HTML: теги b, i, del, u, code, a.
|
||||
86
docs/max-api/02-methods.md
Normal file
86
docs/max-api/02-methods.md
Normal file
@@ -0,0 +1,86 @@
|
||||
# Методы Max Bot API
|
||||
|
||||
## POST /messages — отправить сообщение
|
||||
|
||||
`POST https://platform-api.max.ru/messages?user_id={user_id}` или `?chat_id={chat_id}`
|
||||
|
||||
Заголовки: `Authorization: <token>`, `Content-Type: application/json`.
|
||||
|
||||
Query: user_id (int64) или chat_id (int64), опционально disable_link_preview (bool).
|
||||
|
||||
Тело (NewMessageBody): text (до 4000 символов), attachments, link, notify (по умолч. true), format (markdown | html).
|
||||
|
||||
Пример:
|
||||
|
||||
```bash
|
||||
curl -X POST "https://platform-api.max.ru/messages?user_id=123" \
|
||||
-H "Authorization: TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"text": "Привет!", "format": "markdown"}'
|
||||
```
|
||||
|
||||
Ответ: объект message (Message).
|
||||
|
||||
---
|
||||
|
||||
## GET /updates — Long Polling
|
||||
|
||||
`GET https://platform-api.max.ru/updates`
|
||||
|
||||
Параметры: limit (1-1000, по умолч. 100), timeout (0-90 сек, по умолч. 30), marker (int64), types (массив типов, напр. message_created, message_callback).
|
||||
|
||||
Ответ: updates (массив Update), marker для следующего запроса.
|
||||
|
||||
---
|
||||
|
||||
## POST /subscriptions — Webhook
|
||||
|
||||
`POST https://platform-api.max.ru/subscriptions`
|
||||
|
||||
Тело: url (обязательно https), update_types (массив), secret (5-256 символов, [a-zA-Z0-9_-]) — приходит в заголовке X-Max-Bot-Api-Secret.
|
||||
|
||||
Порты сервера: 80, 8080, 443, 8443, 16384-32383.
|
||||
|
||||
Пример:
|
||||
|
||||
```json
|
||||
{
|
||||
"url": "https://your-domain.com/webhook",
|
||||
"update_types": ["message_created", "bot_started"],
|
||||
"secret": "your_secret"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## POST /answers — ответ на нажатие кнопки
|
||||
|
||||
`POST https://platform-api.max.ru/answers?callback_id={callback_id}`
|
||||
|
||||
callback_id берётся из Update с типом message_callback.
|
||||
|
||||
Тело: message (NewMessageBody, опц.) — обновить сообщение; notification (string, опц.) — одноразовое уведомление.
|
||||
|
||||
---
|
||||
|
||||
## PUT /messages — редактировать сообщение
|
||||
|
||||
`PUT https://platform-api.max.ru/messages?message_id={message_id}`
|
||||
|
||||
Тело: text, attachments (null = не менять, [] = удалить все), link, notify, format.
|
||||
|
||||
**Ограничение:** редактировать можно только сообщения, отправленные **менее 24 часов назад**.
|
||||
|
||||
---
|
||||
|
||||
## DELETE /messages — удалить сообщение
|
||||
|
||||
`DELETE https://platform-api.max.ru/messages?message_id={message_id}`
|
||||
|
||||
**Ограничение:** удалять можно только сообщения, отправленные **менее 24 часов назад**.
|
||||
|
||||
---
|
||||
|
||||
## Остальные методы
|
||||
|
||||
GET /me — информация о боте. GET/PATCH/DELETE /chats, GET/POST/DELETE /chats/{chatId}/members, GET /subscriptions, DELETE /subscriptions, POST /uploads, GET/PUT/DELETE /messages, GET /messages/{messageId}, POST /chats/{chatId}/actions и др. — см. https://dev.max.ru/docs-api
|
||||
131
docs/max-api/03-objects.md
Normal file
131
docs/max-api/03-objects.md
Normal file
@@ -0,0 +1,131 @@
|
||||
# Объекты Max Bot API
|
||||
|
||||
## Update
|
||||
|
||||
Событие, приходящее в Long Polling или на Webhook.
|
||||
|
||||
| Поле | Тип | Описание |
|
||||
|------|-----|----------|
|
||||
| update_type | string | Тип события, например `message_created`, `message_callback` |
|
||||
| timestamp | int64 | Unix-время события |
|
||||
| message | Message | Сообщение (для message_created и др.) |
|
||||
| user_locale | string | Язык пользователя (IETF BCP 47), только в диалогах |
|
||||
|
||||
Для событий из группового чата или канала бот должен быть назначен администратором.
|
||||
|
||||
**Пример:**
|
||||
|
||||
```json
|
||||
{
|
||||
"update_type": "message_created",
|
||||
"timestamp": 0,
|
||||
"message": { ... },
|
||||
"user_locale": "ru"
|
||||
}
|
||||
```
|
||||
|
||||
При нажатии кнопки приходит тип `message_callback`; в объекте есть данные callback (в т.ч. `callback_id` для POST /answers).
|
||||
|
||||
---
|
||||
|
||||
## NewMessageBody
|
||||
|
||||
Тело сообщения при отправке (POST /messages) или обновлении (POST /answers и т.п.).
|
||||
|
||||
| Поле | Тип | Описание |
|
||||
|------|-----|----------|
|
||||
| text | string | Текст до 4000 символов |
|
||||
| attachments | AttachmentRequest[] | Вложения (inline_keyboard и др.) |
|
||||
| link | NewMessageLink | Ссылка на сообщение |
|
||||
| notify | bool | Уведомлять участников (по умолчанию true) |
|
||||
| format | "markdown" \| "html" | Формат текста |
|
||||
|
||||
**Пример:**
|
||||
|
||||
```json
|
||||
{
|
||||
"text": "Текст сообщения",
|
||||
"attachments": [{ "type": "inline_keyboard", "payload": { "buttons": [...] } }],
|
||||
"notify": true,
|
||||
"format": "markdown"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Message
|
||||
|
||||
Объект сообщения (приходит в Update и в ответах API). Полное описание: [dev.max.ru/docs-api/objects/Message](https://dev.max.ru/docs-api/objects/Message).
|
||||
|
||||
| Поле | Тип | Описание |
|
||||
|------|-----|----------|
|
||||
| sender | User | Отправитель (опц.) |
|
||||
| recipient | Recipient | Получатель |
|
||||
| timestamp | int64 | Unix-время |
|
||||
| link | LinkedMessage | Пересланное/ответное сообщение (опц.) |
|
||||
| **body** | **MessageBody** | Содержимое: текст + вложения. Может быть `null`, если только пересланное |
|
||||
| stat | MessageStat | Статистика (опц.) |
|
||||
| url | string | Публичная ссылка на пост в канале (опц.) |
|
||||
|
||||
---
|
||||
|
||||
## MessageBody (входящее сообщение)
|
||||
|
||||
Содержимое сообщения при получении по Webhook или GET /updates. Источник: [Message](https://dev.max.ru/docs-api/objects/Message), [POST /uploads](https://dev.max.ru/docs-api/methods/POST/uploads).
|
||||
|
||||
| Поле | Тип | Описание |
|
||||
|------|-----|----------|
|
||||
| **text** | string | Текст сообщения или подпись к медиа. До 4000 символов. Может отсутствовать (только вложение). |
|
||||
| **attachments** | array | Вложения. Каждый элемент: `{ "type": "<тип>", "payload": { ... } }`. |
|
||||
|
||||
### Типы вложений (attachments[].type)
|
||||
|
||||
По документации загрузки файлов (POST /uploads) поддерживаются типы: **`image`**, **`video`**, **`audio`**, **`file`**. Значение `photo` больше не используется — приходит как **`image`**.
|
||||
|
||||
| Тип | Описание | Форматы (при загрузке) |
|
||||
|-----|----------|-------------------------|
|
||||
| **image** | Фото/картинка | JPG, JPEG, PNG, GIF, TIFF, BMP, HEIC |
|
||||
| **video** | Видео | MP4, MOV, MKV, WEBM, MATROSKA |
|
||||
| **audio** | Голос/аудио | MP3, WAV, M4A и др. |
|
||||
| **file** | Документ или любой файл | Любые типы |
|
||||
|
||||
У видео и аудио в `payload` приходит **token** (используется в т.ч. для GET /videos/{videoToken}). У image/file — в `payload` приходят данные файла (токен или URL после обработки сервером).
|
||||
|
||||
### Как приходят сообщения в Webhook
|
||||
|
||||
- **Только текст:** `message.body.text` — строка, `message.body.attachments` — пустой или отсутствует.
|
||||
- **Только голос/аудио:** `message.body.attachments` — один элемент `type: "audio"`, `payload` с токеном; `message.body.text` может быть пустым.
|
||||
- **Только видео:** `message.body.attachments` — один элемент `type: "video"`, `payload` с токеном; `message.body.text` — по желанию.
|
||||
- **Документ/файл:** `message.body.attachments` — элемент `type: "file"`, в `payload` — данные файла; `message.body.text` — опционально.
|
||||
- **Фото (image):** `message.body.attachments` — элемент `type: "image"` (не `photo`), в `payload` — данные изображения.
|
||||
- **Фото с подписью:** то же, что фото, плюс **`message.body.text`** — подпись (caption).
|
||||
|
||||
Пример (фото с подписью):
|
||||
|
||||
```json
|
||||
{
|
||||
"update_type": "message_created",
|
||||
"message": {
|
||||
"sender": { ... },
|
||||
"recipient": { ... },
|
||||
"timestamp": 1234567890,
|
||||
"body": {
|
||||
"text": "Вот документ по делу",
|
||||
"attachments": [
|
||||
{ "type": "image", "payload": { "token": "..." } }
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Точную структуру `payload` для каждого типа смотри в ответах API (например, отправить боту сообщение и посмотреть тело Webhook в n8n) или в [официальной документации](https://dev.max.ru/docs-api).
|
||||
|
||||
---
|
||||
|
||||
## User, Chat
|
||||
|
||||
- **User:** [dev.max.ru/docs-api/objects/User](https://dev.max.ru/docs-api/objects/User)
|
||||
- **Chat:** [dev.max.ru/docs-api/objects/Chat](https://dev.max.ru/docs-api/objects/Chat)
|
||||
|
||||
Полный список методов и объектов — в [официальной документации](https://dev.max.ru/docs-api).
|
||||
193
docs/max-api/04-formats-and-buttons.md
Normal file
193
docs/max-api/04-formats-and-buttons.md
Normal file
@@ -0,0 +1,193 @@
|
||||
# Форматы текста и кнопки (MAX Bot API)
|
||||
|
||||
## Форматы текста (поле `format` в теле сообщения)
|
||||
|
||||
В **NewMessageBody** укажи `"format": "markdown"` или `"format": "html"`. Тогда текст сообщения будет отформатирован.
|
||||
|
||||
### Markdown
|
||||
|
||||
| Как написать | Результат |
|
||||
|--------------|-----------|
|
||||
| `*курсив*` или `_курсив_` | *курсив* |
|
||||
| `**жирный**` или `__жирный__` | **жирный** |
|
||||
| `~~зачёркнутый~~` | ~~зачёркнутый~~ |
|
||||
| `++подчёркнутый++` | подчёркнутый |
|
||||
| `` `код` `` | моноширинный (переводы строк внутри — как пробелы) |
|
||||
| `[текст ссылки](https://example.com)` | кликабельная ссылка |
|
||||
| @упоминание | `"text": "[Имя Фамилия](max://user/user_id)", "format": "markdown"` — полное имя из профиля MAX |
|
||||
|
||||
### HTML
|
||||
|
||||
| Теги | Результат |
|
||||
|------|-----------|
|
||||
| `<i>`, `<em>` | курсив |
|
||||
| `<b>`, `<strong>` | жирный |
|
||||
| `<del>`, `<s>` | зачёркнутый |
|
||||
| `<ins>`, `<u>` | подчёркнутый |
|
||||
| `<pre>`, `<code>` | моноширинный |
|
||||
| `<a href="https://example.com">Текст</a>` | ссылка |
|
||||
| @упоминание | `"text": "<a href=\"max://user/user_id\">Имя Фамилия</a>", "format": "html"` |
|
||||
|
||||
Пример тела с markdown:
|
||||
|
||||
```json
|
||||
{
|
||||
"text": "**Внимание!** Вы отправили *голосовое*. Обрабатываем.",
|
||||
"format": "markdown"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Кнопки (inline_keyboard)
|
||||
|
||||
Кнопки добавляются через **attachments**: один элемент с `type: "inline_keyboard"` и `payload.buttons` — массив **рядов**, каждый ряд — массив **кнопок**.
|
||||
|
||||
Ограничения:
|
||||
- до **210 кнопок** всего;
|
||||
- до **30 рядов**;
|
||||
- до **7 кнопок в ряду** (для типов `link`, `open_app`, `request_geo_location`, `request_contact` — до **3** в ряду);
|
||||
- для кнопки типа `link` ссылка до **2048** символов.
|
||||
|
||||
### Структура
|
||||
|
||||
```json
|
||||
{
|
||||
"text": "Текст сообщения над кнопками",
|
||||
"format": "markdown",
|
||||
"attachments": [
|
||||
{
|
||||
"type": "inline_keyboard",
|
||||
"payload": {
|
||||
"buttons": [
|
||||
[
|
||||
{ "type": "callback", "text": "Надпись кнопки", "payload": "значение при нажатии" }
|
||||
],
|
||||
[
|
||||
{ "type": "link", "text": "Открыть сайт", "url": "https://example.com" }
|
||||
]
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
`buttons` — массив рядов. Каждый ряд — массив кнопок. Одна кнопка — объект с полями в зависимости от типа.
|
||||
|
||||
### Типы кнопок
|
||||
|
||||
| type | Описание | Поля кнопки |
|
||||
|------|----------|-------------|
|
||||
| **callback** | При нажатии в Webhook приходит `message_callback` с `callback_id` и payload. Нужен для ответа через POST /answers. | `text`, `payload` (строка или объект — то, что придёт в бот) |
|
||||
| **link** | Открывает ссылку в браузере. | `text`, `url` (до 2048 символов) |
|
||||
| **message** | Отправляет боту текстовое сообщение (как будто пользователь написал это). | `text` |
|
||||
| **request_contact** | Запрос контакта (номер телефона). Пользователь нажимает → клиент MAX предлагает отправить контакт → в Webhook приходит `message_created` с данными контакта (телефон и т.д.) в теле сообщения. | `text` (подпись на кнопке) |
|
||||
| **request_geo_location** | Запрос геолокации. Пользователь нажимает → отправляет геолокацию → в Webhook приходит сообщение с координатами. | `text` |
|
||||
| **open_app** | Открывает мини-приложение. | уточнять в [доках](https://dev.max.ru/docs-api) |
|
||||
|
||||
### Пример: кнопка «Поделиться контактом»
|
||||
|
||||
У кнопки тип **request_contact**, поле **text** — подпись (например «📱 Отправить номер телефона»). В одном ряду с такими кнопками MAX разрешает до 3 кнопок.
|
||||
|
||||
```json
|
||||
{
|
||||
"text": "Чтобы мы могли связаться, поделитесь номером телефона:",
|
||||
"format": "markdown",
|
||||
"attachments": [
|
||||
{
|
||||
"type": "inline_keyboard",
|
||||
"payload": {
|
||||
"buttons": [
|
||||
[
|
||||
{ "type": "request_contact", "text": "📱 Отправить номер телефона" }
|
||||
]
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
После нажатия пользователь подтверждает отправку контакта в клиенте MAX. В Webhook придёт **message_created** с вложением `attachments[0].type === "contact"`. Структура:
|
||||
|
||||
- **attachments[0].payload.vcf_info** — строка VCARD (например `TEL;TYPE=cell:79262306381`, `FN:Имя Фамилия`). Телефон достаётся из строки `TEL...:номер`.
|
||||
- **attachments[0].payload.max_info** — объект пользователя MAX: `user_id`, `first_name`, `last_name`, `name`, `is_bot`, `last_activity_time`.
|
||||
|
||||
В нормализаторе для такого сообщения: `answer_type: 'contact'`, `answer_text` — извлечённый номер, `contact_payload` — весь payload, `contact_name` — из max_info.name.
|
||||
|
||||
### Пример: одна callback-кнопка
|
||||
|
||||
```json
|
||||
{
|
||||
"text": "Выберите действие:",
|
||||
"format": "markdown",
|
||||
"attachments": [
|
||||
{
|
||||
"type": "inline_keyboard",
|
||||
"payload": {
|
||||
"buttons": [
|
||||
[
|
||||
{ "type": "callback", "text": "Подтвердить", "payload": "confirm" },
|
||||
{ "type": "callback", "text": "Отмена", "payload": "cancel" }
|
||||
]
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
При нажатии «Подтвердить» в Webhook придёт `update_type: "message_callback"`, в нормализованном объекте будет `answer_text: "confirm"` и `callback_id` для POST /answers (уведомление или обновление сообщения).
|
||||
|
||||
### Пример: кнопка-ссылка и callback в одном сообщении
|
||||
|
||||
```json
|
||||
{
|
||||
"text": "Официальный сайт и обратная связь:",
|
||||
"format": "markdown",
|
||||
"attachments": [
|
||||
{
|
||||
"type": "inline_keyboard",
|
||||
"payload": {
|
||||
"buttons": [
|
||||
[
|
||||
{ "type": "link", "text": "Перейти на сайт", "url": "https://example.com" }
|
||||
],
|
||||
[
|
||||
{ "type": "callback", "text": "Написать в поддержку", "payload": "support" }
|
||||
]
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### В n8n (HTTP Request — тело с кнопками)
|
||||
|
||||
В **JSON Body** ноды можно задать статичное тело или собрать через выражение. Пример статичного тела с кнопками:
|
||||
|
||||
```json
|
||||
{
|
||||
"text": "Выберите действие:",
|
||||
"format": "markdown",
|
||||
"attachments": [
|
||||
{
|
||||
"type": "inline_keyboard",
|
||||
"payload": {
|
||||
"buttons": [
|
||||
[
|
||||
{ "type": "callback", "text": "Да", "payload": "yes" },
|
||||
{ "type": "callback", "text": "Нет", "payload": "no" }
|
||||
]
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
URL и заголовки — как раньше: `POST https://platform-api.max.ru/messages?user_id={{ $json.max_id }}`, `Authorization: <token>`, `Content-Type: application/json`.
|
||||
|
||||
После нажатия callback-кнопки пользователем обрабатывай событие в воркфлоу (по `answer_type === 'callback'` и `answer_text` / `callback_id`) и при необходимости вызывай **POST /answers?callback_id=...** для уведомления или обновления сообщения (см. `02-methods.md`).
|
||||
10
docs/max-api/README.md
Normal file
10
docs/max-api/README.md
Normal file
@@ -0,0 +1,10 @@
|
||||
# Max Bot API — документация (бот Союз потребителей РФ)
|
||||
|
||||
Локальная копия. Официально: https://dev.max.ru/docs-api
|
||||
|
||||
- **01-overview.md** — обзор API, авторизация, клавиатура, форматирование
|
||||
- **02-methods.md** — методы: сообщения, updates, webhook, callback
|
||||
- **03-objects.md** — объекты Update, NewMessageBody, Message, MessageBody (формат входящих: текст, голос, видео, документ, фото с подписью)
|
||||
- **04-formats-and-buttons.md** — форматы текста (markdown, html) и кнопки (inline_keyboard: callback, link, message и др.)
|
||||
|
||||
Базовый URL: `https://platform-api.max.ru`. Авторизация: заголовок `Authorization: <token>`. Лимит 30 rps. В production — только Webhook.
|
||||
88
docs/max-curl-http-request.md
Normal file
88
docs/max-curl-http-request.md
Normal file
@@ -0,0 +1,88 @@
|
||||
# cURL и настройка HTTP Request ноды (ответ пользователю в MAX)
|
||||
|
||||
## cURL — отправить сообщение
|
||||
|
||||
Подставь свой `MAX_BOT_TOKEN` и `max_id` (или `max_chat_id`) получателя:
|
||||
|
||||
```bash
|
||||
curl -X POST "https://platform-api.max.ru/messages?user_id=6200846" \
|
||||
-H "Authorization: ВАШ_MAX_BOT_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"text": "Привет! Сообщение получено.", "format": "markdown"}'
|
||||
```
|
||||
|
||||
С `chat_id` вместо `user_id`:
|
||||
|
||||
```bash
|
||||
curl -X POST "https://platform-api.max.ru/messages?chat_id=188573833" \
|
||||
-H "Authorization: ВАШ_MAX_BOT_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"text": "Привет!", "format": "markdown"}'
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Как ввести в HTTP Request ноду n8n
|
||||
|
||||
| Поле | Значение |
|
||||
|------|----------|
|
||||
| **Method** | `POST` |
|
||||
| **URL** | `https://platform-api.max.ru/messages?user_id={{ $json.max_id }}` |
|
||||
| **Authentication** | Header Auth → Name: `Authorization`, Value: `ВАШ_MAX_BOT_TOKEN` (или credential) |
|
||||
| **Send Headers** | включить, добавить: Name `Content-Type`, Value `application/json` (если нет из Auth) |
|
||||
| **Send Body** | Yes |
|
||||
| **Body Content Type** | JSON |
|
||||
| **Specify Body** | Using JSON |
|
||||
| **JSON Body** | `{"text": "Привет! Сообщение получено.", "format": "markdown"}` |
|
||||
|
||||
Динамический текст из предыдущей ноды:
|
||||
|
||||
```json
|
||||
{
|
||||
"text": "{{ $json.answer_text }}",
|
||||
"format": "markdown"
|
||||
}
|
||||
```
|
||||
|
||||
Или свой ответ из другой ноды:
|
||||
|
||||
```json
|
||||
{
|
||||
"text": "{{ $('Твоя нода с ответом').item.json.reply_text }}",
|
||||
"format": "markdown"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
Подробнее про форматы текста (markdown/html) и про кнопки (callback, link, message и т.д.) — в **`docs/max-api/04-formats-and-buttons.md`**.
|
||||
|
||||
---
|
||||
|
||||
## cURL — ответ на нажатие кнопки (callback)
|
||||
|
||||
Подставь `callback_id` из нормализованного объекта и токен:
|
||||
|
||||
```bash
|
||||
curl -X POST "https://platform-api.max.ru/answers?callback_id=ВАШ_CALLBACK_ID" \
|
||||
-H "Authorization: ВАШ_MAX_BOT_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"notification": "Нажато!"}'
|
||||
```
|
||||
|
||||
В n8n (только когда есть `callback_id`):
|
||||
|
||||
- **URL:** `https://platform-api.max.ru/answers?callback_id={{ $json.callback_id }}`
|
||||
- **Method:** POST
|
||||
- **Headers:** те же
|
||||
- **Body:** `{"notification": "Нажато!"}` или `{"message": {"text": "Новый текст сообщения", "format": "markdown"}}`
|
||||
|
||||
### Удалить кнопки после нажатия
|
||||
|
||||
Обнови сообщение тем же текстом, но **без** `attachments` — клавиатура исчезнет:
|
||||
|
||||
- **URL:** `https://platform-api.max.ru/answers?callback_id={{ $json.callback_id }}`
|
||||
- **Method:** POST
|
||||
- **Body:** `{"message": {"text": "{{ $json.callback_message_text }}", "format": "markdown"}}`
|
||||
|
||||
Текст бери из нормализатора: после callback там есть **`callback_message_text`** (и при желании **`callback_message_mid`**). В body передаётся только `text` и `format`, без `attachments` — MAX перерисует сообщение без кнопок.
|
||||
144
docs/max-webhook.md
Normal file
144
docs/max-webhook.md
Normal file
@@ -0,0 +1,144 @@
|
||||
# Как активировать Webhook для бота MAX
|
||||
|
||||
## 1. В n8n: воркфлоу с Webhook
|
||||
|
||||
1. Открой n8n (https://n8n.clientright.pro).
|
||||
2. Создай новый воркфлоу или открой существующий для MAX.
|
||||
3. Добавь ноду **Webhook** (Trigger → Webhook).
|
||||
4. Настрой Webhook:
|
||||
- **HTTP Method:** POST
|
||||
- **Path:** должен совпадать с путём из URL в `.env`. Сейчас в `.env` указано:
|
||||
- `N8N_MAX_WORKFLOW=https://n8n.clientright.pro/webhook/sprf_max`
|
||||
- значит в ноде Webhook задай **Path** = `sprf_max` (без `/webhook/`).
|
||||
- **Authentication:** Optional (проверку секрета можно сделать следующей нодой: сравнить заголовок `X-Max-Bot-Api-Secret` с переменной `MAX_WEBHOOK_SECRET`).
|
||||
5. Включи воркфлоу (Production mode), чтобы URL был доступен по HTTPS.
|
||||
|
||||
Итог: запросы на `https://n8n.clientright.pro/webhook/sprf_max` будут попадать в этот воркфлоу.
|
||||
|
||||
## 2. Регистрация URL в MAX
|
||||
|
||||
Из каталога проекта выполни:
|
||||
|
||||
```bash
|
||||
python3 register_max_webhook.py
|
||||
```
|
||||
|
||||
Скрипт читает из `.env`:
|
||||
- `MAX_BOT_TOKEN` — токен бота
|
||||
- `N8N_MAX_WORKFLOW` — полный URL webhook (HTTPS)
|
||||
- `MAX_WEBHOOK_SECRET` — секрет (опционально; если задан, MAX будет присылать его в заголовке `X-Max-Bot-Api-Secret`)
|
||||
|
||||
После успешного запуска MAX начнёт отправлять события (новые сообщения, нажатия кнопок, старт бота) на этот URL.
|
||||
|
||||
## 3. Нормализация входящего (Code node под MAX)
|
||||
|
||||
Чтобы не обрабатывать сырой Update, а получать один и тот же формат, что и для Telegram (для общего воркфлоу), используй Code node с содержимым из файла **`n8n-code-node-max-normalize.js`** в корне проекта. Он отдаёт один объект с полями:
|
||||
|
||||
- `max_id` — id пользователя MAX (sender)
|
||||
- `max_chat_id` — id чата/диалога (для лички может совпадать с user_id)
|
||||
- `answer_text` — текст сообщения, подпись к медиа или данные callback
|
||||
- `answer_type` — `text` | `command` | `callback` | `voice` | `audio` | `video` | `file` | `photo`
|
||||
- `channel` — `'max'`
|
||||
- `reply_to_*` — если было ответ/пересланное (из `message.link`)
|
||||
- `callback_id` — только при `answer_type === 'callback'` (для POST /answers)
|
||||
- `attachment_token` / `file_id` — при медиа-вложениях
|
||||
- `raw_update` — исходное тело Webhook
|
||||
|
||||
Групповые чаты и каналы отфильтровываются (возвращается пустой массив).
|
||||
|
||||
## 4. Отправка ответа пользователю (пушим в MAX)
|
||||
|
||||
Чтобы бот написал пользователю, вызывай **MAX Bot API**: отправка сообщения — метод **POST /messages**.
|
||||
|
||||
### Параметры
|
||||
|
||||
- **URL:** `https://platform-api.max.ru/messages?user_id={user_id}` или `?chat_id={chat_id}`
|
||||
В личке можно использовать либо `user_id` (id отправителя), либо `chat_id` (id диалога). Из нашей нормализации: `max_id` — это user_id, `max_chat_id` — chat_id; для ответа подойдёт любой из них в query.
|
||||
- **Метод:** POST
|
||||
- **Заголовки:**
|
||||
- `Authorization: <MAX_BOT_TOKEN>` — токен бота (из .env или из credentials в n8n)
|
||||
- `Content-Type: application/json`
|
||||
- **Тело (JSON):** объект **NewMessageBody**:
|
||||
- `text` (string) — текст до 4000 символов, обязателен если нет вложений
|
||||
- `format` (опц.) — `"markdown"` или `"html"` для форматирования
|
||||
- `attachments` (опц.) — массив вложений (например inline_keyboard с кнопками)
|
||||
- `notify` (опц., по умолч. true) — уведомлять ли пользователя
|
||||
|
||||
Пример тела:
|
||||
|
||||
```json
|
||||
{
|
||||
"text": "Привет! Ваше сообщение получено.",
|
||||
"format": "markdown"
|
||||
}
|
||||
```
|
||||
|
||||
### В n8n (HTTP Request node)
|
||||
|
||||
1. Добавь ноду **HTTP Request** после логики (после Code или ветки, где есть нормализованный объект с `max_id`/`max_chat_id`).
|
||||
2. Настрой:
|
||||
- **Method:** POST
|
||||
- **URL:** `https://platform-api.max.ru/messages?user_id={{ $json.max_id }}`
|
||||
(или `?chat_id={{ $json.max_chat_id }}` — оба варианта для лички рабочие)
|
||||
- **Authentication:** Generic Credential Type → **Header Auth**
|
||||
- Name: `Authorization`
|
||||
- Value: твой `MAX_BOT_TOKEN` (создай в n8n Credentials или подставь через переменную окружения)
|
||||
- **Body Content Type:** JSON
|
||||
- **Specify Body:** Using JSON
|
||||
- **JSON Body:** например `{ "text": "{{ $json.answer_text }}", "format": "markdown" }` или свой текст/поля из предыдущих нод
|
||||
|
||||
Токен бота лучше хранить в n8n Credentials (тип Header Auth или просто в переменной workflow/environment), а не в коде.
|
||||
|
||||
### Ответ на нажатие кнопки (callback)
|
||||
|
||||
Если пользователь нажал кнопку, приходит `message_callback` и в нормализованном объекте есть **`callback_id`**. Чтобы обновить сообщение с кнопкой или показать уведомление:
|
||||
|
||||
- **URL:** `https://platform-api.max.ru/answers?callback_id={{ $json.callback_id }}`
|
||||
- **Method:** POST
|
||||
- **Headers:** те же (`Authorization`, `Content-Type: application/json`)
|
||||
- **Body (JSON):**
|
||||
- `message` (опц.) — объект NewMessageBody (обновить сообщение)
|
||||
- `notification` (опц.) — строка, одноразовое уведомление пользователю
|
||||
|
||||
Подробнее: `docs/max-api/02-methods.md`.
|
||||
|
||||
## 5. Проверка
|
||||
|
||||
Напиши боту в мессенджере MAX — в n8n во входящих данных Webhook должен появиться объект с полями `update_type`, `message` и т.д.
|
||||
|
||||
## Проверка: зарегистрирован ли Webhook
|
||||
|
||||
Выполни в каталоге проекта:
|
||||
|
||||
```bash
|
||||
python3 -c "
|
||||
import os, json, urllib.request
|
||||
from pathlib import Path
|
||||
for line in Path('.env').read_text().splitlines():
|
||||
s = line.strip()
|
||||
if s and not s.startswith('#') and '=' in s:
|
||||
k, v = s.split('=', 1)
|
||||
os.environ[k.strip()] = v.strip()
|
||||
r = urllib.request.urlopen(urllib.request.Request(
|
||||
os.environ.get('MAX_API_BASE','https://platform-api.max.ru').rstrip('/') + '/subscriptions',
|
||||
headers={'Authorization': os.environ['MAX_BOT_TOKEN']}, method='GET'))
|
||||
print(r.read().decode())
|
||||
"
|
||||
```
|
||||
|
||||
В ответе должен быть твой URL в списке `subscriptions`. Если список пустой — заново запусти `python3 register_max_webhook.py`.
|
||||
|
||||
---
|
||||
|
||||
## Отписка от Webhook
|
||||
|
||||
Чтобы отключить доставку на этот URL, вызови в MAX API:
|
||||
|
||||
```bash
|
||||
curl -X DELETE "https://platform-api.max.ru/subscriptions" \
|
||||
-H "Authorization: ВАШ_MAX_BOT_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"url": "https://n8n.clientright.pro/webhook/sprf_max"}'
|
||||
```
|
||||
|
||||
Точный формат отписки см. в документации MAX (DELETE /subscriptions).
|
||||
57
fetch_schema.py
Normal file
57
fetch_schema.py
Normal file
@@ -0,0 +1,57 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Выгрузка структуры таблиц sprf_ из PostgreSQL."""
|
||||
import os
|
||||
import psycopg2
|
||||
from pathlib import Path
|
||||
|
||||
# Загрузка .env
|
||||
env_path = Path(__file__).parent / ".env"
|
||||
for line in env_path.read_text().splitlines():
|
||||
line = line.strip()
|
||||
if not line or line.startswith("#"):
|
||||
continue
|
||||
if "=" in line:
|
||||
k, v = line.split("=", 1)
|
||||
os.environ[k.strip()] = v.strip()
|
||||
|
||||
conn = psycopg2.connect(
|
||||
host=os.environ["PGHOST"],
|
||||
port=int(os.environ.get("PGPORT", 5432)),
|
||||
dbname=os.environ["PGDATABASE"],
|
||||
user=os.environ["PGUSER"],
|
||||
password=os.environ["PGPASSWORD"],
|
||||
)
|
||||
cur = conn.cursor()
|
||||
cur.execute("""
|
||||
SELECT table_name, column_name, data_type,
|
||||
character_maximum_length, is_nullable, column_default, ordinal_position
|
||||
FROM information_schema.columns
|
||||
WHERE table_schema = 'public' AND table_name LIKE 'sprf_%%'
|
||||
ORDER BY table_name, ordinal_position
|
||||
""")
|
||||
rows = cur.fetchall()
|
||||
cur.close()
|
||||
conn.close()
|
||||
|
||||
# Группировка по таблицам
|
||||
from collections import defaultdict
|
||||
by_table = defaultdict(list)
|
||||
for table_name, col_name, data_type, char_max, nullable, default, ord_pos in rows:
|
||||
by_table[table_name].append((col_name, data_type, char_max, nullable, default))
|
||||
|
||||
# Вывод в файл
|
||||
out_path = Path(__file__).parent / "sprf_tables_schema.md"
|
||||
lines = ["# Структура таблиц sprf_ (public, default_db)\n"]
|
||||
for table in sorted(by_table.keys()):
|
||||
lines.append(f"## {table}\n")
|
||||
lines.append("| Колонка | Тип | Размер | NULL | Default |\n")
|
||||
lines.append("|---------|-----|--------|------|--------|\n")
|
||||
for col_name, data_type, char_max, nullable, default in by_table[table]:
|
||||
size = str(char_max) if char_max else ""
|
||||
default_str = (default or "").strip()[:40]
|
||||
if len((default or "")) > 40:
|
||||
default_str += "..."
|
||||
lines.append(f"| {col_name} | {data_type} | {size} | {nullable} | {default_str} |\n")
|
||||
lines.append("\n")
|
||||
out_path.write_text("".join(lines), encoding="utf-8")
|
||||
print(f"Сохранено: {out_path}")
|
||||
41
n8n-code-add-menu-buttons.js
Normal file
41
n8n-code-add-menu-buttons.js
Normal file
@@ -0,0 +1,41 @@
|
||||
// Code node (Run once for each item)
|
||||
// Добавляет text, buttons и готовое тело для MAX (message_body с inline_keyboard, callback).
|
||||
|
||||
return items.map(item => {
|
||||
const text = "Вас давно не было. Выберите, чем хотите заняться:";
|
||||
const buttons = [
|
||||
{ title: "ℹ️ О сервисе", payload: "about" },
|
||||
{ title: "📝 Подать жалобу", payload: "complaint" },
|
||||
{ title: "📋 Мои обращения", payload: "my_tickets" },
|
||||
{ title: "💬 Поддержка", payload: "support" }
|
||||
];
|
||||
|
||||
// MAX: каждый ряд — одна кнопка (во всю ширину). Внизу кнопка "Главное меню" (type: message) —
|
||||
// при нажатии бот получит сообщение "/menu" и можно снова показать это меню.
|
||||
const callbackRows = buttons.map(b => [ { type: "callback", text: b.title, payload: b.payload } ]);
|
||||
// Кнопка "Главное меню": type message — при нажатии бот получит этот text.
|
||||
const menuButtonRow = [ { type: "message", text: "📋 Главное меню" } ];
|
||||
// Кнопка "Поделиться контактом": request_contact — MAX запросит телефон, в webhook придёт message_created с контактом (структуру смотри в payload).
|
||||
const contactButtonRow = [ { type: "request_contact", text: "📱 Отправить номер телефона" } ];
|
||||
const message_body = {
|
||||
text,
|
||||
format: "markdown",
|
||||
attachments: [
|
||||
{
|
||||
type: "inline_keyboard",
|
||||
payload: {
|
||||
buttons: [ ...callbackRows, contactButtonRow, menuButtonRow ]
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
return {
|
||||
json: {
|
||||
...item.json,
|
||||
text,
|
||||
buttons,
|
||||
message_body
|
||||
}
|
||||
};
|
||||
});
|
||||
151
n8n-code-node-max-normalize.js
Normal file
151
n8n-code-node-max-normalize.js
Normal file
@@ -0,0 +1,151 @@
|
||||
// Function node для n8n — нормализация входящего Webhook MAX
|
||||
// Выход: max_id, max_chat_id, answer_text, answer_type, channel: "max", reply_to_*, raw_update
|
||||
|
||||
const input = $input.first().json;
|
||||
// Тело Webhook: в n8n обычно item = { body, headers, params, query }; если прилетел только body — используем input как payload
|
||||
let raw = input?.body ?? input;
|
||||
if (typeof raw === 'string') {
|
||||
try { raw = JSON.parse(raw); } catch (_) {}
|
||||
}
|
||||
|
||||
// ----------------- 0) Игнорируем НЕ-private чаты (группы, каналы) -----------------
|
||||
// В MAX в личке приходит recipient с chat_type: "dialog". В группах/каналах — другой chat_type.
|
||||
const recipient = raw.message?.recipient ?? raw.recipient;
|
||||
const chatType = recipient?.chat_type ?? '';
|
||||
if (recipient && chatType !== '' && chatType !== 'dialog') {
|
||||
return []; // групповой чат или канал
|
||||
}
|
||||
|
||||
// ----------------- Утилиты -----------------
|
||||
const trim = (s) => (s || '').trim();
|
||||
const takeLast = (arr) => (Array.isArray(arr) && arr.length ? arr[arr.length - 1] : null);
|
||||
const safe = (v, fallback = null) => (v === undefined ? fallback : v);
|
||||
|
||||
const EMOJI_RE = /[\p{Extended_Pictographic}\u200D\uFE0F]/gu;
|
||||
function cleanTextForMeaning(txt) {
|
||||
if (!txt) return '';
|
||||
const noEmoji = txt.replace(EMOJI_RE, '').replace(/[\u2000-\u206F\u2E00-\u2E7F\\'!"#$%&()*+,\-./:;<=>?@[\\\]^_`{|}~]/g, ' ');
|
||||
return noEmoji.replace(/\s+/g, ' ').trim();
|
||||
}
|
||||
function isReactionOnly(originalText) {
|
||||
if (!originalText) return false;
|
||||
const cleaned = cleanTextForMeaning(originalText);
|
||||
if (cleaned.length === 0) return true;
|
||||
const lettersCount = (cleaned.match(/[A-Za-zА-Яа-я0-9]/g) || []).length;
|
||||
return lettersCount < 3 && originalText.trim().length <= 3;
|
||||
}
|
||||
|
||||
// ----------------- Результат -----------------
|
||||
const result = {
|
||||
max_id: null,
|
||||
max_chat_id: null,
|
||||
answer_text: null,
|
||||
answer_type: null,
|
||||
channel: 'max',
|
||||
raw_update: raw,
|
||||
reply_to_message_id: null,
|
||||
reply_to_from_id: null,
|
||||
reply_to_from_username: null,
|
||||
reply_to_text: null,
|
||||
};
|
||||
|
||||
const msg = raw?.message ?? raw;
|
||||
const body = msg?.body;
|
||||
const sender = msg?.sender ?? raw?.sender;
|
||||
|
||||
// ----- 1) ID пользователя и чата (MAX) -----
|
||||
// При message_callback сообщение от бота (sender = бот), нажал пользователь — он в raw.callback.user
|
||||
const callbackUser = raw?.callback?.user;
|
||||
const userId = callbackUser?.user_id ?? callbackUser?.id ?? sender?.user_id ?? sender?.id ?? raw?.user_id ?? msg?.sender?.user_id;
|
||||
result.max_id = userId;
|
||||
|
||||
const chatId = msg?.recipient?.chat_id ?? msg?.recipient?.user_id ?? recipient?.chat_id ?? recipient?.user_id ?? userId;
|
||||
result.max_chat_id = chatId;
|
||||
|
||||
// ----- 2) Ответ на сообщение / пересланное (message.link = LinkedMessage) -----
|
||||
const link = msg?.link;
|
||||
if (link) {
|
||||
result.reply_to_message_id = safe(link.message_id ?? link.id);
|
||||
result.reply_to_from_id = safe(link.sender?.user_id ?? link.sender?.id);
|
||||
result.reply_to_from_username = safe(link.sender?.username);
|
||||
if (link.body?.text) {
|
||||
result.reply_to_text = String(link.body.text).replace(/\r?\n/g, ' ').slice(0, 1000);
|
||||
} else if (link.body?.attachments?.length) {
|
||||
const first = link.body.attachments[0];
|
||||
const typeMap = { image: '[photo]', video: '[video]', audio: '[voice]', file: '[document]' };
|
||||
result.reply_to_text = typeMap[first?.type] ?? '[attachment]';
|
||||
}
|
||||
}
|
||||
|
||||
// ----- 3) Типы входящих: callback, message_created (текст/медиа), bot_started -----
|
||||
const updateType = raw.update_type;
|
||||
|
||||
if (updateType === 'message_callback') {
|
||||
// Нажатие кнопки: callback_id для POST /answers, payload — данные кнопки
|
||||
const callbackId = raw.callback_id ?? raw.callback?.callback_id ?? msg?.callback_id;
|
||||
const payload = raw.callback?.payload ?? raw.callback?.data ?? msg?.callback?.payload ?? msg?.callback?.data;
|
||||
result.answer_text = typeof payload === 'string' ? payload : (payload != null ? JSON.stringify(payload) : '');
|
||||
result.answer_type = 'callback';
|
||||
result.callback_id = callbackId;
|
||||
// Текст сообщения с кнопками — чтобы обновить его через POST /answers без кнопок (удалить клавиатуру)
|
||||
result.callback_message_text = msg?.body?.text ?? raw.message?.body?.text ?? null;
|
||||
result.callback_message_mid = msg?.body?.mid ?? raw.message?.body?.mid ?? null;
|
||||
} else if (updateType === 'bot_started') {
|
||||
result.answer_text = '/start';
|
||||
result.answer_type = 'command';
|
||||
} else if (updateType === 'message_created' && body) {
|
||||
const hasText = body.text && trim(body.text).length > 0;
|
||||
const attachments = body.attachments ?? [];
|
||||
const firstAtt = attachments[0];
|
||||
|
||||
if (firstAtt) {
|
||||
const type = firstAtt.type;
|
||||
if (type === 'contact') {
|
||||
// Поделился контактом (кнопка request_contact). MAX присылает payload.vcf_info (VCARD) и payload.max_info (user).
|
||||
const payload = firstAtt.payload ?? {};
|
||||
let phone = payload.phone_number ?? payload.phone ?? '';
|
||||
if (!phone && payload.vcf_info) {
|
||||
const m = payload.vcf_info.match(/TEL[^:]*:([+\d\s\-()]+)/);
|
||||
if (m) phone = m[1].replace(/\s/g, '').trim();
|
||||
}
|
||||
result.answer_text = phone || '[contact]';
|
||||
result.answer_type = 'contact';
|
||||
result.contact_payload = payload;
|
||||
if (payload.max_info) result.contact_name = payload.max_info.name ?? [payload.max_info.first_name, payload.max_info.last_name].filter(Boolean).join(' ');
|
||||
} else {
|
||||
// Вложение: image | video | audio | file
|
||||
result.answer_text = hasText ? body.text.replace(/\r?\n/g, ' ').trim() : (type === 'image' ? '[photo]' : type === 'video' ? '[video]' : type === 'audio' ? '[voice]' : '[document]');
|
||||
result.answer_type = type === 'image' ? 'photo' : type === 'video' ? 'video' : type === 'audio' ? 'voice' : 'file';
|
||||
if (firstAtt.payload?.token) result.attachment_token = firstAtt.payload.token;
|
||||
if (firstAtt.payload?.file_id) result.file_id = firstAtt.payload.file_id;
|
||||
if (firstAtt.payload) result.attachment_payload = firstAtt.payload;
|
||||
}
|
||||
} else if (body.contact) {
|
||||
// Контакт в body.contact (альтернативный формат MAX)
|
||||
const phone = body.contact.phone_number ?? body.contact.phone ?? '';
|
||||
result.answer_text = phone || '[contact]';
|
||||
result.answer_type = 'contact';
|
||||
result.contact_payload = body.contact;
|
||||
} else if (hasText) {
|
||||
// Только текст
|
||||
const rawText = body.text;
|
||||
if (isReactionOnly(rawText)) return [];
|
||||
result.answer_text = rawText.replace(/\r?\n/g, ' ').trim();
|
||||
result.answer_type = result.answer_text.startsWith('/') ? 'command' : 'text';
|
||||
} else {
|
||||
return [];
|
||||
}
|
||||
} else {
|
||||
return [];
|
||||
}
|
||||
|
||||
// ----- 4) Валидация -----
|
||||
if (result.max_id == null) throw new Error('Не удалось извлечь max_id');
|
||||
if (result.max_chat_id == null) throw new Error('Не удалось извлечь max_chat_id');
|
||||
if (!result.answer_type) throw new Error('Не удалось определить тип ответа');
|
||||
|
||||
// ----- 5) Нормализация строк "null" (как в старой ноде) -----
|
||||
if (raw.body?.last_name === 'null') raw.body.last_name = null;
|
||||
if (result.reply_to_text === 'null') result.reply_to_text = null;
|
||||
|
||||
return [{ json: result }];
|
||||
75
register_max_webhook.py
Normal file
75
register_max_webhook.py
Normal file
@@ -0,0 +1,75 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Регистрация Webhook бота MAX на URL n8n.
|
||||
Вызов: python3 register_max_webhook.py
|
||||
|
||||
Перед запуском:
|
||||
1. В n8n создан воркфлоу с нодой Webhook (Production), путь = последняя часть N8N_MAX_WORKFLOW
|
||||
(например, для https://n8n.clientright.pro/webhook/sprf_max путь в ноде = sprf_max).
|
||||
2. В .env заданы MAX_BOT_TOKEN, N8N_MAX_WORKFLOW (полный HTTPS URL webhook).
|
||||
3. По желанию задан MAX_WEBHOOK_SECRET — тогда MAX будет присылать его в заголовке X-Max-Bot-Api-Secret.
|
||||
"""
|
||||
import os
|
||||
import json
|
||||
import urllib.request
|
||||
from pathlib import Path
|
||||
|
||||
# Загрузка .env
|
||||
env_path = Path(__file__).parent / ".env"
|
||||
for line in env_path.read_text().splitlines():
|
||||
line = line.strip()
|
||||
if not line or line.startswith("#"):
|
||||
continue
|
||||
if "=" in line:
|
||||
k, v = line.split("=", 1)
|
||||
os.environ[k.strip()] = v.strip()
|
||||
|
||||
MAX_API_BASE = os.environ.get("MAX_API_BASE", "https://platform-api.max.ru").rstrip("/")
|
||||
MAX_BOT_TOKEN = os.environ.get("MAX_BOT_TOKEN", "").strip()
|
||||
N8N_MAX_WORKFLOW = os.environ.get("N8N_MAX_WORKFLOW", "").strip()
|
||||
MAX_WEBHOOK_SECRET = os.environ.get("MAX_WEBHOOK_SECRET", "").strip()
|
||||
|
||||
if not MAX_BOT_TOKEN:
|
||||
print("Ошибка: в .env не задан MAX_BOT_TOKEN")
|
||||
exit(1)
|
||||
if not N8N_MAX_WORKFLOW or not N8N_MAX_WORKFLOW.startswith("https://"):
|
||||
print("Ошибка: в .env задайте N8N_MAX_WORKFLOW — полный HTTPS URL webhook (например https://n8n.clientright.pro/webhook/sprf_max)")
|
||||
exit(1)
|
||||
|
||||
# Типы событий, которые бот будет получать
|
||||
update_types = ["message_created", "message_callback", "bot_started"]
|
||||
body = {"url": N8N_MAX_WORKFLOW, "update_types": update_types}
|
||||
if MAX_WEBHOOK_SECRET:
|
||||
body["secret"] = MAX_WEBHOOK_SECRET
|
||||
print("Секрет для проверки в n8n (X-Max-Bot-Api-Secret): задан в .env")
|
||||
else:
|
||||
print("Секрет не задан (MAX_WEBHOOK_SECRET в .env). Рекомендуется задать для проверки запросов в n8n.")
|
||||
|
||||
req = urllib.request.Request(
|
||||
f"{MAX_API_BASE}/subscriptions",
|
||||
data=json.dumps(body).encode("utf-8"),
|
||||
headers={
|
||||
"Authorization": MAX_BOT_TOKEN,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
method="POST",
|
||||
)
|
||||
try:
|
||||
with urllib.request.urlopen(req, timeout=15) as resp:
|
||||
data = resp.read().decode()
|
||||
out = json.loads(data) if data else {}
|
||||
if out.get("success") is True:
|
||||
print("Webhook зарегистрирован успешно.")
|
||||
print("URL:", N8N_MAX_WORKFLOW)
|
||||
print("Типы событий:", ", ".join(update_types))
|
||||
else:
|
||||
print("Ответ API:", data)
|
||||
exit(1)
|
||||
except urllib.error.HTTPError as e:
|
||||
body = e.read().decode()
|
||||
print(f"Ошибка HTTP {e.code}: {e.reason}")
|
||||
print("Тело ответа:", body)
|
||||
exit(1)
|
||||
except Exception as e:
|
||||
print("Ошибка:", e)
|
||||
exit(1)
|
||||
217
sprf_tables_schema.md
Normal file
217
sprf_tables_schema.md
Normal file
@@ -0,0 +1,217 @@
|
||||
# Структура таблиц sprf_ (public, default_db)
|
||||
## sprf_chat_messages
|
||||
| Колонка | Тип | Размер | NULL | Default |
|
||||
|---------|-----|--------|------|--------|
|
||||
| id | integer | | NO | nextval('sprf_chat_messages_id_seq'::reg... |
|
||||
| claim_id | uuid | | YES | |
|
||||
| from_user | boolean | | YES | |
|
||||
| message_text | text | | YES | |
|
||||
| file_id | text | | YES | |
|
||||
| sent_at | timestamp with time zone | | YES | |
|
||||
|
||||
## sprf_claim_documents
|
||||
| Колонка | Тип | Размер | NULL | Default |
|
||||
|---------|-----|--------|------|--------|
|
||||
| id | uuid | | NO | |
|
||||
| claim_id | character varying | | YES | |
|
||||
| field_name | text | | YES | |
|
||||
| file_id | text | | YES | |
|
||||
| uploaded_at | timestamp with time zone | | YES | |
|
||||
|
||||
## sprf_claim_statuses
|
||||
| Колонка | Тип | Размер | NULL | Default |
|
||||
|---------|-----|--------|------|--------|
|
||||
| code | text | | NO | |
|
||||
| description | text | | YES | |
|
||||
|
||||
## sprf_claim_types
|
||||
| Колонка | Тип | Размер | NULL | Default |
|
||||
|---------|-----|--------|------|--------|
|
||||
| code | text | | NO | |
|
||||
| description | text | | YES | |
|
||||
|
||||
## sprf_claims
|
||||
| Колонка | Тип | Размер | NULL | Default |
|
||||
|---------|-----|--------|------|--------|
|
||||
| id | character varying | | NO | |
|
||||
| user_id | integer | | YES | |
|
||||
| type_code | text | | YES | |
|
||||
| status_code | text | | YES | |
|
||||
| policy_number | text | | YES | |
|
||||
| payload | jsonb | | YES | |
|
||||
| created_at | timestamp with time zone | | YES | |
|
||||
| updated_at | timestamp with time zone | | YES | |
|
||||
| session_token | character varying | | YES | |
|
||||
| unified_id | character varying | | YES | |
|
||||
| telegram_id | bigint | | YES | |
|
||||
| channel | text | | YES | |
|
||||
| is_confirmed | boolean | | YES | false |
|
||||
|
||||
## sprf_conversation_state
|
||||
| Колонка | Тип | Размер | NULL | Default |
|
||||
|---------|-----|--------|------|--------|
|
||||
| user_id | integer | | NO | |
|
||||
| current_step | text | | YES | |
|
||||
| data | jsonb | | YES | |
|
||||
| updated_at | timestamp with time zone | | YES | |
|
||||
|
||||
## sprf_court_decisions
|
||||
| Колонка | Тип | Размер | NULL | Default |
|
||||
|---------|-----|--------|------|--------|
|
||||
| id | uuid | | NO | gen_random_uuid() |
|
||||
| uuid | character varying | 36 | NO | (gen_random_uuid())::text |
|
||||
| file_name | character varying | 500 | NO | |
|
||||
| file_size | bigint | | YES | |
|
||||
| mime_type | character varying | 100 | YES | |
|
||||
| file_hash | character varying | 64 | NO | |
|
||||
| s3_url | text | | NO | |
|
||||
| telegram_message_id | bigint | | YES | |
|
||||
| telegram_chat_id | bigint | | YES | |
|
||||
| telegram_user_id | bigint | | NO | |
|
||||
| telegram_username | character varying | 255 | YES | |
|
||||
| telegram_full_name | character varying | 500 | YES | |
|
||||
| ocr_processed | boolean | | YES | false |
|
||||
| ocr_processed_at | timestamp with time zone | | YES | |
|
||||
| ocr_text | text | | YES | |
|
||||
| ocr_pages_data | jsonb | | YES | |
|
||||
| ocr_pages_count | integer | | YES | |
|
||||
| vector_processed | boolean | | YES | false |
|
||||
| vector_processed_at | timestamp with time zone | | YES | |
|
||||
| vector_store_id | character varying | 255 | YES | |
|
||||
| vector_file_ids | jsonb | | YES | |
|
||||
| nsfw_checked | boolean | | YES | false |
|
||||
| nsfw_result | boolean | | YES | false |
|
||||
| nsfw_score | numeric | | YES | |
|
||||
| processing_status | character varying | 50 | YES | 'pending'::character varying |
|
||||
| processing_error | text | | YES | |
|
||||
| uploaded_at | timestamp with time zone | | YES | CURRENT_TIMESTAMP |
|
||||
| updated_at | timestamp with time zone | | YES | CURRENT_TIMESTAMP |
|
||||
| crm_claim_id | integer | | YES | |
|
||||
| crm_project_id | integer | | YES | |
|
||||
| metadata | jsonb | | YES | '{}'::jsonb |
|
||||
| court_raw_json | jsonb | | YES | '{}'::jsonb |
|
||||
|
||||
## sprf_court_decisions_view
|
||||
| Колонка | Тип | Размер | NULL | Default |
|
||||
|---------|-----|--------|------|--------|
|
||||
| id | uuid | | YES | |
|
||||
| uuid | character varying | 36 | YES | |
|
||||
| file_name | character varying | 500 | YES | |
|
||||
| file_size | bigint | | YES | |
|
||||
| mime_type | character varying | 100 | YES | |
|
||||
| telegram_user_id | bigint | | YES | |
|
||||
| telegram_username | character varying | 255 | YES | |
|
||||
| telegram_full_name | character varying | 500 | YES | |
|
||||
| ocr_processed | boolean | | YES | |
|
||||
| ocr_pages_count | integer | | YES | |
|
||||
| vector_processed | boolean | | YES | |
|
||||
| processing_status | character varying | 50 | YES | |
|
||||
| uploaded_at | timestamp with time zone | | YES | |
|
||||
| updated_at | timestamp with time zone | | YES | |
|
||||
| ocr_text_preview | text | | YES | |
|
||||
| crm_claim_id | integer | | YES | |
|
||||
| crm_project_id | integer | | YES | |
|
||||
|
||||
## sprf_dialog_history_tg
|
||||
| Колонка | Тип | Размер | NULL | Default |
|
||||
|---------|-----|--------|------|--------|
|
||||
| id | integer | | NO | nextval('sprf_dialog_history_tg_id_seq':... |
|
||||
| telegram_id | bigint | | YES | |
|
||||
| role | character varying | | YES | |
|
||||
| message | text | | YES | |
|
||||
| created_at | timestamp with time zone | | YES | now() |
|
||||
| session_token | character varying | | YES | |
|
||||
| claim_id | character varying | | YES | |
|
||||
| message_type | text | | YES | |
|
||||
| payload | jsonb | | YES | |
|
||||
| tg_message_id | bigint | | YES | |
|
||||
| tg_update_id | bigint | | YES | |
|
||||
|
||||
## sprf_document_embeddings
|
||||
| Колонка | Тип | Размер | NULL | Default |
|
||||
|---------|-----|--------|------|--------|
|
||||
| document_id | uuid | | YES | |
|
||||
| chunk_index | integer | | YES | |
|
||||
| embedding | USER-DEFINED | | YES | |
|
||||
|
||||
## sprf_documents
|
||||
| Колонка | Тип | Размер | NULL | Default |
|
||||
|---------|-----|--------|------|--------|
|
||||
| id | uuid | | NO | |
|
||||
| source | text | | YES | |
|
||||
| content | text | | YES | |
|
||||
| metadata | jsonb | | YES | |
|
||||
| created_at | timestamp with time zone | | YES | |
|
||||
|
||||
## sprf_operators
|
||||
| Колонка | Тип | Размер | NULL | Default |
|
||||
|---------|-----|--------|------|--------|
|
||||
| id | integer | | NO | nextval('sprf_operators_id_seq'::regclas... |
|
||||
| telegram_id | bigint | | YES | |
|
||||
| name | text | | YES | |
|
||||
| is_active | boolean | | YES | |
|
||||
| created_at | timestamp with time zone | | YES | |
|
||||
|
||||
## sprf_sessions
|
||||
| Колонка | Тип | Размер | NULL | Default |
|
||||
|---------|-----|--------|------|--------|
|
||||
| id | uuid | | NO | gen_random_uuid() |
|
||||
| user_id | integer | | YES | |
|
||||
| session_token | character varying | | YES | |
|
||||
| created_at | timestamp with time zone | | YES | now() |
|
||||
| last_activity | timestamp with time zone | | YES | |
|
||||
| expires_at | timestamp with time zone | | YES | |
|
||||
|
||||
## sprf_user_accounts
|
||||
| Колонка | Тип | Размер | NULL | Default |
|
||||
|---------|-----|--------|------|--------|
|
||||
| id | integer | | NO | nextval('sprf_user_accounts_id_seq'::reg... |
|
||||
| user_id | integer | | YES | |
|
||||
| channel | text | | YES | |
|
||||
| channel_user_id | text | | YES | |
|
||||
|
||||
## sprf_users
|
||||
| Колонка | Тип | Размер | NULL | Default |
|
||||
|---------|-----|--------|------|--------|
|
||||
| id | integer | | NO | nextval('sprf_users_id_seq'::regclass) |
|
||||
| universal_id | uuid | | YES | |
|
||||
| phone | character varying | | YES | |
|
||||
| created_at | timestamp with time zone | | YES | |
|
||||
| updated_at | timestamp with time zone | | YES | |
|
||||
| unified_id | character varying | | YES | |
|
||||
|
||||
## sprf_users_tg
|
||||
| Колонка | Тип | Размер | NULL | Default |
|
||||
|---------|-----|--------|------|--------|
|
||||
| telegram_id | bigint | | NO | |
|
||||
| phone_number | character varying | | YES | |
|
||||
| first_name_tg | character varying | | YES | |
|
||||
| last_name_tg | character varying | | YES | |
|
||||
| username | character varying | | YES | |
|
||||
| language_code | character varying | | YES | |
|
||||
| is_premium | boolean | | YES | |
|
||||
| unified_id | character varying | | YES | |
|
||||
| birth_date | character varying | | YES | |
|
||||
| birth_place | character varying | | YES | |
|
||||
| inn | character varying | | YES | |
|
||||
| address | character varying | | YES | |
|
||||
| email | character varying | | YES | |
|
||||
| created_at | timestamp with time zone | | YES | now() |
|
||||
| updated_at | timestamp with time zone | | YES | now() |
|
||||
| first_name | character varying | | YES | |
|
||||
| last_name | character varying | | YES | |
|
||||
| middle_name | character varying | | YES | |
|
||||
| is_confirmed | boolean | | YES | false |
|
||||
|
||||
## sprf_wizard_questions
|
||||
| Колонка | Тип | Размер | NULL | Default |
|
||||
|---------|-----|--------|------|--------|
|
||||
| id | integer | | NO | nextval('sprf_wizard_questions_id_seq'::... |
|
||||
| claim_type | text | | YES | |
|
||||
| step_key | text | | YES | |
|
||||
| question_text | text | | YES | |
|
||||
| answer_type | text | | YES | |
|
||||
| step_order | integer | | YES | |
|
||||
| options | jsonb | | YES | |
|
||||
| is_required | boolean | | YES | |
|
||||
|
||||
Reference in New Issue
Block a user