├── docs ├── .nojekyll ├── ru │ ├── guide │ │ ├── collect.md │ │ ├── installation.md │ │ ├── collect-iterator.md │ │ ├── introduction.md │ │ ├── utils.md │ │ └── modules.md │ └── index.md ├── references │ └── index.md ├── logo.svg ├── index.md └── examples │ ├── advanced │ ├── keyboard-builder.js │ ├── opportunities-hears.js │ ├── context-modification.js │ ├── hear-object.js │ └── middleware-error-fallback.js │ ├── simple-updates-bot.js │ └── simple-keyboard-bot.js ├── packages ├── streaming │ ├── src │ │ ├── errors │ │ │ ├── index.ts │ │ │ └── streaming-rule.ts │ │ ├── contexts │ │ │ └── index.ts │ │ ├── index.ts │ │ ├── helpers.ts │ │ └── fetch.ts │ ├── package.json │ └── README.md ├── vk-io │ ├── src │ │ ├── structures │ │ │ ├── shared │ │ │ │ ├── composer.ts │ │ │ │ ├── index.ts │ │ │ │ ├── message-forward-collection.ts │ │ │ │ └── attachmentable.ts │ │ │ ├── keyboard │ │ │ │ └── index.ts │ │ │ ├── index.ts │ │ │ ├── attachments │ │ │ │ ├── index.ts │ │ │ │ ├── gift.ts │ │ │ │ ├── sticker.ts │ │ │ │ ├── external.ts │ │ │ │ ├── graffiti.ts │ │ │ │ ├── market-album.ts │ │ │ │ ├── helpers.ts │ │ │ │ ├── link.ts │ │ │ │ └── wall-reply.ts │ │ │ └── contexts │ │ │ │ ├── index.ts │ │ │ │ ├── unsupported-event.ts │ │ │ │ ├── vote.ts │ │ │ │ ├── wall-post.ts │ │ │ │ ├── vk-pay-transaction.ts │ │ │ │ ├── message-subscription.ts │ │ │ │ ├── vk-app-payload.ts │ │ │ │ ├── donut-withdraw.ts │ │ │ │ ├── dialog-messages.ts │ │ │ │ ├── group-member.ts │ │ │ │ ├── messages-read.ts │ │ │ │ ├── like.ts │ │ │ │ ├── donut-subscription-price.ts │ │ │ │ ├── dialog-notification-settings.ts │ │ │ │ ├── context.ts │ │ │ │ ├── donut-subscription.ts │ │ │ │ └── dialog-flags.ts │ │ ├── upload │ │ │ ├── index.ts │ │ │ ├── helpers.ts │ │ │ └── types.ts │ │ ├── updates │ │ │ ├── index.ts │ │ │ ├── transports │ │ │ │ └── index.ts │ │ │ └── helpers.ts │ │ ├── errors │ │ │ ├── upload.ts │ │ │ ├── resource.ts │ │ │ ├── updates.ts │ │ │ ├── index.ts │ │ │ ├── collect.ts │ │ │ ├── execute.ts │ │ │ ├── error.ts │ │ │ └── api.ts │ │ ├── api │ │ │ ├── index.ts │ │ │ ├── schemas │ │ │ │ └── index.ts │ │ │ ├── workers │ │ │ │ ├── index.ts │ │ │ │ ├── parallel-selected.ts │ │ │ │ ├── parallel.ts │ │ │ │ └── worker.ts │ │ │ └── request.ts │ │ ├── collect │ │ │ ├── index.ts │ │ │ ├── execute-code.ts │ │ │ ├── executes.ts │ │ │ └── chain.ts │ │ ├── types.ts │ │ ├── index.ts │ │ ├── utils │ │ │ └── fetch.ts │ │ └── vk.ts │ ├── test │ │ ├── types │ │ │ ├── updates-koa-webhook copy.ts │ │ │ └── updates-http-webhook.ts │ │ ├── contexts.test.ts │ │ └── resource-resolver.test.ts │ └── package.json ├── hear │ ├── src │ │ ├── index.ts │ │ ├── types.ts │ │ └── helpers.ts │ ├── package.json │ └── README.md ├── authorization │ ├── src │ │ ├── errors │ │ │ ├── index.ts │ │ │ └── authorization.ts │ │ ├── providers │ │ │ ├── index.ts │ │ │ └── implicit-flow-user.ts │ │ ├── index.ts │ │ ├── fetch.ts │ │ ├── open-api.ts │ │ ├── helpers.ts │ │ └── constants.ts │ ├── package.json │ └── README.md ├── scenes │ ├── src │ │ ├── scenes │ │ │ ├── index.ts │ │ │ ├── scene.ts │ │ │ ├── step.types.ts │ │ │ └── step.ts │ │ ├── contexts │ │ │ ├── index.ts │ │ │ ├── step.types.ts │ │ │ ├── scene.types.ts │ │ │ └── step.ts │ │ ├── index.ts │ │ ├── scene-manager.types.ts │ │ ├── types.ts │ │ ├── scene-manager.ts │ │ └── cache-repository.ts │ ├── package.json │ └── README.md ├── session │ ├── src │ │ ├── index.ts │ │ ├── storages │ │ │ ├── index.ts │ │ │ ├── storage.ts │ │ │ └── memory.ts │ │ ├── types.ts │ │ └── session-manager.ts │ ├── package.json │ └── README.md └── stateless-prompt │ ├── src │ ├── index.ts │ ├── identifier.ts │ ├── types.ts │ ├── stateless-prompt.types.ts │ └── stateless-prompt-manager.ts │ ├── package.json │ └── README.md ├── scripts ├── typings-generator │ ├── generators │ │ ├── index.js │ │ ├── types.js │ │ └── interface.js │ ├── parsers │ │ ├── index.js │ │ ├── response.js │ │ └── parameter.js │ ├── printer.js │ └── utils │ │ └── helpers.js └── deploy-docs.sh ├── .gitignore ├── .editorconfig ├── tsconfig.json ├── .github ├── ISSUE_TEMPLATE │ ├── vk-io.md │ ├── -vk-io-scenes.md │ ├── -vk-io-session.md │ ├── -vk-io-streaming.md │ └── -vk-io-authorization.md └── workflows │ └── tests.yml ├── LICENSE ├── package.json ├── biome.json └── rollup.config.js /docs/.nojekyll: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /packages/streaming/src/errors/index.ts: -------------------------------------------------------------------------------- 1 | export { StreamingRuleError } from './streaming-rule'; 2 | -------------------------------------------------------------------------------- /packages/vk-io/src/structures/shared/composer.ts: -------------------------------------------------------------------------------- 1 | export { Composer } from 'middleware-io'; 2 | -------------------------------------------------------------------------------- /packages/vk-io/src/upload/index.ts: -------------------------------------------------------------------------------- 1 | export * from './upload'; 2 | 3 | export * from './types'; 4 | -------------------------------------------------------------------------------- /packages/vk-io/src/updates/index.ts: -------------------------------------------------------------------------------- 1 | export * from './transports'; 2 | export * from './updates'; 3 | -------------------------------------------------------------------------------- /packages/vk-io/src/updates/transports/index.ts: -------------------------------------------------------------------------------- 1 | export * from './polling'; 2 | export * from './webhook'; 3 | -------------------------------------------------------------------------------- /packages/hear/src/index.ts: -------------------------------------------------------------------------------- 1 | export { HearManager } from './hear-manager'; 2 | 3 | export * from './types'; 4 | -------------------------------------------------------------------------------- /packages/streaming/src/contexts/index.ts: -------------------------------------------------------------------------------- 1 | export { IStreamingContextPayload, StreamingContext } from './streaming'; 2 | -------------------------------------------------------------------------------- /packages/authorization/src/errors/index.ts: -------------------------------------------------------------------------------- 1 | export { AuthorizationError, IAuthorizationErrorOptions } from './authorization'; 2 | -------------------------------------------------------------------------------- /packages/vk-io/src/errors/upload.ts: -------------------------------------------------------------------------------- 1 | import { VKError } from './error'; 2 | 3 | export class UploadError extends VKError {} 4 | -------------------------------------------------------------------------------- /packages/vk-io/src/errors/resource.ts: -------------------------------------------------------------------------------- 1 | import { VKError } from './error'; 2 | 3 | export class ResourceError extends VKError {} 4 | -------------------------------------------------------------------------------- /packages/vk-io/src/errors/updates.ts: -------------------------------------------------------------------------------- 1 | import { VKError } from './error'; 2 | 3 | export class UpdatesError extends VKError {} 4 | -------------------------------------------------------------------------------- /packages/scenes/src/scenes/index.ts: -------------------------------------------------------------------------------- 1 | export { IScene } from './scene'; 2 | export { StepScene } from './step'; 3 | 4 | export * from './step.types'; 5 | -------------------------------------------------------------------------------- /packages/session/src/index.ts: -------------------------------------------------------------------------------- 1 | export { SessionManager } from './session-manager'; 2 | export * from './storages'; 3 | 4 | export * from './types'; 5 | -------------------------------------------------------------------------------- /packages/streaming/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './errors'; 2 | 3 | export * from './contexts'; 4 | 5 | export { StreamingAPI } from './streaming'; 6 | -------------------------------------------------------------------------------- /packages/vk-io/src/api/index.ts: -------------------------------------------------------------------------------- 1 | export * from './api'; 2 | export * from './request'; 3 | export * from './schemas'; 4 | export * from './workers'; 5 | -------------------------------------------------------------------------------- /packages/vk-io/src/structures/keyboard/index.ts: -------------------------------------------------------------------------------- 1 | export * from './builder'; 2 | 3 | export * from './keyboard'; 4 | 5 | export * from './types'; 6 | -------------------------------------------------------------------------------- /packages/vk-io/src/api/schemas/index.ts: -------------------------------------------------------------------------------- 1 | export * as Params from './params'; 2 | export * as Objects from './objects'; 3 | export * as Responses from './responses'; 4 | -------------------------------------------------------------------------------- /packages/vk-io/src/structures/shared/index.ts: -------------------------------------------------------------------------------- 1 | export * from './attachmentable'; 2 | export * from './composer'; 3 | export * from './message-forward-collection'; 4 | -------------------------------------------------------------------------------- /packages/session/src/storages/index.ts: -------------------------------------------------------------------------------- 1 | export { IMemoryStorageOptions, IMemoryStoreLike, MemoryStorage } from './memory'; 2 | export { ISessionStorage } from './storage'; 3 | -------------------------------------------------------------------------------- /packages/vk-io/src/api/workers/index.ts: -------------------------------------------------------------------------------- 1 | export * from './parallel'; 2 | export * from './parallel-selected'; 3 | export * from './sequential'; 4 | export * from './worker'; 5 | -------------------------------------------------------------------------------- /packages/vk-io/src/collect/index.ts: -------------------------------------------------------------------------------- 1 | export * from './chain'; 2 | export { executes, IExecutesOptions, IExecutesPayload } from './executes'; 3 | export * from './iterator'; 4 | -------------------------------------------------------------------------------- /packages/vk-io/src/structures/index.ts: -------------------------------------------------------------------------------- 1 | export * from './contexts'; 2 | 3 | export * from './attachments'; 4 | export * from './shared'; 5 | 6 | export * from './keyboard'; 7 | -------------------------------------------------------------------------------- /packages/scenes/src/contexts/index.ts: -------------------------------------------------------------------------------- 1 | export { SceneContext } from './scene'; 2 | export { StepSceneContext } from './step'; 3 | 4 | export * from './scene.types'; 5 | export * from './step.types'; 6 | -------------------------------------------------------------------------------- /packages/scenes/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './contexts'; 2 | 3 | export { SceneManager } from './scene-manager'; 4 | export * from './scenes'; 5 | 6 | export * from './scene-manager.types'; 7 | export * from './types'; 8 | -------------------------------------------------------------------------------- /scripts/typings-generator/generators/index.js: -------------------------------------------------------------------------------- 1 | const TypesGenerator = require('./types'); 2 | const InterfaceGenerator = require('./interface'); 3 | 4 | module.exports = { 5 | TypesGenerator, 6 | InterfaceGenerator, 7 | }; 8 | -------------------------------------------------------------------------------- /packages/authorization/src/providers/index.ts: -------------------------------------------------------------------------------- 1 | export * from './account-verification'; 2 | export * from './direct'; 3 | export * from './implicit-flow'; 4 | export * from './implicit-flow-groups'; 5 | export * from './implicit-flow-user'; 6 | -------------------------------------------------------------------------------- /packages/stateless-prompt/src/index.ts: -------------------------------------------------------------------------------- 1 | export { getDataHash } from './identifier'; 2 | export { StatelessPromptManager } from './stateless-prompt-manager'; 3 | 4 | export * from './stateless-prompt.types'; 5 | export * from './types'; 6 | -------------------------------------------------------------------------------- /docs/ru/guide/collect.md: -------------------------------------------------------------------------------- 1 | # Collect 2 | 3 | Модуль коллекций разбит на маленькие хелперы 4 | 5 | ## Collect Iterator 6 | 7 | Итератор сбора данных с методов которые поддерживают limit/offset 8 | 9 | [Более подробно](./collect-iterator.md) 10 | -------------------------------------------------------------------------------- /packages/stateless-prompt/src/identifier.ts: -------------------------------------------------------------------------------- 1 | import { createHash } from 'crypto'; 2 | 3 | export const getDataHash = (data: string): string => 4 | createHash('shake256', { 5 | outputLength: 2, 6 | }) 7 | .update(data) 8 | .digest('hex'); 9 | -------------------------------------------------------------------------------- /packages/stateless-prompt/src/types.ts: -------------------------------------------------------------------------------- 1 | import { MessageContext } from 'vk-io'; 2 | 3 | export type Middleware = (context: T, next: () => Promise) => unknown; 4 | 5 | export type HandlerMiddleware = Middleware; 6 | 7 | export { MessageContext }; 8 | -------------------------------------------------------------------------------- /packages/authorization/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './errors'; 2 | 3 | export * from './open-api'; 4 | export * from './providers'; 5 | 6 | export { 7 | AuthErrorCode, 8 | groupScopes, 9 | officialAppCredentials, 10 | userScopes, 11 | } from './constants'; 12 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # IDE Settings 2 | .idea 3 | .vscode 4 | 5 | # Logs 6 | logs 7 | *.log 8 | npm-debug.log* 9 | 10 | # Dependency directories 11 | node_modules 12 | 13 | # Build 14 | packages/*/lib 15 | app.ts 16 | app.js 17 | app.mjs 18 | 19 | docs/.vitepress/dist 20 | docs/.vitepress/cache 21 | -------------------------------------------------------------------------------- /packages/session/src/storages/storage.ts: -------------------------------------------------------------------------------- 1 | export interface ISessionStorage { 2 | get(key: string): Promise; 3 | 4 | set(key: string, value: object): Promise; 5 | 6 | delete(key: string): Promise; 7 | 8 | touch(key: string): Promise; 9 | } 10 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # editorconfig.org 2 | root = true 3 | 4 | [*] 5 | end_of_line = lf 6 | indent_style = space 7 | indent_size = 4 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | 12 | [*.{yaml,yml}] 13 | indent_size = 2 14 | 15 | [*.md] 16 | trim_trailing_whitespace = false 17 | -------------------------------------------------------------------------------- /packages/vk-io/src/api/workers/parallel-selected.ts: -------------------------------------------------------------------------------- 1 | import { ParallelWorker } from './parallel'; 2 | 3 | export class ParallelSelectedWorker extends ParallelWorker { 4 | protected skipMethod(method: string): boolean { 5 | return super.skipMethod(method) || !this.api.options.apiExecuteMethods.includes(method); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /packages/vk-io/test/types/updates-koa-webhook copy.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | import Koa from 'koa'; 3 | 4 | import { VK } from '../..'; 5 | 6 | const vk = new VK({ 7 | token: '12345', 8 | }); 9 | 10 | const app = new Koa(); 11 | 12 | const callback = vk.updates.getKoaWebhookMiddleware(); 13 | 14 | app.use(callback); 15 | -------------------------------------------------------------------------------- /packages/streaming/src/helpers.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copies object params to new object 3 | */ 4 | export const copyParams = (params: T, properties: K[]): Pick => { 5 | const copies = {} as Pick; 6 | 7 | for (const property of properties) { 8 | copies[property] = params[property]; 9 | } 10 | 11 | return copies; 12 | }; 13 | -------------------------------------------------------------------------------- /packages/scenes/src/scene-manager.types.ts: -------------------------------------------------------------------------------- 1 | import type { CacheRepository } from './cache-repository'; 2 | import type { IScene } from './scenes/scene'; 3 | 4 | export type SceneRepository = CacheRepository; 5 | 6 | export interface ISceneManagerOptions { 7 | /** 8 | * Scenes on the interface IScene 9 | */ 10 | scenes?: IScene[]; 11 | 12 | sessionKey?: string; 13 | } 14 | -------------------------------------------------------------------------------- /scripts/typings-generator/parsers/index.js: -------------------------------------------------------------------------------- 1 | const { parseJSONSchema, parseJSONObject } = require('./json-schema'); 2 | 3 | const { parseParameter, parseParameters } = require('./parameter'); 4 | const parseResponses = require('./response'); 5 | 6 | module.exports = { 7 | parseJSONSchema, 8 | parseJSONObject, 9 | 10 | parseParameter, 11 | parseParameters, 12 | parseResponses, 13 | }; 14 | -------------------------------------------------------------------------------- /packages/vk-io/test/types/updates-http-webhook.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | import express from 'express'; 3 | 4 | import { createServer } from 'http'; 5 | 6 | import { VK } from '../..'; 7 | 8 | const vk = new VK({ 9 | token: '12345', 10 | }); 11 | 12 | const app = express(); 13 | 14 | const callback = vk.updates.getWebhookCallback(); 15 | 16 | app.use('/vk-webhook', callback); 17 | 18 | createServer(callback); 19 | -------------------------------------------------------------------------------- /packages/vk-io/src/errors/index.ts: -------------------------------------------------------------------------------- 1 | export * from './error'; 2 | 3 | export * from './api'; 4 | export * from './collect'; 5 | export * from './execute'; 6 | export * from './resource'; 7 | export * from './updates'; 8 | export * from './upload'; 9 | 10 | export { 11 | CollectErrorCode, 12 | ResourceErrorCode, 13 | SharedErrorCode, 14 | UpdatesErrorCode, 15 | UploadErrorCode, 16 | } from '../utils/constants'; 17 | -------------------------------------------------------------------------------- /packages/streaming/src/errors/streaming-rule.ts: -------------------------------------------------------------------------------- 1 | import { VKError } from 'vk-io'; 2 | 3 | export interface IStreamingRuleErrorOptions { 4 | message: string; 5 | error_code: number; 6 | } 7 | 8 | export class StreamingRuleError extends VKError { 9 | /** 10 | * Constructor 11 | */ 12 | public constructor({ message, error_code: code }: IStreamingRuleErrorOptions) { 13 | super({ message, code }); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /packages/scenes/src/contexts/step.types.ts: -------------------------------------------------------------------------------- 1 | import type { IStepContext, StepSceneHandler } from '../scenes/step.types'; 2 | 3 | export interface IStepContextOptions = Record> { 4 | context: IStepContext; 5 | steps: StepSceneHandler[]; 6 | } 7 | 8 | export interface IStepContextGoOptions { 9 | /** 10 | * Logging into a handler without executing it 11 | */ 12 | silent?: boolean; 13 | } 14 | -------------------------------------------------------------------------------- /docs/references/index.md: -------------------------------------------------------------------------------- 1 | # References 2 | 3 | * [vk-io](https://negezor.github.io/vk-io/references/vk-io) 4 | * [@vk-io/hear](https://negezor.github.io/vk-io/references/hear) 5 | * [@vk-io/scenes](https://negezor.github.io/vk-io/references/scenes) 6 | * [@vk-io/session](https://negezor.github.io/vk-io/references/session) 7 | * [@vk-io/streaming](https://negezor.github.io/vk-io/references/streaming) 8 | * [@vk-io/authorization](https://negezor.github.io/vk-io/references/authorization) 9 | -------------------------------------------------------------------------------- /scripts/typings-generator/parsers/response.js: -------------------------------------------------------------------------------- 1 | const { parseJSONObject } = require('./json-schema'); 2 | 3 | const { toPascalCase } = require('../utils/helpers'); 4 | 5 | module.exports = function parseResponses(rawResponses, payload) { 6 | if (!rawResponses) { 7 | return []; 8 | } 9 | 10 | return Object.entries(rawResponses).map(([key, value]) => 11 | parseJSONObject(toPascalCase(key), value.properties.response, payload), 12 | ); 13 | }; 14 | -------------------------------------------------------------------------------- /packages/vk-io/src/updates/helpers.ts: -------------------------------------------------------------------------------- 1 | import type { IncomingMessage } from 'http'; 2 | 3 | export const parseRequestJSON = async (req: IncomingMessage): Promise> => { 4 | const chunks: Buffer[] = []; 5 | let totalSize = 0; 6 | 7 | for await (const chunk of req) { 8 | totalSize += (chunk as Buffer).length; 9 | 10 | chunks.push(chunk as Buffer); 11 | } 12 | 13 | return JSON.parse(Buffer.concat(chunks, totalSize).toString('utf8')); 14 | }; 15 | -------------------------------------------------------------------------------- /packages/scenes/src/scenes/scene.ts: -------------------------------------------------------------------------------- 1 | import type { IContext } from '../types'; 2 | 3 | export interface IScene = Record> { 4 | /** 5 | * The unique name of the scene 6 | */ 7 | slug: string; 8 | 9 | /** 10 | * Enter handler for the scene 11 | */ 12 | enterHandler(context: IContext): unknown; 13 | 14 | /** 15 | * Leave handler for the scene 16 | */ 17 | leaveHandler(context: IContext): unknown; 18 | } 19 | -------------------------------------------------------------------------------- /packages/vk-io/src/types.ts: -------------------------------------------------------------------------------- 1 | import type { IAPIOptions } from './api'; 2 | import type { IUpdatesOptions } from './updates'; 3 | import type { IUploadOptions } from './upload'; 4 | 5 | import type { CallbackService } from './utils/callback-service'; 6 | 7 | export type AllowArray = T | T[]; 8 | 9 | export type Constructor = new (...args: any[]) => T; 10 | 11 | export type VKOptions = IAPIOptions & 12 | IUpdatesOptions & 13 | IUploadOptions & { 14 | callbackService?: CallbackService; 15 | }; 16 | -------------------------------------------------------------------------------- /scripts/deploy-docs.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | set -e 4 | 5 | npx vitepress build docs 6 | 7 | for modulePath in ./packages/*; do 8 | module=`(basename $modulePath)`; 9 | 10 | echo $module 11 | 12 | npx typedoc --out docs/.vitepress/dist/references/$module --readme none packages/$module/src/index.ts 13 | done 14 | 15 | cd docs/.vitepress/dist 16 | 17 | git init 18 | git add -A 19 | git commit -m 'docs: regenerate docs' 20 | 21 | git push -f git@github.com:negezor/vk-io.git master:gh-pages 22 | 23 | cd - 24 | -------------------------------------------------------------------------------- /packages/scenes/src/types.ts: -------------------------------------------------------------------------------- 1 | import type { Context } from 'vk-io'; 2 | 3 | import type { SceneContext } from './contexts'; 4 | 5 | export type Middleware = (context: T, next: () => Promise) => unknown; 6 | 7 | export type ISessionContext = Record & { 8 | current: string; 9 | }; 10 | 11 | export interface IContext = Record> extends Context { 12 | /** 13 | * Scene control context 14 | */ 15 | scene: SceneContext; 16 | [key: string]: any; 17 | } 18 | -------------------------------------------------------------------------------- /packages/hear/src/types.ts: -------------------------------------------------------------------------------- 1 | export type AllowArray = T | T[]; 2 | 3 | export type HearFunctionCondition = (value: V, context: T) => boolean; 4 | 5 | export type HearCondition = HearFunctionCondition | RegExp | string | number | boolean; 6 | 7 | export type HearObjectCondition> = Record>> & { 8 | [P in keyof T]?: AllowArray>; 9 | }; 10 | 11 | export type HearConditions> = 12 | | AllowArray> 13 | | AllowArray>; 14 | -------------------------------------------------------------------------------- /packages/vk-io/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './errors'; 2 | 3 | export * from './api'; 4 | export * from './collect'; 5 | export * from './structures'; 6 | export * from './updates'; 7 | export * from './upload'; 8 | export { VK } from './vk'; 9 | 10 | export { 11 | AttachmentType, 12 | AttachmentTypeString, 13 | CaptchaType, 14 | kSerializeData, 15 | MessageSource, 16 | ResourceType, 17 | UpdateSource, 18 | } from './utils/constants'; 19 | 20 | export { applyMixins, getRandomId } from './utils/helpers'; 21 | export * from './utils/callback-service'; 22 | export * from './utils/resource-resolver'; 23 | -------------------------------------------------------------------------------- /scripts/typings-generator/parsers/parameter.js: -------------------------------------------------------------------------------- 1 | const { parseJSONObject } = require('./json-schema'); 2 | 3 | function parseParameter(rawParameter, payload) { 4 | return parseJSONObject(rawParameter.name, rawParameter, { 5 | arrayUnion: true, 6 | ...payload, 7 | }); 8 | } 9 | 10 | function parseParameters(rawParameters, payload) { 11 | if (!Array.isArray(rawParameters)) { 12 | return []; 13 | } 14 | 15 | return rawParameters.map(rawParameter => parseParameter(rawParameter, payload)); 16 | } 17 | 18 | module.exports = { 19 | parseParameter, 20 | parseParameters, 21 | }; 22 | -------------------------------------------------------------------------------- /packages/streaming/src/fetch.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * NOTICE: This file is needed to bridge between CJS and ESM 3 | * 4 | * @see https://github.com/node-fetch/node-fetch/issues/1266#issuecomment-913216211 5 | */ 6 | const fetchPromise = import('node-fetch').then(mod => mod.default); 7 | 8 | export type RequestInfo = import('node-fetch').RequestInfo | URL; 9 | export type RequestInit = import('node-fetch').RequestInit; 10 | export type Response = import('node-fetch').Response; 11 | 12 | export const fetch = (url: RequestInfo, init?: RequestInit): Promise => 13 | Promise.resolve(fetchPromise).then(fn => fn(url as string, init)); 14 | -------------------------------------------------------------------------------- /packages/vk-io/src/structures/attachments/index.ts: -------------------------------------------------------------------------------- 1 | export * from './attachment'; 2 | export * from './audio'; 3 | export * from './audio-message'; 4 | export * from './document'; 5 | export * from './external'; 6 | export * from './gift'; 7 | export * from './graffiti'; 8 | export * from './link'; 9 | export * from './market'; 10 | export * from './market-album'; 11 | export * from './photo'; 12 | export * from './poll'; 13 | export * from './sticker'; 14 | export * from './story'; 15 | export * from './video'; 16 | export * from './wall'; 17 | export * from './wall-reply'; 18 | 19 | export { transformAttachments } from './helpers'; 20 | -------------------------------------------------------------------------------- /packages/vk-io/src/utils/fetch.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * NOTICE: This file is needed to bridge between CJS and ESM 3 | * 4 | * @see https://github.com/node-fetch/node-fetch/issues/1266#issuecomment-913216211 5 | */ 6 | const fetchPromise = import('node-fetch').then(mod => mod.default); 7 | 8 | export type RequestInfo = import('node-fetch').RequestInfo | URL; 9 | export type RequestInit = import('node-fetch').RequestInit; 10 | export type Response = import('node-fetch').Response; 11 | 12 | export const fetch = (url: RequestInfo, init?: RequestInit): Promise => 13 | Promise.resolve(fetchPromise).then(fn => fn(url as string, init)); 14 | -------------------------------------------------------------------------------- /packages/authorization/src/fetch.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * NOTICE: This file is needed to bridge between CJS and ESM 3 | * 4 | * @see https://github.com/node-fetch/node-fetch/issues/1266#issuecomment-913216211 5 | */ 6 | const fetchPromise = import('node-fetch').then(mod => mod.default); 7 | 8 | export type RequestInfo = import('node-fetch').RequestInfo | URL; 9 | export type RequestInit = import('node-fetch').RequestInit; 10 | export type Response = import('node-fetch').Response; 11 | 12 | export const fetch = (url: RequestInfo, init?: RequestInit): Promise => 13 | Promise.resolve(fetchPromise).then(fn => fn(url as string, init)); 14 | -------------------------------------------------------------------------------- /packages/vk-io/src/errors/collect.ts: -------------------------------------------------------------------------------- 1 | import { VKError } from './error'; 2 | 3 | import type { ExecuteError } from './execute'; 4 | 5 | export interface ICollectErrorOptions { 6 | message: string; 7 | code: string; 8 | 9 | errors: ExecuteError[]; 10 | } 11 | 12 | export class CollectError extends VKError { 13 | /** 14 | * Errors collect 15 | */ 16 | public errors: ExecuteError[]; 17 | 18 | /** 19 | * Constructor 20 | */ 21 | public constructor({ message, code, errors }: ICollectErrorOptions) { 22 | super({ message, code }); 23 | 24 | this.errors = errors; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /packages/vk-io/src/errors/execute.ts: -------------------------------------------------------------------------------- 1 | import { APIError } from './api'; 2 | 3 | export interface IExecuteErrorOptions { 4 | error_code: number; 5 | error_msg: string; 6 | method: string; 7 | } 8 | 9 | export class ExecuteError extends APIError { 10 | /** 11 | * The method in which the error occurred 12 | */ 13 | public method: string; 14 | 15 | /** 16 | * Constructor 17 | */ 18 | public constructor(options: IExecuteErrorOptions) { 19 | super({ 20 | error_code: options.error_code, 21 | error_msg: options.error_msg, 22 | request_params: [], 23 | }); 24 | 25 | this.method = options.method; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2019", 4 | "module": "ESNext", 5 | "lib": ["ES2019", "DOM", "ESNext"], 6 | "moduleResolution": "Node", 7 | "strict": true, 8 | "noEmit": true, 9 | "declaration": true, 10 | "strictNullChecks": true, 11 | "esModuleInterop": true, 12 | "forceConsistentCasingInFileNames": true 13 | }, 14 | "ts-node": { 15 | // these options are overrides used only by ts-node 16 | // same as our --compilerOptions flag and our TS_NODE_COMPILER_OPTIONS environment variable 17 | "compilerOptions": { 18 | "module": "commonjs" 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /packages/authorization/src/errors/authorization.ts: -------------------------------------------------------------------------------- 1 | import { VKError } from 'vk-io'; 2 | 3 | const { DEBUG = '' } = process.env; 4 | 5 | const isDebug = DEBUG.includes('vk-io:authorization'); 6 | 7 | export interface IAuthorizationErrorOptions { 8 | message: string; 9 | code: string; 10 | 11 | pageHtml?: string; 12 | } 13 | 14 | export class AuthorizationError extends VKError { 15 | /** 16 | * HTML error page 17 | */ 18 | public pageHtml?: string; 19 | 20 | /** 21 | * Constructor 22 | */ 23 | public constructor({ message, code, pageHtml }: IAuthorizationErrorOptions) { 24 | super({ message, code }); 25 | 26 | this.pageHtml = isDebug ? pageHtml : undefined; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/vk-io.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: vk-io 3 | about: Report an issue with the 'vk-io' package 4 | title: '' 5 | labels: 'package: vk-io' 6 | assignees: '' 7 | 8 | --- 9 | 10 | 11 | 12 | #### What did you do? 13 | 14 | 15 | #### What did you expect to happen? 16 | 17 | 18 | #### What was the actual result? 19 | 20 | 21 | #### Additional Info 22 | 23 | 24 | #### Versions 25 | 26 | | package | version | 27 | | ---------------------------------- | ------- | 28 | | `vk-io` | `X.Y.Z` | 29 | | `node` | `X.Y.Z` | 30 | | `TypeScript` | `X.Y.Z` | 31 | | `npm` or `yarn` | `X.Y.Z` | 32 | -------------------------------------------------------------------------------- /docs/ru/guide/installation.md: -------------------------------------------------------------------------------- 1 | # Установка 2 | 3 | ## Зависимости 4 | 5 | ### Node.js 6 | Требуется [Node.js](https://nodejs.org) версии **12.0.0** и выше 7 | 8 | ### TypeScript 9 | Если вы используете [TypeScript](https://www.typescriptlang.org/), требуется версия **3.9.0** и выше 10 | 11 | ## NPM 12 | Установка модуля из пакетного менеджера [NPM](https://www.npmjs.com/) 13 | 14 | ```bash 15 | $ npm install vk-io 16 | ``` 17 | 18 | Или 19 | 20 | ```bash 21 | $ yarn add vk-io 22 | ``` 23 | 24 | ## Импорт 25 | ### CJS 26 | ```js 27 | const { VK } = require('vk-io'); 28 | 29 | const vk = new VK({ 30 | token: 'токен' 31 | }); 32 | 33 | // ... 34 | ``` 35 | 36 | ### ESM 37 | ```ts 38 | import { VK } from 'vk-io'; 39 | 40 | const vk = new VK({ 41 | token: 'токен' 42 | }); 43 | 44 | // ... 45 | ``` 46 | -------------------------------------------------------------------------------- /packages/scenes/src/scenes/step.types.ts: -------------------------------------------------------------------------------- 1 | import type { SceneContext, StepSceneContext } from '../contexts'; 2 | 3 | import type { IContext } from '../types'; 4 | 5 | export interface IStepContext> extends IContext { 6 | scene: SceneContext & { 7 | /** 8 | * Stepping scene control context 9 | */ 10 | step: StepSceneContext; 11 | }; 12 | } 13 | 14 | export type StepSceneHandler = Record> = ( 15 | context: IStepContext & T, 16 | ) => unknown; 17 | 18 | export interface IStepSceneOptions> { 19 | steps: StepSceneHandler[]; 20 | enterHandler?: StepSceneHandler; 21 | leaveHandler?: StepSceneHandler; 22 | } 23 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/-vk-io-scenes.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: "@vk-io/scenes" 3 | about: Report an issue with the '@vk-io/scenes' package 4 | title: '' 5 | labels: 'package: @vk-io/scenes' 6 | assignees: '' 7 | 8 | --- 9 | 10 | 11 | 12 | #### What did you do? 13 | 14 | 15 | #### What did you expect to happen? 16 | 17 | 18 | #### What was the actual result? 19 | 20 | 21 | #### Additional Info 22 | 23 | 24 | #### Versions 25 | 26 | | package | version | 27 | | ---------------------------------- | ------- | 28 | | `vk-io` | `X.Y.Z` | 29 | | `@vk-io/scenes` | `X.Y.Z` | 30 | | `node` | `X.Y.Z` | 31 | | `TypeScript` | `X.Y.Z` | 32 | | `npm` or `yarn` | `X.Y.Z` | 33 | -------------------------------------------------------------------------------- /docs/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/-vk-io-session.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: "@vk-io/session" 3 | about: Report an issue with the '@vk-io/session' package 4 | title: '' 5 | labels: 'package: @vk-io/session' 6 | assignees: '' 7 | 8 | --- 9 | 10 | 11 | 12 | #### What did you do? 13 | 14 | 15 | #### What did you expect to happen? 16 | 17 | 18 | #### What was the actual result? 19 | 20 | 21 | #### Additional Info 22 | 23 | 24 | #### Versions 25 | 26 | | package | version | 27 | | ---------------------------------- | ------- | 28 | | `vk-io` | `X.Y.Z` | 29 | | `@vk-io/session` | `X.Y.Z` | 30 | | `node` | `X.Y.Z` | 31 | | `TypeScript` | `X.Y.Z` | 32 | | `npm` or `yarn` | `X.Y.Z` | 33 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/-vk-io-streaming.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: "@vk-io/streaming" 3 | about: Report an issue with the '@vk-io/streaming' package 4 | title: '' 5 | labels: 'package: @vk-io/streaming' 6 | assignees: '' 7 | 8 | --- 9 | 10 | 11 | 12 | #### What did you do? 13 | 14 | 15 | #### What did you expect to happen? 16 | 17 | 18 | #### What was the actual result? 19 | 20 | 21 | #### Additional Info 22 | 23 | 24 | #### Versions 25 | 26 | | package | version | 27 | | ---------------------------------- | ------- | 28 | | `vk-io` | `X.Y.Z` | 29 | | `@vk-io/streaming` | `X.Y.Z` | 30 | | `node` | `X.Y.Z` | 31 | | `TypeScript` | `X.Y.Z` | 32 | | `npm` or `yarn` | `X.Y.Z` | 33 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/-vk-io-authorization.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: "@vk-io/authorization" 3 | about: Report an issue with the '@vk-io/authorization' package 4 | title: '' 5 | labels: 'package: @vk-io/authorization' 6 | assignees: '' 7 | 8 | --- 9 | 10 | 11 | 12 | #### What did you do? 13 | 14 | 15 | #### What did you expect to happen? 16 | 17 | 18 | #### What was the actual result? 19 | 20 | 21 | #### Additional Info 22 | 23 | 24 | #### Versions 25 | 26 | | package | version | 27 | | ---------------------------------- | ------- | 28 | | `vk-io` | `X.Y.Z` | 29 | | `@vk-io/authorization` | `X.Y.Z` | 30 | | `node` | `X.Y.Z` | 31 | | `TypeScript` | `X.Y.Z` | 32 | | `npm` or `yarn` | `X.Y.Z` | 33 | -------------------------------------------------------------------------------- /packages/stateless-prompt/src/stateless-prompt.types.ts: -------------------------------------------------------------------------------- 1 | import type { HandlerMiddleware } from './types'; 2 | 3 | export interface IStatelessPromptManagerOptions { 4 | /** 5 | * The slug should be unique. 6 | * It depends on it that the bot will respond and recognize the request. 7 | */ 8 | slug: string; 9 | 10 | /** 11 | * Handles the prompt 12 | * 13 | * ```ts 14 | * const namePrompt = new StatelessPromptManager({ 15 | * slug: 'prompt-name', 16 | * handler: (context, next) => { 17 | * if (!context.text) { 18 | * return context.send('Please reply your name with text to previous message'); 19 | * } 20 | * 21 | * return context.send(`Your name is ${context.text}`); 22 | * } 23 | * }); 24 | * ``` 25 | */ 26 | handler: HandlerMiddleware; 27 | } 28 | -------------------------------------------------------------------------------- /packages/session/src/types.ts: -------------------------------------------------------------------------------- 1 | import type { Context } from 'vk-io'; 2 | 3 | import type { ISessionStorage } from './storages'; 4 | 5 | export type Middleware = (context: T, next: () => Promise) => unknown; 6 | 7 | export type SessionForceUpdate = () => Promise; 8 | 9 | export interface IContext extends Context { 10 | [key: string]: any; 11 | } 12 | 13 | export interface ISessionContext { 14 | $forceUpdate(): Promise; 15 | [key: string]: any; 16 | } 17 | 18 | export interface ISessionManagerOptions { 19 | /** 20 | * Storage based on ISessionStorage interface 21 | */ 22 | storage: ISessionStorage; 23 | 24 | /** 25 | * Key for session in context 26 | */ 27 | contextKey: string; 28 | 29 | /** 30 | * Returns the key for session storage 31 | */ 32 | getStorageKey(context: IContext & T): string; 33 | } 34 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | # https://vitepress.dev/reference/default-theme-home-page 3 | layout: home 4 | 5 | hero: 6 | name: "VK-IO" 7 | # text: "Modern VK API SDK for Node.js" 8 | tagline: This is a powerful Node.js module that allows you to easily interact with the API VK 🚀 9 | image: 10 | src: /logo.svg 11 | alt: 'Logo' 12 | actions: 13 | - theme: brand 14 | text: Getting Started [RU] 15 | link: /ru/guide/introduction 16 | - theme: alt 17 | text: API Examples 18 | link: https://github.com/negezor/vk-io/tree/master/docs/examples 19 | 20 | features: 21 | - title: Approachable 22 | details: The library already covers all popular tasks 23 | - title: Versatile 24 | details: All basic things are available in convenience abstractions 25 | - title: Typings 26 | details: Trust your code more with typing 27 | 28 | footer: Released under the MIT License. Copyright © 2016-2023 Negezor 29 | --- 30 | 31 | -------------------------------------------------------------------------------- /docs/ru/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | # https://vitepress.dev/reference/default-theme-home-page 3 | layout: home 4 | 5 | hero: 6 | name: "VK-IO" 7 | # text: "Modern VK API SDK for Node.js" 8 | tagline: Это мощный Node.js модуль, который позволяет вам легко взаимодействовать с API ВКонтакте 🚀 9 | image: 10 | src: /logo.svg 11 | alt: 'Logo' 12 | actions: 13 | - theme: brand 14 | text: Начать 15 | link: /ru/guide/introduction 16 | - theme: alt 17 | text: Примеры API 18 | link: https://github.com/negezor/vk-io/tree/master/docs/examples 19 | 20 | features: 21 | - title: Доступная 22 | details: Библиотека уже охватывает все популярные задачи 23 | - title: Разносторонняя 24 | details: Все базовые вещи доступны в удобных абстракциях 25 | - title: Типизация 26 | details: Больше доверяйте своему коду благодаря типизации 27 | 28 | footer: Released under the MIT License. Copyright © 2016-2023 Negezor 29 | --- 30 | 31 | -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: VK-IO CI 2 | 3 | on: [ push, pull_request ] 4 | 5 | jobs: 6 | test: 7 | runs-on: ubuntu-latest 8 | strategy: 9 | matrix: 10 | node-version: 11 | # Segmentation fault (core dumped) https://github.com/nodejs/node/issues/35889 12 | # - 12.x 13 | # Segmentation fault (core dumped) https://github.com/nodejs/node/issues/35889 14 | # - 14.x 15 | # Doesn't support --import for ts-loader in tests 16 | # - 16.x 17 | - 18.x 18 | - 20.x 19 | - 22.x 20 | steps: 21 | - uses: actions/checkout@v4 22 | - name: Use Node.js ${{ matrix.node-version }} 23 | uses: actions/setup-node@v4 24 | with: 25 | node-version: ${{ matrix.node-version }} 26 | - run: npm ci --ignore-scripts 27 | - run: npm run build 28 | - env: 29 | TOKEN: ${{ secrets.VK_TEST_TOKEN }} 30 | run: npm run test 31 | -------------------------------------------------------------------------------- /packages/scenes/src/contexts/scene.types.ts: -------------------------------------------------------------------------------- 1 | import type { SceneRepository } from '../scene-manager.types'; 2 | import type { IContext } from '../types'; 3 | 4 | export interface ISceneContextOptions { 5 | context: IContext; 6 | 7 | sessionKey: string; 8 | 9 | repository: SceneRepository; 10 | } 11 | 12 | export interface ISceneContextEnterOptions> { 13 | /** 14 | * Logging into a handler without executing it 15 | */ 16 | silent?: boolean; 17 | 18 | /** 19 | * The standard state for the scene 20 | */ 21 | state?: S; 22 | } 23 | 24 | export interface ISceneContextLeaveOptions { 25 | /** 26 | * Logging into a handler without executing it 27 | */ 28 | silent?: boolean; 29 | 30 | /** 31 | * Canceled scene 32 | */ 33 | canceled?: boolean; 34 | } 35 | 36 | export enum LastAction { 37 | NONE = 'none', 38 | ENTER = 'enter', 39 | LEAVE = 'leave', 40 | } 41 | -------------------------------------------------------------------------------- /packages/vk-io/src/structures/contexts/index.ts: -------------------------------------------------------------------------------- 1 | export * from './context'; 2 | 3 | export * from './comment'; 4 | export * from './dialog-flags'; 5 | export * from './dialog-messages'; 6 | export * from './dialog-notification-settings'; 7 | export * from './donut-subscription'; 8 | export * from './donut-subscription-price'; 9 | export * from './donut-withdraw'; 10 | export * from './friend-activity'; 11 | export * from './group-member'; 12 | export * from './group-update'; 13 | export * from './group-user'; 14 | export * from './like'; 15 | export * from './market-order'; 16 | export * from './message'; 17 | export * from './message-event'; 18 | export * from './message-flags'; 19 | export * from './message-subscription'; 20 | export * from './messages-read'; 21 | export * from './new-attachments'; 22 | export * from './typing'; 23 | export * from './unsupported-event'; 24 | export * from './vk-app-payload'; 25 | export * from './vk-pay-transaction'; 26 | export * from './vote'; 27 | export * from './wall-post'; 28 | -------------------------------------------------------------------------------- /packages/authorization/src/open-api.ts: -------------------------------------------------------------------------------- 1 | import { createHash } from 'crypto'; 2 | 3 | const openAPIProperties = ['expire', 'secret', 'mid', 'sid']; 4 | 5 | export interface IUserAuthorizedThroughOpenAPIOptions { 6 | clientSecret: string; 7 | params: Record<'expire' | 'mid' | 'secret' | 'sid' | 'sig', string>; 8 | } 9 | 10 | export const userAuthorizedThroughOpenAPI = ({ 11 | clientSecret, 12 | params, 13 | }: IUserAuthorizedThroughOpenAPIOptions): Promise<{ authorized: boolean }> => { 14 | let sign = ([...openAPIProperties] as (keyof typeof params)[]) 15 | .sort() 16 | .map(key => `${key}=${params[key]}`) 17 | .join(''); 18 | 19 | sign += clientSecret; 20 | sign = createHash('md5').update(sign).digest('hex'); 21 | 22 | const expire = Number(params.expire); 23 | 24 | const isExpired = Number.isNaN(expire) || expire < Date.now() / 1000; 25 | const authorized = params.sig === sign && !isExpired; 26 | 27 | return Promise.resolve({ authorized }); 28 | }; 29 | -------------------------------------------------------------------------------- /docs/examples/advanced/keyboard-builder.js: -------------------------------------------------------------------------------- 1 | const { Keyboard } = require('vk-io'); 2 | 3 | const baseBuilder = Keyboard.builder(); 4 | 5 | // Maybe user is not register 6 | const userIsNotRegistered = true; 7 | 8 | if (userIsNotRegistered) { 9 | baseBuilder.textButton({ 10 | label: 'Sign Up', 11 | payload: { 12 | command: 'sign_up', 13 | }, 14 | }); 15 | } 16 | 17 | const shopBuilder = baseBuilder.clone(); 18 | 19 | shopBuilder 20 | .textButton({ 21 | label: 'Buy a coffee', 22 | payload: { 23 | command: 'buy', 24 | item: 'coffee', 25 | }, 26 | }) 27 | .textButton({ 28 | label: 'Buy a tea', 29 | payload: { 30 | command: 'buy', 31 | item: 'tea', 32 | }, 33 | }) 34 | .row() 35 | .textButton({ 36 | label: 'Go back', 37 | payload: { 38 | command: 'go_back', 39 | }, 40 | }); 41 | 42 | console.log('Base builder', String(baseBuilder)); 43 | 44 | console.log('\nShop builder', String(shopBuilder)); 45 | -------------------------------------------------------------------------------- /packages/vk-io/src/structures/attachments/gift.ts: -------------------------------------------------------------------------------- 1 | import { ExternalAttachment, type ExternalAttachmentFactoryOptions } from './external'; 2 | 3 | import { AttachmentType, kSerializeData } from '../../utils/constants'; 4 | 5 | export interface IGiftAttachmentPayload { 6 | id: number; 7 | } 8 | 9 | export type GiftAttachmentOptions = ExternalAttachmentFactoryOptions; 10 | 11 | export class GiftAttachment extends ExternalAttachment { 12 | /** 13 | * Constructor 14 | */ 15 | public constructor(options: GiftAttachmentOptions) { 16 | super({ 17 | ...options, 18 | 19 | type: AttachmentType.GIFT, 20 | }); 21 | } 22 | 23 | /** 24 | * Returns the identifier gift 25 | */ 26 | public get id(): number { 27 | return this.payload.id; 28 | } 29 | 30 | /** 31 | * Returns the custom data 32 | */ 33 | public [kSerializeData](): object { 34 | return { 35 | id: this.id, 36 | }; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /packages/session/src/storages/memory.ts: -------------------------------------------------------------------------------- 1 | import type { ISessionStorage } from './storage'; 2 | 3 | export interface IMemoryStoreLike { 4 | get(key: K): V | undefined; 5 | set(key: K, value: V): this | undefined; 6 | delete(key: K): boolean; 7 | } 8 | 9 | export interface IMemoryStorageOptions { 10 | store: IMemoryStoreLike; 11 | } 12 | 13 | export class MemoryStorage implements ISessionStorage { 14 | private store: IMemoryStorageOptions['store']; 15 | 16 | constructor({ store = new Map() }: Partial = {}) { 17 | this.store = store; 18 | } 19 | 20 | public get(key: string): Promise { 21 | return Promise.resolve(this.store.get(key)); 22 | } 23 | 24 | public set(key: string, value: object): Promise { 25 | this.store.set(key, value); 26 | 27 | return Promise.resolve(true); 28 | } 29 | 30 | public delete(key: string): Promise { 31 | return Promise.resolve(this.store.delete(key)); 32 | } 33 | public async touch(): Promise { 34 | // ... 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016-2024 Negezor 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /packages/session/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@vk-io/session", 3 | "version": "2.2.1", 4 | "description": "Session for the library vk-io", 5 | "license": "MIT", 6 | "author": { 7 | "name": "Vladlen (Negezor)", 8 | "email": "negezor@gmail.com" 9 | }, 10 | "repository": { 11 | "type": "git", 12 | "url": "git+https://github.com/negezor/vk-io.git", 13 | "directory": "packages/session" 14 | }, 15 | "homepage": "https://github.com/negezor/vk-io#readme", 16 | "bugs": "https://github.com/negezor/vk-io/issues", 17 | "keywords": [ 18 | "vk", 19 | "io", 20 | "session" 21 | ], 22 | "files": [ 23 | "lib", 24 | "typings" 25 | ], 26 | "main": "./lib/index.js", 27 | "types": "./lib/index.d.ts", 28 | "exports": { 29 | ".": { 30 | "import": "./lib/index.mjs", 31 | "require": "./lib/index.js", 32 | "types": "./lib/index.d.ts" 33 | } 34 | }, 35 | "sideEffects": false, 36 | "engines": { 37 | "node": ">=12.20.0" 38 | }, 39 | "peerDependencies": { 40 | "vk-io": "^4.0.0" 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /packages/scenes/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@vk-io/scenes", 3 | "version": "1.2.1", 4 | "description": "Scenes for the library vk-io", 5 | "license": "MIT", 6 | "author": { 7 | "name": "Vladlen (Negezor)", 8 | "email": "negezor@gmail.com" 9 | }, 10 | "repository": { 11 | "type": "git", 12 | "url": "git+https://github.com/negezor/vk-io.git", 13 | "directory": "packages/scenes" 14 | }, 15 | "homepage": "https://github.com/negezor/vk-io#readme", 16 | "bugs": "https://github.com/negezor/vk-io/issues", 17 | "keywords": [ 18 | "vk", 19 | "io", 20 | "scenes", 21 | "stage", 22 | "step" 23 | ], 24 | "files": [ 25 | "lib", 26 | "typings" 27 | ], 28 | "main": "./lib/index.js", 29 | "types": "./lib/index.d.ts", 30 | "exports": { 31 | ".": { 32 | "import": "./lib/index.mjs", 33 | "require": "./lib/index.js", 34 | "types": "./lib/index.d.ts" 35 | } 36 | }, 37 | "sideEffects": false, 38 | "engines": { 39 | "node": ">=12.20.0" 40 | }, 41 | "peerDependencies": { 42 | "vk-io": "^4.0.0" 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /packages/hear/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@vk-io/hear", 3 | "version": "1.1.1", 4 | "description": "Hear for the library vk-io", 5 | "license": "MIT", 6 | "author": { 7 | "name": "Vladlen (Negezor)", 8 | "email": "negezor@gmail.com" 9 | }, 10 | "repository": { 11 | "type": "git", 12 | "url": "git+https://github.com/negezor/vk-io.git", 13 | "directory": "packages/hear" 14 | }, 15 | "homepage": "https://github.com/negezor/vk-io#readme", 16 | "bugs": "https://github.com/negezor/vk-io/issues", 17 | "keywords": [ 18 | "vk", 19 | "io", 20 | "hear" 21 | ], 22 | "files": [ 23 | "lib", 24 | "typings" 25 | ], 26 | "main": "./lib/index.js", 27 | "types": "./lib/index.d.ts", 28 | "exports": { 29 | ".": { 30 | "import": "./lib/index.mjs", 31 | "require": "./lib/index.js", 32 | "types": "./lib/index.d.ts" 33 | } 34 | }, 35 | "sideEffects": false, 36 | "engines": { 37 | "node": ">=12.20.0" 38 | }, 39 | "dependencies": { 40 | "middleware-io": "^2.8.1" 41 | }, 42 | "peerDependencies": { 43 | "vk-io": "^4.0.0" 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /packages/stateless-prompt/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@vk-io/stateless-prompt", 3 | "version": "1.0.1", 4 | "description": "Stateless prompts for the library vk-io", 5 | "license": "MIT", 6 | "author": { 7 | "name": "Vladlen (Negezor)", 8 | "email": "negezor@gmail.com" 9 | }, 10 | "repository": { 11 | "type": "git", 12 | "url": "git+https://github.com/negezor/vk-io.git", 13 | "directory": "packages/stateless-prompt" 14 | }, 15 | "homepage": "https://github.com/negezor/vk-io#readme", 16 | "bugs": "https://github.com/negezor/vk-io/issues", 17 | "keywords": [ 18 | "vk", 19 | "io", 20 | "stateless", 21 | "prompt" 22 | ], 23 | "files": [ 24 | "lib", 25 | "typings" 26 | ], 27 | "main": "./lib/index.js", 28 | "types": "./lib/index.d.ts", 29 | "exports": { 30 | ".": { 31 | "import": "./lib/index.mjs", 32 | "require": "./lib/index.js", 33 | "types": "./lib/index.d.ts" 34 | } 35 | }, 36 | "sideEffects": false, 37 | "engines": { 38 | "node": ">=12.20.0" 39 | }, 40 | "peerDependencies": { 41 | "vk-io": "^4.0.0" 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /docs/examples/advanced/opportunities-hears.js: -------------------------------------------------------------------------------- 1 | const { VK } = require('vk-io'); 2 | const { HearManager } = require('@vk-io/hear'); 3 | 4 | const vk = new VK({ 5 | token: process.env.TOKEN, 6 | }); 7 | 8 | const hearManager = new HearManager(); 9 | 10 | vk.updates.on('message_new', hearManager.middleware); 11 | 12 | // Strict string compare 13 | hearManager.hear('/stirct-string', async context => { 14 | await context.send('You written /stirct-string'); 15 | }); 16 | 17 | // Regex match 18 | hearManager.hear(/^\/text (.+)/i, context => { 19 | context.send(`You written ${context.$match[1]}`); 20 | }); 21 | 22 | // Callback validation 23 | hearManager.hear( 24 | value => value?.includes('cat'), 25 | async context => { 26 | await context.send('Who say cat?!'); 27 | }, 28 | ); 29 | 30 | // Callback validation with context 31 | hearManager.hear( 32 | (_value, context) => { 33 | const messagePayload = context.messagePayload || {}; 34 | 35 | return messagePayload.command === 'start'; 36 | }, 37 | async context => { 38 | await context.send('Start button pressed'); 39 | }, 40 | ); 41 | 42 | hearManager.onFallback(async context => { 43 | await context.send('Action not found, write /help'); 44 | }); 45 | 46 | vk.updates.start().catch(console.error); 47 | -------------------------------------------------------------------------------- /packages/vk-io/src/errors/error.ts: -------------------------------------------------------------------------------- 1 | export interface IVKErrorOptions { 2 | code: string | number; 3 | message: string; 4 | cause?: Error; 5 | } 6 | 7 | /** 8 | * General error class 9 | */ 10 | export class VKError extends Error { 11 | /** 12 | * Error code 13 | */ 14 | public code: string | number; 15 | 16 | /** 17 | * Error stack 18 | */ 19 | public stack!: string; 20 | 21 | /** 22 | * Error cause 23 | */ 24 | public cause?: Error; 25 | 26 | /** 27 | * Constructor 28 | */ 29 | public constructor({ code, message, cause }: IVKErrorOptions) { 30 | super(message, { 31 | cause, 32 | }); 33 | 34 | this.code = code; 35 | this.name = this.constructor.name; 36 | 37 | Error.captureStackTrace(this, this.constructor); 38 | } 39 | 40 | /** 41 | * Returns custom tag 42 | */ 43 | public get [Symbol.toStringTag](): string { 44 | return this.constructor.name; 45 | } 46 | 47 | /** 48 | * Returns property for json 49 | */ 50 | public toJSON(): Pick { 51 | const json = {} as Pick; 52 | 53 | for (const key of Object.getOwnPropertyNames(this)) { 54 | json[key as keyof this] = this[key as keyof this]; 55 | } 56 | 57 | return json; 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /packages/hear/src/helpers.ts: -------------------------------------------------------------------------------- 1 | import type { HearFunctionCondition } from './types'; 2 | 3 | export const splitPath = (path: string): string[] => 4 | path 5 | .replace(/\[([^[\]]*)\]/g, '.$1.') 6 | .split('.') 7 | .filter(Boolean); 8 | 9 | export const getObjectValue = (source: Record, selectors: string[]): any => { 10 | let link = source; 11 | 12 | for (const selector of selectors) { 13 | if (!link[selector]) { 14 | return undefined; 15 | } 16 | 17 | link = link[selector]; 18 | } 19 | 20 | return link; 21 | }; 22 | 23 | export const unifyCondition = (condition: unknown): HearFunctionCondition => { 24 | if (typeof condition === 'function') { 25 | return condition as HearFunctionCondition; 26 | } 27 | 28 | if (condition instanceof RegExp) { 29 | return text => condition.test(text as string); 30 | } 31 | 32 | if (Array.isArray(condition)) { 33 | const arrayConditions = condition.map(unifyCondition); 34 | 35 | return (value, context) => 36 | Array.isArray(value) 37 | ? arrayConditions.every((cond): boolean => value.some((val): boolean => cond(val, context))) 38 | : arrayConditions.some((cond): boolean => cond(value, context)); 39 | } 40 | 41 | return value => value === condition; 42 | }; 43 | -------------------------------------------------------------------------------- /docs/examples/advanced/context-modification.js: -------------------------------------------------------------------------------- 1 | const { VK } = require('vk-io'); 2 | const { HearManager } = require('@vk-io/hear'); 3 | 4 | const vk = new VK({ 5 | token: process.env.TOKEN, 6 | }); 7 | 8 | const hearManager = new HearManager(); 9 | 10 | // Some users "database" 11 | const users = new Map([]); 12 | 13 | vk.updates.on('message_new', (context, next) => { 14 | let user = users.get(context.senderId); 15 | 16 | if (!user) { 17 | user = { 18 | displayName: `User ${context.senderId}`, 19 | }; 20 | 21 | users.set(context.senderId, user); 22 | } 23 | 24 | // Add user to context 25 | context.user = user; 26 | 27 | // We add a method with the answer through the appeal 28 | context.answer = (text, params) => context.send(`${context.user.displayName}, ${text}`, params); 29 | 30 | return next(); 31 | }); 32 | 33 | vk.updates.on('message_new', hearManager.middleware); 34 | 35 | hearManager.hear(/hello/i, async context => { 36 | await context.answer('hello!'); // Will send "User 1234, hello!" 37 | }); 38 | 39 | hearManager.hear(/set username (.+)/i, async context => { 40 | const [, displayName] = context.$match; 41 | 42 | // Set new display name 43 | context.user.displayName = displayName; 44 | 45 | await context.answer('display name changed.'); 46 | }); 47 | 48 | vk.updates.start().catch(console.error); 49 | -------------------------------------------------------------------------------- /packages/vk-io/src/structures/contexts/unsupported-event.ts: -------------------------------------------------------------------------------- 1 | import { Context, type ContextDefaultState, type ContextFactoryOptions } from './context'; 2 | 3 | import { kSerializeData } from '../../utils/constants'; 4 | 5 | import { pickProperties } from '../../utils/helpers'; 6 | 7 | export type UnsupportedEventContextType = 'unsupported_event'; 8 | type UnsupportedEventContextSubType = string; 9 | 10 | type UnsupportedEventContextPayload

