arrow&v

8. Proyecto Final


Ahora que ya se cuenta con los conocimientos básicos de node.js y la comunicación entre servidores y navegadores vía HTTP, se desarrollará un programa como proyecto final que permite repasar gran parte de los conceptos estudiados.


El proyecto final consiste en un servidor web que obtiene el precio de bitcoin en moneda USD, al valor de cierre de cada día de la última semana y el último mes y luego muestra en una página una gráfica con el comportamiento del precio esa semana y mes. Ofrece también un campo para especificar una cantidad de bitcoins a comprar o vender:




Al hacer click en el botón de “Aceptar” luego de especificar compra o venta y la cantidad de BTC, se redirecciona a otra página que ofrece 3 enlaces donde se puede ir a comprar o vender BTC; además ofrece crear un recordatorio para la fecha y hora que elijamos en la página:




Al hacer click en el botón de recordar, el programa crea un archivo en formato iCal en directorio especificado en la constante dir_recordatorios en el archivo “servidor_mercado_btc.js”





Si el archivo es abierto con alguna aplicación de calendario, podemos ver los detalles y aceptarlo:




Para que sea más fácil entender el código conforme se realiza el análisis paso por paso, se crean los cuatro archivos con código fuente descritos abajo en un mismo directorio. Para crearlos y visualizarlos, se sugiere que usen un editor de código como Visual Studio Code, Atom o Notepad++ que entienden la sintaxis de JavaScript y HTML para mostrar con distintos colores los varios elementos del código de forma que lo hace más legible. También muestran los números de línea lo cual hará más fácil el poder referenciarlos acá.


Archivo: servidor_mercado_btc.js


// Programa de node.js que implementa un servidor web para extraer de el sitio https://blockchain.info

// el precio del BitCoin el ultimo mes y ultima semana para mostrarlo en graficas

// para ofrecer instrucciones de como comprar/vender y/o crear un recordatorio

// Ver instrucciones de uso del API para obtener los datos en https://www.blockchain.com/api/exchange_rates_api

const https = require('https');

const http = require('http');

const url = require('url');

const fs=require('fs');


const dir_recordatorios='./'

function datos_btc_hileras(timeframe){

/*

Tenemos que devolver una cadena de caracteres con todos los datos leidos para un mes o una semana dependiendo de lo que manden en timeframe en el siguiente formato para poder pasarle al JavasScript dentro del machote para mostrar el analisis de precios (esto es un ejemplo ya con valores):

[

[new Date(1646352000000), 42464.4],

[new Date(1646438400000), 39141.25],

[new Date(1646524800000), 39399.58],

[new Date(1646611200000), 38407.69],

[new Date(1646697600000), 38032.5],

[new Date(1646784000000),38753.1],

[new Date(1646870400000), 41956.53]

]

*/


return new Promise((resolve, reject) => {

// Primero, hay que iniciar la estructura de list con un parentesis cuadrado de apertura

var texto_datos_precios="["

https.get('https://api.blockchain.info/charts/market-price?timespan='+timeframe+'&format=json', (resp) => {

let data = '';

// Un pedazo de data ha sido recibido.

resp.on('data', (pedazo) => {

data += pedazo;

});

// Ya se recibio la respuesta completa. Procedemos a procesarlo

resp.on('end', async () => {

// primero creamos el objeto datos_jason con todos los datos

// que obtuvimos de la lectura del servicio web de precios bitcoin

datos_json=JSON.parse(data);

// ahora leemos los detalles para crear la estructura en una cadena

// de caracteres que necesitamos devolver


// Primero recorremos la lista de los datos que regresaron del servicio

// y extraemos cada punto para agregarlo a la cadena de caracteres que representa

// la lista a pasar al machote

for (let i = 0; i < datos_json.values.length; i++) {

//console.log('fecha: '+datos_json.values[i].x+' valor: '+datos_json.values[i].y);

// el servicio retorna la fecha/hora en segundos desde Epoch pero necesitamos milisegundos

// para poder pasarlo a la libreria graficadora por lo cual tenemos que multiplicar

// por mil antes de convertirlo en cadena de caracteres

texto_datos_precios=texto_datos_precios+

'[new Date('+(datos_json.values[i].x*1000).toString()+

'), '+

datos_json.values[i].y.toString()+

']'

// si no estamos en el ultimo dato, tenemos que agregar una coma

// para que queden bien estructurada la lista

if (i != datos_json.values.length-1) {

texto_datos_precios=texto_datos_precios+','

}

}

// al final de la lista, hay que "cerrar" la estructura

// con parentesis cuadrado

texto_datos_precios=texto_datos_precios+']'

// ahora devolvemos la cadena de caracteres con los datos de los precios en el formato

// necesario

resolve(texto_datos_precios);

})

})

})

}

