diff --git a/.env b/.env new file mode 100644 index 0000000..2b293fd --- /dev/null +++ b/.env @@ -0,0 +1,3 @@ +BOT_TOKEN=1120382460:AAGy8CdhjdMxaS99K3za1Jxoqp-ayPuVC1w +URL_API=http://core.host.redroundrobin.site:9999 +SERVER_PORT=3000 \ No newline at end of file diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..30ddbbb --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +*.js text eol=lf \ No newline at end of file diff --git a/.gitignore b/.gitignore index 5daf6b0..c7afe10 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,5 @@ node_modules .DS_Store -.env - .idea target/ out/ @@ -9,9 +7,4 @@ coverage/ *.iws *.iml *.ipr - -Icon - -Icon - -.env +Icon diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..1113212 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,6 @@ +# docker run --run -d -p 9999:9999 rrr/api +FROM node:12.16.2-alpine3.11 +COPY . /usr/src/telegram +EXPOSE 3000 +WORKDIR /usr/src/telegram +CMD ["sh", "start.sh"] diff --git a/README.md b/README.md index c581d9b..7c188fd 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ # Telegram Bot - ThiReMa -:fire: Versione componente: `v0.1.0-dev` +:fire: Versione componente: `v0.2.0-rc` :pushpin: Main repo: [swe-thirema](https://github.com/Maxelweb/swe-thirema) @@ -34,3 +34,18 @@ - entrare nella cartella con il terminale - $ npm install - $ node bot + + +#### Comandi per code style JS + +- Check: +`npm run prettier-eslint-check` +`npm run prettier-eslint-test-check` +- Autofix: +`npm run prettier-eslint` +`npm run prettier-eslint-test` + + +#### Comandi per i test JS + +`npm test` diff --git a/__tests__/bot.test.js b/__tests__/bot.test.js index 73c4fc4..22cb79a 100644 --- a/__tests__/bot.test.js +++ b/__tests__/bot.test.js @@ -1,7 +1,31 @@ -// NON RIMUOVERE LA RIGA SOTTOSTANTE // -require("../main"); +// test bot +const { botStart } = require("../commands/start"); +const { botInfo } = require("../commands/info"); +const { botLogin } = require("../commands/login"); +const { botHelp } = require("../commands/help"); -test("Extracts value from JSON formatted string", () => { - const num = 4; - expect(num).toBe(4); +const Telegraf = require("telegraf"); +const tokenBot = process.env.BOT_TOKEN; +const bot = new Telegraf(tokenBot); + +// LOGIN +test("Check login", () => { + expect(botLogin(bot)).toBe(undefined); +}); +// LAUNCH +test("Check info", () => { + expect(botInfo(bot)).toBe(undefined); +}); +// START +test("Check status", () => { + expect(botStart(bot)).toBe(undefined); +}); +// INFO +test("Check help", () => { + expect(botHelp(bot)).toBe(undefined); }); +// const s = {"botLaunch": [Function botLaunch]`; +// const t = JSON.parse(s); +// test("Check launch", () => { +// expect(botLaunch).toBe(t); +// }); diff --git a/__tests__/server.test.js b/__tests__/server.test.js new file mode 100644 index 0000000..3e73203 --- /dev/null +++ b/__tests__/server.test.js @@ -0,0 +1,46 @@ +const { sendMessage, checkChatId } = require("../utils/server"); + +test("Send message to invalid chat id", () => { + const message = "test"; + const chatId = "aaaaa"; + try { + sendMessage(message, chatId); + } catch (e) { + expect(e.message).toBe("Errore 400 nell'invio del messaggio"); + } +}); + +test("Send message to valid chat id", () => { + const message = "test"; + const chatId = "192645345"; + try { + sendMessage(message, chatId); + expect(message).toBe("test"); + } catch (e) { + console.log("Errore nell'invio del messaggio"); + } +}); + +test("Check chat id with invalid characters", () => { + const invalidChatId = "a48422329"; + expect(checkChatId(invalidChatId)).toBe(false); +}); + +test("Check chat id with invalid length", () => { + const invalidChatId = "167"; + expect(checkChatId(invalidChatId)).toBe(false); +}); + +// eslint-disable-next-line require-jsdoc +function getRandomArbitrary(min, max) { + return Math.floor(Math.random() * (max - min + 1) + min); +} +const chatId = getRandomArbitrary(100000000, 999999999); +test("Check valid chat id", () => { + expect(checkChatId(chatId)).toBe(true); +}); + +// const req = { metod: "POST", reqType: "authentication" }; +// test("Check authentication request", () => { +// expect(botServer.toString()).toBe("[object Object]"); +// }); diff --git a/commands/devices.js b/commands/devices.js new file mode 100644 index 0000000..2c945cc --- /dev/null +++ b/commands/devices.js @@ -0,0 +1,154 @@ +const axios = require("axios"); +const Markup = require("telegraf/markup"); + +const botDevices = (bot, auth) => { + bot.command("devices", (message) => { + const username = message.from.username; + const axiosInstance = axios.create(); + let admin = false; + let deviceList = []; + const getDeviceList = async () => { + await auth.jwtAuth(axiosInstance, message); + await axiosInstance + .get(`${process.env.URL_API}/users?telegramName=${username}`) + .then((res) => { + const data = res.data[0]; + const typeNumber = data.type; + if (typeNumber === 2) { + admin = true; + } else { + admin = false; + return message.reply( + "Devi essere amministratore per vedere la lista dispositivi" + ); + } + }); + if (admin) { + await axiosInstance + .get(`${process.env.URL_API}/devices?cmdEnabled=true`) + .then((res) => { + const devices = res.data; + devices.forEach((device) => { + deviceList.push( + Markup.callbackButton( + `${device.name}-D#${device.deviceId}`, + device.name + ) + ); + }); + deviceList.push(Markup.callbackButton("Annulla \u{274C}")); + }); + } + }; + + getDeviceList() + .then(() => { + if (deviceList.length !== 0) { + message.reply( + "Seleziona il dispositivo a cui inviare un comando:", + Markup.keyboard(deviceList).oneTime().resize().extra() + ); + deviceList = []; + } + }) + .catch(() => { + message.reply( + "Errore, non hai i permessi per eseguire questa azione, oppure il servizio non è al momento disponibile." + ); + }); + + // user has selected one device + bot.hears(/^(.*)(-)(D#\d{1,11})$/g, (message) => { + const deviceID = message.match[0].match(/(\d{1,11})$/g); + let sensorsList = []; + const axiosInstance = axios.create(); + const getButtons = async () => { + await auth.jwtAuth(axiosInstance, message); + return axiosInstance + .get( + `${process.env.URL_API}/devices/${deviceID}/sensors?cmdEnabled=true` + ) + .then((res) => { + const sensors = res.data; + sensors.forEach((sensor) => { + sensorsList.push( + Markup.callbackButton( + `${sensor.type}-D#${deviceID}-S#${sensor.sensorId}`, + sensor.type + ) + ); + }); + sensorsList.push(Markup.callbackButton("Annulla \u{274C}")); + }); + }; + getButtons().then(() => { + Markup.removeKeyboard(); + message.reply( + "Seleziona il sensore:", + Markup.keyboard(sensorsList).oneTime().resize().extra() + ); + sensorsList = []; + }); + }); + + // user has selected one sensor + bot.hears(/^(.*)(-)(D#\d{1,11})(-)(S#\d{1,11})$/g, (message) => { + const deviceSensorID = message.match[0].match(/(#\d{1,11})/gi); + message.reply( + "Seleziona un input da inviare al comando:", + Markup.keyboard([ + `\u{1F7E2} Attiva_D${deviceSensorID[0]}-S${deviceSensorID[1]}`, + `\u{1F534} Disattiva_D${deviceSensorID[0]}-S${deviceSensorID[1]}`, + `Annulla \u{274C}`, + ]) + .oneTime() + .resize() + .extra() + ); + }); + // user has selected to switch on one sensor + bot.hears( + /^(.*)(Attiva|Disattiva)(_)(D#\d{1,11})(-)(S#\d{1,11})$/g, + (message) => { + const action = message.match[0].match(/(Attiva|Disattiva)/g)[0]; + const deviceID = message.match[0].match(/(\d{1,11})/gi)[0]; + const sensorID = message.match[0].match(/(\d{1,11})$/gi); + const axiosInstance = axios.create(); + const sendCommandToAPI = async (realCommand) => { + await auth.jwtAuth(axiosInstance, message); + await axiosInstance + .put(`${process.env.URL_API}/sensors/${sensorID}`, { + data: realCommand, + }) + .then((res) => { + message.reply( + `\u{2705} Hai richiesto ${ + action === "Attiva" ? "l'attivazione" : "la disattivazione" + } del sensore #${sensorID} del dispositivo #${deviceID}`, + Markup.removeKeyboard().extra() + ); + }) + .catch((err) => { + message.reply( + `[Errore] La richiesta *non* è andata a buon fine. La configurazione potrebbe essere cambiata. Riprova.`, + Markup.removeKeyboard().extra()); + }); + }; + switch (action) { + case "Attiva": + sendCommandToAPI(1); + break; + case "Disattiva": + sendCommandToAPI(0); + break; + } + } + ); + + // user has selected to switch off one sensor + bot.hears(/(Annulla .*)/g, (message) => { + message.reply(`Operazione annullata`, Markup.removeKeyboard().extra()); + }); + }); +}; +module.exports.botDevices = botDevices; diff --git a/commands/help.js b/commands/help.js new file mode 100644 index 0000000..9ad516f --- /dev/null +++ b/commands/help.js @@ -0,0 +1,25 @@ +const botHelp = (bot) => { + bot.command("help", ({ replyWithMarkdown }) => { + replyWithMarkdown(` +*RIoT - Telegram Bot* + +Ecco la lista dei comandi disponibili: + +- /help - informazioni di supporto (corrente) +- /login - primo avvio e registrazione account +- /info - informazioni utente corrente +- /devices - dispositivi a cui è possibile inviare input + +*Procedura di autenticazione Telegram* + +1. Accedere alla web-app +2. Spostarsi nella sezione *impostazioni* +3. Compilare il campo Telegram nel primo modulo delle impostazioni +4. Tornare al bot Telegram +5. Eseguire il comando /login + +In caso di problemi con la procedura, contattare il proprio moderatore ente. + `); + }); +}; +module.exports.botHelp = botHelp; diff --git a/commands/info.js b/commands/info.js index c64448a..e990ee2 100644 --- a/commands/info.js +++ b/commands/info.js @@ -1,11 +1,40 @@ -const botInfo = (bot) => { - bot.command("info", ({ replyWithMarkdown }) => - replyWithMarkdown(` -Ecco la lista dei comandi disponibili: -- Login: /login -- Status: /status -- Info: /info` - ) - ); +const axios = require("axios"); + +const botInfo = (bot, auth) => { + bot.command("info", (message) => { + const username = message.from.username; + const axiosInstance = axios.create(); + const getUserInfo = async () => { + await auth.jwtAuth(axiosInstance, message); + return await axiosInstance + .get(`${process.env.URL_API}/users?telegramName=${username}`) + .then((res) => { + const data = res.data[0]; + const name = data.name; + const surname = data.surname; + const email = data.email; + const typeNumber = data.type; + let type = "Utente"; + if (typeNumber === 1) { + type = "Moderatore"; + } else if (typeNumber === 2) { + type = "Amministratore"; + } + return message.replyWithMarkdown( + ` + Ecco i tuoi dati *${message.from.username}* + - *Nome:* ${name} + - *Cognome:* ${surname} + - *Email:* ${email} + - *Tipo:* ${type}` + ); + }) + .catch((err) => { + console.log(err); + message.reply("Errore nel controllo dei dati!"); + }); + }; + return getUserInfo(); + }); }; -module.exports.botInfo = botInfo; \ No newline at end of file +module.exports.botInfo = botInfo; diff --git a/commands/launch.js b/commands/launch.js deleted file mode 100644 index 545a88e..0000000 --- a/commands/launch.js +++ /dev/null @@ -1,4 +0,0 @@ -const botLaunch = (bot) => { - bot.launch(); -}; -module.exports.botLaunch = botLaunch; \ No newline at end of file diff --git a/commands/login.js b/commands/login.js index 0d9163c..ce876f1 100644 --- a/commands/login.js +++ b/commands/login.js @@ -1,44 +1,9 @@ -// Richieste http const axios = require("axios"); -const botLogin = (bot) => { - bot.command("login", (message) => { - const username = message.from.username; - const chatId = message.from.id; - axios - .post(`http://localhost:9999/auth/telegram`, { - telegramName: username, - telegramChat: chatId, - }) - .then((res) => { - const code = res.data.code; - const token = res.data.token; - - if (code === 1) { - axios.defaults.headers.common["Authorization"] = "Bearer " + token; - return message.reply("Username trovato, registrazione riuscita"); - } else if (code === 2) { - axios.defaults.headers.common["Authorization"] = "Bearer " + token; - return message.reply( - "Account già registrato, nessuna modifica apportata" - ); - } else if (code === 0) { - return message.reply( - "Username non trovato, registra il tuo Username dalla web-app" - ); - } - }) - .catch((err) => { - console.log(err); - // console.log(err.status); - if (err.response.status === 403) { - return message.reply( - "Rieffettua l'autenticazione usando il comando /login" - ); - } else { - return message.reply("Errore nel controllo dei dati"); - } - }); - }); +const botLogin = (bot, auth) => { + bot.command("login", (message) => { + const axiosInstance = axios.create(); + auth.jwtAuth(axiosInstance, message, true); + }); }; -module.exports.botLogin = botLogin; \ No newline at end of file +module.exports.botLogin = botLogin; diff --git a/commands/start.js b/commands/start.js index 6a10402..ea0dd09 100644 --- a/commands/start.js +++ b/commands/start.js @@ -1,13 +1,12 @@ const botStart = (bot) => { - bot.start((message) => { - console.log("started:", message.from.id); - const username = message.from.username; - return message.replyWithMarkdown(` -Ciao *${username}*, benvenuto nel bot di ThiReMa! + bot.start((message) => { + console.log("started:", message.from.id); + const username = message.from.username; + return message.replyWithMarkdown(` +Ciao *${username}*, benvenuto nel RIoT bot! Usa il comando /login per effettuare l'autenticazione. -Per vedere la lista del comandi che puoi utilizzare usa il comando /info` - ); - }); +Se hai bisogno di aiuto digita il comando /help`); + }); }; -module.exports.botStart = botStart; \ No newline at end of file +module.exports.botStart = botStart; diff --git a/commands/status.js b/commands/status.js deleted file mode 100644 index da77b24..0000000 --- a/commands/status.js +++ /dev/null @@ -1,38 +0,0 @@ -// Richieste http -const axios = require("axios"); - -const botStatus = (bot) => { - bot.command("status", (message) => { - axios - .get(`http://localhost:9999/status`) - .then((res) => { - const data = res.data; - const name = data.name; - const surname = data.surname; - const email = data.email; - const typeNumber = data.type; - let type = "Utente"; - if (typeNumber === 1) { - type = "Moderatore"; - } else if (typeNumber === 2) { - type = "Amministratore"; - } - return message.replyWithMarkdown( - ` - Ecco i tuoi dati *${message.from.username}* - - *Nome:* ${name} - - *Cognome:* ${surname} - - *Email:* ${email} - - *Tipo:* ${type}` - ); - }) - .catch((err) => { - if (err.response.status === 403) { - message.reply("Rieffettua l'autenticazione usando il comando /login"); - } else { - message.reply("Errore nel controllo dei dati"); - } - }); - }); -}; -module.exports.botStatus = botStatus; \ No newline at end of file diff --git a/main.js b/main.js index 46d5411..3b56b7c 100644 --- a/main.js +++ b/main.js @@ -1,26 +1,27 @@ -require("dotenv").config(); +require("./utils/config"); const Telegraf = require("telegraf"); +const bot = new Telegraf(process.env.BOT_TOKEN); -// Tokenbot e creazione bot -const tokenBot = process.env.BOT_TOKEN; -const bot = new Telegraf(tokenBot); +const { botServer } = require("./utils/server"); +const auth = require("./utils/auth"); +const cmdStart = require("./commands/start"); +const cmdHelp = require("./commands/help"); +const cmdLogin = require("./commands/login"); +const cmdInfo = require("./commands/info"); +const cmdDevices = require("./commands/devices"); -const server = require("./server"); -const botLaunch = require("./commands/launch"); -const botStart = require("./commands/start"); -const botInfo = require("./commands/info"); -const botLogin = require("./commands/login"); -const botStatus = require("./commands/status"); -// const botStart = require("./commands"); +cmdStart.botStart(bot); +cmdHelp.botHelp(bot); +cmdLogin.botLogin(bot, auth); +cmdInfo.botInfo(bot, auth); +cmdDevices.botDevices(bot, auth); -// Comandi bot -botStart.botStart(bot); -botInfo.botInfo(bot); -botLogin.botLogin(bot); -botStatus.botStatus(bot); +botServer.listen(process.env.SERVER_PORT); +console.log( + "[Telegram] Server di ascolto per API avviato (porta " + + process.env.SERVER_PORT + + ")" +); -server.botServer.listen(3000, "127.0.0.1"); -console.log("Server to port 3000"); - -botLaunch.botLaunch(bot); -console.log("Bot avviato correttamente"); +bot.launch(); +console.log("[Telegram] Bot avviato!"); diff --git a/server.js b/server.js deleted file mode 100644 index 42d34e6..0000000 --- a/server.js +++ /dev/null @@ -1,35 +0,0 @@ -// Richiesta per creazione server -const http = require("http"); -// Richieste http -const axios = require("axios"); - -exports.botServer = http.createServer((req, res) => { - // Request and Response object - if (req.method === "POST") { - let jsonRes = ""; - req.on("data", (data) => { - jsonRes += data.toString(); - console.log(JSON.parse(jsonRes)); - const response = JSON.parse(jsonRes); - const chatId = response.chat_id; - const authCode = response.auth_code; - axios - .post( - `https://api.telegram.org/bot${tokenBot}/sendMessage?chat_id=${chatId}&text=Ecco il tuo codice di autenticazione: ${authCode}` - ) - .then(() => { - console.log("Messaggio inviato con successo"); - }) - .catch((err) => { - console.log( - "Errore " + err.response.status + " nell'invio del messaggio" - ); - }); - }); - req.on("end", () => { - console.log(JSON.parse(jsonRes)); - res.end("ok"); - }); - console.log(jsonRes); - } -}); diff --git a/start.sh b/start.sh new file mode 100644 index 0000000..504526d --- /dev/null +++ b/start.sh @@ -0,0 +1,11 @@ +#!/bin/sh + +if [ ! -d "node_modules/" ]; then + + npm install --production + +fi + +node main.js + +echo "Telegram bot started..." diff --git a/utils/auth.js b/utils/auth.js new file mode 100644 index 0000000..80b1f87 --- /dev/null +++ b/utils/auth.js @@ -0,0 +1,43 @@ +const jwtAuth = (axiosInstance, message, displayReply = false) => { + const username = message.from.username; + const chatId = message.from.id; + + return axiosInstance + .post(`${process.env.URL_API}/auth/telegram`, { + telegramName: username, + telegramChat: chatId, + }) + .then((res) => { + const code = res.data.code; + const token = res.data.token; + if (code === 1 || code === 2) { + axiosInstance.defaults.headers.common["Authorization"] = + "Bearer " + token; + } + if (displayReply) { + switch (code) { + case 1: + return message.reply("Username trovato, registrazione riuscita!"); + case 2: + return message.reply( + "Account già registrato, nessuna modifica apportata." + ); + case 3: + return message.reply( + "Username non trovato, registra il tuo username Telegram dalle impostazioni della web-app." + ); + default: + break; + } + } + }) + .catch((err) => { + console.log(err); + if (displayReply) { + return message.reply( + "Errore nel controllo dei dati, prova ad eseguire nuovamente /login o nel caso contatta l'amministrazione." + ); + } + }); +}; +module.exports.jwtAuth = jwtAuth; diff --git a/utils/config.js b/utils/config.js new file mode 100644 index 0000000..b96d408 --- /dev/null +++ b/utils/config.js @@ -0,0 +1,25 @@ +require("dotenv").config(); +const axios = require("axios"); + +const botCommands = [ + { command: "help", description: "Lista dei comandi del bot e informazioni" }, + { command: "login", description: "Avvio e autenticazione al sistema" }, + { command: "info", description: "Controllo delle informazioni dell'utente" }, + { + command: "devices", + description: "[Admin] Visualizza lista dispositivi a cui inviare input", + }, +]; + +axios + .post(`https://api.telegram.org/bot${process.env.BOT_TOKEN}/setMyCommands`, { + commands: botCommands, + }) + .then(() => { + console.log("[Telegram] Info comandi caricati!"); + }) + .catch(() => { + console.log("[Telegram] Errore caricamento comandi"); + }); + +module.exports.axios = axios; diff --git a/utils/server.js b/utils/server.js new file mode 100644 index 0000000..33055fd --- /dev/null +++ b/utils/server.js @@ -0,0 +1,82 @@ +require("dotenv").config(); +const http = require("http"); +const axios = require("axios"); + +const sendMessage = (message, chatId) => { + axios + .post( + `https://api.telegram.org/bot${process.env.BOT_TOKEN}/sendMessage?chat_id=${chatId}&text=${message}` + ) + .then((res) => { + console.log("Messaggio inviato con successo"); + console.log(res.status); + console.log(res.data); + res.json("OK"); + }) + .catch((err) => { + console.log( + "Errore " + err.response.status + " nell'invio del messaggio" + ); + }); +}; + +const checkChatId = (chatId) => { + const pattern = "^[0-9]{6,}$"; + const regex = new RegExp(pattern); + return regex.test(chatId); +}; + +const botServer = http.createServer((req, res) => { + // Request and Response object + if (req.method === "POST") { + let jsonRes = ""; + req.on("data", (data) => { + jsonRes += data.toString(); + console.log(JSON.parse(jsonRes)); + const response = JSON.parse(jsonRes); + if (response.reqType === "authentication") { + const authCode = response.authCode; + const chatId = response.chatId; + if (!checkChatId(chatId)) { + console.log("Invalid chat id"); + } else { + const authMessage = `Ecco il tuo codice di autenticazione: ${authCode}`; + sendMessage(authMessage, chatId); + } + } else if (response.reqType === "alert") { + const chatIds = response.telegramChatIds; + const deviceId = response.realDeviceId; + const sensorId = response.realSensorId; + const sensorValue = response.currentValue; + const threshold = response.currentThreshold; + let valueType; + switch (response.currentThresholdType) { + case 0: + valueType = "superiore"; + break; + case 1: + valueType = "inferiore"; + break; + case 2: + valueType = "uguale"; + } + const messagePart1 = `Attenzione: il sensore ${sensorId} del dispositivo ${deviceId} ha registrato un valore di `; + const messagePart2 = `${sensorValue} ${valueType} alla soglia (${threshold})`; + const alertMessage = messagePart1 + messagePart2; + // eslint-disable-next-line guard-for-in + for (const index in chatIds) { + const chatId = chatIds[index]; + if (!checkChatId(chatId)) { + console.log("Invalid chat id"); + } else { + sendMessage(alertMessage, chatId); + } + } + } + }); + req.on("end", () => { + res.end("ok"); + }); + } +}); +module.exports = { botServer, checkChatId, sendMessage };