Vk-io: Arquitetura: Qual é a melhor maneira "oficial" de organizar um bot no VK-IO

Criado em 14 dez. 2020  ·  2Comentários  ·  Fonte: negezor/vk-io

Olá, eu assisti sua análise de bots no YouTube, e todos eles tiveram um grande problema: todo o código é um footcloth para 5k linhas. Portanto, tenho uma pergunta: como você vê que um projeto com VK-IO deve ser organizado. Porque em sua forma atual é um footcloth ou um index + config + um monte de arquivos que se parecem com isso:

Frame 4

/** <strong i="8">@filename</strong>: config.ts */
export const vk = new VK({ /* ... */ })
/** <strong i="11">@filename</strong>: feat1.ts */
import { vk } from "./config"

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

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

E também tem problemas:

  1. A ordem de inclusão de recursos no índice afeta o comportamento do bot
  2. Você pode esquecer de conectar um recurso
  3. Para facilitar o trabalho, a instância inicializada do VK deve estar na configuração
  4. Difícil acompanhar as relações entre os recursos

Adoraria ouvir sua resposta para isso e duplicá-la muito explicitamente na documentação e possivelmente README.md

vk-io

Comentários muito úteis

Não vou dizer que a abordagem “oficial”, vou apenas compartilhar minhas preferências pessoais e um pouco da parte prática.

Projeto geral

Eu prefiro usar a abordagem monorepo para organizar módulos distribuídos (a biblioteca realmente a usa). Você pode pegar um modelo pronto para serviços e separar a implementação do bot da biblioteca usando abstrações (já que qualquer alteração importante exigirá muita atenção para adaptá-la). Também é uma boa ideia usar máquinas virtuais para condições idênticas em desenvolvimento e produção, por exemplo, o Docker ajudará aqui.

Arquitetura

As dependências de bot devem ser explícitas, ou seja, sem adicionar "recursos" com uma única importação, caso contrário, um verdadeiro pesadelo de depuração começará aqui. Código abstrato:

// 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);
    });

Coisas importantes do código acima:

  1. Implementado o pacote básico @my-project/core , que contém as coisas necessárias para o bot.
  2. Os comandos usam parâmetros, realizando assim Dispatcher . Por que isso é necessário? Tudo é muito simples - você pode chamar o comando de qualquer lugar com os parâmetros especificados. A partir do texto, analisamos os argumentos descritos no comando de qualquer maneira conveniente, e os botões no teclado já estão definidos com eles e são endereçados ao comando que precisamos. Assim, evitamos a duplicação de lógica e organizamos a validação dos argumentos. Por exemplo, chamando um comando de outro:
export const dndCommand = new Command({
    // ...
    handler(context) {
        return context.commander.enter('random', {
            params: {
                min: 1,
                max: 20
            }
        });
    }
});
  1. Cada módulo gerenciador que foi utilizado tem sua própria área de responsabilidade e dá uma compreensão clara do que é usado e disponível.

Conclusão

Essa é a abordagem que usei em meus bots e acabou sendo bastante conveniente implementar desde bots simples a bots complexos. Na melhor das hipóteses, o pacote @my-project/core deve ser apenas um alias de uma biblioteca que já implementou e testou tudo, e o arquivo fica assim:

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

export { ViewerManager } from './middlewares';

Em casos de alterações na biblioteca, será então possível substituir uma das interfaces. Mas ninguém proíbe manter toda a lógica apenas para o projeto.

Existe outra implementação interessante da lógica do bot nos ganchos, que é usada, por exemplo, em React ou Vue , você pode poke-lo ao vivo usando este código .

Todos 2 comentários

Não vou dizer que a abordagem “oficial”, vou apenas compartilhar minhas preferências pessoais e um pouco da parte prática.

Projeto geral

Eu prefiro usar a abordagem monorepo para organizar módulos distribuídos (a biblioteca realmente a usa). Você pode pegar um modelo pronto para serviços e separar a implementação do bot da biblioteca usando abstrações (já que qualquer alteração importante exigirá muita atenção para adaptá-la). Também é uma boa ideia usar máquinas virtuais para condições idênticas em desenvolvimento e produção, por exemplo, o Docker ajudará aqui.

Arquitetura

As dependências de bot devem ser explícitas, ou seja, sem adicionar "recursos" com uma única importação, caso contrário, um verdadeiro pesadelo de depuração começará aqui. Código abstrato:

// 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);
    });

Coisas importantes do código acima:

  1. Implementado o pacote básico @my-project/core , que contém as coisas necessárias para o bot.
  2. Os comandos usam parâmetros, realizando assim Dispatcher . Por que isso é necessário? Tudo é muito simples - você pode chamar o comando de qualquer lugar com os parâmetros especificados. A partir do texto, analisamos os argumentos descritos no comando de qualquer maneira conveniente, e os botões no teclado já estão definidos com eles e são endereçados ao comando que precisamos. Assim, evitamos a duplicação de lógica e organizamos a validação dos argumentos. Por exemplo, chamando um comando de outro:
export const dndCommand = new Command({
    // ...
    handler(context) {
        return context.commander.enter('random', {
            params: {
                min: 1,
                max: 20
            }
        });
    }
});
  1. Cada módulo gerenciador que foi utilizado tem sua própria área de responsabilidade e dá uma compreensão clara do que é usado e disponível.

Conclusão

Essa é a abordagem que usei em meus bots e acabou sendo bastante conveniente implementar desde bots simples a bots complexos. Na melhor das hipóteses, o pacote @my-project/core deve ser apenas um alias de uma biblioteca que já implementou e testou tudo, e o arquivo fica assim:

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

export { ViewerManager } from './middlewares';

Em casos de alterações na biblioteca, será então possível substituir uma das interfaces. Mas ninguém proíbe manter toda a lógica apenas para o projeto.

Existe outra implementação interessante da lógica do bot nos ganchos, que é usada, por exemplo, em React ou Vue , você pode poke-lo ao vivo usando este código .

Obrigado por uma resposta muito boa e detalhada. E eu gostaria de pedir que você deixe o assunto em aberto para que outras pessoas possam ler também

Esta página foi útil?
0 / 5 - 0 avaliações

Questões relacionadas

Bobrovskih picture Bobrovskih  ·  3Comentários

ogmishanya picture ogmishanya  ·  4Comentários

alexey2baranov picture alexey2baranov  ·  8Comentários

T1MOXA picture T1MOXA  ·  20Comentários

nitreojs picture nitreojs  ·  3Comentários