header-ia-sentiment

Clasificación de texto con JavaScript y TensorFlow.js: Crea una aplicación web de análisis de sentimiento

Clasificación de texto con JavaScript y TensorFlow.js: Crea una aplicación web de análisis de sentimiento

La clasificación de texto es una tarea común en inteligencia artificial y aprendizaje automático. Con JavaScript y TensorFlow.js, podemos realizar este tipo de tareas directamente en una aplicación web, sin depender de servidores externos. En esta guía, aprenderás cómo crear una aplicación interactiva que analiza el sentimiento de un texto usando TensorFlow.js y el modelo pre-entrenado Universal Sentence Encoder.

Paso 1: Configuración del entorno

  1. Crea un nuevo proyecto de Node.js ejecutando el siguiente comando:
npm init -y

Esto generará el archivo package.json.

2. Instala las dependencias necesarias para este proyecto:

npm install @tensorflow/tfjs @tensorflow-models/universal-sentence-encoder --force

Usaremos la librería @tensorflow/tfjs para trabajar con TensorFlow en JavaScript y @tensorflow-models/universal-sentence-encoder como modelo pre-entrenado para procesar texto y generar embeddings.

Paso 2: Cargar el modelo Universal Sentence Encoder

El modelo Universal Sentence Encoder genera representaciones numéricas (embeddings) para oraciones. Vamos a cargar este modelo en nuestro proyecto.

Crea un archivo index.js y agrega el siguiente código:

import * as use from '@tensorflow-models/universal-sentence-encoder';

(async () => {
  const sentences = [
    'Me encanta el clima hoy.',
    'No me gusta nada este lugar.',
    'El café está delicioso.',
    'La película fue aburrida.',
  ];

  // Cargar el modelo pre-entrenado
  const model = await use.load();
  console.log('Modelo Universal Sentence Encoder cargado.');

  // Generar embeddings para las oraciones
  const embeddings = await model.embed(sentences);
  console.log('Embeddings generados:', embeddings.arraySync());
})();
  • Este código carga el modelo y genera embeddings (vectores numéricos) para una lista de oraciones.
  • Estos embeddings serán la base para analizar el sentimiento o clasificar el texto.

Paso 3: Clasificación de texto (simulada)

Como el modelo Universal Sentence Encoder no clasifica directamente el texto, crearemos una regla sencilla para simular el análisis de sentimiento utilizando los embeddings.

Actualiza tu index.js con el siguiente código:

async function classifyText(model, text) {
  const embeddings = await model.embed();
  const score = embeddings.arraySync()[0].reduce((sum, value) => sum + value, 0);

  // Simulación: Sentimiento positivo si el puntaje es mayor que 0
  return score > 0 ? 'positivo' : 'negativo';
}

(async () => {
  const sentences = [
    'Me encanta el clima hoy.',
    'No me gusta nada este lugar.',
    'El café está delicioso.',
    'La película fue aburrida.',
  ];

  const model = await use.load();
  console.log('Modelo cargado exitosamente.');

  for (const sentence of sentences) {
    const sentiment = await classifyText(model, sentence);
    console.log(`"${sentence}" es ${sentiment}.`);
  }
})();
"Me encanta el clima hoy." es positivo.
"No me gusta nada este lugar." es negativo.
"El café está delicioso." es positivo.
"La película fue aburrida." es negativo.

Paso 4: Integración en una aplicación web

Ahora integraremos el modelo en una aplicación web para que los usuarios puedan ingresar su propio texto y obtener una clasificación.

Creamos un archivo index.html con la siguiente estructura:

<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8" />
    <title>Clasificación de Sentimiento con TensorFlow.js</title>
  </head>
  <body>
    <h1>Análisis de Sentimiento</h1>
    <div>
      <textarea id="input-text" rows="4" cols="50" placeholder="Escribe aquí..."></textarea>
      <br />
      <button id="classify-button">Clasificar</button>
      <p>Resultado: <span id="result"></span></p>
    </div>

    <!-- Importar TensorFlow.js y Universal Sentence Encoder -->
    <script src="https://cdn.jsdelivr.net/npm/@tensorflow/[email protected]/dist/tf.min.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/@tensorflow-models/[email protected]/dist/universal-sentence-encoder.min.js"></script>
    <script src="index.js"></script>
  </body>
</html>

Archivo JS: index.js

Este script conecta el HTML con el modelo de TensorFlow.js:

let model;

async function loadModel() {
  model = await use.load();
  console.log('Modelo cargado exitosamente.');
}

async function classifyText(text) {
  const embeddings = await model.embed();
  const score = embeddings.arraySync()[0].reduce((sum, value) => sum + value, 0);
  return score > 0 ? 'positivo' : 'negativo';
}

async function handleClassifyClick() {
  const inputText = document.getElementById('input-text').value;
  const resultElement = document.getElementById('result');

  if (!inputText) {
    resultElement.textContent = 'Por favor, ingresa un texto.';
    return;
  }

  const sentiment = await classifyText(inputText);
  resultElement.textContent = `Sentimiento: ${sentiment}`;
}

// Inicializar
loadModel();
document.getElementById('classify-button').addEventListener('click', handleClassifyClick);

Conclusión

En este artículo, aprendimos cómo usar TensorFlow.js y el modelo Universal Sentence Encoder para analizar y clasificar texto en una aplicación web.

Hemos visto:

  1. Cómo configurar el entorno y cargar el modelo.
  2. Cómo generar embeddings para el texto.
  3. Cómo integrar la funcionalidad en una aplicación interactiva que puede analizar el sentimiento de cualquier texto ingresado por el usuario.

Con esta base, puedes expandir el proyecto utilizando modelos más complejos o entrenando tus propios clasificadores. ¡El límite es tu imaginación!

shorturl

Descubre cómo Acortar y Gestionar tus Enlaces con Tecnops URL Shortener

Descubre cómo Acortar y Gestionar tus Enlaces con Tecnops URL Shortener

En el vertiginoso mundo digital, compartir enlaces largos y complejos puede resultar engorroso. Es en este escenario donde Tecnops URL Shortener, una herramienta desarrollada con tecnologías tan conocidas y usadas como Node.js, Express, JavaScript, CSS y HTML, se presenta como la solución ideal para simplificar y optimizar tus enlaces, ofreciéndote una experiencia de navegación más eficiente.

Creando una URL Corta Personalizada

  1. Accede a la plataforma Tecnops URL Shortener visitando //tecnops.es:10000 en tu navegador favorito.
  2. En el campo URL, introduce la URL que deseas acortar.
  3. Asigna un alias único y fácil de recordar en el campo Alias. Por ejemplo, podrías utilizar mi-alias.
  4. Haz clic en el botón Acortar para generar tu URL corta personalizada con el alias.

crear url

Accediendo a tu URL Acortada

Para acceder a la URL acortada que has creado, sigue estos simples pasos:

  1. Utiliza el siguiente formato de URL: //tecnops.es:10000/code/mi-alias.
  2. Reemplaza mi-alias con el alias que asignaste al acortar la URL.

Al abrir esta URL en tu navegador, experimentarás una redirección automática hacia la URL original asociada con el alias.

Este proceso, respaldado por las tecnologías líderes de Node.js, Express, JavaScript, CSS y HTML, hace que compartir enlaces sea más sencillo y elegante, permitiéndote mantener el control sobre tus enlaces y mejorar la experiencia de tus usuarios.

Beneficios de Tecnops URL Shortener:

  • Personalización: Asigna alias significativos para tus enlaces y crea una identidad única.
  • Gestión Sencilla: Accede a una interfaz intuitiva para crear tus enlaces acortados.
  • Eficiencia: Comparte enlaces de manera efectiva, ya sea en redes sociales, correos electrónicos o mensajes.

Conclusión: Tecnops URL Shortener, construido con tecnologías avanzadas, redefine la forma en que interactuamos con los enlaces en línea. ¡Simplifica tu experiencia en la web hoy mismo y descubre el poder de tus enlaces acortados con Tecnops!

totp-2fa

Autenticación 2FA (TOTP) usando Node, Speakeasy y Google Authenticator

Autenticación 2FA (TOTP) usando Node, Speakeasy y Google Authenticator

En esta nueva entrada vamos a realizar un sistema de autenticación de doble factor  (2FA) usando Node.js en el Back End y Javascript en el Front End.

Bien, antes de empezar a codificar vamos a definir dos conceptos que son la base de este tutorial: 2FA y TOTP.

Las siglas 2FA provienen del ingles Two Factor Authentication (autenticación de dos factores) y TOTP de las siglas Time-based One-Time Password (Contraseña de un solo uso basada en el tiempo).

Como funciona