function leer_machote( archivo, cambios) {


return new Promise((resolve, reject) => {

var texto_modificado="";

fs.readFile(archivo, 'utf8', function(err, data) {

// revisemos si hay error de lectura del archivo HTML

console.log('Abriendo archivo: '+archivo)

if (err) {

// Si hay error, respondamos con texto vacio

console.log('Error abriendo archivo: '+archivo)

resolve("");

} else {

// Respuesta exitosa, no hubo errores!

// extraigamos los valores para llenar las variables

// que necesitamos para llenar el machote

texto_modificado=data;

for (const [key, value] of Object.entries(cambios)) {

console.log(`${key}: ${value}`);

// Ahora hagamos el reemplazo de cada marcador de posición con

// el correspondiente valor de la variable

texto_modificado = texto_modificado.replace( new RegExp("{{"+key+"}}", 'g') , value);

}

resolve(texto_modificado);

}

});

})

}


var server = http.createServer( async function (req, res) {

console.log('req.method: ',req.method);

console.log('req.url: ',req.url);

console.log('Pathname: ',url.parse(req.url,true).pathname)

switch (url.parse(req.url,true).pathname) {

case '/' :

var datos_semana= await datos_btc_hileras("1weeks")

console.log(datos_semana)

var datos_mes= await datos_btc_hileras("1months")

console.log(datos_mes)

var cambios={

'datos_semana' : datos_semana,

'datos_mes' : datos_mes

}

resultado_html=await leer_machote('./btc_analisis_compra_venta_machote.html',cambios)

// seteamos el encabezado de la respuesta

res.writeHead(200, { 'Content-Type': 'text/html' });

// y ahora seteamos el contenido a retornar en HTML para mostrar en el navegador

res.write(resultado_html);

res.end();

break;

case '/aceptar' :

var cantidad=url.parse(req.url,true).query.cantidad;

var transaccion=url.parse(req.url,true).query.transaccion;

console.log('parametro cantidad: ',cantidad, ' transaccion: ',transaccion);

var ahora=new Date();

var fecha_hora_inicial=ahora.getFullYear().toString()+"-"+String(ahora.getMonth()+1).padStart(2, '0')+"-"+

String(ahora.getDate()).padStart(2,'0')+"T"+

String(ahora.getHours()).padStart(2,'0')+":"+String(ahora.getMinutes()).padStart(2,'0')

var cambios={

'cantidad' : cantidad,

'transaccion' : transaccion,

'fecha_hora_inicial' : fecha_hora_inicial

}

resultado_html=await leer_machote('./btc_trans_resultado_machote.html',cambios)


// seteamos el encabezado de la respuesta

res.writeHead(200, { 'Content-Type': 'text/html' });

// y ahora seteamos el contenido a retornar en HTML para mostrar en el navegador

res.write(resultado_html);

res.end();


break;


case '/recordar' :

var recordatorio=url.parse(req.url,true).query.recordatorio;

var transaccion=url.parse(req.url,true).query.transaccion;

var cantidad=url.parse(req.url,true).query.cantidad;


console.log('parametro recordatorio: ',recordatorio);

var fecha_hora=recordatorio;

fecha_hora = fecha_hora.replace(/-/g, '');

fecha_hora = fecha_hora.replace(/:/g, '');

fecha_hora = fecha_hora+'00'

var banda_horaria=Intl.DateTimeFormat().resolvedOptions().timeZone;

var nombre_evento=transaccion+" "+cantidad+" BTC"

console.log("Banda horaria: ",banda_horaria," nombre evento: ",nombre_evento)

var cambios={

'banda_horaria' : banda_horaria,

'nombre_evento' : nombre_evento,

'fecha_hora' : fecha_hora

}

var resultado_ics=await leer_machote('./BTC_transaccion_machote.ics',cambios)

var nombre_archivo="BTC_Transaccion_"+fecha_hora+".ics"

fs.writeFile(dir_recordatorios+nombre_archivo, resultado_ics, 'utf8', (err) => {

if (err) {

console.log(err);

res.end("Error escribiendo archivo .ics")

}

else {

console.log("File written successfully\n");

res.end("Archivo evento "+nombre_evento+" para: "+fecha_hora+" "+banda_horaria+" fue creada en el directorio "+dir_recordatorios)


}

}

);


break;

default :

res.end("Operacion no soportada");

break;

};

});

server.listen(5000); // por aqui estamos escuchando cualquier solicitud al servidor web

console.log('Servidor web de Node.js corriendo en puerto 5000. Acceda con navegador: http://localhost:5000');


btc_analisis_compra_venta_machote.html


<script type="text/javascript" src="https://www.gstatic.com/charts/loader.js"></script>

<h1>Precio Bitcoin</h1>

<h2>Semana</h2>

<div id="chart1_div"></div>

<h2>Mes</h2>

<div id="chart2_div"></div>

<p>

<br>

