Desarrollo de bots: Escriba del Muro. Parte 3

Imagen de un hombre escribiendo sobre un papel con estilo lineart, generado por la inteligencia artificial de bing

El proyecto del escriba del muro es un experimento para desarrollar un medio para la creación de formularios dinámicos con administración manual para la dinamización de canales e interacción con los usuarios. Lo he ido creando al vuelo, por lo que objetivos y funciones pueden cambiar en cada entrada.

Introducción

Mientras he ido desarrollando mas el bot, he encontrado también errores y cosas que no son errores, sino situaciones no controladas y que pueden arruinar la experiencia de uso. Mientras he agregado mas usuarios que me ayudaron a probar el sistema, le han encontrado problema tras problema, pero en general cumple su cometido.

Las tecnologías utilizadas son las siguientes:

Escriba del Muro

El escriba del muro es un proyecto sencillo que busca poner en práctica, no solo la programación sino también la administración y despliegue de proyectos. El objetivo principal es utilizar el gestor de procesos PM2 y el panel de línea de comandos para poder ejecutar los procesos necesarios para el correcto funcionamiento del sistema. Además, cuenta con el soporte de Sequelize ORM, que permite una fácil migración de sistemas de base de datos.Este proyecto ofrece la flexibilidad de utilizar tanto MySQL como SQLite, dependiendo de las necesidades o preferencias del usuario final. Gracias a Sequelize ORM, se simplifica el proceso de migración de sistemas de base de datos, lo cual facilita la administración del sistema en general.El uso de PM2 como gestor de procesos permite garantizar un funcionamiento eficiente y estable del sistema. Esto, junto con el panel de línea de comandos, proporciona una interfaz fácil de usar para ejecutar y supervisar los procesos necesarios.En resumen, el escriba del muro es un proyecto completo que combina programación, administración y despliegue de proyectos. Con su capacidad para utilizar diferentes sistemas de base de datos y su fácil sistema de migración, se adapta a las necesidades y preferencias de cada usuario.

Para trabajar de mejor manera, practicar la programación con clases y facilitar la depuración y creación de nuevas funciones, he escrito un par de clases según la necesidad: persistencia, sesión y sequelize.

Sequelize

Esta clase permite inicializar y gestionar la base de datos y establecer las relaciones entre las tablas. La tabla mas importante y que actualmente es la única que se utiliza debido a las optimizaciones que aprendí a hacer al sistema, es la tabla de usuarios.

Nombre del campoTipo de datoPKNotas
id_usuarioIntegerSi
codigo_usuarioBIG INTNo*
nombre_usuarioStringNo
nombresStringNo
apellidosStringNo
idiomaStringNo
comandos_usuarioStringNo
variables_usuarioStringNo
codigo_canalBIG INTNo*
Tabla Usuarios

Nota: Para el código de usuario y el código del canal utilizo bigint en lugar de integer porque en los cambios de la API de Telegram, advirtieron que debido al aumento de usuarios en la plataforma, era necesario tener IDs más grandes, por lo que el tipo de datos integer no podría contener el ID completo.

sequelize.define("usuarios",{
	id_usuario:{
		type: Sequelize.INTEGER,
		primaryKey: true,
		allowNull: false,
		unique: true,
		autoIncrement: true
	},
	codigo_usuario:{
		type: Sequelize.BIGINT,
		unique: true
	},
	nombre_usuario:{
		type: Sequelize.STRING,
		allowNull:false
	},
	nombres:{
		type: Sequelize.STRING,
		allowNull:true
	},	
	apellidos:{
		type: Sequelize.STRING,
		allowNull:true
	},
	idioma:{
		type: Sequelize.STRING,
	},
	comando_usuario:{
		type: Sequelize.STRING,
	},
	variables_usuario:{
		type:Sequelize.STRING
	},
    codigo_canal:{
        type:Sequelize.BIGINT,
        allowNull:true
    }
})

A modo de optimización, existen dos campos en la tabla Usuarios, que se reciclan para evitar la creación de una tabla Mensajes, Comandos y Variables: comandos_usuario y variables_usuario

El campo comandos_usuario almacena una cadena json que guarda el comando que se encuentra activo en la sesión y se utiliza para tener una persistencia que evita la necesidad de crear más tablas de las necesarias.

El campo variables_usuario almacena una cadena json que guarda las variables necesarias para la persistencia del comando en curso, almacenando los datos requeridos, que no siempre suelen ser solo un campo.

Persistencia