Cuando accedemos a nuestra cuenta de usuario lo hacemos mediante nuestro usuario y una contraseña, pero ahora vamos añadir un sistema de autenticación con el cual realizaremos una segunda validación.

Con este sistema ya tendríamos la autenticación de dos factores (2FA), pero necesitamos dotar a este sistema de mayor seguridad y por eso en los últimos años se ha añadido una nueva característica, un algoritmo conocido como contraseñas de un solo uso basadas en tiempo (TOTP).

Esta técnica nos ofrece mayor seguridad ya que nos permite validar nuestra identidad mediante un código generado en paralelo en un servidor y en una aplicación (normalmente en el teléfono), que se genera y actualiza cada 30 segundos.

Todo este conjunto nos ofrece un sistema muy seguro, ya que necesitamos conocer el usuario, la contraseña y además tener registrado en nuestro teléfono un sistema de validación que esta constantemente generando códigos de un solo uso y con un tiempo limitado para su uso.

Tecnologías y funcionamiento

La aplicación la he creado usando Javascript en el Front End, Node.js en el Back End y para el estilo visual he usado la librería de componentes Metro UI.

qr-app-2fa-totpComo podemos ver en la imagen la aplicación es extremadamente sencilla, simplemente pulsamos en el botón para generar un nuevo código QR.

Con este código registraremos el token de autorización y validación en Google Authenticator, el cual nos generará automáticamente cada 30 segundos un nuevo código de autorización.

google-authenticatorSolo queda añadir el código numérico que ha generador Google Authenticator y comprobarlo en la aplicación.

Como hemos podido ver, es un sistema muy sencillo de integrar en cualquier aplicación y nos permite asegurar el acceso a nuestras aplicaciones.

Para finalizar, desde aquí podéis acceder al repositorio para obtener el código fuente completo.

Patrones de diseño y ejemplos de uso en Javascript

Patrones de diseño y ejemplos de uso en Javascript

En esta entrada veremos que son los patrones de diseño, ya que nos aportan una solución que se aplica al diseño de software y no a un lenguaje de programación en concreto.

Por lo tanto podríamos definir un patrón de diseño como:

Una solución estandarizada y reutilizable que nos permite solucionar problemas comunes cuando desarrollamos cualquier tipo de software.

Debemos entender que un patrón de diseño nos permite resolver problemas con soluciones estandarizadas, y por lo tanto acelerar el desarrollo ya que existe una solución establecida.

Otra cosa que debemos tener en cuenta es no excedernos en el uso de patrones de diseño ya que esto podría volverse en nuestra contra aumentando la complejidad de nuestro software.

Agrupando los patrones de diseño

Existen múltiples patrones de diseño pero todos ellos se pueden agrupan en 3 categorías:

  • Patrones creacionales
    • Solucionan problemas en la creación de instancias, de tal forma que su creación nos permite desacoplarlo de nuestra aplicación.
  • Patrones estructurales
    • Definen como usar estructuras complejas a partir de elementos simples.
  • Patrones de comportamiento
    • Nos permiten desacoplar nuestro código.

Patrones creacionales

En este grupo se engloban los patrones que permiten la creación de objetos, ofreciendo flexibilidad y reutilización.

  • Patrón Constructor:  Este patrón nos permite crear una variedad de objetos complejos desde un objeto raíz, ya que el mismo esta compuesto por diferentes partes que contribuyen a la creación de cada objeto complejo.
    • class Home {
          constructor() {
              this.bathroom = 0;
              this.bedroom = 0;
              this.diningRoom = 0;
              this.kitchen = 0;
              this.livingRoom = 0;
          }
          setBathroom(value) {
              this.bathroom = value;
              return this;
          }
          setBedroom(value) {
              this.bedroom = value;
              return this;
          }
          setDiningRoom(value) {
              this.diningRoom = value;
              return this;
          }
          setKitchen(value) {
              this.kitchen = value;
              return this;
          }
          setLivingRoom(value) {
              this.livingRoom = value;
              return this;
          }
          getHome() {
              return {
                  bathroom: this.bathroom,
                  bedrrom: this.bedroom,
                  diningRoom: this.diningRoom,
                  kitchen: this.kitchen,
                  livingRoom: this.livingRoom,
              };
          }
      }
      const home1 = new Home()
                      .setBedroom(4)
                      .setBathroom(2)
                      .setDiningRoom(1)
                      .setKitchen(1)
                      .setLivingRoom(2);
      
      const home2 = new Home()
                      .setBedroom(1)
                      .setBathroom(1)
                      .setKitchen(1)
                      .setLivingRoom(1);
      
      console.log(home1.getHome()); // {bathroom: 2, bedrrom: 4, diningRoom: 1, kitchen: 1, livingRoom: 2}
      console.log(home2.getHome()); // {bathroom: 1, bedrrom: 1, diningRoom: 0, kitchen: 1, livingRoom: 1}
  • Patrón Módulo: Con este patrón podemos encapsular toda la lógica de nuestra aplicación, ya que dentro del módulo estarán declaradas todas las variables y métodos.
    • const LIBRARY = { 
          API: async (url = '' ,params = {}) => { 
              const urlAPI = new URL(url); 
              urlAPI.search = new URLSearchParams(params).toString(); 
              return await (await fetch(urlAPI)).json(); 
          } 
      };
      
      (async () => { 
          const result = await LIBRARY.API('https://jsonplaceholder.typicode.com/todos'); 
          console.log(result); 
          console.log(LIBRARY);
      })();
  • Patrón Módulo revelador: Misma comportamiento que el anterior patrón pero en este caso tendremos una parte pública y otra privada.
    • const LIBRARY = (() => {
         // Ámbito privado 
         const API = async (url = '' ,params = {}) => {
            const urlAPI = new URL(url);
            urlAPI.search = new URLSearchParams(params).toString();
            return await (await fetch(urlAPI)).json(); 
         } 
         return {
            // Ámbito público
            apiCall: async (url = '', params = {}) => await API(url, params)
         } 
      })(); 
      (async () => {
         const result = await LIBRARY.apiCall('https://jsonplaceholder.typicode.com/todos');
         console.log(result); 
         console.log(LIBRARY); // {apiCall: ƒ} 
      })();
  • Patrón Prototipo: Con este patrón podemos crear un objeto que sirva de modelo para otros objetos mediante la herencia prototípica. Debemos tener en cuenta que nos devuelve un objeto vacío donde las propiedades y los métodos están en el prototipo.
    • const vivienda = {
          direccion: '',
          cp: '',
          poblacion: '',
          provincia: '',
          set: {},
          get: {},
      };    
      Object.keys(vivienda.valueOf()).forEach(value => {
          if (!['set', 'get'].includes(value)) {
              vivienda.set[value] = function(data) {
                  this[value] = data;
              }
              vivienda.get[value] = function() {
                  this[value];
              }
          }
      });
      console.log('OBJETO VIVIENDA', vivienda);
      const v1 = Object.create(vivienda);
      const v2 = Object.create(vivienda);
      console.log(v1, v2, vivienda);
    • Resultado prototype
  • Patrón Singleton: Este patrón nos ofrece una instancia única y proporciona un acceso global a dicha instancia.
    • class SingletonClass {
          static instance;
          constructor() {
              if (SingletonClass.instance) {
                  return SingletonClass.instance;
              }
              SingletonClass.instance = this;
          }
          getInstance() {
              return this.instance;
          }
      }
      const a = new SingletonClass(); // Primera y única instancia
      const b = new SingletonClass(); // Misma instancia que la anterior
      console.log(a.getInstance() === b.getInstance()) // true
    • function SingletonFunction() {
         if (!SingletonFunction.instance) {
             SingletonFunction.instance = this;
         }
         return SingletonFunction.instance;
      }
      const a = new SingletonFunction();
      const b = new SingletonFunction();
      console.log(a === b); // true

Patrones de estructura

