Mattia Zanella
Posted on November 10, 2020
TL;DR
Quello che andremo a creare in questo articolo è più o meno questo:
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 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) ma forse dovrei dire NVM - Yarn
- Password e nome utente del portale Iliad
- Un account email richiesto per poter inviare le email. Account richiesto da nodeMailer
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:
# .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
"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
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.
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
:
[
'\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:
A questo punto mi sono creato una funzione di cleaning in un file apposito utils/clean.js
:
/**
* @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:
...
// 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:
[
[ '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:
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
/**
* @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 è:
{ current: 0.06481, max: 30 }
Calcoliamo ora il consumo effettivo in %
...
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).
// 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:
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:
// 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
...
iliadScraper()
...
a
...
// 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
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.
Posted on November 10, 2020
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.