= P; 11 | 12 | export type UnsupportedEventOptions = ContextFactoryOptions, S>; 13 | 14 | export class UnsupportedEventContext = object> extends Context< 15 | P, 16 | S, 17 | UnsupportedEventContextType, 18 | UnsupportedEventContextSubType 19 | > { 20 | constructor(options: UnsupportedEventOptions) { 21 | super({ 22 | ...options, 23 | 24 | type: 'unsupported_event', 25 | subTypes: [options.updateType as string], 26 | 27 | payload: options.payload, 28 | }); 29 | } 30 | 31 | /** 32 | * Event payload 33 | */ 34 | public get eventPayload(): P { 35 | return this.payload; 36 | } 37 | 38 | /** 39 | * Returns the custom data 40 | */ 41 | public [kSerializeData](): object { 42 | return pickProperties(this, ['eventPayload']); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /scripts/typings-generator/printer.js: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line import/no-extraneous-dependencies 2 | const ts = require('typescript'); 3 | 4 | const nodeFs = require('fs'); 5 | const nodePath = require('path'); 6 | 7 | // HACK!!! 8 | const REPLACE_MULTI_SEMICOLON_RE = /;{2,}/; 9 | const REPLACE_EXPORT_SEMICOLON_RE = /};/; 10 | 11 | /** 12 | * @param {string} path 13 | */ 14 | module.exports = function createPrinter(path) { 15 | const filename = nodePath.basename(path); 16 | 17 | const methodsFile = ts.createSourceFile(filename, '', ts.ScriptTarget.ESNext, false, ts.ScriptKind.TS); 18 | 19 | const printer = ts.createPrinter({ 20 | newLine: ts.NewLineKind.LineFeed, 21 | }); 22 | 23 | const output = nodeFs.createWriteStream(path, { 24 | start: 0, 25 | }); 26 | 27 | output.write('/* eslint-disable */\n'); 28 | 29 | return { 30 | end() { 31 | output.end(); 32 | }, 33 | write(text) { 34 | output.write(text); 35 | }, 36 | /** 37 | * @param {import('typescript').Node} node 38 | */ 39 | writeNode(node) { 40 | const result = printer.printNode(ts.EmitHint.Unspecified, node, methodsFile); 41 | 42 | const hacked = result.replace(REPLACE_MULTI_SEMICOLON_RE, ';').replace(REPLACE_EXPORT_SEMICOLON_RE, '}'); 43 | 44 | output.write(`${hacked}\n\n`); 45 | }, 46 | }; 47 | }; 48 | -------------------------------------------------------------------------------- /packages/vk-io/src/vk.ts: -------------------------------------------------------------------------------- 1 | import { inspectable } from 'inspectable'; 2 | 3 | import { API } from './api'; 4 | import { Updates } from './updates'; 5 | import { Upload } from './upload'; 6 | 7 | import type { VKOptions } from './types'; 8 | 9 | import { CallbackService } from './utils/callback-service'; 10 | 11 | /** 12 | * Main class 13 | */ 14 | export class VK { 15 | public api: API; 16 | 17 | public upload: Upload; 18 | 19 | public updates: Updates; 20 | 21 | public callbackService: CallbackService; 22 | 23 | /** 24 | * Constructor 25 | */ 26 | public constructor(options: Partial & { token: string }) { 27 | this.callbackService = options.callbackService || new CallbackService(); 28 | 29 | this.api = new API({ 30 | ...options, 31 | 32 | callbackService: this.callbackService, 33 | }); 34 | 35 | this.upload = new Upload({ 36 | ...options, 37 | 38 | api: this.api, 39 | }); 40 | 41 | this.updates = new Updates({ 42 | ...options, 43 | 44 | api: this.api, 45 | upload: this.upload, 46 | }); 47 | } 48 | 49 | /** 50 | * Returns custom tag 51 | */ 52 | public get [Symbol.toStringTag](): string { 53 | return this.constructor.name; 54 | } 55 | } 56 | 57 | inspectable(VK, { 58 | serialize: ({ api, updates }) => ({ api, updates }), 59 | }); 60 | -------------------------------------------------------------------------------- /packages/hear/README.md: -------------------------------------------------------------------------------- 1 | # VK-IO Hear 2 | 3 | NPM version 4 | Build Status 5 | NPM downloads 6 | 7 | > VK-IO Hear - Simple implementation of the hears ⚙️ 8 | 9 | ## 📦 Installation 10 | 11 | > **[Node.js](https://nodejs.org/) 12.20.0 or newer is required** 12 | 13 | - **Using `npm`** (recommended) 14 | ```shell 15 | npm i @vk-io/hear 16 | ``` 17 | - **Using `Yarn`** 18 | ```shell 19 | yarn add @vk-io/hear 20 | ``` 21 | - **Using `pnpm`** 22 | ```shell 23 | pnpm add @vk-io/hear 24 | ``` 25 | 26 | ## Example usage 27 | 28 | ```javascript 29 | import { VK, MessageContext } from 'vk-io'; 30 | 31 | import { HearManager } from '@vk-io/hear'; 32 | 33 | const vk = new VK({ 34 | token: process.env.TOKEN 35 | }); 36 | 37 | const hearManager = new HearManager(); 38 | 39 | vk.updates.on('message_new', hearManager.middleware); 40 | 41 | hearManager.hear(/^hello$/, async (context) => { 42 | await context.send('Hello!'); 43 | }); 44 | 45 | vk.updates.start().catch(console.error); 46 | ``` 47 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vk-io-monorepo", 3 | "private": true, 4 | "workspaces": [ 5 | "packages/*" 6 | ], 7 | "engines": { 8 | "node": ">=12.20.0" 9 | }, 10 | "type": "module", 11 | "devDependencies": { 12 | "@biomejs/biome": "^2.3.2", 13 | "@rollup/plugin-json": "^6.1.0", 14 | "@types/node": "^24.9.2", 15 | "rollup": "^4.52.5", 16 | "rollup-plugin-typescript2": "^0.36.0", 17 | "tsx": "^4.20.6", 18 | "typedoc": "^0.28.14", 19 | "typescript": "^5.9.3", 20 | "vitepress": "^1.6.4" 21 | }, 22 | "scripts": { 23 | "prepare": "npm run rollup:build && npm run test", 24 | "build": "npm run rollup:build", 25 | "watch": "npm run rollup:watch", 26 | "clean": "rm -rf ./packages/*/lib", 27 | "rollup:build": "NODE_ENV=production rollup -c rollup.config.js", 28 | "rollup:watch": "npm run rollup:build -- -w", 29 | "docs:dev": "vitepress dev docs", 30 | "docs:build": "sh scripts/deploy-docs.sh", 31 | "docs:preview": "vitepress preview docs", 32 | "test": "npm run check && npm run test:node", 33 | "test:node": "node --import tsx --test packages/*/test/*.test.ts", 34 | "lint": "npm run lint:biome", 35 | "lint:biome": "biome lint .", 36 | "check": "biome check .", 37 | "fmt": "biome format --write .", 38 | "fix": "npm run check -- --write ." 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /docs/ru/guide/collect-iterator.md: -------------------------------------------------------------------------------- 1 | # Collect Iterator 2 | 3 | Хелпер модуль позволяющий упростить получение данных 4 | 5 | ## Описание типов 6 | 7 | [API Reference [EN]](https://negezor.github.io/vk-io/references/vk-io/index.html#createCollectIterator) 8 | 9 | ## Концепт 10 | 11 | Множество методов возвращают только часть данных, и для получения остальной части — нужно указывать смещение. Для этого в библиотеке есть итератор который автоматический ставит смещение. 12 | 13 | ## Пример 14 | 15 | ```ts 16 | import { API, createCollectIterator } from 'vk-io'; 17 | 18 | const api = new API({ 19 | token: process.env.TOKEN 20 | }); 21 | 22 | const iterator = createCollectIterator({ 23 | api, 24 | 25 | method: 'messages.getConversations', 26 | params: { 27 | // Будет получать profiles и groups 28 | extended: 1 29 | }, 30 | 31 | // Максимальный count в методе 32 | countPerRequest: 200, 33 | 34 | // Устанавливайте опцию для методов которые не позволяет получить больше N данных, например `users.search` 35 | // maxCount: 1000, 36 | 37 | // Количество попыток вызвать снова при ошибке 38 | // retryLimit: 3, 39 | 40 | // Количество паралельных вызовов если поддерживается execute 41 | // parallelRequests: 25 42 | }); 43 | 44 | for await (const chunk of iterator) { 45 | // chunk.received 46 | // chunk.percent 47 | // chunk.total 48 | 49 | // chunk.items 50 | // chunk.profiles 51 | // chunk.groups 52 | } 53 | ``` 54 | -------------------------------------------------------------------------------- /docs/examples/advanced/hear-object.js: -------------------------------------------------------------------------------- 1 | const { VK } = require('vk-io'); 2 | const { HearManager } = require('@vk-io/hear'); 3 | 4 | const vk = new VK({ 5 | token: process.env.TOKEN, 6 | }); 7 | 8 | const hearManager = new HearManager(); 9 | 10 | vk.updates.on('message_new', hearManager.middleware); 11 | 12 | // Basic object 13 | hearManager.hear( 14 | { 15 | text: 'test', 16 | senderType: 'user', 17 | isChat: true, 18 | }, 19 | async context => { 20 | await context.send('The text of the message "test" and sent from the user in the chat'); 21 | }, 22 | ); 23 | 24 | // Array allow values with callback 25 | hearManager.hear( 26 | { 27 | text: 'test', 28 | senderId: [1, 2, id => id === 3], 29 | }, 30 | async context => { 31 | await context.send('The text of the message "test" and sent the allowed user ids'); 32 | }, 33 | ); 34 | 35 | // RegExp 36 | hearManager.hear( 37 | { 38 | text: /^test$/, 39 | senderId: [1, 2, 3, /4$/], 40 | }, 41 | async context => { 42 | await context.send('The text of the message "test" and sent with the allowed user ids or ending with 4'); 43 | }, 44 | ); 45 | 46 | // Nested properties 47 | // context.session = { action: 'test' }; 48 | hearManager.hear( 49 | { 50 | 'session.action': 'test', 51 | }, 52 | async context => { 53 | await context.send('Nested property "context.session.action" with value "test"'); 54 | }, 55 | ); 56 | -------------------------------------------------------------------------------- /packages/streaming/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@vk-io/streaming", 3 | "version": "1.2.1", 4 | "description": "Separated module for receiving data with Streaming API", 5 | "license": "MIT", 6 | "author": { 7 | "name": "Vladlen (Negezor)", 8 | "email": "negezor@gmail.com" 9 | }, 10 | "repository": { 11 | "type": "git", 12 | "url": "git+https://github.com/negezor/vk-io.git", 13 | "directory": "packages/streaming" 14 | }, 15 | "homepage": "https://github.com/negezor/vk-io#readme", 16 | "bugs": "https://github.com/negezor/vk-io/issues", 17 | "keywords": [ 18 | "vk", 19 | "io", 20 | "api", 21 | "streaming" 22 | ], 23 | "files": [ 24 | "lib" 25 | ], 26 | "main": "./lib/index.js", 27 | "types": "./lib/index.d.ts", 28 | "exports": { 29 | ".": { 30 | "import": "./lib/index.mjs", 31 | "require": "./lib/index.js", 32 | "types": "./lib/index.d.ts" 33 | } 34 | }, 35 | "sideEffects": false, 36 | "engines": { 37 | "node": ">=12.20.0" 38 | }, 39 | "peerDependencies": { 40 | "vk-io": "^4.0.0" 41 | }, 42 | "dependencies": { 43 | "debug": "^4.4.3", 44 | "inspectable": "^2.1.0", 45 | "node-fetch": "^3.3.2", 46 | "ws": "^8.18.3" 47 | }, 48 | "devDependencies": { 49 | "@types/debug": "^4.1.12", 50 | "@types/ws": "^8.18.1" 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /scripts/typings-generator/utils/helpers.js: -------------------------------------------------------------------------------- 1 | const nodeFs = require('fs'); 2 | const nodePath = require('path'); 3 | const nodeUtil = require('util'); 4 | 5 | const readFile = nodeUtil.promisify(nodeFs.readFile).bind(nodeUtil); 6 | 7 | const writeFile = nodeUtil.promisify(nodeFs.writeFile).bind(nodeUtil); 8 | 9 | const getDirname = url => nodePath.dirname(new URL(url).pathname).replace(/^\//, ''); 10 | 11 | const upperFirstLetter = str => str[0].toUpperCase() + str.substring(1); 12 | 13 | const toPascalCase = str => { 14 | const s = str 15 | .match(/[A-Z]{2,}(?=[A-Z][a-z]+[0-9]*|\b)|[A-Z]?[a-z]+[0-9]*|[A-Z]|[0-9]+/g) 16 | .map(x => x.slice(0, 1).toUpperCase() + x.slice(1).toLowerCase()) 17 | .join(''); 18 | 19 | return upperFirstLetter(s); 20 | }; 21 | 22 | const readJSONFile = async path => { 23 | const content = await readFile(path, 'utf-8'); 24 | 25 | return JSON.parse(content); 26 | }; 27 | 28 | const NEW_LINE_RE = /\n/; 29 | 30 | const formatTSComments = text => { 31 | let template = '*\n'; 32 | 33 | const lines = text 34 | .trim('\n') 35 | .split(NEW_LINE_RE) 36 | .map(str => str.trim()); 37 | 38 | for (const line of lines) { 39 | template += ` *${line !== '' ? ` ${line}` : ''}\n`; 40 | } 41 | 42 | template += ' '; 43 | 44 | return template; 45 | }; 46 | 47 | module.exports = { 48 | readFile, 49 | writeFile, 50 | getDirname, 51 | upperFirstLetter, 52 | toPascalCase, 53 | readJSONFile, 54 | formatTSComments, 55 | }; 56 | -------------------------------------------------------------------------------- /docs/ru/guide/introduction.md: -------------------------------------------------------------------------------- 1 | # Введение 2 | 3 | ## Что такое VK-IO? 4 | VK-IO - это мощный [Node.js](https://nodejs.org) модуль, который позволяет вам легко взаимодействовать с API ВКонтакте, благодаря упрощённому интерфейсу и своей гибкости, предоставляя уровень абстракции над стандартным API. 5 | 6 | ### Возможности 7 | 1. **Завершённая.** `100%` покрытие VK API 8 | 2. **Простая.** Предсказуемая абстракция VK API. Сопоставление API 1 к 1 9 | ```ts 10 | vk.api.users.get({ ... }); 11 | ``` 12 | 3. **Надёжная.** Библиотека написана с использованием **TypeScript** и покрыта тестами. 13 | 4. **Мощная.** Поддерживает следующие дополнительные функции: 14 | - Поддержка **прокси** через свои [Agent](https://nodejs.org/api/https.html#new-agentoptions); 15 | - Автоматическое **распараллеливание запросов** для обработки большого количества запросов к API; 16 | - Пользовательская [авторизация](./packages/authorization/README.md) (даже с логином и паролем); 17 | - [Экосистема для разработки ботов](#useful-modules-that-may-be-useful-to-you). 18 | 19 | ### Предыстория 20 | В начале мне нужна была простая и удобная библиотека для работы с API, но каждый раз когда я пробовал новую библиотеку, она зачастую оказывалась неудобной или попросту не обновлялась уже пару лет. Так и появилась на свет `vk-io` которая учитывала все эти неудобства и исправила их в корне. 21 | 22 | ## Начнём 23 | [Установка](./installation.md) 24 | 25 | ::: tip Совет 26 | Руководство предполагает знания JavaScript на среднем уровне. Если вы новичок в разработке Node.js приложений, вам стоит изучить основы, а затем продолжить. 27 | ::: 28 | -------------------------------------------------------------------------------- /packages/authorization/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@vk-io/authorization", 3 | "version": "1.4.1", 4 | "description": "Separated module for authorization", 5 | "license": "MIT", 6 | "author": { 7 | "name": "Vladlen (Negezor)", 8 | "email": "negezor@gmail.com" 9 | }, 10 | "repository": { 11 | "type": "git", 12 | "url": "git+https://github.com/negezor/vk-io.git", 13 | "directory": "packages/authorization" 14 | }, 15 | "homepage": "https://github.com/negezor/vk-io#readme", 16 | "bugs": "https://github.com/negezor/vk-io/issues", 17 | "keywords": [ 18 | "vk", 19 | "io", 20 | "api", 21 | "authorization" 22 | ], 23 | "files": [ 24 | "lib" 25 | ], 26 | "main": "./lib/index.js", 27 | "types": "./lib/index.d.ts", 28 | "exports": { 29 | ".": { 30 | "import": "./lib/index.mjs", 31 | "require": "./lib/index.js", 32 | "types": "./lib/index.d.ts" 33 | } 34 | }, 35 | "sideEffects": false, 36 | "engines": { 37 | "node": ">=12.20.0" 38 | }, 39 | "peerDependencies": { 40 | "vk-io": "^4.0.0" 41 | }, 42 | "dependencies": { 43 | "abort-controller": "^3.0.0", 44 | "cheerio": "^1.1.2", 45 | "debug": "^4.4.3", 46 | "inspectable": "^2.1.0", 47 | "node-fetch": "^3.3.2", 48 | "tough-cookie": "^4.1.4" 49 | }, 50 | "devDependencies": { 51 | "@types/cheerio": "^1.0.0", 52 | "@types/debug": "^4.1.12", 53 | "@types/tough-cookie": "^4.0.5" 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /packages/vk-io/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vk-io", 3 | "version": "4.10.1", 4 | "description": "Modern VK API SDK for Node.js", 5 | "license": "MIT", 6 | "author": { 7 | "name": "Vladlen (Negezor)", 8 | "email": "negezor@gmail.com" 9 | }, 10 | "repository": { 11 | "type": "git", 12 | "url": "git+https://github.com/negezor/vk-io.git", 13 | "directory": "packages/vk-io" 14 | }, 15 | "homepage": "https://github.com/negezor/vk-io#readme", 16 | "bugs": "https://github.com/negezor/vk-io/issues", 17 | "keywords": [ 18 | "vk", 19 | "api", 20 | "sdk", 21 | "bot", 22 | "node", 23 | "js", 24 | "es6", 25 | "es7" 26 | ], 27 | "files": [ 28 | "lib" 29 | ], 30 | "main": "./lib/index.js", 31 | "types": "./lib/index.d.ts", 32 | "exports": { 33 | ".": { 34 | "import": "./lib/index.mjs", 35 | "require": "./lib/index.js", 36 | "types": "./lib/index.d.ts" 37 | } 38 | }, 39 | "sideEffects": false, 40 | "engines": { 41 | "node": ">=12.20.0" 42 | }, 43 | "devDependencies": { 44 | "@types/debug": "^4.1.12", 45 | "@types/express": "^4.17.21", 46 | "@types/koa": "^2.15.0" 47 | }, 48 | "dependencies": { 49 | "abort-controller": "^3.0.0", 50 | "debug": "^4.4.3", 51 | "form-data-encoder": "^1.7.2", 52 | "formdata-node": "^4.4.1", 53 | "inspectable": "^2.1.0", 54 | "middleware-io": "^2.8.1", 55 | "node-fetch": "^3.3.2" 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /packages/vk-io/src/collect/execute-code.ts: -------------------------------------------------------------------------------- 1 | import { getExecuteParams } from '../utils/helpers'; 2 | 3 | export interface IExecuteCodeOptions { 4 | method: string; 5 | params: Record; 6 | } 7 | 8 | export const getExecuteCode = ({ method, params }: IExecuteCodeOptions): string => ` 9 | var params = ${getExecuteParams(params)}; 10 | 11 | params.offset = parseInt(Args.offset); 12 | 13 | var total = parseInt(Args.total); 14 | var received = parseInt(Args.received); 15 | 16 | var parallelRequests = parseInt(Args.parallelRequests); 17 | 18 | var proceed = total == 0 || received < total; 19 | 20 | var i = 0, items = [], profiles = [], groups = [], result, length; 21 | 22 | while (i < parallelRequests && proceed) { 23 | result = API.${method}(params); 24 | length = result.items.length; 25 | 26 | if (total == 0 || total > result.count) { 27 | total = result.count; 28 | } 29 | 30 | items = items + result.items; 31 | if (result.profiles) 32 | profiles = profiles + result.profiles; 33 | if (result.groups) 34 | groups = groups + result.groups; 35 | 36 | received = received + length; 37 | params.offset = params.offset + length; 38 | 39 | proceed = received < total; 40 | i = i + 1; 41 | } 42 | 43 | return { 44 | count: total, 45 | items: items.splice(0, total), 46 | profiles: profiles, 47 | groups: groups 48 | }; 49 | `; 50 | -------------------------------------------------------------------------------- /packages/stateless-prompt/src/stateless-prompt-manager.ts: -------------------------------------------------------------------------------- 1 | import { getDataHash } from './identifier'; 2 | 3 | import type { IStatelessPromptManagerOptions } from './stateless-prompt.types'; 4 | import type { HandlerMiddleware, MessageContext, Middleware } from './types'; 5 | 6 | export class StatelessPromptManager { 7 | protected slug: string; 8 | 9 | protected uniqueIdentifier: string; 10 | 11 | protected handler: HandlerMiddleware; 12 | 13 | public constructor({ slug, handler }: IStatelessPromptManagerOptions) { 14 | this.slug = slug; 15 | this.uniqueIdentifier = getDataHash(slug); 16 | 17 | this.handler = handler; 18 | } 19 | 20 | /** 21 | * The suffix to add to the end of the message. 22 | * Due to this, the bot understands who is being addressed. 23 | * 24 | * ```ts 25 | * context.send('How old are you? Please reply to this message. ' + agePrompt.suffix); 26 | * ``` 27 | */ 28 | public get suffix(): string { 29 | return this.uniqueIdentifier; 30 | } 31 | 32 | /** 33 | * Returns the middleware for intercept 34 | * 35 | * ```ts 36 | * updates.on('message_new', agePrompt.middlewareIntercept); 37 | * ``` 38 | */ 39 | public get middlewareIntercept(): Middleware { 40 | return (context: MessageContext, next: () => Promise): unknown => { 41 | const { replyMessage } = context; 42 | 43 | if (!replyMessage?.text?.endsWith(this.uniqueIdentifier)) { 44 | return next(); 45 | } 46 | 47 | return this.handler(context, next); 48 | }; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /packages/vk-io/src/upload/helpers.ts: -------------------------------------------------------------------------------- 1 | import { PassThrough, Stream } from 'stream'; 2 | 3 | import type { UploadAllowedSource, UploadNormalizedSourceOptions } from './types'; 4 | 5 | /** 6 | * Check object is stream 7 | */ 8 | export const isStream = (source: NodeJS.ReadableStream | Buffer | string): boolean => 9 | typeof source === 'object' && source instanceof Stream; 10 | 11 | /** 12 | * Copies object params to new object 13 | */ 14 | export const pickExistingProperties = (params: T, properties: K[]): Pick => { 15 | const copies: Pick = {} as Pick; 16 | 17 | for (const property of properties) { 18 | if (params[property] !== undefined) { 19 | copies[property] = params[property]; 20 | } 21 | } 22 | 23 | return copies; 24 | }; 25 | 26 | export const normalizeSource = (rawSource: UploadAllowedSource): UploadNormalizedSourceOptions => { 27 | if ('value' in rawSource) { 28 | return { 29 | values: [rawSource], 30 | }; 31 | } 32 | 33 | return { 34 | ...rawSource, 35 | 36 | values: Array.isArray(rawSource.values) ? rawSource.values : [rawSource.values], 37 | }; 38 | }; 39 | 40 | export const streamToBuffer = async (rawStream: NodeJS.ReadableStream): Promise => { 41 | const stream = new PassThrough(); 42 | 43 | rawStream.pipe(stream); 44 | 45 | const chunks: Buffer[] = []; 46 | let totalSize = 0; 47 | 48 | for await (const chunk of stream) { 49 | totalSize += (chunk as Buffer).length; 50 | 51 | chunks.push(chunk as Buffer); 52 | } 53 | 54 | return Buffer.concat(chunks, totalSize); 55 | }; 56 | -------------------------------------------------------------------------------- /packages/streaming/README.md: -------------------------------------------------------------------------------- 1 | # VK-IO Streaming 2 | NPM version 3 | Build Status 4 | NPM downloads 5 | 6 | > VK-IO Streaming API - Separated module for receiving data with Streaming API ⚙️ 7 | 8 | ## 📦 Installation 9 | 10 | > **[Node.js](https://nodejs.org/) 12.20.0 or newer is required** 11 | 12 | - **Using `npm`** (recommended) 13 | ```shell 14 | npm i @vk-io/streaming 15 | ``` 16 | - **Using `Yarn`** 17 | ```shell 18 | yarn add @vk-io/streaming 19 | ``` 20 | - **Using `pnpm`** 21 | ```shell 22 | pnpm add @vk-io/streaming 23 | ``` 24 | 25 | ## Example usage 26 | ```javascript 27 | import { VK } from 'vk-io'; 28 | 29 | import { StreamingAPI } from '@vk-io/streaming'; 30 | 31 | const vk = new VK({ 32 | token: process.env.TOKEN 33 | }); 34 | 35 | const streaming = new StreamingAPI({ 36 | api: vk.api, 37 | updates: vk.updates 38 | }); 39 | 40 | vk.updates.on('publication', (context) => { 41 | console.log('Streaming context', context); 42 | }); 43 | 44 | async function run() { 45 | await streaming.startWebSocket(); 46 | 47 | await streaming.addRule({ 48 | tag: 'halloween', 49 | value: 'тыква' 50 | }); 51 | } 52 | 53 | run().catch(console.error); 54 | ``` 55 | -------------------------------------------------------------------------------- /docs/examples/simple-updates-bot.js: -------------------------------------------------------------------------------- 1 | const { VK } = require('vk-io'); 2 | const { HearManager } = require('@vk-io/hear'); 3 | 4 | const vk = new VK({ 5 | token: process.env.TOKEN, 6 | }); 7 | 8 | const hearManager = new HearManager(); 9 | 10 | vk.updates.on('message_new', hearManager.middleware); 11 | 12 | hearManager.hear('/start', async context => { 13 | await context.send(` 14 | My commands list 15 | 16 | /cat - Cat photo 17 | /purr - Cat purring 18 | /time - The current date 19 | /reverse - Reverse text 20 | `); 21 | }); 22 | 23 | hearManager.hear('/cat', async context => { 24 | await Promise.all([ 25 | context.send('Wait for the uploads awesome 😻'), 26 | 27 | context.sendPhotos({ 28 | value: 'https://loremflickr.com/400/300/', 29 | }), 30 | ]); 31 | }); 32 | 33 | hearManager.hear(['/time', '/date'], async context => { 34 | await context.send(String(new Date())); 35 | }); 36 | 37 | hearManager.hear(/^\/reverse (.+)/i, async context => { 38 | await context.send(context.$match[1].split('').reverse().join('')); 39 | }); 40 | 41 | const catsPurring = [ 42 | 'http://ronsen.org/purrfectsounds/purrs/trip.mp3', 43 | 'http://ronsen.org/purrfectsounds/purrs/maja.mp3', 44 | 'http://ronsen.org/purrfectsounds/purrs/chicken.mp3', 45 | ]; 46 | 47 | hearManager.hear('/purr', async context => { 48 | const link = catsPurring[Math.floor(Math.random() * catsPurring.length)]; 49 | 50 | await Promise.all([ 51 | context.send('Wait for the uploads purring 😻'), 52 | 53 | context.sendAudioMessage({ 54 | value: link, 55 | }), 56 | ]); 57 | }); 58 | 59 | vk.updates.start().catch(console.error); 60 | -------------------------------------------------------------------------------- /packages/vk-io/src/collect/executes.ts: -------------------------------------------------------------------------------- 1 | import type { ExecuteError } from '../errors'; 2 | 3 | import { type API, APIRequest } from '../api'; 4 | 5 | import { getChainReturn, resolveExecuteTask } from '../utils/helpers'; 6 | 7 | export interface IExecutesPayload { 8 | response: any[]; 9 | errors: ExecuteError[]; 10 | } 11 | 12 | export interface IExecutesOptions { 13 | api: API; 14 | 15 | method: string; 16 | queue: Record[]; 17 | } 18 | 19 | export const executeRequests = async (api: API, queue: APIRequest[]): Promise => { 20 | const out: IExecutesPayload = { 21 | response: [], 22 | errors: [], 23 | }; 24 | 25 | if (queue.length === 0) { 26 | return out; 27 | } 28 | 29 | while (queue.length > 0) { 30 | const tasks = queue.splice(0, 25); 31 | const code = getChainReturn(tasks.map(String)); 32 | 33 | try { 34 | const response = await api.execute({ code }); 35 | 36 | resolveExecuteTask(tasks, response); 37 | out.response.push(...response.response); 38 | out.errors.push(...response.errors); 39 | } catch (error) { 40 | for (const task of tasks) { 41 | task.reject(error); 42 | } 43 | 44 | throw error; 45 | } 46 | } 47 | 48 | return out; 49 | }; 50 | 51 | export const executes = ({ api, method, queue }: IExecutesOptions): Promise => 52 | executeRequests( 53 | api, 54 | queue.map( 55 | params => 56 | new APIRequest({ 57 | api, 58 | method, 59 | params, 60 | }), 61 | ), 62 | ); 63 | -------------------------------------------------------------------------------- /packages/scenes/src/scenes/step.ts: -------------------------------------------------------------------------------- 1 | import type { MessageContext } from 'vk-io'; 2 | 3 | import { StepSceneContext } from '../contexts'; 4 | 5 | import type { IScene } from './scene'; 6 | 7 | import { LastAction } from '../contexts/scene.types'; 8 | import type { IStepContext, IStepSceneOptions, StepSceneHandler } from './step.types'; 9 | 10 | export class StepScene = Record> 11 | implements IScene 12 | { 13 | public slug: string; 14 | 15 | private steps: StepSceneHandler[]; 16 | 17 | private onEnterHandler: NonNullable['enterHandler']>; 18 | 19 | private onLeaveHandler: NonNullable['leaveHandler']>; 20 | 21 | public constructor(slug: string, rawOptions: IStepSceneOptions | StepSceneHandler[]) { 22 | const options = Array.isArray(rawOptions) ? { steps: rawOptions } : rawOptions; 23 | 24 | this.slug = slug; 25 | 26 | this.steps = options.steps; 27 | this.onEnterHandler = options.enterHandler || ((): void => {}); 28 | this.onLeaveHandler = options.leaveHandler || ((): void => {}); 29 | } 30 | 31 | public async enterHandler(context: IStepContext & T): Promise { 32 | context.scene.step = new StepSceneContext({ 33 | context, 34 | // @ts-expect-error 35 | steps: this.steps, 36 | }); 37 | 38 | await this.onEnterHandler(context); 39 | 40 | if (context.scene.lastAction !== LastAction.LEAVE) { 41 | await context.scene.step.reenter(); 42 | } 43 | } 44 | 45 | public leaveHandler(context: IStepContext & T): Promise { 46 | return Promise.resolve(this.onLeaveHandler(context)); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /scripts/typings-generator/generators/types.js: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line import/no-extraneous-dependencies 2 | const ts = require('typescript'); 3 | 4 | // biome-ignore lint/complexity/noStaticOnlyClass: legacy code 5 | module.exports = class TypesGenerator { 6 | static get any() { 7 | return ts.factory.createKeywordTypeNode(ts.SyntaxKind.AnyKeyword); 8 | } 9 | 10 | static get string() { 11 | return ts.factory.createKeywordTypeNode(ts.SyntaxKind.StringKeyword); 12 | } 13 | 14 | static get number() { 15 | return ts.factory.createKeywordTypeNode(ts.SyntaxKind.NumberKeyword); 16 | } 17 | 18 | static get boolean() { 19 | return ts.factory.createKeywordTypeNode(ts.SyntaxKind.BooleanKeyword); 20 | } 21 | 22 | static array(type) { 23 | return ts.factory.createArrayTypeNode(type); 24 | } 25 | 26 | static union(types) { 27 | return ts.factory.createUnionTypeNode(types); 28 | } 29 | 30 | static type(name, type) { 31 | return ts.factory.createTypeAliasDeclaration(undefined, name, undefined, type); 32 | } 33 | 34 | static genericType(name, types) { 35 | return ts.factory.createTypeReferenceNode(name, types); 36 | } 37 | 38 | static promiseType(type) { 39 | return TypesGenerator.genericType('Promise', [type]); 40 | } 41 | 42 | static parameter({ name, type, required = false }) { 43 | return ts.factory.createParameterDeclaration( 44 | undefined, 45 | undefined, 46 | name, 47 | !required ? ts.factory.createKeywordTypeNode(ts.SyntaxKind.QuestionToken) : undefined, 48 | type, 49 | ); 50 | } 51 | 52 | static declarationExport(exportClause) { 53 | return ts.factory.createExportDeclaration(undefined, false, exportClause); 54 | } 55 | }; 56 | -------------------------------------------------------------------------------- /packages/vk-io/test/contexts.test.ts: -------------------------------------------------------------------------------- 1 | import { strictEqual } from 'node:assert'; 2 | import { describe, it } from 'node:test'; 3 | 4 | import { Context, UpdateSource, VK } from '..'; 5 | 6 | const vk = new VK({ 7 | // biome-ignore lint/style/noNonNullAssertion: to be honest, they're just tests 8 | token: process.env.TOKEN!, 9 | }); 10 | 11 | const { api, upload } = vk; 12 | 13 | describe('Contexts', (): void => { 14 | describe('Context', (): void => { 15 | describe('#context.is()', (): void => { 16 | const getContext = (): Context => { 17 | const context = new Context({ 18 | api, 19 | upload, 20 | 21 | type: 'message', 22 | subTypes: ['edit_message', 'text'], 23 | payload: {}, 24 | updateType: 'test', 25 | source: UpdateSource.POLLING, 26 | }); 27 | 28 | return context; 29 | }; 30 | 31 | it('should return true when the required types are present', (): void => { 32 | const context = getContext(); 33 | 34 | strictEqual(context.is(['message']), true); 35 | strictEqual(context.is(['edit_message']), true); 36 | 37 | strictEqual(context.is(['new_message', 'text']), true); 38 | strictEqual(context.is(['message', 'edit_message']), true); 39 | }); 40 | 41 | it('should return false if the required types are not present', (): void => { 42 | const context = getContext(); 43 | 44 | strictEqual(context.is(['test']), false); 45 | strictEqual(context.is(['sub_test']), false); 46 | 47 | strictEqual(context.is(['test', 'sub_test']), false); 48 | }); 49 | }); 50 | }); 51 | }); 52 | -------------------------------------------------------------------------------- /packages/vk-io/src/structures/contexts/vote.ts: -------------------------------------------------------------------------------- 1 | import { Context, type ContextDefaultState, type ContextFactoryOptions } from './context'; 2 | 3 | import { kSerializeData } from '../../utils/constants'; 4 | 5 | import { pickProperties } from '../../utils/helpers'; 6 | 7 | export type VoteContextType = 'vote'; 8 | 9 | export type VoteContextSubType = 'poll_vote_new'; 10 | 11 | export interface IVoteContextPayload { 12 | poll_id: number; 13 | user_id: number; 14 | owner_id: number; 15 | option_id: number; 16 | } 17 | 18 | export type VoteContextOptions = ContextFactoryOptions; 19 | 20 | export class VoteContext extends Context< 21 | IVoteContextPayload, 22 | S, 23 | VoteContextType, 24 | VoteContextSubType 25 | > { 26 | public constructor(options: VoteContextOptions) { 27 | super({ 28 | ...options, 29 | 30 | type: 'vote', 31 | subTypes: [options.updateType as VoteContextSubType], 32 | }); 33 | } 34 | 35 | /** 36 | * Returns the identifier poll 37 | */ 38 | public get id(): number { 39 | return this.payload.poll_id; 40 | } 41 | 42 | /** 43 | * Returns the identifier user 44 | */ 45 | public get userId(): number { 46 | return this.payload.user_id; 47 | } 48 | 49 | /** 50 | * Returns the identifier owner 51 | */ 52 | public get ownerId(): number { 53 | return this.payload.owner_id; 54 | } 55 | 56 | /** 57 | * Returns the identifier option 58 | */ 59 | public get optionId(): number { 60 | return this.payload.option_id; 61 | } 62 | 63 | /** 64 | * Returns the custom data 65 | */ 66 | public [kSerializeData](): object { 67 | return pickProperties(this, ['id', 'userId', 'ownerId', 'optionId']); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /packages/vk-io/src/errors/api.ts: -------------------------------------------------------------------------------- 1 | import { APIErrorCode } from '../api/schemas/constants'; 2 | 3 | import { VKError } from './error'; 4 | 5 | export interface IAPIErrorParam { 6 | key: string; 7 | value: string; 8 | } 9 | 10 | export interface IAPIErrorOptions { 11 | error_code: number; 12 | error_msg: string; 13 | request_params: IAPIErrorParam[]; 14 | 15 | captcha_sid?: string; 16 | captcha_img?: string; 17 | redirect_uri?: string; 18 | confirmation_text?: string; 19 | } 20 | 21 | export class APIError extends VKError { 22 | /** 23 | * Request parameters 24 | */ 25 | public params: IAPIErrorParam[]; 26 | 27 | /** 28 | * Session identifier captcha 29 | */ 30 | public captchaSid?: string; 31 | 32 | /** 33 | * Image of captcha 34 | */ 35 | public captchaImg?: string; 36 | 37 | /** 38 | * Redirect URL, e.g. validation 39 | */ 40 | public redirectUri?: string; 41 | 42 | /** 43 | * Required confirmation text 44 | */ 45 | public confirmationText?: string; 46 | 47 | /** 48 | * Constructor 49 | */ 50 | public constructor(payload: IAPIErrorOptions) { 51 | const code = Number(payload.error_code); 52 | const message = `Code №${code} - ${payload.error_msg}`; 53 | 54 | super({ code, message }); 55 | 56 | this.params = payload.request_params; 57 | if (code === APIErrorCode.CAPTCHA) { 58 | this.captchaSid = payload.captcha_sid; 59 | this.captchaImg = payload.captcha_img; 60 | this.redirectUri = payload.redirect_uri; 61 | } else if (code === APIErrorCode.AUTH_VALIDATION) { 62 | this.redirectUri = payload.redirect_uri; 63 | } else if (code === APIErrorCode.NEED_CONFIRMATION) { 64 | this.confirmationText = payload.confirmation_text; 65 | } 66 | } 67 | } 68 | 69 | export { APIErrorCode }; 70 | -------------------------------------------------------------------------------- /packages/vk-io/src/structures/contexts/wall-post.ts: -------------------------------------------------------------------------------- 1 | import { Context, type ContextDefaultState, type ContextFactoryOptions } from './context'; 2 | 3 | import { type IWallAttachmentPayload, WallAttachment } from '../attachments'; 4 | 5 | import { kSerializeData } from '../../utils/constants'; 6 | 7 | import { pickProperties } from '../../utils/helpers'; 8 | 9 | export type WallPostContextType = 'wall_post'; 10 | 11 | export type WallPostContextSubType = 'wall_post_new' | 'wall_repost'; 12 | 13 | export interface IWallPostContextPayload extends IWallAttachmentPayload {} 14 | 15 | export type WallPostContextOptions = ContextFactoryOptions; 16 | 17 | export class WallPostContext extends Context< 18 | IWallPostContextPayload, 19 | S, 20 | WallPostContextType, 21 | WallPostContextSubType 22 | > { 23 | public wall: WallAttachment; 24 | 25 | public constructor(options: WallPostContextOptions) { 26 | super({ 27 | ...options, 28 | 29 | type: 'wall_post', 30 | subTypes: [options.updateType as WallPostContextSubType], 31 | }); 32 | 33 | this.wall = new WallAttachment({ 34 | api: this.api, 35 | payload: this.payload, 36 | }); 37 | } 38 | 39 | /** 40 | * Checks is repost 41 | */ 42 | public get isRepost(): boolean { 43 | return this.subTypes.includes('wall_repost'); 44 | } 45 | 46 | /** 47 | * Removes a record from the wall 48 | */ 49 | public deletePost(): Promise { 50 | const { wall } = this; 51 | 52 | return this.api.wall.delete({ 53 | post_id: wall.id, 54 | owner_id: wall.ownerId, 55 | }); 56 | } 57 | 58 | /** 59 | * Returns the custom data 60 | */ 61 | public [kSerializeData](): object { 62 | return pickProperties(this, ['wall', 'isRepost']); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /packages/vk-io/src/api/workers/parallel.ts: -------------------------------------------------------------------------------- 1 | import { APIRequest } from '../request'; 2 | import { SequentialWorker } from './sequential'; 3 | 4 | import { getChainReturn, resolveExecuteTask } from '../../utils/helpers'; 5 | 6 | export class ParallelWorker extends SequentialWorker { 7 | protected async execute(): Promise { 8 | const { queue } = this; 9 | 10 | if (this.skipMethod(queue[0].method)) { 11 | void super.execute(); 12 | 13 | return; 14 | } 15 | 16 | const { apiExecuteCount } = this.api.options; 17 | 18 | const tasks: APIRequest[] = []; 19 | 20 | for (let i = 0; i < this.queue.length; i += 1) { 21 | if (this.skipMethod(queue[i].method)) { 22 | continue; 23 | } 24 | 25 | tasks.push(queue.splice(i, 1)[0]); 26 | 27 | i -= 1; 28 | 29 | if (tasks.length >= apiExecuteCount) { 30 | break; 31 | } 32 | } 33 | 34 | if (tasks.length === 0) { 35 | return; 36 | } 37 | 38 | const request = new APIRequest({ 39 | api: this.api, 40 | method: 'execute', 41 | params: { 42 | code: getChainReturn(tasks.map(String)), 43 | }, 44 | }); 45 | 46 | void super.execute(request); 47 | 48 | try { 49 | resolveExecuteTask( 50 | tasks, 51 | (await request.promise) as { 52 | errors: object[]; 53 | response: (object | false)[]; 54 | }, 55 | ); 56 | } catch (error) { 57 | for (const task of tasks) { 58 | task.reject(error); 59 | } 60 | } 61 | } 62 | protected skipMethod(method: string): boolean { 63 | return method.startsWith('execute') || this.api.options.apiExecuteUnsupportedMethods.includes(method); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /packages/vk-io/src/upload/types.ts: -------------------------------------------------------------------------------- 1 | import type { Agent } from 'https'; 2 | 3 | import type { API } from '../api'; 4 | 5 | import type { AllowArray } from '../types'; 6 | 7 | /** 8 | * Stream, buffer, url or file path 9 | */ 10 | export type UploadSourceValue = NodeJS.ReadableStream | Buffer | string; 11 | 12 | export interface IUploadSourceMedia { 13 | value: UploadSourceValue; 14 | 15 | filename?: string; 16 | contentType?: string; 17 | contentLength?: number; 18 | } 19 | 20 | export interface IUploadSourceOptions { 21 | values: AllowArray; 22 | 23 | uploadUrl?: string; 24 | timeout?: number; 25 | } 26 | 27 | export type UploadNormalizedSourceOptions = IUploadSourceOptions & { 28 | values: IUploadSourceMedia[]; 29 | }; 30 | 31 | export type UploadAllowedSource = IUploadSourceOptions | IUploadSourceMedia; 32 | 33 | export interface IUploadParams { 34 | source: UploadAllowedSource; 35 | } 36 | 37 | export interface IUploadConduct { 38 | /** 39 | * Field name 40 | */ 41 | field: string; 42 | /** 43 | * Upload params 44 | */ 45 | params: IUploadParams & Record; 46 | 47 | /** 48 | * Get server functions 49 | */ 50 | getServer: (params: any) => Promise<{ upload_url: string }>; 51 | /** 52 | * Copies server params 53 | */ 54 | serverParams?: string[]; 55 | 56 | /** 57 | * Save files functions 58 | */ 59 | saveFiles: (params: any) => Promise; 60 | /** 61 | * Copies save params 62 | */ 63 | saveParams?: string[]; 64 | 65 | /** 66 | * Upload params 67 | */ 68 | uploadParams?: string[]; 69 | 70 | /** 71 | * Max uploaded files for one request 72 | */ 73 | maxFiles: number; 74 | /** 75 | * Attachment type 76 | */ 77 | attachmentType?: string; 78 | } 79 | 80 | export interface IUploadOptions { 81 | api: API; 82 | 83 | agent?: Agent; 84 | uploadTimeout?: number; 85 | } 86 | -------------------------------------------------------------------------------- /packages/vk-io/src/structures/attachments/sticker.ts: -------------------------------------------------------------------------------- 1 | import { ExternalAttachment, type ExternalAttachmentFactoryOptions } from './external'; 2 | 3 | import { AttachmentType, kSerializeData } from '../../utils/constants'; 4 | 5 | import { pickProperties } from '../../utils/helpers'; 6 | 7 | export interface IStickerImage { 8 | url: string; 9 | width: number; 10 | height: number; 11 | } 12 | 13 | export interface IStickerAttachmentPayload { 14 | sticker_id: number; 15 | product_id: number; 16 | images: IStickerImage[]; 17 | images_with_background: IStickerImage[]; 18 | } 19 | 20 | export type StickerAttachmentOptions = ExternalAttachmentFactoryOptions; 21 | 22 | export class StickerAttachment extends ExternalAttachment< 23 | IStickerAttachmentPayload, 24 | AttachmentType.STICKER | 'sticker' 25 | > { 26 | /** 27 | * Constructor 28 | */ 29 | public constructor(options: StickerAttachmentOptions) { 30 | super({ 31 | ...options, 32 | 33 | type: AttachmentType.STICKER, 34 | }); 35 | } 36 | 37 | /** 38 | * Returns the identifier sticker 39 | */ 40 | public get id(): number { 41 | return this.payload.sticker_id; 42 | } 43 | 44 | /** 45 | * Returns the identifier product 46 | */ 47 | public get productId(): number { 48 | return this.payload.product_id; 49 | } 50 | 51 | /** 52 | * Returns the images sizes 53 | */ 54 | public get images(): IStickerImage[] { 55 | return this.payload.images || []; 56 | } 57 | 58 | /** 59 | * Returns the images sizes with backgrounds 60 | */ 61 | public get imagesWithBackground(): IStickerImage[] { 62 | return this.payload.images_with_background || []; 63 | } 64 | 65 | /** 66 | * Returns the custom data 67 | */ 68 | public [kSerializeData](): object { 69 | return pickProperties(this, ['id', 'productId', 'images', 'imagesWithBackground']); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /packages/scenes/src/scene-manager.ts: -------------------------------------------------------------------------------- 1 | import { SceneContext } from './contexts'; 2 | 3 | import { CacheRepository } from './cache-repository'; 4 | import type { IScene } from './scenes/scene'; 5 | 6 | import type { ISceneManagerOptions, SceneRepository } from './scene-manager.types'; 7 | import type { IContext, Middleware } from './types'; 8 | 9 | export class SceneManager { 10 | private repository: SceneRepository = new CacheRepository(); 11 | 12 | private sessionKey: string; 13 | 14 | public constructor({ scenes, sessionKey = 'session' }: ISceneManagerOptions = {}) { 15 | this.sessionKey = sessionKey; 16 | 17 | if (scenes) { 18 | this.addScenes(scenes); 19 | } 20 | } 21 | 22 | /** 23 | * Checks for has a scene 24 | */ 25 | public hasScene(slug: string): boolean { 26 | return this.repository.has(slug); 27 | } 28 | 29 | /** 30 | * Adds scenes to the repository 31 | */ 32 | public addScenes(scenes: IScene[]): this { 33 | for (const scene of scenes) { 34 | this.repository.set(scene.slug, scene); 35 | } 36 | 37 | return this; 38 | } 39 | 40 | /** 41 | * Returns the middleware for embedding 42 | */ 43 | public get middleware(): Middleware { 44 | return (context: IContext, next: () => Promise): Promise => { 45 | context.scene = new SceneContext({ 46 | context, 47 | sessionKey: this.sessionKey, 48 | repository: this.repository, 49 | }); 50 | 51 | return next(); 52 | }; 53 | } 54 | 55 | /** 56 | * Returns the middleware for intercept 57 | */ 58 | public get middlewareIntercept(): Middleware { 59 | return (context: IContext, next: () => Promise): Promise => { 60 | if (!context.scene.current) { 61 | return next(); 62 | } 63 | 64 | return context.scene.reenter(); 65 | }; 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /packages/vk-io/src/structures/contexts/vk-pay-transaction.ts: -------------------------------------------------------------------------------- 1 | import { Context, type ContextDefaultState, type ContextFactoryOptions } from './context'; 2 | 3 | import { kSerializeData } from '../../utils/constants'; 4 | 5 | import { pickProperties } from '../../utils/helpers'; 6 | 7 | export type VKPayTransactionContextType = 'vk_pay_transaction'; 8 | 9 | export type VKPayTransactionContextSubType = 'vkpay_transaction'; 10 | 11 | export interface IVKPayTransactionPayload { 12 | from_id: number; 13 | amount: number; 14 | description: string; 15 | date: number; 16 | } 17 | 18 | export type VKPayTransactionContextOptions = ContextFactoryOptions; 19 | 20 | export class VKPayTransactionContext extends Context< 21 | IVKPayTransactionPayload, 22 | S, 23 | VKPayTransactionContextType, 24 | VKPayTransactionContextSubType 25 | > { 26 | public constructor(options: VKPayTransactionContextOptions) { 27 | super({ 28 | ...options, 29 | 30 | type: 'vk_pay_transaction', 31 | subTypes: [options.updateType as VKPayTransactionContextSubType], 32 | }); 33 | } 34 | 35 | /** 36 | * Returns the identifier transfer sender 37 | */ 38 | public get fromId(): number { 39 | return this.payload.from_id; 40 | } 41 | 42 | /** 43 | * Returns the transfer amount in thousandths of a ruble 44 | */ 45 | public get amount(): number { 46 | return this.payload.amount; 47 | } 48 | 49 | /** 50 | * Returns the description on the translation 51 | */ 52 | public get description(): string { 53 | return this.payload.description; 54 | } 55 | 56 | /** 57 | * Returns the unixtime transfer time 58 | */ 59 | public get createdAt(): number { 60 | return this.payload.date; 61 | } 62 | 63 | /** 64 | * Returns the custom data 65 | */ 66 | public [kSerializeData](): object { 67 | return pickProperties(this, ['fromId', 'amount', 'description', 'createdAt']); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /packages/session/README.md: -------------------------------------------------------------------------------- 1 | # VK-IO Session 2 | NPM version 3 | Build Status 4 | NPM downloads 5 | 6 | > VK-IO Session - Simple implementation of the sessions ⚙️ 7 | 8 | ## 📦 Installation 9 | 10 | > **[Node.js](https://nodejs.org/) 12.20.0 or newer is required** 11 | 12 | - **Using `npm`** (recommended) 13 | ```shell 14 | npm i @vk-io/session 15 | ``` 16 | - **Using `Yarn`** 17 | ```shell 18 | yarn add @vk-io/session 19 | ``` 20 | - **Using `pnpm`** 21 | ```shell 22 | pnpm add @vk-io/session 23 | ``` 24 | 25 | ## Example usage 26 | ```javascript 27 | import { VK } from 'vk-io'; 28 | 29 | import { SessionManager } from '@vk-io/session'; 30 | 31 | const vk = new VK({ 32 | token: process.env.TOKEN 33 | }); 34 | 35 | const sessionManager = new SessionManager(); 36 | 37 | vk.updates.on('message_new', sessionManager.middleware); 38 | 39 | vk.updates.on('message_new', async (context, next) => { 40 | if (context.text !== '/counter') { 41 | return next(); 42 | } 43 | 44 | const { session } = context; 45 | 46 | if (!session.counter) { 47 | session.counter = 0; 48 | } 49 | 50 | session.counter += 1; 51 | 52 | await context.send(`You turned to the bot (${session.counter}) times`); 53 | }); 54 | 55 | vk.updates.start().catch(console.error); 56 | ``` 57 | 58 | ## Community 59 | ### Useful modules that may be useful to you 60 | 61 | * [vk-io-redis-storage](https://github.com/xtcry/vk-io-redis-storage): Simple storage add-on for [@vk-io/session](packages/session) 62 | 63 | > If you want to add your module in the list, create a [new issue](https://github.com/negezor/vk-io/issues/new) in the repository. 64 | -------------------------------------------------------------------------------- /docs/examples/advanced/middleware-error-fallback.js: -------------------------------------------------------------------------------- 1 | const { VK, APIError } = require('vk-io'); 2 | const { HearManager } = require('@vk-io/hear'); 3 | 4 | const vk = new VK({ 5 | token: process.env.TOKEN, 6 | }); 7 | 8 | const hearManager = new HearManager(); 9 | 10 | const logger = console; 11 | 12 | // Custom catch all the errors 13 | vk.updates.use(async (_context, next) => { 14 | try { 15 | await next(); 16 | } catch (error) { 17 | logger.error('An error has occurred', error); 18 | } 19 | }); 20 | 21 | class MyNetworkError extends Error {} 22 | 23 | // Custom handle the errors 24 | vk.updates.use(async (context, next) => { 25 | try { 26 | await next(); 27 | } catch (error) { 28 | // We do not respond to messages 29 | if (!context.is('message')) { 30 | throw error; 31 | } 32 | 33 | // If there is no access in the chat (https://dev.vk.ru/method/messages.getConversationsById) 34 | if (error instanceof APIError && error.code === 917) { 35 | await context.send('I do not have access to the chat, please give it to me.'); 36 | 37 | return; 38 | } 39 | 40 | if (error instanceof MyNetworkError) { 41 | await context.send('There was a problem with the connection.'); 42 | 43 | return; 44 | } 45 | 46 | // Will be caught in the previous middleware 47 | throw error; 48 | } 49 | }); 50 | 51 | vk.updates.on('message_new', hearManager.middleware); 52 | 53 | hearManager.hear(/get chat/i, async context => { 54 | if (!context.isChat) { 55 | return context.send('We are not in chat!'); 56 | } 57 | 58 | // Throw an error if there is no access 59 | const { items } = await vk.api.messages.getConversationsById({ 60 | peer_ids: context.peerId, 61 | }); 62 | 63 | const [conversation] = items; 64 | 65 | return context.send(`Chat: ${JSON.stringify(conversation, null, ' ')}`); 66 | }); 67 | 68 | hearManager.hear(/throw network error/i, async () => { 69 | throw new MyNetworkError(); 70 | }); 71 | 72 | vk.updates.start().catch(console.error); 73 | -------------------------------------------------------------------------------- /packages/scenes/src/cache-repository.ts: -------------------------------------------------------------------------------- 1 | type CacheRepositorySortingValues = (a: Value, b: Value) => number; 2 | 3 | export class CacheRepository { 4 | private readonly collection = new Map(); 5 | 6 | public keys: Key[] = []; 7 | 8 | public values: Value[] = []; 9 | 10 | protected sortingValues?: CacheRepositorySortingValues; 11 | 12 | public constructor({ 13 | sortingValues, 14 | }: { 15 | sortingValues?: CacheRepositorySortingValues; 16 | } = {}) { 17 | this.sortingValues = sortingValues; 18 | } 19 | 20 | /** 21 | * Checks has value by key 22 | */ 23 | public has(key: Key): boolean { 24 | return this.collection.has(key); 25 | } 26 | 27 | /** 28 | * Sets value by key 29 | */ 30 | public set(key: Key, value: Value): void { 31 | this.collection.set(key, value); 32 | 33 | this.keys = [...this.collection.keys()]; 34 | this.values = [...this.collection.values()]; 35 | 36 | if (this.sortingValues) { 37 | this.values.sort(this.sortingValues); 38 | } 39 | } 40 | 41 | /** 42 | * Returns value by key 43 | */ 44 | public get(key: Key): Value | undefined { 45 | return this.collection.get(key); 46 | } 47 | 48 | /** 49 | * Sets value by key else error if exits 50 | */ 51 | public strictSet(key: Key, value: Value): void { 52 | if (this.collection.has(key)) { 53 | throw new Error(`Value by ${key} already exists`); 54 | } 55 | 56 | this.set(key, value); 57 | } 58 | 59 | /** 60 | * Returns value by key else error 61 | */ 62 | public strictGet(key: Key): Value { 63 | const value = this.get(key); 64 | 65 | if (!value) { 66 | throw new Error(`Value by ${key} not found`); 67 | } 68 | 69 | return value; 70 | } 71 | 72 | /** 73 | * Returns iterator 74 | */ 75 | public [Symbol.iterator](): IterableIterator<[Key, Value]> { 76 | return this.collection[Symbol.iterator](); 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /packages/vk-io/src/structures/contexts/message-subscription.ts: -------------------------------------------------------------------------------- 1 | import { Context, type ContextDefaultState, type ContextFactoryOptions } from './context'; 2 | 3 | import { kSerializeData } from '../../utils/constants'; 4 | 5 | import { pickProperties } from '../../utils/helpers'; 6 | 7 | export type MessageSubscriptionContextType = 'message_subscription'; 8 | 9 | export type MessageSubscriptionContextSubType = 'message_allow' | 'message_deny'; 10 | 11 | export interface IMessageSubscriptionContextPayload { 12 | user_id: number; 13 | key: string; 14 | } 15 | 16 | export type MessageSubscriptionContextOptions = ContextFactoryOptions; 17 | 18 | export class MessageSubscriptionContext extends Context< 19 | IMessageSubscriptionContextPayload, 20 | S, 21 | MessageSubscriptionContextType, 22 | MessageSubscriptionContextSubType 23 | > { 24 | public constructor(options: MessageSubscriptionContextOptions) { 25 | super({ 26 | ...options, 27 | 28 | type: 'message_subscription', 29 | subTypes: [options.updateType as MessageSubscriptionContextSubType], 30 | }); 31 | } 32 | 33 | /** 34 | * Checks that the user has subscribed to messages 35 | */ 36 | public get isSubscribed(): boolean { 37 | return this.subTypes.includes('message_allow'); 38 | } 39 | 40 | /** 41 | * Checks that the user has unsubscribed from the messages 42 | */ 43 | public get isUnsubscribed(): boolean { 44 | return this.subTypes.includes('message_deny'); 45 | } 46 | 47 | /** 48 | * Returns the identifier user 49 | */ 50 | public get userId(): number { 51 | return this.payload.user_id; 52 | } 53 | 54 | /** 55 | * Returns the key 56 | */ 57 | public get key(): string | undefined { 58 | return this.payload.key; 59 | } 60 | 61 | /** 62 | * Returns the custom data 63 | */ 64 | public [kSerializeData](): object { 65 | return pickProperties(this, ['userId', 'key', 'isSubscribed', 'isUnsubscribed']); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /packages/vk-io/src/structures/attachments/external.ts: -------------------------------------------------------------------------------- 1 | import { inspectable } from 'inspectable'; 2 | 3 | import type { API } from '../../api'; 4 | import type { AttachmentFactoryOptions, IAttachmentOptions } from './attachment'; 5 | 6 | import { type AttachmentType, kSerializeData } from '../../utils/constants'; 7 | 8 | export type IExternalAttachmentOptions = IAttachmentOptions; 9 | 10 | export type ExternalAttachmentFactoryOptions

= AttachmentFactoryOptions

; 11 | 12 | export class ExternalAttachment

{ 13 | public type: Type; 14 | 15 | protected $filled: boolean; 16 | 17 | protected api!: API; 18 | 19 | protected payload: P; 20 | 21 | /** 22 | * Constructor 23 | */ 24 | public constructor(options: IExternalAttachmentOptions) { 25 | this.api = options.api; 26 | 27 | this.type = options.type; 28 | 29 | this.payload = options.payload; 30 | 31 | this.$filled = false; 32 | } 33 | 34 | /** 35 | * Returns custom tag 36 | */ 37 | public get [Symbol.toStringTag](): string { 38 | return this.constructor.name; 39 | } 40 | 41 | /** 42 | * Returns whether the attachment is filled 43 | */ 44 | public get isFilled(): boolean { 45 | return this.$filled; 46 | } 47 | 48 | /** 49 | * Can be attached via string representation 50 | */ 51 | public get canBeAttached(): boolean { 52 | return false; 53 | } 54 | 55 | /** 56 | * Returns data for JSON 57 | */ 58 | public toJSON(): object { 59 | return this[kSerializeData](); 60 | } 61 | 62 | /** 63 | * Returns the custom data 64 | */ 65 | public [kSerializeData](): object { 66 | return { 67 | payload: this.payload, 68 | }; 69 | } 70 | } 71 | 72 | inspectable(ExternalAttachment, { 73 | serialize: instance => instance.toJSON(), 74 | stringify: (instance, payload, context): string => 75 | `${context.stylize(instance.constructor.name, 'special')} ${context.inspect(payload)}`, 76 | }); 77 | -------------------------------------------------------------------------------- /packages/vk-io/src/structures/attachments/graffiti.ts: -------------------------------------------------------------------------------- 1 | import { Attachment, type AttachmentFactoryOptions } from './attachment'; 2 | 3 | import { AttachmentType, kSerializeData } from '../../utils/constants'; 4 | 5 | import { pickProperties } from '../../utils/helpers'; 6 | 7 | export interface IGraffitiAttachmentPayload { 8 | id: number; 9 | owner_id: number; 10 | access_key?: string; 11 | 12 | height?: number; 13 | width?: number; 14 | url?: string; 15 | } 16 | 17 | export type GraffitiAttachmentOptions = AttachmentFactoryOptions; 18 | 19 | export class GraffitiAttachment extends Attachment { 20 | /** 21 | * Constructor 22 | */ 23 | public constructor(options: GraffitiAttachmentOptions) { 24 | super({ 25 | ...options, 26 | 27 | type: AttachmentType.GRAFFITI, 28 | }); 29 | 30 | this.$filled = this.payload.url !== undefined; 31 | } 32 | 33 | /** 34 | * Load attachment payload 35 | */ 36 | public async loadAttachmentPayload(): Promise { 37 | if (this.$filled) { 38 | return; 39 | } 40 | 41 | const [document] = await this.api.docs.getById({ 42 | docs: `${this.ownerId}_${this.id}`, 43 | }); 44 | 45 | this.payload = document as unknown as IGraffitiAttachmentPayload; 46 | 47 | this.$filled = true; 48 | } 49 | 50 | /** 51 | * Returns the graffiti height 52 | */ 53 | public get height(): number | undefined { 54 | return this.payload.height; 55 | } 56 | 57 | /** 58 | * Returns the graffiti width 59 | */ 60 | public get width(): number | undefined { 61 | return this.payload.width; 62 | } 63 | 64 | /** 65 | * Returns the URL of the document 66 | */ 67 | public get url(): string | undefined { 68 | return this.payload.url; 69 | } 70 | 71 | /** 72 | * Returns the custom data 73 | */ 74 | public [kSerializeData](): object { 75 | return pickProperties(this, ['height', 'width', 'url']); 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /packages/vk-io/src/structures/contexts/vk-app-payload.ts: -------------------------------------------------------------------------------- 1 | import { Context, type ContextDefaultState, type ContextFactoryOptions } from './context'; 2 | 3 | import { kSerializeData } from '../../utils/constants'; 4 | 5 | import { pickProperties } from '../../utils/helpers'; 6 | 7 | export type VKAppPayloadContextType = 'vk_app_event'; 8 | 9 | export type VKAppPayloadContextSubType = 'app_payload'; 10 | 11 | export interface IVKAppPayloadPayload { 12 | user_id: number; 13 | app_id: number; 14 | payload: string; 15 | group_id: number; 16 | } 17 | 18 | export type VKAppPayloadContextOptions = ContextFactoryOptions; 19 | 20 | export class VKAppPayloadContext = object> extends Context< 21 | IVKAppPayloadPayload, 22 | S, 23 | VKAppPayloadContextType, 24 | VKAppPayloadContextSubType 25 | > { 26 | public constructor(options: VKAppPayloadContextOptions) { 27 | super({ 28 | ...options, 29 | 30 | type: 'vk_app_event', 31 | subTypes: [options.updateType as VKAppPayloadContextSubType], 32 | }); 33 | } 34 | 35 | /** 36 | * Returns the identifier of the user whose action the event was sent to in the application 37 | */ 38 | public get userId(): number { 39 | return this.payload.user_id; 40 | } 41 | 42 | /** 43 | * Returns the identifier of the application from which the event was sent 44 | */ 45 | public get appId(): number { 46 | return this.payload.app_id; 47 | } 48 | 49 | /** 50 | * Returns the identifier of the community to which the notification was sent 51 | */ 52 | public get groupId(): number { 53 | return this.payload.group_id; 54 | } 55 | 56 | /** 57 | * Returns the transferred useful data 58 | */ 59 | public get eventPayload(): P { 60 | return JSON.parse(this.payload.payload); 61 | } 62 | 63 | /** 64 | * Returns the custom data 65 | */ 66 | public [kSerializeData](): object { 67 | return pickProperties(this, ['userId', 'appId', 'groupId', 'eventPayload']); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /packages/vk-io/src/structures/shared/message-forward-collection.ts: -------------------------------------------------------------------------------- 1 | import type { ContextDefaultState } from '../contexts/context'; 2 | 3 | import type { Attachment, ExternalAttachment } from '../attachments'; 4 | /* eslint-disable max-classes-per-file */ 5 | import type { MessageContext } from '../contexts/message'; 6 | import type { Attachmentable } from './attachmentable'; 7 | 8 | import type { AttachmentTypeString } from '../../utils/constants'; 9 | 10 | import { applyMixins } from '../../utils/helpers'; 11 | 12 | const getForwards = (rootForwards: MessageContext[]): MessageContext[] => { 13 | const forwards: MessageContext[] = []; 14 | 15 | for (const forward of rootForwards) { 16 | forwards.push(forward, ...getForwards(forward.forwards)); 17 | } 18 | 19 | return forwards; 20 | }; 21 | 22 | const kFlatten = Symbol('flatten'); 23 | 24 | class MessageForwardsCollection extends Array> { 25 | protected [kFlatten]?: MessageContext[]; 26 | 27 | /** 28 | * Returns a flat copy of forwards 29 | */ 30 | public get flatten(): MessageContext[] { 31 | if (!this[kFlatten]) { 32 | this[kFlatten] = getForwards(this); 33 | } 34 | 35 | return this[kFlatten]; 36 | } 37 | } 38 | 39 | interface MessageForwardsCollection extends Attachmentable {} 40 | applyMixins(MessageForwardsCollection, [ 41 | class CustomAttachmentable { 42 | public flatten!: MessageContext[]; 43 | 44 | public hasAttachments(type?: AttachmentTypeString): boolean { 45 | return this.flatten.some(forward => forward.hasAttachments(type)); 46 | } 47 | 48 | public getAttachments(type?: AttachmentTypeString): (Attachment | ExternalAttachment)[] { 49 | const attachments = this.flatten.map(forward => 50 | // @ts-expect-error too annoying for overload types 51 | forward.getAttachments(type), 52 | ); 53 | 54 | return ([] as (Attachment | ExternalAttachment)[]).concat(...attachments); 55 | } 56 | }, 57 | ]); 58 | 59 | export { MessageForwardsCollection }; 60 | -------------------------------------------------------------------------------- /packages/vk-io/src/structures/contexts/donut-withdraw.ts: -------------------------------------------------------------------------------- 1 | import { Context, type ContextDefaultState, type ContextFactoryOptions } from './context'; 2 | 3 | import { kSerializeData } from '../../utils/constants'; 4 | 5 | import { pickProperties } from '../../utils/helpers'; 6 | 7 | export type DonutWithdrawContextType = 'donut_withdraw'; 8 | 9 | export type DonutWithdrawContextSubType = 'donut_money_withdraw' | 'donut_money_withdraw_error'; 10 | 11 | export interface IDonutWithdrawContextPayload { 12 | reason?: string; 13 | 14 | amount?: number; 15 | amount_without_fee?: number; 16 | } 17 | 18 | export type DonutWithdrawContextOptions = ContextFactoryOptions; 19 | 20 | export class DonutWithdrawContext extends Context< 21 | IDonutWithdrawContextPayload, 22 | S, 23 | DonutWithdrawContextType, 24 | DonutWithdrawContextSubType 25 | > { 26 | public constructor(options: DonutWithdrawContextOptions) { 27 | super({ 28 | ...options, 29 | 30 | type: 'donut_withdraw', 31 | subTypes: [options.updateType as DonutWithdrawContextSubType], 32 | }); 33 | } 34 | 35 | /** 36 | * Checks if error for withdraw 37 | */ 38 | public get isError(): boolean { 39 | return this.is(['donut_money_withdraw_error']); 40 | } 41 | 42 | /** 43 | * Returns the amount 44 | */ 45 | public get amount(): number { 46 | // biome-ignore lint/style/noNonNullAssertion: always present 47 | return this.payload.amount!; 48 | } 49 | 50 | /** 51 | * Returns the amount without fee 52 | */ 53 | public get amountWithoutFee(): number { 54 | // biome-ignore lint/style/noNonNullAssertion: always present 55 | return this.payload.amount_without_fee!; 56 | } 57 | 58 | /** 59 | * Returns the reason for the error 60 | */ 61 | public get reason(): string | undefined { 62 | return this.payload.reason; 63 | } 64 | 65 | /** 66 | * Returns the custom data 67 | */ 68 | public [kSerializeData](): object { 69 | return pickProperties(this, ['isError', 'amount', 'amountWithoutFee', 'reason']); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /packages/vk-io/src/structures/contexts/dialog-messages.ts: -------------------------------------------------------------------------------- 1 | import { Context, type ContextDefaultState, type ContextFactoryOptions } from './context'; 2 | 3 | import { kSerializeData } from '../../utils/constants'; 4 | 5 | import { pickProperties } from '../../utils/helpers'; 6 | 7 | export type DialogMessagesContextType = 'dialog_messages'; 8 | 9 | export type DialogMessagesContextSubType = 'dialog_messages_delete'; 10 | 11 | const subTypes: Record = { 12 | 10013: 'dialog_messages_delete', 13 | }; 14 | export interface IDialogMessagesContextPayload { 15 | local_id: number; 16 | peer_id: number; 17 | } 18 | 19 | export type DialogMessagesContextOptions = ContextFactoryOptions; 20 | 21 | export class DialogMessagesContext extends Context< 22 | IDialogMessagesContextPayload, 23 | S, 24 | DialogMessagesContextType, 25 | DialogMessagesContextSubType 26 | > { 27 | public constructor(options: DialogMessagesContextOptions) { 28 | const [eventId, peerId, localId] = options.payload; 29 | 30 | super({ 31 | ...options, 32 | 33 | type: 'dialog_messages', 34 | subTypes: [subTypes[eventId]], 35 | 36 | payload: { 37 | peer_id: peerId, 38 | local_id: localId, 39 | }, 40 | }); 41 | } 42 | 43 | /** 44 | * Checks if messages are delete 45 | */ 46 | public get isDelete(): boolean { 47 | return this.subTypes.includes('dialog_messages_delete'); 48 | } 49 | 50 | /** 51 | * Checks if messages are restored 52 | * @deprecated 53 | */ 54 | public readonly isRestore = false; 55 | 56 | /** 57 | * Returns the peer identifier 58 | */ 59 | public get peerId(): number { 60 | return this.payload.peer_id; 61 | } 62 | 63 | /** 64 | * Returns the identifier of the local message 65 | */ 66 | public get localId(): number { 67 | return this.payload.local_id; 68 | } 69 | 70 | /** 71 | * Returns the custom data 72 | */ 73 | public [kSerializeData](): object { 74 | return pickProperties(this, ['peerId', 'localId', 'isDelete', 'isRestore']); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /packages/authorization/README.md: -------------------------------------------------------------------------------- 1 | # VK-IO Authorization 2 | 3 | NPM version 4 | Build Status 5 | NPM downloads 6 | 7 | > VK-IO Authorization API - Separated module for authorization by login & password, and etc... ⚙️ 8 | 9 | ## 📦 Installation 10 | 11 | > **[Node.js](https://nodejs.org/) 12.20.0 or newer is required** 12 | 13 | - **Using `npm`** (recommended) 14 | ```shell 15 | npm i @vk-io/authorization 16 | ``` 17 | - **Using `Yarn`** 18 | ```shell 19 | yarn add @vk-io/authorization 20 | ``` 21 | - **Using `pnpm`** 22 | ```shell 23 | pnpm add @vk-io/authorization 24 | ``` 25 | 26 | ## Example usage 27 | 28 | ```javascript 29 | import { CallbackService } from 'vk-io'; 30 | import { DirectAuthorization, officialAppCredentials } from '@vk-io/authorization'; 31 | 32 | const callbackService = new CallbackService(); 33 | 34 | const direct = new DirectAuthorization({ 35 | callbackService, 36 | 37 | scope: 'all', 38 | 39 | // Direct authorization is only available for official applications 40 | ...officialAppCredentials.android, // { clientId: string; clientSecret: string; } 41 | 42 | // Or manually provide app credentials 43 | // clientId: process.env.CLIENT_ID, 44 | // clientSecret: process.env.CLIENT_SECRET, 45 | 46 | login: process.env.LOGIN, 47 | password: process.env.PASSWORD, 48 | 49 | apiVersion: '5.199' 50 | }); 51 | 52 | async function run() { 53 | const response = await direct.run(); 54 | 55 | console.log('Token:', response.token); 56 | console.log('Expires:', response.expires); 57 | 58 | console.log('Email:', response.email); 59 | console.log('User ID:', response.userId); 60 | } 61 | 62 | run().catch(console.error); 63 | ``` 64 | 65 | ## Additional info 66 | 67 | The module also supports `ImplicitFlowUser`, `ImplicitFlowGroup` and `AccountVerification` 68 | -------------------------------------------------------------------------------- /packages/vk-io/src/structures/contexts/group-member.ts: -------------------------------------------------------------------------------- 1 | import { Context, type ContextDefaultState, type ContextFactoryOptions } from './context'; 2 | 3 | import { kSerializeData } from '../../utils/constants'; 4 | 5 | import { pickProperties } from '../../utils/helpers'; 6 | 7 | export type GroupMemberContextType = 'group_member'; 8 | 9 | export type GroupMemberContextSubType = 'group_leave' | 'group_join'; 10 | 11 | export interface IGroupMemberContextPayload { 12 | user_id: number; 13 | self?: number; 14 | join_type?: string; 15 | } 16 | 17 | export type GroupMemberContextOptions = ContextFactoryOptions; 18 | 19 | export class GroupMemberContext extends Context< 20 | IGroupMemberContextPayload, 21 | S, 22 | GroupMemberContextType, 23 | GroupMemberContextSubType 24 | > { 25 | public constructor(options: GroupMemberContextOptions) { 26 | super({ 27 | ...options, 28 | 29 | type: 'group_member', 30 | subTypes: [options.updateType as GroupMemberContextSubType], 31 | }); 32 | } 33 | 34 | /** 35 | * Checks is join user 36 | */ 37 | public get isJoin(): boolean { 38 | return this.subTypes.includes('group_join'); 39 | } 40 | 41 | /** 42 | * Checks is leave user 43 | */ 44 | public get isLeave(): boolean { 45 | return this.subTypes.includes('group_leave'); 46 | } 47 | 48 | /** 49 | * Checks is self leave user 50 | */ 51 | public get isSelfLeave(): boolean | undefined { 52 | if (this.isJoin) { 53 | return undefined; 54 | } 55 | 56 | return Boolean(this.payload.self); 57 | } 58 | 59 | /** 60 | * Returns the identifier user 61 | */ 62 | public get userId(): number { 63 | return this.payload.user_id; 64 | } 65 | 66 | /** 67 | * Returns the join type 68 | */ 69 | public get joinType(): string | undefined { 70 | if (this.isLeave) { 71 | return undefined; 72 | } 73 | 74 | return this.payload.join_type; 75 | } 76 | 77 | /** 78 | * Returns the custom data 79 | */ 80 | public [kSerializeData](): object { 81 | return pickProperties(this, ['userId', 'joinType', 'isJoin', 'isLeave', 'isSelfLeave']); 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /packages/vk-io/src/structures/contexts/messages-read.ts: -------------------------------------------------------------------------------- 1 | import { Context, type ContextDefaultState, type ContextFactoryOptions } from './context'; 2 | 3 | import { kSerializeData } from '../../utils/constants'; 4 | 5 | import { pickProperties } from '../../utils/helpers'; 6 | 7 | export type MessagesReadContextType = 'messages_read'; 8 | 9 | export type MessagesReadContextSubType = 'messages_read_inbox' | 'messages_read_outbox'; 10 | 11 | const subTypes: Record = { 12 | 10006: 'messages_read_inbox', 13 | 10007: 'messages_read_outbox', 14 | }; 15 | 16 | export interface IMessagesReadContextPayload { 17 | peer_id: number; 18 | local_id: number; 19 | } 20 | 21 | export type MessagesReadContextContextOptions = ContextFactoryOptions; 22 | 23 | export class MessagesReadContext extends Context< 24 | IMessagesReadContextPayload, 25 | S, 26 | MessagesReadContextType, 27 | MessagesReadContextSubType 28 | > { 29 | public constructor(options: MessagesReadContextContextOptions) { 30 | const [eventId, peerId, localId] = options.payload; 31 | 32 | super({ 33 | ...options, 34 | 35 | type: 'messages_read', 36 | subTypes: [subTypes[eventId]], 37 | 38 | payload: { 39 | peer_id: peerId, 40 | local_id: localId, 41 | }, 42 | }); 43 | } 44 | 45 | /** 46 | * Checks that inbox messages are read 47 | */ 48 | public get isInbox(): boolean { 49 | return this.subTypes.includes('messages_read_inbox'); 50 | } 51 | 52 | /** 53 | * Checks that outbox messages are read 54 | */ 55 | public get isOutbox(): boolean { 56 | return this.subTypes.includes('messages_read_outbox'); 57 | } 58 | 59 | /** 60 | * Returns the peer ID 61 | */ 62 | public get peerId(): number { 63 | return this.payload.peer_id; 64 | } 65 | 66 | /** 67 | * Returns the identifier of the local message 68 | */ 69 | public get localId(): number { 70 | return this.payload.local_id; 71 | } 72 | 73 | /** 74 | * Returns the custom data 75 | */ 76 | public [kSerializeData](): object { 77 | return pickProperties(this, ['id', 'peerId', 'isInbox', 'isOutbox']); 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /packages/vk-io/src/collect/chain.ts: -------------------------------------------------------------------------------- 1 | import { inspectable } from 'inspectable'; 2 | 3 | import { APIRequest } from '../api/request'; 4 | 5 | import { VKError } from '../errors'; 6 | 7 | import type { API } from '../api'; 8 | import { executeRequests, type IExecutesPayload } from './executes'; 9 | 10 | export interface IChainOptions { 11 | api: API; 12 | } 13 | 14 | export class Chain { 15 | public started = false; 16 | 17 | protected api: API; 18 | 19 | protected queue: APIRequest[] = []; 20 | 21 | /** 22 | * Constructor 23 | */ 24 | public constructor({ api }: IChainOptions) { 25 | this.api = api; 26 | } 27 | 28 | /** 29 | * Returns custom tag 30 | */ 31 | public get [Symbol.toStringTag](): string { 32 | return this.constructor.name; 33 | } 34 | 35 | /** 36 | * Adds method to queue 37 | */ 38 | public append(method: string, params: object): Promise { 39 | if (this.started) { 40 | return Promise.reject( 41 | new VKError({ 42 | message: 'Chain already started', 43 | code: 'ALREADY_STARTED', 44 | }), 45 | ); 46 | } 47 | 48 | const request = new APIRequest({ 49 | api: this.api, 50 | method, 51 | params, 52 | }); 53 | 54 | this.queue.push(request); 55 | 56 | return request.promise as Promise; 57 | } 58 | 59 | /** 60 | * Promise based 61 | */ 62 | // biome-ignore lint/suspicious/noThenProperty: alias for .run().then() or await chain 63 | public then(thenFn: (value: IExecutesPayload) => unknown, catchFn: (reason: unknown) => unknown): Promise { 64 | return this.run().then(thenFn, catchFn); 65 | } 66 | 67 | /** 68 | * Starts the chain 69 | */ 70 | public async run(): Promise { 71 | if (this.started) { 72 | throw new VKError({ 73 | message: 'Chain already started', 74 | code: 'ALREADY_STARTED', 75 | }); 76 | } 77 | 78 | this.started = true; 79 | 80 | return executeRequests(this.api, this.queue); 81 | } 82 | } 83 | 84 | inspectable(Chain, { 85 | // @ts-expect-error queue is private 86 | serialize: ({ started, queue }) => ({ 87 | started, 88 | queue, 89 | }), 90 | }); 91 | -------------------------------------------------------------------------------- /packages/authorization/src/helpers.ts: -------------------------------------------------------------------------------- 1 | import type { load as cheerioLoad } from 'cheerio'; 2 | 3 | import type { Response } from './fetch-cookie'; 4 | 5 | import { groupScopes, userScopes } from './constants'; 6 | 7 | export type CheerioStatic = ReturnType; 8 | 9 | /** 10 | * Returns the bit mask of the user permission by name 11 | */ 12 | export const getUserPermissionsByName = (rawScope: string | string[]): number => { 13 | const scope = !Array.isArray(rawScope) ? rawScope.split(/,\s*/) : rawScope; 14 | 15 | let bitMask = 0; 16 | 17 | for (const name of scope) { 18 | const scopeBit = userScopes.get(name); 19 | 20 | if (scopeBit) { 21 | bitMask += scopeBit; 22 | } 23 | } 24 | 25 | return bitMask; 26 | }; 27 | 28 | /** 29 | * Returns the bit mask of the group permission by name 30 | */ 31 | export const getGroupPermissionsByName = (rawScope: string | string[]): number => { 32 | const scope = !Array.isArray(rawScope) ? rawScope.split(/,\s*/) : rawScope; 33 | 34 | let bitMask = 0; 35 | 36 | for (const name of scope) { 37 | const scopeBit = groupScopes.get(name); 38 | 39 | if (scopeBit) { 40 | bitMask += scopeBit; 41 | } 42 | } 43 | 44 | return bitMask; 45 | }; 46 | 47 | export const getAllUserPermissions = (): number => getUserPermissionsByName([...userScopes.keys()]); 48 | 49 | export const getAllGroupPermissions = (): number => getGroupPermissionsByName([...groupScopes.keys()]); 50 | 51 | /** 52 | * Parse form 53 | */ 54 | export const parseFormField = ( 55 | $: unknown, 56 | ): { 57 | action: string; 58 | fields: Record; 59 | } => { 60 | const $form = ($ as CheerioStatic)('form[action][method]'); 61 | 62 | const fields: Record = {}; 63 | 64 | for (const { name, value } of $form.serializeArray()) { 65 | fields[name] = value; 66 | } 67 | 68 | const action = $form.attr('action'); 69 | 70 | if (!action) { 71 | throw new Error('Form action is not found'); 72 | } 73 | 74 | return { 75 | action, 76 | fields, 77 | }; 78 | }; 79 | 80 | /** 81 | * Returns full URL use Response 82 | */ 83 | export const getFullURL = (action: string, { url }: Response): URL => { 84 | if (action.startsWith('https://')) { 85 | return new URL(action); 86 | } 87 | 88 | const { protocol, host } = new URL(url); 89 | 90 | return new URL(action, `${protocol}//${host}`); 91 | }; 92 | -------------------------------------------------------------------------------- /packages/stateless-prompt/README.md: -------------------------------------------------------------------------------- 1 | # VK-IO Stateless Prompt 2 | 3 | NPM version 4 | Build Status 5 | NPM downloads 6 | 7 | > VK-IO Stateless Prompt - Simple implementation of middleware-based stateless prompt 8 | 9 | ## How it works 10 | 11 | The basic concept is to send your message with a special text at the end. Then the user replies to the message, the bot checks message for a special text at the end of a reply message. If the message contains a special text, it calls a handler with a user message. If it doesn't contain a special text, then it skips. This is all! 12 | 13 | ![Example](https://user-images.githubusercontent.com/9392723/134985949-e5cf1758-0469-428e-85ed-12229e36e58b.png) 14 | 15 | ## 📦 Installation 16 | 17 | > **[Node.js](https://nodejs.org/) 12.20.0 or newer is required** 18 | 19 | - **Using `Yarn`** (recommended) 20 | ```shell 21 | yarn add @vk-io/stateless-prompt 22 | ``` 23 | - **Using `npm`** 24 | ```shell 25 | npm i @vk-io/stateless-prompt 26 | ``` 27 | - **Using `pnpm`** 28 | ```shell 29 | pnpm add @vk-io/stateless-prompt 30 | ``` 31 | 32 | ## Example usage 33 | 34 | ```javascript 35 | import { VK } from 'vk-io'; 36 | 37 | import { StatelessPromptManager } from '@vk-io/stateless-prompt'; 38 | 39 | const vk = new VK({ 40 | token: process.env.TOKEN 41 | }); 42 | 43 | const namePrompt = new StatelessPromptManager({ 44 | slug: 'name', 45 | handler: (context, next) => { 46 | if (!context.text) { 47 | return context.send('Please reply your name with text to previous message'); 48 | } 49 | 50 | return context.send(`Your name is ${context.text}`); 51 | } 52 | }); 53 | 54 | vk.updates.on('message_new', namePrompt.middlewareIntercept); 55 | 56 | vk.updates.on('message_new', (context, next) => { 57 | if (context.text === '/signup') { 58 | return context.send('What\'s your name? Please reply to this message. ' + namePrompt.suffix); 59 | } 60 | 61 | return next(); 62 | }); 63 | 64 | vk.updates.start().catch(console.error); 65 | ``` 66 | -------------------------------------------------------------------------------- /packages/vk-io/src/api/workers/worker.ts: -------------------------------------------------------------------------------- 1 | import type { API } from '../api'; 2 | import type { APIRequest } from '../request'; 3 | 4 | import { MINIMUM_TIME_INTERVAL_API } from '../../utils/constants'; 5 | 6 | export abstract class APIWorker { 7 | public busy = false; 8 | 9 | public paused = false; 10 | 11 | protected queue: APIRequest[] = []; 12 | 13 | protected api: API; 14 | 15 | protected intervalPerRequests: number; 16 | 17 | /** 18 | * Constructor 19 | */ 20 | public constructor(api: API) { 21 | this.api = api; 22 | 23 | this.intervalPerRequests = Math.ceil(MINIMUM_TIME_INTERVAL_API / this.api.options.apiLimit); 24 | } 25 | 26 | public enqueue(request: APIRequest): void { 27 | this.queue.push(request); 28 | 29 | this.immediateHeat(); 30 | } 31 | 32 | public requeue(request: APIRequest): void { 33 | this.queue.unshift(request); 34 | 35 | this.immediateHeat(); 36 | } 37 | 38 | public pause(): void { 39 | this.paused = true; 40 | } 41 | 42 | public resume(): void { 43 | this.paused = false; 44 | 45 | this.immediateHeat(); 46 | } 47 | 48 | public heat(): void { 49 | if (this.paused || this.busy || this.queue.length === 0) { 50 | return; 51 | } 52 | 53 | this.busy = true; 54 | 55 | if (this.api.options.apiRequestMode === 'sequential') { 56 | this.execute(); 57 | 58 | setTimeout(() => { 59 | this.busy = false; 60 | 61 | this.immediateHeat(); 62 | }, this.intervalPerRequests); 63 | 64 | return; 65 | } 66 | 67 | // Burst mode 68 | 69 | const limit = Math.min(this.api.options.apiLimit, this.queue.length); 70 | 71 | for (let i = 0; i < limit && this.queue.length !== 0; i += 1) { 72 | this.execute(); 73 | } 74 | 75 | const interval = Math.ceil(MINIMUM_TIME_INTERVAL_API - limit * this.intervalPerRequests); 76 | 77 | setTimeout( 78 | () => { 79 | this.busy = false; 80 | 81 | this.immediateHeat(); 82 | }, 83 | interval <= 0 ? MINIMUM_TIME_INTERVAL_API : interval, 84 | ); 85 | } 86 | 87 | protected immediateHeat(): void { 88 | // Wait next end loop, saves one request or more 89 | setImmediate(() => this.heat()); 90 | } 91 | 92 | protected abstract execute(request?: APIRequest): unknown; 93 | } 94 | -------------------------------------------------------------------------------- /biome.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "./node_modules/@biomejs/biome/configuration_schema.json", 3 | "formatter": { 4 | "indentStyle": "space", 5 | "indentWidth": 4, 6 | "lineWidth": 120, 7 | "lineEnding": "lf" 8 | }, 9 | "vcs": { 10 | "enabled": true, 11 | "clientKind": "git", 12 | "useIgnoreFile": true 13 | }, 14 | "linter": { 15 | "rules": { 16 | "style": { 17 | "useNodejsImportProtocol": "off" 18 | }, 19 | "suspicious": { 20 | "noExplicitAny": "off" 21 | } 22 | } 23 | }, 24 | "javascript": { 25 | "formatter": { 26 | "quoteStyle": "single", 27 | "semicolons": "always", 28 | "arrowParentheses": "asNeeded" 29 | } 30 | }, 31 | "assist": { 32 | "actions": { 33 | "source": { 34 | "organizeImports": { 35 | "level": "on", 36 | "options": { 37 | "groups": [ 38 | [":PACKAGE:", "!vk-io", "!:NODE:"], 39 | ":BLANK_LINE:", 40 | "vk-io", 41 | ":BLANK_LINE:", 42 | ":NODE:", 43 | "**/{api,updates,upload}/**/*", 44 | ":BLANK_LINE:", 45 | ["**/{errors,error}", "**/{errors,error}/*"], 46 | ":BLANK_LINE:", 47 | ["**/contexts", "**/context"], 48 | ":BLANK_LINE:", 49 | "**/attachments", 50 | [ 51 | ":PATH:", 52 | "!**/{keyboard,constants,types,helpers,utils}", 53 | "!**/{keyboard,constants,types,helpers,utils}/**/*", 54 | "!**/*.types" 55 | ], 56 | ":BLANK_LINE:", 57 | "**/keyboard", 58 | ":BLANK_LINE:", 59 | "**/constants", 60 | ["**/types", "**/*.types"], 61 | ":BLANK_LINE:", 62 | ["**/{helpers,utils}"] 63 | ] 64 | } 65 | } 66 | } 67 | } 68 | }, 69 | "files": { 70 | "includes": ["**", "!packages/vk-io/src/api/schemas", "!scripts/typings-generator/schemas"] 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /packages/vk-io/src/structures/attachments/market-album.ts: -------------------------------------------------------------------------------- 1 | import { Attachment, type AttachmentFactoryOptions } from './attachment'; 2 | import { type IPhotoAttachmentPayload, PhotoAttachment } from './photo'; 3 | 4 | import { AttachmentType } from '../../utils/constants'; 5 | 6 | export interface IMarketAlbumAttachmentPayload { 7 | id: number; 8 | owner_id: number; 9 | access_key?: string; 10 | 11 | title?: string; 12 | photo?: IPhotoAttachmentPayload; 13 | count?: number; 14 | updated_time?: number; 15 | } 16 | export type MarketAlbumAttachmentOptions = AttachmentFactoryOptions; 17 | 18 | export class MarketAlbumAttachment extends Attachment< 19 | IMarketAlbumAttachmentPayload, 20 | AttachmentType.MARKET_ALBUM | 'market_album' 21 | > { 22 | public photo?: PhotoAttachment; 23 | 24 | /** 25 | * Constructor 26 | */ 27 | public constructor(options: MarketAlbumAttachmentOptions) { 28 | super({ 29 | ...options, 30 | 31 | type: AttachmentType.MARKET_ALBUM, 32 | }); 33 | 34 | this.$filled = this.payload.title !== undefined && this.payload.updated_time !== undefined; 35 | 36 | this.applyPayload(options.payload); 37 | } 38 | 39 | /** 40 | * Load attachment payload 41 | */ 42 | public async loadAttachmentPayload(): Promise { 43 | if (this.$filled) { 44 | return; 45 | } 46 | 47 | const { items } = await this.api.market.getAlbumById({ 48 | owner_id: this.ownerId, 49 | album_ids: this.id, 50 | }); 51 | 52 | this.$filled = true; 53 | 54 | this.applyPayload(items[0] as IMarketAlbumAttachmentPayload); 55 | } 56 | 57 | /** 58 | * Returns album title 59 | */ 60 | get title(): string | undefined { 61 | return this.payload.title; 62 | } 63 | 64 | /** 65 | * Returns count of products on the album 66 | */ 67 | get count(): number | undefined { 68 | return this.payload.count; 69 | } 70 | 71 | /** 72 | * Returns the date when this album was updated 73 | */ 74 | get updatedAt(): number | undefined { 75 | return this.payload.updated_time; 76 | } 77 | 78 | /** 79 | * Applies the payload 80 | */ 81 | private applyPayload(payload: IMarketAlbumAttachmentPayload): void { 82 | this.payload = payload; 83 | 84 | if (this.payload.photo) { 85 | this.photo = new PhotoAttachment({ 86 | api: this.api, 87 | payload: this.payload.photo, 88 | }); 89 | } 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /docs/ru/guide/utils.md: -------------------------------------------------------------------------------- 1 | # Utils 2 | 3 | Базовые утилиты 4 | 5 | ## resolveResource 6 | 7 | Позволяет получить информацию о ресурсе внутри ВКонтакте. Зачастую возникает задача получить идентификатор ресурса из ссылки/упоминания или числа. Для того что бы не писать эту логику самостоятельно можно воспользоваться `resolveResource`. Поддерживается следующие приведённые примеры: 8 | 9 | Ссылка 10 | - https://vk.ru/id1 — `{ id: 1, type: 'user' }` 11 | - https://vk.ru/durov — `{ id: 1, type: 'user' }` 12 | - https://vk.ru/wall1_2442097 — `{ id: 2442097, ownerId: 1, type: 'wall' }` 13 | - https://vk.ru/durov?w=wall1_2442097 — `{ id: 2442097, ownerId: 1, type: 'wall' }` 14 | - https://vk.ru/club1 — `{ id: 1, type: 'group' }` 15 | - https://vk.ru/app1 — `{ id: 1, type: 'application' }` 16 | 17 | Упоминание 18 | - \[id1|Durov\] — `{ id: 1, type: 'user' }` 19 | - \[club1|VKontakte API\] — `{ id: 1, type: 'group' }` 20 | 21 | Идентификатор 22 | - 1 — `{ id: 1, type: 'user' }` 23 | - -1 — `{ id: 1, type: 'group' }` 24 | 25 | Slug 26 | - id1 — `{ id: 1, type: 'user' }` 27 | - durov — `{ id: 1, type: 'user' }` 28 | 29 | Ресурсы делятся на два типа: 30 | 31 | [Целевые](https://negezor.github.io/vk-io/references/vk-io/interfaces/IResolvedTargetResource.html) — это единичные сущности. 32 | ```ts 33 | interface IResolvedTargetResource { 34 | id: number; 35 | type: 'user' | 'group' | 'application' | 'albums' | 'videos' | 'audios'; 36 | } 37 | ``` 38 | 39 | [С владельцем](https://negezor.github.io/vk-io/references/vk-io/interfaces/IResolvedOwnerResource.html) — это сущности с владельцем, в основном прикрепления. 40 | 41 | ```ts 42 | interface IResolvedOwnerResource { 43 | id: number; 44 | ownerId: number; 45 | type: 'photo' | 'audio' | 'video' | 'doc' | 'wall' | 'topic' | 'album'; 46 | } 47 | ``` 48 | 49 | ### Использование resolveResource 50 | [Опции функции](https://negezor.github.io/vk-io/references/vk-io/interfaces/IResolveResourceOptions.html) 51 | 52 | ::: warning Внимание 53 | Вы вполне можете не передавать класс API, однако если будет встречено короткое имя ([slug](https://en.wikipedia.org/wiki/Clean_URL#Slug)) по типу `durov` — вы получите ошибку, так как он нужен для вызова метода [utils.resolveScreenName](https://dev.vk.ru/method/utils.resolveScreenName). 54 | ::: 55 | 56 | ```ts 57 | import { API, resolveResource } from 'vk-io'; 58 | 59 | const api = new API({ 60 | token: process.env.TOKEN 61 | }); 62 | 63 | const resource = await resolveResource({ 64 | api, 65 | // Или 66 | // api: vk.api, 67 | 68 | // Ресурс который нужно разобрать 69 | resource: 'https://vk.ru/id1' 70 | }); 71 | 72 | console.log(resource); // { id: 1, type: 'user' } 73 | ``` 74 | -------------------------------------------------------------------------------- /packages/vk-io/src/structures/contexts/like.ts: -------------------------------------------------------------------------------- 1 | import { Context, type ContextDefaultState, type ContextFactoryOptions } from './context'; 2 | 3 | import { kSerializeData } from '../../utils/constants'; 4 | 5 | import { pickProperties } from '../../utils/helpers'; 6 | 7 | export type LikeContextType = 'like'; 8 | 9 | export type LikeContextSubType = 'like_add' | 'like_remove'; 10 | 11 | export interface ILikeContextPayload { 12 | liker_id: number; 13 | 14 | object_id: number; 15 | object_owner_id: number; 16 | object_type: 17 | | 'video' 18 | | 'photo' 19 | | 'comment' 20 | | 'note' 21 | | 'topic_comment' 22 | | 'photo_comment' 23 | | 'video_comment' 24 | | 'market' 25 | | 'market_comment'; 26 | 27 | post_id: number; 28 | thread_reply_id: number; 29 | } 30 | 31 | export type LikeContextOptions = ContextFactoryOptions; 32 | 33 | export class LikeContext extends Context< 34 | ILikeContextPayload, 35 | S, 36 | LikeContextType, 37 | LikeContextSubType 38 | > { 39 | public constructor(options: LikeContextOptions) { 40 | super({ 41 | ...options, 42 | 43 | type: 'like', 44 | subTypes: [options.updateType as LikeContextSubType], 45 | }); 46 | } 47 | 48 | /** 49 | * Returns the id of the user who interacts with the like 50 | */ 51 | public get likerId(): number { 52 | return this.payload.liker_id; 53 | } 54 | 55 | /** 56 | * Returns the material id 57 | */ 58 | public get objectId(): number { 59 | return this.payload.object_id; 60 | } 61 | 62 | /** 63 | * Returns the material owner id 64 | */ 65 | public get objectOwnerId(): number { 66 | return this.payload.object_owner_id; 67 | } 68 | 69 | /** 70 | * Returns the material type 71 | */ 72 | public get objectType(): ILikeContextPayload['object_type'] { 73 | return this.payload.object_type; 74 | } 75 | 76 | /** 77 | * Returns the post id (returned for a comment left under the post) 78 | */ 79 | public get postId(): number { 80 | return this.payload.post_id; 81 | } 82 | 83 | /** 84 | * Returns the parent comment / post id. 85 | */ 86 | public get threadReplyId(): number { 87 | return this.payload.thread_reply_id; 88 | } 89 | 90 | /** 91 | * Returns the custom data 92 | */ 93 | public [kSerializeData](): object { 94 | return pickProperties(this, ['likerId', 'objectId', 'objectOwnerId', 'objectType', 'postId', 'threadReplyId']); 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /scripts/typings-generator/generators/interface.js: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line import/no-extraneous-dependencies 2 | const ts = require('typescript'); 3 | 4 | const TypesGenerator = require('./types'); 5 | 6 | module.exports = class InterfaceGenerator { 7 | /** 8 | * Constructor 9 | */ 10 | constructor({ name, description }) { 11 | this.name = ts.factory.createIdentifier(name); 12 | this.description = description; 13 | 14 | this.properties = []; 15 | this.methods = []; 16 | } 17 | 18 | addProperty({ 19 | name, 20 | description, 21 | 22 | type, 23 | 24 | required = false, 25 | }) { 26 | const property = ts.factory.createPropertyDeclaration( 27 | undefined, 28 | name, 29 | !required ? ts.factory.createKeywordTypeNode(ts.SyntaxKind.QuestionToken) : undefined, 30 | type, 31 | undefined, 32 | ); 33 | 34 | if (!description) { 35 | this.methods.push(property); 36 | 37 | return; 38 | } 39 | 40 | this.properties.push( 41 | ts.addSyntheticLeadingComment(property, ts.SyntaxKind.MultiLineCommentTrivia, description, true), 42 | ); 43 | } 44 | 45 | addMethod({ 46 | name, 47 | description, 48 | 49 | parameters, 50 | result, 51 | 52 | modifiers = undefined, 53 | }) { 54 | const method = ts.factory.createMethodDeclaration( 55 | modifiers, 56 | undefined, 57 | name, 58 | undefined, 59 | undefined, 60 | parameters, 61 | result, 62 | ); 63 | 64 | if (!description) { 65 | this.methods.push(method); 66 | 67 | return; 68 | } 69 | 70 | this.methods.push( 71 | ts.addSyntheticLeadingComment(method, ts.SyntaxKind.MultiLineCommentTrivia, description, true), 72 | ); 73 | } 74 | 75 | toASTNode({ exported = false } = {}) { 76 | let iterfaceDeclaration = ts.factory.createInterfaceDeclaration(undefined, this.name, undefined, undefined, [ 77 | ...this.properties, 78 | ...this.methods, 79 | ]); 80 | 81 | if (exported) { 82 | iterfaceDeclaration = TypesGenerator.declarationExport(iterfaceDeclaration); 83 | } 84 | 85 | if (this.description) { 86 | iterfaceDeclaration = ts.addSyntheticLeadingComment( 87 | iterfaceDeclaration, 88 | ts.SyntaxKind.MultiLineCommentTrivia, 89 | this.description, 90 | true, 91 | ); 92 | } 93 | 94 | return iterfaceDeclaration; 95 | } 96 | }; 97 | -------------------------------------------------------------------------------- /packages/authorization/src/constants.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Blank html redirect 3 | */ 4 | export const CALLBACK_BLANK = 'https://oauth.vk.ru/blank.html'; 5 | 6 | /** 7 | * User-Agent for standalone auth 8 | */ 9 | export const DESKTOP_USER_AGENT = 10 | 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/84.0.4147.89 Safari/537.36'; 11 | 12 | /** 13 | * Auth error codes 14 | */ 15 | export enum AuthErrorCode { 16 | PAGE_BLOCKED = 'PAGE_BLOCKED', 17 | INVALID_PHONE_NUMBER = 'INVALID_PHONE_NUMBER', 18 | AUTHORIZATION_FAILED = 'AUTHORIZATION_FAILED', 19 | FAILED_PASSED_CAPTCHA = 'FAILED_PASSED_CAPTCHA', 20 | FAILED_PASSED_TWO_FACTOR = 'FAILED_PASSED_TWO_FACTOR', 21 | USERNAME_OR_PASSWORD_IS_INCORRECT = 'USERNAME_OR_PASSWORD_IS_INCORRECT', 22 | TOO_MUCH_TRIES = 'TOO_MUCH_TRIES', 23 | WRONG_OTP = 'WRONG_OTP', 24 | OTP_FORMAT_IS_INCORRECT = 'OTP_FORMAT_IS_INCORRECT', 25 | } 26 | 27 | /** 28 | * List of user permissions and their bit mask 29 | */ 30 | export const userScopes = new Map([ 31 | ['notify', 1], 32 | ['friends', 2], 33 | ['photos', 4], 34 | ['audio', 8], 35 | ['video', 16], 36 | ['pages', 128], 37 | ['link', 256], 38 | ['status', 1024], 39 | ['notes', 2048], 40 | ['messages', 4096], 41 | ['wall', 8192], 42 | ['ads', 32768], 43 | ['offline', 65536], 44 | ['docs', 131072], 45 | ['groups', 262144], 46 | ['notifications', 524288], 47 | ['stats', 1048576], 48 | ['email', 4194304], 49 | ['market', 134217728], 50 | ]); 51 | 52 | /** 53 | * List of group permissions and their bit mask 54 | */ 55 | export const groupScopes = new Map([ 56 | ['stories', 1], 57 | ['photos', 4], 58 | // ['app_widget', 64], 59 | ['messages', 4096], 60 | ['docs', 131072], 61 | ['manage', 262144], 62 | ]); 63 | 64 | export const officialAppCredentials = { 65 | android: { 66 | clientId: '2274003', 67 | clientSecret: 'hHbZxrka2uZ6jB1inYsH', 68 | headers: { 69 | 'User-Agent': 'VKAndroidApp/7.7-10445 (Android 11; SDK 30; arm64-v8a; Xiaomi M2003J15SC; ru; 2340x1080)', 70 | }, 71 | }, 72 | windows: { 73 | clientId: '3697615', 74 | clientSecret: 'AlVXZFMUqyrnABp8ncuU', 75 | }, 76 | windowsPhone: { 77 | clientId: '3502557', 78 | clientSecret: 'PEObAuQi6KloPM4T30DV', 79 | }, 80 | iphone: { 81 | clientId: '3140623', 82 | clientSecret: 'VeWdmVclDCtn6ihuP1nt', 83 | }, 84 | ipad: { 85 | clientId: '3682744', 86 | clientSecret: 'mY6CDUswIVdJLCD3j15n', 87 | }, 88 | vkMe: { 89 | clientId: '6146827', 90 | clientSecret: 'qVxWRF1CwHERuIrKBnqe', 91 | }, 92 | }; 93 | -------------------------------------------------------------------------------- /packages/vk-io/src/structures/contexts/donut-subscription-price.ts: -------------------------------------------------------------------------------- 1 | import { Context, type ContextDefaultState, type ContextFactoryOptions } from './context'; 2 | 3 | import { kSerializeData } from '../../utils/constants'; 4 | 5 | import { pickProperties } from '../../utils/helpers'; 6 | 7 | export type DonutSubscriptionPriceContextType = 'donut_subscription_price'; 8 | 9 | export type DonutSubscriptionPriceContextSubType = 'donut_subscription_price_changed'; 10 | 11 | export interface IDonutSubscriptionPriceContextPayload { 12 | user_id: number; 13 | 14 | amount_old: number; 15 | amount_new: number; 16 | amount_diff: number; 17 | amount_diff_without_fee: number; 18 | } 19 | 20 | export type DonutSubscriptionPriceContextOptions = ContextFactoryOptions; 21 | 22 | export class DonutSubscriptionPriceContext extends Context< 23 | IDonutSubscriptionPriceContextPayload, 24 | S, 25 | DonutSubscriptionPriceContextType, 26 | DonutSubscriptionPriceContextSubType 27 | > { 28 | public constructor(options: DonutSubscriptionPriceContextOptions) { 29 | super({ 30 | ...options, 31 | 32 | type: 'donut_subscription_price', 33 | subTypes: [options.updateType as DonutSubscriptionPriceContextSubType], 34 | }); 35 | } 36 | 37 | /** 38 | * Checks if subscription changed 39 | */ 40 | public get isChanged(): boolean { 41 | return this.is(['donut_subscription_price_changed']); 42 | } 43 | 44 | /** 45 | * Returns the id of the user who interacts with the vk donut 46 | */ 47 | public get userId(): number { 48 | return this.payload.user_id; 49 | } 50 | 51 | /** 52 | * Returns the old amount 53 | */ 54 | public get oldAmount(): number { 55 | return this.payload.amount_old; 56 | } 57 | 58 | /** 59 | * Returns the new amount 60 | */ 61 | public get newAmount(): number { 62 | return this.payload.amount_new; 63 | } 64 | 65 | /** 66 | * Returns the diff amount 67 | */ 68 | public get diffAmount(): number { 69 | return this.payload.amount_diff; 70 | } 71 | 72 | /** 73 | * Returns the diff amount without fee 74 | */ 75 | public get diffAmountWithoutFee(): number { 76 | return this.payload.amount_diff_without_fee; 77 | } 78 | 79 | /** 80 | * Returns the custom data 81 | */ 82 | public [kSerializeData](): object { 83 | return pickProperties(this, [ 84 | 'isChanged', 85 | 'userId', 86 | 'oldAmount', 87 | 'newAmount', 88 | 'diffAmount', 89 | 'diffAmountWithoutFee', 90 | ]); 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /packages/scenes/README.md: -------------------------------------------------------------------------------- 1 | # VK-IO Scenes 2 | 3 | NPM version 4 | Build Status 5 | NPM downloads 6 | 7 | > VK-IO Scenes - Simple implementation of middleware-based scene management 🎬 8 | 9 | ## 📦 Installation 10 | 11 | > **[Node.js](https://nodejs.org/) 12.20.0 or newer is required** 12 | 13 | - **Using `Yarn`** (recommended) 14 | ```shell 15 | yarn add @vk-io/scenes 16 | ``` 17 | - **Using `npm`** 18 | ```shell 19 | npm i @vk-io/scenes 20 | ``` 21 | - **Using `pnpm`** 22 | ```shell 23 | pnpm add @vk-io/scenes 24 | ``` 25 | 26 | ## Example usage 27 | 28 | ```javascript 29 | import { VK } from 'vk-io'; 30 | 31 | // Session implementation can be any 32 | import { SessionManager } from '@vk-io/session'; 33 | import { SceneManager, StepScene } from '@vk-io/scenes'; 34 | 35 | const vk = new VK({ 36 | token: process.env.TOKEN 37 | }); 38 | 39 | const sessionManager = new SessionManager(); 40 | const sceneManager = new SceneManager(); 41 | 42 | vk.updates.on('message_new', sessionManager.middleware); 43 | 44 | vk.updates.on('message_new', sceneManager.middleware); 45 | vk.updates.on('message_new', sceneManager.middlewareIntercept); // Default scene entry handler 46 | 47 | vk.updates.on('message_new', (context, next) => { 48 | if (context.text === '/signup') { 49 | return context.scene.enter('signup'); 50 | } 51 | 52 | return next(); 53 | }); 54 | 55 | sceneManager.addScenes([ 56 | new StepScene('signup', [ 57 | (context) => { 58 | if (context.scene.step.firstTime || !context.text) { 59 | return context.send('What\'s your name?'); 60 | } 61 | 62 | context.scene.state.firstName = context.text; 63 | 64 | return context.scene.step.next(); 65 | }, 66 | (context) => { 67 | if (context.scene.step.firstTime || !context.text) { 68 | return context.send('How old are you?'); 69 | } 70 | 71 | context.scene.state.age = Number(context.text); 72 | 73 | return context.scene.step.next(); 74 | }, 75 | async (context) => { 76 | const { firstName, age } = context.scene.state; 77 | 78 | await context.send(`👤 ${firstName} ${age} ages`); 79 | 80 | return context.scene.step.next(); // Automatic exit, since this is the last scene 81 | } 82 | ]) 83 | ]); 84 | 85 | vk.updates.start().catch(console.error); 86 | ``` 87 | -------------------------------------------------------------------------------- /packages/vk-io/src/structures/attachments/helpers.ts: -------------------------------------------------------------------------------- 1 | import type { API } from '../../api'; 2 | import { 3 | type Attachment, 4 | AudioAttachment, 5 | AudioMessageAttachment, 6 | DocumentAttachment, 7 | type ExternalAttachment, 8 | GiftAttachment, 9 | GraffitiAttachment, 10 | LinkAttachment, 11 | MarketAlbumAttachment, 12 | MarketAttachment, 13 | PhotoAttachment, 14 | PollAttachment, 15 | StickerAttachment, 16 | StoryAttachment, 17 | VideoAttachment, 18 | WallAttachment, 19 | WallReplyAttachment, 20 | } from '.'; 21 | 22 | import { AttachmentType } from '../../utils/constants'; 23 | 24 | const attachmentsTypes = { 25 | [AttachmentType.ALBUM]: undefined, 26 | [AttachmentType.PODCAST]: undefined, 27 | [AttachmentType.TEXTLIVE]: undefined, 28 | 29 | [AttachmentType.POLL]: (): typeof PollAttachment => PollAttachment, 30 | [AttachmentType.GIFT]: (): typeof GiftAttachment => GiftAttachment, 31 | [AttachmentType.WALL]: (): typeof WallAttachment => WallAttachment, 32 | [AttachmentType.LINK]: (): typeof LinkAttachment => LinkAttachment, 33 | [AttachmentType.PHOTO]: (): typeof PhotoAttachment => PhotoAttachment, 34 | [AttachmentType.AUDIO]: (): typeof AudioAttachment => AudioAttachment, 35 | [AttachmentType.STORY]: (): typeof StoryAttachment => StoryAttachment, 36 | [AttachmentType.VIDEO]: (): typeof VideoAttachment => VideoAttachment, 37 | [AttachmentType.DOCUMENT]: (): typeof DocumentAttachment => DocumentAttachment, 38 | [AttachmentType.MARKET]: (): typeof MarketAttachment => MarketAttachment, 39 | [AttachmentType.STICKER]: (): typeof StickerAttachment => StickerAttachment, 40 | [AttachmentType.GRAFFITI]: (): typeof GraffitiAttachment => GraffitiAttachment, 41 | [AttachmentType.WALL_REPLY]: (): typeof WallReplyAttachment => WallReplyAttachment, 42 | [AttachmentType.MARKET_ALBUM]: (): typeof MarketAlbumAttachment => MarketAlbumAttachment, 43 | [AttachmentType.AUDIO_MESSAGE]: (): typeof AudioMessageAttachment => AudioMessageAttachment, 44 | }; 45 | 46 | /** 47 | * Transform raw attachments to wrapper 48 | */ 49 | export const transformAttachments = (rawAttachments: any[], api: API): (Attachment | ExternalAttachment)[] => { 50 | const attachments: (Attachment | ExternalAttachment)[] = []; 51 | 52 | for (const rawAttachment of rawAttachments) { 53 | const type = rawAttachment.type as AttachmentType; 54 | 55 | const attachmentFactory = attachmentsTypes[type]; 56 | 57 | if (attachmentFactory === undefined) { 58 | continue; 59 | } 60 | 61 | const AttachmentConstructor = attachmentFactory(); 62 | 63 | attachments.push( 64 | new AttachmentConstructor({ 65 | api, 66 | payload: rawAttachment[type], 67 | }), 68 | ); 69 | } 70 | 71 | return attachments; 72 | }; 73 | -------------------------------------------------------------------------------- /packages/vk-io/src/structures/contexts/dialog-notification-settings.ts: -------------------------------------------------------------------------------- 1 | import { Context, type ContextDefaultState, type ContextFactoryOptions } from './context'; 2 | 3 | import { kSerializeData } from '../../utils/constants'; 4 | 5 | import { pickProperties } from '../../utils/helpers'; 6 | 7 | export type DialogNotificationSettingsContextType = 'dialog_notification_settings'; 8 | 9 | export type DialogNotificationSettingsContextSubType = 10 | | 'dialog_notification_settings_subscribe' 11 | | 'dialog_notification_settings_unsubscribe'; 12 | 13 | export interface IDialogNotificationSettingsContextPayload { 14 | peer_id: number; 15 | sound: 0 | 1; 16 | disabled_until: number; 17 | } 18 | 19 | export type DialogNotificationSettingsContextOptions = ContextFactoryOptions< 20 | [number, IDialogNotificationSettingsContextPayload], 21 | S 22 | >; 23 | 24 | export class DialogNotificationSettingsContext extends Context< 25 | IDialogNotificationSettingsContextPayload, 26 | S, 27 | DialogNotificationSettingsContextType, 28 | DialogNotificationSettingsContextSubType 29 | > { 30 | public constructor(options: DialogNotificationSettingsContextOptions) { 31 | const [, payload] = options.payload; 32 | 33 | const { disabled_until } = payload; 34 | 35 | super({ 36 | ...options, 37 | 38 | type: 'dialog_notification_settings', 39 | subTypes: [ 40 | disabled_until === 0 41 | ? 'dialog_notification_settings_subscribe' 42 | : 'dialog_notification_settings_unsubscribe', 43 | ], 44 | 45 | payload: payload, 46 | }); 47 | } 48 | 49 | /** 50 | * Returns the peer identifier 51 | */ 52 | public get peerId(): number { 53 | return this.payload.peer_id; 54 | } 55 | 56 | /** 57 | * Returns time until that notifications are disabled in seconds 58 | */ 59 | public get disabledUntil(): number { 60 | return this.payload.disabled_until; 61 | } 62 | 63 | /** 64 | * Checks that the notifications have sound 65 | */ 66 | public get hasSound(): boolean { 67 | return Boolean(this.payload.sound); 68 | } 69 | 70 | /** 71 | * Checks that the user has subscribed to dialog 72 | */ 73 | public get isSubscribed(): boolean { 74 | return this.subTypes.includes('dialog_notification_settings_subscribe'); 75 | } 76 | 77 | /** 78 | * Checks that the user has unsubscribed to dialog 79 | */ 80 | public get isUnsubscribed(): boolean { 81 | return this.subTypes.includes('dialog_notification_settings_unsubscribe'); 82 | } 83 | 84 | /** 85 | * Returns the custom data 86 | */ 87 | public [kSerializeData](): object { 88 | return pickProperties(this, ['peerId', 'hasSound', 'disabledUntil', 'isSubscribed', 'isUnsubscribed']); 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /packages/session/src/session-manager.ts: -------------------------------------------------------------------------------- 1 | import { MemoryStorage } from './storages'; 2 | 3 | import type { IContext, ISessionContext, ISessionManagerOptions, Middleware } from './types'; 4 | 5 | export class SessionManager { 6 | protected storage: ISessionManagerOptions['storage']; 7 | 8 | protected contextKey: ISessionManagerOptions['contextKey']; 9 | 10 | protected getStorageKey: ISessionManagerOptions['getStorageKey']; 11 | 12 | public constructor(options: Partial> = {}) { 13 | this.storage = options.storage || new MemoryStorage(); 14 | 15 | this.contextKey = options.contextKey || 'session'; 16 | 17 | this.getStorageKey = options.getStorageKey || ((context): string => String(context.senderId || context.userId)); 18 | } 19 | 20 | /** 21 | * Returns the middleware for embedding 22 | */ 23 | public get middleware(): Middleware { 24 | const { storage, contextKey, getStorageKey } = this; 25 | 26 | return async (context: IContext, next: () => Promise): Promise => { 27 | const storageKey = getStorageKey(context); 28 | 29 | let changed = false; 30 | const wrapSession = (targetRaw: object): ISessionContext => 31 | new Proxy( 32 | { ...targetRaw, $forceUpdate }, 33 | { 34 | set: (target, prop: string, value): boolean => { 35 | changed = true; 36 | 37 | target[prop] = value; 38 | 39 | return true; 40 | }, 41 | deleteProperty: (target, prop: string): boolean => { 42 | changed = true; 43 | 44 | delete target[prop]; 45 | 46 | return true; 47 | }, 48 | }, 49 | ); 50 | 51 | const $forceUpdate = (): Promise => { 52 | if (Object.keys(session).length > 1) { 53 | changed = false; 54 | return storage.set(storageKey, session); 55 | } 56 | 57 | return storage.delete(storageKey); 58 | }; 59 | 60 | const initialSession = (await storage.get(storageKey)) || {}; 61 | 62 | let session = wrapSession(initialSession); 63 | 64 | Object.defineProperty(context, contextKey, { 65 | get: (): ISessionContext => session, 66 | set: (newSession): void => { 67 | session = wrapSession(newSession as object); 68 | changed = true; 69 | }, 70 | }); 71 | 72 | await next(); 73 | 74 | if (changed) { 75 | await $forceUpdate(); 76 | } else { 77 | await storage.touch(storageKey); 78 | } 79 | }; 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /packages/vk-io/src/structures/shared/attachmentable.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | import type { 3 | Attachment, 4 | AudioAttachment, 5 | AudioMessageAttachment, 6 | DocumentAttachment, 7 | ExternalAttachment, 8 | GiftAttachment, 9 | GraffitiAttachment, 10 | LinkAttachment, 11 | MarketAlbumAttachment, 12 | MarketAttachment, 13 | PhotoAttachment, 14 | PollAttachment, 15 | StickerAttachment, 16 | StoryAttachment, 17 | VideoAttachment, 18 | WallAttachment, 19 | WallReplyAttachment, 20 | } from '../attachments'; 21 | 22 | import type { AttachmentType, AttachmentTypeString } from '../../utils/constants'; 23 | 24 | export class Attachmentable { 25 | public attachments!: (Attachment | ExternalAttachment)[]; 26 | 27 | /** 28 | * Checks for the presence of attachments 29 | */ 30 | public hasAttachments(type?: AttachmentType | AttachmentTypeString): boolean { 31 | if (type === undefined) { 32 | return this.attachments.length > 0; 33 | } 34 | 35 | return this.attachments.some(attachment => attachment.type === type); 36 | } 37 | 38 | /** 39 | * Returns the attachments 40 | */ 41 | public getAttachments(type: AttachmentType.AUDIO | 'audio'): AudioAttachment[]; 42 | 43 | public getAttachments(type: AttachmentType.AUDIO_MESSAGE | 'audio_message'): AudioMessageAttachment[]; 44 | 45 | public getAttachments(type: AttachmentType.GRAFFITI | 'graffiti'): GraffitiAttachment[]; 46 | 47 | public getAttachments(type: AttachmentType.DOCUMENT | 'doc'): DocumentAttachment[]; 48 | 49 | public getAttachments(type: AttachmentType.MARKET_ALBUM | 'market_album'): MarketAlbumAttachment[]; 50 | 51 | public getAttachments(type: AttachmentType.MARKET | 'market'): MarketAttachment[]; 52 | 53 | public getAttachments(type: AttachmentType.PHOTO | 'photo'): PhotoAttachment[]; 54 | 55 | public getAttachments(type: AttachmentType.STORY | 'story'): StoryAttachment[]; 56 | 57 | public getAttachments(type: AttachmentType.VIDEO | 'video'): VideoAttachment[]; 58 | 59 | public getAttachments(type: AttachmentType.WALL | 'wall'): WallAttachment[]; 60 | 61 | public getAttachments(type: AttachmentType.POLL | 'poll'): PollAttachment[]; 62 | 63 | public getAttachments(type: AttachmentType.GIFT | 'gift'): GiftAttachment[]; 64 | 65 | public getAttachments(type: AttachmentType.LINK | 'link'): LinkAttachment[]; 66 | 67 | public getAttachments(type: AttachmentType.STICKER | 'sticker'): StickerAttachment[]; 68 | 69 | public getAttachments(type: AttachmentType.WALL_REPLY | 'wall_reply'): WallReplyAttachment[]; 70 | 71 | public getAttachments(type?: AttachmentType | AttachmentTypeString): (Attachment | ExternalAttachment)[] { 72 | if (type === undefined) { 73 | return this.attachments; 74 | } 75 | 76 | return this.attachments.filter(attachment => attachment.type === type); 77 | } 78 | } 79 | 80 | export interface IAllAttachmentable { 81 | hasAllAttachments: Attachmentable['hasAttachments']; 82 | getAllAttachments: Attachmentable['getAttachments']; 83 | } 84 | -------------------------------------------------------------------------------- /packages/vk-io/src/structures/contexts/context.ts: -------------------------------------------------------------------------------- 1 | import { inspectable } from 'inspectable'; 2 | 3 | import type { API } from '../../api'; 4 | import type { Upload } from '../../upload'; 5 | 6 | import { kSerializeData, type UpdateSource } from '../../utils/constants'; 7 | 8 | export type ContextDefaultState = Record; 9 | 10 | export interface IContextOptions { 11 | api: API; 12 | upload: Upload; 13 | 14 | type: Type; 15 | subTypes: SubType[]; 16 | 17 | payload: P; 18 | state?: S; 19 | 20 | source: UpdateSource; 21 | updateType: string | number; 22 | 23 | groupId?: number; 24 | } 25 | 26 | export type ContextFactoryOptions = Omit, 'type' | 'subTypes'>; 27 | 28 | export class Context< 29 | P = object, 30 | S = ContextDefaultState, 31 | Type extends string = string, 32 | SubType extends string = string, 33 | > { 34 | public type: Type; 35 | 36 | public subTypes: SubType[]; 37 | 38 | public state: S; 39 | 40 | protected api: API; 41 | 42 | protected upload: Upload; 43 | 44 | public $groupId?: number; 45 | 46 | protected payload: P; 47 | [key: string]: any; 48 | 49 | /** 50 | * Constructor 51 | */ 52 | public constructor(options: IContextOptions) { 53 | this.api = options.api; 54 | this.upload = options.upload; 55 | 56 | this.type = options.type; 57 | this.subTypes = options.subTypes; 58 | 59 | this.payload = options.payload; 60 | this.state = options.state || ({} as S); 61 | 62 | this.$groupId = options.groupId; 63 | } 64 | 65 | /** 66 | * Returns custom tag 67 | */ 68 | public get [Symbol.toStringTag](): string { 69 | return this.constructor.name; 70 | } 71 | 72 | /** 73 | * Checks whether the context of some of these types 74 | */ 75 | public is(rawTypes: (Type | SubType)[]): boolean { 76 | const types = !Array.isArray(rawTypes) ? [rawTypes] : rawTypes; 77 | 78 | if (types.includes(this.type)) { 79 | return true; 80 | } 81 | 82 | return this.subTypes.some((type): boolean => types.includes(type)); 83 | } 84 | 85 | /** 86 | * Returns data for JSON 87 | */ 88 | public toJSON(): object { 89 | return { 90 | ...this[kSerializeData](), 91 | 92 | type: this.type, 93 | subTypes: this.subTypes, 94 | state: this.state, 95 | }; 96 | } 97 | 98 | /** 99 | * Returns the custom data 100 | */ 101 | public [kSerializeData](): object { 102 | const { api: _, upload: __, ...payload } = this; 103 | 104 | return payload; 105 | } 106 | } 107 | 108 | inspectable(Context, { 109 | serialize: instance => instance.toJSON(), 110 | stringify: (instance, payload, context): string => 111 | `${context.stylize(instance.constructor.name, 'special')} ${context.inspect(payload)}`, 112 | }); 113 | -------------------------------------------------------------------------------- /packages/authorization/src/providers/implicit-flow-user.ts: -------------------------------------------------------------------------------- 1 | import createDebug from 'debug'; 2 | 3 | import { AuthorizationError } from '../errors'; 4 | 5 | import type { Response } from '../fetch-cookie'; 6 | import { ImplicitFlow } from './implicit-flow'; 7 | 8 | import { AuthErrorCode, CALLBACK_BLANK } from '../constants'; 9 | 10 | import { getAllUserPermissions, getUserPermissionsByName } from '../helpers'; 11 | 12 | const debug = createDebug('vk-io:authorization:implicit-flow-user'); 13 | 14 | const { AUTHORIZATION_FAILED } = AuthErrorCode; 15 | 16 | export class ImplicitFlowUser extends ImplicitFlow { 17 | /** 18 | * Returns permission page 19 | */ 20 | protected getPermissionsPage(): Promise { 21 | const { clientId } = this.options; 22 | let { scope } = this.options; 23 | 24 | if (scope === undefined) { 25 | throw new Error('Required option "scope" not set'); 26 | } 27 | 28 | if (scope === 'all') { 29 | scope = getAllUserPermissions(); 30 | } else if (typeof scope !== 'number') { 31 | scope = getUserPermissionsByName(scope); 32 | } 33 | 34 | debug('auth scope %s', scope); 35 | 36 | const params = new URLSearchParams({ 37 | redirect_uri: CALLBACK_BLANK, 38 | response_type: 'token', 39 | display: 'page', 40 | v: this.options.apiVersion, 41 | client_id: clientId, 42 | scope: String(scope), 43 | }); 44 | 45 | const url = new URL(`https://oauth.vk.ru/authorize?${params.toString()}`); 46 | 47 | return this.fetch(url, { 48 | method: 'GET', 49 | }); 50 | } 51 | 52 | /** 53 | * Starts authorization 54 | */ 55 | public async run(): Promise<{ 56 | email: string | undefined; 57 | userId: number | undefined; 58 | token: string; 59 | expires: number | undefined; 60 | }> { 61 | const { response } = await super.login(); 62 | 63 | let { hash } = new URL(response.url); 64 | 65 | if (hash.startsWith('#')) { 66 | hash = hash.substring(1); 67 | } 68 | 69 | const params = new URLSearchParams(hash); 70 | 71 | if (params.has('error')) { 72 | throw new AuthorizationError({ 73 | message: `Failed passed grant access: ${params.get('error_description') || 'Unknown error'}`, 74 | code: AUTHORIZATION_FAILED, 75 | }); 76 | } 77 | 78 | const userId = params.get('user_id'); 79 | const expires = params.get('expires_in'); 80 | const accessToken = params.get('access_token'); 81 | 82 | if (!accessToken) { 83 | throw new AuthorizationError({ 84 | message: 'Field access_token is not found', 85 | code: AUTHORIZATION_FAILED, 86 | }); 87 | } 88 | 89 | return { 90 | email: params.get('email') || undefined, 91 | userId: userId !== null ? Number(userId) : undefined, 92 | 93 | token: accessToken, 94 | expires: expires !== null ? Number(expires) : undefined, 95 | }; 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /packages/vk-io/src/structures/attachments/link.ts: -------------------------------------------------------------------------------- 1 | import { ExternalAttachment, type ExternalAttachmentFactoryOptions } from './external'; 2 | import { type IPhotoAttachmentPayload, PhotoAttachment } from './photo'; 3 | 4 | import { AttachmentType, kSerializeData } from '../../utils/constants'; 5 | 6 | import { pickProperties } from '../../utils/helpers'; 7 | 8 | const kPhoto = Symbol('kPhoto'); 9 | 10 | export interface ILinkAttachmentPayload { 11 | title: string; 12 | caption?: string; 13 | description?: string; 14 | url: string; 15 | product?: { 16 | price: object; 17 | }; 18 | button?: { 19 | title: string; 20 | action: { 21 | type: string; 22 | url: string; 23 | }; 24 | }; 25 | photo?: IPhotoAttachmentPayload; 26 | } 27 | 28 | export type LinkAttachmentOptions = ExternalAttachmentFactoryOptions; 29 | 30 | export class LinkAttachment extends ExternalAttachment { 31 | protected [kPhoto]: PhotoAttachment | undefined; 32 | 33 | /** 34 | * Constructor 35 | */ 36 | public constructor(options: LinkAttachmentOptions) { 37 | super({ 38 | ...options, 39 | 40 | type: AttachmentType.LINK, 41 | }); 42 | 43 | if (this.payload.photo) { 44 | this[kPhoto] = new PhotoAttachment({ 45 | api: this.api, 46 | payload: this.payload.photo, 47 | }); 48 | } 49 | } 50 | 51 | /** 52 | * Checks for the presence of a photo in a link 53 | */ 54 | public get hasPhoto(): boolean { 55 | return Boolean(this[kPhoto]); 56 | } 57 | 58 | /** 59 | * Returns the title 60 | */ 61 | public get title(): string { 62 | return this.payload.title; 63 | } 64 | 65 | /** 66 | * Returns the title 67 | */ 68 | public get caption(): string | undefined { 69 | return this.payload.caption; 70 | } 71 | 72 | /** 73 | * Returns the description 74 | */ 75 | public get description(): string | undefined { 76 | return this.payload.description; 77 | } 78 | 79 | /** 80 | * Returns the URL of the link 81 | */ 82 | public get url(): string { 83 | return this.payload.url; 84 | } 85 | 86 | /** 87 | * Returns the product 88 | */ 89 | public get product(): ILinkAttachmentPayload['product'] | undefined { 90 | return this.payload.product; 91 | } 92 | 93 | /** 94 | * Returns the button 95 | */ 96 | public get button(): ILinkAttachmentPayload['button'] | undefined { 97 | return this.payload.button; 98 | } 99 | 100 | /** 101 | * Returns the photo 102 | */ 103 | public get photo(): PhotoAttachment | undefined { 104 | return this[kPhoto]; 105 | } 106 | 107 | /** 108 | * Returns the custom data 109 | */ 110 | public [kSerializeData](): object { 111 | return pickProperties(this, ['title', 'caption', 'description', 'url', 'product', 'button', 'photo']); 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /packages/vk-io/src/structures/contexts/donut-subscription.ts: -------------------------------------------------------------------------------- 1 | import { Context, type ContextDefaultState, type ContextFactoryOptions } from './context'; 2 | 3 | import { kSerializeData } from '../../utils/constants'; 4 | 5 | import { pickProperties } from '../../utils/helpers'; 6 | 7 | export type DonutSubscriptionContextType = 'donut_subscription'; 8 | 9 | export type DonutSubscriptionContextSubType = 10 | | 'donut_subscription_create' 11 | | 'donut_subscription_prolonged' 12 | | 'donut_subscription_expired' 13 | | 'donut_subscription_cancelled'; 14 | 15 | export interface IDonutSubscriptionContextPayload { 16 | user_id: number; 17 | 18 | amount?: number; 19 | amount_without_fee?: number; 20 | } 21 | 22 | export type DonutSubscriptionContextOptions = ContextFactoryOptions; 23 | 24 | export class DonutSubscriptionContext extends Context< 25 | IDonutSubscriptionContextPayload, 26 | S, 27 | DonutSubscriptionContextType, 28 | DonutSubscriptionContextSubType 29 | > { 30 | public constructor(options: DonutSubscriptionContextOptions) { 31 | super({ 32 | ...options, 33 | 34 | type: 'donut_subscription', 35 | subTypes: [options.updateType as DonutSubscriptionContextSubType], 36 | }); 37 | } 38 | 39 | /** 40 | * Checks if subscription created 41 | */ 42 | public get isCreated(): boolean { 43 | return this.is(['donut_subscription_create']); 44 | } 45 | 46 | /** 47 | * Checks if subscription prolonged 48 | */ 49 | public get isProlonged(): boolean { 50 | return this.is(['donut_subscription_prolonged']); 51 | } 52 | 53 | /** 54 | * Checks if subscription expired 55 | */ 56 | public get isExpired(): boolean { 57 | return this.is(['donut_subscription_expired']); 58 | } 59 | 60 | /** 61 | * Checks if subscription cancelled 62 | */ 63 | public get isCancelled(): boolean { 64 | return this.is(['donut_subscription_cancelled']); 65 | } 66 | 67 | /** 68 | * Returns the id of the user who interacts with the vk donut 69 | */ 70 | public get userId(): number { 71 | return this.payload.user_id; 72 | } 73 | 74 | /** 75 | * Returns the amount 76 | */ 77 | public get amount(): number { 78 | // biome-ignore lint/style/noNonNullAssertion: always present 79 | return this.payload.amount!; 80 | } 81 | 82 | /** 83 | * Returns the amount without fee 84 | */ 85 | public get amountWithoutFee(): number { 86 | // biome-ignore lint/style/noNonNullAssertion: always present 87 | return this.payload.amount_without_fee!; 88 | } 89 | 90 | /** 91 | * Returns the custom data 92 | */ 93 | public [kSerializeData](): object { 94 | return pickProperties(this, [ 95 | 'isCreated', 96 | 'isProlonged', 97 | 'isExpired', 98 | 'isCancelled', 99 | 'userId', 100 | 'amount', 101 | 'amountWithoutFee', 102 | ]); 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import jsonPlugin from '@rollup/plugin-json'; 2 | import typescriptPlugin from 'rollup-plugin-typescript2'; 3 | 4 | import { builtinModules } from 'node:module'; 5 | import { tmpdir } from 'node:os'; 6 | import { join as pathJoin } from 'node:path'; 7 | import { fileURLToPath } from 'node:url'; 8 | 9 | const MODULES = ['vk-io', 'hear', 'scenes', 'session', 'streaming', 'authorization', 'stateless-prompt']; 10 | 11 | const coreModules = builtinModules.filter(name => !/(^_|\/)/.test(name)); 12 | 13 | const cacheRoot = pathJoin(tmpdir(), '.rpt2_cache'); 14 | 15 | const getModulePath = path => pathJoin(fileURLToPath(new URL('.', import.meta.url)), 'packages', path); 16 | 17 | export default async () => 18 | Promise.all( 19 | MODULES.map(getModulePath).map(async modulePath => { 20 | const modulePkg = await import(pathJoin(modulePath, 'package.json'), { 21 | with: { 22 | type: 'json', 23 | }, 24 | }); 25 | 26 | const src = pathJoin(modulePath, 'src'); 27 | const lib = pathJoin(modulePath, 'lib'); 28 | 29 | return { 30 | input: pathJoin(src, 'index.ts'), 31 | plugins: [ 32 | jsonPlugin(), 33 | typescriptPlugin({ 34 | cacheRoot, 35 | 36 | useTsconfigDeclarationDir: false, 37 | 38 | tsconfigOverride: { 39 | outDir: lib, 40 | rootDir: src, 41 | include: [src], 42 | }, 43 | }), 44 | // https://rollupjs.org/guide/en/#renderdynamicimport 45 | { 46 | name: 'retain-import-expression', 47 | resolveDynamicImport(specifier) { 48 | if (specifier === 'node-fetch') return false; 49 | return null; 50 | }, 51 | renderDynamicImport({ targetModuleId }) { 52 | if (targetModuleId === 'node-fetch') { 53 | return { 54 | left: 'import(', 55 | right: ')', 56 | }; 57 | } 58 | 59 | return undefined; 60 | }, 61 | }, 62 | ], 63 | external: [ 64 | ...Object.keys(modulePkg.dependencies || {}), 65 | ...Object.keys(modulePkg.peerDependencies || {}), 66 | // TODO: To make better 67 | ...MODULES.map(moduleName => `@vk-io/${moduleName}`), 68 | ...coreModules, 69 | ], 70 | output: [ 71 | { 72 | file: pathJoin(lib, 'index.js'), 73 | format: 'cjs', 74 | exports: 'named', 75 | }, 76 | { 77 | file: pathJoin(lib, 'index.mjs'), 78 | format: 'esm', 79 | }, 80 | ], 81 | }; 82 | }), 83 | ); 84 | -------------------------------------------------------------------------------- /packages/vk-io/src/structures/attachments/wall-reply.ts: -------------------------------------------------------------------------------- 1 | import type { Attachment } from './attachment'; 2 | import { ExternalAttachment, type ExternalAttachmentFactoryOptions } from './external'; 3 | 4 | import { AttachmentType } from '../../utils/constants'; 5 | 6 | import { transformAttachments } from './helpers'; 7 | 8 | export interface IWallReplyAttachmentPayload { 9 | id: number; 10 | 11 | owner_id: number; 12 | post_id: number; 13 | 14 | from_id: number; 15 | date: number; 16 | text: string; 17 | reply_to_user: number; 18 | reply_to_comment: number; 19 | parents_stack: number[]; 20 | attachments: any[]; 21 | thread: { 22 | count: number; 23 | items: IWallReplyAttachmentPayload[]; 24 | can_post: boolean; 25 | show_reply_button: boolean; 26 | groups_can_post: boolean; 27 | }; 28 | } 29 | 30 | export type WallReplyAttachmentOptions = ExternalAttachmentFactoryOptions; 31 | 32 | export class WallReplyAttachment extends ExternalAttachment< 33 | IWallReplyAttachmentPayload, 34 | AttachmentType.WALL_REPLY | 'wall_reply' 35 | > { 36 | public attachments: (Attachment | ExternalAttachment)[]; 37 | 38 | /** 39 | * Constructor 40 | */ 41 | public constructor(options: WallReplyAttachmentOptions) { 42 | super({ 43 | ...options, 44 | 45 | type: AttachmentType.WALL_REPLY, 46 | }); 47 | 48 | this.attachments = transformAttachments(options.payload.attachments || [], this.api); 49 | } 50 | 51 | /** 52 | * Returns the comment id 53 | */ 54 | public get id(): number { 55 | return this.payload.id; 56 | } 57 | 58 | /** 59 | * Returns the comment owner id 60 | */ 61 | public get ownerId(): number { 62 | return this.payload.owner_id; 63 | } 64 | 65 | /** 66 | * Returns the comment post id 67 | */ 68 | public get postId(): number { 69 | return this.payload.owner_id; 70 | } 71 | 72 | /** 73 | * Returns the identifier of the user or community to which the current comment was posted 74 | */ 75 | public get replyToUserId(): number { 76 | return this.payload.reply_to_user; 77 | } 78 | 79 | /** 80 | * Returns the identifier of the comment, in response to which the current is left 81 | */ 82 | public get replyToCommentId(): number { 83 | return this.payload.reply_to_comment; 84 | } 85 | 86 | /** 87 | * Returns the array of parent comment identifiers 88 | */ 89 | public get parentCommentIds(): number[] { 90 | return this.payload.parents_stack; 91 | } 92 | 93 | /** 94 | * Returns the post text 95 | */ 96 | public get text(): string | undefined { 97 | return this.payload.text; 98 | } 99 | 100 | /** 101 | * Returns the date when this post was created 102 | */ 103 | public get createdAt(): number | undefined { 104 | return this.payload.date; 105 | } 106 | 107 | /** 108 | * Returns information about a nested comment branch, an object with fields 109 | */ 110 | public get thread(): IWallReplyAttachmentPayload['thread'] | undefined { 111 | return this.payload.thread; 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /docs/ru/guide/modules.md: -------------------------------------------------------------------------------- 1 | # Модули 2 | 3 | Библиотека разбита на модули, вы можете использовать их по отдельности, главное передать необходимые зависимости. 4 | 5 | ## API 6 | 7 | Базовый модуль [запросов](https://dev.vk.ru/api_requests), позволяет вызывать методы API. 8 | 9 | ```ts 10 | import { VK } from 'vk-io'; 11 | 12 | const vk = new VK({ 13 | token: process.env.TOKEN 14 | }); 15 | 16 | const users = await vk.api.users.get({ 17 | user_ids: 1 18 | }); 19 | ``` 20 | 21 | [Более подробно](./api) 22 | 23 | ## Upload 24 | 25 | Базовый модуль для [загрузки](https://dev.vk.ru/api/upload/overview), позволяет: 26 | - Загружать файлы на стену (фото/документ) 27 | - Загружать фото в альбом 28 | - Загружать файлы в личные сообщения (фото/документ/граффити/голосовое сообщение) 29 | - Загружать видео/истории/аудио/документы 30 | - Устанавливать обложку сообщества 31 | - Устанавливать главное фото сообщества/пользователя/беседы/товара/опроса 32 | 33 | ```ts 34 | import { VK } from 'vk-io'; 35 | 36 | const vk = new VK({ 37 | token: process.env.TOKEN 38 | }); 39 | 40 | const attachment = await vk.upload.messagePhoto({ 41 | peer_id: ..., 42 | source: { 43 | value: './cat.jpeg' 44 | } 45 | }); 46 | ``` 47 | 48 | [Более подробно](./upload) 49 | 50 | ## Updates 51 | 52 | Базовый модуль для получения обновлений, поддерживает: 53 | - [Callback API](https://dev.vk.ru/api/callback/getting-started) 54 | - [Bots Long Poll](https://dev.vk.ru/api/bots-long-poll/getting-started) 55 | - [User Long Poll](https://dev.vk.ru/api/user-long-poll/getting-started) 56 | 57 | ```ts 58 | import { VK } from 'vk-io'; 59 | 60 | const vk = new VK({ 61 | token: process.env.TOKEN 62 | }); 63 | 64 | vk.updates.on('message_new', async (context) => { 65 | if (context.text === 'Привет') { 66 | await context.send('Привет!'); 67 | } 68 | }); 69 | 70 | await vk.updates.start(); 71 | ``` 72 | 73 | [Более подробно](./updates) 74 | 75 | ## Collect 76 | 77 | Базовый модуль для работы с коллекциями, позволяет: 78 | - Вызывать множество однотипных методов через [execute](https://dev.vk.ru/method/execute) 79 | - Цепочка вызовов с контролируемым результатом запроса через [execute](https://dev.vk.ru/method/execute) 80 | - Итератор сбора данных с методов которые поддерживают limit/offset 81 | 82 | [Более подробно](./collect) 83 | 84 | ## Keyboard 85 | 86 | Базовый модуль для работы с клавиатурой, предоставляет: 87 | - Абстракцию над [структурой клавиатуры](https://dev.vk.ru/api/bots/development/keyboard) 88 | - Две вариации составления клавиатуры, через сборщик или ручное составление 89 | 90 | ```ts 91 | await api.messages.send({ 92 | message: 'Hey!', 93 | keyboard: Keyboard.builder() 94 | .urlButton({ 95 | label: 'View on site', 96 | url: 'https://coffee.mania/view/coffee' 97 | }) 98 | .callbackButton({ 99 | label: 'Buy a coffee', 100 | payload: { 101 | command: 'buy', 102 | item: 'coffee' 103 | } 104 | }) 105 | .row() 106 | .textButton({ 107 | label: 'Back to the menu', 108 | payload: { 109 | command: 'menu' 110 | } 111 | }) 112 | }); 113 | ``` 114 | 115 | [Более подробно](./keyboard) 116 | 117 | ## Utils 118 | 119 | [Базовые утилиты](./utils) 120 | -------------------------------------------------------------------------------- /packages/vk-io/src/api/request.ts: -------------------------------------------------------------------------------- 1 | import { AbortController } from 'abort-controller'; 2 | import { inspectable } from 'inspectable'; 3 | 4 | import type { API } from './api'; 5 | 6 | import { getExecuteMethod } from '../utils/helpers'; 7 | import type { ICallbackServiceValidate } from '../utils/callback-service'; 8 | import { fetch } from '../utils/fetch'; 9 | 10 | export interface IAPIRequestOptions { 11 | api: API; 12 | 13 | method: string; 14 | params: Record; 15 | 16 | headers?: Record; 17 | } 18 | 19 | export class APIRequest { 20 | public method: string; 21 | public params: Record; 22 | 23 | public headers: Record; 24 | 25 | public retries = 0; 26 | public promise: Promise; 27 | 28 | public resolve!: (value: unknown) => unknown; 29 | 30 | public reject!: (reason: unknown) => unknown; 31 | 32 | public captchaValidate?: ICallbackServiceValidate; 33 | 34 | protected api: API; 35 | 36 | /** 37 | * Constructor 38 | */ 39 | public constructor({ api, method, params = {}, headers = {} }: IAPIRequestOptions) { 40 | this.api = api; 41 | 42 | this.method = method; 43 | this.params = { ...params }; 44 | 45 | this.headers = headers; 46 | 47 | this.promise = new Promise((resolve, reject): void => { 48 | this.resolve = resolve; 49 | this.reject = reject; 50 | }); 51 | } 52 | 53 | /** 54 | * Returns custom tag 55 | */ 56 | public get [Symbol.toStringTag](): string { 57 | return this.constructor.name; 58 | } 59 | 60 | /** 61 | * Returns string to execute 62 | */ 63 | public toString(): string { 64 | return getExecuteMethod(this.method, this.params); 65 | } 66 | 67 | /** 68 | * Sends a request to the server 69 | */ 70 | public async make(): Promise { 71 | const { options } = this.api; 72 | 73 | const params: APIRequest['params'] = { 74 | access_token: options.token, 75 | v: options.apiVersion, 76 | 77 | ...this.params, 78 | }; 79 | 80 | if (options.language !== undefined) { 81 | params.lang = options.language; 82 | } 83 | 84 | const controller = new AbortController(); 85 | 86 | const timeout = setTimeout(() => controller.abort(), options.apiTimeout); 87 | 88 | try { 89 | const response = await fetch(`${options.apiBaseUrl}/${this.method}`, { 90 | method: 'POST', 91 | compress: false, 92 | agent: options.agent, 93 | signal: controller.signal, 94 | headers: { 95 | ...options.apiHeaders, 96 | ...this.headers, 97 | 98 | connection: 'keep-alive', 99 | }, 100 | body: new URLSearchParams(Object.entries(params).filter(({ 1: value }) => value !== undefined)), 101 | }); 102 | 103 | const result = await response.json(); 104 | 105 | return result; 106 | } finally { 107 | clearTimeout(timeout); 108 | } 109 | } 110 | } 111 | 112 | inspectable(APIRequest, { 113 | serialize: ({ method, params }) => ({ 114 | method, 115 | params, 116 | }), 117 | }); 118 | -------------------------------------------------------------------------------- /packages/vk-io/src/structures/contexts/dialog-flags.ts: -------------------------------------------------------------------------------- 1 | import { Context, type ContextDefaultState, type ContextFactoryOptions } from './context'; 2 | 3 | import type { Params } from '../../api'; 4 | 5 | import { kSerializeData } from '../../utils/constants'; 6 | 7 | import { pickProperties } from '../../utils/helpers'; 8 | 9 | export type DialogFlagsContextType = 'dialog_flags'; 10 | 11 | export type DialogFlagsContextSubType = 'dialog_flags_add' | 'dialog_flags_delete'; 12 | 13 | const subTypes: Record = { 14 | 10: 'dialog_flags_delete', 15 | 12: 'dialog_flags_add', 16 | }; 17 | 18 | /* eslint-disable no-bitwise */ 19 | enum DialogFlag { 20 | IMPORTANT = 1 << 0, 21 | UNANSWERED = 1 << 1, 22 | } 23 | /* eslint-enable no-bitwise */ 24 | 25 | export interface IDialogFlagsContextPayload { 26 | peer_id: number; 27 | flags: number; 28 | } 29 | 30 | export type DialogFlagsContextOptions = ContextFactoryOptions; 31 | 32 | export class DialogFlagsContext extends Context< 33 | IDialogFlagsContextPayload, 34 | S, 35 | DialogFlagsContextType, 36 | DialogFlagsContextSubType 37 | > { 38 | public constructor(options: DialogFlagsContextOptions) { 39 | const [eventId, peerId, flags] = options.payload; 40 | 41 | super({ 42 | ...options, 43 | 44 | type: 'dialog_flags', 45 | subTypes: [subTypes[eventId]], 46 | 47 | payload: { 48 | peer_id: peerId, 49 | flags, 50 | }, 51 | }); 52 | } 53 | 54 | /** 55 | * Checks if dialogue is important 56 | */ 57 | public get isImportant(): boolean { 58 | return this.hasFlag(DialogFlag.IMPORTANT); 59 | } 60 | 61 | /** 62 | * Checks if the dialog is unanswered 63 | */ 64 | public get isUnanswered(): boolean { 65 | return this.hasFlag(DialogFlag.UNANSWERED); 66 | } 67 | 68 | /** 69 | * Returns the destination identifier 70 | */ 71 | public get peerId(): number { 72 | return this.payload.peer_id; 73 | } 74 | 75 | /** 76 | * Returns the values of the flags 77 | */ 78 | public get flags(): number { 79 | return this.payload.flags; 80 | } 81 | 82 | /** 83 | * Marks the conversation as answered or unchecked 84 | */ 85 | public markAsAnsweredConversation(params: Params.MessagesMarkAsAnsweredConversationParams): Promise { 86 | return this.api.messages.markAsAnsweredConversation({ 87 | ...params, 88 | 89 | peer_id: this.peerId, 90 | }); 91 | } 92 | 93 | /** 94 | * Marks the conversation as important or removes the mark 95 | */ 96 | public markAsImportantConversation(params: Params.MessagesMarkAsImportantConversationParams): Promise { 97 | return this.api.messages.markAsImportantConversation({ 98 | ...params, 99 | 100 | peer_id: this.peerId, 101 | }); 102 | } 103 | 104 | protected hasFlag(flag: DialogFlag): boolean { 105 | return Boolean(this.flags & flag); 106 | } 107 | 108 | /** 109 | * Returns the custom data 110 | */ 111 | public [kSerializeData](): object { 112 | return pickProperties(this, ['peerId', 'flags', 'isImportant', 'isUnanswered']); 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /packages/scenes/src/contexts/step.ts: -------------------------------------------------------------------------------- 1 | import type { StepSceneHandler } from '../scenes/step.types'; 2 | import { LastAction } from './scene.types'; 3 | import type { IStepContextGoOptions, IStepContextOptions } from './step.types'; 4 | 5 | export class StepSceneContext> { 6 | private context: IStepContextOptions['context']; 7 | 8 | private steps: IStepContextOptions['steps']; 9 | 10 | private stepChanged = false; 11 | 12 | public constructor(options: IStepContextOptions) { 13 | this.context = options.context; 14 | 15 | this.steps = options.steps; 16 | } 17 | 18 | /** 19 | * The first enter to the handler 20 | */ 21 | public get firstTime(): boolean { 22 | const { firstTime = true } = this.context.scene.session; 23 | 24 | return firstTime; 25 | } 26 | 27 | /** 28 | * Returns current stepId 29 | */ 30 | public get stepId(): number { 31 | return this.context.scene.session.stepId || 0; 32 | } 33 | 34 | /** 35 | * Sets current stepId 36 | */ 37 | public set stepId(stepId: number) { 38 | const { session } = this.context.scene; 39 | 40 | session.stepId = stepId; 41 | session.firstTime = true; 42 | 43 | this.stepChanged = true; 44 | } 45 | 46 | /** 47 | * Returns current handler 48 | */ 49 | public get current(): StepSceneHandler | undefined { 50 | return this.steps[this.stepId]; 51 | } 52 | 53 | /** 54 | * Reenter current step handler 55 | * 56 | * ```ts 57 | * ctx.scene.step.reenter(); 58 | * ``` 59 | */ 60 | public async reenter(): Promise { 61 | const { current } = this; 62 | 63 | if (!current) { 64 | await this.context.scene.leave(); 65 | 66 | return; 67 | } 68 | 69 | this.stepChanged = false; 70 | 71 | await current(this.context); 72 | 73 | if (this.context.scene.lastAction !== LastAction.LEAVE && !this.stepChanged) { 74 | this.context.scene.session.firstTime = false; 75 | } 76 | } 77 | 78 | /** 79 | * The go method goes to a specific step 80 | * 81 | * ```ts 82 | * ctx.scene.step.go(3); 83 | * ctx.scene.step.go(3, { 84 | * silent: true 85 | * }); 86 | * ``` 87 | */ 88 | public go(stepId: number, { silent = false }: IStepContextGoOptions = {}): Promise { 89 | this.stepId = stepId; 90 | 91 | if (silent) { 92 | return Promise.resolve(); 93 | } 94 | 95 | return this.reenter(); 96 | } 97 | 98 | /** 99 | * Move to the next handler 100 | * 101 | * ```ts 102 | * ctx.scene.step.next(); 103 | * ctx.scene.step.next({ 104 | * silent: true 105 | * }); 106 | * ``` 107 | */ 108 | public next(options?: IStepContextGoOptions): Promise { 109 | return this.go(this.stepId + 1, options); 110 | } 111 | 112 | /** 113 | * Move to the previous handler 114 | * 115 | * ```ts 116 | * ctx.scene.step.previous(); 117 | * ctx.scene.step.previous({ 118 | * silent: true 119 | * }); 120 | * ``` 121 | */ 122 | public previous(options?: IStepContextGoOptions): Promise { 123 | return this.go(this.stepId - 1, options); 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /docs/examples/simple-keyboard-bot.js: -------------------------------------------------------------------------------- 1 | const { VK, Keyboard } = require('vk-io'); 2 | const { HearManager } = require('@vk-io/hear'); 3 | 4 | const vk = new VK({ 5 | token: process.env.TOKEN, 6 | }); 7 | 8 | const hearManager = new HearManager(); 9 | 10 | vk.updates.on('message_new', (context, next) => { 11 | const { messagePayload } = context; 12 | 13 | context.state.command = messagePayload?.command || null; 14 | 15 | return next(); 16 | }); 17 | 18 | vk.updates.on('message_new', hearManager.middleware); 19 | 20 | // Simple wrapper for commands 21 | const hearCommand = (name, conditions, handle) => { 22 | if (typeof handle !== 'function') { 23 | handle = conditions; 24 | conditions = [`/${name}`]; 25 | } 26 | 27 | if (!Array.isArray(conditions)) { 28 | conditions = [conditions]; 29 | } 30 | 31 | hearManager.hear([(_text, { state }) => state.command === name, ...conditions], handle); 32 | }; 33 | 34 | // Handle start button 35 | hearCommand('start', (context, next) => { 36 | context.state.command = 'help'; 37 | 38 | return Promise.all([ 39 | context.send('Hello!'), 40 | 41 | next(), 42 | ]); 43 | }); 44 | 45 | hearCommand('help', async context => { 46 | await context.send({ 47 | message: ` 48 | My commands list 49 | 50 | /help - The help 51 | /time - The current date 52 | /cat - Cat photo 53 | /purr - Cat purring 54 | `, 55 | keyboard: Keyboard.builder() 56 | .textButton({ 57 | label: 'The help', 58 | payload: { 59 | command: 'help', 60 | }, 61 | }) 62 | .row() 63 | .textButton({ 64 | label: 'The current date', 65 | payload: { 66 | command: 'time', 67 | }, 68 | }) 69 | .row() 70 | .textButton({ 71 | label: 'Cat photo', 72 | payload: { 73 | command: 'cat', 74 | }, 75 | color: Keyboard.PRIMARY_COLOR, 76 | }) 77 | .textButton({ 78 | label: 'Cat purring', 79 | payload: { 80 | command: 'purr', 81 | }, 82 | color: Keyboard.PRIMARY_COLOR, 83 | }), 84 | }); 85 | }); 86 | 87 | hearCommand('cat', async context => { 88 | await Promise.all([ 89 | context.send('Wait for the uploads awesome 😻'), 90 | 91 | context.sendPhotos({ 92 | value: 'https://loremflickr.com/400/300/', 93 | }), 94 | ]); 95 | }); 96 | 97 | hearCommand('time', ['/time', '/date'], async context => { 98 | await context.send(String(new Date())); 99 | }); 100 | 101 | const catsPurring = [ 102 | 'http://ronsen.org/purrfectsounds/purrs/trip.mp3', 103 | 'http://ronsen.org/purrfectsounds/purrs/maja.mp3', 104 | 'http://ronsen.org/purrfectsounds/purrs/chicken.mp3', 105 | ]; 106 | 107 | hearCommand('purr', async context => { 108 | const link = catsPurring[Math.floor(Math.random() * catsPurring.length)]; 109 | 110 | await Promise.all([ 111 | context.send('Wait for the uploads purring 😻'), 112 | 113 | context.sendAudioMessage({ 114 | value: link, 115 | }), 116 | ]); 117 | }); 118 | 119 | vk.updates.start().catch(console.error); 120 | -------------------------------------------------------------------------------- /packages/vk-io/test/resource-resolver.test.ts: -------------------------------------------------------------------------------- 1 | import { deepStrictEqual } from 'node:assert'; 2 | import { describe, it } from 'node:test'; 3 | 4 | import { type IResolvedOwnerResource, type IResolvedTargetResource, resolveResource, VK } from '..'; 5 | 6 | const { TOKEN } = process.env; 7 | 8 | // biome-ignore lint/style/noNonNullAssertion: to be honest, they're just tests 9 | const vk = new VK({ token: TOKEN! }); 10 | 11 | const durovUser: IResolvedTargetResource = { 12 | id: 1, 13 | type: 'user', 14 | }; 15 | 16 | const durovPhoto: IResolvedOwnerResource = { 17 | id: 456264771, 18 | ownerId: 1, 19 | type: 'photo', 20 | }; 21 | 22 | const apiclubGroup: IResolvedTargetResource = { 23 | id: 1, 24 | type: 'group', 25 | }; 26 | 27 | const dataset: [string | number, IResolvedTargetResource | IResolvedOwnerResource][] = [ 28 | [1, durovUser], 29 | ['1', durovUser], 30 | ['id1', durovUser], 31 | ['durov', durovUser], 32 | ['vk.ru/id1', durovUser], 33 | ['[id1|Durov]', durovUser], 34 | ['vk.ru/durov', durovUser], 35 | ['m.vk.ru/id1', durovUser], 36 | ['m.vk.ru/durov', durovUser], 37 | ['https://vk.ru/id1', durovUser], 38 | ['https://vk.ru/durov', durovUser], 39 | ['https://m.vk.ru/id1', durovUser], 40 | ['https://m.vk.ru/durov', durovUser], 41 | ['photo1_456264771', durovPhoto], 42 | ['https://vk.ru/photo1_456264771', durovPhoto], 43 | ['https://m.vk.ru/photo1_456264771', durovPhoto], 44 | ['https://vk.ru/durov?z=photo1_456264771', durovPhoto], 45 | ['https://m.vk.ru/durov?z=photo1_456264771%2Falbum1_0%2Frev', durovPhoto], 46 | ['https://m.vk.ru/photo1_456264771?list=album1_0&z=photo1_456264771%2Falbum1_0', durovPhoto], 47 | [-1, apiclubGroup], 48 | ['club1', apiclubGroup], 49 | ['[club1|VK API]', apiclubGroup], 50 | ['[public1|APICLUB]', apiclubGroup], 51 | ['public1', apiclubGroup], 52 | [ 53 | 'apiclub', 54 | { 55 | id: 166562603, 56 | type: 'group', 57 | }, 58 | ], 59 | [ 60 | 'app1', 61 | { 62 | id: 1, 63 | type: 'application', 64 | }, 65 | ], 66 | [ 67 | 'albums1', 68 | { 69 | id: 1, // User ID 70 | type: 'albums', 71 | }, 72 | ], 73 | [ 74 | 'album1_0', 75 | { 76 | id: 0, 77 | ownerId: 1, 78 | type: 'album', 79 | }, 80 | ], 81 | [ 82 | 'https://vk.ru/id1?w=wall1_32279', 83 | { 84 | id: 32279, 85 | ownerId: 1, 86 | type: 'wall', 87 | }, 88 | ], 89 | [ 90 | 'https://vk.ru/club1?w=wall-1_49296', 91 | { 92 | id: 49296, 93 | ownerId: -1, 94 | type: 'wall', 95 | }, 96 | ], 97 | ]; 98 | 99 | describe('resolveResource', (): void => { 100 | for (const [resource, result] of dataset) { 101 | it( 102 | `should resolve correctly "${resource}"`, 103 | { timeout: 60_000, skip: TOKEN === undefined ? 'not set env TOKEN=' : undefined }, 104 | async (): Promise => { 105 | const resolvedResource = await resolveResource({ 106 | resource, 107 | api: vk.api, 108 | }); 109 | 110 | deepStrictEqual(resolvedResource, result); 111 | }, 112 | ); 113 | } 114 | }); 115 | --------------------------------------------------------------------------------