Aquí agruparemos los patrones que hacen referencia a la composición de objetos, y que nos simplifica la relación entre ellos.

  • Patrón Mixin: Este patrón nos permite añadir mas funcionalidades a una clase existente sin tener que modificar la clase.
    • const mixin = {
          getNombre() {
              return this.nombre;
          },
          getEdad() {
              return this.edad;
          }
      };
      
      class Persona {
          constructor(nombre = '', edad = 0) {
              this.nombre = nombre;
              this.edad = edad;
          }
      }
      
      Object.assign(Persona.prototype, mixin);
      
      const p1 = new Persona('Iván', 43);
      console.log(p1, p1.getNombre(), p1.getEdad()); // {nombre: "Iván", edad: 43}, edad: 43, nombre: "Iván"
  • Patrón Decorador: Este patrón es parecido al patrón mixin, salvo que en lugar de añadir funcionalidades al prototipo se hace en las instancias de la clase.
    • class Videoclub {
          constructor(title = '', rented = false) {
              this.title = title;
              this.rented = rented;
              this.time = 0;
          }
      }
      
      const person1 = new Videoclub('The mummy', true);
      
      person1.rentedTime = function(value) {
          this.time = value;
      };
      
      person1.rentedTime(2);
      
      console.log(person1); // rented: true, rentedTime: ƒ (value), time: 2, title: "The mummy"
  • Patrón Facade: Este patrón proporciona una interfaz que nos abstrae de una funcionalidad compleja. Al aplicar este patrón solo exponemos lo necesario haciendo el código más simplificado y fácil de utilizar.
    • (() => {
          const Maths = function() {
              // Lógica compleja que aislamos
              const multiply = (number, cont = 0, arr = []) => {
                  arr.push(number * cont);
                  return cont < 10 ? multiply(number, ++cont, [...arr]) : [...arr];
              }
              const saveTable = (number, cb) => {
                  if (tableExist(number)) {
                      window.localStorage.removeItem(`Table ${number}`);
                  }
                  window.localStorage.setItem(`Table ${number}`, cb(number));
                  return window.localStorage.getItem(`Table ${number}`)
              }
              const tableExist = (number) => {
                  return window.localStorage.getItem(`Table ${number}`) !== null;
              }
              
              // Lógica simple que exponemos (fachada - facade)
              this.numberTable = (number) => saveTable(number, multiply);
          }
      
          const t1 = new Maths();
          for (let cont = 1; cont <= 10; cont++) {
              t1.numberTable(cont);    
          }
      })();
  • Patrón adaptador: Este patrón nos permite convertir la interfaz de una clase en otra que se adapte a nuestras necesidades, consiguiendo de esta forma que ambas clases puedan convivir sin problema.
    • (async () => {
          /* ------------------------------ */
          class oldClass {
              constructor() {
                  this.url = '';
              }
              setURL(url) {
                  this.url = url;
              }
              call(cb) {
                  const xhttp = new XMLHttpRequest();
                  xhttp.onreadystatechange = function() {
                      const readyState = this.readyState;
                      const status = this.status;
                      if (readyState === 4 && status === 200) {
                          cb(JSON.parse(this.responseText));
                      }
                  };
                  xhttp.open("GET", this.url, true);
                  xhttp.send();        
              }
          }
      
          oldC.setURL('https://swapi.dev/api/');
          oldC.call((data) => console.log('Old class', data));
          /* ------------------------------ */
          /* ------------------------------ */
          class newClass {
              constructor() {
                  this.url = '';
              }
              setURL(url) {
                  debugger;
                  this.url = url;
              }
              async call() {
                  debugger;
                  return await (await fetch(this.url)).json();
              }
          }
          const newC = new newClass();
          newC.setURL('https://swapi.dev/api/');
          const data = await newC.call();
          console.log('New class', data);
          /* ------------------------------ */
          /* ------------------------------ */
          class adapterClass {
              constructor() {
                  debugger;
                  this.url = '';
                  this.newClass = new newClass();
              }
              setURL(url) {
                  debugger;
                  this.newClass.setURL(url);
              }
              async call(cb) {
                  debugger;
                  const result = await this.newClass.call();
                  cb(result);
              }
          }
          const adapterC = new adapterClass();
          // Tenemos la sintaxis de la clase antigua, pero con la lógica adaptada de la nueva clase
          adapterC.setURL('https://swapi.dev/api/');
          adapterC.call((data) => console.log('Adapter', data));
          /* ------------------------------ */
      })();
  • Patrón proxy: Este patrón nos permite usar un objeto que haga de intermediario de otro, permitiéndonos controlar el acceso a el objeto.
    • (() => {
          const proxyPatter = {};
          (function() {
              // this === proxyPatter
      
              const privateNumber = 100;
              const privateMethod = function() {
                  return privateNumber;
              };
              this.publicMethod = function() {
                  return privateMethod();
              }
              this.getTHIS = function() {
                  return this;
              }
      
          }).apply(proxyPatter);
      
          console.log(proxyPatter);
          // { publicMethod: ƒ, getTHIS: ƒ }
      
          console.log(proxyPatter.publicMethod());
          // 100
      })();

Patrones de comportamiento

En este último grupo añadiremos los patrones que se encargan de la comunicación entre objetos.

  • Patrón observador: Este patrón nos permite tener el control y la gestión de eventos mediante dos objetos, ya que uno de los objetos se encarga de publicar los eventos y el otro suscribirse. Un ejemplo muy usado en Javascrtipt lo podemos ver cuando usamos addEventListener.
    • (() => {
          class ObserverPattern {
              constructor() {
                  this.observers = [];
              }
              on(observer) {
                  return this.observers.push(observer);
              }
              off(observer) {
                  const index = this.observers.indexOf(observer);
                  if (index > -1) {
                      this.observers.splice(index, 1);
                  }
              }
              notify(observer, message) {
                  const [ob] = this.observers.filter(ob => ob === observer);
                  ob?.notify?.(message);
              }
          }
          
          class Person {
              constructor(name) {
                  debugger;
                  this.name = name;
              }
              notify(message) {
                  debugger;
                  console.log('MENSAJE', message);
              }
          }
      
          const p1 = new Person('Person 1');
          const p2 = new Person('Person 2');
          const p3 = new Person('Person 3');
      
          const notify = new ObserverPattern();
          notify.on(p1);
          notify.on(p2);
          notify.on(p3);
          notify.notify(p1, 'Hola P1'); // MENSAJE Hola P1
          notify.notify(p2, 'Hola P2'); // MENSAJE Hola P2
          notify.notify(p3, 'Hola P3'); // MENSAJE Hola P3
          notify.off(p1);               // Eliminamos el observer a P1
          notify.off(p2);               // Eliminamos el observer a P2
          notify.notify(p1, 'Hola P1'); //
          notify.notify(p2, 'Hola P2'); //
          notify.notify(p3, 'Hola P3'); // MENSAJE Hola P3
      
      })();
      
  • Patrón mediador:  Este patrón define un objeto que será el encargado de gestionar la comunicación entre el resto de los objetos.
    • (() => {
          // Mediator 
          class ControlTower {
              constructor() {
                  this.airplane = {};    
              }
              add(airplane) {
                  this.airplane[airplane.name] = airplane;
                  airplane.mediator = this;
              }
              send(message, from, to = null) {
                  if (to) {
                      to.receive(message, from);
                  } else {
                      Object.values(this.airplane).forEach((item, index, arr) => {
                          if (item.id !== from.id) {
                              item.receive(message, from);
                          }
                      });    
                  }
              }
          }
          class Airplane {
              constructor(name) {
                  this.name = name;
                  this.mediator = null;
                  const [id] = window.crypto.getRandomValues(new Uint32Array(1));
                  this.id = id;
              }
              send(message, to) {
                  this.mediator.send(message, this, to);
              }
              receive(message, from) {
                  console.log(`[${this.name}]: You have a message from....${from.name} ==> ${message}`);
              }
          }
      
          const plane1 = new Airplane('P1');
          const plane2 = new Airplane('P2');
          const plane3 = new Airplane('P3');
          const plane4 = new Airplane('P4');
      
          const control = new ControlTower();
          control.add(plane1);
          control.add(plane2);
          control.add(plane3);
          control.add(plane4);
      
          plane1.send('Prueba1');
          plane2.send('Prueba2');
          plane3.send('Prueba3');
          plane4.send('Prueba4');
      
          plane1.send('1 to 2', plane2);
          plane4.send('4 to 3', plane3);
      
      })();
  • Patrón comando: Con este patrón ejecutamos una operación sin saber el contenido de la misma, encapsulando la petición y facilitando su parametrización.
    • (() => {
          const Command = function() {
              this.item = [];
              this.add = function(...args) {
                  debugger;
                  this.item.push(...args);
              }
              this.getItems = function() {
                  debugger;
                  return this.item;
              }
              return {
                  cmd: (command, ...args) => {
                      return this?.[command]?.(args);
                  }
              }
          };
              
          const exec = new Command();
          debugger;
          exec.cmd('add', 1, 2, 3, 4, 5, 'a', 'b', 'c', 'd', true, false, () => 'function');
          console.log(exec.cmd('getItems'));
      })();
  • Patrón cadena de responsabilidad: Este patrón permite pasar solicitudes entre manejadores, ya que cada manejador decide si procesar o pasar al siguiente.
    • (() => {
          class ManipulationText {
              constructor(text, seed) {
                  this.text = text;
                  this.seed = seed;
              }
              cut() {
                  this.text = Array.from(this.text)
                  return this;
              }
              letterToNumber() {
                  this.text = this.text.map(i => {
                      if (i.toLowerCase() === 'a') return 4;
                      else if (i.toLowerCase() === 'b') return 6;
                      else if (i.toLowerCase() === 'e') return 3;
                      else if (i.toLowerCase() === 'l') return 1;
                      else if (i.toLowerCase() === 'o') return 0;
                      else if (i.toLowerCase() === 't') return 7;
                      else return i;
                  });
                  return this;
              }
              convertToBinary() {
                  this.text = this.text.map(i => {
                      const isString = typeof i === 'string';
                      return parseInt(isString ? i.charCodeAt().toString(2) : i.toString(2));
                  });
                  return this;
              }
              compact() {
                  this.text = this.text.map(i => i & this.seed);
                  return this;
              }
              join() {
                  this.text = this.text.join('.');
                  return this;
              }
      
          }
          const seed = Math.floor(Math.random() * 100000) + 1;
          const text1 = new ManipulationText('Hello World', parseInt(seed.toString(2)));
          const crypto = text1.cut().letterToNumber().convertToBinary().compact().join();
      })();
  • Patrón iterador: Con este patrón podemos acceder a los elementos de un objeto secuencialmente sin necesidad de conocer su estructura, y además la responsabilidad de recorrer los elementos recae sobre el iterador.
    • (async () => {
          async function* Iterator(data) {
              let index = 0;
              while (index < data.length) {
                  const response = await (await fetch(data[index++])).json();
                  yield response;    
              }
          }
          const baseURL = 'https://jsonplaceholder.typicode.com/';
          const urls = {
              todos: 'todos/',
              posts: 'posts/',
              comments: 'comments/',
              albums: 'albums/',
              photos: 'photos/',
              users: 'users/',
          };
      
          const list = Object.values(urls).map(i => `${baseURL}${i}`);
          const urlIterator = Iterator(list);
          debugger;
          console.log(await urlIterator.next());
          console.log(await urlIterator.next());
          console.log(await urlIterator.next());
          console.log(await urlIterator.next());
          console.log(await urlIterator.next());
          console.log(await urlIterator.next());
          console.log(await urlIterator.next());
          console.log(await urlIterator.next());
      })();
  • Patrón state: Con este patrón podemos modificar el comportamiento de un objeto en base a lo que suceda dentro del mismo.
    • (() => {
          const QueueManagement = function() {
              var count = 0;
              var currentState = new x1(this);
              this.change = function(state) {
                  console.log(count);
                  currentState = state;
                  count++;
                  if (count > 10) count = 0;
                  currentState.next();
              };
              this.count = function() {
                  return count;
              }
              this.run = function() {
                  currentState.next();
              };
          };
          const x1 = function(data) {
              this.data = data;
              this.next = function() {
                  console.log('X1', data)
                  const send = data.count() < 10 ? new x1(data) : new x10(data);
                  data.change(send)
              }
          };
          const x10 = function(data) {
              this.data = data;
              this.next = function() {
                  console.log('X10', data)
                  const send = data.count() < 10 ? new x10(data) : new x100(data);
                  data.change(send)
              }
          };
          const x100 = function(data) {
              this.data = data;
              this.next = function() {
                  console.log('FIN - X100', data)
              }
          };
          const statePattern = new QueueManagement();
          statePattern.run();
      })();

