lastuniverse
@lastuniverse
Всегда вокруг да около IT тем

Модуль для структуризации обработки команд ботами для ТЕЛЕГРАМ, ДИСКОРД, VK и др. (наподобие системы middleware в express.js)?

Для личных надобностей решил написать универсальный модуль для структуризации обработки команд ботам наподобие системы middleware в express.js. Задумка вполне себе получилась, но в процессе возникло несколько вопросов/желаний:
1. Искал уже готовые решения для этой цели, но подходящих не нашел. Если кто знает модули реализующие именно такой функционал, подскажите пожалуйста.
2. Сам модуль index.js хоть и выполняет свою задачу, но преследует ощущение что иду не верным путем. До сего дня такие вещи не писал, очень слабо себе представляю как их делать правильно. Если кто подскажет ссылки на хорошую литературу в которой подробно разжеван этот вопрос, буду благодарен.
3. Если не можете подсказать по п.п. 1, 2, то буду признателен на ревью логики модуля с подсказкой/примерами того где и как можно было сделать лучше, правильнее.

Универсальность данного модуля обусловлена возможностью использовать его для обработки команд пользователей в телеграм, дискорд, VK, IRC и даже в собственных, самописных чатах.

сам модуль index.js
'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);
}


Главный файл программы в котором должны подключиться бот и мой модуль
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");


роутер с обработчиками команды help
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;
  • Вопрос задан
  • 107 просмотров
Пригласить эксперта
Ваш ответ на вопрос

Войдите, чтобы написать ответ

Войти через центр авторизации
Похожие вопросы