# План интеграции 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(method: string, params: Record): Promise>; } type VkResponse = | { response: T; error?: never } | { response?: never; error: VkError }; interface VkError { error_code: number; error_msg: string; } // Ограничение скорости interface RateLimiter { acquire(tokenType: VkTokenType, cost?: number): Promise; updateLimits(tokenType: VkTokenType, remaining: number, resetAt: Date): void; } type VkTokenType = 'service' | 'user' | 'community'; // Политика повторных попыток interface RetryPolicy { execute(task: () => Promise, context: RetryContext): Promise; } 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` и требуют проверки в процессе разработки.