Aunque existen mas patrones de diseño, he añadido los que he usado y he podido aplicar tanto a proyectores reales como personales.

chat realtime cover

Un chat creado con NodeJS, Socket.io, Express.js y Vanilla JS

Un chat creado con NodeJS, Socket.io, Express.js y Vanilla JS

Con esta nueva entrada quiero mostrar como he creado un chat usando NodeJS Socket.io. Para realizar el chat he usado la guía que ofrece la librería socket.io y su extensa documentación.

El código del proyecto

Para realizar este pequeño proyecto no he usado ningún framework basado en Javascript, ya que en principio iba a ser una prueba de concepto con pocas líneas para entender como funciona la librería socket.io.

Revisando la documentación oficial de socket.io se puede desarrollar un chat básico sin problema, pero como la idea era investigar, he probado algunas cosas añadiendo las siguientes funcionalidades:

  • Subida de ficheros
  • Chat privados
  • Información visual sobre los nuevos mensajes.
  • Cerrar y tener salas privadas

A continuación veremos el código correspondiente a la vista que he realizado usando bulma, el cual aconsejo echar un ojo ya que además de ligero, es bastante completo tanto en la documentación como en la cantidad de ejemplos de código.

index.html

<!DOCTYPE html>
<html lang="es">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>CHAT</title>
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/[email protected]/css/bulma.min.css">
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.14.0/css/all.min.css" />
  <link rel="stylesheet" href="./style.css">
</head>
<body>
  <div class="notification hidden">
    <p></p>
  </div>
  <div class="container">
    <div class="columns">
      <div class="column is-one-quarter mt-6">
        <div class="card  has-background-black">
          <div class="card-content">
            <div class="media mb-0">
              <div class="media-content">
                <p class="title is-6 has-text-white">Usuarios conectados</p>
                <hr>
              </div>
            </div>
            <div class="content has-text-white" id="userConnected">
            </div>
          </div>
        </div>
      </div>
      <div class="column">
        <div class="tabs is-boxed pt-6 mb-0">
          <ul>
            <li id="tabGeneral" data-content="chatGeneral">
              <a class="active">
                <span class="has-text-white">General</span>
                <i class="fas fa-comment newMessage hide"></i>
              </a>
            </li>
          </ul>
        </div>
        <div class="columns">
          <div class="column containerChats pb-0">
            <div id="chatGeneral" class="chat textarea contentTab"></div>
            <div class="infoInput"></div>
          </div>
        </div>
        <div class="columns is-mobile">
          <div class="column is-one-quarter pr-0 pt-1">
            <input class="input has-text-white has-background-black" type="text" id="user" placeholder="Usuario">
          </div>
          <div class="column ml-1 mt-1 mr-3">
            <div class="columns is-mobile">
              <input class="input has-text-white has-background-black" type="text" id="message" placeholder="Mensaje" title="Subir fichero">
              <form>
                <button class="button is-info" id="btnUploadFile">
                  <i class="fas fa-file-upload"></i>
                  <input class="file-input" type="file" name="uploadFile" accept="image/x-png,image/gif,image/jpeg">
                </button>
              </form>
            </div>
          </div>
        </div>
        <div class="columns is-mobile hidden" id="containerProgress">
          <div class="column">
            <progress id="upload-progress-bar" max="100" value="0">0</progress>
            <div class="info-progress" />
          </div>
        </div>
      </div>
    </div>
  </div>
  <script src="/socket.io/socket.io.js"></script>
  <script src="./index.js"></script>
</body>
</html>

style.css

@import url('https://fonts.googleapis.com/css2?family=Montserrat:wght@300&display=swap');
html {
  overflow: hidden;
}
body {
  font-family: 'Montserrat', sans-serif;
  height: 100vh;
  width: 100vw;
  margin: 0;
  padding: 0;
  background: #354b52;
  color: #ffffff;
  overflow: hidden;
}
input {
  padding: 10px;
  font-size: 14px;
}
.chat {
  width: 100%;
  height: 250px;
  padding: 10px;
  font-size: 14px;
  overflow-y: auto;
  border: 1px solid #dddddd;
  background: #000000;
  color: #ffffff;
  resize: none !important;
}
.infoInput {
  font-size: 11px;
  position: relative;
  left: 10px;
  top: -5px;
}
.disabled {
  pointer-events: none;
  color: #aaaaaa !important;
}
.chatUser {
  font-weight: bold;
  margin-right: 5px;
}
::placeholder {
  color: #ffffff !important;
  opacity: .3 !important;
}
.chat::-webkit-scrollbar,
#userConnected::-webkit-scrollbar,
.tabs ul::-webkit-scrollbar {
  width: 10px;
}
.chat::-webkit-scrollbar-track,
#userConnected::-webkit-scrollbar-track,
.tabs ul::-webkit-scrollbar-track {
  border-radius: 10px;
}
.chat::-webkit-scrollbar-thumb,
#userConnected::-webkit-scrollbar-thumb,
.tabs ul::-webkit-scrollbar-thumb {
  background: #777777;       
  border-radius: 3px;
}
.chat::-webkit-scrollbar-thumb:hover,
#userConnected::-webkit-scrollbar-thumb:hover,
.tabs ul::-webkit-scrollbar-thumb:hover {
  background: #ffffff; 
  cursor: pointer;
}
.tabs ul {
  border-bottom: 0 !important;
  font-size: 14px;
  overflow: auto;
}
.tabs.is-boxed a:hover, .active {
  background-color: #000000;
  border: 1px solid #ffffff;
}
.containerChats .infoInput {
  text-align: right;
  left: 0;
  top: 0px;
  height: 15px;
}
.hide {
  display: none;
}
.card {
  border: 1px solid #dddddd;
  max-height: 343px;
  height: 343px;
}
.notification {
  transition: 300ms ease-in-out;
  position: fixed;
  bottom: -24px;
  width: 100%;
  opacity: 1;
  z-index: 10;
}
.notification.hidden {
  transition: 300ms ease-in-out;
  bottom: -100px;
  opacity: 0;
  z-index: -1;
}
#userConnected {
  overflow-y: auto;
  max-height: 225px;
  height: 225px;
}
#userConnected .chatUser {
  cursor: pointer !important;
}
@media (max-width: 768px) {
  .tabs  {
    padding-top: 0px !important;
  }
}
.newMessage {
  font-size: 11px;
  left: 5px;
  position: relative;
  color: #2196F3;
}
.closeChat {
  position: relative;
  left: 12px;
  top: -11px;
  color: #d46a10;
  font-size: 10px;
}
@media all and (max-width: 1023px) {
  .container {
    max-width: 960px;
    margin-left: 32px !important;
    margin-right: 32px !important;
  }
}
progress {
  width: 100%;
  position: relative;
  bottom: 25px;
  transition: 200ms ease-in-out;
  opacity: 1;
}
.info-progress {
  font-size: 11px;
  text-align: right;
  width: 100%;
  bottom: 30px;
  position: relative;
}
.hidden {
  opacity: 0;
  transition: 200ms ease-in-out;
}

