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 campo | Tipo de dato | PK | Notas |
id_usuario | Integer | Si | |
codigo_usuario | BIG INT | No | * |
nombre_usuario | String | No | |
nombres | String | No | |
apellidos | String | No | |
idioma | String | No | |
comandos_usuario | String | No | |
variables_usuario | String | No | |
codigo_canal | BIG INT | No | * |
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!