diff --git a/plugins/vk-full-admin/README.md b/plugins/vk-full-admin/README.md index 41c7419..93f6463 100644 --- a/plugins/vk-full-admin/README.md +++ b/plugins/vk-full-admin/README.md @@ -18,17 +18,19 @@ npm ci --ignore-scripts Log in to VK with the account that owns or manages the target communities. -1. Create or open a VK developer app at `https://vk.com/dev/standalone` or `https://vk.com/apps?act=manage` and copy its **Application ID**. For a manual local flow, use `https://oauth.vk.com/blank.html` as the redirect URI. -2. Create the user token for `vk_user_token`. In Teleton, run `vk_auth_user_url` with the copied `client_id`, open the returned URL, approve the requested scopes, and copy the `access_token` value from the redirected URL fragment. +1. Create or open a VK developer standalone app at `https://vk.com/dev/standalone` or `https://vk.com/apps?act=manage` and copy its **Application ID**. For a manual local flow, use `https://oauth.vk.ru/blank.html` as the redirect URI. +2. Create the user token for `vk_user_token`. In Teleton, run `vk_auth_user_url` with the copied `client_id`, open the returned URL, approve the requested scopes, and copy the `access_token` value from the redirected URL fragment. The helper uses comma-separated scope names because VK ID OAuth expects names, not a numeric bitmask. 3. Create a community token for each managed community. In VK, open the community, go to **Manage** -> **Settings** -> **API usage** -> **Access tokens**, click **Create token**, allow the permissions needed by the plugin, confirm, and copy the generated token. 4. If you prefer OAuth for community tokens, run `vk_auth_group_url` with the copied `client_id` and `group_ids`, open the returned URL, approve access, and copy each `access_token_` value from the redirected URL fragment. Recommended user token scopes: ```text -offline, wall, messages, friends, photos, groups, stats, notifications +offline, wall, friends, photos, groups, stats, notifications ``` +The `messages` user scope is restricted by VK to eligible standalone applications that passed moderation or already had this access. Do not request it by default. If your app is eligible and you need `vk_user_messages_send`, pass `messages` explicitly in `vk_auth_user_url.scopes`. + Recommended community token scopes: ```text @@ -40,8 +42,8 @@ The helper tools only build VK OAuth URLs. They do not store tokens. If the helper tools are not available yet during first install, use these OAuth URL templates directly: ```text -https://oauth.vk.com/authorize?client_id=&display=page&redirect_uri=https://oauth.vk.com/blank.html&scope=offline,wall,messages,friends,photos,groups,stats,notifications&response_type=token&v=5.199 -https://oauth.vk.com/authorize?client_id=&display=page&redirect_uri=https://oauth.vk.com/blank.html&scope=manage,messages,photos,docs&response_type=token&v=5.199&group_ids=123456,789012 +https://oauth.vk.ru/authorize?client_id=&display=page&redirect_uri=https://oauth.vk.ru/blank.html&scope=offline,wall,friends,photos,groups,stats,notifications&response_type=token&v=5.199 +https://oauth.vk.ru/authorize?client_id=&display=page&redirect_uri=https://oauth.vk.ru/blank.html&scope=manage,messages,photos,docs&response_type=token&v=5.199&group_ids=123456,789012 ``` ### Link Tokens To Teleton @@ -76,7 +78,7 @@ export VK_FULL_ADMIN_VK_USER_TOKEN="vk1.a...." export VK_FULL_ADMIN_VK_COMMUNITY_TOKENS='{"123456":"vk1.a.group-token"}' ``` -`vk_user_token` is required for personal account tools and for validating that the user has community manager rights. `vk_community_tokens` is a JSON object keyed by community ID without the minus sign. Community write tools require both a valid user token and a matching community token. +`vk_user_token` is required for personal account tools and for validating that the user has community manager rights. `vk_user_messages_send` additionally requires a user token created with the restricted `messages` scope. `vk_community_tokens` is a JSON object keyed by community ID without the minus sign. Community write tools require both a valid user token and a matching community token. ### Verify Installation diff --git a/plugins/vk-full-admin/index.js b/plugins/vk-full-admin/index.js index 1c85686..a8857d0 100644 --- a/plugins/vk-full-admin/index.js +++ b/plugins/vk-full-admin/index.js @@ -14,6 +14,8 @@ const { groupScopes, userScopes } = require("@vk-io/authorization"); const PLUGIN_ID = "vk-full-admin"; const DEFAULT_API_VERSION = "5.199"; +const OAUTH_AUTHORIZE_URL = "https://oauth.vk.ru/authorize"; +const DEFAULT_REDIRECT_URI = "https://oauth.vk.ru/blank.html"; const TOKEN_CACHE_TTL_MS = 50 * 60 * 1000; const DEFAULT_TIMEOUT_MS = 15_000; const DEFAULT_RATE_LIMIT_PER_SECOND = 3; @@ -21,7 +23,6 @@ const MAX_ERROR_LENGTH = 500; const DEFAULT_USER_SCOPES = [ "offline", "wall", - "messages", "friends", "photos", "groups", @@ -58,7 +59,7 @@ export const manifest = { required: true, env: "VK_FULL_ADMIN_VK_USER_TOKEN", description: - "VK user access token from OAuth implicit flow with wall, messages, friends, photos, groups, stats, notifications, and offline scopes", + "VK user access token from OAuth implicit flow with offline, wall, friends, photos, groups, stats, and notifications scopes. Add messages explicitly only for eligible standalone apps.", }, vk_community_tokens: { required: false, @@ -433,11 +434,12 @@ export function createVkFullAdminTools(sdk, options = {}) { redirect_uri: { type: "string", description: "OAuth redirect URI registered in VK app settings.", - default: "https://oauth.vk.com/blank.html", + default: DEFAULT_REDIRECT_URI, }, scopes: { type: "array", - description: "VK user scope names. Defaults cover this plugin.", + description: + "VK user scope names. Defaults avoid the restricted messages scope; request messages explicitly only for eligible standalone apps.", items: { type: "string" }, }, revoke: { @@ -454,7 +456,7 @@ export function createVkFullAdminTools(sdk, options = {}) { url: buildOAuthUrl({ clientId: params.client_id, redirectUri: params.redirect_uri, - scope: scopeMask(scopes, userScopes), + scope: scopeNames(scopes, userScopes), revoke: params.revoke, }), scopes, @@ -481,7 +483,7 @@ export function createVkFullAdminTools(sdk, options = {}) { redirect_uri: { type: "string", description: "OAuth redirect URI registered in VK app settings.", - default: "https://oauth.vk.com/blank.html", + default: DEFAULT_REDIRECT_URI, }, scopes: { type: "array", @@ -498,7 +500,7 @@ export function createVkFullAdminTools(sdk, options = {}) { url: buildOAuthUrl({ clientId: params.client_id, redirectUri: params.redirect_uri, - scope: scopeMask(scopes, groupScopes), + scope: scopeNames(scopes, groupScopes), groupIds: params.group_ids, revoke: params.revoke, }), @@ -615,7 +617,7 @@ export function createVkFullAdminTools(sdk, options = {}) { return { name: "vk_user_messages_send", description: - "Send a VK direct message from the user account to user_id or peer_id. Uses random_id for idempotency.", + "Send a VK direct message from the user account to user_id or peer_id. Requires a user token with the restricted messages scope. Uses random_id for idempotency.", scope: "dm-only", category: "action", parameters: { @@ -1685,21 +1687,22 @@ function randomId() { return randomInt(1, 2_147_483_647); } -function scopeMask(scopes, scopeMap) { - let mask = 0; +function scopeNames(scopes, scopeMap) { + const names = []; for (const scope of normalizeList(scopes)) { - const value = scopeMap.get(String(scope)); + const name = String(scope).trim(); + const value = scopeMap.get(name); if (!value) throw new Error(`Unknown VK scope: ${scope}`); - mask |= value; + names.push(name); } - return String(mask); + return names.join(","); } function buildOAuthUrl({ clientId, redirectUri, scope, groupIds, revoke = false }) { - const url = new URL("https://oauth.vk.com/authorize"); + const url = new URL(OAUTH_AUTHORIZE_URL); url.searchParams.set("client_id", String(clientId)); url.searchParams.set("display", "page"); - url.searchParams.set("redirect_uri", redirectUri ?? "https://oauth.vk.com/blank.html"); + url.searchParams.set("redirect_uri", redirectUri ?? DEFAULT_REDIRECT_URI); url.searchParams.set("scope", String(scope)); url.searchParams.set("response_type", "token"); url.searchParams.set("v", DEFAULT_API_VERSION); diff --git a/plugins/vk-full-admin/manifest.json b/plugins/vk-full-admin/manifest.json index 533819a..d5c6743 100644 --- a/plugins/vk-full-admin/manifest.json +++ b/plugins/vk-full-admin/manifest.json @@ -14,7 +14,7 @@ "secrets": { "vk_user_token": { "required": true, - "description": "VK user access token created through VK OAuth implicit flow; use the plugin vk_auth_user_url helper or https://oauth.vk.com/blank.html redirect with offline, wall, messages, friends, photos, groups, stats, and notifications scopes" + "description": "VK user access token created through VK OAuth implicit flow; use the plugin vk_auth_user_url helper or https://oauth.vk.ru/blank.html redirect with offline, wall, friends, photos, groups, stats, and notifications scopes. Request messages explicitly only for eligible standalone apps." }, "vk_community_tokens": { "required": false, diff --git a/plugins/vk-full-admin/tests/index.test.js b/plugins/vk-full-admin/tests/index.test.js index ebaa5f7..c871e12 100644 --- a/plugins/vk-full-admin/tests/index.test.js +++ b/plugins/vk-full-admin/tests/index.test.js @@ -86,6 +86,35 @@ describe("vk-full-admin", () => { assert.equal(names.length, 34); }); + it("builds a default user OAuth URL without restricted message access", async () => { + const calls = []; + const tools = createVkFullAdminTools(makeSdk(), { VKClass: makeMockVK({ calls }) }); + const result = await getTool(tools, "vk_auth_user_url").execute({ client_id: "12345" }); + + assert.equal(result.success, true); + assert.equal(result.data.scopes.includes("messages"), false); + + const url = new URL(result.data.url); + assert.equal(url.origin, "https://oauth.vk.ru"); + assert.equal(url.searchParams.get("client_id"), "12345"); + assert.equal(url.searchParams.get("scope"), "offline,wall,friends,photos,groups,stats,notifications"); + }); + + it("builds OAuth URLs with comma-separated scope names", async () => { + const calls = []; + const tools = createVkFullAdminTools(makeSdk(), { VKClass: makeMockVK({ calls }) }); + const result = await getTool(tools, "vk_auth_group_url").execute({ + client_id: "12345", + group_ids: [123, -456], + }); + + assert.equal(result.success, true); + + const url = new URL(result.data.url); + assert.equal(url.searchParams.get("scope"), "manage,messages,photos,docs"); + assert.equal(url.searchParams.get("group_ids"), "123,456"); + }); + it("checks admin rights before posting to a community with the community token", async () => { const calls = []; const tools = createVkFullAdminTools(makeSdk(), { VKClass: makeMockVK({ calls }) });