R
R
Roman2019-10-13 22:46:12
JavaScript
Roman, 2019-10-13 22:46:12

A module for structuring command processing by bots for TELEGRAM, DISCORD, VK, etc. (similar to the middleware system in express.js)?

For personal needs, I decided to write a universal module for structuring command processing for bots like the middleware system in express.js. The idea turned out quite well, but in the process several questions / desires arose:
1. I was looking for ready-made solutions for this purpose, but I did not find suitable ones. If anyone knows of modules that implement exactly this functionality, please tell me.
2. The index.js module itself, although it fulfills its task, is haunted by the feeling that I am going the wrong way. Until now, I have not written such things, I have very little idea how to do them correctly. If anyone can suggest links to good literature in which this issue is chewed in detail, I will be grateful.
3. If you can’t tell on p.p. 1, 2, then I will be grateful for the review of the logic of the modulewith a hint / examples of where and how it could be done better, more correctly.

The versatility of this module is due to the ability to use it to process user commands in telegrams, discord, VK, IRC, and even in your own, self-written chats.

the index.js module itself
'use strict';
const re = /.*/;
class Router {

  constructor(options={}){
    this.listeners = [];
    this.options = {
      name:"undefined123",
      // prefix: "!",
      ...options
    };
    this.parent = this;
    this.templatename = "!";
    this.template = re;
  }

  /**
   * Метод позволяет установить обработчики для команд (частей составляющих команды)
   * 
   * @param  {String} - задает имя, под которым будет находится текущая часть
   *                    команды в context.params. По данному имени в обработчике
   *                    можно будет обратиться к соответствующей части команды
   * @param  {String|Number|Function|RegExp} - задает шаблон соответствия для части 
   *                    команды. Шаблон может быть строкой, числом, функцией и 
   *                    регулярным выражением. В случае если шаблон это функция, то
   *                    она должна возвращать true если текущая часть команды
   *                    соответствует условиям и false если нет
   * @param  {...[Function|Router]} - массив обработчиков, может быть функцией или
   *                    объектом класса Router. При вызове функции обработчика ей
   *                    передаются 2 параметра: context{Object} и next{Function}. 
   *                    В контексте содержатся свойства:
   *                    params - объект с именованными значениями частей введенной команды
   *                             например если была введена команда:
   *                             /help join
   *                             в params будет:
   *                             {
   *                               "prefix": "/", 
   *                               "заданное имя команды": "help",
   *                               "заданное имя части": "join"
   *                             }
   *                    list - массив, состоящий из частей команды.
   *                             например если была введена команда:
   *                             /help join
   *                             в params будет:
   *                             ["/","help","join"]
   *                    current - значение части команды обрабатываемое в данный момент
   */
  on(templatename, template, ...listeners){
    
    listeners = validateArguments(templatename, template, ...listeners);
    
    if (!listeners)
      return;

    listeners = listeners.map(item=>{
      if (typeof item === "function")
        return item;

      item.init(this, templatename, template);
      return function(...args){ item.route(...args); };
    })

    listeners.forEach(handler=>{
      this.listeners.push({
        templatename: templatename,
        template: template,
        handler: handler
      });
    })


  }

  /**
   * Метод позволяет установить обработчики для команд (частей составляющих команды)
   * не проверяющие на соответствие шаблону текущей части команды. Такие обработчики
   * будут выполнены всегда, если выполнение дойдет до роутера в котором они объявлены.
   * 
   * @param  {...[Function|Router]} - массив обработчиков. аналогичен описанному в методе Route.on
   */
  use(...listeners){
    this.on(undefined, re, ...listeners);
  }

  /**
   * Для внутреннего использования
   */
  init(parent, templatename, template){
    this.parent = parent;
    this.templatename = templatename;
    this.template = template;
  };
  /**
   * Метод инициирует процесс обработки пришедшей команды. В результате
   * процесса будут вызваны соответствующие команде и ее частям обработчики
   * @param  {String} - введенная команда
   */
  parse(command){
    if (!command || typeof command !== "string") return;
    
    const list = [];
    const prefix = command[0];
    if( this.options.prefix ){
      if (Array.isArray(this.options.prefix) && !this.options.prefix.includes(prefix)) return;
      if (typeof this.options.prefix === "string" && this.options.prefix !== prefix) return;
      list.push(prefix);
    }
    
    list.push(...command.substr(1).split(/\s+/));
    
    if( !list || !list.length ) return;

    this.route({
      params: { prefix: prefix },
      list: list,
      index: 0,
      current: list[0],
      queues: []
    },()=>{});
  }

