randomayzer/docs/VK_INTEGRATION_PLAN.md

255 lines
25 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# План интеграции VK API для Randomayzer
> **Контекст:** Randomayzer — Next.js + TypeScript + Prisma + PostgreSQL сервис для проведения доказуемо честных розыгрышей ВКонтакте. В текущей реализации (`src/providers/vk/vk-provider.ts`) используется сервисный токен `VK_SERVICE_TOKEN` и методы `wall.getById`, `likes.getList`, `wall.getComments`, `groups.isMember`. OAuth / VK ID не реализованы.
>
> **Статус документа:** планировочный, без production-кода. Все факты, которые не удалось подтвердить официальной документацией VK, помечены как `UNVERIFIED`.
---
## Authentication
Для работы с VK API существуют три типа ключей доступа. Для Randomayzer наиболее подходящим **на старте** является **сервисный ключ** (соответствует текущей реализации `VkProvider`), так как он не требует авторизации пользователя и покрывает базовые сценарии розыгрыша: получение метаданных поста, сбор лайков/комментариев с открытых стен и пакетную проверку подписки на сообщество.
Расширенные сценарии (проверка репостов на закрытых страницах, получение списка руководителей сообщества, действия от имени сообщества) требуют **ключа пользователя** или **ключа сообщества**.
### Service Token (сервисный ключ доступа)
- **Как получить.** В панели управления приложением на [dev.vk.ru](https://dev.vk.ru/ru/admin/apps-list) (раздел **Разработка → Ключи доступа**) либо в кабинете сервиса авторизации VK ID при создании Standalone-приложения/сайта.
- **Scope/права.** Права доступа не запрашиваются. Ключ предназначен для работы с публичными данными игр/мини-приложений и методами, не требующими авторизации пользователя.
- **Применимые методы.** `wall.getById`, `likes.getList`, `wall.getComments`, `wall.getReposts`, `groups.isMember`, `groups.getMembers` (с ограничениями), `execute` UNVERIFIED.
- **Ограничения.**
- Работает только с открытыми профилями и открытыми группами.
- Не позволяет выполнять действия от имени пользователя или сообщества.
- Сбор репостов ограничен политикой приватности VK (закрытые профили не видны).
- **Lifetime.** Не ограничен; при компрометации можно перевыпустить в настройках приложения.
- **Риски.** Утечка ключа открывает публичные данные приложения. Ключ должен храниться только на сервере, никогда — в клиентском коде.
### User Token (ключ доступа пользователя / OAuth 2.0 / VK ID)
- **Как получить.** Через сервис авторизации VK ID (Authorization Code Flow для сервера, Implicit Flow для клиента) или событие `VKWebAppGetAuthToken` VK Bridge в мини-приложениях/играх.
- **Scope/права.** Определяются правами доступа, которые пользователь выдал приложению (например, `wall`, `groups`, `friends`). Базовые права (имя, фото, почта) доступны сразу; расширенные требуют подтверждения профиля бизнеса.
- **Применимые методы.** Все методы, доступные сервисному ключу, плюс методы, требующие авторизации конкретного пользователя: просмотр закрытых профилей при наличии доступа, расширенная работа со стеной, `groups.get` и т.д.
- **Ограничения.**
- Требует прохождения пользователем экрана согласия.
- Короткий срок жизни (см. ниже), необходимо обновление.
- **Lifetime.** **1 час** для ключа, полученного через VK ID / OAuth.
- **Риски.**
- Необходимость безопасного хранения refresh-токенов и секретов приложения.
- Нужно реализовать OAuth flow и обработку отзыва разрешений пользователем.
- При передаче в клиент увеличивается риск перехвата.
### Community Token (ключ доступа сообщества)
- **Как получить.** В настройках сообщества: **Управление → Дополнительно → Работа с API → Ключи доступа**. Можно создать несколько ключей с разным набором прав. Программно — через OAuth ВКонтакте (Authorization Code Flow / Implicit Flow) либо событие `VKWebAppGetCommunityToken` VK Bridge.
- **Scope/права.** Права назначаются при создании ключа (например, `wall`, `photos`, `messages`, `manage`).
- **Применимые методы.**
- Методы в рамках одного сообщества: `wall.getById`, `likes.getList`, `wall.getComments`, `groups.isMember`, `groups.getMembers` (включая `filter=managers` для получения ролей).
- Методы управления сообществом — не требуются Randomayzer на этапе сбора участников.
- **Ограничения.**
- Действует только в рамках сообщества, для которого выдан.
- Получить ключ может только администратор сообщества.
- **Lifetime.** Не ограничен; администратор может отозвать ключ в любой момент.
- **Риски.**
- Утечка ключа с правами `manage` даёт полный контроль над сообществом.
- Требует строгого разграничения прав по ключам (принцип минимальных привилегий).
### Рекомендация для Randomayzer
| Этап | Рекомендуемый токен | Обоснование |
|---|---|---|
| MVP / текущая реализация | Сервисный ключ | Без OAuth, серверная работа, публичные посты и лайки |
| Проверка репостов в закрытых профилях | Ключ пользователя организатора | Требуется авторизация владельца стены/профиля |
| Проверка администраторов/модераторов сообщества | Ключ сообщества | `groups.getMembers filter=managers` доступно с правами администратора |
| Массовые розыгрыши 10 000+ участников | Сервисный + пакетирование (`execute`) UNVERIFIED | Высшие лимиты и отсутствие необходимости UI-авторизации |
---
## Tokens
| Тип токена | Назначение | Доступные методы (для розыгрышей) | Ограничения | Срок жизни | Риски |
|---|---|---|---|---|---|
| **Сервисный ключ** | Серверные запросы без авторизации пользователя | `wall.getById`, `likes.getList`, `wall.getComments`, `wall.getReposts`, `groups.isMember`, `groups.getMembers` | Только открытые профили/группы; репосты ограничены приватностью; `execute` — UNVERIFIED | Не ограничен | Утечка открывает публичные данные; нельзя передавать на клиент |
| **Ключ пользователя (VK ID OAuth)** | Запросы от имени пользователя | Все методы сервисного ключа + методы, требующие авторизации (`groups.get`, доступ к закрытым данным при согласии) | Требуется согласие пользователя; короткий срок жизни; refresh-логика | **1 час** | Хранение секретов/refresh; необходимость OAuth flow; отзыв прав |
| **Ключ сообщества** | Запросы от имени сообщества | Методы в рамках одного сообщества, включая `groups.getMembers filter=managers` | Только своё сообщество; нужен администратор для создания | Не ограничен | Утечка ключа с правами `manage` даёт контроль над сообществом |
### Что каждый токен может и не может делать для розыгрышей
- **Сервисный ключ**
- ✅ Получить метаданные открытого поста.
- ✅ Собрать лайки (`likes.getList`) и комментарии (`wall.getComments`).
- ✅ Проверить подписку на сообщество (`groups.isMember`).
- ❌ Получить список руководителей сообщества (`groups.getMembers filter=managers`) — UNVERIFIED, вероятно требуется ключ сообщества.
- ❌ Увидеть репосты на закрытых профилях.
- **Ключ пользователя**
- ✅ Всё, что умеет сервисный ключ (если профили/группы доступны пользователю).
- ✅ Доступ к расширенным данным при наличии соответствующих прав.
-Не даёт прав администратора чужого сообщества.
- **Ключ сообщества**
-Все операции в рамках своего сообщества.
- ✅ Проверка ролей (`groups.getMembers filter=managers`) — при наличии прав.
-Не применим к постам/группам других организаторов.
---
## Required VK methods
| Функциональность | VK Method | Токен | Права / Параметры | Ограничения |
|---|---|---|---|---|
| Загрузка метаданных поста | `wall.getById` | Сервисный или пользовательский | `posts={owner_id}_{post_id}`, `extended=1` | Возвращает массив объектов `post`; при `extended=1` дополнительно `profiles` и `groups`. Ошибка `104 Not found`, если запись недоступна. |
| Сбор лайков | `likes.getList` | Сервисный или пользовательский | `type=post`, `owner_id`, `item_id`, `filter=likes`, `extended=1`, `count` до 1000, `offset` | Максимум 1000 идентификаторов за запрос. При `filter=copies` возвращает пользователей, поделившихся записью, но только если запрос отправляет администратор группы (права редактора и выше) или владелец стены. |
| Сбор комментариев | `wall.getComments` | Сервисный или пользовательский | `owner_id`, `post_id`, `extended=1`, `count` до 100, `offset`, `fields` | Максимум 100 комментариев за запрос. При `extended=1` возвращает `profiles` и `groups`. Ошибка `212 Access to post comments denied`. |
| Проверка подписки на сообщество | `groups.isMember` | Сервисный, пользовательский или ключ сообщества | `group_id`, `user_id` или `user_ids` (до 500), `extended=1` | При пакетной проверке возвращает массив объектов `{user_id, member}`. При `extended=1` дополнительно `request`, `invitation`, `can_invite`. |
| Информация о сообществе / список участников | `groups.getMembers` | Сервисный, пользовательский или ключ сообщества | `group_id`, `count`, `offset`, `fields`, `filter` | `filter=managers` доступно при запросе от имени администратора сообщества; возвращает `role` (`advertiser`, `moderator`, `editor`, `administrator`, `creator`). Максимальное значение `count` — UNVERIFIED. |
| Сбор / проверка репостов | `wall.getReposts` | Сервисный или пользовательский | `owner_id`, `post_id`, `offset`, `count` | Возвращает `items` (записи-репосты), `profiles`, `groups`. Закрытые профили не попадают в выдачу без авторизации владельца стены. |
| Альтернативная проверка репостов | `likes.getList` с `filter=copies` | Ключ сообщества (администратор группы) или владелец стены | `type=post`, `owner_id`, `item_id`, `filter=copies`, `extended=1` | Возвращает пользователей, поделившихся записью, только при наличии соответствующих прав. |
| Проверка администраторов/модераторов | `groups.getMembers` с `filter=managers` | Ключ сообщества с правами администратора | `group_id`, `filter=managers` | Возвращает руководителей сообщества с полем `role`. Недоступно сервисному ключу — UNVERIFIED. |
### Примечания к таблице
- Типы токенов для каждого метода определены по цветовым индикаторам в официальном справочнике VK: серый — сервисный ключ, оранжевый — пользовательский, синий — ключ сообщества.
- `wall.getById`, `likes.getList`, `wall.getComments`, `wall.getReposts` имеют индикаторы сервисного и пользовательского токенов.
- `groups.isMember` и `groups.getMembers` имеют индикаторы всех трёх типов токенов.
---
## Rate Limit Strategy
> **Важно:** этот раздел описывает только архитектуру и контракты. Код не реализуется в рамках данного документа.
### Целевая архитектура
```text
VK API
┌─────────────┐ ┌──────────────┐ ┌─────────────┐ ┌─────────────┐
│ VkClient │────▶│ RateLimiter │────▶│ RetryPolicy │────▶│ VkProvider │
│ (HTTP+Auth)│ │ (token-bucket)│ │ (backoff) │ │(SocialMediaProvider)
└─────────────┘ └──────────────┘ └─────────────┘ └─────────────┘
```
`VkProvider` реализует интерфейс `SocialMediaProvider` и делегирует низкоуровневые вызовы `VkClient`. `RateLimiter` и `RetryPolicy` являются отдельными, тестируемыми абстракциями.
### Контракты / интерфейсы
```typescript
// Абстракция HTTP-клиента VK API
interface VkClient {
call<T>(method: string, params: Record<string, unknown>): Promise<VkResponse<T>>;
}
type VkResponse<T> =
| { response: T; error?: never }
| { response?: never; error: VkError };
interface VkError {
error_code: number;
error_msg: string;
}
// Ограничение скорости
interface RateLimiter {
acquire(tokenType: VkTokenType, cost?: number): Promise<void>;
updateLimits(tokenType: VkTokenType, remaining: number, resetAt: Date): void;
}
type VkTokenType = 'service' | 'user' | 'community';
// Политика повторных попыток
interface RetryPolicy {
execute<T>(task: () => Promise<T>, context: RetryContext): Promise<T>;
}
interface RetryContext {
maxAttempts: number;
isRetryable: (error: VkError) => boolean;
computeDelay: (attempt: number, error: VkError) => number;
}
```
### Exponential backoff
- Начальная задержка: 5001000 мс.
- Множитель: 2 (с jitter до 2030 %), чтобы избежать «thundering herd».
- Максимальная задержка: 3060 с.
- Общее время ожидания должно быть ограничено (например, 5 минут на одну операцию), после чего ошибка прокидывается вызывающему коду.
### Retryable vs non-retryable errors
| Код ошибки | Статус | Действие |
|---|---|---|
| `6` Too many requests per second | Retryable | Повторить после backoff, уменьшить скорость |
| `9` Flood control | Retryable | Увеличить задержку, возможно — запросить капчу UNVERIFIED |
| `10` Internal server error | Retryable | Повторить с backoff |
| `29` Rate limit reached | Retryable/Non-retryable | Дневной лимит; повторять с большим интервалом либо прекратить |
| `15` Access denied | Non-retryable | Закрытая группа/профиль; зафиксировать в аудите |
| `18` User was deleted or banned | Non-retryable | Исключить пользователя из выборки |
| `30` Private profile | Non-retryable | Исключить с причиной `PRIVATE_PROFILE_OR_NO_REPOST` |
| `104` Not found | Non-retryable | Пост не найден |
| `212` Access to post comments denied | Non-retryable | Ограничены комментарии |
| `232` Reaction can not be applied | Non-retryable | Ошибка параметров `likes.getList` |
### Rate limit handling
- Лимиты по типу токена (подтверждено документацией VK):
- Пользовательский ключ: **3 запроса/сек**.
- Ключ сообщества: **20 запросов/сек**.
- Сервисный ключ: от **5 до 60 запросов/сек** в зависимости от количества пользователей приложения.
- Рекомендуется использовать token-bucket per `VkTokenType` с консервативным начальным значением (например, 50 % от заявленного лимита) и адаптацией при получении ошибки `6`.
- Все запросы одного розыгрыша должны учитывать общий bucket, даже если они выполняются в разных корутинах/процессах.
### Pagination strategy
- `likes.getList`: `count=1000`, увеличивать `offset` на 1000 до достижения `response.count`.
- `wall.getComments`: `count=100`, увеличивать `offset` на 100.
- `groups.isMember`: батчировать `user_ids` по **500** ID.
- `groups.getMembers`: UNVERIFIED — предположительно `count` до 1000, но в документации точное максимальное значение не указано.
- При изменении данных во время pagination (например, пользователь удалил лайк) возможны дубли или пропуски. Для розыгрышей рекомендуется фиксировать `snapshotTime` и игнорировать изменения после него.
### Batching через `execute`
- Метод `execute` позволяет выполнять код на серверах VK (VKScript).
- Предполагаемые лимиты (UNVERIFIED): до 25 вызовов API внутри одного `execute`.
- Потенциальная эффективность:
- `likes.getList`: 25 × 1000 = до 25 000 лайков за 1 HTTP-запрос.
- `groups.isMember`: 25 × 500 = до 12 500 проверок подписки за 1 HTTP-запрос.
- `wall.getComments`: 25 × 100 = до 2 500 комментариев за 1 HTTP-запрос.
- **Риски:** один упавший под-вызов внутри `execute` может прервать весь батч; требуется гранулярная обработка ошибок и fallback на последовательные запросы.
### Timeout и cancellation
- Установить разумный network timeout для VK API: 3060 с.
- Поддержать `AbortSignal` / cancellation token на уровне `VkClient`, чтобы длительные операции сбора участников можно было прервать из UI.
- При отмене сохранять уже загруженные данные и статус прогресса.
---
## References
Официальная документация VK, использованная при составлении плана:
- Общий формат запросов и лимиты: https://dev.vk.ru/ru/api/api-requests
- Ключи доступа — обзор: https://dev.vk.com/api/access-token
- Сервисный ключ доступа: https://dev.vk.ru/ru/api/access-token/service-token
- Ключ доступа пользователя: https://dev.vk.ru/ru/api/access-token/user-token
- Ключ доступа сообщества: https://dev.vk.ru/ru/api/access-token/community-token
- Справочник ошибок: https://dev.vk.ru/ru/reference/errors
- Метод `wall.getById`: https://dev.vk.com/method/wall.getById
- Метод `likes.getList`: https://dev.vk.com/method/likes.getList
- Метод `wall.getComments`: https://dev.vk.com/method/wall.getComments
- Метод `groups.isMember`: https://dev.vk.com/method/groups.isMember
- Метод `groups.getMembers`: https://dev.vk.com/method/groups.getMembers
- Метод `wall.getReposts`: https://dev.vk.com/method/wall.getReposts
- Метод `execute`: https://dev.vk.com/method/execute
---
## Резюме для команды
1. **Сейчас** (`VkProvider`) используется только сервисный ключ `VK_SERVICE_TOKEN`. Это корректный MVP-подход: без OAuth, серверная работа, базовый сбор лайков/комментариев и проверка подписки.
2. **Дальнейшее развитие** требует выбора между:
- **OAuth VK ID** для получения пользовательского токена организатора (для закрытых профилей и расширенных прав);
- **ключом сообщества** (для проверки ролей админов/модераторов и работы строго в рамках одного сообщества).
3. **Перед внедрением OAuth** необходимо отдельно спроектировать поток авторизации, хранение токенов, refresh-логику и аудит действий от имени пользователя.
4. **Rate limiting** должен быть вынесен в отдельные слои `RateLimiter` и `RetryPolicy`, чтобы `VkProvider` оставался чистым адаптером `SocialMediaProvider`.
5. Все неподтверждённые официальной документацией числовые лимиты (`execute`, максимальный `count` для `groups.getMembers` и др.) помечены `UNVERIFIED` и требуют проверки в процессе разработки.