Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Архитектура: Какой официально оптимальный способ организации бота на VK-IO #387

Open
AlexXanderGrib opened this issue Dec 14, 2020 · 2 comments
Labels
package: vk-io Issues related to vk-io

Comments

@AlexXanderGrib
Copy link
Contributor

AlexXanderGrib commented Dec 14, 2020

Привет, смотрел твой разбор ботов на ютубе, и у них у всех была одна большая проблема: весь код - портянка на 5к строк. Поэтому у меня созрел вопрос: как ты видишь, что должен быть устроен проект с VK-IO. Потому что в нынешнем виде это либо портянка, либо индекс + конфиг + куча файлов, который выглядит примерно вот так:

Frame 4

/** @filename: config.ts */
export const vk = new VK({ /* ... */ })
/** @filename: feat1.ts */
import { vk } from "./config"

vk.updates.on('message', async(ctx, next) => {
  /* code */
})
/** @filename: index.ts */
import { vk } from "./config"
import "./feat1"

vk.updates.startPolling().then(() => console.log("Bot works!"))

И у него тоже есть проблемы:

  1. Порядок поключения фич в индексе влияет на поведение бота
  2. Можно забыть подключить фичу
  3. Чтобы было удобно работать, инициализированый экземпляр ВК должен лежать в конфиге
  4. Сложно уследить за связями между фичами

Хотелось бы услышать твой ответ на это и дублирование его в очень явном виде в документацию, и возможно в README.md

@AlexXanderGrib AlexXanderGrib added the package: vk-io Issues related to vk-io label Dec 14, 2020
@negezor
Copy link
Owner

negezor commented Dec 15, 2020

Не скажу что "официальный" подход, просто поделюсь личными предпочтениями и немного практической части.

Общий дизайн

Я предпочитаю использовать подход monorepo для организации распределённых модулей (собственно его использует библиотека). Можно взять уже готовый шаблон для сервисов, и отделить реализацию бота от библиотеки с помощью абстракций (так как любое критическое изменения потребует большого внимания для его адаптирования). Так же по-хорошему стоит использовать виртуальные машины для идентичных условий в разработке и продакшене, здесь поможет например Docker.

Архитектура

Зависимости бота должны быть явными, т.е. никаких добавлений "фич" с помощью одного импорта, иначе тут начнётся сущий кошмар отладки. Абстрактный код:

// commands/random.ts
import { Command } from '@my-project/core';
import { getRandomIntegerInRange } from '@my-project/utils';

export const randomCommand = new Command({
    slug: 'random',
    
    aliases: [
        'рандом',
        'random'
    ],

    description = 'рандмоное число в промежутке';

    arguments: [
        {
            type: 'integer',
            key: 'min',
            label: 'минк/макс',
            default: null
        },
        {
            type: 'integer',
            key: 'max',
            label: 'минк/макс',
            default: null
        }
    ],

    handler(context) {
        // Работаем с аргументами, а не текстом
        let { min = null, max = null } = context.commander.params;

        if (min === null && max === null) {
            min = 0;
            max = 100;
        } else if (max === null) {
            max = min;
            min = 0;
        }

        const result = getRandomIntegerInRange(min, max);

        return context.answer({
            text: `число в промежутке ${min} - ${max}: ${result}`
        });
    }
});

// commands/index.ts
export * from './random';

// bot.ts
import {
    Bot,

    SessionManager,
    RedisSessionStorage,

    RateLimitManager,

    CommanderManager
} from '@my-project/core';

import * as commands from './commands';

const sessionManager = new SessionManager({
    storage: new RedisSessionStorage({})
});

const rateLimitManager = new RateLimitManager({
    maxPerSecond: 1
});

const commanderManager = new CommanderManager();

for (const command of Object.values(commands)) {
    commanderManager.add(command);
}

const bot = new Bot({
    // ...options
});

// Это может быть кастомная цепочка middleware в боте
bot.incoming.on('message', sessionManager.middleware);
bot.incoming.on('message', rateLimitManager.middleware);
bot.incoming.on('message', commanderManager.middleware);

bot.start()
    .then(() => {
        console.log('Bot started', error);
    })
    .catch((error: Error) => {
        console.error('Error starting bot', error);

        process.exit(1);
    });

Важные вещи из кода выше:

  1. Реализован базовый пакет @my-project/core, в котором находятся вещи необходимые для бота.
  2. Команды используют параметры, тем самым реализуя Dispatcher. Зачем же это нужно? Всё очень просто — можно вызвать команду из любого места с указанными параметрами. Из текста мы парсим любым удобным способом аргументы которые описаны в команде, а кнопки в клавиатуре просто задаются уже с ними и адресуются к нужной нам команде. Тем самым мы избежали дублирование логики и организовали валидацию аргументов. Например вызов одной команды из другой:
export const dndCommand = new Command({
    // ...
    handler(context) {
        return context.commander.enter('random', {
            params: {
                min: 1,
                max: 20
            }
        });
    }
});
  1. Каждый модуль менеджера который был использован, имеет свою зону ответственности и даёт чёткое понимание, что использовано и доступно.

Заключение

Это подход я использовал в моих ботах, и он оказался вполне удобным для реализации от простых до сложных ботов. В лучшем случае пакет @my-project/core должен быть только алиасом библиотеки которая уже всё реализовала и протестировала, а файл выглядит следующим образом:

export { Bot, Command } from 'super-bot-library';

export { ViewerManager } from './middlewares';

В случаях изменений в библиотеке можно тогда будет заменить один из интерфейсов. Но никто не запрещает держать всё логику только для проекта.

Есть ещё интересный вариант реализации логики бота на хуках, который применяется допустим в React или Vue, потыкать вживую можно используя этот код.

@AlexXanderGrib
Copy link
Contributor Author

Спасибо, очень хороший и развёрнутый ответ. И я бы хотел попросить, тебя оставить issue открытым, чтобы другие люди тоже смогли прочитать

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
package: vk-io Issues related to vk-io
Projects
None yet
Development

No branches or pull requests

2 participants