Sobre el código anterior no hay mucho que añadir, una página simple pero suficiente para la prueba de concepto.

El último trozo de código que veremos, es el encargado de que la aplicación funcione en nuestro navegador.

index.js

// Definición de constantes y variables para la gestión del chat
let socket = null;
let user = null;
let message = null;
let usersPanel = null;
let notification = null;
let uploadFile = null;
let webrtc = null;
const CHAT_GENERAL = 'chatGeneral';
const LENGTH_MIN_USERNAME = 3;
const EMPTY = 0;
const LITERAL = {
  sameUser: 'No puedes enviarte un mensaje a ti mismo',
  minSizeUser: `El nombre del usuario debe tener <b>mínimo ${LENGTH_MIN_USERNAME} caracteres</b>`,
  uploadFile: 'Ha compartido un fichero',
  uploadSuccess: 'El fichero se ha subido y se esta compartiendo correctamente',
};

/** 
 * @description Cierra una sala de chat privada
 * @param {string} selectorID ID de la sala a cerrar
 */
const closeChat = selectorID => {
  document.querySelector(`.tabs ul li[data-content='${selectorID}']`).removeEventListener('click', selectedChat);
  document.querySelector(`.tabs ul li[data-content='${selectorID}']`).remove();
  document.querySelector(`.containerChats #${selectorID}`).remove();
  document.querySelector(`li[data-content='chatGeneral']`).click();
};

/** 
 * @description Creamos una sala privada
 * @param {object} data Información para crear la sala
 */
const chatTo = data => {
  const selectorID = data.idHTML || data.idOrigenHTML;
  const selectorIdUser = data.idUser ||data.idOrigen;
  const selectorUser = data.user || data.userOrigen;
  const existChat = document.querySelectorAll(`#${selectorID}`).length;
  if (existChat === EMPTY) {
    if (user.dataset.idhtml !== selectorID || data.idDestinoHTML !== data.idOrigenHTML) {
      document.querySelectorAll('.tabs ul li').forEach(item => {
        item.children[0].classList.remove('active');
      });
      document.querySelectorAll('.containerChats .chat').forEach(item => {
        item.classList.add('hide');
      });
      const chat = document.createElement('div');
      const li = document.createElement('li');
      const a = document.createElement('a');
      const span = document.createElement('span');
      const iNewChat = document.createElement('i');
      const iCloseChat = document.createElement('i');
      chat.setAttribute('id', selectorID);
      chat.classList.add('chat','textarea', 'contentTab');
      document.querySelector('.containerChats').prepend(chat);
      li.setAttribute('data-content', selectorID.toString());
      li.setAttribute('data-idUser', selectorIdUser.toString());
      a.classList.add('active');
      span.style.color = data.color;
      span.classList.add('has-text-white', 'chatUser');
      span.innerHTML = selectorUser;
      a.appendChild(span);
      iNewChat.classList.add('fas', 'fa-comment', 'newMessage', 'hide');
      a.appendChild(iNewChat);
      iCloseChat.classList.add('fas', 'fa-times-circle', 'closeChat');
      iCloseChat.onclick = () => { closeChat(selectorID.toString()); };
      a.appendChild(iCloseChat);
      li.appendChild(a);
      document.querySelector('.tabs ul').appendChild(li);
      document.querySelector(`.tabs ul li[data-content='${selectorID}']`).addEventListener('click', selectedChat);
    } else {
      notify(LITERAL.sameUser, 'danger');
    }
  }
};

/** 
 * @description Seleccionar una sala para charlar
 * @param {object} evt Evento implicito en la acción ejecutada
 */
const selectedChat = evt => {
  document.querySelectorAll('.containerChats .chat').forEach(item => {
    item.classList.add('hide');
  });
  const showChat = evt.currentTarget.dataset['content'];
  const chatElement = document.querySelector(`#${showChat}`);
  if (chatElement) {
    chatElement.classList.remove('hide');
  }
  document.querySelectorAll('.tabs a').forEach(item => item.classList.remove('active'));
  evt.currentTarget.children[0].classList.add('active');
  if (!Array.from(evt.currentTarget.children[0].querySelector('i').classList).includes('hide')) {
    evt.currentTarget.children[0].querySelector('i').classList.add('hide')
  }
};

/** 
 * @description Envia mensajes al chat general y a un usuario privado
 * @param {object} evt Evento implicito en la acción ejecutada
 * @param {boolean} status Indicamos el estado para saber que usuario esta escribiendo
 */
const sendMessage = (evt, status) => {
  if (evt.key === 'Enter') {
    if (user.value.length >= LENGTH_MIN_USERNAME && message.value.trim().length > EMPTY) {
      const parent = document.querySelectorAll('.tabs a.active')[0].parentElement;
      const content = parent.dataset.content;
      const currentlyChat = document.querySelector(`#${content}`);
      const isChatGeneral = content === CHAT_GENERAL;
      const infoMensaje = {
        idOrigen: user.dataset.iduser,
        idOrigenHTML: user.dataset.idhtml,
        idDestino: isChatGeneral ? CHAT_GENERAL : parent.dataset.iduser,
        idDestinoHTML: content,
        userOrigen: user.value,
        userDestino: isChatGeneral ? CHAT_GENERAL : parent.innerText,
        message: message.value
      };
      if (content !== CHAT_GENERAL) {
        const color = document.querySelector(`#userConnected .chatUser[data-idhtml='${user.dataset.idhtml}']`).style.color;
        const messageSend = document.querySelector(`.containerChats #${content}`);
        const div = document.createElement('div');
        const span = document.createElement('span');
        span.style.color = color;
        span.classList.add('chatUser');
        span.appendChild(document.createTextNode(`${infoMensaje.userOrigen}:`));
        div.appendChild(span);
        div.innerHTML += infoMensaje.message;
        messageSend.appendChild(div);
      }
      socket.emit(`message-chat-${isChatGeneral ? 'general' : 'private'}`, infoMensaje);
      message.value = '';
      currentlyChat.scrollTo(0, currentlyChat.scrollHeight);
    } 
    user.classList[user.value.length >= LENGTH_MIN_USERNAME ? 'remove' : 'add']('has-background-danger');
  } else {
    if (user.value.length >= LENGTH_MIN_USERNAME) {
      if (evt.key !== 'Tab') {
        socket.emit('write-client', { data: status ? user.value : ''});
      }
    }
  }
};

/** 
 * @description Muestra texto indicando quien esta escribiendo
 * @param {object} payload Información que pinta cuando pulsamos un tecla
 */
const clientBeenWriting = payload => {
  document.querySelector('.containerChats .infoInput').innerHTML = payload;
};

/** 
 * @description Muestra texto en el chat general
 * @param {object} data Información relativa al usuario
 */