Esta clase permite controlar los procesos relacionados con la interacción con la base de datos, esto permite al desarrollador agregar nuevas funciones sin afectar a los demás procesos ya existentes.

Los métodos disponibles son los siguientes:

registrar_mensaje(ctx)

Esta función sirve de concentrador de mensajes. Debe ser llamada siempre para poder hacer las siguientes validaciones:

  • Si el usuario existe
  • Si el usuario no existe
  • Si el usuario existe y esta vinculado al canal
  • Si el usuario no existe y esta vinculado al canal
  • Si el usuario existe y no esta vinculado al canal

Esta función también permite controlar las actualizaciones de estado, las cuales traen información sobre entrada y salida al canal vinculado al Bot.

async registrar_mensaje(ctx) {
		//para mensajes
		if (ctx.update.message) {
			//paso 1, validar si el usuario existe en la tabla de usuarios
			if (await this.validar_usuario(ctx.update.message.from.id) == false) {
				//si el usuario no existe, entonces se registran sus datos en la tabla
				this.registrar_usuario(ctx.update.message.from.id, ctx.update.message.from.username, ctx.update.message.from.first_name, ctx.update.message.from.last_name, ctx.update.message.from.language_code)
			}
			//paso 2, validar si el usuario ya se encuentra registrado en el canal
			if (await this.validar_usuario_canal(ctx.update.message.from.id, process.env.CHANNEL) == false) {
				console.log("el usuario no se encuentra en el canal, aunque ya deberia estar en la base de datos")
			}
		}
		//para actualizaciones de estado
		//paso 1 determinar si el mensaje es una actualizacion de nuevo miembro del canal y actualizar al usuario, si existe
		if (ctx.update.chat_member) {
			//significa que es un estado de anuncio de nuevo miembro de chat
			//console.log(ctx.update.chat_member)
			//comprobar si el usuario ya existe
			if(this.validar_usuario(ctx.update.chat_member.from.id)==false){
				console.log("el usuario se ha suscrito, pero no registrado en la base de datos")
				console.log("registro al usuario debido a que no existe en el canal y tampoco ha sido registrado anteriormente")
				this.registrar_usuario(ctx.update.chat_member.from.id, ctx.update.chat_member.from.username, ctx.update.chat_member.from.first_name, ctx.update.chat_member.from.last_name, ctx.update.chat_member.from.language_code)
			}else{
				console.log("el usuario ya esta en el sistema")
			}
			//comprobar si el usuario ya se ha suscrito al canal
			if (ctx.update.chat_member.new_chat_member.status == "member") {
				console.log("El usuario se ha suscrito")
				//console.log(await this.validar_usuario_canal(ctx.update.chat_member.from.id, process.env.CHANNEL))
				if (await this.validar_usuario_canal(ctx.update.chat_member.from.id, process.env.CHANNEL) == false) {
					console.log("actualizo el canal del usuario debido a que se ha registrado en el canal")
					this.registrar_usuario_canal(ctx.update.chat_member.from.id, process.env.CHANNEL)
					bot.api.sendMessage(ctx.update.chat_member.from.id, "¡Genial! Ahora podremos escribir en el «Muro de los Escritores».");
					bot.api.sendMessage(ctx.update.chat_member.from.id, "Para continuar, elije cualquiera de los comandos disponibles.\nSiempre puedes pedir ayuda con el comando /ayuda o escribir un mensaje al admin mediante el comando de /soporte");
				}
			}

			if (ctx.update.chat_member.new_chat_member.status == "left") {
				console.log("El usuario se ha desuscrito")
				if (await this.validar_usuario_canal(ctx.update.chat_member.from.id, process.env.CHANNEL) == true) {
					//si se ha desuscrito y esta en la base, eliminarlo de la base
					this.eliminar_usuario_canal(ctx.update.chat_member.from.id, process.env.CHANNEL)
					bot.api.sendMessage(ctx.update.chat_member.from.id, "Es una lastima que te hayas desuscrito.\nSi te molestaba la cantidad de notificaciones, podrias silenciarlas.");
					bot.api.sendMessage(ctx.update.chat_member.from.id, "Pero no te preocupes, puedes renovar tu suscripcion para poder volver a publicar en el @muro_escritores");
				}
			}
		}
    const before = Date.now(); // milliseconds
		const { models } = sequelize
		/*models.usuarios.create({
			codigo_usuario:"1231235"
		})*/
		const after = Date.now(); // milliseconds
		console.log(`Response time: ${after - before} ms`);


	}

validar_usuario(codigo_usuario)

