Baseline md2vk: docs, audit log, docker 8420, openspec, deploy

This commit is contained in:
estorozhenko
2026-09-18 22:05:01 +00:00
commit 09e960a3a9
39 changed files with 3716 additions and 0 deletions
+84
View File
@@ -0,0 +1,84 @@
# md2vk — Реквизиты и доступы
**Цель этого файла — собрать все реквизиты/доступы к смежным сервисам, чтобы не искать их потом.**
Секреты (токены, пароли) в git **не** помещаются — вместо них указывается файл-источник. Пароль
приложения хранится только как bcrypt-хэш (см. ниже).
## Схема продакшена
```
Внешний мир
│ https://md2vk.nixg.ru
▼
vps02 (87.242.100.206) ── Caddy (:443, basic auth + fail2ban)
│ reverse_proxy 10.8.0.2:8420 (WireGuard)
▼
bigbox (10.8.0.2) ── docker-контейнер md2vk ── 127.0.0.1:8420 ── SQLite /opt/md2vk/data/md2vk.db
│
▼ VK API: https://api.vk.com/method/wall.post
```
| Роль | Значение |
|------|----------|
| Прод-домен | `md2vk.nixg.ru` → A 87.242.100.206 (vps02) |
| bigbox | 10.8.0.2 (WireGuard), хост проекта `/opt/md2vk` |
| vps02 | 87.242.100.206, ssh-алиас `vps02` (estorozhenko, sudo по ключу) |
| Caddy | контейнер `caddy` на vps02, конфиг `/opt/caddy/Caddyfile`, reload: `docker exec caddy caddy reload` |
| Порт приложения | 127.0.0.1:8420 на bigbox (контейнер), извне не торчит |
| БД | SQLite `/opt/md2vk/data/md2vk.db` (в docker — `/data/md2vk.db`) |
## Git
| Forge | URL | Логин | Как авторизоваться |
|-------|-----|-------|--------------------|
| **gitverse.ru (истина)** | `git@gitverse.ru:kpa39l/md2vk.git` | **kpa39l** | SSH-ключ `/home/estorozhenko/.ssh/gitverse`; PAT — в `git-tokens.env` |
| gitea.nixg.ru (pull mirror) | `https://gitea.nixg.ru/estorozhenko/md2vk.git` | estorozhenko | токен `GITEA_NIXG_TOKEN` |
- Источник токенов: `/opt/hermes/.hermes/secrets/git-tokens.env` (`source` его, не искать токены по каталогам).
Переменные: `GITVERSE_LOGIN=kpa39l`, `GITVERSE_PAT`, `GITEA_NIXG_API`, `GITEA_NIXG_TOKEN`.
- Gitea локальный API (bigbox): `http://127.0.0.1:3000/api/v1`, токен `GITEA_TOKEN` (там же).
- Правила работы с зеркалами: см. skill `git-forge-management`, reference `gitea-pull-mirror-gitverse.md`
(migrate работает только по HTTPS+PAT: `https://oauth2:<PAT>@gitverse.ru/kpa39l/md2vk.git`).
- `tea` CLI: `~/.local/bin/tea`, логин `gitea.nixg-full` — для работы с gitea.nixg.ru.
## Доступы к сервисам (смежные)
| Сервис | URL/адрес | Доступ |
|--------|-----------|--------|
| DNS nixg.ru | NS: ns1..ns4.jino.ru (Jino) | панель регистратора Jino (кто владеет доменом), A-записи |
| VK API | https://dev.vk.com/ru/reference | открытая документация |
| VK ID (приложение) | https://id.vk.ru/about/business/go/docs/ru/vkid/latest/vk-id/connection/create-application | нужен аккаунт VK для client_id/secret |
| Caddy vps02 | /opt/caddy/Caddyfile | ssh vps02, sudo |
| fail2ban vps02 | jail local, `/etc/fail2ban/` | ssh vps02, sudo |
## Пароль приложения (basic auth на Caddy)
- Пользователь: **estorozhenko**
- Пароль: задан пользователем (md2vk!Ghjcgtrn73 — в менеджере паролей пользователя; в git не хранится).
- В Caddyfile: bcrypt-хэш: `$2a$14$T5gAji7gmg1t3Ifxw0CJy.jat9vpFfOiJIE8j5bnHynEVH1q.QTPS` (генерируется заново при создании записи).
- Генерация: `docker exec caddy caddy hash-password --plaintext '<пароль>'`.
- Блокировка: fail2ban ban IP после 5 неудачных попыток подряд, ручной unban:
`fail2ban-client -c /etc/fail2ban unban <ip>` (на vps02). Детали — [security.md](security.md).
## VK OAuth (для OAuth-флоу VK ID)
- Ссылка авторизации: `https://oauth.vk.com/authorize?client_id=...&scope=wall,offline&redirect_uri=...&response_type=token`
- API: `https://api.vk.com/method/wall.post?access_token=...&owner_id=...&message=...&v=5.199`
- Версия API в конфиге: `VK_API_VERSION=5.199`
- Требуется пользовательский токен (не токен сообщества из настроек группы); для стены группы — токен администратора с `wall`, `photos`, публикация через `from_group=1`.
- Ограничение VK: создать статью (лонгрид) через API нельзя; через `wall.post` можно прикрепить уже опубликованную: `attachments=articleXXX_YYY`. Подробности — [vk-api.md](vk-api.md).
## Файлы секретов проекта (не в git)
| Файл | Что это |
|------|---------|
| `/opt/md2vk/secrets/token_encryption_key` | Fernet-ключ шифрования VK-токенов (генерируется `python3 -c "from cryptography.fernet import Fernet; print(Fernet.generate_key().decode())"`) |
| `/opt/md2vk/secrets/estorozhenko_api_key.txt` | API-ключ пользователя estorozhenko (md2vk_...) для вызова API |
| `/opt/hermes/.hermes/secrets/git-tokens.env` | токены gitverse/gitea |
## Наблюдаемость
- Лог авторизации/запросов API: `/opt/md2vk/logs/access.log` (JSONL) и `access.{YYYY-MM-DD}.log` (ротация по дням).
В docker: каталог монтируется как `/logs`. Поля: `ts, ip, method, path, api_key_prefix, user_id, status, success, latency_ms, error`.
- Health: `GET /api/v1/health` → `{"status":"ok"}`.
- Метрики доступности/успешности/активной сессии — собираются из лога (см. deploy.md).
+84
View File
@@ -0,0 +1,84 @@
# md2vk — Архитектура
## Обзор
FastAPI-сервис, публикует Markdown на стену VK через `wall.post` с `format_data`.
Полный стек: Python 3.12, FastAPI, SQLAlchemy 2.0 async, SQLite, httpx, cryptography (Fernet), Docker.
## Компоненты
```
┌─────────────────────────────── /opt/md2vk (bigbox) ───────────────────────────────┐
│ │
│ docker (контейнер md2vk, порт 127.0.0.1:8420) │
│ ┌────────────────────────────────────────────────────────────────────┐ │
│ │ app/main.py FastAPI entrypoint, lifespan → init_db │ │
│ │ app/config.py Settings из env (pydantic-settings) │ │
│ │ app/database.py async engine (aiosqlite), get_db │ │
│ │ app/models.py ORM: User, VkAccount, Publication │ │
│ │ app/security.py Fernet encrypt/decrypt, API-key gen/verify │ │
│ │ app/vk_client.py httpx-клиент: wall.post, users.get │ │
│ │ app/converters/markdown_to_vk.py MD → VK format_data + чанки │ │
│ │ app/api/v1.py эндпоинты /api/v1/* │ │
│ │ app/api/deps.py auth по API-ключу (Bearer / тело) │ │
│ └────────────────────────────────────────────────────────────────────┘ │
│ │
│ /data (volume) → SQLite /data/md2vk.db │
│ /logs → access.{date}.log (JSONL аудит) │
│ /run/secrets/token_encryption_key (Docker secret) │
└───────────────────────────────────────────────────────────────────────────────────┘
```
## Поток публикации
1. Клиент (Hermes/skill) `POST /api/v1/publish` c `api_key`, `vk_account_id`, `message_md`
2. Auth: hash api_key → User.is_active
3. Load VkAccount → decrypt_token (Fernet, только в памяти)
4. `markdown_to_vk(message_md)` → chunks (VK-лимит ~4096 симв., режет по абзацам; `@` = 2 симв.)
5. Publication (status=draft|scheduled)
6. Если `publish_date` задан → status=scheduled (планировщик — TODO)
7. Иначе `vk.wall_post(message, owner_id, from_group, ..., format_data=items)`
8. Ответ: post_id, owner_id, url `https://vk.com/wall{owner}_{post}`; ошибки → status=error + error_message
## Порт/интерфейсы
| Интерфейс | Адрес | Назначение |
|-----------|-------|------------|
| Локальный | 127.0.0.1:8420 | внутренний (Caddy на vps02 → 10.8.0.2:8420) |
| VK API | https://api.vk.com/method | исходящий (wall.post, users.get) |
| Swagger | /docs | openapi, за basic auth |
## База данных (SQLite)
| Таблица | Ключевые поля |
|---------|---------------|
| users | id, name, email, api_key_hash, api_key_prefix, is_active, created_at, updated_at |
| vk_accounts | id, user_id→users, vk_user_id (owner_id, `-` = сообщество), display_name, access_token_enc (Fernet), token_type (user\|group), is_active, expires_at, last_used_at |
| publications | id, vk_account_id→vk_accounts, status (draft\|scheduled\|published\|error), markdown_original, vk_text, vk_format_data, vk_post_id, vk_owner_id, attachments, scheduled_at, published_at, error_message |
## Отложенные посты
Модель поддерживает `scheduled_at`, `/publish` с `publish_date` создаёт запись `scheduled`.
**Планировщика нет** — открытая задача (worker-процесс/cron, выбирающий `status=scheduled AND scheduled_at<=now`).
## Ошибки VK API
Код | Смысл
----|------
`VkApiError` | обёртка: `error_code` + `error_msg` из ответа VK; пишется в `publications.error_message`
## Конвертер Markdown
| Markdown | VK format |
|----------|-----------|
| `**bold**`, `__bold__` | `bold` |
| `*italic*`, `_italic_` | `italic` |
| `***bold italic***` | `bold` + `italic` |
| `` `code` `` | `inline_code` |
| `[text](url)` | `link` (+url) |
| `# H1..H6` | `bold` (uppercase) |
| `> quote` | `italic` |
| ```` ```code```` | `code` (без format) |
| `---` | `───` |
Длинный текст режется на чанки по `\n\n` (абзацы), запас 10% от лимита 4096.
+127
View File
@@ -0,0 +1,127 @@
# md2vk — Деплой
## Топология
```
https://md2vk.nixg.ru (Caddy на vps02, basic auth)
└─ reverse_proxy 10.8.0.2:8420 (WireGuard: vps02 .4 → bigbox .1)
└─ docker-контейнер md2vk на bigbox, 127.0.0.1:8420
```
- Порт приложения **8420** (8000 занят docker-search-api на bigbox).
- Контейнер слушает на 127.0.0.1:8420 (снаружи bigbox не открыт).
## bigbox (10.8.0.2) — приложение
```bash
cd /opt/md2vk
make docker-up # docker compose up -d --build
docker ps | grep md2vk # статус
curl -s http://127.0.0.1:8420/api/v1/health # {"status":"ok"}
```
- Данные: named volume `md2vk_data` → `/data/md2vk.db` (SQLite).
- Секрет: `./secrets/token_encryption_key` → `/run/secrets/token_encryption_key`.
- Логи аудита: `./logs/` монтируется в `/logs`.
- Health: `/api/v1/health`, проверяется Docker healthcheck.
- Восстановление после перезагрузки: `restart: unless-stopped`.
- Если надо пересоздать БД — удалить том (только по явной команде, данные!):
`docker compose down -v && make docker-up` (НЕ делать без подтверждения).
## Создание пользователя и API-ключа
```bash
cd /opt/md2vk
source venv/bin/activate
TOKEN_ENCRYPTION_KEY_FILE=/opt/md2vk/secrets/token_encryption_key ./scripts/create_user.py \
--name estorozhenko --api-key-out secrets/estorozhenko_api_key.txt
# вывод: API key сохранён в secrets/estorozhenko_api_key.txt (md2vk_...)
```
Источник: `scripts/create_user.py` (добавляет User с сгенерированным ключом; ключ отображается один раз).
## vps02 (87.242.100.206) — Caddy
Файл `/opt/caddy/Caddyfile` на vps02, секция:
```
md2vk.nixg.ru {
basic_auth /* {
estorozhenko $2a$14$T5gAji7gmg1t3Ifxw0CJy.jat9vpFfOiJIE8j5bnHynEVH1q.QTPS
}
reverse_proxy 10.8.0.2:8420
}
```
Применить (контейнер caddy, монтирует Caddyfile):
```bash
ssh vps02
docker exec caddy caddy validate --config /etc/caddy/Caddyfile # проверить синтаксис
docker exec caddy caddy reload --config /etc/caddy/Caddyfile # перезагрузить
# если нужно: docker restart caddy
```
- TLS: Caddy автоматически выпускает Let's Encrypt сертификат для md2vk.nixg.ru (acme в Caddyfile: `email kpa39l@yandex.ru`).
- Сертификат на caddy_data (volume), переживает рестарты.
## vps02 — fail2ban (блокировка после 5 попыток)
Пакет ставится на vps02 (Debian/Ubuntu):
```bash
ssh vps02
sudo apt-get update && sudo apt-get install -y fail2ban
```
Конфиг jail (`/etc/fail2ban/jail.local`) и фильтр (`/etc/fail2ban/filter.d/md2vk.conf`):
```ini
# /etc/fail2ban/filter.d/md2vk.conf
[Definition]
failregex = ^.*"status":401.*
ignoreregex =
```
```ini
# /etc/fail2ban/jail.local
[md2vk]
enabled = true
port = http,https
filter = md2vk
logpath = /var/log/caddy/access.log
maxretry = 5
findtime = 600
bantime = -1
action = iptables-allports
```
- Лог Caddy в JSON: настроить в Caddyfile `log { output file /var/log/caddy/access.log format json }` (глобальный блок).
- Перезапуск: `sudo systemctl restart fail2ban`.
- Проверка: `sudo fail2ban-client status md2vk`.
- Ручной unban: `sudo fail2ban-client -c /etc/fail2ban unban <ip>`.
## Сквозная проверка
```bash
# 1. health через Caddy (без auth → 401)
curl -s -o /dev/null -w "%{http_code}\n" https://md2vk.nixg.ru/api/v1/health # 401
# 2. health c auth → 200
curl -s -u 'estorozhenko:<пароль>' https://md2vk.nixg.ru/api/v1/health # {"status":"ok"}
# 3. публикация не требует VK-токена? нет — конвертация:
curl -s -u 'estorozhenko:<пароль>' -H 'Authorization: Bearer <api_key>' \
-X POST https://md2vk.nixg.ru/api/v1/convert \
-H 'Content-Type: application/json' \
-d '{"message_md":"**test** *curl*"}' # format_data
# 4. баним проверкой: 5 раз неверный пароль → fail2ban ban IP
for i in 1 2 3 4 5 6; do curl -s -o /dev/null -w "%{http_code}\n" -u 'estorozhenko:wrong' https://md2vk.nixg.ru/api/v1/health; done
# → 401 ×6, затем соединение DROP (curl timeout)
sudo fail2ban-client status md2vk # IP в banned
sudo fail2ban-client unban <ваш-ip>
```
## Восстановление после аварии
- Приложение: `docker compose up -d --force-recreate` на bigbox.
- Caddy: `docker restart caddy` на vps02 (volume caddy_data хранит сертификаты).
- БД: том `md2vk_data`; бэкап = копия `/opt/md2vk/data/md2vk.db` (вне контейнера) — включена в homelab-backup (/opt/backup).
+65
View File
@@ -0,0 +1,65 @@
# md2vk — Документация
Указатель документации проекта. Все реквизиты и доступы — в [access.md](access.md).
## Разделы
| Файл | Содержание |
|------|------------|
| [index.md](index.md) | Этот указатель, обзор проекта |
| [access.md](access.md) | **Все реквизиты и доступы к смежным сервисам** (vps02, Caddy, gitverse, gitea, DNS, VK API, fail2ban) |
| [architecture.md](architecture.md) | Архитектура, компоненты, порты, схема |
| [vk-api.md](vk-api.md) | Порядок взаимодействия с VK API: OAuth-флоу, wall.post, format_data, ограничения (лонгриды) |
| [security.md](security.md) | Безопасность: пароль, basic auth, блокировка после 5 попыток, Fernet, API-ключ |
| [deploy.md](deploy.md) | Деплой: docker на bigbox, Caddy на vps02, fail2ban |
| [status.md](status.md) | Статус реализации по фазам (в т.ч. история) |
## Обзор
Сервис публикации Markdown-текста на стене VK через официальный VK API (`wall.post`).
```
┌─────────────┐ ┌─────────────┐ ┌──────────────┐
│ Hermes │────▶│ md2vk │────▶│ VK API │
│ Agent │ │ :8420 │ │ wall.post │
│ (skill) │◀────│ FastAPI │ │ vk.com │
└─────────────┘ └─────────────┘ └──────────────┘
```
- Публикация Markdown → VK wall.post с `format_data` (жирный, курсив, код, ссылки, заголовки, цитаты)
- Конвертация Markdown → VK format_data без публикации (preview)
- Управление VK-аккаунтами (несколько страниц), шифрование токенов (Fernet)
- Архив публикаций с фильтрацией по статусу
- Отложенные посты (статус `scheduled`; планировщик — открытая задача)
## Внешние точки
| Что | Адрес |
|-----|-------|
| Прод | https://md2vk.nixg.ru (basic auth, см. [security.md](security.md)) |
| Swagger | https://md2vk.nixg.ru/docs (за basic auth) |
| Локально | http://127.0.0.1:8420 (bigbox), Swagger http://127.0.0.1:8420/docs |
## Быстрый старт (dev)
```bash
cd /opt/md2vk
source venv/bin/activate
pip install -r requirements.txt
# ключ шифрования уже есть: secrets/token_encryption_key (не в git)
make dev # uvicorn :8000 --reload (для dev; прод-порт 8420 в docker)
```
## Git
Схема: **gitverse.ru (истина) → gitea.nixg.ru (pull mirror)**. Подробности в [access.md](access.md).
## Открытые задачи (см. [status.md](status.md) и openspec/)
- Планировщик отложенных постов (worker + cron)
- Web UI (форма поста + превью), сейчас только API
- OAuth-флоу VK VK ID (кнопка «Прикрепить аккаунт») — сейчас токен вводится вручную
- Unit/integration тесты (pytest)
- Rate limiting (in-memory bucket)
- Retry при сетевых ошибках VK API
- Read-only rootfs в Docker (частично: `read_only: true` в compose)
+71
View File
@@ -0,0 +1,71 @@
# md2vk — Безопасность
## Два уровня доступа
### 1. Внешний — Caddy basic_auth (на vps02)
- Весь https://md2vk.nixg.ru закрыт HTTP Basic Auth.
- Пользователь: **estorozhenko**; пароль — у пользователя, в конфиге только bcrypt-хэш.
- Хэш в Caddyfile: `admin $2a$14$T5gAji7gmg1t3Ifxw0CJy.jat9vpFfOiJIE8j5bnHynEVH1q.QTPS`
(генерируется `docker exec caddy caddy hash-password --plaintext '<пароль>'`).
- Без basic auth сервис наружу не отдаётся.
### 2. Внутренний — API-ключ (FastAPI)
- Все эндпоинты `/api/v1/*` (кроме `/health`) требуют API-ключ `md2vk_...`:
- в заголовке `Authorization: Bearer <api_key>` (GET) или в теле `{"api_key": ...}` (POST);
- API-ключ даёт право публиковать, но **не позволяет прочитать VK-токен** (raw токен не возвращается ни одним эндпоинтом).
- Хранится только SHA-256 хэш (`api_key_hash`), сравнение constant-time (`hmac.compare_digest`).
- Пользователь estorozhenko + API-ключ: создаётся скриптом, ключ кладётся в `secrets/estorozhenko_api_key.txt` (не в git).
## Блокировка после 5 неудачных попыток (fail2ban на vps02)
- На vps02 установлен fail2ban, jail `md2vk`:
- следит за логами Caddy (JSON: `/var/log/caddy/access.log`),
- фильтр: basic auth failure (`401` с `"err"`, absence of `"user_id"`):
```
^.*"status":401.*
```
- правило: ban IP после **5 неудачных попыток подряд** (`maxretry=5`, `findtime=600`),
- наказание: **ban до ручного снятия** (`bantime = -1`),
- действие: `iptables-allports` (ban на уровне ядра, DROP).
- **Ручная разблокировка** (на vps02, sudo):
```bash
fail2ban-client -c /etc/fail2ban unban <ip>
fail2ban-client -c /etc/fail2ban status md2vk # проверить
```
- Логи fail2ban: `journalctl -u fail2ban -e` или `/var/log/fail2ban.log`.
## Шифрование VK-токенов
- VK OAuth-токены в БД только в шифрованном виде: **Fernet (AES-128-CBC + HMAC-SHA256)**.
- Ключ — Docker secret `/run/secrets/token_encryption_key` (файл на bigbox: `secrets/token_encryption_key`, не в git).
- Расшифровка только в памяти при публикации.
## Rate limiting
- В конфиге `RATE_LIMIT_PER_MINUTE=10` (запросов/мин на VK-аккаунт).
- In-memory bucket — НЕ реализован (открытая задача).
## Аудит-лог
- Пишется JSONL в `/opt/md2vk/logs/access.{YYYY-MM-DD}.log` (вне контейнера — `logs/` на bigbox).
- Событие на каждый запрос `/api/v1/*`: `ts, ip, method, path, api_key_prefix, user_id, status, success, latency_ms, error`.
- Используется для мониторинга (доступность, успешность, активная сессия) и расследований.
- Caddy на vps02 пишет свой access-лог (используется fail2ban).
## Матрица рисков
| Сценарий | Последствия | Защита |
|---|---|---|
| Утечка .db | Токены зашифрованы | Fernet + Docker secret |
| Утечка API-ключа | Можно постить, но не украсть токен | API-ключ ≠ VK-токен |
| Брутфорс basic auth | Полный доступ к веб-интерфейсу API | fail2ban ban после 5 попыток, ручной unban |
| Перехват HTTP | — | HTTPS (Caddy/Let's Encrypt) |
| Компрометация контейнера | Полный доступ к токенам | read-only rootfs, audit log |
| Отзыв токена VK | Пост не выйдет | VK вернёт ошибку → error_message |
## Правило пользователя
> Никогда не удалять файлы пользователя без явного подтверждения. Пароли и токены не сохранять в коде —
> только через переменные окружения, Docker secrets или файлы, исключённые из git (.gitignore).
+59
View File
@@ -0,0 +1,59 @@
# md2vk — Статус (история изменений)
> Актуальный статус работ — в `openspec/` и `todo`-файлах; здесь хроника и границы.
## 2026-09: Документация, деплой, git, openspec
**Сделано:**
- [x] Аудит проекта (FastAPI ядро, Phase 1)
- [x] Документация в `docs/` (index, access, architecture, vk-api, security, deploy, status) + `AGENTS.md`
- [x] Аудит-лог авторизации (JSONL, ротация по дням) — см. `app/api/audit.py` + middleware
- [x] Docker: порт 8420 (8000 занят), bind `./data`→`/data`, `./logs`→`/logs`, HEALTHCHECK через python (curl в slim-образе нет)
- [x] openspec инициализирован (specs/ baseline)
- [x] git init, push на gitverse.ru (kpa39l/md2vk), pull mirror в gitea.nixg.ru (estorozhenko/md2vk)
- [x] Caddy на vps02: md2vk.nixg.ru + basic_auth estorozhenko (bcrypt), reverse_proxy 10.8.0.2:8420
- [x] fail2ban на vps02: jail md2vk, ban после 5 попыток, ручной unban
- [x] Сквозная проверка: https + auth + блокировка + unban
## Phase 1 — Ядро (DONE, из STATUS.md)
- Каркас проекта, структура директорий
- Config из env (pydantic-settings)
- Async SQLAlchemy + SQLite, инициализация БД
- ORM-модели (User, VkAccount, Publication)
- Fernet-шифрование/дешифрование токенов
- Генерация и верификация API-ключей
- Конвертер Markdown → VK format_data
- VK API клиент (wall.post, users.get, check_token)
- Pydantic-схемы запросов/ответов
- API: /health, /accounts (CRUD), /publish, /convert, /publications
- Аутентификация по API-ключу (заголовок + тело)
- Генерация ключа шифрования
- Проверка импортов и запуск
- Тест конвертации через API (curl)
## Открытые задачи
- [ ] Планировщик отложенных постов (status=scheduled → wall.post). Модель готова, worker нет.
- [ ] OAuth-флоу VK ID (кнопка «Прикрепить аккаунт»); нужны client_id/secret VK-приложения
- [ ] Web UI (форма поста + превью)
- [ ] Unit-тесты конвертера (pytest)
- [ ] Integration-тесты API (pytest + httpx)
- [ ] Rate limiting (in-memory bucket, RATE_LIMIT_PER_MINUTE)
- [ ] Retry при сетевых ошибках VK API
- [ ] Graceful shutdown
- [ ] Read-only rootfs (уже `read_only: true` в compose, но /logs мешает; пересмотреть)
- [ ] CORS (ограничение по origin)
- [ ] Обработка длинных постов (multipart: пост + комментарии)
- [ ] Hermes skill md2vk (проверка сервиса, публикация, добавление аккаунта)
## Границы Phase 1
- Конвертация Markdown → VK format_data (жирный, курсив, код, ссылки, заголовки, цитаты, блоки кода)
- Публикация поста на стене VK
- Шифрование токенов (Fernet + Docker secret)
- Аутентификация по API-ключу
- Управление VK-аккаунтами (добавить, список, удалить)
- Архив публикаций с фильтрацией
- Конвертация без публикации (preview)
- Отложенные посты: только запись `scheduled` (без планировщика)
+83
View File
@@ -0,0 +1,83 @@
# md2vk — Взаимодействие с VK API
> Источник: официальная документация VK (dev.vk.com / id.vk.ru), проверено 2026-09.
> Услуга работает через официальный протокол OAuth 2.0 и VK API — никакого «взлома».
## Как это работает
1. **OAuth-авторизация.** Пользователь нажимает «Прикрепить аккаунт» → редирект на VK.
Пользователь подтверждает вход и выдаёт приложению права (scopes, обязателен `wall`).
Сервис не получает пароль — только временный код/токен.
2. **Access Token.** VK возвращает сервису access token — цифровой ключ с ограниченными правами
(например, только публикация записей).
3. **Публикация через API.** Сервис вызывает `wall.post` с текстом/вложениями, `owner_id`, `from_group`.
Авторизация — `access_token` в параметрах запроса.
## Ссылки на официальную документацию
| Что | Ссылка |
|-----|--------|
| Создание и настройка приложения (VK ID, Standalone) | https://id.vk.com/about/business/go/docs/ru/vkid/latest/vk-id/connection/create-application |
| Общая документация по API | https://dev.vk.com/ru/reference |
| Авторизация (OAuth 2.0/2.1) | https://id.vk.com/about/business/go/docs/ru/vkid/latest/vk-id/connection/start-integration/auth-without-sdk/auth-without-sdk-web |
| Метод wall.post | https://dev.vk.com/ru/method/wall.post |
| Создать приложение (dev.vk.com) | https://dev.vk.com |
## OAuth-флоу (ссылка для получения токена)
```
https://oauth.vk.com/authorize?client_id=<client_id>&scope=wall,offline&redirect_uri=<redirect>&response_type=token
```
- `client_id` — ID приложения VK (см. VK ID).
- `scope` — обязателен `wall`; `offline` — долгоживущий токен.
- `response_type=token` — токен приходит в фрагменте редиректа.
- После ответа: `access_token`, `user_id`, `expires_in` (0 = бессрочно).
## Публикация (wall.post)
```
https://api.vk.com/method/wall.post
POST data: access_token, v=5.199, owner_id, message, from_group, attachments, publish_date, format_data
```
Параметры в нашем клиенте (`app/vk_client.py`):
| Параметр | Когда | Значение |
|----------|-------|----------|
| `owner_id` | группа | отрицательный ID сообщества (`-123`), для пользователя не передаём |
| `from_group` | группа | `1` — пост от имени группы |
| `friends_only` | опц. | только друзьям |
| `publish_date` | отложка | Unix timestamp |
| `attachments` | опц. | `photo123_456,...` |
| `signed` | группа | подпись автора |
| `format_data` | всегда | JSON `{"version":1,"items":[...]}` |
Ответ: `{"response":{"post_id":N,"owner_id":M}}` → URL `https://vk.com/wall{M}_{N}`.
### Важно про токены
- Для **личной стены** — токен пользователя.
- Для **стены сообщества** — токен **администратора группы** с правами `wall` и `photos`.
- Токены сообщества из настроек группы («Работа с API») для `wall.post` **не подходят** —
нужен именно пользовательский токен.
## Лонгриды (статьи) — ограничение
- **Создавать статьи через официальный VK API нельзя.** В FAQ VK: «методов для работы с лонгридами пока что нет».
- Можно **опубликовать на стене существующую статью** через `wall.post`:
`attachments=articleXXX_YYY`, где `XXX` — ID автора (для сообщества — с минусом), `YYY` — ID статьи.
Статья должна быть уже опубликована (черновик прикрепить нельзя).
- Значит, полный автоматический дубликат блога (генерация лонгридов «из коробки») невозможен.
Варианты: посты+фото+ссылки через API, статьи вручную, гибрид.
## Версия API
- В конфиге `VK_API_VERSION=5.199` (`app/config.py`).
- Проверка токена: `users.get` (пустой user_ids → текущий пользователь).
## Ошибки
Формат ошибки VK: `{"error":{"error_code":N,"error_msg":"..."}}`.
Наш клиент бросает `VkApiError(error_code, error_msg)`; в API публикации ошибка пишется в
`publications.error_message`, ответ `success=false`.