Especifica abajo le monto de BTC que quieres vender o comprar <br>

</p>


<form class="form-inline" action="/aceptar">

<label for="cantidad">Cantidad:</label>

<input type="text" id="cantidad" placeholder="0" name="cantidad">

<br>

<br>

<input type="radio" name="transaccion" value="compra"> Compra

<input type="radio" name="transaccion" value="venta" checked> Venta

<br>

<br>

<button type="submit">Aceptar</button>

</form>


<script>


google.charts.load('current', {packages: ['corechart', 'line']});

google.charts.setOnLoadCallback(drawBackgroundColor);


function drawBackgroundColor() {


// mas detalles de como usar la libreria de graficas que usamos aca

// en: https://developers.google.com/chart/interactive/docs


var data1 = new google.visualization.DataTable();

data1.addColumn('datetime', 'X');

data1.addColumn('number', 'Precio');


data1.addRows({{datos_semana}});


var data2 = new google.visualization.DataTable();

data2.addColumn('datetime', 'X');

data2.addColumn('number', 'Precio');


data2.addRows({{datos_mes}});

var options = {

hAxis: {

title: 'Fecha'

},

vAxis: {

title: 'Precio USD'

},

backgroundColor: '#f1f8e9'

};


var chart1 = new google.visualization.LineChart(document.getElementById('chart1_div'));

chart1.draw(data1, options);


var chart2 = new google.visualization.LineChart(document.getElementById('chart2_div'));

chart2.draw(data2, options);


}


</script>


btc_trans_resultado_machote.html


<h1>Transaccion {{transaccion}} de {{cantidad}} BTC</h1>

<p>

<br>

Puedes usar cualquiera de estos sitios: <br>

</p>

<div>


<br>

<a href="https://www.kraken.com/es-es/">Kraken</a>

<br>

<a href="https://www.binance.com/es">Binance</a>

<br>

<br>

</div>

<form class="form-inline" action="/recordar">

<br>

<button type="submit">Recordar</button>

<br>

<input type="hidden" value={{transaccion}} name="transaccion">

<input type="hidden" value={{cantidad}} name="cantidad">

<label for="recordatorio">Fecha y hora recordatorio:</label>

<input type="datetime-local" id="recordatorio" name="recordatorio" value="{{fecha_hora_inicial}}" min="{{fecha_hora_inicial}}">

<br>

<br>

</form>


Archivo: BTC_transaccion_machote.ics


BEGIN:VCALENDAR

VERSION:2.0

CALSCALE:GREGORIAN

BEGIN:VEVENT

SUMMARY:{{nombre_evento}}

DTSTART;TZID={{banda_horaria}}:{{fecha_hora}}

DTEND;TZID={{banda_horaria}}:{{fecha_hora}}

LOCATION:https://www.coinbase.com/es-LA https://www.kraken.com/es-es/ https://www.binance.com/es

DESCRIPTION: {{nombre_evento}}

STATUS:CONFIRMED

SEQUENCE:3

BEGIN:VALARM

TRIGGER:-PT10M

DESCRIPTION:{{nombre_evento}}

ACTION:DISPLAY

END:VALARM

END:VEVENT

END:VCALENDAR


El programa contenido en “servidor_mercado_btc.js” es una evolución de lo que se ha trabajado en los capítulos anteriores.


Para ejecutarlo y ver las páginas mostradas anteriormente, después de crear los 4 archivos especificados arriba y guardarlos en el mismo directorio. Solo hay que ejecutar “servidor_mercado_btc.js” asi:


$ node servidor_mercado_btc.js


Luego, con un navegador en la máquina donde se está ejecutando el programa, accedes a esta dirección: http://localhost:5000


NOTA: Si estás usando MacOS Monterrey o más reciente, hay un servicio llamado "Receptor de AirPlay” que también usa puerto 5000 y, si esta corriendo, puede que te cause recibir un error de node.js asi:


Error: listen EADDRINUSE: address already in use :::5000


Para solucionarlo, puede apagar el receptor de AirPlay como se indica acá: https://support.apple.com/es-lamr/guide/mac-help/mchl15c9e4b5/mac


Por razones de simplicidad, se recomienda sencillamente cambiar el puerto a usar para el programa en la línea 204:


server.listen(5000); // por aqui estamos escuchando cualquier solicitud al servidor web


Una vez efectuado el cambio del puerto 5000 a otro valor; como por ejemplo 5500 para evitar el conflicto, entonces debe hacerse el mismo cambio en la dirección a usar en el navegador para lanzarlo. En este caso sería entonces http://localhost:5500


El receptor de AirPlay no es el único servicio/programa que puede estarte causando conflictos y también puede ocurrir en Windows o Linux entonces siempre hay que estar listo para encontrar el conflicto y apagar el otro servicio que lo esté usando o hacer el cambio en nuestro programa.