Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 8 additions & 6 deletions plugins/vk-full-admin/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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_<group_id>` 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
Expand All @@ -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=<APP_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=<APP_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=<APP_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=<APP_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
Expand Down Expand Up @@ -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

Expand Down
33 changes: 18 additions & 15 deletions plugins/vk-full-admin/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,15 @@ 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;
const MAX_ERROR_LENGTH = 500;
const DEFAULT_USER_SCOPES = [
"offline",
"wall",
"messages",
"friends",
"photos",
"groups",
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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: {
Expand All @@ -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,
Expand All @@ -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",
Expand All @@ -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,
}),
Expand Down Expand Up @@ -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: {
Expand Down Expand Up @@ -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);
Expand Down
2 changes: 1 addition & 1 deletion plugins/vk-full-admin/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
29 changes: 29 additions & 0 deletions plugins/vk-full-admin/tests/index.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 }) });
Expand Down
Loading