  /**
   * В случае если в качестве обработчика был передан объект класса Route
   * процессе обработки поступающих команд в качестве обработчика будет
   * вызываться этот метод переданного объекта
   * @param  {Object} - объект с контекстом обработки команды
   * @param  {Function} - функция next() для передачи эстафеты следующему
   *                      обработчику. В случае если next не будет вызвана
   *                      процесс обработки текущей команды будет прерван.
   */
  route(ctx, next){
    const context = {...ctx};
    context.index++;
    const current = context.current = context.list[context.index];
    let index = 0;
    let item = this.listeners[index];
    

    const _start = ()=>{
      setImmediate(()=>{
        // console.log("listener:", item);
        
        if (item.template instanceof RegExp) {
          
          if (!item.template.test(current))
            return _next();

        } if (typeof item.template === "function") {
          
          if (!item.template(current))
            return _next();

        } if (typeof item.template === "string" || typeof item.template === "number") {
          
          if (current != item.template)
            return _next();

        }

        if(item.templatename)
          context.params[item.templatename] = context.current;

        item.handler(context,()=>{
          _next();
        });
  
      });
    }

    const _next = ()=>{
      index++;
      item = this.listeners[index];

      if(index >= this.listeners.length )
        return next();

      _start();
    }

    _start();

  }
}



module.exports = (options)=>{
  return new Router(options);
};




/**
 * Далее временные костыли, их можно не смотреть
 */

function validateTemplateName(templatename){
  if (templatename === undefined)
    return true;

  if (templatename && typeof templatename === "string")
    return true;
    
  return false;
}

function validateTemplate(template){
  if (!template &&
    typeof template !== "string" && 
    typeof template !== "function" && 
    !Array.isArray(template) && 
    !(template instanceof RegExp))
    return false;
  
  return true;
}

function validateListeners(...listeners){
  if (!listeners)
    return false;

  listeners = listeners.filter(h=>{
    if (typeof h === "function") 
      return true;

    if (h instanceof Router) 
      return true;

    return false;
  });

  if(!listeners.length)
    return false;
  
  return listeners;
}

function validateArguments(templatename, template, ...listeners){
  if (!validateTemplateName(templatename))
    return;
  
  if (!validateTemplate(template))
    return;
  
  return validateListeners(...listeners);
}


The main program file in which the bot and my module should connect
const Router = require("bot-commands-router");
const router = Router({
  name:"!",
  prefix: ["/","!"] 
});


const help = require("./routers/help.js");

router.use((ctx,next)=>{
  // имитируем задержку в обработке
  setTimeout(function() {
    console.log("test.js router.use", ctx.params, ctx.current, 111);
    next();
  }, Math.floor(Math.random()*500));
});

router.on("command", "help", help);

router.use((ctx,next)=>{
  // имитируем задержку в обработке
  setTimeout(function() {
    console.log("test.js router.use", ctx.params, ctx.current, 666);
    next();
  }, Math.floor(Math.random()*500));
});


// имитируем одновременно пришедшие боту команды для проверки 
// ассинхронности выполнения обработчиков вывод будет отличаться префиксом
router.parse("/help join");
router.parse("!help join");


router with help command handlers
const Router = require("bot-commands-router");
const router = Router({name:"help"});

router.on("target", "join", 
  (ctx,next)=>{
    // имитируем задержку в обработке
    setTimeout(function() {
      console.log("help.js router.on", ctx.params, ctx.current, 222);
      next();
    }, Math.floor(Math.random()*500));
  },
  (ctx,next)=>{
    // имитируем задержку в обработке
    setTimeout(function() {
      console.log("help.js router.on", ctx.params, ctx.current, 333);
      next();
    }, Math.floor(Math.random()*500));
  }
);

router.use((ctx,next)=>{
    // имитируем задержку в обработке
    setTimeout(function() {
      console.log("help.js router.гыу", ctx.params, ctx.current, 444);
      next();
    }, Math.floor(Math.random()*500));
});

router.on("target", "join", (ctx,next)=>{
    // имитируем задержку в обработке
    setTimeout(function() {
      console.log("help.js router.on", ctx.params, ctx.current, 555);
      next();
    }, Math.floor(Math.random()*500));
});

// router.use();

module.exports = router;

Answer the question

In order to leave comments, you need to log in

Didn't find what you were looking for?

Ask your question

Ask a Question

731 491 924 answers to any question