const reciveMessage = data => {
  const {
    color,
    idDestinoHTML,
    idOrigenHTML,
    userOrigen,
    message 
  } = data;
  let activeChatID = document.querySelector(`#${idDestinoHTML}`) || document.querySelector(`#${idOrigenHTML}`);
  if (!activeChatID) {
    chatTo(data);
    activeChatID = document.querySelector(`#${idDestinoHTML}`) || document.querySelector(`#${idOrigenHTML}`);
  }
  if (idDestinoHTML !== idOrigenHTML) {
    const div = document.createElement('div');
    const span = document.createElement('span');
    span.style.color = color;
    span.classList.add('chatUser');
    span.appendChild(document.createTextNode(`${userOrigen}:`));
    div.appendChild(span);
    div.innerHTML += message;
    activeChatID.appendChild(div);
    activeChatID.scrollTo(0, activeChatID.scrollHeight);
  }
  const newMessage = document.querySelector('.tabs a.active').parentElement.dataset.content
  if (newMessage !== activeChatID.id && newMessage !== idDestinoHTML) {
    const destino = document.querySelector(`.tabs li[data-content='${idDestinoHTML}'] i`);
    const origen = document.querySelector(`.tabs li[data-content='${idOrigenHTML}'] i`);
    (destino || origen).classList.remove('hide');
  }
};

/** 
 * @description Añade y actualiza el panel lateral con el listado de los usuarios
 * @param {object} payload Información de cada uno de los usuarios
 */
const registerUser = payload => {
  Array.from(usersPanel.children).forEach(item => item.remove())
  payload.forEach(item => {
    const div = document.createElement('div');
    div.style.color = item.color;
    div.classList.add('chatUser');
    div.onclick = () => { chatTo(item); };
    div.setAttribute('data-idUser', item.idUser);
    div.setAttribute('data-idHTML', item.idHTML);
    div.appendChild(document.createTextNode(item.user));
    usersPanel.appendChild(div);
    if (user.value === item.user) {
      user.setAttribute('data-idHTML', item.idHTML);
      user.setAttribute('data-idUser', item.idUser);
    }
  });
};

/** 
 * @description Muestra mensaje de error al registrar el usuario
 * @param {object} payload Información de cada uno de los usuarios
 * @param {object} evt Evento implicito en la acción ejecutada
 */
const errorRegisteredUser = (payload, evt) => {
  const { error } = payload;
  notify(error, 'danger');
  evt.target.classList.remove('disabled');
  user.classList.add('has-background-danger');
};

/** 
 * @description Realiza la conexion al servidor
 * @param {object} evt Evento implicito en la acción ejecutada
 */
const connectedToServer = evt => {
  const target = evt.target;
  if (target.value.length >= LENGTH_MIN_USERNAME) {
    target.classList.add('disabled');
    user.classList.remove('has-background-danger');
    socket = io.connect();
    socket.on('recive-message', reciveMessage);
    socket.on('registered-user', registerUser);
    socket.on('error-registered-user', payload => errorRegisteredUser(payload, evt));
    socket.on('client-been-writing', clientBeenWriting);
    socket.on('upload-progress', data => uploadProgress(data));
    const idHTML = generateIDHtml();
    socket.emit('connected-to-server', { user: target.value, idHTML });
  } else {
    notify(LITERAL.minSizeUser, 'danger');
  }
};

/** 
 * @description Mostramos el progreso de la subida
 * @param {object} data Información sobre la subida del archivo
 */
const uploadProgress = data => {
  const { recived, total, who } = data;
  const porcent = Math.floor((recived * 100) / total);
  const currentlySize = (recived / 1024) / 1024; // MB
  const totalSize = (total / 1024) / 1024; // MB
  const progress = document.querySelector('#upload-progress-bar');
  const infoProgress = document.querySelector('.info-progress');
  progress.value = Math.floor(porcent);
  progress.innerHTML = porcent.toFixed(2);
  infoProgress.innerHTML = `${currentlySize.toFixed(2)} MB ${totalSize.toFixed(2)} MB ${porcent} %`;
};

/** 
 * @description Cargamos un fichero y se sube al servidor
 * @param {object} evt Evento implicito en la acción ejecutada
 */
const upload = async evt => {
  if (user.value.length >= LENGTH_MIN_USERNAME) {
    const uploadProgress = document.querySelector('#containerProgress');
    uploadProgress.classList.remove('hidden');
    const btnUploadFile = document.querySelector('#btnUploadFile');
    btnUploadFile.classList.add('is-loading');
    btnUploadFile.setAttribute('disabled', true);
    const files = evt.target.files;
    const data = new FormData();
    data.append('archivo', files[0]);
    socket.emit('upload-file', { idUser: user.dataset.iduser });
    const result = await (await fetch('/upload-file', {
      method: 'POST',
      body: data
    })).json();
    btnUploadFile.classList.remove('is-loading');
    btnUploadFile.removeAttribute('disabled');
    const { statusCode, path, statusMessage } = result;
    if (statusCode === 200) {
      message.value = `${LITERAL.uploadFile} <a href='${statusMessage}' target='_blank'>[${files[0].name}]</a>`;
      sendMessage(evt = {key: 'Enter'}, true);
      notify(LITERAL.uploadSuccess, 'success', 4000);
    } else {
      notify(statusMessage, 'danger', 4000);
    }
    setTimeout(() => {
      uploadProgress.classList.add('hidden');
    }, 2000);
  } else {
    document.querySelector('form').reset();
    notify(LITERAL.minSizeUser, 'danger');
  }
};

/** 
 * @description Inicialización del chat
 */
const load = () => {
  const tabGeneral = document.querySelector('#tabGeneral');
  user = document.querySelector('#user');
  message = document.querySelector('#message');
  usersPanel = document.querySelector('#userConnected');
  notification = document.querySelector('.notification');
  message.addEventListener('keydown', evt => sendMessage(evt, true));
  message.addEventListener('keyup',  evt => sendMessage(evt, false));
  user.addEventListener('blur', connectedToServer);
  tabGeneral.addEventListener('click', selectedChat);
  uploadFile = document.querySelector('input[type=file]');
  uploadFile.addEventListener('change', upload);
};

/** 
 * @description Muestra notificaciones en el chat
 * @param {string} msn Mensaje que se muestra en la notificación
 * @param {string} type Tipo de mensaje (danger, info, success, warning)
 * @param {number} timeout Duración de la notificación
 */
const notify = (msn = '', type = 'info', timeout = 2000) => {
  notification.children[0].innerHTML = msn;
  notification.classList.add(`is-${type}`);
  notification.classList.remove('hidden');
  setTimeout(() => {
    notification.classList.add('hidden');
    notification.classList.remove(`is-${type}`);
  }, timeout);
};

/** 
 * @description Generamos un ID para cada usuario
 */
const generateIDHtml = () => new Date().getTime().toString().split('').map(i => String.fromCharCode(parseInt(i) + 65)).join('');

/** 
 * @description Espera a que este la página completamente cargada
 */
document.addEventListener("DOMContentLoaded", load);

En un futuro me gustaría realizar esta misma aplicación usando React Vue, entre otras cosas para aprovechar la magia que ofrecen y poder añadir mas características como, llamadas de audio y video, compartir pantalla, guardar conversaciones, stickers, gifs, etc…

Para finalizar esta entrada, si estás interesado en usar el código, lo puedes obtener desde Github y ademas puedes probar el chat en la siguiente url: https://chat.tecnops.es/

streaming torrent cover

Streaming de torrents usando NodeJS y VLC

Streaming de torrents usando NodeJS y VLC

Antes de comenzar, me gustaría aclarar algunos puntos importantes debido a la aplicación y al código que voy a publicar:

  • Bajo ningún concepto apoyo la piratería de ningún medio, formato o producto digital y/o físico, ya que toda persona tiene derecho a cobrar por su trabajo.
  • De la misma forma que he aprovechado el conocimiento de otras personas, yo ofrezco mis conocimientos para quien pueda o quiera aprovecharlos mediante este blog.
  • Tanto el código fuente que he desarrollado, como las dependencias de terceros y las adaptaciones de las mismas están a disposición de todo el mundo.
  • La aplicación y el código fuente aquí expuestos, se ofrecen y publican solo con fines educativos, descargando toda la responsabilidad sobre quien realice un mal uso de la misma.

Bien, una vez aclarados estos puntos comencemos con la aplicación.

La idea surge cuando intento realizar streaming de un vídeo usando WebRTC y NodeJS.

Como no me convence la solución, me pongo a buscar información por internet y me encuentro con esta página.

Para sorpresa mía, resulta que es una librería que te permite hacer streaming de ficheros torrent desde el navegador y con unas pocas lineas de código.

Pasadas unas horas y viendo que no conseguía que funcionase con ficheros torrent, encuentro en la documentación que esta librería usa WebRTC para hacer el per-to-per pero no es compatible con los fichero .torrent.

