From 52ca0854eb9d894ae10a3f483e5c7ef7600c9500 Mon Sep 17 00:00:00 2001 From: Danylo Bilokha Date: Sat, 18 Apr 2020 14:01:29 +0300 Subject: [PATCH 1/9] Clean up --- .huskyrc.json | 5 + .lintstagedrc.json | 7 + .prettierrc.json | 8 + package-lock.json | 16 ++ package.json | 16 +- .../telegram/botResponse/adviceResponse.ts | 9 +- .../telegram/botResponse/assistantResponse.ts | 87 ++++---- .../telegram/botResponse/availableResponse.ts | 20 +- .../telegram/botResponse/countriesResponse.ts | 151 ++++++++------ .../telegram/botResponse/countryResponse.ts | 87 ++++---- .../bots/telegram/botResponse/noResponse.ts | 27 +-- .../telegram/botResponse/startResponse.ts | 14 +- .../telegram/botResponse/subscribeResponse.ts | 76 +++---- .../botResponse/unsubscribeResponse.ts | 70 +++---- server/src/bots/telegram/index.ts | 137 ++++++------ server/src/bots/telegram/models/index.ts | 45 +--- server/src/bots/telegram/services/keyboard.ts | 131 +++++------- .../services/messageHandlerRegistry.ts | 180 ++++++++++++++++ .../bots/telegram/services/messageRegistry.ts | 165 --------------- .../services/subscriptionNotifierManager.ts | 196 ++++++++++-------- server/src/bots/telegram/utils/chat.ts | 6 +- .../utils/getUserMessageFromIKorText.ts | 31 ++- server/src/index.ts | 34 ++- server/src/models/subscription.models.ts | 6 +- server/src/routes/base/base.ts | 14 +- server/src/services/api/api-covid19.ts | 11 +- server/src/services/api/api-knowledgebase.ts | 76 ++++--- server/src/services/domain/chart.ts | 56 ++--- .../services/domain/countriesByContinent.ts | 23 +- server/src/services/domain/countryLookup.ts | 6 +- server/src/services/domain/covid19.ts | 159 ++++++++------ server/src/services/domain/storage.ts | 87 +++++--- server/src/services/domain/subscriptions.ts | 87 +++++--- .../src/services/infrastructure/firebase.ts | 16 +- .../src/services/infrastructure/scheduler.ts | 20 +- server/src/utils/getErrorMessages.ts | 4 + server/src/utils/getLoggerMessages.ts | 5 - server/src/utils/logger.ts | 20 +- .../utils/removeCommandFromMessageIfExist.ts | 16 +- server/src/utils/textAfterCommand.ts | 3 +- 40 files changed, 1134 insertions(+), 993 deletions(-) create mode 100644 .huskyrc.json create mode 100644 .lintstagedrc.json create mode 100644 .prettierrc.json create mode 100644 server/src/bots/telegram/services/messageHandlerRegistry.ts delete mode 100644 server/src/bots/telegram/services/messageRegistry.ts create mode 100644 server/src/utils/getErrorMessages.ts delete mode 100644 server/src/utils/getLoggerMessages.ts diff --git a/.huskyrc.json b/.huskyrc.json new file mode 100644 index 0000000..4d077c8 --- /dev/null +++ b/.huskyrc.json @@ -0,0 +1,5 @@ +{ + "hooks": { + "pre-commit": "lint-staged" + } +} diff --git a/.lintstagedrc.json b/.lintstagedrc.json new file mode 100644 index 0000000..a64acbd --- /dev/null +++ b/.lintstagedrc.json @@ -0,0 +1,7 @@ +{ + "server/src/**/*.ts": [ + "prettier --write", + "npm run tslint:fix", + "npm run tslint" + ] +} diff --git a/.prettierrc.json b/.prettierrc.json new file mode 100644 index 0000000..5280a73 --- /dev/null +++ b/.prettierrc.json @@ -0,0 +1,8 @@ +{ + "printWidth": 100, + "tabWidth": 4, + "trailingComma": "es5", + "singleQuote": true, + "jsxBracketSameLine": false, + "bracketSpacing": true +} diff --git a/package-lock.json b/package-lock.json index 0104f60..dee1132 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1607,6 +1607,16 @@ "resolved": "https://registry.npmjs.org/@types/node/-/node-13.9.3.tgz", "integrity": "sha512-01s+ac4qerwd6RHD+mVbOEsraDHSgUaefQlEdBbUolnQFjKwCr7luvAlEwW1RFojh67u0z4OUTjPn9LEl4zIkA==" }, + "@types/node-telegram-bot-api": { + "version": "0.40.3", + "resolved": "https://registry.npmjs.org/@types/node-telegram-bot-api/-/node-telegram-bot-api-0.40.3.tgz", + "integrity": "sha512-YJVoP4cMz5m/PG9VLKHTc0pvK6wGmIC5AYScOdlIhe5+vOzcLKuBYgrIIjRujcwCGs99fToE6jW80njXdMoU+A==", + "dev": true, + "requires": { + "@types/node": "*", + "@types/request": "*" + } + }, "@types/parse-json": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.0.tgz", @@ -10166,6 +10176,12 @@ "resolved": "https://registry.npmjs.org/prepend-http/-/prepend-http-1.0.4.tgz", "integrity": "sha1-1PRWKwzjaW5BrFLQ4ALlemNdxtw=" }, + "prettier": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.0.4.tgz", + "integrity": "sha512-SVJIQ51spzFDvh4fIbCLvciiDMCrRhlN3mbZvv/+ycjvmF5E73bKdGfU8QDLNmjYJf+lsGnDBC4UUnvTe5OO0w==", + "dev": true + }, "pretty-format": { "version": "25.1.0", "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-25.1.0.tgz", diff --git a/package.json b/package.json index 3b64718..d782c53 100644 --- a/package.json +++ b/package.json @@ -11,8 +11,8 @@ "build": "webpack --config webpack.config.js", "start:watch": "ts-node-dev --respawn --transpileOnly server/src/index.ts", "start:inspect": "ts-node-dev --inspect --respawn --transpileOnly server/src/index.ts", - "lint": "tslint \"server/src/**/*.ts\"", - "lint:fix": "tslint \"server/src/**/*.ts\" --fix" + "tslint": "tslint \"server/src/**/*.ts\"", + "tslint:fix": "tslint \"server/src/**/*.ts\" --fix" }, "homepage": "https://github.com/danbilokha/covid19liveupdates", "bugs": { @@ -50,6 +50,7 @@ "@types/dotenv": "^8.2.0", "@types/express": "^4.17.3", "@types/jest": "^25.1.4", + "@types/node-telegram-bot-api": "^0.40.3", "@types/supertest": "^2.0.8", "chai": "^4.2.0", "coveralls": "^3.0.11", @@ -57,6 +58,7 @@ "jest": "^25.3.0", "lint-staged": "^10.1.3", "mocha": "^7.1.0", + "prettier": "2.0.4", "supertest": "^4.0.2", "ts-jest": "^25.3.1", "ts-loader": "^6.2.1", @@ -86,16 +88,6 @@ "url": "https://github.com/danbilokha" } ], - "lint-staged": { - "server/src/**/*.ts": [ - "npm run lint:fix && npm run lint" - ] - }, - "husky": { - "hooks": { - "pre-commit": "lint-staged" - } - }, "jest": { "globals": { "ts-jest": { diff --git a/server/src/bots/telegram/botResponse/adviceResponse.ts b/server/src/bots/telegram/botResponse/adviceResponse.ts index b8b0abb..ddf4b80 100644 --- a/server/src/bots/telegram/botResponse/adviceResponse.ts +++ b/server/src/bots/telegram/botResponse/adviceResponse.ts @@ -3,15 +3,16 @@ import { encouragingMessage, getCovid19ExplanationVideo, socialDistancing, - suggestedBehaviors + suggestedBehaviors, } from '../../../messages/feature/adviceMessages'; -import {CallBackQueryHandler} from '../models'; +import { CallBackQueryHandler } from '../models'; +import * as TelegramBot from 'node-telegram-bot-api'; export const showAdvicesHowToBehaveResponse: CallBackQueryHandler = async ( bot, message, chatId -): Promise => { +): Promise => { return bot.sendMessage( chatId, `ℹ Suggested Behaviors for ${getCovid19ExplanationVideo()} @@ -19,6 +20,6 @@ export const showAdvicesHowToBehaveResponse: CallBackQueryHandler = async ( ${socialDistancing()} \nℹ Alternative Greetings ${alternativeGreetings()} \n${encouragingMessage()} `, - {parse_mode: 'HTML'} + { parse_mode: 'HTML' } ); }; diff --git a/server/src/bots/telegram/botResponse/assistantResponse.ts b/server/src/bots/telegram/botResponse/assistantResponse.ts index 077b02d..f87493d 100644 --- a/server/src/bots/telegram/botResponse/assistantResponse.ts +++ b/server/src/bots/telegram/botResponse/assistantResponse.ts @@ -1,66 +1,75 @@ -import {fetchAnswer, fetchKnowledgeMetainformation} from '../../../services/api/api-knowledgebase'; -import {Answer} from '../../../models/knowledgebase/answer.models'; +import { + fetchAnswer, + fetchKnowledgeMetainformation, +} from '../../../services/api/api-knowledgebase'; +import { Answer } from '../../../models/knowledgebase/answer.models'; import { getAnswersOnQuestionMessage, getAssistantFeaturesMessage, - noAnswersOnQuestionMessage + noAnswersOnQuestionMessage, } from '../../../messages/feature/assistantMessages'; -import {textAfterUserCommand} from '../../../utils/textAfterCommand'; -import {isCommandOnly, isMatchingDashboardItem, isMessageStartsWithCommand} from '../../../utils/incomingMessages'; -import {KnowledgebaseMeta} from '../../../models/knowledgebase/meta.models'; -import {UserMessages} from '../../../models/constants'; -import {CallBackQueryHandler} from '../models'; +import { textAfterUserCommand } from '../../../utils/textAfterCommand'; +import { + isCommandOnly, + isMatchingDashboardItem, + isMessageStartsWithCommand, +} from '../../../utils/incomingMessages'; +import { KnowledgebaseMeta } from '../../../models/knowledgebase/meta.models'; +import { UserMessages } from '../../../models/constants'; +import { CallBackQueryHandler } from '../models'; +import * as TelegramBot from 'node-telegram-bot-api'; export const assistantStrategyResponse: CallBackQueryHandler = async ( - bot, - message, - chatId): Promise => { - if ((isMessageStartsWithCommand(message.text) && isCommandOnly(message.text)) - || isMatchingDashboardItem(message.text, UserMessages.Assistant)) { - return showAssistantFeatures(bot, message, chatId) as Promise; + bot: TelegramBot, + message: TelegramBot.Message, + chatId?: number +): Promise => { + if ( + (isMessageStartsWithCommand(message.text) && isCommandOnly(message.text)) || + isMatchingDashboardItem(message.text, UserMessages.Assistant) + ) { + return showAssistantFeatures(bot, message, chatId) as Promise; } - return answerOnQuestion(bot, message, chatId) as Promise; + return answerOnQuestion(bot, message, chatId) as Promise; }; export const showAssistantFeatures: CallBackQueryHandler = async ( - bot, - message, - chatId): Promise => { + bot: TelegramBot, + message: TelegramBot.Message, + chatId: number +): Promise => { const knowledgebaseMeta: KnowledgebaseMeta = await fetchKnowledgeMetainformation(); - return bot.sendMessage( - chatId, - getAssistantFeaturesMessage(knowledgebaseMeta) - ) + return bot.sendMessage(chatId, getAssistantFeaturesMessage(knowledgebaseMeta)); }; export const answerOnQuestion: CallBackQueryHandler = async ( - bot, - message, - chatId): Promise => { - const question = textAfterUserCommand(message.text); + bot: TelegramBot, + message: TelegramBot.Message, + chatId: number +): Promise => { + const question: string = textAfterUserCommand(message.text); const answers: Array = await fetchAnswer(question); if (!answers.length) { - return assistantNoAnswerResponse(bot, message, chatId) as Promise; + return assistantNoAnswerResponse(bot, message, chatId) as Promise; } return assistantResponse(bot, answers, chatId); }; export const assistantNoAnswerResponse: CallBackQueryHandler = async ( - bot, - message, - chatId): Promise => { - return bot.sendMessage( - chatId, - noAnswersOnQuestionMessage() - ) + bot: TelegramBot, + message: TelegramBot.Message, + chatId: number +): Promise => { + return bot.sendMessage(chatId, noAnswersOnQuestionMessage()); }; -export const assistantResponse = async (bot, answers, chatId) => { - return bot.sendMessage( - chatId, - getAnswersOnQuestionMessage(answers) - ); +export const assistantResponse = async ( + bot: TelegramBot, + answers: Array, + chatId: number +) => { + return bot.sendMessage(chatId, getAnswersOnQuestionMessage(answers)); }; diff --git a/server/src/bots/telegram/botResponse/availableResponse.ts b/server/src/bots/telegram/botResponse/availableResponse.ts index cde55cf..7b342c4 100644 --- a/server/src/bots/telegram/botResponse/availableResponse.ts +++ b/server/src/bots/telegram/botResponse/availableResponse.ts @@ -1,12 +1,14 @@ -import {getAvailableCountries} from '../../../services/domain/covid19'; -import {Country} from '../../../models/country.models'; -import {getShowCountriesMessage} from '../../../messages/feature/availableMessages'; -import {CallBackQueryHandler} from '../models'; +import { getAvailableCountries } from '../../../services/domain/covid19'; +import { Country } from '../../../models/country.models'; +import { getShowCountriesMessage } from '../../../messages/feature/availableMessages'; +import { CallBackQueryHandler } from '../models'; +import * as TelegramBot from 'node-telegram-bot-api'; -export const showAvailableCountriesResponse: CallBackQueryHandler = async (bot, message, chatId) => { +export const showAvailableCountriesResponse: CallBackQueryHandler = async ( + bot: TelegramBot, + message: TelegramBot.Message, + chatId: number +): Promise => { const countries: Array = await getAvailableCountries(); - return bot.sendMessage( - chatId, - getShowCountriesMessage(countries), - ); + return bot.sendMessage(chatId, getShowCountriesMessage(countries)); }; diff --git a/server/src/bots/telegram/botResponse/countriesResponse.ts b/server/src/bots/telegram/botResponse/countriesResponse.ts index c474986..05e90ef 100644 --- a/server/src/bots/telegram/botResponse/countriesResponse.ts +++ b/server/src/bots/telegram/botResponse/countriesResponse.ts @@ -1,37 +1,45 @@ -import {ContinentCountriesSituation, CountrySituation, CountrySituationInfo} from '../../../models/covid19.models'; -import {getChatId} from '../utils/chat'; -import {getCountriesSituation} from '../../../services/domain/covid19'; -import {getTableHeader, getTableRowMessageForCountry} from '../../../messages/feature/countryMessages'; -import {Country} from '../../../models/country.models'; -import {getCountriesSumupMessage, getCountriesTableHTML} from '../../../messages/feature/countriesMessages'; -import {getContinentsInlineKeyboard} from '../services/keyboard'; -import {CallBackQueryHandler} from '../models'; +import { + ContinentCountriesSituation, + CountrySituation, + CountrySituationInfo, +} from '../../../models/covid19.models'; +import { getCountriesSituation } from '../../../services/domain/covid19'; +import { + getTableHeader, + getTableRowMessageForCountry, +} from '../../../messages/feature/countryMessages'; +import { Country } from '../../../models/country.models'; +import { + getCountriesSumupMessage, + getCountriesTableHTML, +} from '../../../messages/feature/countriesMessages'; +import { getContinentsInlineKeyboard } from '../services/keyboard'; +import { CallBackQueryHandler } from '../models'; // TODO: Move this logic to domain and leave here only Telegram bot specific message response // Sending response itself export const countriesByContinent = (continent) => async (bot, message, chatId) => { - const countriesSituation: Array<[Country, Array]> = await getCountriesSituation(); + const countriesSituation: Array<[ + Country, + Array + ]> = await getCountriesSituation(); const continentCountries: ContinentCountriesSituation = {}; - countriesSituation - .forEach(([country, situations]: [Country, Array]) => { - const {confirmed, recovered, deaths} = situations[situations.length - 1]; + countriesSituation.forEach(([country, situations]: [Country, Array]) => { + const { confirmed, recovered, deaths } = situations[situations.length - 1]; - const countrySituationResult: CountrySituation = { - lastUpdateDate: situations[situations.length - 1].date, - country, - confirmed, - recovered, - deaths - }; - const prevCountriesResult: Array = continentCountries[country.continent] - ? continentCountries[country.continent] - : []; - continentCountries[country.continent] = [ - ...prevCountriesResult, - countrySituationResult - ]; - }); + const countrySituationResult: CountrySituation = { + lastUpdateDate: situations[situations.length - 1].date, + country, + confirmed, + recovered, + deaths, + }; + const prevCountriesResult: Array = continentCountries[country.continent] + ? continentCountries[country.continent] + : []; + continentCountries[country.continent] = [...prevCountriesResult, countrySituationResult]; + }); const portionMessage = [getTableHeader()]; let continentTotalConfirmed: number = 0; @@ -41,66 +49,73 @@ export const countriesByContinent = (continent) => async (bot, message, chatId) portionMessage.push(); continentCountries[continent] .sort((country1, country2) => country2.confirmed - country1.confirmed) - .forEach(({ - country: {name}, - lastUpdateDate, - confirmed, - recovered, - deaths - }: CountrySituation) => { + .forEach( + ({ + country: { name }, + lastUpdateDate, + confirmed, + recovered, + deaths, + }: CountrySituation) => { continentTotalConfirmed += confirmed; continentTotalRecovered += recovered; continentTotalDeath += deaths; - portionMessage.push( - getTableRowMessageForCountry({ - name, - confirmed, - recovered, - deaths, - lastUpdateDate - }) - ); - }); + portionMessage.push( + getTableRowMessageForCountry({ + name, + confirmed, + recovered, + deaths, + lastUpdateDate, + }) + ); + } + ); await bot.sendMessage( chatId, - getCountriesTableHTML({continent, continentTotalConfirmed, continentTotalRecovered, continentTotalDeath, portionMessage}), - {parse_mode: 'HTML'} + getCountriesTableHTML({ + continent, + continentTotalConfirmed, + continentTotalRecovered, + continentTotalDeath, + portionMessage, + }), + { parse_mode: 'HTML' } ); }; // TODO: Move this logic to domain and leave here only Telegram bot specific message response // Sending response itself export const countriesResponse: CallBackQueryHandler = async (bot, message, chatId) => { - const countriesSituation: Array<[Country, Array]> = await getCountriesSituation(); + const countriesSituation: Array<[ + Country, + Array + ]> = await getCountriesSituation(); const continentCountries: ContinentCountriesSituation = {}; let worldTotalConfirmed = 0; let worldTotalRecovered = 0; let worldTotalDeaths = 0; - countriesSituation - .forEach(([country, situations]: [Country, Array]) => { - const {confirmed, recovered, deaths} = situations[situations.length - 1]; + countriesSituation.forEach(([country, situations]: [Country, Array]) => { + const { confirmed, recovered, deaths } = situations[situations.length - 1]; - worldTotalConfirmed += confirmed; - worldTotalRecovered += recovered; - worldTotalDeaths += deaths; + worldTotalConfirmed += confirmed; + worldTotalRecovered += recovered; + worldTotalDeaths += deaths; - const countrySituationResult: CountrySituation = { - lastUpdateDate: situations[situations.length - 1].date, - country, - confirmed, - recovered, - deaths - }; - const prevCountriesResult = continentCountries[country.continent] - ? continentCountries[country.continent] - : []; - continentCountries[country.continent] = [ - ...prevCountriesResult, - countrySituationResult - ]; - }); + const countrySituationResult: CountrySituation = { + lastUpdateDate: situations[situations.length - 1].date, + country, + confirmed, + recovered, + deaths, + }; + const prevCountriesResult = continentCountries[country.continent] + ? continentCountries[country.continent] + : []; + continentCountries[country.continent] = [...prevCountriesResult, countrySituationResult]; + }); // Send overall world info, return bot.sendMessage( diff --git a/server/src/bots/telegram/botResponse/countryResponse.ts b/server/src/bots/telegram/botResponse/countryResponse.ts index 145b52e..ec9247e 100644 --- a/server/src/bots/telegram/botResponse/countryResponse.ts +++ b/server/src/bots/telegram/botResponse/countryResponse.ts @@ -1,46 +1,48 @@ -import {ApiCovid19Situation, CountrySituationInfo} from '../../../models/covid19.models'; -import {adaptCountryToSystemRepresentation, getCountriesSituation} from '../../../services/domain/covid19'; -import {Country} from '../../../models/country.models'; +import { ApiCovid19Situation, CountrySituationInfo } from '../../../models/covid19.models'; +import { + adaptCountryToSystemRepresentation, + getCountriesSituation, +} from '../../../services/domain/covid19'; +import { Country } from '../../../models/country.models'; import { getMessageForCountry, - getMessageForUserInputWithoutCountryName + getMessageForUserInputWithoutCountryName, } from '../../../messages/feature/countryMessages'; -import {Cache} from '../../../utils/cache'; -import {flag, name} from 'country-emoji'; -import {getAfterCountryResponseInlineKeyboard} from '../services/keyboard'; -import {textAfterUserCommand} from '../../../utils/textAfterCommand'; -import {isMessageIsCommand} from '../../../utils/incomingMessages'; -import {UserRegExps} from '../../../models/constants'; -import {CallBackQueryHandler} from '../models'; +import { Cache } from '../../../utils/cache'; +import { flag, name } from 'country-emoji'; +import { getAfterCountryResponseInlineKeyboard } from '../services/keyboard'; +import { textAfterUserCommand } from '../../../utils/textAfterCommand'; +import { isMessageIsCommand } from '../../../utils/incomingMessages'; +import { UserRegExps } from '../../../models/constants'; +import { CallBackQueryHandler } from '../models'; +import * as TelegramBot from 'node-telegram-bot-api'; export const showCountryByNameStrategyResponse: CallBackQueryHandler = async ( bot, message, chatId -): Promise => +): Promise => isMessageIsCommand(message.text, UserRegExps.CountryData) ? bot.sendMessage(chatId, getMessageForUserInputWithoutCountryName()) - : showCountryResponse(bot, - adaptCountryToSystemRepresentation(textAfterUserCommand(message.text)), - chatId,); + : showCountryResponse( + bot, + adaptCountryToSystemRepresentation(textAfterUserCommand(message.text)), + chatId + ); export const showCountryByFlag: CallBackQueryHandler = async ( bot, message, chatId -): Promise => - showCountryResponse( - bot, - adaptCountryToSystemRepresentation(name(message.text)), - chatId, - ); +): Promise => + showCountryResponse(bot, adaptCountryToSystemRepresentation(name(message.text)), chatId); // TODO: Split and move messages to /messages/feature and /domain directories export const showCountryResponse = async ( bot, requestedCountry, chatId -): Promise => { +): Promise => { if (!requestedCountry) { // Because of // [https://github.com/danbilokha/covid19liveupdates/issues/61] @@ -52,17 +54,26 @@ export const showCountryResponse = async ( return; } - const allCountriesSituations: Array<[Country, Array]> = await getCountriesSituation(); - const foundCountrySituations: [Country, Array] = allCountriesSituations - .find(([receivedCountry, situations]) => receivedCountry.name === requestedCountry); - if (!foundCountrySituations || !foundCountrySituations?.length - || !foundCountrySituations[0] - || !foundCountrySituations[1].length) { - bot.sendMessage( + const allCountriesSituations: Array<[ + Country, + Array + ]> = await getCountriesSituation(); + const foundCountrySituations: [ + Country, + Array + ] = allCountriesSituations.find( + ([receivedCountry, situations]) => receivedCountry.name === requestedCountry + ); + if ( + !foundCountrySituations || + !foundCountrySituations?.length || + !foundCountrySituations[0] || + !foundCountrySituations[1].length + ) { + return bot.sendMessage( chatId, `Sorry, but I cannot find anything for ${requestedCountry}. I will save your request and will work on it` ); - return; } const [foundCountry, foundSituation] = foundCountrySituations; @@ -74,20 +85,22 @@ export const showCountryResponse = async ( let totalConfirmed = 0; let totalDeaths = 0; - [foundSituation[foundSituation.length - 1]].forEach(({confirmed, deaths, recovered}: ApiCovid19Situation) => { - totalRecovered += recovered; - totalConfirmed += confirmed; - totalDeaths += deaths; - }); + [foundSituation[foundSituation.length - 1]].forEach( + ({ confirmed, deaths, recovered }: ApiCovid19Situation) => { + totalRecovered += recovered; + totalConfirmed += confirmed; + totalDeaths += deaths; + } + ); - bot.sendMessage( + return bot.sendMessage( chatId, getMessageForCountry({ name: foundCountry.name, confirmed: totalConfirmed, recovered: totalRecovered, deaths: totalDeaths, - lastUpdateDate: foundSituation[foundSituation.length - 1].date + lastUpdateDate: foundSituation[foundSituation.length - 1].date, }), getAfterCountryResponseInlineKeyboard(foundCountry.name) ); diff --git a/server/src/bots/telegram/botResponse/noResponse.ts b/server/src/bots/telegram/botResponse/noResponse.ts index 4fea631..5e9020b 100644 --- a/server/src/bots/telegram/botResponse/noResponse.ts +++ b/server/src/bots/telegram/botResponse/noResponse.ts @@ -1,17 +1,18 @@ -import {noResponseForUserMessage} from '../../../messages/userMessage'; -import {TelegramMessage} from '../models'; -import {logger} from '../../../utils/logger'; -import {getHelpProposalInlineKeyboard} from '../services/keyboard'; -import {LogglyTypes} from '../../../models/loggly.models'; +import { noResponseForUserMessage } from '../../../messages/userMessage'; +import { logger } from '../../../utils/logger'; +import { getHelpProposalInlineKeyboard } from '../services/keyboard'; +import { LogglyTypes } from '../../../models/loggly.models'; +import * as TelegramBot from 'node-telegram-bot-api'; -export const noResponse = async (bot, message: TelegramMessage, chatId: number): Promise => { - logger.log( - 'error', - { - ...message, - type: LogglyTypes.NoSuitableResponseToUserError, - } - ); +export const noResponse = async ( + bot: TelegramBot, + message: TelegramBot.Message, + chatId: number +): Promise => { + logger.log('error', { + ...message, + type: LogglyTypes.NoSuitableResponseToUserError, + }); return bot.sendMessage( chatId, diff --git a/server/src/bots/telegram/botResponse/startResponse.ts b/server/src/bots/telegram/botResponse/startResponse.ts index 15159f6..1016495 100644 --- a/server/src/bots/telegram/botResponse/startResponse.ts +++ b/server/src/bots/telegram/botResponse/startResponse.ts @@ -1,14 +1,10 @@ -import {getFullMenuKeyboard} from '../services/keyboard'; -import {greetUser} from '../../../messages/userMessage'; -import {showHelpInfoResponse} from './helpResponse'; -import {CallBackQueryHandler} from '../models'; +import { getFullMenuKeyboard } from '../services/keyboard'; +import { greetUser } from '../../../messages/userMessage'; +import { showHelpInfoResponse } from './helpResponse'; +import { CallBackQueryHandler } from '../models'; export const startResponse: CallBackQueryHandler = async (bot, message, chatId) => { - await bot.sendMessage( - chatId, - `${greetUser(message.from)}`, - getFullMenuKeyboard(message) - ); + await bot.sendMessage(chatId, `${greetUser(message.from)}`, getFullMenuKeyboard(message)); await showHelpInfoResponse(bot, message, chatId); }; diff --git a/server/src/bots/telegram/botResponse/subscribeResponse.ts b/server/src/bots/telegram/botResponse/subscribeResponse.ts index 38d5f60..0e079cb 100644 --- a/server/src/bots/telegram/botResponse/subscribeResponse.ts +++ b/server/src/bots/telegram/botResponse/subscribeResponse.ts @@ -3,53 +3,64 @@ import { showMySubscriptionMessage, subscribeError, subscriptionManagerResponseMessage, - subscriptionResultMessage + subscriptionResultMessage, } from '../../../messages/feature/subscribeMessages'; import { isCommandOnly, isMatchingDashboardItem, isMessageIsCommand, - isMessageStartsWithCommand + isMessageStartsWithCommand, } from '../../../utils/incomingMessages'; -import {CustomSubscriptions, UserMessages, UserRegExps} from '../../../models/constants'; -import {subscribeOn} from '../../../services/domain/subscriptions'; -import {catchAsyncError} from '../../../utils/catchError'; -import {getFullMenuKeyboard, getSubscriptionMessageInlineKeyboard} from '../services/keyboard'; -import {getTelegramActiveUserSubscriptions} from '../services/storage'; -import {getUserMessageFromIKorText} from '../utils/getUserMessageFromIKorText'; -import {UserSubscription} from '../../../models/subscription.models'; -import {removeCommandFromMessageIfExist} from '../../../utils/removeCommandFromMessageIfExist'; +import { CustomSubscriptions, UserMessages, UserRegExps } from '../../../models/constants'; +import { subscribeOn } from '../../../services/domain/subscriptions'; +import { catchAsyncError } from '../../../utils/catchError'; +import { getFullMenuKeyboard, getSubscriptionMessageInlineKeyboard } from '../services/keyboard'; +import { getTelegramActiveUserSubscriptions } from '../services/storage'; +import { getUserMessageFromIKorText } from '../utils/getUserMessageFromIKorText'; +import { UserSubscription } from '../../../models/subscription.models'; +import { removeCommandFromMessageIfExist } from '../../../utils/removeCommandFromMessageIfExist'; +import * as TelegramBot from 'node-telegram-bot-api'; // TODO: Take a look in all handlers and remove unneeded parameters where they are not used -export const subscriptionManagerResponse = async (bot, message, chatId): Promise => { +export const subscriptionManagerResponse = async ( + bot, + message, + chatId +): Promise => { return bot.sendMessage( chatId, subscriptionManagerResponseMessage(), - getSubscriptionMessageInlineKeyboard(), - ) + getSubscriptionMessageInlineKeyboard() + ); }; -export const showExistingSubscriptionsResponse = async (bot, message, chatId): Promise => { - const activeUserSubscription: UserSubscription = await getTelegramActiveUserSubscriptions(chatId); +export const showExistingSubscriptionsResponse = async ( + bot, + message, + chatId +): Promise => { + const activeUserSubscription: UserSubscription = await getTelegramActiveUserSubscriptions( + chatId + ); if (!activeUserSubscription?.subscriptionsOn?.length) { - return bot.sendMessage( - chatId, - noSubscriptionsResponseMessage() - ) + return bot.sendMessage(chatId, noSubscriptionsResponseMessage()); } - return bot.sendMessage( - chatId, - showMySubscriptionMessage(activeUserSubscription) - ); + return bot.sendMessage(chatId, showMySubscriptionMessage(activeUserSubscription)); }; // If it's called from InlineKeyboard, then @param ikCbData will be available // otherwise @param ikCbData will be null -export const subscribingStrategyResponse = async (bot, message, chatId, ikCbData?: string): Promise => { - if ((isMessageStartsWithCommand(message.text) && isCommandOnly(message.text)) - || isMessageIsCommand(message.text, UserRegExps.Subscribe) - || isMatchingDashboardItem(message.text, UserMessages.SubscriptionManager) +export const subscribingStrategyResponse = async ( + bot, + message, + chatId, + ikCbData?: string +): Promise => { + if ( + (isMessageStartsWithCommand(message.text) && isCommandOnly(message.text)) || + isMessageIsCommand(message.text, UserRegExps.Subscribe) || + isMatchingDashboardItem(message.text, UserMessages.SubscriptionManager) ) { return showExistingSubscriptionsResponse(bot, message, chatId); } @@ -64,15 +75,8 @@ export const subscribingStrategyResponse = async (bot, message, chatId, ikCbData ) ); if (err) { - return bot.sendMessage( - chatId, - subscribeError(err.message) - ) + return bot.sendMessage(chatId, subscribeError(err.message)); } - return bot.sendMessage( - chatId, - subscriptionResultMessage(result), - getFullMenuKeyboard(chatId) - ) + return bot.sendMessage(chatId, subscriptionResultMessage(result), getFullMenuKeyboard(chatId)); }; diff --git a/server/src/bots/telegram/botResponse/unsubscribeResponse.ts b/server/src/bots/telegram/botResponse/unsubscribeResponse.ts index 2c59cf0..54432c3 100644 --- a/server/src/bots/telegram/botResponse/unsubscribeResponse.ts +++ b/server/src/bots/telegram/botResponse/unsubscribeResponse.ts @@ -1,57 +1,60 @@ -import {UserSubscription} from '../../../models/subscription.models'; +import { UserSubscription } from '../../../models/subscription.models'; import { getUnsubscribeResponseMessage, unSubscribeError, - unsubscribeResultMessage + unsubscribeResultMessage, } from '../../../messages/feature/unsubscribeMessages'; -import {getFullMenuKeyboard, getUnsubscribeMessageInlineKeyboard} from '../services/keyboard'; +import { getFullMenuKeyboard, getUnsubscribeMessageInlineKeyboard } from '../services/keyboard'; import { isCommandOnly, isMatchingDashboardItem, isMessageIsCommand, - isMessageStartsWithCommand + isMessageStartsWithCommand, } from '../../../utils/incomingMessages'; -import {CustomSubscriptions, UserMessages, UserRegExps} from '../../../models/constants'; -import {catchAsyncError} from '../../../utils/catchError'; -import {unsubscribeMeFrom} from '../../../services/domain/subscriptions'; -import {getUserMessageFromIKorText} from '../utils/getUserMessageFromIKorText'; -import {noSubscriptionsResponseMessage} from '../../../messages/feature/subscribeMessages'; -import {removeCommandFromMessageIfExist} from '../../../utils/removeCommandFromMessageIfExist'; -import {getTelegramActiveUserSubscriptions} from '../services/storage'; +import { CustomSubscriptions, UserMessages, UserRegExps } from '../../../models/constants'; +import { catchAsyncError } from '../../../utils/catchError'; +import { unsubscribeMeFrom } from '../../../services/domain/subscriptions'; +import { getUserMessageFromIKorText } from '../utils/getUserMessageFromIKorText'; +import { noSubscriptionsResponseMessage } from '../../../messages/feature/subscribeMessages'; +import { removeCommandFromMessageIfExist } from '../../../utils/removeCommandFromMessageIfExist'; +import { getTelegramActiveUserSubscriptions } from '../services/storage'; +import * as TelegramBot from 'node-telegram-bot-api'; -export const buildUnsubscribeInlineResponse = async (bot, message, chatId): Promise => { +export const buildUnsubscribeInlineResponse = async ( + bot, + message, + chatId +): Promise => { const userSubscription: UserSubscription = await getTelegramActiveUserSubscriptions(chatId); if (!userSubscription?.subscriptionsOn?.length) { - return bot.sendMessage( - chatId, - noSubscriptionsResponseMessage() - ) + return bot.sendMessage(chatId, noSubscriptionsResponseMessage()); } return bot.sendMessage( chatId, getUnsubscribeResponseMessage(), - getUnsubscribeMessageInlineKeyboard( - userSubscription - .subscriptionsOn - .map(v => v.value) - ) - ) + getUnsubscribeMessageInlineKeyboard(userSubscription.subscriptionsOn.map((v) => v.value)) + ); }; // If it's called from InlineKeyboard, then @param ikCbData will be available // otherwise @param ikCbData will be null -export const unsubscribeStrategyResponse = async (bot, message, chatId, ikCbData?: string): Promise => { +export const unsubscribeStrategyResponse = async ( + bot, + message, + chatId, + ikCbData?: string +): Promise => { // If it's called from InlineKeyboard, then @param ikCbData will be available // otherwise @param ikCbData will be null - if (ikCbData - && isMatchingDashboardItem(ikCbData, UserMessages.Unsubscribe)) { + if (ikCbData && isMatchingDashboardItem(ikCbData, UserMessages.Unsubscribe)) { return buildUnsubscribeInlineResponse(bot, message, chatId); } - if ((isMessageStartsWithCommand(message.text) && isCommandOnly(message.text)) - || isMessageIsCommand(message.text, UserRegExps.Unsubscribe) - || isMatchingDashboardItem(message.text, UserMessages.Unsubscribe) + if ( + (isMessageStartsWithCommand(message.text) && isCommandOnly(message.text)) || + isMessageIsCommand(message.text, UserRegExps.Unsubscribe) || + isMatchingDashboardItem(message.text, UserMessages.Unsubscribe) ) { return buildUnsubscribeInlineResponse(bot, message, chatId); } @@ -71,15 +74,8 @@ export const unsubscribeStrategyResponse = async (bot, message, chatId, ikCbData ); if (err) { - return bot.sendMessage( - chatId, - unSubscribeError(err.message) - ) + return bot.sendMessage(chatId, unSubscribeError(err.message)); } - return bot.sendMessage( - chatId, - unsubscribeResultMessage(result), - getFullMenuKeyboard(chatId) - ) + return bot.sendMessage(chatId, unsubscribeResultMessage(result), getFullMenuKeyboard(chatId)); }; diff --git a/server/src/bots/telegram/index.ts b/server/src/bots/telegram/index.ts index dc3d913..e19c754 100644 --- a/server/src/bots/telegram/index.ts +++ b/server/src/bots/telegram/index.ts @@ -1,39 +1,43 @@ -import {countriesByContinent, countriesResponse} from './botResponse/countriesResponse'; -import {showCountryByFlag, showCountryByNameStrategyResponse} from './botResponse/countryResponse'; -import {Continents, CustomSubscriptions, UserMessages, UserRegExps} from '../../models/constants'; -import {showAdvicesHowToBehaveResponse} from './botResponse/adviceResponse'; -import {showHelpInfoResponse} from './botResponse/helpResponse'; -import {Express} from 'express'; -import {cachedCovid19CountriesData, getAvailableCountries,} from '../../services/domain/covid19'; -import {Country} from '../../models/country.models'; -import {flag} from 'country-emoji'; -import {assistantStrategyResponse} from './botResponse/assistantResponse'; +import { countriesByContinent, countriesResponse } from './botResponse/countriesResponse'; +import { + showCountryByFlag, + showCountryByNameStrategyResponse, +} from './botResponse/countryResponse'; +import { Continents, CustomSubscriptions, UserMessages, UserRegExps } from '../../models/constants'; +import { showAdvicesHowToBehaveResponse } from './botResponse/adviceResponse'; +import { showHelpInfoResponse } from './botResponse/helpResponse'; +import { Express } from 'express'; +import { cachedCovid19CountriesData, getAvailableCountries } from '../../services/domain/covid19'; +import { Country } from '../../models/country.models'; +import { flag } from 'country-emoji'; +import { assistantStrategyResponse } from './botResponse/assistantResponse'; import * as TelegramBot from 'node-telegram-bot-api'; import Config from '../../environments/environment'; -import {logger} from '../../utils/logger'; -import {startResponse} from './botResponse/startResponse'; -import {showAvailableCountriesResponse} from './botResponse/availableResponse'; +import { logger } from '../../utils/logger'; +import { startResponse } from './botResponse/startResponse'; +import { showAvailableCountriesResponse } from './botResponse/availableResponse'; import { showExistingSubscriptionsResponse, subscribingStrategyResponse, - subscriptionManagerResponse + subscriptionManagerResponse, } from './botResponse/subscribeResponse'; -import {SubscriptionType} from '../../models/subscription.models'; -import {registry, withCommandArgument} from './services/messageRegistry'; -import {subscriptionNotifierHandler} from './services/subscriptionNotifierManager'; -import {unsubscribeStrategyResponse} from './botResponse/unsubscribeResponse'; -import {showTrendsByCountry} from './botResponse/trendResponse'; -import {CountrySituationInfo} from '../../models/covid19.models'; -import {catchAsyncError} from '../../utils/catchError'; -import {LogglyTypes} from '../../models/loggly.models'; -import {getErrorMessage} from '../../utils/getLoggerMessages'; +import { SubscriptionType } from '../../models/subscription.models'; +import { MessageHandlerRegistry, withCommandArgument } from './services/messageHandlerRegistry'; +import { subscriptionNotifierHandler } from './services/subscriptionNotifierManager'; +import { unsubscribeStrategyResponse } from './botResponse/unsubscribeResponse'; +import { showTrendsByCountry } from './botResponse/trendResponse'; +import { CountrySituationInfo } from '../../models/covid19.models'; +import { catchAsyncError } from '../../utils/catchError'; +import { LogglyTypes } from '../../models/loggly.models'; +import { getErrorMessage } from '../../utils/getErrorMessages'; -function runTelegramBot(app: Express, ngRokUrl: string) { +function runTelegramBot(app: Express, appUrl: string) { // Create a bot that uses 'polling' to fetch new updates - const bot = new TelegramBot(Config.TELEGRAM_TOKEN, {polling: true}); + const bot = new TelegramBot(Config.TELEGRAM_TOKEN, { polling: true }); + const messageHandlerRegistry = new MessageHandlerRegistry(bot); // This informs the Telegram servers of the new webhook - bot.setWebHook(`${ngRokUrl}/bot${Config.TELEGRAM_TOKEN}`); + bot.setWebHook(`${appUrl}/bot${Config.TELEGRAM_TOKEN}`); // We are receiving updates at the route below! app.post(`/bot${Config.TELEGRAM_TOKEN}`, (req, res) => { @@ -41,13 +45,11 @@ function runTelegramBot(app: Express, ngRokUrl: string) { res.sendStatus(200); }); - registry.setBot(bot); // TODO: DO IT COOLER - registry.addSingleParameterCommands([ + messageHandlerRegistry.addSingleParameterCommands([ UserRegExps.CountryData, - UserRegExps.Trends + UserRegExps.Trends, ]); - - registry + messageHandlerRegistry .registerMessageHandler(UserRegExps.Start, startResponse) // Feature: Countries / Country .registerMessageHandler(UserMessages.CountriesData, countriesResponse) @@ -69,42 +71,61 @@ function runTelegramBot(app: Express, ngRokUrl: string) { .registerMessageHandler(UserMessages.Existing, showExistingSubscriptionsResponse) .registerMessageHandler(UserRegExps.Subscribe, subscribingStrategyResponse) .registerMessageHandler(UserRegExps.Unsubscribe, unsubscribeStrategyResponse) - .registerMessageHandler(UserRegExps.Trends, withCommandArgument(showTrendsByCountry)); - registry.registerCallBackQueryHandler(CustomSubscriptions.SubscribeMeOn, subscribingStrategyResponse); - registry.registerCallBackQueryHandler(CustomSubscriptions.UnsubscribeMeFrom, unsubscribeStrategyResponse); - registry.registerCallBackQueryHandler(UserMessages.Existing, showExistingSubscriptionsResponse); - registry.registerCallBackQueryHandler(UserMessages.Unsubscribe, unsubscribeStrategyResponse); - registry.registerCallBackQueryHandler(UserMessages.Help, showHelpInfoResponse); - registry.registerCallBackQueryHandler(UserRegExps.Trends, withCommandArgument(showTrendsByCountry)); - + .registerMessageHandler( + UserRegExps.Trends, + withCommandArgument(messageHandlerRegistry, showTrendsByCountry) + ); + messageHandlerRegistry.registerCallBackQueryHandler( + CustomSubscriptions.SubscribeMeOn, + subscribingStrategyResponse + ); + messageHandlerRegistry.registerCallBackQueryHandler( + CustomSubscriptions.UnsubscribeMeFrom, + unsubscribeStrategyResponse + ); + messageHandlerRegistry.registerCallBackQueryHandler( + UserMessages.Existing, + showExistingSubscriptionsResponse + ); + messageHandlerRegistry.registerCallBackQueryHandler( + UserMessages.Unsubscribe, + unsubscribeStrategyResponse + ); + messageHandlerRegistry.registerCallBackQueryHandler(UserMessages.Help, showHelpInfoResponse); + messageHandlerRegistry.registerCallBackQueryHandler( + UserRegExps.Trends, + withCommandArgument(messageHandlerRegistry, showTrendsByCountry) + ); // Feature: Countries / Country for (const continent of Object.keys(Continents)) { - registry.registerCallBackQueryHandler(continent, countriesByContinent(continent)); + messageHandlerRegistry.registerCallBackQueryHandler( + continent, + countriesByContinent(continent) + ); } + // Feature: Countries / Country - getAvailableCountries() - .then((countries: Array) => { - const single = countries - .map(c => flag(c.name)?.trim() ?? undefined) - .filter(v => !!v) // TODO: Find flag that we lack for [https://github.com/danbilokha/covid19liveupdates/issues/61] - .join('//'); + getAvailableCountries().then((countries: Array) => { + const single = countries + .map((c) => flag(c.name)?.trim() ?? undefined) + .filter((v) => !!v) // TODO: Find flag that we lack for [https://github.com/danbilokha/covid19liveupdates/issues/61] + .join('//'); - registry.registerMessageHandler(`[~${single}~]`, showCountryByFlag); - }); + messageHandlerRegistry.registerMessageHandler(`[~${single}~]`, showCountryByFlag); + }); // Feature: Subscriptions cachedCovid19CountriesData.subscribe( async (countriesData: [number, Array<[Country, Array]>]) => { - const [err, result] = await catchAsyncError(subscriptionNotifierHandler(countriesData)); + const [err, result] = await catchAsyncError( + subscriptionNotifierHandler(messageHandlerRegistry, countriesData) + ); if (err) { - logger.log( - 'error', - { - type: LogglyTypes.SubscriptionNotifierHandlerError, - message: `${getErrorMessage(err)}. subscriptionNotifierHandler failed` - } - ); + logger.log('error', { + type: LogglyTypes.SubscriptionNotifierHandlerError, + message: `${getErrorMessage(err)}. subscriptionNotifierHandler failed`, + }); } }, [SubscriptionType.Country] @@ -112,7 +133,7 @@ function runTelegramBot(app: Express, ngRokUrl: string) { bot.on('message', (message, ...args) => { logger.log('info', message); - registry.runCommandHandler(message); + messageHandlerRegistry.runCommandHandler(message); }); bot.on('polling_error', (err) => logger.log('error', err)); @@ -120,4 +141,4 @@ function runTelegramBot(app: Express, ngRokUrl: string) { bot.on('error', (err) => logger.log('error', err)); } -export {runTelegramBot}; +export { runTelegramBot }; diff --git a/server/src/bots/telegram/models/index.ts b/server/src/bots/telegram/models/index.ts index aad999e..3bae04a 100644 --- a/server/src/bots/telegram/models/index.ts +++ b/server/src/bots/telegram/models/index.ts @@ -1,49 +1,14 @@ -export interface TelegramMessage { - message_id: number; - from: TelegramFrom; - chat: TelegramChat; - date: number; - text: string; - level: string; - reply_markup?: unknown; -} +import * as TelegramBot from 'node-telegram-bot-api'; -export interface TelegramFrom { - id: number; - is_bot: boolean; - first_name: string; - last_name: string; - username: string; - language_code: string; -} - -export interface TelegramChat { - id: number; - first_name: string; - last_name: string; - username: string; - type: string | 'private'; -} - -export interface TelegramMessageReplyMarkup { - reply_markup: { - inline_keyboard: Array> - } -} - -export interface TelegramBot { - sendMessage: Function -} - -export type CallBackQueryHandler = ( +export type CallBackQueryHandler = ( bot: TelegramBot, - message: TelegramMessage, - chatId: number, + message: TelegramBot.Message, + chatId: number ) => unknown; export type CallBackQueryHandlerWithCommandArgument = ( bot: TelegramBot, - message: TelegramMessage, + message: TelegramBot.Message, chatId: number, commandArgument: string ) => unknown; diff --git a/server/src/bots/telegram/services/keyboard.ts b/server/src/bots/telegram/services/keyboard.ts index 258f6f7..49aaba6 100644 --- a/server/src/bots/telegram/services/keyboard.ts +++ b/server/src/bots/telegram/services/keyboard.ts @@ -1,62 +1,63 @@ -import {Cache} from '../../../utils/cache'; -import {Continents, CustomSubscriptions, UserMessages, UserRegExps} from '../../../models/constants'; -import {InlineKeyboard, ReplyKeyboard} from 'node-telegram-keyboard-wrapper'; -import {UNSUBSCRIPTIONS_ROW_ITEMS_NUMBER} from '../models'; - -export const getFullMenuKeyboard = (chatId): unknown => { +import { Cache } from '../../../utils/cache'; +import { + Continents, + CustomSubscriptions, + UserMessages, + UserRegExps, +} from '../../../models/constants'; +import { InlineKeyboard, ReplyKeyboard } from 'node-telegram-keyboard-wrapper'; +import { UNSUBSCRIPTIONS_ROW_ITEMS_NUMBER } from '../models'; +import * as TelegramBot from 'node-telegram-bot-api'; + +export const getFullMenuKeyboard = (chatId): TelegramBot.SendMessageOptions => { const rk = new ReplyKeyboard(); - const latestSelectedCountries: Array = Cache - .get(`${chatId}_commands_country`); + const latestSelectedCountries: Array = Cache.get(`${chatId}_commands_country`); if (latestSelectedCountries.length > 0) { rk.addRow.apply(rk, latestSelectedCountries); } - rk - .addRow(UserMessages.CountriesData, UserMessages.AvailableCountries) + rk.addRow(UserMessages.CountriesData, UserMessages.AvailableCountries) .addRow(UserMessages.Assistant, UserMessages.GetAdvicesHowToBehave) .addRow(UserMessages.SubscriptionManager, UserMessages.Help); - return rk.open({resize_keyboard: true}) + return rk.open({ resize_keyboard: true }); }; -export const getAfterCountryResponseInlineKeyboard = (country: string): unknown => { +export const getAfterCountryResponseInlineKeyboard = ( + country: string +): TelegramBot.SendMessageOptions => { const ik = new InlineKeyboard(); - ik - .addRow( - { - text: `${CustomSubscriptions.SubscribeMeOn} ${country}`, - callback_data: `${CustomSubscriptions.SubscribeMeOn} ${country}` - } - ) - .addRow( - { - text: `Show weekly chart`, - callback_data: `${UserRegExps.Trends} ${country}` - } - ); + ik.addRow({ + text: `${CustomSubscriptions.SubscribeMeOn} ${country}`, + callback_data: `${CustomSubscriptions.SubscribeMeOn} ${country}`, + }).addRow({ + text: `Show weekly chart`, + callback_data: `${UserRegExps.Trends} ${country}`, + }); return ik.build(); }; -export const getSubscriptionMessageInlineKeyboard = (): unknown => { +export const getSubscriptionMessageInlineKeyboard = (): TelegramBot.SendMessageOptions => { const ik = new InlineKeyboard(); - ik - .addRow( - { - text: UserMessages.Existing, - callback_data: UserMessages.Existing - }, - { - text: UserMessages.Unsubscribe, - callback_data: UserMessages.Unsubscribe - }, - ); + ik.addRow( + { + text: UserMessages.Existing, + callback_data: UserMessages.Existing, + }, + { + text: UserMessages.Unsubscribe, + callback_data: UserMessages.Unsubscribe, + } + ); return ik.build(); }; -export const getUnsubscribeMessageInlineKeyboard = (values: Array): unknown => { +export const getUnsubscribeMessageInlineKeyboard = ( + values: Array +): TelegramBot.SendMessageOptions => { const ik = new InlineKeyboard(); let i: number = 0; @@ -64,12 +65,10 @@ export const getUnsubscribeMessageInlineKeyboard = (values: Array): unkn const rows = []; let rowItem: number = 0; while (!!values[i] && rowItem < UNSUBSCRIPTIONS_ROW_ITEMS_NUMBER) { - rows.push( - { - text: values[i], - callback_data: `${CustomSubscriptions.UnsubscribeMeFrom} ${values[i++]}`, - } - ); + rows.push({ + text: values[i], + callback_data: `${CustomSubscriptions.UnsubscribeMeFrom} ${values[i++]}`, + }); rowItem += 1; } ik.addRow(...rows); @@ -78,49 +77,27 @@ export const getUnsubscribeMessageInlineKeyboard = (values: Array): unkn return ik.build(); }; -export const getContinentsInlineKeyboard = (): unknown => { - const ik = new InlineKeyboard(); - ik - .addRow( - {text: Continents.Europe, callback_data: Continents.Europe}, - {text: Continents.Asia, callback_data: Continents.Asia} - ) - .addRow( - {text: Continents.Africa, callback_data: Continents.Africa}, - {text: Continents.Americas, callback_data: Continents.Americas}, - ) - .addRow( - {text: Continents.Other, callback_data: Continents.Other}, - {text: Continents.Oceania, callback_data: Continents.Oceania}, - ); - - return ik.build(); -}; - - -export const getHowToAdviceInlineKeyboard = (): unknown => { +export const getContinentsInlineKeyboard = (): TelegramBot.SendMessageOptions => { const ik = new InlineKeyboard(); - ik - .addRow( - {text: Continents.Europe, callback_data: Continents.Europe}, - {text: Continents.Asia, callback_data: Continents.Asia} - ) + ik.addRow( + { text: Continents.Europe, callback_data: Continents.Europe }, + { text: Continents.Asia, callback_data: Continents.Asia } + ) .addRow( - {text: Continents.Africa, callback_data: Continents.Africa}, - {text: Continents.Americas, callback_data: Continents.Americas}, + { text: Continents.Africa, callback_data: Continents.Africa }, + { text: Continents.Americas, callback_data: Continents.Americas } ) .addRow( - {text: Continents.Other, callback_data: Continents.Other}, - {text: Continents.Oceania, callback_data: Continents.Oceania}, + { text: Continents.Other, callback_data: Continents.Other }, + { text: Continents.Oceania, callback_data: Continents.Oceania } ); return ik.build(); }; -export const getHelpProposalInlineKeyboard = (): unknown => { +export const getHelpProposalInlineKeyboard = (): TelegramBot.SendMessageOptions => { const ik = new InlineKeyboard(); - ik - .addRow({text: UserMessages.Help, callback_data: UserMessages.Help}); + ik.addRow({ text: UserMessages.Help, callback_data: UserMessages.Help }); return ik.build(); }; diff --git a/server/src/bots/telegram/services/messageHandlerRegistry.ts b/server/src/bots/telegram/services/messageHandlerRegistry.ts new file mode 100644 index 0000000..4e074b1 --- /dev/null +++ b/server/src/bots/telegram/services/messageHandlerRegistry.ts @@ -0,0 +1,180 @@ +import { logger } from '../../../utils/logger'; +import { CallBackQueryHandlerWithCommandArgument } from '../models'; +import { getChatId } from '../utils/chat'; +import { + getCountryByMessage, + getCountryNameByFlag, + isMessageCountryFlag, +} from '../../../utils/featureHelpers/isMessageCountry'; +import { showCountryResponse } from '../botResponse/countryResponse'; +import { Country } from '../../../models/country.models'; +import { + adaptCountryToSystemRepresentation, + getAvailableCountries, +} from '../../../services/domain/covid19'; +import { Answer } from '../../../models/knowledgebase/answer.models'; +import { fetchAnswer } from '../../../services/api/api-knowledgebase'; +import { assistantResponse } from '../botResponse/assistantResponse'; +import { noResponse } from '../botResponse/noResponse'; +import { UserRegExps } from '../../../models/constants'; +import { LogglyTypes } from '../../../models/loggly.models'; +import * as TelegramBot from 'node-telegram-bot-api'; + +export class MessageHandlerRegistry { + _cbQueryHandlers: { [regexp: string]: CallBackQueryHandlerWithCommandArgument } = {}; + _messageHandlers: { [regexp: string]: CallBackQueryHandlerWithCommandArgument } = {}; + + _singleParameterCommandRegex: RegExp; + + constructor(private readonly bot: TelegramBot) { + this.registerCallBackQuery(); + } + + public addSingleParameterCommands(commands: Array) { + this._singleParameterCommandRegex = new RegExp( + `(?${commands.join('|\\')})\\s(?.*)` + ); + } + + public registerMessageHandler( + regexp: string, + callback: CallBackQueryHandlerWithCommandArgument + ): MessageHandlerRegistry { + this._messageHandlers[regexp] = callback; + return this; + } + + public registerCallBackQueryHandler( + regexp: string, + callback: CallBackQueryHandlerWithCommandArgument + ): MessageHandlerRegistry { + this._cbQueryHandlers[regexp] = callback; + return this; + } + + public sendUserNotification( + chatId: number, + notification: string + ): Promise { + return this.bot.sendMessage(chatId, notification); + } + + public async runCommandHandler( + message: TelegramBot.Message, + ikCbData?: string + ): Promise { + const runCheckupAgainstStr = ikCbData ? ikCbData : message.text; + const cbHandlers = ikCbData ? this._cbQueryHandlers : this._messageHandlers; + + const suitableKeys: Array = Object.keys(cbHandlers).filter( + (cbHandlerRegExpKey: string) => + !!runCheckupAgainstStr.match(new RegExp(cbHandlerRegExpKey, 'g')) + ); + + if (suitableKeys.length === 0) { + return this.tryDeduceUserCommand(message); + } + + if (suitableKeys.length > 1) { + logger.log('info', { + type: LogglyTypes.MoreThenOneAvailableResponseError, + message: `[INFO] (Might be an error) Several suitable keys for ${runCheckupAgainstStr}. \nKEYS:\n${suitableKeys.join( + ';\n' + )}`, + }); + } + + return cbHandlers[suitableKeys[0]].call( + this, + this.bot, + message, + getChatId(message), + ikCbData + ); + } + + private registerCallBackQuery() { + this.bot.on('callback_query', ({ id, data, message, from }) => { + this.bot.answerCallbackQuery(id, { text: `${data} in progress...` }).then(() => { + return this.runCommandHandler( + { + ...message, + from, // As in cases of answerCallbackQuery original from in message will be bot sender, + // But we do want it still to be user. Do we? :D + }, + data + ); + }); + }); + } + + private async tryDeduceUserCommand(message: TelegramBot.Message): Promise { + const chatId = getChatId(message); + + if (isMessageCountryFlag(message.text)) { + const countryName: string = getCountryNameByFlag(message.text); + return showCountryResponse(this.bot, countryName, chatId); + } + + const countries: Array = await getAvailableCountries(); + const country: Country | undefined = getCountryByMessage( + adaptCountryToSystemRepresentation(message.text), + countries + ); + if (country) { + return showCountryResponse(this.bot, country.name, chatId); + } + + const answers: Array = await fetchAnswer(message.text); + if (answers?.length) { + return assistantResponse(this.bot, answers, chatId); + } + + return noResponse(this.bot, message, chatId); + } +} + +export const withCommandArgument = ( + context: MessageHandlerRegistry, + handlerFn: CallBackQueryHandlerWithCommandArgument +): CallBackQueryHandlerWithCommandArgument => { + return ( + bot: TelegramBot, + message: TelegramBot.Message, + chatId: number, + ikCbData?: string + ): unknown => { + try { + const commandArgument: string = getArgFromMessage.call( + context, + ikCbData ?? message.text + ); + return handlerFn.call(context, bot, message, chatId, commandArgument); + } catch (err) { + logger.log('warn', { + ...message, + type: LogglyTypes.CommandError, + message: err.message, + }); + } + }; +}; + +function getArgFromMessage(messageText: string): string { + if (!messageText) { + throw new Error('message could not be empty'); + } + + const execResult = this._singleParameterCommandRegex.exec(messageText); + if (!execResult) { + throw new Error('Please input any command from available'); + } + + /* tslint:disable:no-string-literal */ + if (execResult.groups['command'] && !execResult.groups['firstargument']) { + throw new Error(`please provide argument for \\${execResult.groups['command']}`); + } + + return execResult.groups['firstargument']; + /* tslint:enable:no-string-literal */ +} diff --git a/server/src/bots/telegram/services/messageRegistry.ts b/server/src/bots/telegram/services/messageRegistry.ts deleted file mode 100644 index 2645e1a..0000000 --- a/server/src/bots/telegram/services/messageRegistry.ts +++ /dev/null @@ -1,165 +0,0 @@ -import {logger} from '../../../utils/logger'; -import {CallBackQueryHandlerWithCommandArgument, TelegramBot, TelegramMessage} from '../models'; -import {getChatId} from '../utils/chat'; -import { - getCountryByMessage, - getCountryNameByFlag, - isMessageCountryFlag -} from '../../../utils/featureHelpers/isMessageCountry'; -import {showCountryResponse} from '../botResponse/countryResponse'; -import {Country} from '../../../models/country.models'; -import {adaptCountryToSystemRepresentation, getAvailableCountries} from '../../../services/domain/covid19'; -import {Answer} from '../../../models/knowledgebase/answer.models'; -import {fetchAnswer} from '../../../services/api/api-knowledgebase'; -import {assistantResponse} from '../botResponse/assistantResponse'; -import {noResponse} from '../botResponse/noResponse'; -import {UserRegExps} from '../../../models/constants'; -import {LogglyTypes} from '../../../models/loggly.models'; - -class MessageRegistry { - // TODO: change type to unknown and Handle casting to BotType - _bot: any; - _cbQueryHandlers: { [regexp: string]: CallBackQueryHandlerWithCommandArgument } = {}; - _messageHandlers: { [regexp: string]: CallBackQueryHandlerWithCommandArgument } = {}; - - _singleParameterCommandRegex: RegExp; - - public setBot(bot): void { - this._bot = bot; - this.registerCallBackQuery(); - } - - public addSingleParameterCommands(commands: Array) { - this._singleParameterCommandRegex = - new RegExp(`(?${commands.join('|\\')})\\s(?.*)`); - } - - public registerMessageHandler(regexp: string, callback: CallBackQueryHandlerWithCommandArgument): MessageRegistry { - this._messageHandlers[regexp] = callback; - return this; - }; - - public registerCallBackQueryHandler(regexp: string, callback: CallBackQueryHandlerWithCommandArgument): MessageRegistry { - this._cbQueryHandlers[regexp] = callback; - return this; - }; - - public sendUserNotification(chatId: number, notification: string): Promise { - return this._bot.sendMessage( - chatId, - notification - ); - } - - public async runCommandHandler(message: TelegramMessage, ikCbData?: string): Promise { - const runCheckupAgainstStr = ikCbData ? ikCbData : message.text; - const cbHandlers = ikCbData ? this._cbQueryHandlers : this._messageHandlers; - - const suitableKeys: Array = Object.keys(cbHandlers) - .filter((cbHandlerRegExpKey: string) => - !!runCheckupAgainstStr.match(new RegExp(cbHandlerRegExpKey, 'g')) - ); - - if (suitableKeys.length === 0) { - return this.tryDeduceUserCommand(message); - } - - if (suitableKeys.length > 1) { - logger.log( - 'info', - { - type: LogglyTypes.MoreThenOneAvailableResponseError, - message: `[INFO] (Might be an error) Several suitable keys for ${runCheckupAgainstStr}. \nKEYS:\n${suitableKeys.join(';\n')}` - } - ); - } - - return cbHandlers[suitableKeys[0]] - .call( - this, - this._bot, - message, - getChatId(message), - ikCbData - ); - } - - private registerCallBackQuery() { - this._bot.on('callback_query', ({id, data, message, from}) => { - this._bot.answerCallbackQuery(id, {text: `${data} in progress...`}) - .then(() => { - - return this.runCommandHandler( - { - ...message, - from // As in cases of answerCallbackQuery original from in message will be bot sender, - // But we do want it still to be user. Do we? :D - }, - data - ) - }); - }); - } - - private async tryDeduceUserCommand(message: TelegramMessage): Promise { - const chatId = getChatId(message); - - if (isMessageCountryFlag(message.text)) { - const countryName: string = getCountryNameByFlag(message.text); - return showCountryResponse(this._bot, countryName, chatId) - } - - const countries: Array = await getAvailableCountries(); - const country: Country | undefined = getCountryByMessage( - adaptCountryToSystemRepresentation(message.text), - countries - ); - if (country) { - return showCountryResponse(this._bot, country.name, chatId) - } - - const answers: Array = await fetchAnswer(message.text); - if (answers?.length) { - return assistantResponse(this._bot, answers, chatId); - } - - return noResponse(this._bot, message, chatId); - } -} - -export const registry = new MessageRegistry(); - -export const withCommandArgument = (handlerFn: CallBackQueryHandlerWithCommandArgument): CallBackQueryHandlerWithCommandArgument => { - const context = registry; - return (bot: TelegramBot, message: TelegramMessage, chatId: number, ikCbData?: string): unknown => { - try { - const commandArgument: string = getArgFromMessage.call(context, ikCbData ?? message.text); - return handlerFn.call(context, bot, message, chatId, commandArgument) - } catch (err) { - logger.log('warn', { - ...message, - type: LogglyTypes.CommandError, - message: err.message, - }); - } - } -} - -function getArgFromMessage(messageText: string): string { - if (!messageText) { - throw new Error('message could not be empty'); - } - - const execResult = this._singleParameterCommandRegex.exec(messageText); - if (!execResult) { - throw new Error('Please input any command from available'); - } - - /* tslint:disable:no-string-literal */ - if (execResult.groups['command'] && !execResult.groups['firstargument']) { - throw new Error(`please provide argument for \\${execResult.groups['command']}`); - } - - return execResult.groups['firstargument']; - /* tslint:enable:no-string-literal */ -} diff --git a/server/src/bots/telegram/services/subscriptionNotifierManager.ts b/server/src/bots/telegram/services/subscriptionNotifierManager.ts index b1c90bd..742953f 100644 --- a/server/src/bots/telegram/services/subscriptionNotifierManager.ts +++ b/server/src/bots/telegram/services/subscriptionNotifierManager.ts @@ -1,51 +1,59 @@ -import {Country} from '../../../models/country.models'; -import {CountrySituationInfo} from '../../../models/covid19.models'; -import {SubscriptionStorage} from '../../../models/storage.models'; +import { Country } from '../../../models/country.models'; +import { CountrySituationInfo } from '../../../models/covid19.models'; +import { SubscriptionStorage } from '../../../models/storage.models'; import { Subscription, SubscriptionType, UserSubscription, - UserSubscriptionNotification + UserSubscriptionNotification, } from '../../../models/subscription.models'; -import {registry} from './messageRegistry'; -import {getTelegramSubscriptions, setTelegramSubscription} from './storage'; -import {catchAsyncError} from '../../../utils/catchError'; -import {logger} from '../../../utils/logger'; -import {getErrorMessage} from '../../../utils/getLoggerMessages'; -import {isCountrySituationHasChangedSinceLastData} from '../../../services/domain/subscriptions'; -import {showCountrySubscriptionMessage} from '../../../messages/feature/subscribeMessages'; -import {LogglyTypes} from '../../../models/loggly.models'; +import { getTelegramSubscriptions, setTelegramSubscription } from './storage'; +import { catchAsyncError } from '../../../utils/catchError'; +import { logger } from '../../../utils/logger'; +import { getErrorMessage } from '../../../utils/getErrorMessages'; +import { isCountrySituationHasChangedSinceLastData } from '../../../services/domain/subscriptions'; +import { showCountrySubscriptionMessage } from '../../../messages/feature/subscribeMessages'; +import { LogglyTypes } from '../../../models/loggly.models'; +import { MessageHandlerRegistry } from './messageHandlerRegistry'; -export const subscriptionNotifierHandler = async (countriesData: [number, Array<[Country, Array]>]): Promise => { +export const subscriptionNotifierHandler = async ( + messageHandlerRegistry: MessageHandlerRegistry, + countriesData: [number, Array<[Country, Array]>] +): Promise => { const allUsersSubscriptions: SubscriptionStorage = await getTelegramSubscriptions(); const [_, countriesInfo] = countriesData; - const countriesInfoMap = new Map( - countriesInfo - .map(([country, countrySituations]) => - ([country.name.toLocaleLowerCase(), countrySituations])) + const countriesInfoMap: Map> = new Map( + countriesInfo.map(([country, countrySituations]) => [ + country.name.toLocaleLowerCase(), + countrySituations, + ]) ); for (const [chatId, userSubscription] of Object.entries(allUsersSubscriptions)) { const [err, result] = await catchAsyncError( - getAndSendUserNotificationSubscriptions(countriesInfoMap, userSubscription, chatId) + getAndSendUserNotificationSubscriptions( + messageHandlerRegistry, + countriesInfoMap, + userSubscription, + chatId + ) ); if (err) { - logger.log( - 'error', - { - type: LogglyTypes.SubscriptionNotifierGeneralError, - message: `${getErrorMessage(err)}. General subscriptionNotifierHandler, sending user ${chatId} notification failed` - } - ); - continue; + logger.log('error', { + type: LogglyTypes.SubscriptionNotifierGeneralError, + message: `${getErrorMessage( + err + )}. General subscriptionNotifierHandler, sending user ${chatId} notification failed`, + }); } } }; const getAndSendUserNotificationSubscriptions = async ( + messageHandlerRegistry: MessageHandlerRegistry, countriesInfoMap: Map>, userSubscription: UserSubscription, - chatId: string, + chatId: string ) => { const userSubscriptionsUpdate: Array = getUserActiveSubscriptionNotifications( countriesInfoMap, @@ -53,51 +61,53 @@ const getAndSendUserNotificationSubscriptions = async ( ); if (!!userSubscriptionsUpdate?.length) { - const [sendingNotificationErr, sendingNotificationResult] = await catchAsyncError(registry.sendUserNotification( - parseInt(chatId, 10), - userSubscriptionsUpdate - .map((subUp: UserSubscriptionNotification) => - subUp.subscriptionMessage - ).join('\n\n'), - )); + const [sendingNotificationErr, sendingNotificationResult] = await catchAsyncError( + messageHandlerRegistry.sendUserNotification( + parseInt(chatId, 10), + userSubscriptionsUpdate + .map((subUp: UserSubscriptionNotification) => subUp.subscriptionMessage) + .join('\n\n') + ) + ); if (!!sendingNotificationErr) { - return logger.log( - 'error', - { - type: LogglyTypes.SubscriptionNotifierError, - message: `${getErrorMessage(sendingNotificationErr)}. User ${chatId} notifications has not been send` - } - ); + return logger.log('error', { + type: LogglyTypes.SubscriptionNotifierError, + message: `${getErrorMessage( + sendingNotificationErr + )}. User ${chatId} notifications has not been send`, + }); } - const mergeAllUserSubscriptions: Array = (userSubscription as UserSubscription) - .subscriptionsOn - .map((sub: Subscription) => { - const updateUserSub = userSubscriptionsUpdate - .find(({subscription: {value, type, active}}: UserSubscriptionNotification) => - active === sub.active && type === sub.type && value === sub.value); + const mergeAllUserSubscriptions: Array = (userSubscription as UserSubscription).subscriptionsOn.map( + (sub: Subscription) => { + const updateUserSub = userSubscriptionsUpdate.find( + ({ subscription: { value, type, active } }: UserSubscriptionNotification) => + active === sub.active && type === sub.type && value === sub.value + ); if (updateUserSub) { return updateUserSub.subscription; } return sub; - }); + } + ); - const [updatingUserSubscriptionErr, updatingUserSubscriptionResult] = await catchAsyncError(setTelegramSubscription({ - chat: userSubscription.chat, - subscriptionsOn: mergeAllUserSubscriptions - })); + const [updatingUserSubscriptionErr, updatingUserSubscriptionResult] = await catchAsyncError( + setTelegramSubscription({ + chat: userSubscription.chat, + subscriptionsOn: mergeAllUserSubscriptions, + }) + ); if (!!updatingUserSubscriptionErr) { - return logger.log( - 'error', - { - type: LogglyTypes.SubscriptionNotifierError, - message: `${getErrorMessage(updatingUserSubscriptionErr)}. User ${chatId} notifications has not been updated. Thus, system will wrongly send more updates even that it's already sent such messages to a user`, - } - ); + return logger.log('error', { + type: LogglyTypes.SubscriptionNotifierError, + message: `${getErrorMessage( + updatingUserSubscriptionErr + )}. User ${chatId} notifications has not been updated. Thus, system will wrongly send more updates even that it's already sent such messages to a user`, + }); } } -} +}; const getUserActiveSubscriptionNotifications = ( countriesInfoMap: Map>, @@ -105,40 +115,44 @@ const getUserActiveSubscriptionNotifications = ( ): Array => { let userSubscriptionNotifications: Array = []; - userActiveSubscriptions - .forEach((subscription: Subscription) => { - if (subscription.type === SubscriptionType.Country) { // TODO: Take into account timezone - const userSubscriptionCountry = countriesInfoMap.get(subscription.value.toLocaleLowerCase()); - if (!userSubscriptionCountry) { - return; - } + userActiveSubscriptions.forEach((subscription: Subscription) => { + if (subscription.type === SubscriptionType.Country) { + // TODO: Take into account timezone + const userSubscriptionCountry = countriesInfoMap.get( + subscription.value.toLocaleLowerCase() + ); + if (!userSubscriptionCountry) { + return; + } - const subscriptionCountryLastInfo: CountrySituationInfo = userSubscriptionCountry[userSubscriptionCountry.length - 1]; - if (subscription.lastReceivedData - && !isCountrySituationHasChangedSinceLastData( - subscriptionCountryLastInfo, - subscription.lastReceivedData - )) { - return; - } + const subscriptionCountryLastInfo: CountrySituationInfo = + userSubscriptionCountry[userSubscriptionCountry.length - 1]; + if ( + subscription.lastReceivedData && + !isCountrySituationHasChangedSinceLastData( + subscriptionCountryLastInfo, + subscription.lastReceivedData + ) + ) { + return; + } - userSubscriptionNotifications = [ - ...userSubscriptionNotifications, - { - subscription: { - ...subscription, - lastReceivedData: subscriptionCountryLastInfo, - lastUpdate: Date.now(), - }, - subscriptionMessage: showCountrySubscriptionMessage( - subscriptionCountryLastInfo, - subscription.lastReceivedData ?? {} - ) + userSubscriptionNotifications = [ + ...userSubscriptionNotifications, + { + subscription: { + ...subscription, + lastReceivedData: subscriptionCountryLastInfo, + lastUpdate: Date.now(), }, - ] - } - }); + subscriptionMessage: showCountrySubscriptionMessage( + subscriptionCountryLastInfo, + subscription.lastReceivedData ?? {} + ), + }, + ]; + } + }); return userSubscriptionNotifications; }; - diff --git a/server/src/bots/telegram/utils/chat.ts b/server/src/bots/telegram/utils/chat.ts index dd83019..cfd77f0 100644 --- a/server/src/bots/telegram/utils/chat.ts +++ b/server/src/bots/telegram/utils/chat.ts @@ -1,3 +1,5 @@ -export const getChatId = (message): number => { +import * as TelegramBot from 'node-telegram-bot-api'; + +export const getChatId = (message: TelegramBot.Message): number => { return message.chat.id; -}; \ No newline at end of file +}; diff --git a/server/src/bots/telegram/utils/getUserMessageFromIKorText.ts b/server/src/bots/telegram/utils/getUserMessageFromIKorText.ts index 1dd6e79..9193304 100644 --- a/server/src/bots/telegram/utils/getUserMessageFromIKorText.ts +++ b/server/src/bots/telegram/utils/getUserMessageFromIKorText.ts @@ -1,20 +1,33 @@ -import {TelegramMessage, TelegramMessageReplyMarkup} from '../models'; -import {isNullOrUndefined} from '../../../utils/isNullOrUndefined'; +import { isNullOrUndefined } from '../../../utils/isNullOrUndefined'; +import * as TelegramBot from 'node-telegram-bot-api'; // If it's called from InlineKeyboard, then @param ikCbData will be available // otherwise @param ikCbData will be null -export function getUserMessageFromIKorText(message: TelegramMessage): string; -export function getUserMessageFromIKorText(ikCbData: string, replace: string, replaceValue: string): string; -export function getUserMessageFromIKorText(message: TelegramMessageReplyMarkup, replace: string, replaceValue: string): string -export function getUserMessageFromIKorText(message: T, replace?: string, replaceValue?: string): string { +export function getUserMessageFromIKorText(message: TelegramBot.Message): string; +export function getUserMessageFromIKorText( + ikCbData: string, + replace: string, + replaceValue: string +): string; +export function getUserMessageFromIKorText( + message: TelegramBot.Message, + replace: string, + replaceValue: string +): string; +export function getUserMessageFromIKorText( + message: T, + replace?: string, + replaceValue?: string +): string { if (typeof message === 'string') { return !isNullOrUndefined(replace) && !isNullOrUndefined(replaceValue) ? (message as string).replace(replace, replaceValue).trim() : message; } - return message.reply_markup?.inline_keyboard?.[0]?.[0].text + return ( + message.reply_markup?.inline_keyboard?.[0]?.[0].text .replace(replace, replaceValue) - .trim() - ?? message.text + .trim() ?? message.text + ); } diff --git a/server/src/index.ts b/server/src/index.ts index 9f63df0..ab722c8 100644 --- a/server/src/index.ts +++ b/server/src/index.ts @@ -1,37 +1,32 @@ import * as express from 'express'; import * as bodyParser from 'body-parser'; import * as baseController from './routes/base/base'; -import {runTelegramBot} from './bots/telegram'; -import {runNgrok, stopNgrok} from './runNgrok'; +import { runTelegramBot } from './bots/telegram'; +import { runNgrok, stopNgrok } from './runNgrok'; import environments from './environments/environment'; -import {initFirebase} from './services/infrastructure/firebase'; -import {CONSOLE_LOG_DELIMITER, CONSOLE_LOG_EASE_DELIMITER} from './models/constants'; +import { initFirebase } from './services/infrastructure/firebase'; +import { CONSOLE_LOG_DELIMITER, CONSOLE_LOG_EASE_DELIMITER } from './models/constants'; import * as firebase from 'firebase'; -import {checkCovid19Updates} from './services/infrastructure/scheduler'; +import { checkCovid19Updates } from './services/infrastructure/scheduler'; +import { catchAsyncError } from './utils/catchError'; export const app = express(); const PORT = process.env.PORT || 3000; const environmentName = process.env.ENVIRONMENT_NAME; app.use(bodyParser.json()); -app.use(bodyParser.urlencoded({extended: true})); +app.use(bodyParser.urlencoded({ extended: true })); app.get('/', baseController.base); const server = app.listen(PORT, async () => { - let appUrl = environments.APP_URL; + const appUrl = environments.APP_URL; // tslint:disable-next-line:no-console - console.log( - ('App is running at http://localhost:%d in %s mode'), - PORT, - environmentName, - ); - // tslint:disable-next-line:no-console - console.log('\nPress CTRL-C to stop'); + console.log('App is running at http://localhost:%d in %s mode', PORT, environmentName); if (environments.IsNgRokMode()) { - // tslint:disable-next-line:no-console - console.log(`${CONSOLE_LOG_DELIMITER}Starting ngrok`); - appUrl = environments.NGROK_URL || await runNgrok(PORT); + const [err, appUrl] = await catchAsyncError( + environments.NGROK_URL ? Promise.resolve(environments.NGROK_URL) : runNgrok(PORT) + ); // tslint:disable-next-line:no-console console.log(`${CONSOLE_LOG_EASE_DELIMITER} NGROK started on ngRokUrl: ${appUrl}`); } @@ -39,7 +34,9 @@ const server = app.listen(PORT, async () => { const [e, isFirebaseInit] = initFirebase(environments); if (!isFirebaseInit) { // tslint:disable-next-line:no-console - console.log(`${CONSOLE_LOG_DELIMITER}Firebase did not start. Error ${e.name}, ${e.message}. Stack: ${e.stack}`); + console.log( + `${CONSOLE_LOG_DELIMITER}Firebase did not start. Error ${e.name}, ${e.message}. Stack: ${e.stack}` + ); } checkCovid19Updates(); @@ -64,4 +61,3 @@ process.on('SIGTERM', () => { process.exit(0); }); }); - diff --git a/server/src/models/subscription.models.ts b/server/src/models/subscription.models.ts index 70d96ae..2fb6920 100644 --- a/server/src/models/subscription.models.ts +++ b/server/src/models/subscription.models.ts @@ -1,5 +1,5 @@ -import {TelegramChat} from '../bots/telegram/models'; -import {CountrySituationInfo} from './covid19.models'; +import { CountrySituationInfo } from './covid19.models'; +import * as TelegramBot from 'node-telegram-bot-api'; export enum SubscriptionType { TrackCountryUpdates = 'New country', @@ -17,7 +17,7 @@ export interface Subscription { } export interface UserSubscription { - chat: TelegramChat; + chat: TelegramBot.Chat; subscriptionsOn: Array; } diff --git a/server/src/routes/base/base.ts b/server/src/routes/base/base.ts index 1c419a1..a23639d 100644 --- a/server/src/routes/base/base.ts +++ b/server/src/routes/base/base.ts @@ -1,11 +1,13 @@ -import {Request, Response} from 'express'; - -const pkg = require(__dirname + '/../../../../package.json'); +import { Request, Response } from 'express'; +import { version } from '../../../../package.json'; export let base = (req: Request, res: Response) => { res.json({ - message: 'Welcome to Covid19 live bot api. Refer to documentation here - https://github.com/danbilokha/covid19liveupdates', - version: pkg.version || 'They didn\'n say me :c', - containerVersion: process.env.CONTAINER_VERSION || 'They didn\'n say me :c But it should be the same as package.version', + message: + 'Welcome to Covid19 live bot api. Refer to documentation here - https://github.com/danbilokha/covid19liveupdates', + version: version ?? 'They didn\'n say me :c', + containerVersion: + process.env.CONTAINER_VERSION ?? + 'They didn\'n say me :c But it should be the same as package.version', }); }; diff --git a/server/src/services/api/api-covid19.ts b/server/src/services/api/api-covid19.ts index d4a8379..ca33f9f 100644 --- a/server/src/services/api/api-covid19.ts +++ b/server/src/services/api/api-covid19.ts @@ -1,8 +1,9 @@ -import {ApiCountriesCovid19Situation} from '../../models/covid19.models'; -import axios, {AxiosResponse} from 'axios'; +import { ApiCountriesCovid19Situation } from '../../models/covid19.models'; +import axios, { AxiosResponse } from 'axios'; import Config from '../../environments/environment'; export function fetchCovid19Data(): Promise { - return axios.get(`${Config.COVID19API_URL}/timeseries.json`) - .then((response: AxiosResponse): ApiCountriesCovid19Situation => response.data) -} \ No newline at end of file + return axios + .get(`${Config.COVID19API_URL}/timeseries.json`) + .then((response: AxiosResponse): ApiCountriesCovid19Situation => response.data); +} diff --git a/server/src/services/api/api-knowledgebase.ts b/server/src/services/api/api-knowledgebase.ts index 1df5dba..d2fe87f 100644 --- a/server/src/services/api/api-knowledgebase.ts +++ b/server/src/services/api/api-knowledgebase.ts @@ -1,38 +1,54 @@ -import axios, {AxiosResponse} from 'axios'; -import {Answer, ApiAnswer} from '../../models/knowledgebase/answer.models'; +import axios, { AxiosResponse } from 'axios'; +import { Answer, ApiAnswer } from '../../models/knowledgebase/answer.models'; import environments from '../../environments/environment'; -import {KnowledgebaseMeta} from '../../models/knowledgebase/meta.models'; +import { KnowledgebaseMeta } from '../../models/knowledgebase/meta.models'; export function fetchKnowledgeMetainformation(): Promise { - return axios.get(`${environments.KNOWLEDGEBASE_URL}/meta/all`, { - headers: {'x-access-token': environments.KNOWLEDGEBASE_SECRET_KEY} - }) - .then((response: AxiosResponse): KnowledgebaseMeta => response.data) + return axios + .get(`${environments.KNOWLEDGEBASE_URL}/meta/all`, { + headers: { 'x-access-token': environments.KNOWLEDGEBASE_SECRET_KEY }, + }) + .then((response: AxiosResponse): KnowledgebaseMeta => response.data); } export function fetchAnswer(question: string): Promise> { - return axios.get(`${environments.KNOWLEDGEBASE_URL}/question`, { - headers: {'x-access-token': environments.KNOWLEDGEBASE_SECRET_KEY}, - params: {question}, - } - ) + return axios + .get(`${environments.KNOWLEDGEBASE_URL}/question`, { + headers: { 'x-access-token': environments.KNOWLEDGEBASE_SECRET_KEY }, + params: { question }, + }) .then((response: AxiosResponse): Array => response.data) .then( - (answers: Array): Array => answers.map(({ - category, - countries, - answer, - links, - additional_answers, - additional_links - }: ApiAnswer): Answer => ({ - category, - answer, - question, - countries: countries.split(';').map(v => v.trim()).filter(v => !!v), - links: links.replace(/ /g, '').split(';').filter(v => !!v), - additionalAnswers: additional_answers.split(';').map(v => v.trim()).filter(v => !!v), - additionalLinks: additional_links.replace(/ /g, '').split(';').filter(v => !!v), - })) - ) -} \ No newline at end of file + (answers: Array): Array => + answers.map( + ({ + category, + countries, + answer, + links, + additional_answers, + additional_links, + }: ApiAnswer): Answer => ({ + category, + answer, + question, + countries: countries + .split(';') + .map((v) => v.trim()) + .filter((v) => !!v), + links: links + .replace(/ /g, '') + .split(';') + .filter((v) => !!v), + additionalAnswers: additional_answers + .split(';') + .map((v) => v.trim()) + .filter((v) => !!v), + additionalLinks: additional_links + .replace(/ /g, '') + .split(';') + .filter((v) => !!v), + }) + ) + ); +} diff --git a/server/src/services/domain/chart.ts b/server/src/services/domain/chart.ts index cabefd7..b724a5f 100644 --- a/server/src/services/domain/chart.ts +++ b/server/src/services/domain/chart.ts @@ -1,31 +1,33 @@ -import {Status} from '../../models/constants' -import {ChartModel} from '../../models/chart.models' -import {CountrySituationInfo} from '../../models/covid19.models'; +import { Status } from '../../models/constants'; +import { ChartModel } from '../../models/chart.models'; +import { CountrySituationInfo } from '../../models/covid19.models'; -export const Transform = (situations: CountrySituationInfo[]): ChartModel => { - const days = situations.map(x => x.date); - return { - type: 'line', - data: { - labels: days, +export const Transform = (situations: CountrySituationInfo[]): ChartModel => { + const days = situations.map((x) => x.date); + return { + type: 'line', + data: { + labels: days, datasets: [ - { label: Status.Confirmed, data: situations.map(x => x.confirmed), - fill: false, - borderColor: 'blue' - }, - { - label: Status.Deaths, - data: situations.map(x => x.deaths), - fill: false, - borderColor: 'red' + { + label: Status.Confirmed, + data: situations.map((x) => x.confirmed), + fill: false, + borderColor: 'blue', }, - { - label: Status.Recovered, - data: situations.map(x => x.recovered), - fill: false, - borderColor: 'green' - } - ] - } + { + label: Status.Deaths, + data: situations.map((x) => x.deaths), + fill: false, + borderColor: 'red', + }, + { + label: Status.Recovered, + data: situations.map((x) => x.recovered), + fill: false, + borderColor: 'green', + }, + ], + }, }; -} \ No newline at end of file +}; diff --git a/server/src/services/domain/countriesByContinent.ts b/server/src/services/domain/countriesByContinent.ts index 1df3e9a..aff7b3e 100644 --- a/server/src/services/domain/countriesByContinent.ts +++ b/server/src/services/domain/countriesByContinent.ts @@ -1,21 +1,20 @@ -import {Country} from '../../models/country.models'; -import {ContinentsCountries} from '../../models/continent.models'; +import { Country } from '../../models/country.models'; +import { ContinentsCountries } from '../../models/continent.models'; export const getCountriesByContinent = (countries: Array): ContinentsCountries => { const continentsCountries: ContinentsCountries = {}; - countries - .forEach(({name, continent}: Country) => { - const continentCountries = continentsCountries[continent]; + countries.forEach(({ name, continent }: Country) => { + const continentCountries = continentsCountries[continent]; - if (!!continentCountries?.length) { - continentCountries.push(name); - return; - } - - continentsCountries[continent] = [name]; + if (!!continentCountries?.length) { + continentCountries.push(name); return; - }); + } + + continentsCountries[continent] = [name]; + return; + }); return continentsCountries; }; diff --git a/server/src/services/domain/countryLookup.ts b/server/src/services/domain/countryLookup.ts index 25d4502..82c6f51 100644 --- a/server/src/services/domain/countryLookup.ts +++ b/server/src/services/domain/countryLookup.ts @@ -1,4 +1,4 @@ -import {CountryLookup} from '../../models/country-code-lookup.models'; +import { CountryLookup } from '../../models/country-code-lookup.models'; import * as lookup from 'country-code-lookup'; const COUNTRIES_LOOKUP_LIST: Array = lookup.countries; @@ -12,14 +12,14 @@ export const getDefaultCountry = (name): CountryLookup => ({ iso2: 'Other', iso3: 'Other', isoNo: 'Other', - internet: 'Other' + internet: 'Other', }); export const getCountryByName = (name: string): CountryLookup => { /* tslint:disable:prefer-for-of */ for (let i = 0; i < COUNTRIES_LOOKUP_LIST.length; i++) { if (COUNTRIES_LOOKUP_LIST[i].country.toLocaleLowerCase() === name.toLocaleLowerCase()) { - return COUNTRIES_LOOKUP_LIST[i] + return COUNTRIES_LOOKUP_LIST[i]; } } diff --git a/server/src/services/domain/covid19.ts b/server/src/services/domain/covid19.ts index bf6994d..6bbce7e 100644 --- a/server/src/services/domain/covid19.ts +++ b/server/src/services/domain/covid19.ts @@ -1,27 +1,39 @@ -import {UserPresentationalCountryNameString} from '../../models/tsTypes.models'; -import {ApiCountriesCovid19Situation, ApiCovid19Situation, CountrySituationInfo} from '../../models/covid19.models'; -import {COVID19_FETCH_SALT, TIMES} from '../../models/constants'; -import {Country} from '../../models/country.models'; -import {fetchCovid19Data} from '../api/api-covid19'; -import {CountryLookup} from '../../models/country-code-lookup.models'; -import {getCountryNameFormat} from '../../utils/featureHelpers/country'; -import {getCountryByName, getDefaultCountry} from './countryLookup'; -import {SubscriptionType} from '../../models/subscription.models'; -import {logger} from '../../utils/logger'; -import {LogglyTypes} from '../../models/loggly.models'; +import { UserPresentationalCountryNameString } from '../../models/tsTypes.models'; +import { + ApiCountriesCovid19Situation, + ApiCovid19Situation, + CountrySituationInfo, +} from '../../models/covid19.models'; +import { COVID19_FETCH_SALT, TIMES } from '../../models/constants'; +import { Country } from '../../models/country.models'; +import { fetchCovid19Data } from '../api/api-covid19'; +import { CountryLookup } from '../../models/country-code-lookup.models'; +import { getCountryNameFormat } from '../../utils/featureHelpers/country'; +import { getCountryByName, getDefaultCountry } from './countryLookup'; +import { SubscriptionType } from '../../models/subscription.models'; +import { logger } from '../../utils/logger'; +import { LogglyTypes } from '../../models/loggly.models'; +import * as TelegramBot from 'node-telegram-bot-api'; // TODO: Improve Cached management class CachedCovid19CountriesData { private subscribers: Array<[Array, Function]> = []; - public set countriesData(countriesData: [number, Array<[Country, Array]>]) { + public set countriesData( + countriesData: [number, Array<[Country, Array]>] + ) { this.cachedCountriesData = countriesData; this.subscribers.forEach(([subscriptionsType, cb]: [Array, Function]) => { - if (subscriptionsType.some((subscriptionType: SubscriptionType) => subscriptionType !== SubscriptionType.TrackCountryUpdates)) { + if ( + subscriptionsType.some( + (subscriptionType: SubscriptionType) => + subscriptionType !== SubscriptionType.TrackCountryUpdates + ) + ) { cb(this.countriesData); } - }) + }); } public get countriesData(): [number, Array<[Country, Array]>] { @@ -32,19 +44,28 @@ class CachedCovid19CountriesData { this.cachedAvailableCountriesData = countries; this.subscribers.forEach(([subscriptionsType, cb]: [Array, Function]) => { - if (subscriptionsType.some((subscriptionType: SubscriptionType) => subscriptionType === SubscriptionType.TrackCountryUpdates)) { + if ( + subscriptionsType.some( + (subscriptionType: SubscriptionType) => + subscriptionType === SubscriptionType.TrackCountryUpdates + ) + ) { cb(this.availableCountriesData); } - }) + }); } public get availableCountriesData(): Array { return this.cachedAvailableCountriesData; } - constructor(private cachedCountriesData: [number, Array<[Country, Array]>] = [0, []], - private cachedAvailableCountriesData: Array = []) { - } + constructor( + private cachedCountriesData: [number, Array<[Country, Array]>] = [ + 0, + [], + ], + private cachedAvailableCountriesData: Array = [] + ) {} public subscribe(cb: Function, subscriptionsType: Array): void { this.subscribers = [...this.subscribers, [subscriptionsType, cb]]; @@ -53,66 +74,70 @@ class CachedCovid19CountriesData { export const cachedCovid19CountriesData = new CachedCovid19CountriesData(); -export const adaptCountryToSystemRepresentation = - (country: string): UserPresentationalCountryNameString => getCountryNameFormat( - country - .trim() - .toLocaleUpperCase() +export const adaptCountryToSystemRepresentation = ( + country: string +): UserPresentationalCountryNameString => getCountryNameFormat(country.trim().toLocaleUpperCase()); + +function adaptApiCountriesResponse( + apiCountriesSituation: ApiCountriesCovid19Situation +): Array<[Country, Array]> { + return Object.entries(apiCountriesSituation).map( + ([apiCountry, apiSituations]: [string, Array]) => { + const adaptedCountry: UserPresentationalCountryNameString = adaptCountryToSystemRepresentation( + apiCountry + ); + const countryLookup: CountryLookup = + getCountryByName(adaptedCountry) ?? getDefaultCountry(adaptedCountry); + + const country: Country = { + name: adaptedCountry, + region: countryLookup.region, + continent: countryLookup.continent, + }; + + return [ + country, + apiSituations.map((situation: ApiCovid19Situation) => ({ + ...country, + ...situation, + recovered: situation.recovered ?? 0, + deaths: situation.deaths ?? 0, + confirmed: situation.confirmed ?? 0, + })), + ]; + } ); - -function adaptApiCountriesResponse(apiCountriesSituation: ApiCountriesCovid19Situation): Array<[Country, Array]> { - return Object.entries(apiCountriesSituation) - .map(([apiCountry, apiSituations]: [string, Array]) => { - const adaptedCountry: UserPresentationalCountryNameString = adaptCountryToSystemRepresentation(apiCountry); - const countryLookup: CountryLookup = getCountryByName(adaptedCountry) ?? getDefaultCountry(adaptedCountry); - - const country: Country = { - name: adaptedCountry, - region: countryLookup.region, - continent: countryLookup.continent, - }; - - return [ - country, - apiSituations.map((situation: ApiCovid19Situation) => ({ - ...country, - ...situation, - recovered: situation.recovered ?? 0, - deaths: situation.deaths ?? 0, - confirmed: situation.confirmed ?? 0, - })) - ] - } - ); } function getCovid19Data(): Promise]>> { - return fetchCovid19Data() - .then((apiCountriesSituation: ApiCountriesCovid19Situation) => { - const countriesSituation: Array<[Country, Array]> = adaptApiCountriesResponse(apiCountriesSituation); - cachedCovid19CountriesData.availableCountriesData = countriesSituation.map(([country]) => country); - cachedCovid19CountriesData.countriesData = [Date.now(), countriesSituation]; + return fetchCovid19Data().then((apiCountriesSituation: ApiCountriesCovid19Situation) => { + const countriesSituation: Array<[ + Country, + Array + ]> = adaptApiCountriesResponse(apiCountriesSituation); + cachedCovid19CountriesData.availableCountriesData = countriesSituation.map( + ([country]) => country + ); + cachedCovid19CountriesData.countriesData = [Date.now(), countriesSituation]; - return countriesSituation; - }); + return countriesSituation; + }); } -export function tryToUpdateCovid19Cache(): Promise { +export function tryToUpdateCovid19Cache(): Promise { return getCovid19Data() - .then(v => undefined) - .catch(e => logger.log( - 'error', - { + .then((v) => undefined) + .catch((e) => + logger.log('error', { type: LogglyTypes.Covid19DataUpdateError, message: `[ERROR] While fetching Hopkins uni data. ${e?.message}, ${e?.stack}`, - } - )); + }) + ); } export function getCountriesSituation(): Promise]>> { const [lastFetchedTime, countriesSituation] = cachedCovid19CountriesData.countriesData ?? []; - if (lastFetchedTime > Date.now() - TIMES.MILLISECONDS_IN_HOUR - COVID19_FETCH_SALT) { return Promise.resolve(countriesSituation); } @@ -125,8 +150,8 @@ export function getAvailableCountries(): Promise> { return Promise.resolve(cachedCovid19CountriesData.availableCountriesData); } - return getCovid19Data() - .then((countriesSituation: Array<[Country, Array]>) => + return getCovid19Data().then( + (countriesSituation: Array<[Country, Array]>) => countriesSituation.map(([country]) => country) - ) + ); } diff --git a/server/src/services/domain/storage.ts b/server/src/services/domain/storage.ts index 0927e58..a18c812 100644 --- a/server/src/services/domain/storage.ts +++ b/server/src/services/domain/storage.ts @@ -1,7 +1,7 @@ import * as firebase from 'firebase'; -import {Subscription, UserSubscription} from '../../models/subscription.models'; -import {SubscriptionStorage} from '../../models/storage.models'; -import {TelegramMessage} from '../../bots/telegram/models'; +import { Subscription, UserSubscription } from '../../models/subscription.models'; +import { SubscriptionStorage } from '../../models/storage.models'; +import * as TelegramBot from 'node-telegram-bot-api'; import DataSnapshot = firebase.database.DataSnapshot; export const getFllStorage = async (): Promise => { @@ -9,27 +9,39 @@ export const getFllStorage = async (): Promise => { return snapshot.val() ?? {}; }; -export const getMessengerStorage = (messengerPrefix: string): Function => async (): Promise => { +export const getMessengerStorage = (messengerPrefix: string): Function => async (): Promise< + T +> => { const snapshot: DataSnapshot = await firebase.database().ref(messengerPrefix).once('value'); return snapshot.val() ?? {}; }; export const listenSubscriptionsChanges = (messengerPrefix: string): Function => ( cb: (a: firebase.database.DataSnapshot, b?: string | null) => unknown -): (a: firebase.database.DataSnapshot | null, b?: string | null) => unknown => { - return firebase.database() - .ref(`${messengerPrefix}/subscriptions`) - .on('value', cb); +): ((a: firebase.database.DataSnapshot | null, b?: string | null) => unknown) => { + return firebase.database().ref(`${messengerPrefix}/subscriptions`).on('value', cb); }; -export const getSubscriptions = (messengerPrefix: string) => async (): Promise => { - const snapshot = await firebase.database().ref(`${messengerPrefix}/subscriptions`).once('value'); +export const getSubscriptions = (messengerPrefix: string) => async (): Promise< + SubscriptionStorage +> => { + const snapshot = await firebase + .database() + .ref(`${messengerPrefix}/subscriptions`) + .once('value'); return snapshot.val() ?? {}; }; -export const getActiveSubscriptions = (messengerPrefix: string) => async (): Promise => { - const snapshot = await firebase.database().ref(`${messengerPrefix}/subscriptions`).once('value'); - const subscriptionStorage: SubscriptionStorage | undefined = snapshot.val() as SubscriptionStorage; +export const getActiveSubscriptions = (messengerPrefix: string) => async (): Promise< + SubscriptionStorage +> => { + const snapshot = await firebase + .database() + .ref(`${messengerPrefix}/subscriptions`) + .once('value'); + const subscriptionStorage: + | SubscriptionStorage + | undefined = snapshot.val() as SubscriptionStorage; if (!subscriptionStorage) { return {}; } @@ -39,43 +51,54 @@ export const getActiveSubscriptions = (messengerPrefix: string) => async (): for (const [chatId, userSubscription] of Object.entries(subscriptionStorage)) { activeSubscriptions[chatId] = { ...userSubscription, - subscriptionsOn: userSubscription?.subscriptionsOn?.filter((subscription: Subscription) => subscription.active) + subscriptionsOn: userSubscription?.subscriptionsOn?.filter( + (subscription: Subscription) => subscription.active + ), }; } return activeSubscriptions; }; -export const getUserSubscription = (messengerPrefix: string) => async (chatId: number): Promise => { - const snapshot = await firebase.database().ref(`${messengerPrefix}/subscriptions/${chatId}`).once('value'); +export const getUserSubscription = (messengerPrefix: string) => async ( + chatId: number +): Promise => { + const snapshot = await firebase + .database() + .ref(`${messengerPrefix}/subscriptions/${chatId}`) + .once('value'); return snapshot.val() ?? {}; }; -export const getActiveUserSubscription = (messengerPrefix: string) => async (chatId: number): Promise => { - const snapshot = await firebase.database().ref(`${messengerPrefix}/subscriptions/${chatId}`).once('value'); +export const getActiveUserSubscription = (messengerPrefix: string) => async ( + chatId: number +): Promise => { + const snapshot = await firebase + .database() + .ref(`${messengerPrefix}/subscriptions/${chatId}`) + .once('value'); const userSubscription: UserSubscription = snapshot.val() as UserSubscription; return { ...userSubscription, - subscriptionsOn: userSubscription?.subscriptionsOn?.filter((sub: Subscription) => sub.active) + subscriptionsOn: userSubscription?.subscriptionsOn?.filter( + (sub: Subscription) => sub.active + ), }; }; -export const setSubscription = (messengerPrefix: string) => async ( - {chat, subscriptionsOn}: UserSubscription -): Promise => { - return firebase.database() - .ref(`${messengerPrefix}/subscriptions/${chat.id}`) - .set(({ - chat, - subscriptionsOn - })) +export const setSubscription = (messengerPrefix: string) => async ({ + chat, + subscriptionsOn, +}: UserSubscription): Promise => { + return firebase.database().ref(`${messengerPrefix}/subscriptions/${chat.id}`).set({ + chat, + subscriptionsOn, + }); }; export const setQueryToAnalyse = (messengerPrefix: string) => async ( - message: TelegramMessage // I think it's OK to have this dependency here, we can have + message: TelegramBot.Message // I think it's OK to have this dependency here, we can have // different messengers intersection here ): Promise => { - return firebase.database() - .ref(`${messengerPrefix}/analyse/${message.message_id}`) - .set((message)) + return firebase.database().ref(`${messengerPrefix}/analyse/${message.message_id}`).set(message); }; diff --git a/server/src/services/domain/subscriptions.ts b/server/src/services/domain/subscriptions.ts index c80dac7..b472992 100644 --- a/server/src/services/domain/subscriptions.ts +++ b/server/src/services/domain/subscriptions.ts @@ -1,30 +1,43 @@ -import {TelegramChat} from '../../bots/telegram/models'; -import {getCountriesSituation} from './covid19'; -import {Country} from '../../models/country.models'; -import {getTelegramUserSubscriptions, setTelegramSubscription} from '../../bots/telegram/services/storage'; -import {Subscription, SubscriptionType} from '../../models/subscription.models'; -import {catchAsyncError} from '../../utils/catchError'; -import {ALREADY_SUBSCRIBED_MESSAGE} from '../../messages/feature/subscribeMessages'; -import {CountrySituationInfo} from '../../models/covid19.models'; +import { getCountriesSituation } from './covid19'; +import { Country } from '../../models/country.models'; +import { + getTelegramUserSubscriptions, + setTelegramSubscription, +} from '../../bots/telegram/services/storage'; +import { Subscription, SubscriptionType } from '../../models/subscription.models'; +import { catchAsyncError } from '../../utils/catchError'; +import { ALREADY_SUBSCRIBED_MESSAGE } from '../../messages/feature/subscribeMessages'; +import { CountrySituationInfo } from '../../models/covid19.models'; +import * as TelegramBot from 'node-telegram-bot-api'; /* @params Assume subscribeMeOn is just country name (for now) */ -export const subscribeOn = async (chat: TelegramChat, subscribeMeOn: string): Promise => { - const availableCountries: Array<[Country, Array]> = await getCountriesSituation(); +export const subscribeOn = async ( + chat: TelegramBot.Chat, + subscribeMeOn: string +): Promise => { + const availableCountries: Array<[ + Country, + Array + ]> = await getCountriesSituation(); - const [subscribeMeOnCountry, countrySituations]: [Country, Array] = availableCountries - .find(([country, _]: [Country, Array]) => - country.name.toLocaleLowerCase() === subscribeMeOn.toLocaleLowerCase()); + const [subscribeMeOnCountry, countrySituations]: [ + Country, + Array + ] = availableCountries.find( + ([country, _]: [Country, Array]) => + country.name.toLocaleLowerCase() === subscribeMeOn.toLocaleLowerCase() + ); if (!subscribeMeOnCountry) { - throw Error('Is not supported, yet') + throw Error('Is not supported, yet'); } // TODO: Remove Telegram dependency - const existingSubscriptions: Array = (await getTelegramUserSubscriptions(chat.id) ?? {}) - .subscriptionsOn ?? []; + const existingSubscriptions: Array = + ((await getTelegramUserSubscriptions(chat.id)) ?? {}).subscriptionsOn ?? []; const checkIfAlreadySubscribed = existingSubscriptions .filter((subscription: Subscription) => subscription.active) @@ -44,47 +57,53 @@ export const subscribeOn = async (chat: TelegramChat, subscribeMeOn: string): Pr value: subscribeMeOnCountry.name, lastReceivedData: countrySituations[countrySituations.length - 1], lastUpdate: Date.now(), - } - ] + }, + ], }); return subscribeMeOnCountry.name; }; -export const unsubscribeMeFrom = async (chat: TelegramChat, unsubscribeMeFrom: string): Promise => { +export const unsubscribeMeFrom = async ( + chat: TelegramBot.Chat, + unsubscribeMeFrom: string +): Promise => { // TODO: Remove Telegram dependency - const existingSubscriptions: Array = (await getTelegramUserSubscriptions(chat.id) ?? {}) - .subscriptionsOn ?? []; + const existingSubscriptions: Array = + ((await getTelegramUserSubscriptions(chat.id)) ?? {}).subscriptionsOn ?? []; let foundSubscription: Subscription; - const updatedSubscriptions: Array = existingSubscriptions - .map((subscription: Subscription) => { + const updatedSubscriptions: Array = existingSubscriptions.map( + (subscription: Subscription) => { if (subscription.value === unsubscribeMeFrom) { foundSubscription = subscription; subscription.active = false; } return subscription; - }); + } + ); if (!foundSubscription) { throw new Error('I was not able to find your subscription'); } - const [err, result] = await catchAsyncError(setTelegramSubscription({ - chat, - subscriptionsOn: updatedSubscriptions - })); + const [err, result] = await catchAsyncError( + setTelegramSubscription({ + chat, + subscriptionsOn: updatedSubscriptions, + }) + ); if (err) { - throw Error('Issue with Subscription service. Try later or drop us a message. Sorry for inconvenience'); + throw Error( + 'Issue with Subscription service. Try later or drop us a message. Sorry for inconvenience' + ); } return unsubscribeMeFrom; }; export const isCountrySituationHasChangedSinceLastData = ( - {confirmed, deaths, recovered}: CountrySituationInfo, - {confirmed: prevConfirmed, deaths: prevDeaths, recovered: prevRecovered}: CountrySituationInfo, + { confirmed, deaths, recovered }: CountrySituationInfo, + { confirmed: prevConfirmed, deaths: prevDeaths, recovered: prevRecovered }: CountrySituationInfo ): boolean => { - return confirmed !== prevConfirmed - || deaths !== prevDeaths - || recovered !== prevRecovered + return confirmed !== prevConfirmed || deaths !== prevDeaths || recovered !== prevRecovered; }; diff --git a/server/src/services/infrastructure/firebase.ts b/server/src/services/infrastructure/firebase.ts index 9417137..3121d19 100644 --- a/server/src/services/infrastructure/firebase.ts +++ b/server/src/services/infrastructure/firebase.ts @@ -1,14 +1,14 @@ import * as firebase from 'firebase/app'; export const initFirebase = ({ - FIREBASE_API_KEY, - FIREBASE_AUTHDOMAIN, - FIREBASE_DATABASE_URL, - FIREBASE_PROJECT_ID, - FIREBASE_STORAGE_BUCKET, - FIREBASE_MESSAGING_SENDER_ID, - FIREBASE_APP_ID - }): [Error, boolean] => { + FIREBASE_API_KEY, + FIREBASE_AUTHDOMAIN, + FIREBASE_DATABASE_URL, + FIREBASE_PROJECT_ID, + FIREBASE_STORAGE_BUCKET, + FIREBASE_MESSAGING_SENDER_ID, + FIREBASE_APP_ID, +}): [Error, boolean] => { try { firebase.initializeApp({ apiKey: FIREBASE_API_KEY, diff --git a/server/src/services/infrastructure/scheduler.ts b/server/src/services/infrastructure/scheduler.ts index 905d75d..967b952 100644 --- a/server/src/services/infrastructure/scheduler.ts +++ b/server/src/services/infrastructure/scheduler.ts @@ -1,18 +1,16 @@ import * as schedule from 'node-schedule'; -import {tryToUpdateCovid19Cache} from '../domain/covid19'; -import {logger} from '../../utils/logger'; -import {LogglyTypes} from '../../models/loggly.models'; +import { tryToUpdateCovid19Cache } from '../domain/covid19'; +import { logger } from '../../utils/logger'; +import { LogglyTypes } from '../../models/loggly.models'; export const checkCovid19Updates = () => { // Check covid19 info every hour (at hh:30 mins, e.g. 1:30, 2:30 ...) schedule.scheduleJob('30 * * * *', () => { - tryToUpdateCovid19Cache() - .then(() => logger.log( - 'info', - { - type: LogglyTypes.Covid19DataUpdateInfo, - message: 'Covid19 cache updated', - } - )); + tryToUpdateCovid19Cache().then(() => + logger.log('info', { + type: LogglyTypes.Covid19DataUpdateInfo, + message: 'Covid19 cache updated', + }) + ); }); }; diff --git a/server/src/utils/getErrorMessages.ts b/server/src/utils/getErrorMessages.ts new file mode 100644 index 0000000..28eb1aa --- /dev/null +++ b/server/src/utils/getErrorMessages.ts @@ -0,0 +1,4 @@ +export const getErrorMessage = ({ message, name, stack }: Error): string => + `[ERROR] ${name}, ${message}, ${stack}`; + +export const getInfoMessage = (message: string): string => `[INFO] ${message}`; diff --git a/server/src/utils/getLoggerMessages.ts b/server/src/utils/getLoggerMessages.ts deleted file mode 100644 index 7127b46..0000000 --- a/server/src/utils/getLoggerMessages.ts +++ /dev/null @@ -1,5 +0,0 @@ -export const getErrorMessage = ({message, name, stack}: Error): string => - `[ERROR] ${name}, ${message}, ${stack}`; - -export const getInfoMessage = (message: string): string => - `[INFO] ${message}`; diff --git a/server/src/utils/logger.ts b/server/src/utils/logger.ts index 709a579..6554947 100644 --- a/server/src/utils/logger.ts +++ b/server/src/utils/logger.ts @@ -1,16 +1,16 @@ import * as winston from 'winston'; -import {Loggly} from 'winston-loggly-bulk'; +import { Loggly } from 'winston-loggly-bulk'; import environment from '../environments/environment'; -import {LogglyModels} from '../models/loggly.models'; - if (environment.LOGGLY_TOKEN) { - winston.add(new Loggly({ - token: environment.LOGGLY_TOKEN, - subdomain: environment.LOGGLY_SUBDOMAIN, - tags: environment.LOGGLY_TAGS, - json: true - })); + winston.add( + new Loggly({ + token: environment.LOGGLY_TOKEN, + subdomain: environment.LOGGLY_SUBDOMAIN, + tags: environment.LOGGLY_TAGS, + json: true, + }) + ); } export const logger = { @@ -20,5 +20,5 @@ export const logger = { // tslint:disable-next-line:no-console console.log(severity, message); } - } + }, }; diff --git a/server/src/utils/removeCommandFromMessageIfExist.ts b/server/src/utils/removeCommandFromMessageIfExist.ts index 4447774..8dd3a7b 100644 --- a/server/src/utils/removeCommandFromMessageIfExist.ts +++ b/server/src/utils/removeCommandFromMessageIfExist.ts @@ -1,21 +1,9 @@ -import {isMessageStartsWithCommand} from './incomingMessages'; -import {getInfoMessage} from './getLoggerMessages'; -import {logger} from './logger'; -import {LogglyTypes} from '../models/loggly.models'; +import { isMessageStartsWithCommand } from './incomingMessages'; export const removeCommandFromMessageIfExist = (message: string, command: string): string => { if (!isMessageStartsWithCommand(message)) { - logger.log( - 'info', - { - type: LogglyTypes.RemoveCommandFromMessageIfExistInfo, - message: `${getInfoMessage(`Cannot remove command from ${message}`)}`, - } - ); return message; } - return message.includes(command) - ? message.replace(command, '').trim() - : message; + return message.includes(command) ? message.replace(command, '').trim() : message; }; diff --git a/server/src/utils/textAfterCommand.ts b/server/src/utils/textAfterCommand.ts index 51e4c61..a3f48c3 100644 --- a/server/src/utils/textAfterCommand.ts +++ b/server/src/utils/textAfterCommand.ts @@ -1 +1,2 @@ -export const textAfterUserCommand = (message: string): string => message.slice(message.indexOf(' ')).trim(); \ No newline at end of file +export const textAfterUserCommand = (message: string): string => + message.slice(message.indexOf(' ')).trim(); From c6108c37fd7488bbdc314d8fcfcf7de788cb7fdd Mon Sep 17 00:00:00 2001 From: Danylo Bilokha Date: Sat, 18 Apr 2020 14:22:54 +0300 Subject: [PATCH 2/9] update .env.example --- server/src/environments/.env.example | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/server/src/environments/.env.example b/server/src/environments/.env.example index b1900e3..45ba110 100644 --- a/server/src/environments/.env.example +++ b/server/src/environments/.env.example @@ -1,9 +1,17 @@ COUNTRIESDATA_URL='https://pomber.github.io/covid19' KNOWLEDGEBASE_URL='' +KNOWLEDGEBASE_SECRET_KEY='JWT token for knowledgebase' TELEGRAM_TOKEN='Token from BotFather' CONTAINER_VERSION='' LOGGLY_TOKEN='Token for loggy' LOGGLY_SUBDOMAIN='covid19liveupd' LOGGLY_TAGS='Setup in Heroku interface' APP_URL='Setup in Heroku interface' -NGROK_URL='Automatically setup for development purposes' \ No newline at end of file +NGROK_URL='Automatically setup for development purposes' +FIREBASE_API_KEY='firebase' +FIREBASE_AUTHDOMAIN='firebase' +FIREBASE_DATABASE_URL='firebase' +FIREBASE_PROJECT_ID='firebase' +FIREBASE_STORAGE_BUCKET='firebase' +FIREBASE_MESSAGING_SENDER_ID='firebase' +FIREBASE_APP_ID='firebase' From 4a9b59e836ba0d234598b0431f5993bec4250d0d Mon Sep 17 00:00:00 2001 From: Danylo Bilokha Date: Sat, 18 Apr 2020 20:25:17 +0300 Subject: [PATCH 3/9] improve messageHandlerRegistry.ts --- .github/workflows/integrate.yml | 3 +- .../telegram/botResponse/subscribeResponse.ts | 25 ++-- .../botResponse/unsubscribeResponse.ts | 36 +----- server/src/bots/telegram/index.ts | 110 ++++++++---------- server/src/bots/telegram/models/index.ts | 2 +- server/src/bots/telegram/services/keyboard.ts | 12 +- .../services/messageHandlerRegistry.ts | 74 +++++++----- server/src/models/constants.ts | 13 +-- 8 files changed, 117 insertions(+), 158 deletions(-) diff --git a/.github/workflows/integrate.yml b/.github/workflows/integrate.yml index 4136f25..e5ac81c 100644 --- a/.github/workflows/integrate.yml +++ b/.github/workflows/integrate.yml @@ -14,5 +14,6 @@ jobs: with: node-version: 12 - run: npm ci - - run: npm test + - run: npm run tslint + - run: npm run test - run: npm run build diff --git a/server/src/bots/telegram/botResponse/subscribeResponse.ts b/server/src/bots/telegram/botResponse/subscribeResponse.ts index 0e079cb..de80e19 100644 --- a/server/src/bots/telegram/botResponse/subscribeResponse.ts +++ b/server/src/bots/telegram/botResponse/subscribeResponse.ts @@ -5,13 +5,7 @@ import { subscriptionManagerResponseMessage, subscriptionResultMessage, } from '../../../messages/feature/subscribeMessages'; -import { - isCommandOnly, - isMatchingDashboardItem, - isMessageIsCommand, - isMessageStartsWithCommand, -} from '../../../utils/incomingMessages'; -import { CustomSubscriptions, UserMessages, UserRegExps } from '../../../models/constants'; +import { CustomSubscriptions, UserRegExps } from '../../../models/constants'; import { subscribeOn } from '../../../services/domain/subscriptions'; import { catchAsyncError } from '../../../utils/catchError'; import { getFullMenuKeyboard, getSubscriptionMessageInlineKeyboard } from '../services/keyboard'; @@ -20,6 +14,7 @@ import { getUserMessageFromIKorText } from '../utils/getUserMessageFromIKorText' import { UserSubscription } from '../../../models/subscription.models'; import { removeCommandFromMessageIfExist } from '../../../utils/removeCommandFromMessageIfExist'; import * as TelegramBot from 'node-telegram-bot-api'; +import { CallBackQueryHandlerWithCommandArgument } from '../models'; // TODO: Take a look in all handlers and remove unneeded parameters where they are not used export const subscriptionManagerResponse = async ( @@ -51,17 +46,13 @@ export const showExistingSubscriptionsResponse = async ( // If it's called from InlineKeyboard, then @param ikCbData will be available // otherwise @param ikCbData will be null -export const subscribingStrategyResponse = async ( - bot, - message, - chatId, - ikCbData?: string +export const subscribingStrategyResponse: CallBackQueryHandlerWithCommandArgument = async ( + bot: TelegramBot, + message: TelegramBot.Message, + chatId: number, + parameterAfterCommand?: string ): Promise => { - if ( - (isMessageStartsWithCommand(message.text) && isCommandOnly(message.text)) || - isMessageIsCommand(message.text, UserRegExps.Subscribe) || - isMatchingDashboardItem(message.text, UserMessages.SubscriptionManager) - ) { + if (!parameterAfterCommand) { return showExistingSubscriptionsResponse(bot, message, chatId); } diff --git a/server/src/bots/telegram/botResponse/unsubscribeResponse.ts b/server/src/bots/telegram/botResponse/unsubscribeResponse.ts index 54432c3..93a670e 100644 --- a/server/src/bots/telegram/botResponse/unsubscribeResponse.ts +++ b/server/src/bots/telegram/botResponse/unsubscribeResponse.ts @@ -5,18 +5,9 @@ import { unsubscribeResultMessage, } from '../../../messages/feature/unsubscribeMessages'; import { getFullMenuKeyboard, getUnsubscribeMessageInlineKeyboard } from '../services/keyboard'; -import { - isCommandOnly, - isMatchingDashboardItem, - isMessageIsCommand, - isMessageStartsWithCommand, -} from '../../../utils/incomingMessages'; -import { CustomSubscriptions, UserMessages, UserRegExps } from '../../../models/constants'; import { catchAsyncError } from '../../../utils/catchError'; import { unsubscribeMeFrom } from '../../../services/domain/subscriptions'; -import { getUserMessageFromIKorText } from '../utils/getUserMessageFromIKorText'; import { noSubscriptionsResponseMessage } from '../../../messages/feature/subscribeMessages'; -import { removeCommandFromMessageIfExist } from '../../../utils/removeCommandFromMessageIfExist'; import { getTelegramActiveUserSubscriptions } from '../services/storage'; import * as TelegramBot from 'node-telegram-bot-api'; @@ -39,40 +30,21 @@ export const buildUnsubscribeInlineResponse = async ( // If it's called from InlineKeyboard, then @param ikCbData will be available // otherwise @param ikCbData will be null -export const unsubscribeStrategyResponse = async ( +export const unsubscribeStrategy = async ( bot, message, chatId, - ikCbData?: string + parameterAfterCommand?: string ): Promise => { // If it's called from InlineKeyboard, then @param ikCbData will be available // otherwise @param ikCbData will be null - if (ikCbData && isMatchingDashboardItem(ikCbData, UserMessages.Unsubscribe)) { - return buildUnsubscribeInlineResponse(bot, message, chatId); - } - - if ( - (isMessageStartsWithCommand(message.text) && isCommandOnly(message.text)) || - isMessageIsCommand(message.text, UserRegExps.Unsubscribe) || - isMatchingDashboardItem(message.text, UserMessages.Unsubscribe) - ) { + if (!parameterAfterCommand) { return buildUnsubscribeInlineResponse(bot, message, chatId); } const [err, result] = await catchAsyncError( - unsubscribeMeFrom( - message.chat, - removeCommandFromMessageIfExist( - getUserMessageFromIKorText( - ikCbData ?? message, - CustomSubscriptions.UnsubscribeMeFrom, - '' - ), - UserRegExps.Unsubscribe - ) - ) + unsubscribeMeFrom(message.chat, parameterAfterCommand) ); - if (err) { return bot.sendMessage(chatId, unSubscribeError(err.message)); } diff --git a/server/src/bots/telegram/index.ts b/server/src/bots/telegram/index.ts index e19c754..7e093a5 100644 --- a/server/src/bots/telegram/index.ts +++ b/server/src/bots/telegram/index.ts @@ -22,9 +22,9 @@ import { subscriptionManagerResponse, } from './botResponse/subscribeResponse'; import { SubscriptionType } from '../../models/subscription.models'; -import { MessageHandlerRegistry, withCommandArgument } from './services/messageHandlerRegistry'; +import { MessageHandlerRegistry } from './services/messageHandlerRegistry'; import { subscriptionNotifierHandler } from './services/subscriptionNotifierManager'; -import { unsubscribeStrategyResponse } from './botResponse/unsubscribeResponse'; +import { unsubscribeStrategy } from './botResponse/unsubscribeResponse'; import { showTrendsByCountry } from './botResponse/trendResponse'; import { CountrySituationInfo } from '../../models/covid19.models'; import { catchAsyncError } from '../../utils/catchError'; @@ -34,8 +34,6 @@ import { getErrorMessage } from '../../utils/getErrorMessages'; function runTelegramBot(app: Express, appUrl: string) { // Create a bot that uses 'polling' to fetch new updates const bot = new TelegramBot(Config.TELEGRAM_TOKEN, { polling: true }); - const messageHandlerRegistry = new MessageHandlerRegistry(bot); - // This informs the Telegram servers of the new webhook bot.setWebHook(`${appUrl}/bot${Config.TELEGRAM_TOKEN}`); @@ -45,77 +43,62 @@ function runTelegramBot(app: Express, appUrl: string) { res.sendStatus(200); }); - messageHandlerRegistry.addSingleParameterCommands([ - UserRegExps.CountryData, - UserRegExps.Trends, - ]); + const messageHandlerRegistry = new MessageHandlerRegistry(bot); messageHandlerRegistry - .registerMessageHandler(UserRegExps.Start, startResponse) - // Feature: Countries / Country - .registerMessageHandler(UserMessages.CountriesData, countriesResponse) - .registerMessageHandler(UserRegExps.CountriesData, countriesResponse) - .registerMessageHandler(UserMessages.AvailableCountries, showAvailableCountriesResponse) - .registerMessageHandler(UserRegExps.AvailableCountries, showAvailableCountriesResponse) - .registerMessageHandler(UserRegExps.CountryData, showCountryByNameStrategyResponse) - // Feature: Advices - .registerMessageHandler(UserMessages.GetAdvicesHowToBehave, showAdvicesHowToBehaveResponse) - .registerMessageHandler(UserRegExps.Advice, showAdvicesHowToBehaveResponse) - // Feature: Help - .registerMessageHandler(UserMessages.Help, showHelpInfoResponse) - .registerMessageHandler(UserRegExps.Help, showHelpInfoResponse) - // Feature: Assistant - .registerMessageHandler(UserMessages.Assistant, assistantStrategyResponse) - .registerMessageHandler(UserRegExps.Assistant, assistantStrategyResponse) - // Feature: Subscriptions - .registerMessageHandler(UserMessages.SubscriptionManager, subscriptionManagerResponse) - .registerMessageHandler(UserMessages.Existing, showExistingSubscriptionsResponse) - .registerMessageHandler(UserRegExps.Subscribe, subscribingStrategyResponse) - .registerMessageHandler(UserRegExps.Unsubscribe, unsubscribeStrategyResponse) + .registerMessageHandler([UserRegExps.Start], startResponse) + // Message handler for feature Countries / Country .registerMessageHandler( - UserRegExps.Trends, - withCommandArgument(messageHandlerRegistry, showTrendsByCountry) - ); - messageHandlerRegistry.registerCallBackQueryHandler( - CustomSubscriptions.SubscribeMeOn, - subscribingStrategyResponse - ); - messageHandlerRegistry.registerCallBackQueryHandler( - CustomSubscriptions.UnsubscribeMeFrom, - unsubscribeStrategyResponse - ); - messageHandlerRegistry.registerCallBackQueryHandler( - UserMessages.Existing, - showExistingSubscriptionsResponse - ); - messageHandlerRegistry.registerCallBackQueryHandler( - UserMessages.Unsubscribe, - unsubscribeStrategyResponse - ); - messageHandlerRegistry.registerCallBackQueryHandler(UserMessages.Help, showHelpInfoResponse); - messageHandlerRegistry.registerCallBackQueryHandler( - UserRegExps.Trends, - withCommandArgument(messageHandlerRegistry, showTrendsByCountry) - ); + [UserRegExps.CountriesData, UserMessages.CountriesData], + countriesResponse + ) + .registerMessageHandler( + [UserRegExps.AvailableCountries, UserMessages.AvailableCountries], + showAvailableCountriesResponse + ) + .registerMessageHandler([UserRegExps.CountryData], showCountryByNameStrategyResponse) + // Message handler for feature Advices + .registerMessageHandler( + [UserRegExps.Advice, UserMessages.GetAdviceHowToBehave], + showAdvicesHowToBehaveResponse + ) + // Message handler for feature Help + .registerMessageHandler([UserRegExps.Help, UserMessages.Help], showHelpInfoResponse) + // Message handler for feature Assistant + .registerMessageHandler( + [UserRegExps.Assistant, UserMessages.Assistant], + assistantStrategyResponse + ) + // Message handler for feature Subscriptions + .registerMessageHandler([UserMessages.SubscriptionManager], subscriptionManagerResponse) + .registerMessageHandler([UserMessages.Existing], showExistingSubscriptionsResponse) + .registerMessageHandler( + [UserRegExps.Subscribe, CustomSubscriptions.SubscribeMeOn], + subscribingStrategyResponse + ) + .registerMessageHandler( + [ + CustomSubscriptions.UnsubscribeMeFrom, + UserRegExps.Unsubscribe, + UserMessages.Unsubscribe, + ], + unsubscribeStrategy + ) + .registerMessageHandler([UserRegExps.Trends], showTrendsByCountry); - // Feature: Countries / Country + // Message handler for feature Countries / Country for (const continent of Object.keys(Continents)) { - messageHandlerRegistry.registerCallBackQueryHandler( - continent, - countriesByContinent(continent) - ); + messageHandlerRegistry.registerMessageHandler([continent], countriesByContinent(continent)); } - - // Feature: Countries / Country getAvailableCountries().then((countries: Array) => { const single = countries .map((c) => flag(c.name)?.trim() ?? undefined) .filter((v) => !!v) // TODO: Find flag that we lack for [https://github.com/danbilokha/covid19liveupdates/issues/61] .join('//'); - messageHandlerRegistry.registerMessageHandler(`[~${single}~]`, showCountryByFlag); + messageHandlerRegistry.registerMessageHandler([`[~${single}~]`], showCountryByFlag); }); - // Feature: Subscriptions + // Sending subscriptions cachedCovid19CountriesData.subscribe( async (countriesData: [number, Array<[Country, Array]>]) => { const [err, result] = await catchAsyncError( @@ -131,8 +114,7 @@ function runTelegramBot(app: Express, appUrl: string) { [SubscriptionType.Country] ); - bot.on('message', (message, ...args) => { - logger.log('info', message); + bot.on('message', (message) => { messageHandlerRegistry.runCommandHandler(message); }); diff --git a/server/src/bots/telegram/models/index.ts b/server/src/bots/telegram/models/index.ts index 3bae04a..14a2008 100644 --- a/server/src/bots/telegram/models/index.ts +++ b/server/src/bots/telegram/models/index.ts @@ -10,7 +10,7 @@ export type CallBackQueryHandlerWithCommandArgument = ( bot: TelegramBot, message: TelegramBot.Message, chatId: number, - commandArgument: string + parameterAfterCommand?: string ) => unknown; export const TELEGRAM_PREFIX: string = 'telegram'; diff --git a/server/src/bots/telegram/services/keyboard.ts b/server/src/bots/telegram/services/keyboard.ts index 49aaba6..324714b 100644 --- a/server/src/bots/telegram/services/keyboard.ts +++ b/server/src/bots/telegram/services/keyboard.ts @@ -18,7 +18,7 @@ export const getFullMenuKeyboard = (chatId): TelegramBot.SendMessageOptions => { } rk.addRow(UserMessages.CountriesData, UserMessages.AvailableCountries) - .addRow(UserMessages.Assistant, UserMessages.GetAdvicesHowToBehave) + .addRow(UserMessages.Assistant, UserMessages.GetAdviceHowToBehave) .addRow(UserMessages.SubscriptionManager, UserMessages.Help); return rk.open({ resize_keyboard: true }); @@ -32,7 +32,7 @@ export const getAfterCountryResponseInlineKeyboard = ( text: `${CustomSubscriptions.SubscribeMeOn} ${country}`, callback_data: `${CustomSubscriptions.SubscribeMeOn} ${country}`, }).addRow({ - text: `Show weekly chart`, + text: 'Show weekly chart', callback_data: `${UserRegExps.Trends} ${country}`, }); @@ -44,7 +44,7 @@ export const getSubscriptionMessageInlineKeyboard = (): TelegramBot.SendMessageO ik.addRow( { text: UserMessages.Existing, - callback_data: UserMessages.Existing, + callback_data: `${UserMessages.Existing}`, }, { text: UserMessages.Unsubscribe, @@ -97,7 +97,11 @@ export const getContinentsInlineKeyboard = (): TelegramBot.SendMessageOptions => export const getHelpProposalInlineKeyboard = (): TelegramBot.SendMessageOptions => { const ik = new InlineKeyboard(); - ik.addRow({ text: UserMessages.Help, callback_data: UserMessages.Help }); + + ik.addRow({ + text: UserMessages.Help, + callback_data: UserMessages.Help, + }); return ik.build(); }; diff --git a/server/src/bots/telegram/services/messageHandlerRegistry.ts b/server/src/bots/telegram/services/messageHandlerRegistry.ts index 4e074b1..78d44a5 100644 --- a/server/src/bots/telegram/services/messageHandlerRegistry.ts +++ b/server/src/bots/telegram/services/messageHandlerRegistry.ts @@ -16,39 +16,28 @@ import { Answer } from '../../../models/knowledgebase/answer.models'; import { fetchAnswer } from '../../../services/api/api-knowledgebase'; import { assistantResponse } from '../botResponse/assistantResponse'; import { noResponse } from '../botResponse/noResponse'; -import { UserRegExps } from '../../../models/constants'; import { LogglyTypes } from '../../../models/loggly.models'; import * as TelegramBot from 'node-telegram-bot-api'; +import { getInfoMessage } from '../../../utils/getErrorMessages'; export class MessageHandlerRegistry { - _cbQueryHandlers: { [regexp: string]: CallBackQueryHandlerWithCommandArgument } = {}; _messageHandlers: { [regexp: string]: CallBackQueryHandlerWithCommandArgument } = {}; - - _singleParameterCommandRegex: RegExp; + _singleParameterAfterCommands: Array = []; constructor(private readonly bot: TelegramBot) { this.registerCallBackQuery(); } - public addSingleParameterCommands(commands: Array) { - this._singleParameterCommandRegex = new RegExp( - `(?${commands.join('|\\')})\\s(?.*)` - ); - } - public registerMessageHandler( - regexp: string, + regexps: Array, callback: CallBackQueryHandlerWithCommandArgument ): MessageHandlerRegistry { - this._messageHandlers[regexp] = callback; - return this; - } + this._singleParameterAfterCommands = [...this._singleParameterAfterCommands, ...regexps]; - public registerCallBackQueryHandler( - regexp: string, - callback: CallBackQueryHandlerWithCommandArgument - ): MessageHandlerRegistry { - this._cbQueryHandlers[regexp] = callback; + regexps.forEach( + (regexp: string) => + (this._messageHandlers[regexp] = withSingleParameterAfterCommand(this, callback)) + ); return this; } @@ -63,8 +52,10 @@ export class MessageHandlerRegistry { message: TelegramBot.Message, ikCbData?: string ): Promise { + logger.log('info', message); + const runCheckupAgainstStr = ikCbData ? ikCbData : message.text; - const cbHandlers = ikCbData ? this._cbQueryHandlers : this._messageHandlers; + const cbHandlers = this._messageHandlers; const suitableKeys: Array = Object.keys(cbHandlers).filter( (cbHandlerRegExpKey: string) => @@ -84,6 +75,9 @@ export class MessageHandlerRegistry { }); } + // This statement will invoke wrapper (withSingleParameterAfterCommand) + // around original handler which is defined by default in + // this.registerMessageHandler return cbHandlers[suitableKeys[0]].call( this, this.bot, @@ -134,7 +128,10 @@ export class MessageHandlerRegistry { } } -export const withCommandArgument = ( +// This function is wrapper around the original User's query handler +// It adds an additional parameter (if such exist) to original handler, +// which will be an parameter following after command +export const withSingleParameterAfterCommand = ( context: MessageHandlerRegistry, handlerFn: CallBackQueryHandlerWithCommandArgument ): CallBackQueryHandlerWithCommandArgument => { @@ -145,13 +142,14 @@ export const withCommandArgument = ( ikCbData?: string ): unknown => { try { - const commandArgument: string = getArgFromMessage.call( + const userEnteredArgumentAfterCommand: string = getParameterAfterCommandFromMessage.call( context, ikCbData ?? message.text ); - return handlerFn.call(context, bot, message, chatId, commandArgument); + + return handlerFn.call(context, bot, message, chatId, userEnteredArgumentAfterCommand); } catch (err) { - logger.log('warn', { + logger.log('error', { ...message, type: LogglyTypes.CommandError, message: err.message, @@ -160,19 +158,31 @@ export const withCommandArgument = ( }; }; -function getArgFromMessage(messageText: string): string { - if (!messageText) { - throw new Error('message could not be empty'); - } - - const execResult = this._singleParameterCommandRegex.exec(messageText); +// Check out how it works here +// https://codepen.io/belokha/pen/xxwOdWg?editors=0012 +function getParameterAfterCommandFromMessage( + userFullInput: string | undefined +): string | undefined { + const makeMagicOverUserFullInput: string = this._singleParameterAfterCommands.find( + (parameter) => parameter === userFullInput + ) + ? userFullInput + ' ' // Problem is that userInput is the same as RegExp it returns null, but when it has + : // at least one whitespace it is not null + // https://codepen.io/belokha/pen/xxwOdWg?editors=0012, Example 5. + userFullInput; + + const execResult = new RegExp( + `(?${this._singleParameterAfterCommands.join('|\\')})\\s(?.*)` + ).exec(makeMagicOverUserFullInput); if (!execResult) { - throw new Error('Please input any command from available'); + logger.log('info', getInfoMessage('Entered unsupported command')); + return undefined; } /* tslint:disable:no-string-literal */ if (execResult.groups['command'] && !execResult.groups['firstargument']) { - throw new Error(`please provide argument for \\${execResult.groups['command']}`); + logger.log('info', getInfoMessage(`No parameter for ${execResult.groups['command']}`)); + return undefined; } return execResult.groups['firstargument']; diff --git a/server/src/models/constants.ts b/server/src/models/constants.ts index a95aafb..51436e6 100644 --- a/server/src/models/constants.ts +++ b/server/src/models/constants.ts @@ -10,7 +10,7 @@ export const CONSOLE_LOG_DELIMITER: string = '\n\n==============> '; export enum CustomSubscriptions { SubscribeMeOn = `Subscribe on`, - UnsubscribeMeFrom = `Unsubscribe me from` + UnsubscribeMeFrom = `Unsubscribe me from`, } export enum UserRegExps { @@ -23,18 +23,18 @@ export enum UserRegExps { Advice = '/advice', Subscribe = '/subscribe', Unsubscribe = '/unsubscribe', - Help = '/help' + Help = '/help', } export enum UserMessages { Assistant = 'Assistant 👦', CountriesData = 'Countries data 🌍', AvailableCountries = 'Countries we track', - GetAdvicesHowToBehave = 'Advice how not to 😷', + GetAdviceHowToBehave = 'Advice how not to 😷', SubscriptionManager = 'Subscriptions 💌', Existing = 'Existing', Unsubscribe = 'Unsubscribe', - Help = 'ℹ What can you do?' + Help = 'ℹ What can you do?', } export enum Continents { @@ -43,12 +43,11 @@ export enum Continents { Africa = 'Africa', Americas = 'Americas', Oceania = 'Oceania', - Other = 'Other' + Other = 'Other', } export enum Status { Confirmed = 'confirmed', Deaths = 'deaths', - Recovered = 'recovered' + Recovered = 'recovered', } - From 96be4b2e9d662523c1af5781170dbb1706b499c4 Mon Sep 17 00:00:00 2001 From: Danylo Bilokha Date: Sat, 18 Apr 2020 20:54:01 +0300 Subject: [PATCH 4/9] working on separating the logic --- .prettierrc.json | 2 +- .../telegram/botResponse/adviceResponse.ts | 4 +- .../telegram/botResponse/assistantResponse.ts | 67 +++++-------- .../telegram/botResponse/availableResponse.ts | 4 +- .../telegram/botResponse/countriesResponse.ts | 96 ++++++++++++------- .../telegram/botResponse/countryResponse.ts | 24 +++-- .../bots/telegram/botResponse/helpResponse.ts | 20 ++-- .../telegram/botResponse/startResponse.ts | 20 ++-- .../telegram/botResponse/trendResponse.ts | 56 +++++++---- .../botResponse/unsubscribeResponse.ts | 34 ++++--- server/src/bots/telegram/index.ts | 73 ++++++++++---- server/src/bots/telegram/models/index.ts | 6 -- .../src/messages/feature/countriesMessages.ts | 30 ++++-- .../services/domain/countriesByContinent.ts | 5 +- 14 files changed, 268 insertions(+), 173 deletions(-) diff --git a/.prettierrc.json b/.prettierrc.json index 5280a73..bab614e 100644 --- a/.prettierrc.json +++ b/.prettierrc.json @@ -1,5 +1,5 @@ { - "printWidth": 100, + "printWidth": 80, "tabWidth": 4, "trailingComma": "es5", "singleQuote": true, diff --git a/server/src/bots/telegram/botResponse/adviceResponse.ts b/server/src/bots/telegram/botResponse/adviceResponse.ts index ddf4b80..820a2f7 100644 --- a/server/src/bots/telegram/botResponse/adviceResponse.ts +++ b/server/src/bots/telegram/botResponse/adviceResponse.ts @@ -5,10 +5,10 @@ import { socialDistancing, suggestedBehaviors, } from '../../../messages/feature/adviceMessages'; -import { CallBackQueryHandler } from '../models'; +import { CallBackQueryHandlerWithCommandArgument } from '../models'; import * as TelegramBot from 'node-telegram-bot-api'; -export const showAdvicesHowToBehaveResponse: CallBackQueryHandler = async ( +export const showAdvicesHowToBehaveResponse: CallBackQueryHandlerWithCommandArgument = async ( bot, message, chatId diff --git a/server/src/bots/telegram/botResponse/assistantResponse.ts b/server/src/bots/telegram/botResponse/assistantResponse.ts index f87493d..8a957a8 100644 --- a/server/src/bots/telegram/botResponse/assistantResponse.ts +++ b/server/src/bots/telegram/botResponse/assistantResponse.ts @@ -8,59 +8,24 @@ import { getAssistantFeaturesMessage, noAnswersOnQuestionMessage, } from '../../../messages/feature/assistantMessages'; -import { textAfterUserCommand } from '../../../utils/textAfterCommand'; -import { - isCommandOnly, - isMatchingDashboardItem, - isMessageStartsWithCommand, -} from '../../../utils/incomingMessages'; import { KnowledgebaseMeta } from '../../../models/knowledgebase/meta.models'; -import { UserMessages } from '../../../models/constants'; -import { CallBackQueryHandler } from '../models'; +import { CallBackQueryHandlerWithCommandArgument } from '../models'; import * as TelegramBot from 'node-telegram-bot-api'; -export const assistantStrategyResponse: CallBackQueryHandler = async ( - bot: TelegramBot, - message: TelegramBot.Message, - chatId?: number -): Promise => { - if ( - (isMessageStartsWithCommand(message.text) && isCommandOnly(message.text)) || - isMatchingDashboardItem(message.text, UserMessages.Assistant) - ) { - return showAssistantFeatures(bot, message, chatId) as Promise; - } - - return answerOnQuestion(bot, message, chatId) as Promise; -}; - -export const showAssistantFeatures: CallBackQueryHandler = async ( +export const showAssistantFeatures: CallBackQueryHandlerWithCommandArgument = async ( bot: TelegramBot, message: TelegramBot.Message, chatId: number ): Promise => { const knowledgebaseMeta: KnowledgebaseMeta = await fetchKnowledgeMetainformation(); - return bot.sendMessage(chatId, getAssistantFeaturesMessage(knowledgebaseMeta)); + return bot.sendMessage( + chatId, + getAssistantFeaturesMessage(knowledgebaseMeta) + ); }; -export const answerOnQuestion: CallBackQueryHandler = async ( +export const assistantNoAnswerResponse = async ( bot: TelegramBot, - message: TelegramBot.Message, - chatId: number -): Promise => { - const question: string = textAfterUserCommand(message.text); - const answers: Array = await fetchAnswer(question); - - if (!answers.length) { - return assistantNoAnswerResponse(bot, message, chatId) as Promise; - } - - return assistantResponse(bot, answers, chatId); -}; - -export const assistantNoAnswerResponse: CallBackQueryHandler = async ( - bot: TelegramBot, - message: TelegramBot.Message, chatId: number ): Promise => { return bot.sendMessage(chatId, noAnswersOnQuestionMessage()); @@ -73,3 +38,21 @@ export const assistantResponse = async ( ) => { return bot.sendMessage(chatId, getAnswersOnQuestionMessage(answers)); }; + +export const assistantStrategyResponse: CallBackQueryHandlerWithCommandArgument = async ( + bot: TelegramBot, + message: TelegramBot.Message, + chatId: number, + parameterAfterCommand?: string +): Promise => { + if (!parameterAfterCommand) { + return showAssistantFeatures(bot, message, chatId); + } + + const answers: Array = await fetchAnswer(parameterAfterCommand); + if (!answers.length) { + return assistantNoAnswerResponse(bot, chatId); + } + + return assistantResponse(bot, answers, chatId); +}; diff --git a/server/src/bots/telegram/botResponse/availableResponse.ts b/server/src/bots/telegram/botResponse/availableResponse.ts index 7b342c4..9137493 100644 --- a/server/src/bots/telegram/botResponse/availableResponse.ts +++ b/server/src/bots/telegram/botResponse/availableResponse.ts @@ -1,10 +1,10 @@ import { getAvailableCountries } from '../../../services/domain/covid19'; import { Country } from '../../../models/country.models'; import { getShowCountriesMessage } from '../../../messages/feature/availableMessages'; -import { CallBackQueryHandler } from '../models'; +import { CallBackQueryHandlerWithCommandArgument } from '../models'; import * as TelegramBot from 'node-telegram-bot-api'; -export const showAvailableCountriesResponse: CallBackQueryHandler = async ( +export const showAvailableCountriesResponse: CallBackQueryHandlerWithCommandArgument = async ( bot: TelegramBot, message: TelegramBot.Message, chatId: number diff --git a/server/src/bots/telegram/botResponse/countriesResponse.ts b/server/src/bots/telegram/botResponse/countriesResponse.ts index 05e90ef..8444498 100644 --- a/server/src/bots/telegram/botResponse/countriesResponse.ts +++ b/server/src/bots/telegram/botResponse/countriesResponse.ts @@ -10,36 +10,49 @@ import { } from '../../../messages/feature/countryMessages'; import { Country } from '../../../models/country.models'; import { - getCountriesSumupMessage, + getCountriesWorldMessage, getCountriesTableHTML, } from '../../../messages/feature/countriesMessages'; import { getContinentsInlineKeyboard } from '../services/keyboard'; -import { CallBackQueryHandler } from '../models'; +import { CallBackQueryHandlerWithCommandArgument } from '../models'; // TODO: Move this logic to domain and leave here only Telegram bot specific message response // Sending response itself -export const countriesByContinent = (continent) => async (bot, message, chatId) => { +export const countriesByContinentResponse = (continent) => async ( + bot, + message, + chatId +) => { const countriesSituation: Array<[ Country, Array ]> = await getCountriesSituation(); const continentCountries: ContinentCountriesSituation = {}; - countriesSituation.forEach(([country, situations]: [Country, Array]) => { - const { confirmed, recovered, deaths } = situations[situations.length - 1]; + countriesSituation.forEach( + ([country, situations]: [Country, Array]) => { + const { confirmed, recovered, deaths } = situations[ + situations.length - 1 + ]; - const countrySituationResult: CountrySituation = { - lastUpdateDate: situations[situations.length - 1].date, - country, - confirmed, - recovered, - deaths, - }; - const prevCountriesResult: Array = continentCountries[country.continent] - ? continentCountries[country.continent] - : []; - continentCountries[country.continent] = [...prevCountriesResult, countrySituationResult]; - }); + const countrySituationResult: CountrySituation = { + lastUpdateDate: situations[situations.length - 1].date, + country, + confirmed, + recovered, + deaths, + }; + const prevCountriesResult: Array = continentCountries[ + country.continent + ] + ? continentCountries[country.continent] + : []; + continentCountries[country.continent] = [ + ...prevCountriesResult, + countrySituationResult, + ]; + } + ); const portionMessage = [getTableHeader()]; let continentTotalConfirmed: number = 0; @@ -87,7 +100,11 @@ export const countriesByContinent = (continent) => async (bot, message, chatId) // TODO: Move this logic to domain and leave here only Telegram bot specific message response // Sending response itself -export const countriesResponse: CallBackQueryHandler = async (bot, message, chatId) => { +export const countriesResponse: CallBackQueryHandlerWithCommandArgument = async ( + bot, + message, + chatId +) => { const countriesSituation: Array<[ Country, Array @@ -97,30 +114,37 @@ export const countriesResponse: CallBackQueryHandler = async (bot, message, chat let worldTotalRecovered = 0; let worldTotalDeaths = 0; - countriesSituation.forEach(([country, situations]: [Country, Array]) => { - const { confirmed, recovered, deaths } = situations[situations.length - 1]; + countriesSituation.forEach( + ([country, situations]: [Country, Array]) => { + const { confirmed, recovered, deaths } = situations[ + situations.length - 1 + ]; - worldTotalConfirmed += confirmed; - worldTotalRecovered += recovered; - worldTotalDeaths += deaths; + worldTotalConfirmed += confirmed; + worldTotalRecovered += recovered; + worldTotalDeaths += deaths; - const countrySituationResult: CountrySituation = { - lastUpdateDate: situations[situations.length - 1].date, - country, - confirmed, - recovered, - deaths, - }; - const prevCountriesResult = continentCountries[country.continent] - ? continentCountries[country.continent] - : []; - continentCountries[country.continent] = [...prevCountriesResult, countrySituationResult]; - }); + const countrySituationResult: CountrySituation = { + lastUpdateDate: situations[situations.length - 1].date, + country, + confirmed, + recovered, + deaths, + }; + const prevCountriesResult = continentCountries[country.continent] + ? continentCountries[country.continent] + : []; + continentCountries[country.continent] = [ + ...prevCountriesResult, + countrySituationResult, + ]; + } + ); // Send overall world info, return bot.sendMessage( chatId, - getCountriesSumupMessage( + getCountriesWorldMessage( worldTotalConfirmed, worldTotalRecovered, worldTotalDeaths, diff --git a/server/src/bots/telegram/botResponse/countryResponse.ts b/server/src/bots/telegram/botResponse/countryResponse.ts index ec9247e..fa0546c 100644 --- a/server/src/bots/telegram/botResponse/countryResponse.ts +++ b/server/src/bots/telegram/botResponse/countryResponse.ts @@ -1,4 +1,7 @@ -import { ApiCovid19Situation, CountrySituationInfo } from '../../../models/covid19.models'; +import { + ApiCovid19Situation, + CountrySituationInfo, +} from '../../../models/covid19.models'; import { adaptCountryToSystemRepresentation, getCountriesSituation, @@ -14,10 +17,10 @@ import { getAfterCountryResponseInlineKeyboard } from '../services/keyboard'; import { textAfterUserCommand } from '../../../utils/textAfterCommand'; import { isMessageIsCommand } from '../../../utils/incomingMessages'; import { UserRegExps } from '../../../models/constants'; -import { CallBackQueryHandler } from '../models'; +import { CallBackQueryHandlerWithCommandArgument } from '../models'; import * as TelegramBot from 'node-telegram-bot-api'; -export const showCountryByNameStrategyResponse: CallBackQueryHandler = async ( +export const showCountryByNameStrategyResponse: CallBackQueryHandlerWithCommandArgument = async ( bot, message, chatId @@ -26,16 +29,22 @@ export const showCountryByNameStrategyResponse: CallBackQueryHandler = async ( ? bot.sendMessage(chatId, getMessageForUserInputWithoutCountryName()) : showCountryResponse( bot, - adaptCountryToSystemRepresentation(textAfterUserCommand(message.text)), + adaptCountryToSystemRepresentation( + textAfterUserCommand(message.text) + ), chatId ); -export const showCountryByFlag: CallBackQueryHandler = async ( +export const showCountryByFlag: CallBackQueryHandlerWithCommandArgument = async ( bot, message, chatId ): Promise => - showCountryResponse(bot, adaptCountryToSystemRepresentation(name(message.text)), chatId); + showCountryResponse( + bot, + adaptCountryToSystemRepresentation(name(message.text)), + chatId + ); // TODO: Split and move messages to /messages/feature and /domain directories export const showCountryResponse = async ( @@ -62,7 +71,8 @@ export const showCountryResponse = async ( Country, Array ] = allCountriesSituations.find( - ([receivedCountry, situations]) => receivedCountry.name === requestedCountry + ([receivedCountry, situations]) => + receivedCountry.name === requestedCountry ); if ( !foundCountrySituations || diff --git a/server/src/bots/telegram/botResponse/helpResponse.ts b/server/src/bots/telegram/botResponse/helpResponse.ts index 34aef6e..dafea59 100644 --- a/server/src/bots/telegram/botResponse/helpResponse.ts +++ b/server/src/bots/telegram/botResponse/helpResponse.ts @@ -1,13 +1,11 @@ -import {getHelpMessage} from '../../../messages/feature/helpMessages'; -import {CallBackQueryHandler} from '../models'; +import { getHelpMessage } from '../../../messages/feature/helpMessages'; +import { CallBackQueryHandlerWithCommandArgument } from '../models'; +import * as TelegramBot from 'node-telegram-bot-api'; -export const showHelpInfoResponse: CallBackQueryHandler = async ( - bot, - message, - chatId -) => { - return bot.sendMessage( - chatId, - getHelpMessage() - ); +export const helpInfoResponse: CallBackQueryHandlerWithCommandArgument = async ( + bot: TelegramBot, + message: TelegramBot.Message, + chatId: number +): Promise => { + return bot.sendMessage(chatId, getHelpMessage()); }; diff --git a/server/src/bots/telegram/botResponse/startResponse.ts b/server/src/bots/telegram/botResponse/startResponse.ts index 1016495..29bf921 100644 --- a/server/src/bots/telegram/botResponse/startResponse.ts +++ b/server/src/bots/telegram/botResponse/startResponse.ts @@ -1,10 +1,18 @@ import { getFullMenuKeyboard } from '../services/keyboard'; import { greetUser } from '../../../messages/userMessage'; -import { showHelpInfoResponse } from './helpResponse'; -import { CallBackQueryHandler } from '../models'; +import { CallBackQueryHandlerWithCommandArgument } from '../models'; +import * as TelegramBot from 'node-telegram-bot-api'; +import { helpInfoResponse } from './helpResponse'; -export const startResponse: CallBackQueryHandler = async (bot, message, chatId) => { - await bot.sendMessage(chatId, `${greetUser(message.from)}`, getFullMenuKeyboard(message)); - - await showHelpInfoResponse(bot, message, chatId); +export const startResponse: CallBackQueryHandlerWithCommandArgument = async ( + bot: TelegramBot, + message: TelegramBot.Message, + chatId: number +): Promise => { + await bot.sendMessage( + chatId, + `${greetUser(message.from)}`, + getFullMenuKeyboard(message) + ); + return helpInfoResponse(bot, message, chatId); }; diff --git a/server/src/bots/telegram/botResponse/trendResponse.ts b/server/src/bots/telegram/botResponse/trendResponse.ts index 9d9a4fd..689f715 100644 --- a/server/src/bots/telegram/botResponse/trendResponse.ts +++ b/server/src/bots/telegram/botResponse/trendResponse.ts @@ -1,19 +1,33 @@ -import {getCovidTrends} from '../../../services/api/api-chart' -import {addDays, Now} from '../../../utils/dateUtils' -import {CountrySituationInfo} from '../../../models/covid19.models'; -import {Country} from '../../../models/country.models'; -import {getCountriesSituation} from '../../../services/domain/covid19'; -import {Transform} from '../../../services/domain/chart'; +import { getCovidTrends } from '../../../services/api/api-chart'; +import { addDays, Now } from '../../../utils/dateUtils'; +import { CountrySituationInfo } from '../../../models/covid19.models'; +import { Country } from '../../../models/country.models'; +import { getCountriesSituation } from '../../../services/domain/covid19'; +import { Transform } from '../../../services/domain/chart'; -export const showTrendsByCountry = async (bot, message, chatId, requestedCountry): Promise => { - const allCountries: Array<[Country, Array]> = await getCountriesSituation(); - const foundCountrySituations: [Country, Array] = allCountries - .find(([receivedCountry, situations]) => +export const trendsByCountryResponse = async ( + bot, + message, + chatId, + requestedCountry +): Promise => { + const allCountries: Array<[ + Country, + Array + ]> = await getCountriesSituation(); + const foundCountrySituations: [ + Country, + Array + ] = allCountries.find( + ([receivedCountry, situations]) => receivedCountry.name === requestedCountry - ); - if (!foundCountrySituations || !foundCountrySituations?.length - || !foundCountrySituations[0] - || !foundCountrySituations[1].length) { + ); + if ( + !foundCountrySituations || + !foundCountrySituations?.length || + !foundCountrySituations[0] || + !foundCountrySituations[1].length + ) { return bot.sendMessage( chatId, `Sorry, but I cannot find anything for ${requestedCountry}. I will save your request and will work on it` @@ -22,10 +36,12 @@ export const showTrendsByCountry = async (bot, message, chatId, requestedCountry const [foundCountry, foundSituation] = foundCountrySituations; - const lastWeekSituation = foundSituation.filter((c: CountrySituationInfo) => { - const date = new Date(c.date); - return date < Now && date > addDays(Now, -7); - }); + const lastWeekSituation = foundSituation.filter( + (c: CountrySituationInfo) => { + const date = new Date(c.date); + return date < Now && date > addDays(Now, -7); + } + ); - return bot.sendPhoto(chatId, getCovidTrends(Transform(lastWeekSituation))) -} + return bot.sendPhoto(chatId, getCovidTrends(Transform(lastWeekSituation))); +}; diff --git a/server/src/bots/telegram/botResponse/unsubscribeResponse.ts b/server/src/bots/telegram/botResponse/unsubscribeResponse.ts index 93a670e..c12abc9 100644 --- a/server/src/bots/telegram/botResponse/unsubscribeResponse.ts +++ b/server/src/bots/telegram/botResponse/unsubscribeResponse.ts @@ -4,19 +4,25 @@ import { unSubscribeError, unsubscribeResultMessage, } from '../../../messages/feature/unsubscribeMessages'; -import { getFullMenuKeyboard, getUnsubscribeMessageInlineKeyboard } from '../services/keyboard'; +import { + getFullMenuKeyboard, + getUnsubscribeMessageInlineKeyboard, +} from '../services/keyboard'; import { catchAsyncError } from '../../../utils/catchError'; import { unsubscribeMeFrom } from '../../../services/domain/subscriptions'; import { noSubscriptionsResponseMessage } from '../../../messages/feature/subscribeMessages'; import { getTelegramActiveUserSubscriptions } from '../services/storage'; import * as TelegramBot from 'node-telegram-bot-api'; +import { CallBackQueryHandlerWithCommandArgument } from '../models'; export const buildUnsubscribeInlineResponse = async ( - bot, - message, - chatId + bot: TelegramBot, + message: TelegramBot.Message, + chatId: number ): Promise => { - const userSubscription: UserSubscription = await getTelegramActiveUserSubscriptions(chatId); + const userSubscription: UserSubscription = await getTelegramActiveUserSubscriptions( + chatId + ); if (!userSubscription?.subscriptionsOn?.length) { return bot.sendMessage(chatId, noSubscriptionsResponseMessage()); } @@ -24,16 +30,18 @@ export const buildUnsubscribeInlineResponse = async ( return bot.sendMessage( chatId, getUnsubscribeResponseMessage(), - getUnsubscribeMessageInlineKeyboard(userSubscription.subscriptionsOn.map((v) => v.value)) + getUnsubscribeMessageInlineKeyboard( + userSubscription.subscriptionsOn.map((v) => v.value) + ) ); }; // If it's called from InlineKeyboard, then @param ikCbData will be available // otherwise @param ikCbData will be null -export const unsubscribeStrategy = async ( - bot, - message, - chatId, +export const unsubscribeStrategyResponse: CallBackQueryHandlerWithCommandArgument = async ( + bot: TelegramBot, + message: TelegramBot.Message, + chatId: number, parameterAfterCommand?: string ): Promise => { // If it's called from InlineKeyboard, then @param ikCbData will be available @@ -49,5 +57,9 @@ export const unsubscribeStrategy = async ( return bot.sendMessage(chatId, unSubscribeError(err.message)); } - return bot.sendMessage(chatId, unsubscribeResultMessage(result), getFullMenuKeyboard(chatId)); + return bot.sendMessage( + chatId, + unsubscribeResultMessage(result), + getFullMenuKeyboard(chatId) + ); }; diff --git a/server/src/bots/telegram/index.ts b/server/src/bots/telegram/index.ts index 7e093a5..e1eec10 100644 --- a/server/src/bots/telegram/index.ts +++ b/server/src/bots/telegram/index.ts @@ -1,13 +1,24 @@ -import { countriesByContinent, countriesResponse } from './botResponse/countriesResponse'; +import { + countriesByContinentResponse, + countriesResponse, +} from './botResponse/countriesResponse'; import { showCountryByFlag, showCountryByNameStrategyResponse, } from './botResponse/countryResponse'; -import { Continents, CustomSubscriptions, UserMessages, UserRegExps } from '../../models/constants'; +import { + Continents, + CustomSubscriptions, + UserMessages, + UserRegExps, +} from '../../models/constants'; import { showAdvicesHowToBehaveResponse } from './botResponse/adviceResponse'; -import { showHelpInfoResponse } from './botResponse/helpResponse'; +import { helpInfoResponse } from './botResponse/helpResponse'; import { Express } from 'express'; -import { cachedCovid19CountriesData, getAvailableCountries } from '../../services/domain/covid19'; +import { + cachedCovid19CountriesData, + getAvailableCountries, +} from '../../services/domain/covid19'; import { Country } from '../../models/country.models'; import { flag } from 'country-emoji'; import { assistantStrategyResponse } from './botResponse/assistantResponse'; @@ -24,8 +35,8 @@ import { import { SubscriptionType } from '../../models/subscription.models'; import { MessageHandlerRegistry } from './services/messageHandlerRegistry'; import { subscriptionNotifierHandler } from './services/subscriptionNotifierManager'; -import { unsubscribeStrategy } from './botResponse/unsubscribeResponse'; -import { showTrendsByCountry } from './botResponse/trendResponse'; +import { unsubscribeStrategyResponse } from './botResponse/unsubscribeResponse'; +import { trendsByCountryResponse } from './botResponse/trendResponse'; import { CountrySituationInfo } from '../../models/covid19.models'; import { catchAsyncError } from '../../utils/catchError'; import { LogglyTypes } from '../../models/loggly.models'; @@ -55,22 +66,34 @@ function runTelegramBot(app: Express, appUrl: string) { [UserRegExps.AvailableCountries, UserMessages.AvailableCountries], showAvailableCountriesResponse ) - .registerMessageHandler([UserRegExps.CountryData], showCountryByNameStrategyResponse) + .registerMessageHandler( + [UserRegExps.CountryData], + showCountryByNameStrategyResponse + ) // Message handler for feature Advices .registerMessageHandler( [UserRegExps.Advice, UserMessages.GetAdviceHowToBehave], showAdvicesHowToBehaveResponse ) // Message handler for feature Help - .registerMessageHandler([UserRegExps.Help, UserMessages.Help], showHelpInfoResponse) + .registerMessageHandler( + [UserRegExps.Help, UserMessages.Help], + helpInfoResponse + ) // Message handler for feature Assistant .registerMessageHandler( [UserRegExps.Assistant, UserMessages.Assistant], assistantStrategyResponse ) // Message handler for feature Subscriptions - .registerMessageHandler([UserMessages.SubscriptionManager], subscriptionManagerResponse) - .registerMessageHandler([UserMessages.Existing], showExistingSubscriptionsResponse) + .registerMessageHandler( + [UserMessages.SubscriptionManager], + subscriptionManagerResponse + ) + .registerMessageHandler( + [UserMessages.Existing], + showExistingSubscriptionsResponse + ) .registerMessageHandler( [UserRegExps.Subscribe, CustomSubscriptions.SubscribeMeOn], subscribingStrategyResponse @@ -81,13 +104,16 @@ function runTelegramBot(app: Express, appUrl: string) { UserRegExps.Unsubscribe, UserMessages.Unsubscribe, ], - unsubscribeStrategy + unsubscribeStrategyResponse ) - .registerMessageHandler([UserRegExps.Trends], showTrendsByCountry); + .registerMessageHandler([UserRegExps.Trends], trendsByCountryResponse); // Message handler for feature Countries / Country for (const continent of Object.keys(Continents)) { - messageHandlerRegistry.registerMessageHandler([continent], countriesByContinent(continent)); + messageHandlerRegistry.registerMessageHandler( + [continent], + countriesByContinentResponse(continent) + ); } getAvailableCountries().then((countries: Array) => { const single = countries @@ -95,19 +121,32 @@ function runTelegramBot(app: Express, appUrl: string) { .filter((v) => !!v) // TODO: Find flag that we lack for [https://github.com/danbilokha/covid19liveupdates/issues/61] .join('//'); - messageHandlerRegistry.registerMessageHandler([`[~${single}~]`], showCountryByFlag); + messageHandlerRegistry.registerMessageHandler( + [`[~${single}~]`], + showCountryByFlag + ); }); // Sending subscriptions cachedCovid19CountriesData.subscribe( - async (countriesData: [number, Array<[Country, Array]>]) => { + async ( + countriesData: [ + number, + Array<[Country, Array]> + ] + ) => { const [err, result] = await catchAsyncError( - subscriptionNotifierHandler(messageHandlerRegistry, countriesData) + subscriptionNotifierHandler( + messageHandlerRegistry, + countriesData + ) ); if (err) { logger.log('error', { type: LogglyTypes.SubscriptionNotifierHandlerError, - message: `${getErrorMessage(err)}. subscriptionNotifierHandler failed`, + message: `${getErrorMessage( + err + )}. subscriptionNotifierHandler failed`, }); } }, diff --git a/server/src/bots/telegram/models/index.ts b/server/src/bots/telegram/models/index.ts index 14a2008..f8c8b0f 100644 --- a/server/src/bots/telegram/models/index.ts +++ b/server/src/bots/telegram/models/index.ts @@ -1,11 +1,5 @@ import * as TelegramBot from 'node-telegram-bot-api'; -export type CallBackQueryHandler = ( - bot: TelegramBot, - message: TelegramBot.Message, - chatId: number -) => unknown; - export type CallBackQueryHandlerWithCommandArgument = ( bot: TelegramBot, message: TelegramBot.Message, diff --git a/server/src/messages/feature/countriesMessages.ts b/server/src/messages/feature/countriesMessages.ts index af50f1f..1fb7658 100644 --- a/server/src/messages/feature/countriesMessages.ts +++ b/server/src/messages/feature/countriesMessages.ts @@ -1,18 +1,28 @@ -import {getActiveCases} from '../covid19Messages'; -import {table, tableConfig} from '../../models/table.models'; +import { getActiveCases } from '../covid19Messages'; +import { table, tableConfig } from '../../models/table.models'; -export const getCountriesSumupMessage = ( +export const getCountriesWorldMessage = ( worldConfirmed: number, worldRecovered: number, worldDeaths: number, countries: number, - continents: number, + continents: number ): string => - `In the world 🌍\nConfirmed: ${worldConfirmed}, active: ${getActiveCases(worldConfirmed, worldRecovered, worldDeaths)} recovered: ${worldRecovered}, death: ${worldDeaths} in ${countries} countries, on ${continents} continents`; + `In the world 🌍\nConfirmed: ${worldConfirmed}, active: ${getActiveCases( + worldConfirmed, + worldRecovered, + worldDeaths + )} recovered: ${worldRecovered}, death: ${worldDeaths} in ${countries} countries, on ${continents} continents`; export const getCountriesTableHTML = ({ - continent, - continentTotalConfirmed, continentTotalRecovered, continentTotalDeath, - portionMessage, - }): string => `🗺️ ${continent}. Total active: ${continentTotalConfirmed}, recovered: ${continentTotalRecovered}, death: ${continentTotalDeath} - \n
${table(portionMessage, tableConfig)}
`; + continent, + continentTotalConfirmed, + continentTotalRecovered, + continentTotalDeath, + portionMessage, +}): string => + `🗺️ ${continent}. Total active: ${continentTotalConfirmed}, recovered: ${continentTotalRecovered}, death: ${continentTotalDeath} + \n
${table(
+                                           portionMessage,
+                                           tableConfig
+                                       )}
`; diff --git a/server/src/services/domain/countriesByContinent.ts b/server/src/services/domain/countriesByContinent.ts index aff7b3e..4be7126 100644 --- a/server/src/services/domain/countriesByContinent.ts +++ b/server/src/services/domain/countriesByContinent.ts @@ -1,7 +1,9 @@ import { Country } from '../../models/country.models'; import { ContinentsCountries } from '../../models/continent.models'; -export const getCountriesByContinent = (countries: Array): ContinentsCountries => { +export const getCountriesByContinent = ( + countries: Array +): ContinentsCountries => { const continentsCountries: ContinentsCountries = {}; countries.forEach(({ name, continent }: Country) => { @@ -13,7 +15,6 @@ export const getCountriesByContinent = (countries: Array): ContinentsCo } continentsCountries[continent] = [name]; - return; }); return continentsCountries; From 7d36d61db81cd5b16154a5511167d1cd8bdec7b0 Mon Sep 17 00:00:00 2001 From: Danylo Bilokha Date: Sun, 19 Apr 2020 13:25:25 +0300 Subject: [PATCH 5/9] refactor countries.ts --- .../telegram/botResponse/countriesResponse.ts | 4 +- .../telegram/botResponse/countryResponse.ts | 158 ++++++++---------- .../telegram/botResponse/subscribeResponse.ts | 15 +- .../telegram/botResponse/trendResponse.ts | 45 ++--- .../botResponse/unsubscribeResponse.ts | 4 +- server/src/bots/telegram/models/index.ts | 4 +- .../services/messageHandlerRegistry.ts | 74 +++++--- .../src/messages/feature/availableMessages.ts | 19 ++- .../src/messages/feature/countryMessages.ts | 66 +++++--- .../src/messages/feature/subscribeMessages.ts | 58 ++++--- .../messages/feature/unsubscribeMessages.ts | 4 +- server/src/services/domain/countries.ts | 51 ++++++ .../services/domain/countriesByContinent.ts | 21 --- 13 files changed, 304 insertions(+), 219 deletions(-) create mode 100644 server/src/services/domain/countries.ts delete mode 100644 server/src/services/domain/countriesByContinent.ts diff --git a/server/src/bots/telegram/botResponse/countriesResponse.ts b/server/src/bots/telegram/botResponse/countriesResponse.ts index 8444498..fc65053 100644 --- a/server/src/bots/telegram/botResponse/countriesResponse.ts +++ b/server/src/bots/telegram/botResponse/countriesResponse.ts @@ -6,7 +6,7 @@ import { import { getCountriesSituation } from '../../../services/domain/covid19'; import { getTableHeader, - getTableRowMessageForCountry, + getTableCountryRowMessage, } from '../../../messages/feature/countryMessages'; import { Country } from '../../../models/country.models'; import { @@ -74,7 +74,7 @@ export const countriesByContinentResponse = (continent) => async ( continentTotalRecovered += recovered; continentTotalDeath += deaths; portionMessage.push( - getTableRowMessageForCountry({ + getTableCountryRowMessage({ name, confirmed, recovered, diff --git a/server/src/bots/telegram/botResponse/countryResponse.ts b/server/src/bots/telegram/botResponse/countryResponse.ts index fa0546c..350c466 100644 --- a/server/src/bots/telegram/botResponse/countryResponse.ts +++ b/server/src/bots/telegram/botResponse/countryResponse.ts @@ -1,58 +1,48 @@ +import { adaptCountryToSystemRepresentation } from '../../../services/domain/covid19'; import { - ApiCovid19Situation, - CountrySituationInfo, -} from '../../../models/covid19.models'; -import { - adaptCountryToSystemRepresentation, - getCountriesSituation, -} from '../../../services/domain/covid19'; -import { Country } from '../../../models/country.models'; -import { - getMessageForCountry, - getMessageForUserInputWithoutCountryName, + getCountryIKActionMessage, + getCountryMessage, + getUserInputWithoutCountryNameMessage, } from '../../../messages/feature/countryMessages'; import { Cache } from '../../../utils/cache'; import { flag, name } from 'country-emoji'; -import { getAfterCountryResponseInlineKeyboard } from '../services/keyboard'; -import { textAfterUserCommand } from '../../../utils/textAfterCommand'; -import { isMessageIsCommand } from '../../../utils/incomingMessages'; -import { UserRegExps } from '../../../models/constants'; +import { + getAfterCountryResponseInlineKeyboard, + getFullMenuKeyboard, +} from '../services/keyboard'; import { CallBackQueryHandlerWithCommandArgument } from '../models'; import * as TelegramBot from 'node-telegram-bot-api'; +import { getRequestedCountry } from '../../../services/domain/countries'; +import { catchAsyncError } from '../../../utils/catchError'; +import { logger } from '../../../utils/logger'; +import { getErrorMessage } from '../../../utils/getErrorMessages'; export const showCountryByNameStrategyResponse: CallBackQueryHandlerWithCommandArgument = async ( - bot, - message, - chatId -): Promise => - isMessageIsCommand(message.text, UserRegExps.CountryData) - ? bot.sendMessage(chatId, getMessageForUserInputWithoutCountryName()) - : showCountryResponse( - bot, - adaptCountryToSystemRepresentation( - textAfterUserCommand(message.text) - ), - chatId - ); + bot: TelegramBot, + message: TelegramBot.Message, + chatId: number, + parameterAfterCommand?: string +): Promise => { + if (!parameterAfterCommand) { + return bot.sendMessage(chatId, getUserInputWithoutCountryNameMessage()); + } -export const showCountryByFlag: CallBackQueryHandlerWithCommandArgument = async ( - bot, - message, - chatId -): Promise => - showCountryResponse( + return showCountryResponse( bot, - adaptCountryToSystemRepresentation(name(message.text)), - chatId + message, + chatId, + adaptCountryToSystemRepresentation(parameterAfterCommand) ); +}; -// TODO: Split and move messages to /messages/feature and /domain directories -export const showCountryResponse = async ( - bot, - requestedCountry, - chatId +export const showCountryByFlag: CallBackQueryHandlerWithCommandArgument = async ( + bot: TelegramBot, + message: TelegramBot.Message, + chatId: number, + parameterAfterCommand?: string ): Promise => { - if (!requestedCountry) { + if ( + !parameterAfterCommand || // Because of // [https://github.com/danbilokha/covid19liveupdates/issues/61] // fix of https://github.com/danbilokha/covid19liveupdates/issues/58 @@ -60,58 +50,54 @@ export const showCountryResponse = async ( // MessageRegistry.registerMessageHandler this._bot.onText( // Regexp works not as we expect it to work // Theoretically should be fixed with https://github.com/danbilokha/covid19liveupdates/issues/49 - return; - } - - const allCountriesSituations: Array<[ - Country, - Array - ]> = await getCountriesSituation(); - const foundCountrySituations: [ - Country, - Array - ] = allCountriesSituations.find( - ([receivedCountry, situations]) => - receivedCountry.name === requestedCountry - ); - if ( - !foundCountrySituations || - !foundCountrySituations?.length || - !foundCountrySituations[0] || - !foundCountrySituations[1].length + !adaptCountryToSystemRepresentation(name(parameterAfterCommand)) ) { - return bot.sendMessage( - chatId, - `Sorry, but I cannot find anything for ${requestedCountry}. I will save your request and will work on it` - ); + return bot.sendMessage(chatId, getUserInputWithoutCountryNameMessage()); } - const [foundCountry, foundSituation] = foundCountrySituations; + return showCountryResponse( + bot, + message, + chatId, + adaptCountryToSystemRepresentation(name(parameterAfterCommand)) + ); +}; - Cache.set(`${chatId}_commands_country`, flag(foundCountry.name)); +export const showCountryResponse: CallBackQueryHandlerWithCommandArgument = async ( + bot: TelegramBot, + message: TelegramBot.Message, + chatId: number, + requestedCountry: string +): Promise => { + const [err, result] = await catchAsyncError( + getRequestedCountry(requestedCountry) + ); + if (err) { + logger.log('error', getErrorMessage(err)); + return bot.sendMessage(chatId, err.message); + } - // TODO: Optimize! - let totalRecovered = 0; - let totalConfirmed = 0; - let totalDeaths = 0; + const [{ name }, foundSituation] = result; + Cache.set(`${chatId}_commands_country`, flag(name)); - [foundSituation[foundSituation.length - 1]].forEach( - ({ confirmed, deaths, recovered }: ApiCovid19Situation) => { - totalRecovered += recovered; - totalConfirmed += confirmed; - totalDeaths += deaths; - } + const { recovered, confirmed, deaths, date } = foundSituation[ + foundSituation.length - 1 + ]; + // two send messages due to https://stackoverflow.com/a/41841237/6803463 + await bot.sendMessage( + chatId, + getCountryMessage({ + name, + confirmed, + recovered, + deaths, + lastUpdateDate: date, + }), + getFullMenuKeyboard(chatId) ); - return bot.sendMessage( chatId, - getMessageForCountry({ - name: foundCountry.name, - confirmed: totalConfirmed, - recovered: totalRecovered, - deaths: totalDeaths, - lastUpdateDate: foundSituation[foundSituation.length - 1].date, - }), - getAfterCountryResponseInlineKeyboard(foundCountry.name) + getCountryIKActionMessage(name), + getAfterCountryResponseInlineKeyboard(name) ); }; diff --git a/server/src/bots/telegram/botResponse/subscribeResponse.ts b/server/src/bots/telegram/botResponse/subscribeResponse.ts index de80e19..209be8a 100644 --- a/server/src/bots/telegram/botResponse/subscribeResponse.ts +++ b/server/src/bots/telegram/botResponse/subscribeResponse.ts @@ -8,7 +8,7 @@ import { import { CustomSubscriptions, UserRegExps } from '../../../models/constants'; import { subscribeOn } from '../../../services/domain/subscriptions'; import { catchAsyncError } from '../../../utils/catchError'; -import { getFullMenuKeyboard, getSubscriptionMessageInlineKeyboard } from '../services/keyboard'; +import { getSubscriptionMessageInlineKeyboard } from '../services/keyboard'; import { getTelegramActiveUserSubscriptions } from '../services/storage'; import { getUserMessageFromIKorText } from '../utils/getUserMessageFromIKorText'; import { UserSubscription } from '../../../models/subscription.models'; @@ -41,7 +41,10 @@ export const showExistingSubscriptionsResponse = async ( return bot.sendMessage(chatId, noSubscriptionsResponseMessage()); } - return bot.sendMessage(chatId, showMySubscriptionMessage(activeUserSubscription)); + return bot.sendMessage( + chatId, + showMySubscriptionMessage(activeUserSubscription) + ); }; // If it's called from InlineKeyboard, then @param ikCbData will be available @@ -60,7 +63,11 @@ export const subscribingStrategyResponse: CallBackQueryHandlerWithCommandArgumen subscribeOn( message.chat, removeCommandFromMessageIfExist( - getUserMessageFromIKorText(message, CustomSubscriptions.SubscribeMeOn, ''), + getUserMessageFromIKorText( + message, + CustomSubscriptions.SubscribeMeOn, + '' + ), UserRegExps.Subscribe ) ) @@ -69,5 +76,5 @@ export const subscribingStrategyResponse: CallBackQueryHandlerWithCommandArgumen return bot.sendMessage(chatId, subscribeError(err.message)); } - return bot.sendMessage(chatId, subscriptionResultMessage(result), getFullMenuKeyboard(chatId)); + return bot.sendMessage(chatId, subscriptionResultMessage(result)); }; diff --git a/server/src/bots/telegram/botResponse/trendResponse.ts b/server/src/bots/telegram/botResponse/trendResponse.ts index 689f715..1982860 100644 --- a/server/src/bots/telegram/botResponse/trendResponse.ts +++ b/server/src/bots/telegram/botResponse/trendResponse.ts @@ -1,41 +1,28 @@ import { getCovidTrends } from '../../../services/api/api-chart'; import { addDays, Now } from '../../../utils/dateUtils'; import { CountrySituationInfo } from '../../../models/covid19.models'; -import { Country } from '../../../models/country.models'; -import { getCountriesSituation } from '../../../services/domain/covid19'; import { Transform } from '../../../services/domain/chart'; +import { catchAsyncError } from '../../../utils/catchError'; +import { getRequestedCountry } from '../../../services/domain/countries'; +import { logger } from '../../../utils/logger'; +import { getErrorMessage } from '../../../utils/getErrorMessages'; +import { CallBackQueryHandlerWithCommandArgument } from '../models'; +import * as TelegramBot from 'node-telegram-bot-api'; -export const trendsByCountryResponse = async ( - bot, - message, - chatId, - requestedCountry +export const trendsByCountryResponse: CallBackQueryHandlerWithCommandArgument = async ( + bot: TelegramBot, + message: TelegramBot.Message, + chatId: number, + requestedCountry?: string | undefined ): Promise => { - const allCountries: Array<[ - Country, - Array - ]> = await getCountriesSituation(); - const foundCountrySituations: [ - Country, - Array - ] = allCountries.find( - ([receivedCountry, situations]) => - receivedCountry.name === requestedCountry + const [err, [foundCountry, foundSituation]] = await catchAsyncError( + getRequestedCountry(requestedCountry) ); - if ( - !foundCountrySituations || - !foundCountrySituations?.length || - !foundCountrySituations[0] || - !foundCountrySituations[1].length - ) { - return bot.sendMessage( - chatId, - `Sorry, but I cannot find anything for ${requestedCountry}. I will save your request and will work on it` - ); + if (err) { + logger.log('error', getErrorMessage(err)); + return bot.sendMessage(chatId, err.message); } - const [foundCountry, foundSituation] = foundCountrySituations; - const lastWeekSituation = foundSituation.filter( (c: CountrySituationInfo) => { const date = new Date(c.date); diff --git a/server/src/bots/telegram/botResponse/unsubscribeResponse.ts b/server/src/bots/telegram/botResponse/unsubscribeResponse.ts index c12abc9..8ef17ce 100644 --- a/server/src/bots/telegram/botResponse/unsubscribeResponse.ts +++ b/server/src/bots/telegram/botResponse/unsubscribeResponse.ts @@ -1,7 +1,7 @@ import { UserSubscription } from '../../../models/subscription.models'; import { getUnsubscribeResponseMessage, - unSubscribeError, + unSubscribeErrorMessage, unsubscribeResultMessage, } from '../../../messages/feature/unsubscribeMessages'; import { @@ -54,7 +54,7 @@ export const unsubscribeStrategyResponse: CallBackQueryHandlerWithCommandArgumen unsubscribeMeFrom(message.chat, parameterAfterCommand) ); if (err) { - return bot.sendMessage(chatId, unSubscribeError(err.message)); + return bot.sendMessage(chatId, unSubscribeErrorMessage(err.message)); } return bot.sendMessage( diff --git a/server/src/bots/telegram/models/index.ts b/server/src/bots/telegram/models/index.ts index f8c8b0f..a5a553c 100644 --- a/server/src/bots/telegram/models/index.ts +++ b/server/src/bots/telegram/models/index.ts @@ -1,11 +1,11 @@ import * as TelegramBot from 'node-telegram-bot-api'; -export type CallBackQueryHandlerWithCommandArgument = ( +export type CallBackQueryHandlerWithCommandArgument = ( bot: TelegramBot, message: TelegramBot.Message, chatId: number, parameterAfterCommand?: string -) => unknown; +) => Promise; export const TELEGRAM_PREFIX: string = 'telegram'; export const UNSUBSCRIPTIONS_ROW_ITEMS_NUMBER: number = 3; diff --git a/server/src/bots/telegram/services/messageHandlerRegistry.ts b/server/src/bots/telegram/services/messageHandlerRegistry.ts index 78d44a5..66caa18 100644 --- a/server/src/bots/telegram/services/messageHandlerRegistry.ts +++ b/server/src/bots/telegram/services/messageHandlerRegistry.ts @@ -21,7 +21,9 @@ import * as TelegramBot from 'node-telegram-bot-api'; import { getInfoMessage } from '../../../utils/getErrorMessages'; export class MessageHandlerRegistry { - _messageHandlers: { [regexp: string]: CallBackQueryHandlerWithCommandArgument } = {}; + _messageHandlers: { + [regexp: string]: CallBackQueryHandlerWithCommandArgument; + } = {}; _singleParameterAfterCommands: Array = []; constructor(private readonly bot: TelegramBot) { @@ -32,11 +34,16 @@ export class MessageHandlerRegistry { regexps: Array, callback: CallBackQueryHandlerWithCommandArgument ): MessageHandlerRegistry { - this._singleParameterAfterCommands = [...this._singleParameterAfterCommands, ...regexps]; + this._singleParameterAfterCommands = [ + ...this._singleParameterAfterCommands, + ...regexps, + ]; regexps.forEach( (regexp: string) => - (this._messageHandlers[regexp] = withSingleParameterAfterCommand(this, callback)) + (this._messageHandlers[ + regexp + ] = withSingleParameterAfterCommand(this, callback)) ); return this; } @@ -59,7 +66,9 @@ export class MessageHandlerRegistry { const suitableKeys: Array = Object.keys(cbHandlers).filter( (cbHandlerRegExpKey: string) => - !!runCheckupAgainstStr.match(new RegExp(cbHandlerRegExpKey, 'g')) + !!runCheckupAgainstStr.match( + new RegExp(cbHandlerRegExpKey, 'g') + ) ); if (suitableKeys.length === 0) { @@ -89,25 +98,34 @@ export class MessageHandlerRegistry { private registerCallBackQuery() { this.bot.on('callback_query', ({ id, data, message, from }) => { - this.bot.answerCallbackQuery(id, { text: `${data} in progress...` }).then(() => { - return this.runCommandHandler( - { - ...message, - from, // As in cases of answerCallbackQuery original from in message will be bot sender, - // But we do want it still to be user. Do we? :D - }, - data - ); - }); + this.bot + .answerCallbackQuery(id, { text: `${data} in progress...` }) + .then(() => { + return this.runCommandHandler( + { + ...message, + from, // As in cases of answerCallbackQuery original from in message will be bot sender, + // But we do want it still to be user. Do we? :D + }, + data + ); + }); }); } - private async tryDeduceUserCommand(message: TelegramBot.Message): Promise { + private async tryDeduceUserCommand( + message: TelegramBot.Message + ): Promise { const chatId = getChatId(message); if (isMessageCountryFlag(message.text)) { const countryName: string = getCountryNameByFlag(message.text); - return showCountryResponse(this.bot, countryName, chatId); + return showCountryResponse( + this.bot, + message, + chatId, + countryName + ) as Promise; } const countries: Array = await getAvailableCountries(); @@ -116,7 +134,12 @@ export class MessageHandlerRegistry { countries ); if (country) { - return showCountryResponse(this.bot, country.name, chatId); + return showCountryResponse( + this.bot, + message, + chatId, + country.name + ) as Promise; } const answers: Array = await fetchAnswer(message.text); @@ -147,7 +170,13 @@ export const withSingleParameterAfterCommand = ( ikCbData ?? message.text ); - return handlerFn.call(context, bot, message, chatId, userEnteredArgumentAfterCommand); + return handlerFn.call( + context, + bot, + message, + chatId, + userEnteredArgumentAfterCommand + ); } catch (err) { logger.log('error', { ...message, @@ -172,7 +201,9 @@ function getParameterAfterCommandFromMessage( userFullInput; const execResult = new RegExp( - `(?${this._singleParameterAfterCommands.join('|\\')})\\s(?.*)` + `(?${this._singleParameterAfterCommands.join( + '|\\' + )})\\s(?.*)` ).exec(makeMagicOverUserFullInput); if (!execResult) { logger.log('info', getInfoMessage('Entered unsupported command')); @@ -181,7 +212,10 @@ function getParameterAfterCommandFromMessage( /* tslint:disable:no-string-literal */ if (execResult.groups['command'] && !execResult.groups['firstargument']) { - logger.log('info', getInfoMessage(`No parameter for ${execResult.groups['command']}`)); + logger.log( + 'info', + getInfoMessage(`No parameter for ${execResult.groups['command']}`) + ); return undefined; } diff --git a/server/src/messages/feature/availableMessages.ts b/server/src/messages/feature/availableMessages.ts index c281355..e493199 100644 --- a/server/src/messages/feature/availableMessages.ts +++ b/server/src/messages/feature/availableMessages.ts @@ -1,17 +1,18 @@ -import {Country} from '../../models/country.models'; -import {getCountriesByContinent} from '../../services/domain/countriesByContinent'; +import { Country } from '../../models/country.models'; +import { getCountriesByContinent } from '../../services/domain/countries'; export const getShowCountriesMessage = (countries: Array): string => { const availableFor: string = `Available for ${countries.length} countries around the 🌍.`; - const countriesList: string = Object.entries(getCountriesByContinent(countries)) - .map( - ([continentName, countries]: [string, Array]): string => `\n🗺️ ${continentName}, totally ${countries.length} countries\n` - .concat(countries.join('; ')) + const countriesList: string = Object.entries( + getCountriesByContinent(countries) + ) + .map(([continentName, countries]: [string, Array]): string => + `\n🗺️ ${continentName}, totally ${countries.length} countries\n`.concat( + countries.join('; ') + ) ) .join('\n'); const hint: string = `ℹ i.e. /country ${countries[0].name}`; - return availableFor - .concat(`\n\n${countriesList}`) - .concat(`\n\n${hint}`) + return availableFor.concat(`\n\n${countriesList}`).concat(`\n\n${hint}`); }; diff --git a/server/src/messages/feature/countryMessages.ts b/server/src/messages/feature/countryMessages.ts index 002db30..12bb57b 100644 --- a/server/src/messages/feature/countryMessages.ts +++ b/server/src/messages/feature/countryMessages.ts @@ -1,26 +1,48 @@ -import {Country, CountryMessage} from '../../models/country.models'; -import {getActiveCases} from '../covid19Messages'; -import {flag} from 'country-emoji'; -import {UserRegExps} from '../../models/constants'; +import { Country, CountryMessage } from '../../models/country.models'; +import { getActiveCases } from '../covid19Messages'; +import { flag } from 'country-emoji'; +import { UserRegExps } from '../../models/constants'; -export const getMessageForUserInputWithoutCountryName = (): string => `Sorry, but I can show country only by country name. Enter country name by a pattern ${UserRegExps.CountryData} [country name]`; +export const getUserInputWithoutCountryNameMessage = (): string => + `Sorry, but I can show country only by country name.` + + ` Enter country name by a pattern ${UserRegExps.CountryData} [country name]`; -export const getTableRowMessageForCountry = ({ - name, - confirmed, - recovered, - deaths, - lastUpdateDate, - }: CountryMessage): Array => - [`${flag(name) ?? ''} ${name}`, `${getActiveCases(confirmed, recovered, deaths)}`, `${recovered}`, `${deaths}`]; +export const getCountryNonExistenceErrorMessage = (country: string): string => + `Sorry, but I cannot find anything for ${country}.` + + ` I will save your request and will work on it`; -export const getTableHeader = (): Array => ['Country', 'Active', 'Recovered', 'Deaths']; +export const getTableCountryRowMessage = ({ + name, + confirmed, + recovered, + deaths, + lastUpdateDate, +}: CountryMessage): Array => [ + `${flag(name) ?? ''} ${name}`, + `${getActiveCases(confirmed, recovered, deaths)}`, + `${recovered}`, + `${deaths}`, +]; -export const getMessageForCountry = ({ - name, - confirmed, - recovered, - deaths, - lastUpdateDate, - }: CountryMessage): string => - `${flag(name) ?? ''} ${name}, ${getActiveCases(confirmed, recovered, deaths)} active, ${recovered} recovered, ${deaths} deaths. ⏱️${lastUpdateDate}`; \ No newline at end of file +export const getTableHeader = (): Array => [ + 'Country', + 'Active', + 'Recovered', + 'Deaths', +]; + +export const getCountryMessage = ({ + name, + confirmed, + recovered, + deaths, + lastUpdateDate, +}: CountryMessage): string => + `${flag(name) ?? ''} ${name}, ${getActiveCases( + confirmed, + recovered, + deaths + )} active, ${recovered} recovered, ${deaths} deaths. ⏱️${lastUpdateDate}`; + +export const getCountryIKActionMessage = (country: string): string => + `More on ${country}`; diff --git a/server/src/messages/feature/subscribeMessages.ts b/server/src/messages/feature/subscribeMessages.ts index 5527290..2cf6893 100644 --- a/server/src/messages/feature/subscribeMessages.ts +++ b/server/src/messages/feature/subscribeMessages.ts @@ -1,48 +1,66 @@ -import {Subscription, UserSubscription} from '../../models/subscription.models'; -import {getMessageForCountry} from './countryMessages'; -import {CountrySituationInfo} from '../../models/covid19.models'; -import {getDiffMessage} from '../covid19Messages'; +import { + Subscription, + UserSubscription, +} from '../../models/subscription.models'; +import { getCountryMessage } from './countryMessages'; +import { CountrySituationInfo } from '../../models/covid19.models'; +import { getDiffMessage } from '../covid19Messages'; -export const ALREADY_SUBSCRIBED_MESSAGE: string = 'You are already subscribed on the country'; +export const ALREADY_SUBSCRIBED_MESSAGE: string = + 'You are already subscribed on the country'; export const subscriptionManagerResponseMessage = (): string => { - return `Easy way to manage your subscriptions 💌` + return `Easy way to manage your subscriptions 💌`; }; export const noSubscriptionsResponseMessage = (): string => { - return `It seems you haven't subscribed for any 🥺` + return `It seems you haven't subscribed for any 🥺`; }; export const subscribeError = (message: string): string => { - return `${message}, sorry 🙇🏽‍` + return `${message}, sorry 🙇🏽‍`; }; export const subscriptionResultMessage = (message: string): string => { return `Cool, subscribed on ${message} 😎`; }; -export const showMySubscriptionMessage = (userSubscription: UserSubscription): string => { - return `You're 🔔 subscribed on: ` - .concat(userSubscription.subscriptionsOn.map((sub: Subscription) => `${sub.value}`).join(', ')) +export const showMySubscriptionMessage = ( + userSubscription: UserSubscription +): string => { + return `You're 🔔 subscribed on: `.concat( + userSubscription.subscriptionsOn + .map((sub: Subscription) => `${sub.value}`) + .join(', ') + ); }; export const showCountrySubscriptionMessage = ( - {name, confirmed, recovered, deaths, date}: Partial, - {confirmed: prevConfirmed, recovered: prevRecovered, deaths: prevDeaths, date: prevDate}: Partial + { name, confirmed, recovered, deaths, date }: Partial, + { + confirmed: prevConfirmed, + recovered: prevRecovered, + deaths: prevDeaths, + date: prevDate, + }: Partial ): string => { - return `🔔 ` - + getMessageForCountry({ + return ( + `🔔 ` + + getCountryMessage({ name, confirmed, recovered, deaths, lastUpdateDate: date, - }) - + `\n\n📈 Country change, since ⏱️${prevDate}\n` - + getDiffMessage( - {confirmed, recovered, deaths}, + }) + + `\n\n📈 Country change, since ⏱️${prevDate}\n` + + getDiffMessage( + { confirmed, recovered, deaths }, { - confirmed: prevConfirmed, recovered: prevRecovered, deaths: prevDeaths + confirmed: prevConfirmed, + recovered: prevRecovered, + deaths: prevDeaths, } ) + ); }; diff --git a/server/src/messages/feature/unsubscribeMessages.ts b/server/src/messages/feature/unsubscribeMessages.ts index 724d716..f714900 100644 --- a/server/src/messages/feature/unsubscribeMessages.ts +++ b/server/src/messages/feature/unsubscribeMessages.ts @@ -2,8 +2,8 @@ export const getUnsubscribeResponseMessage = (): string => { return 'Choose items to unsubscribe from'; }; -export const unSubscribeError = (message: string): string => { - return `${message}, sorry 🙇🏽` +export const unSubscribeErrorMessage = (message: string): string => { + return `${message}, sorry 🙇🏽`; }; export const unsubscribeResultMessage = (message: string): string => { diff --git a/server/src/services/domain/countries.ts b/server/src/services/domain/countries.ts new file mode 100644 index 0000000..40cf1c2 --- /dev/null +++ b/server/src/services/domain/countries.ts @@ -0,0 +1,51 @@ +import { Country } from '../../models/country.models'; +import { ContinentsCountries } from '../../models/continent.models'; +import { CountrySituationInfo } from '../../models/covid19.models'; +import { getCountriesSituation } from './covid19'; +import { getCountryNonExistenceErrorMessage } from '../../messages/feature/countryMessages'; + +export const getCountriesByContinent = ( + countries: Array +): ContinentsCountries => { + const continentsCountries: ContinentsCountries = {}; + + countries.forEach(({ name, continent }: Country) => { + const continentCountries = continentsCountries[continent]; + + if (!!continentCountries?.length) { + continentCountries.push(name); + return; + } + + continentsCountries[continent] = [name]; + }); + + return continentsCountries; +}; + +export const getRequestedCountry = async ( + countryName: string +): Promise<[Country, Array]> => { + const allCountriesSituations: Array<[ + Country, + Array + ]> = await getCountriesSituation(); + + const foundCountrySituations: [ + Country, + Array + ] = allCountriesSituations.find( + ([receivedCountry, situations]) => receivedCountry.name === countryName + ); + + if ( + !foundCountrySituations || + !foundCountrySituations?.length || + !foundCountrySituations[0] || + !foundCountrySituations[1].length + ) { + throw new Error(getCountryNonExistenceErrorMessage(countryName)); + } + + return foundCountrySituations; +}; diff --git a/server/src/services/domain/countriesByContinent.ts b/server/src/services/domain/countriesByContinent.ts deleted file mode 100644 index 4be7126..0000000 --- a/server/src/services/domain/countriesByContinent.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { Country } from '../../models/country.models'; -import { ContinentsCountries } from '../../models/continent.models'; - -export const getCountriesByContinent = ( - countries: Array -): ContinentsCountries => { - const continentsCountries: ContinentsCountries = {}; - - countries.forEach(({ name, continent }: Country) => { - const continentCountries = continentsCountries[continent]; - - if (!!continentCountries?.length) { - continentCountries.push(name); - return; - } - - continentsCountries[continent] = [name]; - }); - - return continentsCountries; -}; From ac5920663dda8f7b7037d630ed041233994a6af3 Mon Sep 17 00:00:00 2001 From: Danylo Bilokha Date: Sun, 19 Apr 2020 17:28:50 +0300 Subject: [PATCH 6/9] fix country by flag; implement #87 (Make command not case sensitive) --- .../telegram/botResponse/countriesResponse.ts | 161 +++++------------- .../telegram/botResponse/countryResponse.ts | 27 ++- .../telegram/botResponse/trendResponse.ts | 2 +- server/src/bots/telegram/index.ts | 2 +- .../services/messageHandlerRegistry.ts | 33 ++-- server/src/messages/covid19Messages.ts | 27 +-- .../src/messages/feature/availableMessages.ts | 6 +- .../src/messages/feature/countriesMessages.ts | 47 +++-- .../src/messages/feature/countryMessages.ts | 35 +--- .../src/messages/feature/subscribeMessages.ts | 8 +- server/src/models/continent.models.ts | 3 - server/src/models/country.models.ts | 10 +- server/src/models/covid19.models.ts | 27 ++- server/src/services/domain/countries.ts | 112 +++++++++++- server/src/utils/featureHelpers/country.ts | 20 ++- 15 files changed, 279 insertions(+), 241 deletions(-) delete mode 100644 server/src/models/continent.models.ts diff --git a/server/src/bots/telegram/botResponse/countriesResponse.ts b/server/src/bots/telegram/botResponse/countriesResponse.ts index fc65053..f157a56 100644 --- a/server/src/bots/telegram/botResponse/countriesResponse.ts +++ b/server/src/bots/telegram/botResponse/countriesResponse.ts @@ -1,155 +1,82 @@ import { - ContinentCountriesSituation, - CountrySituation, CountrySituationInfo, + WorldOverallInformation, } from '../../../models/covid19.models'; -import { getCountriesSituation } from '../../../services/domain/covid19'; import { - getTableHeader, - getTableCountryRowMessage, -} from '../../../messages/feature/countryMessages'; -import { Country } from '../../../models/country.models'; -import { - getCountriesWorldMessage, getCountriesTableHTML, + getCountriesWorldMessage, + getTableCountryRowMessage, + getTableHeader, } from '../../../messages/feature/countriesMessages'; import { getContinentsInlineKeyboard } from '../services/keyboard'; import { CallBackQueryHandlerWithCommandArgument } from '../models'; +import { + getContinentOverallInformation, + getWorldOverallInformation, +} from '../../../services/domain/countries'; -// TODO: Move this logic to domain and leave here only Telegram bot specific message response -// Sending response itself export const countriesByContinentResponse = (continent) => async ( bot, message, chatId ) => { - const countriesSituation: Array<[ - Country, - Array - ]> = await getCountriesSituation(); - const continentCountries: ContinentCountriesSituation = {}; - - countriesSituation.forEach( - ([country, situations]: [Country, Array]) => { - const { confirmed, recovered, deaths } = situations[ - situations.length - 1 - ]; - - const countrySituationResult: CountrySituation = { - lastUpdateDate: situations[situations.length - 1].date, - country, - confirmed, - recovered, - deaths, - }; - const prevCountriesResult: Array = continentCountries[ - country.continent - ] - ? continentCountries[country.continent] - : []; - continentCountries[country.continent] = [ - ...prevCountriesResult, - countrySituationResult, - ]; - } - ); + const { + confirmed, + recovered, + deaths, + countriesSituation, + } = await getContinentOverallInformation(continent); const portionMessage = [getTableHeader()]; - let continentTotalConfirmed: number = 0; - let continentTotalRecovered: number = 0; - let continentTotalDeath: number = 0; - portionMessage.push(); - continentCountries[continent] - .sort((country1, country2) => country2.confirmed - country1.confirmed) - .forEach( - ({ - country: { name }, - lastUpdateDate, - confirmed, - recovered, - deaths, - }: CountrySituation) => { - continentTotalConfirmed += confirmed; - continentTotalRecovered += recovered; - continentTotalDeath += deaths; - portionMessage.push( - getTableCountryRowMessage({ - name, - confirmed, - recovered, - deaths, - lastUpdateDate, - }) - ); - } + + countriesSituation + .sort((country1, country2) => country2.active - country1.active) + .forEach(({ name, active, recovered, deaths }) => + portionMessage.push( + getTableCountryRowMessage(name, active, recovered, deaths) + ) ); - await bot.sendMessage( + return bot.sendMessage( chatId, - getCountriesTableHTML({ + getCountriesTableHTML( continent, - continentTotalConfirmed, - continentTotalRecovered, - continentTotalDeath, - portionMessage, - }), + confirmed, + recovered, + deaths, + countriesSituation, + portionMessage + ), { parse_mode: 'HTML' } ); }; -// TODO: Move this logic to domain and leave here only Telegram bot specific message response -// Sending response itself export const countriesResponse: CallBackQueryHandlerWithCommandArgument = async ( bot, message, chatId ) => { - const countriesSituation: Array<[ - Country, - Array - ]> = await getCountriesSituation(); - const continentCountries: ContinentCountriesSituation = {}; - let worldTotalConfirmed = 0; - let worldTotalRecovered = 0; - let worldTotalDeaths = 0; - - countriesSituation.forEach( - ([country, situations]: [Country, Array]) => { - const { confirmed, recovered, deaths } = situations[ - situations.length - 1 - ]; - - worldTotalConfirmed += confirmed; - worldTotalRecovered += recovered; - worldTotalDeaths += deaths; - - const countrySituationResult: CountrySituation = { - lastUpdateDate: situations[situations.length - 1].date, - country, - confirmed, - recovered, - deaths, - }; - const prevCountriesResult = continentCountries[country.continent] - ? continentCountries[country.continent] - : []; - continentCountries[country.continent] = [ - ...prevCountriesResult, - countrySituationResult, - ]; - } - ); + const { + confirmed, + recovered, + deaths, + continentCountriesSituations, + }: WorldOverallInformation = await getWorldOverallInformation(); // Send overall world info, return bot.sendMessage( chatId, getCountriesWorldMessage( - worldTotalConfirmed, - worldTotalRecovered, - worldTotalDeaths, - countriesSituation.length, - Object.keys(continentCountries).length + confirmed, + recovered, + deaths, + Object.values(continentCountriesSituations).reduce( + (acc: number, val: Array): number => + acc + val.length, + 0 + ) as number, + Object.keys(continentCountriesSituations).length ), getContinentsInlineKeyboard() ); diff --git a/server/src/bots/telegram/botResponse/countryResponse.ts b/server/src/bots/telegram/botResponse/countryResponse.ts index 350c466..309fee7 100644 --- a/server/src/bots/telegram/botResponse/countryResponse.ts +++ b/server/src/bots/telegram/botResponse/countryResponse.ts @@ -12,10 +12,12 @@ import { } from '../services/keyboard'; import { CallBackQueryHandlerWithCommandArgument } from '../models'; import * as TelegramBot from 'node-telegram-bot-api'; -import { getRequestedCountry } from '../../../services/domain/countries'; import { catchAsyncError } from '../../../utils/catchError'; import { logger } from '../../../utils/logger'; import { getErrorMessage } from '../../../utils/getErrorMessages'; +import { Country } from '../../../models/country.models'; +import { CountrySituationInfo } from '../../../models/covid19.models'; +import { getRequestedCountry } from '../../../services/domain/countries'; export const showCountryByNameStrategyResponse: CallBackQueryHandlerWithCommandArgument = async ( bot: TelegramBot, @@ -38,11 +40,11 @@ export const showCountryByNameStrategyResponse: CallBackQueryHandlerWithCommandA export const showCountryByFlag: CallBackQueryHandlerWithCommandArgument = async ( bot: TelegramBot, message: TelegramBot.Message, - chatId: number, - parameterAfterCommand?: string + chatId: number ): Promise => { + const countryFlag = message.text; if ( - !parameterAfterCommand || + !countryFlag || // Because of // [https://github.com/danbilokha/covid19liveupdates/issues/61] // fix of https://github.com/danbilokha/covid19liveupdates/issues/58 @@ -50,7 +52,7 @@ export const showCountryByFlag: CallBackQueryHandlerWithCommandArgument = async // MessageRegistry.registerMessageHandler this._bot.onText( // Regexp works not as we expect it to work // Theoretically should be fixed with https://github.com/danbilokha/covid19liveupdates/issues/49 - !adaptCountryToSystemRepresentation(name(parameterAfterCommand)) + !adaptCountryToSystemRepresentation(name(countryFlag)) ) { return bot.sendMessage(chatId, getUserInputWithoutCountryNameMessage()); } @@ -59,7 +61,7 @@ export const showCountryByFlag: CallBackQueryHandlerWithCommandArgument = async bot, message, chatId, - adaptCountryToSystemRepresentation(name(parameterAfterCommand)) + adaptCountryToSystemRepresentation(name(countryFlag)) ); }; @@ -69,7 +71,10 @@ export const showCountryResponse: CallBackQueryHandlerWithCommandArgument = asyn chatId: number, requestedCountry: string ): Promise => { - const [err, result] = await catchAsyncError( + const [err, result]: [ + Error, + [Country, Array] + ] = await catchAsyncError<[Country, Array]>( getRequestedCountry(requestedCountry) ); if (err) { @@ -86,13 +91,7 @@ export const showCountryResponse: CallBackQueryHandlerWithCommandArgument = asyn // two send messages due to https://stackoverflow.com/a/41841237/6803463 await bot.sendMessage( chatId, - getCountryMessage({ - name, - confirmed, - recovered, - deaths, - lastUpdateDate: date, - }), + getCountryMessage(name, confirmed, recovered, deaths, date), getFullMenuKeyboard(chatId) ); return bot.sendMessage( diff --git a/server/src/bots/telegram/botResponse/trendResponse.ts b/server/src/bots/telegram/botResponse/trendResponse.ts index 1982860..5d4a256 100644 --- a/server/src/bots/telegram/botResponse/trendResponse.ts +++ b/server/src/bots/telegram/botResponse/trendResponse.ts @@ -14,7 +14,7 @@ export const trendsByCountryResponse: CallBackQueryHandlerWithCommandArgument = message: TelegramBot.Message, chatId: number, requestedCountry?: string | undefined -): Promise => { +): Promise => { const [err, [foundCountry, foundSituation]] = await catchAsyncError( getRequestedCountry(requestedCountry) ); diff --git a/server/src/bots/telegram/index.ts b/server/src/bots/telegram/index.ts index e1eec10..76afbb2 100644 --- a/server/src/bots/telegram/index.ts +++ b/server/src/bots/telegram/index.ts @@ -119,7 +119,7 @@ function runTelegramBot(app: Express, appUrl: string) { const single = countries .map((c) => flag(c.name)?.trim() ?? undefined) .filter((v) => !!v) // TODO: Find flag that we lack for [https://github.com/danbilokha/covid19liveupdates/issues/61] - .join('//'); + .join('><'); messageHandlerRegistry.registerMessageHandler( [`[~${single}~]`], diff --git a/server/src/bots/telegram/services/messageHandlerRegistry.ts b/server/src/bots/telegram/services/messageHandlerRegistry.ts index 66caa18..2ba070a 100644 --- a/server/src/bots/telegram/services/messageHandlerRegistry.ts +++ b/server/src/bots/telegram/services/messageHandlerRegistry.ts @@ -34,12 +34,16 @@ export class MessageHandlerRegistry { regexps: Array, callback: CallBackQueryHandlerWithCommandArgument ): MessageHandlerRegistry { + const systemRegExps = regexps.map((regexp: string) => + regexp.toLocaleLowerCase() + ); + this._singleParameterAfterCommands = [ ...this._singleParameterAfterCommands, - ...regexps, + ...systemRegExps, ]; - regexps.forEach( + systemRegExps.forEach( (regexp: string) => (this._messageHandlers[ regexp @@ -61,7 +65,10 @@ export class MessageHandlerRegistry { ): Promise { logger.log('info', message); - const runCheckupAgainstStr = ikCbData ? ikCbData : message.text; + const runCheckupAgainstStr = (ikCbData + ? ikCbData + : message.text + ).toLocaleLowerCase(); const cbHandlers = this._messageHandlers; const suitableKeys: Array = Object.keys(cbHandlers).filter( @@ -120,12 +127,7 @@ export class MessageHandlerRegistry { if (isMessageCountryFlag(message.text)) { const countryName: string = getCountryNameByFlag(message.text); - return showCountryResponse( - this.bot, - message, - chatId, - countryName - ) as Promise; + return showCountryResponse(this.bot, message, chatId, countryName); } const countries: Array = await getAvailableCountries(); @@ -134,12 +136,7 @@ export class MessageHandlerRegistry { countries ); if (country) { - return showCountryResponse( - this.bot, - message, - chatId, - country.name - ) as Promise; + return showCountryResponse(this.bot, message, chatId, country.name); } const answers: Array = await fetchAnswer(message.text); @@ -163,11 +160,11 @@ export const withSingleParameterAfterCommand = ( message: TelegramBot.Message, chatId: number, ikCbData?: string - ): unknown => { + ): Promise => { try { const userEnteredArgumentAfterCommand: string = getParameterAfterCommandFromMessage.call( context, - ikCbData ?? message.text + (ikCbData ?? message.text).toLocaleLowerCase() ); return handlerFn.call( @@ -183,6 +180,8 @@ export const withSingleParameterAfterCommand = ( type: LogglyTypes.CommandError, message: err.message, }); + + return noResponse(this.bot, message, chatId); } }; }; diff --git a/server/src/messages/covid19Messages.ts b/server/src/messages/covid19Messages.ts index b85408a..46a3e84 100644 --- a/server/src/messages/covid19Messages.ts +++ b/server/src/messages/covid19Messages.ts @@ -1,15 +1,18 @@ -import {CountrySituationInfo} from '../models/covid19.models'; -import {addNumberChangeSymbol} from '../utils/addNumberChangeSymbol'; - -export const getActiveCases = (totalConfirmed: number, - totalRecovered: number, - totalDeaths: number): number => - totalConfirmed - totalRecovered - totalDeaths ?? 0; +import { CountrySituationInfo } from '../models/covid19.models'; +import { addNumberChangeSymbol } from '../utils/addNumberChangeSymbol'; +import { getActiveCases } from '../services/domain/countries'; export const getDiffMessage = ( - {confirmed, recovered, deaths}: Partial, - {confirmed: prevConfirmed, recovered: prevRecovered, deaths: prevDeaths}: Partial + { confirmed, recovered, deaths }: Partial, + { + confirmed: prevConfirmed, + recovered: prevRecovered, + deaths: prevDeaths, + }: Partial ): string => - `${addNumberChangeSymbol(getActiveCases(confirmed, recovered, deaths) - getActiveCases(prevConfirmed, prevRecovered, prevDeaths))} active, ` - + `${addNumberChangeSymbol(recovered - prevRecovered)} recovered, ` - + `${addNumberChangeSymbol(deaths - prevDeaths)} death`; + `${addNumberChangeSymbol( + getActiveCases(confirmed, recovered, deaths) - + getActiveCases(prevConfirmed, prevRecovered, prevDeaths) + )} active, ` + + `${addNumberChangeSymbol(recovered - prevRecovered)} recovered, ` + + `${addNumberChangeSymbol(deaths - prevDeaths)} death`; diff --git a/server/src/messages/feature/availableMessages.ts b/server/src/messages/feature/availableMessages.ts index e493199..a713ee2 100644 --- a/server/src/messages/feature/availableMessages.ts +++ b/server/src/messages/feature/availableMessages.ts @@ -2,13 +2,13 @@ import { Country } from '../../models/country.models'; import { getCountriesByContinent } from '../../services/domain/countries'; export const getShowCountriesMessage = (countries: Array): string => { - const availableFor: string = `Available for ${countries.length} countries around the 🌍.`; + const availableFor: string = `Available for ${countries.length} countries around the 🌍`; const countriesList: string = Object.entries( getCountriesByContinent(countries) ) - .map(([continentName, countries]: [string, Array]): string => + .map(([continentName, countries]: [string, Array]): string => `\n🗺️ ${continentName}, totally ${countries.length} countries\n`.concat( - countries.join('; ') + countries.map((country: Country) => country.name).join('; ') ) ) .join('\n'); diff --git a/server/src/messages/feature/countriesMessages.ts b/server/src/messages/feature/countriesMessages.ts index 1fb7658..f314eb5 100644 --- a/server/src/messages/feature/countriesMessages.ts +++ b/server/src/messages/feature/countriesMessages.ts @@ -1,5 +1,6 @@ -import { getActiveCases } from '../covid19Messages'; import { table, tableConfig } from '../../models/table.models'; +import { flag } from 'country-emoji'; +import { getActiveCases } from '../../services/domain/countries'; export const getCountriesWorldMessage = ( worldConfirmed: number, @@ -14,15 +15,37 @@ export const getCountriesWorldMessage = ( worldDeaths )} recovered: ${worldRecovered}, death: ${worldDeaths} in ${countries} countries, on ${continents} continents`; -export const getCountriesTableHTML = ({ +export const getTableHeader = (): Array => [ + 'Country', + 'Active', + 'Recovered', + 'Deaths', +]; + +export const getTableCountryRowMessage = ( + name, + active, + recovered, + deaths +): Array => [ + `${flag(name) ?? ''} ${name}`, + `${active}`, + `${recovered}`, + `${deaths}`, +]; + +export const getCountriesTableHTML = ( continent, - continentTotalConfirmed, - continentTotalRecovered, - continentTotalDeath, - portionMessage, -}): string => - `🗺️ ${continent}. Total active: ${continentTotalConfirmed}, recovered: ${continentTotalRecovered}, death: ${continentTotalDeath} - \n
${table(
-                                           portionMessage,
-                                           tableConfig
-                                       )}
`; + confirmed, + recovered, + deaths, + countriesSituation, + portionMessage +): string => + `🗺️ ${continent}\nConfirmed: ${confirmed}, active: ${getActiveCases( + confirmed, + recovered, + deaths + )} recovered: ${recovered}, death: ${deaths}` + + ` in ${countriesSituation.length} countries, ${continent}` + + `\n
${table(portionMessage, tableConfig)}
`; diff --git a/server/src/messages/feature/countryMessages.ts b/server/src/messages/feature/countryMessages.ts index 12bb57b..70ccae9 100644 --- a/server/src/messages/feature/countryMessages.ts +++ b/server/src/messages/feature/countryMessages.ts @@ -1,7 +1,6 @@ -import { Country, CountryMessage } from '../../models/country.models'; -import { getActiveCases } from '../covid19Messages'; -import { flag } from 'country-emoji'; import { UserRegExps } from '../../models/constants'; +import { getActiveCases } from '../../services/domain/countries'; +import { flag } from 'country-emoji'; export const getUserInputWithoutCountryNameMessage = (): string => `Sorry, but I can show country only by country name.` + @@ -11,38 +10,18 @@ export const getCountryNonExistenceErrorMessage = (country: string): string => `Sorry, but I cannot find anything for ${country}.` + ` I will save your request and will work on it`; -export const getTableCountryRowMessage = ({ - name, - confirmed, - recovered, - deaths, - lastUpdateDate, -}: CountryMessage): Array => [ - `${flag(name) ?? ''} ${name}`, - `${getActiveCases(confirmed, recovered, deaths)}`, - `${recovered}`, - `${deaths}`, -]; - -export const getTableHeader = (): Array => [ - 'Country', - 'Active', - 'Recovered', - 'Deaths', -]; - -export const getCountryMessage = ({ +export const getCountryMessage = ( name, confirmed, recovered, deaths, - lastUpdateDate, -}: CountryMessage): string => - `${flag(name) ?? ''} ${name}, ${getActiveCases( + date +): string => + `${flag(name) ?? ''} ${name}, ${confirmed} confirmed, ${getActiveCases( confirmed, recovered, deaths - )} active, ${recovered} recovered, ${deaths} deaths. ⏱️${lastUpdateDate}`; + )} active, ${recovered} recovered, ${deaths} deaths. ⏱️${date}`; export const getCountryIKActionMessage = (country: string): string => `More on ${country}`; diff --git a/server/src/messages/feature/subscribeMessages.ts b/server/src/messages/feature/subscribeMessages.ts index 2cf6893..437c7ff 100644 --- a/server/src/messages/feature/subscribeMessages.ts +++ b/server/src/messages/feature/subscribeMessages.ts @@ -46,13 +46,7 @@ export const showCountrySubscriptionMessage = ( ): string => { return ( `🔔 ` + - getCountryMessage({ - name, - confirmed, - recovered, - deaths, - lastUpdateDate: date, - }) + + getCountryMessage(name, confirmed, recovered, deaths, date) + `\n\n📈 Country change, since ⏱️${prevDate}\n` + getDiffMessage( { confirmed, recovered, deaths }, diff --git a/server/src/models/continent.models.ts b/server/src/models/continent.models.ts deleted file mode 100644 index 20c30c1..0000000 --- a/server/src/models/continent.models.ts +++ /dev/null @@ -1,3 +0,0 @@ -export interface ContinentsCountries { - [continentName: string]: Array; -} \ No newline at end of file diff --git a/server/src/models/country.models.ts b/server/src/models/country.models.ts index b75946b..08a0384 100644 --- a/server/src/models/country.models.ts +++ b/server/src/models/country.models.ts @@ -1,15 +1,7 @@ -import {UserPresentationalCountryNameString} from './tsTypes.models'; +import { UserPresentationalCountryNameString } from './tsTypes.models'; export interface Country { name: UserPresentationalCountryNameString; region: string; continent: string; } - -export interface CountryMessage { - name: string; - confirmed: number; - recovered: number; - deaths: number; - lastUpdateDate: string; -} \ No newline at end of file diff --git a/server/src/models/covid19.models.ts b/server/src/models/covid19.models.ts index edfaaff..fae42f0 100644 --- a/server/src/models/covid19.models.ts +++ b/server/src/models/covid19.models.ts @@ -1,4 +1,4 @@ -import {Country} from './country.models'; +import { Country } from './country.models'; export interface ApiCountriesCovid19Situation { [country: string]: Array; @@ -12,15 +12,28 @@ export interface ApiCovid19Situation { } export type CountrySituationInfo = Country & ApiCovid19Situation; +export type CountryActiveSituationInfo = CountrySituationInfo & { + active: number; +}; -export interface CountrySituation { - lastUpdateDate: string; - country: Country; +export interface ContinentsCountries { + [continentName: string]: Array; +} + +export interface ContinentCountriesSituations { + [continentName: string]: Array; +} + +export interface WorldOverallInformation { confirmed: number; recovered: number; deaths: number; + continentCountriesSituations: ContinentCountriesSituations; } -export interface ContinentCountriesSituation { - [continentName: string]: Array -} \ No newline at end of file +export interface ContinentOverallInformation { + confirmed: number; + recovered: number; + deaths: number; + countriesSituation: Array; +} diff --git a/server/src/services/domain/countries.ts b/server/src/services/domain/countries.ts index 40cf1c2..5ade29f 100644 --- a/server/src/services/domain/countries.ts +++ b/server/src/services/domain/countries.ts @@ -1,23 +1,35 @@ import { Country } from '../../models/country.models'; -import { ContinentsCountries } from '../../models/continent.models'; -import { CountrySituationInfo } from '../../models/covid19.models'; +import { + ContinentCountriesSituations, + ContinentOverallInformation, + ContinentsCountries, + CountryActiveSituationInfo, + CountrySituationInfo, + WorldOverallInformation, +} from '../../models/covid19.models'; import { getCountriesSituation } from './covid19'; import { getCountryNonExistenceErrorMessage } from '../../messages/feature/countryMessages'; +export const getActiveCases = ( + totalConfirmed: number, + totalRecovered: number, + totalDeaths: number +): number => totalConfirmed - totalRecovered - totalDeaths ?? 0; + export const getCountriesByContinent = ( countries: Array ): ContinentsCountries => { const continentsCountries: ContinentsCountries = {}; - countries.forEach(({ name, continent }: Country) => { - const continentCountries = continentsCountries[continent]; + countries.forEach((country: Country) => { + const continentCountries = continentsCountries[country.continent]; if (!!continentCountries?.length) { - continentCountries.push(name); + continentCountries.push(country); return; } - continentsCountries[continent] = [name]; + continentsCountries[country.continent] = [country]; }); return continentsCountries; @@ -49,3 +61,91 @@ export const getRequestedCountry = async ( return foundCountrySituations; }; + +export const getWorldOverallInformation = async (): Promise< + WorldOverallInformation +> => { + const countriesSituation: Array<[ + Country, + Array + ]> = await getCountriesSituation(); + + const continentCountriesSituations: ContinentCountriesSituations = {}; + let worldTotalConfirmed = 0; + let worldTotalRecovered = 0; + let worldTotalDeaths = 0; + + countriesSituation.forEach( + ([country, situations]: [Country, Array]) => { + const lastCountrySituation: CountrySituationInfo = + situations[situations.length - 1]; + const { confirmed, recovered, deaths } = lastCountrySituation; + + worldTotalConfirmed += confirmed; + worldTotalRecovered += recovered; + worldTotalDeaths += deaths; + + const prevCountriesResult = continentCountriesSituations[ + country.continent + ] + ? continentCountriesSituations[country.continent] + : []; + continentCountriesSituations[country.continent] = [ + ...prevCountriesResult, + lastCountrySituation, + ]; + } + ); + + return { + confirmed: worldTotalConfirmed, + recovered: worldTotalRecovered, + deaths: worldTotalDeaths, + continentCountriesSituations, + }; +}; + +export const getContinentOverallInformation = async ( + continent: string +): Promise => { + const allCountriesSituation: Array<[ + Country, + Array + ]> = await getCountriesSituation(); + + let countriesSituation: Array = []; + let continentTotalConfirmed: number = 0; + let continentTotalRecovered: number = 0; + let continentTotalDeath: number = 0; + + allCountriesSituation + .filter( + ([country, _]: [Country, Array]) => + country.continent === continent + ) + .forEach(([_, situations]: [Country, Array]) => { + const lastCountrySituationInfo: CountrySituationInfo = + situations[situations.length - 1]; + const { confirmed, recovered, deaths } = lastCountrySituationInfo; + const active = getActiveCases(confirmed, recovered, deaths); + + continentTotalConfirmed += confirmed; + continentTotalRecovered += recovered; + continentTotalDeath += deaths; + + countriesSituation = [ + ...countriesSituation, + { + ...lastCountrySituationInfo, + active, + }, + ]; + }); + + return { + confirmed: continentTotalConfirmed, + recovered: continentTotalRecovered, + deaths: continentTotalDeath, + countriesSituation, + }; +}; diff --git a/server/src/utils/featureHelpers/country.ts b/server/src/utils/featureHelpers/country.ts index aa7f539..ee2648c 100644 --- a/server/src/utils/featureHelpers/country.ts +++ b/server/src/utils/featureHelpers/country.ts @@ -1,6 +1,13 @@ -import {UpperCaseString, UserPresentationalCountryNameString} from '../../models/tsTypes.models'; +import { + UpperCaseString, + UserPresentationalCountryNameString, +} from '../../models/tsTypes.models'; -const countriesExceptionMap: Map = new Map([ +const countriesExceptionMap: Map = new Map< + UpperCaseString, + string +>([ + ['UNITED STATES', 'United States'], ['US', 'United States'], ['USA', 'United States'], ['RUSSIAN FEDERATION', 'Russia'], @@ -24,7 +31,12 @@ const countriesExceptionMap: Map = new Map +export const getCountryNameFormat = ( + country: UpperCaseString +): UserPresentationalCountryNameString => countriesExceptionMap.has(country) ? countriesExceptionMap.get(country) - : country.slice(0, 1).toLocaleUpperCase().concat(country.slice(1).toLocaleLowerCase()); + : country + .slice(0, 1) + .toLocaleUpperCase() + .concat(country.slice(1).toLocaleLowerCase()); From fd8e0486a74f77659496e6a15b5f62bb5aadefa1 Mon Sep 17 00:00:00 2001 From: Danylo Bilokha Date: Sun, 19 Apr 2020 17:38:19 +0300 Subject: [PATCH 7/9] fix comments from @Proladge --- .../telegram/botResponse/assistantResponse.ts | 6 +++--- .../bots/telegram/botResponse/countryResponse.ts | 6 +++--- .../telegram/botResponse/subscribeResponse.ts | 4 ++-- .../telegram/botResponse/unsubscribeResponse.ts | 6 +++--- server/src/bots/telegram/models/index.ts | 2 +- .../telegram/services/messageHandlerRegistry.ts | 16 ++++++++++------ 6 files changed, 22 insertions(+), 18 deletions(-) diff --git a/server/src/bots/telegram/botResponse/assistantResponse.ts b/server/src/bots/telegram/botResponse/assistantResponse.ts index 8a957a8..6f0d303 100644 --- a/server/src/bots/telegram/botResponse/assistantResponse.ts +++ b/server/src/bots/telegram/botResponse/assistantResponse.ts @@ -43,13 +43,13 @@ export const assistantStrategyResponse: CallBackQueryHandlerWithCommandArgument bot: TelegramBot, message: TelegramBot.Message, chatId: number, - parameterAfterCommand?: string + commandParameter?: string ): Promise => { - if (!parameterAfterCommand) { + if (!commandParameter) { return showAssistantFeatures(bot, message, chatId); } - const answers: Array = await fetchAnswer(parameterAfterCommand); + const answers: Array = await fetchAnswer(commandParameter); if (!answers.length) { return assistantNoAnswerResponse(bot, chatId); } diff --git a/server/src/bots/telegram/botResponse/countryResponse.ts b/server/src/bots/telegram/botResponse/countryResponse.ts index 309fee7..64c2202 100644 --- a/server/src/bots/telegram/botResponse/countryResponse.ts +++ b/server/src/bots/telegram/botResponse/countryResponse.ts @@ -23,9 +23,9 @@ export const showCountryByNameStrategyResponse: CallBackQueryHandlerWithCommandA bot: TelegramBot, message: TelegramBot.Message, chatId: number, - parameterAfterCommand?: string + commandParameter?: string ): Promise => { - if (!parameterAfterCommand) { + if (!commandParameter) { return bot.sendMessage(chatId, getUserInputWithoutCountryNameMessage()); } @@ -33,7 +33,7 @@ export const showCountryByNameStrategyResponse: CallBackQueryHandlerWithCommandA bot, message, chatId, - adaptCountryToSystemRepresentation(parameterAfterCommand) + adaptCountryToSystemRepresentation(commandParameter) ); }; diff --git a/server/src/bots/telegram/botResponse/subscribeResponse.ts b/server/src/bots/telegram/botResponse/subscribeResponse.ts index 209be8a..fc063b8 100644 --- a/server/src/bots/telegram/botResponse/subscribeResponse.ts +++ b/server/src/bots/telegram/botResponse/subscribeResponse.ts @@ -53,9 +53,9 @@ export const subscribingStrategyResponse: CallBackQueryHandlerWithCommandArgumen bot: TelegramBot, message: TelegramBot.Message, chatId: number, - parameterAfterCommand?: string + commandParameter?: string ): Promise => { - if (!parameterAfterCommand) { + if (!commandParameter) { return showExistingSubscriptionsResponse(bot, message, chatId); } diff --git a/server/src/bots/telegram/botResponse/unsubscribeResponse.ts b/server/src/bots/telegram/botResponse/unsubscribeResponse.ts index 8ef17ce..c088512 100644 --- a/server/src/bots/telegram/botResponse/unsubscribeResponse.ts +++ b/server/src/bots/telegram/botResponse/unsubscribeResponse.ts @@ -42,16 +42,16 @@ export const unsubscribeStrategyResponse: CallBackQueryHandlerWithCommandArgumen bot: TelegramBot, message: TelegramBot.Message, chatId: number, - parameterAfterCommand?: string + commandParameter?: string ): Promise => { // If it's called from InlineKeyboard, then @param ikCbData will be available // otherwise @param ikCbData will be null - if (!parameterAfterCommand) { + if (!commandParameter) { return buildUnsubscribeInlineResponse(bot, message, chatId); } const [err, result] = await catchAsyncError( - unsubscribeMeFrom(message.chat, parameterAfterCommand) + unsubscribeMeFrom(message.chat, commandParameter) ); if (err) { return bot.sendMessage(chatId, unSubscribeErrorMessage(err.message)); diff --git a/server/src/bots/telegram/models/index.ts b/server/src/bots/telegram/models/index.ts index a5a553c..77811fe 100644 --- a/server/src/bots/telegram/models/index.ts +++ b/server/src/bots/telegram/models/index.ts @@ -4,7 +4,7 @@ export type CallBackQueryHandlerWithCommandArgument = ( bot: TelegramBot, message: TelegramBot.Message, chatId: number, - parameterAfterCommand?: string + commandParameter?: string ) => Promise; export const TELEGRAM_PREFIX: string = 'telegram'; diff --git a/server/src/bots/telegram/services/messageHandlerRegistry.ts b/server/src/bots/telegram/services/messageHandlerRegistry.ts index 2ba070a..8e60c81 100644 --- a/server/src/bots/telegram/services/messageHandlerRegistry.ts +++ b/server/src/bots/telegram/services/messageHandlerRegistry.ts @@ -148,9 +148,11 @@ export class MessageHandlerRegistry { } } -// This function is wrapper around the original User's query handler -// It adds an additional parameter (if such exist) to original handler, -// which will be an parameter following after command +/** + * This function is wrapper around the original User's query handler + * It adds an additional parameter (if such exist) to original handler, + * which will be an parameter following after command + */ export const withSingleParameterAfterCommand = ( context: MessageHandlerRegistry, handlerFn: CallBackQueryHandlerWithCommandArgument @@ -186,8 +188,10 @@ export const withSingleParameterAfterCommand = ( }; }; -// Check out how it works here -// https://codepen.io/belokha/pen/xxwOdWg?editors=0012 +/** + * Check out how it works here + * https://codepen.io/belokha/pen/xxwOdWg?editors=0012 + */ function getParameterAfterCommandFromMessage( userFullInput: string | undefined ): string | undefined { @@ -205,7 +209,7 @@ function getParameterAfterCommandFromMessage( )})\\s(?.*)` ).exec(makeMagicOverUserFullInput); if (!execResult) { - logger.log('info', getInfoMessage('Entered unsupported command')); + logger.log('warn', getInfoMessage('Entered unsupported command')); return undefined; } From 5fdabfc059eb62a0c962acabe4cc103301743997 Mon Sep 17 00:00:00 2001 From: Danylo Bilokha Date: Sun, 19 Apr 2020 17:59:51 +0300 Subject: [PATCH 8/9] fix subscriptions.ts --- .../botResponse/unsubscribeResponse.ts | 3 +- server/src/bots/telegram/services/keyboard.ts | 10 ++++-- server/src/models/constants.ts | 2 +- server/src/services/domain/subscriptions.ts | 35 ++++++++++++++----- server/src/utils/featureHelpers/country.ts | 2 +- 5 files changed, 37 insertions(+), 15 deletions(-) diff --git a/server/src/bots/telegram/botResponse/unsubscribeResponse.ts b/server/src/bots/telegram/botResponse/unsubscribeResponse.ts index c088512..cfe9126 100644 --- a/server/src/bots/telegram/botResponse/unsubscribeResponse.ts +++ b/server/src/bots/telegram/botResponse/unsubscribeResponse.ts @@ -14,6 +14,7 @@ import { noSubscriptionsResponseMessage } from '../../../messages/feature/subscr import { getTelegramActiveUserSubscriptions } from '../services/storage'; import * as TelegramBot from 'node-telegram-bot-api'; import { CallBackQueryHandlerWithCommandArgument } from '../models'; +import { adaptCountryToSystemRepresentation } from '../../../services/domain/covid19'; export const buildUnsubscribeInlineResponse = async ( bot: TelegramBot, @@ -59,7 +60,7 @@ export const unsubscribeStrategyResponse: CallBackQueryHandlerWithCommandArgumen return bot.sendMessage( chatId, - unsubscribeResultMessage(result), + unsubscribeResultMessage(adaptCountryToSystemRepresentation(result)), getFullMenuKeyboard(chatId) ); }; diff --git a/server/src/bots/telegram/services/keyboard.ts b/server/src/bots/telegram/services/keyboard.ts index 324714b..7d29f44 100644 --- a/server/src/bots/telegram/services/keyboard.ts +++ b/server/src/bots/telegram/services/keyboard.ts @@ -11,7 +11,9 @@ import * as TelegramBot from 'node-telegram-bot-api'; export const getFullMenuKeyboard = (chatId): TelegramBot.SendMessageOptions => { const rk = new ReplyKeyboard(); - const latestSelectedCountries: Array = Cache.get(`${chatId}_commands_country`); + const latestSelectedCountries: Array = Cache.get( + `${chatId}_commands_country` + ); if (latestSelectedCountries.length > 0) { rk.addRow.apply(rk, latestSelectedCountries); @@ -30,7 +32,7 @@ export const getAfterCountryResponseInlineKeyboard = ( const ik = new InlineKeyboard(); ik.addRow({ text: `${CustomSubscriptions.SubscribeMeOn} ${country}`, - callback_data: `${CustomSubscriptions.SubscribeMeOn} ${country}`, + callback_data: `${UserRegExps.Subscribe} ${country}`, }).addRow({ text: 'Show weekly chart', callback_data: `${UserRegExps.Trends} ${country}`, @@ -67,7 +69,9 @@ export const getUnsubscribeMessageInlineKeyboard = ( while (!!values[i] && rowItem < UNSUBSCRIPTIONS_ROW_ITEMS_NUMBER) { rows.push({ text: values[i], - callback_data: `${CustomSubscriptions.UnsubscribeMeFrom} ${values[i++]}`, + callback_data: `${CustomSubscriptions.UnsubscribeMeFrom} ${ + values[i++] + }`, }); rowItem += 1; } diff --git a/server/src/models/constants.ts b/server/src/models/constants.ts index 51436e6..5017d24 100644 --- a/server/src/models/constants.ts +++ b/server/src/models/constants.ts @@ -9,7 +9,7 @@ export const CONSOLE_LOG_EASE_DELIMITER: string = '==============> '; export const CONSOLE_LOG_DELIMITER: string = '\n\n==============> '; export enum CustomSubscriptions { - SubscribeMeOn = `Subscribe on`, + SubscribeMeOn = `Subscribe me on`, UnsubscribeMeFrom = `Unsubscribe me from`, } diff --git a/server/src/services/domain/subscriptions.ts b/server/src/services/domain/subscriptions.ts index b472992..8575aa0 100644 --- a/server/src/services/domain/subscriptions.ts +++ b/server/src/services/domain/subscriptions.ts @@ -4,7 +4,10 @@ import { getTelegramUserSubscriptions, setTelegramSubscription, } from '../../bots/telegram/services/storage'; -import { Subscription, SubscriptionType } from '../../models/subscription.models'; +import { + Subscription, + SubscriptionType, +} from '../../models/subscription.models'; import { catchAsyncError } from '../../utils/catchError'; import { ALREADY_SUBSCRIBED_MESSAGE } from '../../messages/feature/subscribeMessages'; import { CountrySituationInfo } from '../../models/covid19.models'; @@ -28,7 +31,8 @@ export const subscribeOn = async ( Array ] = availableCountries.find( ([country, _]: [Country, Array]) => - country.name.toLocaleLowerCase() === subscribeMeOn.toLocaleLowerCase() + country.name.toLocaleLowerCase() === + subscribeMeOn.toLocaleLowerCase() ); if (!subscribeMeOnCountry) { @@ -37,11 +41,14 @@ export const subscribeOn = async ( // TODO: Remove Telegram dependency const existingSubscriptions: Array = - ((await getTelegramUserSubscriptions(chat.id)) ?? {}).subscriptionsOn ?? []; + ((await getTelegramUserSubscriptions(chat.id)) ?? {}).subscriptionsOn ?? + []; const checkIfAlreadySubscribed = existingSubscriptions .filter((subscription: Subscription) => subscription.active) - .find((subscription: Subscription) => subscription.value === subscribeMeOn); + .find( + (subscription: Subscription) => subscription.value === subscribeMeOn + ); if (!!checkIfAlreadySubscribed) { // TODO: it's not actually error, re-write it be not an error throw new Error(`${ALREADY_SUBSCRIBED_MESSAGE}`); @@ -55,7 +62,8 @@ export const subscribeOn = async ( active: true, type: SubscriptionType.Country, value: subscribeMeOnCountry.name, - lastReceivedData: countrySituations[countrySituations.length - 1], + lastReceivedData: + countrySituations[countrySituations.length - 1], lastUpdate: Date.now(), }, ], @@ -70,11 +78,12 @@ export const unsubscribeMeFrom = async ( ): Promise => { // TODO: Remove Telegram dependency const existingSubscriptions: Array = - ((await getTelegramUserSubscriptions(chat.id)) ?? {}).subscriptionsOn ?? []; + ((await getTelegramUserSubscriptions(chat.id)) ?? {}).subscriptionsOn ?? + []; let foundSubscription: Subscription; const updatedSubscriptions: Array = existingSubscriptions.map( (subscription: Subscription) => { - if (subscription.value === unsubscribeMeFrom) { + if (subscription.value.toLocaleLowerCase() === unsubscribeMeFrom) { foundSubscription = subscription; subscription.active = false; } @@ -103,7 +112,15 @@ export const unsubscribeMeFrom = async ( export const isCountrySituationHasChangedSinceLastData = ( { confirmed, deaths, recovered }: CountrySituationInfo, - { confirmed: prevConfirmed, deaths: prevDeaths, recovered: prevRecovered }: CountrySituationInfo + { + confirmed: prevConfirmed, + deaths: prevDeaths, + recovered: prevRecovered, + }: CountrySituationInfo ): boolean => { - return confirmed !== prevConfirmed || deaths !== prevDeaths || recovered !== prevRecovered; + return ( + confirmed !== prevConfirmed || + deaths !== prevDeaths || + recovered !== prevRecovered + ); }; diff --git a/server/src/utils/featureHelpers/country.ts b/server/src/utils/featureHelpers/country.ts index ee2648c..5f07ab9 100644 --- a/server/src/utils/featureHelpers/country.ts +++ b/server/src/utils/featureHelpers/country.ts @@ -34,7 +34,7 @@ const countriesExceptionMap: Map = new Map< export const getCountryNameFormat = ( country: UpperCaseString ): UserPresentationalCountryNameString => - countriesExceptionMap.has(country) + countriesExceptionMap.has(country.toLocaleUpperCase()) ? countriesExceptionMap.get(country) : country .slice(0, 1) From da02c4a9fb3cae9c3cb4cfe8c8ca6a08fc6e0126 Mon Sep 17 00:00:00 2001 From: Danylo Bilokha Date: Sun, 19 Apr 2020 18:28:00 +0300 Subject: [PATCH 9/9] update README.md and example file for development run --- CONTRIBUTING.md | 13 +++++++++---- server/src/bots/telegram/index.ts | 15 ++++++++------- server/src/environments/.env.example | 25 +++++++++++++------------ server/src/environments/README.md | 7 +++++++ server/src/index.ts | 21 ++++++++++++++++----- 5 files changed, 53 insertions(+), 28 deletions(-) create mode 100644 server/src/environments/README.md diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 65bc1b1..972f039 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -58,11 +58,16 @@ on Windows 10, x64 machine - Open BotFather, - Create your development version of the bot (for your local testing purpose) via `/newbot` command - Receive from BotFather Key & Copy it, -- Create `.env` in `/server/src/bots/telegram` file and add received **key** there (more about it, example of the file), +- Create `.env` in `/server/src/bots/telegram` file and add received **key** there (more about it, example of the file), - Add to the project, - run `npm i` -- run `npm run start:watch` +- run `npm run start:watch`, `npm run start:inspect` for debugging +(Useful link) +Hints +- If you\'re running locally, you can run `ngrok http 3000` copy `https url` +paste to `APP_URL` +env variable and reloading will be faster and cheaper for CPU ##### If any questions and/or issues, - contact any collaborator, @@ -71,6 +76,6 @@ on Windows 10, x64 machine ### Restrictions and Limitations For running the application we need to have secure connection (as some all known messengers -do not support running via http) thus we are using **ngrok** library for that currently. It -creates public domain with secured connection with we are using for redirecting all messages +do not support running via http) thus we are using **ngrok** library +for that, currently. It creates public domain with secured connection with we are using for redirecting all messages to our local machines. diff --git a/server/src/bots/telegram/index.ts b/server/src/bots/telegram/index.ts index 76afbb2..752c61c 100644 --- a/server/src/bots/telegram/index.ts +++ b/server/src/bots/telegram/index.ts @@ -23,7 +23,6 @@ import { Country } from '../../models/country.models'; import { flag } from 'country-emoji'; import { assistantStrategyResponse } from './botResponse/assistantResponse'; import * as TelegramBot from 'node-telegram-bot-api'; -import Config from '../../environments/environment'; import { logger } from '../../utils/logger'; import { startResponse } from './botResponse/startResponse'; import { showAvailableCountriesResponse } from './botResponse/availableResponse'; @@ -42,14 +41,18 @@ import { catchAsyncError } from '../../utils/catchError'; import { LogglyTypes } from '../../models/loggly.models'; import { getErrorMessage } from '../../utils/getErrorMessages'; -function runTelegramBot(app: Express, appUrl: string) { +export function runTelegramBot( + app: Express, + appUrl: string, + telegramToken: string +) { // Create a bot that uses 'polling' to fetch new updates - const bot = new TelegramBot(Config.TELEGRAM_TOKEN, { polling: true }); + const bot = new TelegramBot(telegramToken, { polling: true }); // This informs the Telegram servers of the new webhook - bot.setWebHook(`${appUrl}/bot${Config.TELEGRAM_TOKEN}`); + bot.setWebHook(`${appUrl}/bot${telegramToken}`); // We are receiving updates at the route below! - app.post(`/bot${Config.TELEGRAM_TOKEN}`, (req, res) => { + app.post(`/bot${telegramToken}`, (req, res) => { bot.processUpdate(req.body); res.sendStatus(200); }); @@ -161,5 +164,3 @@ function runTelegramBot(app: Express, appUrl: string) { bot.on('webhook_error', (err) => logger.log('error', err)); bot.on('error', (err) => logger.log('error', err)); } - -export { runTelegramBot }; diff --git a/server/src/environments/.env.example b/server/src/environments/.env.example index 45ba110..b721fe1 100644 --- a/server/src/environments/.env.example +++ b/server/src/environments/.env.example @@ -1,17 +1,18 @@ +#env example file COUNTRIESDATA_URL='https://pomber.github.io/covid19' KNOWLEDGEBASE_URL='' KNOWLEDGEBASE_SECRET_KEY='JWT token for knowledgebase' TELEGRAM_TOKEN='Token from BotFather' -CONTAINER_VERSION='' -LOGGLY_TOKEN='Token for loggy' +CONTAINER_VERSION='For information purposes' +LOGGLY_TOKEN='Logs token' LOGGLY_SUBDOMAIN='covid19liveupd' -LOGGLY_TAGS='Setup in Heroku interface' -APP_URL='Setup in Heroku interface' -NGROK_URL='Automatically setup for development purposes' -FIREBASE_API_KEY='firebase' -FIREBASE_AUTHDOMAIN='firebase' -FIREBASE_DATABASE_URL='firebase' -FIREBASE_PROJECT_ID='firebase' -FIREBASE_STORAGE_BUCKET='firebase' -FIREBASE_MESSAGING_SENDER_ID='firebase' -FIREBASE_APP_ID='firebase' +LOGGLY_TAGS='Logs, setup in Heroku interface' +APP_URL='If you\'re running locally, you can run `ngrok http 3000` cope past https url here and reloading will be faster and cheaper for CPU' +NGROK_URL='Automatically setup for development purposes, should be empty for development purposes' +FIREBASE_API_KEY="AIzaSyAHLUsFC7krb4cPctWeJYAGufQi6cpnlRU" +FIREBASE_AUTHDOMAIN="covid19liveupdates-dev.firebaseapp.com" +FIREBASE_DATABASE_URL="https://covid19liveupdates-dev.firebaseio.com" +FIREBASE_PROJECT_ID="covid19liveupdates-dev" +FIREBASE_STORAGE_BUCKET="covid19liveupdates-dev.appspot.com" +FIREBASE_MESSAGING_SENDER_ID="476819025765" +FIREBASE_APP_ID="1:476819025765:web:5e918bbb729e261a937b8c" diff --git a/server/src/environments/README.md b/server/src/environments/README.md new file mode 100644 index 0000000..7627b29 --- /dev/null +++ b/server/src/environments/README.md @@ -0,0 +1,7 @@ +To run either for development, demo or production you need setup a few environment variables. +Example file. + +If you believe that you should have -demo and/or production +env's, do reach out Vladimir +or Danylo. Another option is to +take a look on Heroku. diff --git a/server/src/index.ts b/server/src/index.ts index ab722c8..ca30501 100644 --- a/server/src/index.ts +++ b/server/src/index.ts @@ -5,7 +5,10 @@ import { runTelegramBot } from './bots/telegram'; import { runNgrok, stopNgrok } from './runNgrok'; import environments from './environments/environment'; import { initFirebase } from './services/infrastructure/firebase'; -import { CONSOLE_LOG_DELIMITER, CONSOLE_LOG_EASE_DELIMITER } from './models/constants'; +import { + CONSOLE_LOG_DELIMITER, + CONSOLE_LOG_EASE_DELIMITER, +} from './models/constants'; import * as firebase from 'firebase'; import { checkCovid19Updates } from './services/infrastructure/scheduler'; import { catchAsyncError } from './utils/catchError'; @@ -21,14 +24,22 @@ app.get('/', baseController.base); const server = app.listen(PORT, async () => { const appUrl = environments.APP_URL; // tslint:disable-next-line:no-console - console.log('App is running at http://localhost:%d in %s mode', PORT, environmentName); + console.log( + 'App is running at http://localhost:%d in %s mode', + PORT, + environmentName + ); if (environments.IsNgRokMode()) { const [err, appUrl] = await catchAsyncError( - environments.NGROK_URL ? Promise.resolve(environments.NGROK_URL) : runNgrok(PORT) + environments.NGROK_URL + ? Promise.resolve(environments.NGROK_URL) + : runNgrok(PORT) ); // tslint:disable-next-line:no-console - console.log(`${CONSOLE_LOG_EASE_DELIMITER} NGROK started on ngRokUrl: ${appUrl}`); + console.log( + `${CONSOLE_LOG_EASE_DELIMITER} NGROK started on ngRokUrl: ${appUrl}` + ); } const [e, isFirebaseInit] = initFirebase(environments); @@ -42,7 +53,7 @@ const server = app.listen(PORT, async () => { checkCovid19Updates(); // tslint:disable-next-line:no-console console.log(`${CONSOLE_LOG_DELIMITER}Starting Telegram bot`); - runTelegramBot(app, appUrl); + runTelegramBot(app, appUrl, environments.TELEGRAM_TOKEN); }); process.on('SIGTERM', () => {