Esta función valida si el usuario se encuentra registrado en la base de datos, devuelve verdadero si existe y falso si no.

	async validar_usuario(codigo_usuario) {
		const { models } = sequelize

		const existe = await models.usuarios.findOne({
			where: {
				codigo_usuario: codigo_usuario
			}
		})
		if (existe === null) {
			return false
		} else {
			return true
		}

registrar_usuario(codigo_usuario, nombre_usuario, nombres, apellidos, idioma)

Esta función permite agregar usuarios a la base de datos.

	registrar_usuario(codigo_usuario, nombre_usuario, nombres, apellidos, idioma) {
		const { models } = sequelize
		models.usuarios.create({
			codigo_usuario: codigo_usuario,
			nombre_usuario: nombre_usuario,
			nombres: nombres,
			apellidos: apellidos,
			idioma: idioma
		})
	}

validar_usuario_canal(codigo_usuario,codigo_canal)

Esta función permite validar si el usuario se encuentra registrado en el canal. Devuelve verdadero si es cierto y falso si no.

/**
	 * # Validar usuario canal
	 * 
	 * Valida si el usuario se encuentra registrado en el canal
	 */
	async validar_usuario_canal(codigo_usuario, codigo_canal) {
		const { models } = sequelize

		const existe = await models.usuarios.findOne({
			where: {
				codigo_usuario: codigo_usuario,
				codigo_canal: codigo_canal
			}
		})

		if (existe === null) {
			return false
		} else {
			return true
		}
	}

registrar_usuario_canal(codigo_usuario,codigo_canal)

Esta función permite registrar a un usuario en el canal.

registrar_usuario_canal(codigo_usuario, codigo_canal) {
		const { models } = sequelize

		models.usuarios.update({
			codigo_canal: codigo_canal
		}, {
			where: {
				codigo_usuario: codigo_usuario
			}
		})
	}

eliminar_usuario_canal(codigo_usuario)

Esta función permite eliminar un usuario cuando se desvincula de un canal.

eliminar_usuario_canal(codigo_usuario) {
		const { models } = sequelize

		models.usuarios.update({
			codigo_canal: ""
		}, {
			where: {
				codigo_usuario: codigo_usuario
			}
		})
	}

Sesion

Esta clase permite controlar el flujo de sesión.

verificar_sesion(codigo_usuario)

Esta función permite comprobar si hay alguna sesión en curso revisando el campo comando_usuario de la tabla usuarios. Devuelve true si la sesión existe y false si no.

async verificar_sesion(codigo_usuario){
	console.log("verificando si el usuario tiene sesion")
		const { models } = sequelize

		const existe=await models.usuarios.findOne({
			where:{
				codigo_usuario:codigo_usuario,
				comando_usuario:{
					[Op.not]:""
				}
			}
		})

		if(existe=== null){
			return false
		}else{
			return true
		}
		
	}

crear_sesion(codigo_usuario,comando)

Esta funcion permite crear una sesion mediante almacenar un comando en el campo comando_usuario de la tabla usuarios.

crear_sesion(codigo_usuario,comando){
		const {models} = sequelize

		const variables={
			username:""
		}

		models.usuarios.update({
			comando_usuario:comando,
			variables_usuario: JSON.stringify(variables)
		},{
			where:{
				codigo_usuario:codigo_usuario
			}
		})
	}

cancelar_sesion(codigo_usuario)

Esta función permite borrar el contenido del campo comando_usuario de la tabla usuarios

cancelar_sesion(codigo_usuario){
		const { models } = sequelize

		models.usuarios.update({
			comando_usuario:"",
			variables_usuario:""
		},{
			where:{
				codigo_usuario:codigo_usuario
			}
		})
	}

actualizar_comando(codigo_usuario,nombre_comando)

Esta función permite reemplazar el comando existente por uno nuevo al escribir el nuevo comando en el campo comando_usuario de la tabla usuarios.

actualizar_comando(codigo_usuario,nombre_comando){
		const { models} = sequelize

		models.usuarios.update({
			comando_usuario:nombre_comando
		},{
			where:{
				codigo_usuario:codigo_usuario
			}
		})
	}

obtener_comando(codigo_usuario)

Esta función permite leer el comando registrado en el campo comando_usuario de la tabla usuarios.

async obtener_comando(codigo_usuario){
		const { models } = sequelize

		const comando = await models.usuarios.findOne({
			where:{
				codigo_usuario:codigo_usuario
			}
		})

		if(comando.comando_usuario){
			return comando.comando_usuario
		}else{
			return null
		}
	}

actualizar_variable(codigo_usuario,variable,contenido)

Esta función permite actualizar el contenido de el campo

actualizar_variable(codigo_usuario,variable,contenido){
		const { models } = sequelize
		models.usuarios.findOne({
			where:{
				codigo_usuario:codigo_usuario
			}
		}).then(resp=>{
			let variables= JSON.parse(resp.dataValues.variables_usuario)//obtengo las variables
			variables[variable]=contenido
			models.usuarios.update({
				variables_usuario:JSON.stringify(variables)
			},{
				where:{
					codigo_usuario:codigo_usuario
				}
			})
		})
	}

obtener_variable(codigo_usuario,nombre_variable)

Esta funcion permite obtener una variable de las variables registradas en el campo variables_usuario en la tabla usuarios

async obtener_variable(codigo_usuario,nombre_variable){
		const { models}= sequelize

		var variable= await models.usuarios.findOne({
			where:{
				codigo_usuario:codigo_usuario
			}
		})
		if(variable.variables_usuario){
			return JSON.parse(variable.variables_usuario)[nombre_variable]	
		}else{
			return null
		}
		
	}

obtener_entrada_formateada(codigo_usuario)

Esta funcion reune los valores necesarios de las variables registradas en el campo variables_usuario de la tabla usuarios para construir la salida final del mensaje formateado.

async obtener_entrada_formateada(codigo_usuario){
		const { models} = sequelize

		var usuarios = await models.usuarios.findOne({
			where:{
				codigo_usuario:codigo_usuario
			}
		})

		if(usuarios){
			var variables= JSON.parse(usuarios.dataValues.variables_usuario)
			
			var mensaje="<b>Nueva entrada de autor:</b>\n\n🖊️<b>Nombre</b>🖊️: "+variables.username + "\n#"+variables.username +"\n\n📱<b>Redes</b>📱:\n"+variables.redes + "\n\n📧<b>Mensaje📧:</b>\n"+ variables.contenido + "\n\nRecuerden seguir y compartir para continuar creciendo.\n\n Comparte arte en el @muro_escritores con el ✍@escriba_muro_bot\n\nTambien recuerden que pueden compartir arte con @dimension2draw"
	
			return mensaje	
		}else{
			return null
		}
		
		
	}

obtener_entrada_revision()

Esta función permite obtener una entrada para la moderación por parte del administrador.

async obtener_entrada_revision(){
		const { models } = sequelize

		var usuario = await models.usuarios.findOne({
			where:{
				comando_usuario:"revision"
			}
		})

		if(usuario){
			return usuario.dataValues.codigo_usuario	
		}else{
			return null
		}
	}

obtener_usuario_soporte()

Esta funcion obtiene el codigo de usuario del usuario que tenga el comando «soporte» en el campo comandos_usuario de la tabla usuarios.

async obtener_usuario_soporte(){
		const { models }= sequelize

		var usuario = await models.usuarios.findOne({
			where:{
				comando_usuario:"soporte"
			}
		})

		if(usuario){
			return usuario.dataValues.codigo_usuario
		}else{
			return null
		}
	}

obtener_usuarios()

Esta función permite obtener los usuarios de la tabla usuarios.

async obtener_usuarios(){
		const {models} = sequelize

		var usuario= await models.usuarios.findAll()

		if(usuario){
			return usuario.dataValues
		}else{
			return null
		}
	}

Estado actual del escriba del muro

Actualmente quedan problemas por solucionar, pero los que más interesantes me han resultado son los problemas de usabilidad, que desafían intrínsecamente al propósito del sistema de ser una plataforma amigable con el usuario. La pura naturaleza de los bots de Telegram hace que las órdenes se escuchen después de que el usuario habla y si cambia de opinión, hay que repetir el ciclo hasta que se resuelve el punto. Por esta razón, el flujo de diálogo establecido actualmente puede producir confusión en usuarios menos asiduos, haciendo que se enreden en un bucle infinito o terminen enviando datos inválidos. Es necesario corregir esto en próximas versiones.

La próxima entrega de esta serie de artículos ya será el despliegue del bot y la presentación del mismo. Estoy emocionado por compartir con ustedes todos los detalles y características que hacen de este bot tan especial. Espero que les haya parecido interesante hasta ahora y que continúen disfrutando de este experimento. Estoy seguro de que esta próxima entrega será aún más emocionante. ¡Estén atentos!


Descubre más desde Interlan

Suscríbete y recibe las últimas entradas en tu correo electrónico.


Deja un comentario

Interlan