Como soy un poco cabezón, continuo buscando y por fin encuentro unas dependencias para usar en NodeJS que te permiten hacer streaming de torrents.

A partir de ese momento, solo queda pelearse un poco con el código y las dependencias.

Al cabo de unos días creo una pequeña aplicación y su repositorio correspondiente.

Poniendo la aplicación en marcha

Una vez bajado / clonado el repositorio ejecutaremos npm i para instalar todas las dependencias necesarias.

A continuación ejecutaremos el comando node server.js, el cual nos levantara un servidor en local que se encargara de todo el proceso de streaming.

Levantando el servidor

Levantando el servidor

Ahora que nuestro servidor esta funcionando, abriremos la página index.html

Abriendo la página

Abriendo la página

Como había indicado al principio y para evitar problemas con derechos de autor, vamos a usar un fichero torrent sin copyright.

Seleccionado el torrent

Seleccionado el torrent

 

Cuando tengamos el fichero torrent en nuestro equipo, procederemos a subirlo a nuestra aplicación

Subiendo el torrent

Subiendo el torrent

En el momento hagamos clic sobre el botón, esta acción subirá en nuestro servidor de NodeJS a una carpeta llamada torrents el fichero del cual realizara el streaming.

Streaming torrent

Streaming torrent

Una cosa a tener en cuenta, es que desde el navegador que usemos para abrir el index.html deberemos instalar una extensión para evitar problemas de CORS.

El siguiente paso, sera abrir VLC y cargar un vídeo desde una URL que nos proporcionara NodeJS.

Carga url VLC

Carga url VLC

Carga url VLC

Carga url VLC

Comenzara el proceso de carga del vídeo desde la URL de NodeJS.

Carga url VLC

Carga url VLC

Cuando se haya obtenido una pequeña parte de la información, VLC comenzara a reproducir el vídeo mientras NodeJS continuar obteniendo el resto de la información, hasta completar todo el fichero.

Reproducir URL VLC

Reproducir URL VLC

Y ya solo nos queda comprobar que nuestro vídeo se muestra correctamente.

youtube mp3 cover

Descargar MP3 de Youtube

En esta ocasión y a falta de terminar la segunda entrega del tutorial / guía de referencia sobre Javascript y sus mejoras, he decido crear una pequeña aplicación que hacia tiempo tenia ganas de crear.

La aplicación es para convertir y descargar el audio MP3 de cualquier vídeo de Youtube.

Para llevar a cabo esta aplicación he usado las siguiente herramientas:

  • Para el diseño de la aplicación he usado Materialize CSS que es bastante simple y tiene un buen acabado.
  • La lógica esta realizada en Javascript nativo ya que tampoco tiene mucha complicación.
  • Para la descarga / conversión he usado NodeJS y algunas dependencias extras como ffmpeg, express y ytdl-core

A continuación un pequeño vídeo de la aplicación en funcionamiento y pulsando aquí puedes usarla.

Web Scraping con NodeJS y Cheerio

¿Qué es el Web scraping?

El web scraping es una técnica para extraer la información que necesitemos de una página web de forma automatizada.

Para realizar este ejemplo he usado NodeJS y Cheerio

NodeJS ya es un gran conocido y realmente lo uso junto a express para realizar una api muy sencilla.

Quien hace la magia en este ejemplo es cheerio

Cheerio es una implementación rápida, flexible y ligera del core jQuery diseñada específicamente para el servidor.

Pero, ¿que significa esto?. Cheerio analiza el marcado y proporciona una API para atravesar/manipular la estructura de datos resultante.

Con todo este proceso al final conseguimos crear un api de cualquier web del mundo.

A continuación un ejemplo de una api que he creado haciendo web scraping con cheerio, nodejs y express:

UPDATE (10/09/2018)

Gracias a un comentario acabo de darme cuenta que no había añadido el código fuente.

Desde aquí puedes acceder al repositorio en Github con la documentación incluida.

Autenticación con JWT en JSON Server

Introducción

En este artículo te explico como crear un API Rest completa junto a faker.js para que puedas hacer tus pruebas sin muchas complicaciones.

Como ampliación me parece muy interesante añadir a nuestra API Rest seguridad, así que en este nuevo artículo vamos a usar JWT para agregar un sistema de autenticación por token.

Voy a dar por sentado que tanto json-server (imprescindible) como faker.js (opcional) está(n) instalado(s).

Ampliación JSON server

Quiero aprovechar para hacer una pequeña ampliación sobre algunas funciones avanzadas que ofrece json-server, porque en el artículo anterior solo probamos el CRUD.

Para no extenderme mucho voy añadir el código que genera los datos falsos y el indice con las funciones avanzadas que podemos hacer en JSON Server:

let faker = require('faker');
let generateData = () => {
    let data = []
    for (let id = 0; id < 1000; id++) {
        data.push({
            "id": id,
            "address": faker.address.streetAddress(),
            "latitude": faker.address.latitude(),
            "longitude": faker.address.longitude(),
            "first_name": faker.name.firstName()
        });
    }
    return { "data": data }
}
module.exports = generateData

/*
------------------------
FILTER
[
  {
    "id": 4,
    "address": "866 Mohr Light",
    "latitude": "-55.7908",
    "longitude": "170.3427",
    "first_name": "Tyree"
  }
]
CURL: http://localhost:3000/data?id=4
------------------------
------------------------
PAGINATE
[
  {
    "id": 10,
    "address": "612 Fahey Locks",
    "latitude": "57.2960",
    "longitude": "-54.5350",
    "first_name": "Antone"
  },
  {
    "id": 11,
    "address": "09094 Tillman Brooks",
    "latitude": "69.1174",
    "longitude": "-134.4193",
    "first_name": "Tito"
  },
  {
    "id": 12,
    "address": "8161 Kelton Shoal",
    "latitude": "86.5114",
    "longitude": "65.8792",
    "first_name": "Freda"
  },
  {
    "id": 13,
    "address": "21785 Jasmin Trail",
    "latitude": "-33.6972",
    "longitude": "16.9923",
    "first_name": "Bettye"
  },
  {
    "id": 14,
    "address": "620 Abernathy Ports",
    "latitude": "-9.2774",
    "longitude": "-47.5246",
    "first_name": "Braden"
  },
  {
    "id": 15,
    "address": "24260 Wilfred Parks",
    "latitude": "54.5527",
    "longitude": "111.0772",
    "first_name": "Roselyn"
  },
  {
    "id": 16,
    "address": "2826 Oral Trafficway",
    "latitude": "-61.6411",
    "longitude": "-124.6945",
    "first_name": "Maude"
  },
  {
    "id": 17,
    "address": "0318 Hessel Junction",
    "latitude": "-87.0497",
    "longitude": "147.6963",
    "first_name": "Allison"
  },
  {
    "id": 18,
    "address": "93964 Darion Lights",
    "latitude": "-82.7756",
    "longitude": "176.7674",
    "first_name": "Quincy"
  },
  {
    "id": 19,
    "address": "341 Akeem Way",
    "latitude": "11.6085",
    "longitude": "67.5816",
    "first_name": "Theresa"
  }
]
CURL: http://localhost:3000/data?_page=2
Por defecto son paginaciones de 10 en 10 registros
------------------------
------------------------
SORT
[
  ........
  {
    "id": 605,
    "address": "688 Shanna Via",
    "latitude": "55.7688",
    "longitude": "168.6224",
    "first_name": "Zoila"
  }
]
CURL: http://localhost:3000/data?_sort=first_name&order=asc
------------------------
*/

Para obtener mas información puedes ir a la siguiente url

Añadiendo la autenticación JWT

Lo primero que tenemos que hacer es instalar el paquete jsonwebtoken

npm i jsonwebtoken –save

Una vez terminada la instalación crearemos un fichero que usaremos configurar nuestra API REST junto a JWT. En el siguiente enlace puedes acceder al repositorio.

No obstante voy añadir el código completo con una pequeña explicación.

servidor.js

// Añadimos los módulos necesarios
const FS = require('fs');
const bodyParser = require('body-parser');
const jsonServer = require('json-server');
const JWT = require('jsonwebtoken');
const middlewares = jsonServer.defaults()

// Servidor Express
const server = jsonServer.create();

// Enrutador Express
const router = jsonServer.router('./db.json');

// Creamos un JSON con los usuarios (03f996214fba4a1d05a68b18fece8e71 == MD5 Hash 'usuarios' )
const userdb = JSON.parse(FS.readFileSync('./03f996214fba4a1d05a68b18fece8e71.json', 'UTF-8'));

// Middlewares predeterminados (logger, static, cors y no-cache)
server.use(middlewares)

// Parseo del body
server.use(jsonServer.bodyParser);

