Automazione Iliad con Node.js e puppeteer.
Tags: iliad, node, puppeteer, cronjob
Published: 2020-11-10 10:10:26

---
title: Automazione Iliad con Node.js e puppeteer.
published: true
description: Creare un cron-job con node.js & puppeteer per verificare lo stato attuale dei GIGA consumati dall'utenza Iliad.
tags: iliad,node,puppeteer, cronjob
cover_image: https://www.mondomobileweb.it/wp-content/uploads/2019/01/Iliad-messaggio-e1546463410748.jpg
---

# TL;DR
Quello che andremo a creare in questo articolo è **più o meno** questo:
{% vimeo 476218740 %}
Ho scritto *più o meno* perché il video è puramente esemplificativo ed esula le seguenti ragioni:
- Sarà un cron-job, quindi non una cosa da lanciare manualmente ogni volta.
- Solo se l'utilizzo corrente è maggiore a una `X%` (ora impostata a 70) allora si riceve la mail.

# Intro
Una delle cose che non ho mai digerito della mia giovinezza è stata quella di aver speso delle cospique decine di euro per acquistare un Raspberry Pi 3 e averlo usato principalmente per raccogliere la polvere.

Non tutti i mali vengono per nuocere, dicono, e in questo periodo di pandemia spesso mi sono ritrovato a lavorare *da remoto* facendo affidamento solo ed esclusivamente al [tethering](https://it.wikipedia.org/wiki/Tethering) del mio telefono.
Ed è qui che nasce l'esigenza di tenere monitorati i consumi effettivi del mio piano Ilaid.
> Piccolo disclaimer: la mia è la 1° offerta uscita di Iliad e beneficio di un tetto massimo di 30GB al mese. Se sei anche tu un utente Iliad probabilmente avrai quella più recente da 50.

Come puoi vedere dall'immagine in copertina, Iliad offre un servizio analogo ma solo quando arrivi al 100% di quei 30 o 50GB. Servizio utile come...lascio a te il sillogismo migliore.

# Requisiti per lo script
- Node.js ([versioni `LTS` consigliate](https://nodejs.org/en/about/releases/)) ma forse dovrei dire [NVM](https://github.com/nvm-sh/nvm)
- [Yarn](https://classic.yarnpkg.com/en/docs/install/#debian-stable)
- Password e nome utente del portale [Iliad](https://www.iliad.it/account/)
- Un account email richiesto per poter inviare le email. Account richiesto da [nodeMailer](https://nodemailer.com/about/#example)

#### Percisazione prima di iniziare:
> Il mio target device sarà un raspberry ma può essere tranquillamente il tuo pc.

#Let's rock 🤘
Da terminale:
```
mkdir iliad-script && cd $_
yarn init -y

// Dependencies required:
yarn add -D dotenv node-cron nodemailer puppeteer
```
Una volta installate le dipendenze, il 1° step da fare è quello di creare un file `.env` all'interno del repo poc'anzi creato.
E' un file **essenziale** e deve avere le seguenti variabili:
```yml
# .env
PUPPETEER_PRODUCT=chrome

NODE_ENV=development

ILIAD_LOGIN_PAGE=https://www.iliad.it/account/
ILIAD_USER=YOUR_AMAZING_USERNAME
ILIAD_PASS=YOUR_AMAZING_PASS

# ATTENZIONE: Mail DA dove si inviano le notifiche 👇 (vedi requisiti)
# Nel mio caso è NETSONS
NODEMAILER_HOST=srv-hp4.netsons.net
NODEMAILER_MAIL=YOUR_AMAZING_EMAIL
NODEMAILER_PASS=YOUR_AMAZING_EMAIL_PASS

# ATTENZIONE: Mail dove si riceve le notifiche 👇
EMAIL_RECIPIENT=my_amazing_email@icloud.com

PERCENTACE_GUARD_DEVELOPMENT=5
PERCENTACE_GUARD_PRODUCTION=70
```
Il 2° step è quello di aggiungere gli script di yarn al `package.json`
```json
  "scripts" :{
    "start": "node index.js",
    "dev": "node index.js"
  },
```

A questo punto possiamo iniziare a scrivere il pezzo forte dello script e creiamo un file `index.js`

```javascript
require("dotenv").config()

const puppeteer = require("puppeteer")
const mailManager = require('./mailManager.js')

const iliadScraper = async () => {
  const browser = await puppeteer.launch()
  const page = await browser.newPage()

  try {
    await page.goto(process.env.ILIAD_LOGIN_PAGE)

    await page.type("input[type=text]", process.env.ILIAD_USER)
    await page.type("input[type=password]", process.env.ILIAD_PASS)
    await page.click("button[type=submit]")

    await page.waitForNavigation({ waitUntil: "networkidle0" })

    const fatherSelector = ".conso-infos.conso-local"
    await page.waitForSelector(fatherSelector)

    const gridChildValues = await page.$$(
      `${fatherSelector} .conso__content .conso__text`
    )

    const chain = gridChildValues.map((child) => {
      return page.evaluate((element) => element.textContent, child)
    })
    const res = await Promise.all(chain)
    console.log(res)
  } catch (error) {
    console.error(`ERROR::${JSON.stringify(error, null, 2)}`)
    browser.close()
  }
}

iliadScraper()
```

Trovo verboso dover spiegare ogni metodo di puppeteer quando è già presente una [documentazione abbastanza esaustiva](https://pptr.dev/#?product=Puppeteer&version=v5.4.1&show=api-class-puppeteer).
In sonstanza le operazioni sono:
- Avvio di chrome in headless-mode (default è `true`).
- Indirizzo chrome alla pagina di login di Iliad (definito in `.env` file).
- Inserimento di user e pass del profilo Iliad (definito in `.env` file).
- Click del bottone di login.
- Attesa della dashboard.

A questo punto mi soffermerei sul log della costante `res`:
```javascript
[
  '\n' +
    '                     Chiamate: 1m 34s\n' +
    '                     Consumi voce: 0.00€\n' +
    '                  ',
  '1 SMS\n                     SMS extra: 0.00€\n                  ',
  '\n' +
    '                     64,81mb / 30GB\n' +
    '                     Consumi Dati: 0.00€\n' +
    '                  ',
  '\n' +
    '                     0 MMS\n' +
    '                     Consumi MMS: 0.00€\n' +
    '                  '
]
```

L'array è la seguente rappresentazione dei box evidenziati:

![Boxes](https://dev-to-uploads.s3.amazonaws.com/i/c8sdni9bc7npxbl7q4nj.jpg)

A questo punto mi sono creato una funzione di *cleaning* in un file apposito `utils/clean.js`:
```javascript
/**
 * @description Since ILIAD's DOM has no ID or specific class for the consume
 * We have to parse all the grid 2x2.
 * @param {Array} arrayResponses
 * @returns {Array}
 */
const clean = (arrayResponses) =>
  arrayResponses.map((response) =>
    response
      .split("\n")
      .map((s) => s.trim())
      .filter(Boolean)
  );

module.exports = clean;
```
Che successivamente importo in `index.js` e lo uso per avere una risposta più esaustiva:

```javascript

...
// Custom utils.
const clean = require('./utils/clean.js')
...
    const res = await Promise.all(chain)
    const cleanedRes = clean(res)
...
```
A questo punto `cleanedRes` diventa nel seguente modo:
```javascript
[
  [ 'Chiamate: 1m 34s', 'Consumi voce: 0.00€' ],
  [ '1 SMS', 'SMS extra: 0.00€' ],
  [ '64,81mb / 30GB', 'Consumi Dati: 0.00€' ],
  [ '0 MMS', 'Consumi MMS: 0.00€' ]
]
```
Sappiamo che il figlio desiderto per calcolare il **consumo corrente** è il 3°, quindi:
```javascript
const extract = require('./utils/extract.js')
...
    // We know that consume is the third child.
    const splittedConsumeValues = cleanedRes[2][0].split("/").map(val => val.trim())
    const consume = {
      current: extract(splittedConsumeValues[0]),
      max: extract(splittedConsumeValues[1])
    }
    console.log(consume)
...
```
È l'occasione per creare una 2° utility `utils/extract.js`
```javascript
  /**
   * @description Given a plain text from DOM, extracting the number.
   * @param {String} str Like 2,64GB or plain 20GB.
   * Attention! can be also something like...200mb, 402.34mb
   * @returns {Number} Pay attention to comma vs dot.
   */
  const extractValue = str => {
    const unit = str.match(/[a-z]+/ig).join()
    const digits = str.match(/\d+/ig).join('.')

    return unit === 'mb'
      ? (parseFloat(digits) / 1000)
      : parseFloat(digits)
  }

  module.exports = extractValue
```
Che permette di estrarre consumo corrente e il tetto massimo dell'offerta (inseriti poi nell'oggetto `consume`).
L'oggetto consume ora è:
```javascript
{ current: 0.06481, max: 30 }
```
Calcoliamo ora il **consumo effettivo in %**
```javascript
...
    const currentProgress = Math.round(consume.current / consume.max * 100)
...
```

A questo punto abbiamo `0` come consumo in %.
Da qui in poi aggiungo una 3° utility per prevenire l'invio della mail se siamo sotto a una determinata % (definito in `.env` file).
```javascript
// utils/mustKillFlow.js

  /**
   * @description This is a utility guard to kill the MAIL flow in specific cases.
   * Pay attentio to PERCENTACE_GUARD, it's a default value fixed to 70.
   * By doing so we're sending email-alert ONLY if the user is over 70% of his GIGA.
   * @param {Number | any} percentage SHOULD be a number of current consume.
   * @returns {Boolean}
   */
  const mustKillFlow = percentage => {
    const alertLimit = process.env.NODE_ENV === 'development'
      ? process.env.PERCENTACE_GUARD_DEVELOPMENT
      : process.env.PERCENTACE_GUARD_PRODUCTION

    return !percentage || isNaN(percentage) || (percentage <= alertLimit)
  }

  module.exports = mustKillFlow
```
e in index.js avrò dunque:
```javascript
const mustKillFlow = require('./utils/mustKillFlow.js')
...
    const currentProgress = Math.round(consume.current / consume.max * 100)
    if ( mustKillFlow(currentProgress) ) return browser.close()

    const mailOptions = {
      from: process.env.NODEMAILER_MAIL,
      to: process.env.EMAIL_RECIPIENT,
      subject: 'Report from iliad cron-job ⏰.',
      html: `<p>📵 Your're at more or less ${currentProgress}% of your GIGA. 📵</p>`
    }

    mailManager.sendEmail(mailOptions).catch(console.error)
    browser.close()
} catch(error) {
...
```
Rimane solo da definire il file:
```javascript
// mailManager.js, è in root.
const nodemailer = require("nodemailer");

module.exports = {
	async sendEmail(mailOptions) {
		// @see https://ethereal.email/
		const transporter = nodemailer.createTransport({
			host: process.env.NODEMAILER_HOST,
			port: 465,
			secure: true, // true for 465, false for other ports
			auth: {
				user: process.env.NODEMAILER_MAIL,
				pass: process.env.NODEMAILER_PASS
			}
		});
		
		const sent = await transporter.sendMail(mailOptions);

		console.log("Message sent: %s", sent.messageId);
		// Message sent: <b658f8ca-6296-ccf4-8306-87d57a0b4321@example.com>
	}    
}
```
Bene, per finalità di utilizzo cambiamo l'ultima parte di `index.js` da
```javascript
...
iliadScraper()
...
```
a
```javascript
...
// It will run at 1 A.M and 1 P.M
const timing = process.env.NODE_ENV === 'development' ? '* * * * *' : '00 01,13 * * *'
cron.schedule(timing, iliadScraper)
```
E nel file `.env` cambiamo la variabile 
```yml
NODE_ENV=production
```

Perfetto, il tutorial è concluso.
Fintanto che terrete in vita il processo, il cron continuerà a girare 2 volte al giorno e verificherà che la vostra soglia non superi il limite impostato.