255 lines
25 KiB
Markdown
255 lines
25 KiB
Markdown
# План интеграции 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
|
||
|
||
- Начальная задержка: 500–1000 мс.
|
||
- Множитель: 2 (с jitter до 20–30 %), чтобы избежать «thundering herd».
|
||
- Максимальная задержка: 30–60 с.
|
||
- Общее время ожидания должно быть ограничено (например, 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: 30–60 с.
|
||
- Поддержать `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` и требуют проверки в процессе разработки.
|