// Configuración TOKEN y duración
const SECRET_KEY = 'zxcasdqwe098765';
const expiresIn = '1h';

// Crear un TOKEN desde un payload 
createToken = (payload) => JWT.sign(payload, SECRET_KEY, { expiresIn });

// Verificar el TOKEN 
verifyToken = (token) => JWT.verify(token, SECRET_KEY, (err, decode) => decode !== undefined ? decode : err);

// Comprobamos si el usuario existe en nuestra 'base de datos'
isAuthenticated = ({ email, password }) => userdb.users.findIndex(user => user.email === email && user.password === password) !== - 1;

// Creamos un ENDPOINT para comprobar si el usuario existe y poder crear y enviar un TOKEN
server.post('/auth/login', (req, res) => {
    const { email, password } = req.body;
    if (isAuthenticated({ email, password }) === false) {
        const status = 401;
        const message = 'Contraseña y/o password incorrectos';
        res.status(status).json({ status, message })
        console.log(message);
        return;
    }
    const access_token = createToken({ email, password });
    res.status(200).json({ access_token })
});

// Añadir un middleware Express que verifique si el encabezado de autorización.
// Luego verificara si el token es válido para todas las rutas, excepto la ruta anterior, 
// ya que esta es la que usamos para iniciar sesión en los usuarios.
server.use(/^(?!\/auth).*$/, (req, res, next) => {
    if (req.headers.authorization === undefined || req.headers.authorization.split(' ')[0] !== 'Bearer') {
        const status = 401;
        const message = 'Header con autorización incorrecta';
        res.status(status).json({ status, message });
        console.log(message);
        return;
    }
    try {
        verifyToken(req.headers.authorization.split(' ')[1]);
        next();
    } catch (err) {
        const status = 401;
        const message = 'Error: TOKEN de acceso no válido';
        res.status(status).json({ status, message });
        console.log(message);
    }
})
server.use(router);

server.listen(3000, () => {
    console.log('API REST FUNCIONANDO')
});

El fichero para la base de datos lo he generado con el siguiente script en NodeJS (db.js) :

let faker = require('faker');
let data = { data: [] };
for (let id = 0; id < 1000; id++) {
    data.data.push({
        "id": id,
        "address": faker.address.streetAddress(),
        "latitude": faker.address.latitude(),
        "longitude": faker.address.longitude(),
        "first_name": faker.name.firstName()
    });
}

console.log(JSON.stringify(data));

Para generar nuestra base de datos usaremos el siguiente comando:

node db.js > db.json

Y para finalizar, la tabla de los usuarios es un fichero JSON con unos mínimos datos:

{
    "users": [
        {
            "id": 1,
            "name": "ivan",
            "email": "[email protected]",
            "password": "qwerty1234"
        },
        {
            "id": 2,
            "name": "rafael",
            "email": "[email protected]",
            "password": "asdfgh56789"
        }
    ]
}

Cuando tengamos todo el proyecto preparado usaremos el siguiente comando para iniciar nuestro servidor:

node servidor.js

A continuación unas pruebas del funcionamiento de JWT:

Ahora con el usuario y la contraseña validos:

Creando una API REST con JSON Server y Faker.js

Introducción

Actualmente cualquier programador que necesite obtener datos para su aplicación, la mayoría de las ocasiones las realizara mediante llamadas a un API REST. De hecho, a día de hoy no existe un proyecto por muy pequeño que sea que no disponga de una API REST para la consulta, modificación, etc.. de los datos.

Una tarea bastante común para cualquier programador es simular un servicio REST, para entregar algunos datos en formato JSON a una aplicación y comprobar que todo funciona correctamente.

Para crear un API REST podemos montar un servidor back-end usando cualquiera de las tecnologías actuales como NodeJS, PHP, JAVA, C#, etc…

El problema de montar este tipo de back-end es que muchas veces por falta de tiempo, recursos o conocimientos no se hace correctamente y eso acaba afectando al resultado final de nuestra aplicación.

Creando nuestra API REST

En esta ocasión vamos a usar JSON Server, que es un proyecto que te ayudara a configurar una API REST con operaciones CRUD de forma rápida y sencilla.

En la url https://github.com/typicode/json-server puedes encontrar toda la información del proyecto y el repositorio.

A continuación vamos a configurar el servidor JSON y publicaremos una API REST para probar.

También usaremos la librería llamada faker.js, que se encarga de generar datos falsos.

Para instalar el proyecto usaremos npm con el siguiente comando: npm i -g json-server. En este caso he usado el parámetro -g para instalarlo de forma global y poder ejecutarlo desde cualquier directorio.

Una vez terminada la instalación vamos a crear nuestra «base de datos», que sera un fichero JSON con la estructura que necesitemos para poder simular nuestra API REST.

Para no complicarnos llamaremos al fichero database.json:

database.json

{
  "datos-personales": [
    {
      "id": 1,
      "nombre": "A",
      "edad": 30,
      "sexo": "H"
    }
  ]
}

La estructura JSON que usaremos como «base de datos« consiste en un objeto que tiene un conjunto de datos asignados, y este objeto tiene unas propiedades.

Ahora ejecutaremos el servidor para usar nuestra API REST con el siguiente comando:

json-server –watch database.json.

Como parámetro necesitamos indicar el archivo que contiene nuestra estructura JSON (database.json).

También usaremos el parámetro -watch para que el servidor se inicie en modo escucha, con esto hacemos que el servidor este vigilando los cambios del archivos y actualice la API expuesta.

Ahora si vamos a nuestro navegador y abrimos la url http://localhost:3000 veremos nuestro servidor funcionando y mostrando nuestra API REST y sus endpoints.

En las imágenes anteriores podemos comprobar como se ha cargado nuestro fichero y tenemos nuestra API REST preparada.

Los siguientes endpoints son creados automáticamente por el servidor JSON:

OBTENER / datos-personales
GET / datos-personales/ {id}
POST / datos-personales
PUT / datos-personales/ {id}
PATCH / datos-personales/ {id}
DELETE / datos-personales/ {id}

Si realizas solicitudes POST, PUT, PATCH o DELETE los cambios se guardarán automáticamente en database.json.

Una solicitud POST, PUT o PATCH debe incluir un encabezado application/type:application/json para usar el JSON en el cuerpo de la solicitud, de lo contrario dará como resultado un 200 OK pero sin cambios en los datos.

Con todo funcionando la mejor forma de probarlo es usar postman, que puedes descargarlo en forma de extensión para Google Chrome o instalarlo como aplicación de escritorio.

GET: localhost:3000/datos-personales

GET: localhost:3000/datos-personales/:id

POST: localhost:3000/datos-personales/

DELETE: localhost:3000/datos-personales/1

PUT: localhost:3000/datos-personales/2

Como hemos podido comprobar ya tenemos una API REST completa, funcionando y en apenas 5 minutos.

Si queremos tener una respuesta que sea idéntica a la API REST real o que necesitemos simular, solo deberemos modificar el fichero database.json (o el nombre que queramos), con la respuesta que necesitemos.

Pero, ¿y si necesitamos tener por ejemplo tener 1.000, 10.000 o 1.000.000 de registros?.

Añadiendo datos falsos a nuestra API REST

Sin problemas!!, pues para realizar ese trabajo vamos a usar faker.js, ya que si tenemos que añadir todo eso a mano nos podemos eternizar y no es nada practico.

¿Y que es faker.js?, pues es un generador de datos falsos y lo puedes encontrar en la siguiente url https://github.com/marak/Faker.js/.

Para instalar faker.js usaremos npm con el siguiente comando: npm i faker

Con faker.js instalado solo nos queda crear un pequeño script para NodeJS que añada todos esos registros.

fichero_script_que_genera_datos.js

// database-faker.js
let faker = require('faker');
let generateData = () => {
    let data = []
    for (let id = 0; id < 1000; id++) {
        data.push({
            "id": id,
            "address": faker.address.streetAddress(),
            "latitude": faker.address.latitude(),
            "longitude": faker.address.longitude(),
            "first_name": faker.name.firstName()
        });
    }
    return { "data": data }
}
module.exports = generateData

Ahora desde la consola de comandos en vez de usar el comando json-server –watch fichero.json usaremos el siguiente comando json-server –watch fichero_script_que_genera_datos.js.

Y el resultado de nuestra nueva API REST es el siguiente:

El inicio de la respuesta, y podemos ver que son datos totalmente aleatorios creados por faker.js:

Y el final de la respuesta:

Bien, con esto ya tendríamos nuestro API REST funcionando y de forma muy simple.

Tanto json-server como faker.js, en sus respectivas páginas puedes encontrar el repositorio y toda la documentación necesaria para personalizar a tu gusto o necesidades.