From 145f4b6749c1f52b25780297e4fc8839ea7520d5 Mon Sep 17 00:00:00 2001 From: Felix Krause Date: Tue, 30 Apr 2019 12:56:30 +0200 Subject: [PATCH] Refactor bot to use separate source files --- Procfile | 3 +- README.md | 9 + classes/config.js | 5 + classes/config.ts | 20 ++ classes/google_sheets.js | 61 ++++ classes/google_sheets.ts | 73 +++++ classes/telegram.js | 6 + classes/telegram.ts | 7 + package.json | 11 +- reminder.js | 76 +++++ reminder.ts | 92 ++++++ web.js | 48 +++ web.ts | 58 ++++ app.js => worker.js | 485 +++++++++++-------------------- app.ts => worker.ts | 612 +++++++++++++-------------------------- 15 files changed, 836 insertions(+), 730 deletions(-) create mode 100644 classes/config.js create mode 100644 classes/config.ts create mode 100644 classes/google_sheets.js create mode 100644 classes/google_sheets.ts create mode 100644 classes/telegram.js create mode 100644 classes/telegram.ts create mode 100644 reminder.js create mode 100644 reminder.ts create mode 100644 web.js create mode 100644 web.ts rename app.js => worker.js (72%) rename app.ts => worker.ts (71%) diff --git a/Procfile b/Procfile index 403c9db..d033372 100644 --- a/Procfile +++ b/Procfile @@ -1 +1,2 @@ -web: npm run prod +web: npm run web +worker: npm run worker diff --git a/README.md b/README.md index 047b738..75c8619 100644 --- a/README.md +++ b/README.md @@ -234,6 +234,7 @@ After using `npm run dev`, open [chrome://inspect](chrome://inspect) to use the ``` export TELEGRAM_BOT_TOKEN="" export TELEGRAM_USER_ID="" +export TELEGRAM_CHAT_ID="" export GOOGLE_SHEETS_DOC_ID="" export GOOGLE_SHEETS_CLIENT_EMAIL="" @@ -245,6 +246,14 @@ export WEATHER_API_KEY="" The `GOOGLE_SHEETS_PRIVATE_KEY` has to start with `-----BEGIN PRIVATE KEY-----\\n...`, with the `...` being the first characters of the private key. All the new lines have to be escaped to be `\\n`, and the ending has to be `=\\n-----END PRIVATE KEY-----\\n` +### Scheduler + +Use the Heroku scheduler, and set it to run every hour to remind you to run certain commands according to the defined schedule (`weekly`, etc) + +``` +npm run reminder +``` + ### Spreadsheet Long term, I'll provide a template. For now, the following options need to be met: diff --git a/classes/config.js b/classes/config.js new file mode 100644 index 0000000..8323402 --- /dev/null +++ b/classes/config.js @@ -0,0 +1,5 @@ +"use strict"; +exports.__esModule = true; +var userConfig = require("../lifesheet.json"); +console.log("Successfully loaded user config"); +module.exports.userConfig = userConfig; diff --git a/classes/config.ts b/classes/config.ts new file mode 100644 index 0000000..b5a4975 --- /dev/null +++ b/classes/config.ts @@ -0,0 +1,20 @@ +// Interfaces +interface Command { + description: String; + schedule: String; + questions: [QuestionToAsk]; +} + +interface QuestionToAsk { + key: String; + question: String; + type: String; + buttons: { [key: string]: String }; + replies: { [key: string]: String }; +} + +let userConfig: { [key: string]: Command } = require("../lifesheet.json"); +console.log("Successfully loaded user config"); + +module.exports.userConfig = userConfig; +export { Command, QuestionToAsk }; diff --git a/classes/google_sheets.js b/classes/google_sheets.js new file mode 100644 index 0000000..848d592 --- /dev/null +++ b/classes/google_sheets.js @@ -0,0 +1,61 @@ +"use strict"; +exports.__esModule = true; +// Sheets setup +var GoogleSpreadsheet = require("google-spreadsheet"); +var async = require("async"); +// spreadsheet key is the long id in the sheets URL +console.log("Loading " + process.env.GOOGLE_SHEETS_DOC_ID); +var doc = new GoogleSpreadsheet(process.env.GOOGLE_SHEETS_DOC_ID); +var rawDataSheet; +var lastRunSheet; +function setupGoogleSheets(callback) { + console.log("Starting Google Sheets Login, this might take a few seconds..."); + async.series([ + function setAuth(step) { + var creds = { + client_email: process.env.GOOGLE_SHEETS_CLIENT_EMAIL, + private_key: process.env.GOOGLE_SHEETS_PRIVATE_KEY.replace(/\\n/g, "\n") + }; + doc.useServiceAccountAuth(creds, step); + }, + function getInfoAndWorksheets(step) { + doc.getInfo(function (error, info) { + if (error) { + console.error(error); + } + console.log("Loaded doc: " + info.title + " by " + info.author.email); + for (var i = 0; i < info.worksheets.length; i++) { + // we iterate over those and check the name, as we don't want to use indexes to access a sheet directly + // to allow the user to order those sheets to however they want + var currentSheet = info.worksheets[i]; + if (currentSheet.title.toLowerCase() == "rawdata") { + rawDataSheet = currentSheet; + } + else if (currentSheet.title.toLowerCase() == "lastrun") { + lastRunSheet = currentSheet; + } + else { + console.log("Ignoring user's sheet named " + currentSheet.title); + } + } + if (rawDataSheet == null || lastRunSheet == null) { + console.error("Something is wrong with the Sheet, please make sure to create 2 sheets: RawData and LastRun, according to the project's README and try again"); + return; + } + console.log("Found the relevant sheets, already " + + rawDataSheet.rowCount + + " existing data entries"); + step(); + }); + } + ], function (err) { + if (err) { + console.log("Error: " + err); + } + else { + console.log("Google login successful"); + callback(rawDataSheet, lastRunSheet); + } + }); +} +module.exports.setupGoogleSheets = setupGoogleSheets; diff --git a/classes/google_sheets.ts b/classes/google_sheets.ts new file mode 100644 index 0000000..01f0e89 --- /dev/null +++ b/classes/google_sheets.ts @@ -0,0 +1,73 @@ +// Sheets setup +var GoogleSpreadsheet = require("google-spreadsheet"); +var async = require("async"); + +// spreadsheet key is the long id in the sheets URL +console.log("Loading " + process.env.GOOGLE_SHEETS_DOC_ID); +var doc = new GoogleSpreadsheet(process.env.GOOGLE_SHEETS_DOC_ID); +var rawDataSheet; +var lastRunSheet; + +function setupGoogleSheets(callback) { + console.log("Starting Google Sheets Login, this might take a few seconds..."); + + async.series( + [ + function setAuth(step) { + var creds = { + client_email: process.env.GOOGLE_SHEETS_CLIENT_EMAIL, + private_key: process.env.GOOGLE_SHEETS_PRIVATE_KEY.replace( + /\\n/g, + "\n" + ) + }; + + doc.useServiceAccountAuth(creds, step); + }, + function getInfoAndWorksheets(step) { + doc.getInfo(function(error, info) { + if (error) { + console.error(error); + } + console.log("Loaded doc: " + info.title + " by " + info.author.email); + for (let i = 0; i < info.worksheets.length; i++) { + // we iterate over those and check the name, as we don't want to use indexes to access a sheet directly + // to allow the user to order those sheets to however they want + let currentSheet = info.worksheets[i]; + if (currentSheet.title.toLowerCase() == "rawdata") { + rawDataSheet = currentSheet; + } else if (currentSheet.title.toLowerCase() == "lastrun") { + lastRunSheet = currentSheet; + } else { + console.log("Ignoring user's sheet named " + currentSheet.title); + } + } + if (rawDataSheet == null || lastRunSheet == null) { + console.error( + "Something is wrong with the Sheet, please make sure to create 2 sheets: RawData and LastRun, according to the project's README and try again" + ); + return; + } + + console.log( + "Found the relevant sheets, already " + + rawDataSheet.rowCount + + " existing data entries" + ); + step(); + }); + } + ], + function(err) { + if (err) { + console.log("Error: " + err); + } else { + console.log("Google login successful"); + callback(rawDataSheet, lastRunSheet); + } + } + ); +} + +module.exports.setupGoogleSheets = setupGoogleSheets; +export {}; diff --git a/classes/telegram.js b/classes/telegram.js new file mode 100644 index 0000000..82fe053 --- /dev/null +++ b/classes/telegram.js @@ -0,0 +1,6 @@ +"use strict"; +exports.__esModule = true; +// Telegram setup +var Telegraf = require("telegraf"); +var bot = new Telegraf(process.env.TELEGRAM_BOT_TOKEN); +module.exports.bot = bot; diff --git a/classes/telegram.ts b/classes/telegram.ts new file mode 100644 index 0000000..2ff06a1 --- /dev/null +++ b/classes/telegram.ts @@ -0,0 +1,7 @@ +// Telegram setup +const Telegraf = require("telegraf"); + +const bot = new Telegraf(process.env.TELEGRAM_BOT_TOKEN); + +module.exports.bot = bot; +export {}; diff --git a/package.json b/package.json index 0297b71..0b0e690 100644 --- a/package.json +++ b/package.json @@ -1,8 +1,13 @@ { "scripts": { - "prettier": "prettier app.ts --write", - "dev": "source .keys && prettier app.ts --write && tsc app.ts && node --inspect app.js", - "prod": "prettier app.ts --write && tsc app.ts && NODE_ENV=production node app.js" + "prettier": "prettier worker.ts web.ts reminder.ts classes/*.ts --write", + "typescript": "tsc web.ts worker.ts reminder.ts classes/google_sheets.ts classes/telegram.ts classes/config.ts", + + "dev": "source .keys && npm run prettier && npm run typescript && heroku local", + + "web": "npm run prettier && npm run typescript && NODE_ENV=production node web.js", + "worker": "npm run prettier && npm run typescript && NODE_ENV=production node worker.js", + "reminder": "npm run prettier && npm run typescript && NODE_ENV=production node reminder.js" }, "dependencies": { "google-spreadsheet": "^2.0.7", diff --git a/reminder.js b/reminder.js new file mode 100644 index 0000000..d807eb2 --- /dev/null +++ b/reminder.js @@ -0,0 +1,76 @@ +"use strict"; +exports.__esModule = true; +// Third party dependencies +var moment = require("moment"); +// Internal dependencies +var config = require("./classes/config.js"); +var google_sheets = require("./classes/google_sheets.js"); +var telegram = require("./classes/telegram.js"); +google_sheets.setupGoogleSheets(runReminders); +function runReminders(rawDataSheet, lastRunSheet) { + console.log("Checking if we need to send a reminder to the user..."); + lastRunSheet.getRows({ + offset: 1, + limit: 100 + }, function (error, rows) { + for (var i = 0; i < rows.length; i++) { + var currentRow = rows[i]; + var command = currentRow.command; + var lastRun = moment(Number(currentRow.lastrun)); + if (config.userConfig[command] == null) { + console.error("Error, command not found, means it's not on the last run sheet, probably due to renaming a command: " + + command); + break; + } + var scheduleType = config.userConfig[command].schedule; + var timeDifferenceHours = moment().diff(moment(lastRun), "hours"); // hours + var shouldRemindUser = false; + if (scheduleType == "fourTimesADay") { + if (timeDifferenceHours >= 24 / 4) { + shouldRemindUser = true; + } + } + else if (scheduleType == "daily") { + if (timeDifferenceHours >= 24 * 1.1) { + shouldRemindUser = true; + } + } + else if (scheduleType == "weekly") { + if (timeDifferenceHours >= 24 * 7 * 1.05) { + shouldRemindUser = true; + } + } + else if (scheduleType == "monthly") { + if (timeDifferenceHours >= 24 * 30 * 1.05) { + shouldRemindUser = true; + } + } + else if (scheduleType == "manual") { + // Never remind the user + } + else { + console.error("Unknown schedule type " + scheduleType); + } + console.log(command + + ": " + + lastRun.format() + + " should remind? " + + shouldRemindUser + + " time diff in h " + + timeDifferenceHours); + if (shouldRemindUser) { + console.log("Reminding user to run questions again..."); + var textToSend = "Please run /" + + command + + " again, it's been " + + timeDifferenceHours + + " hours since you last filled it out"; + if (process.env.TELEGRAM_CHAT_ID == null) { + console.error("Please set the `TELEGRAM_CHAT_ID` ENV variable"); + } + telegram.bot.telegram.sendMessage(process.env.TELEGRAM_CHAT_ID, textToSend); + } + } + console.log("Reminder check done"); + }); +} diff --git a/reminder.ts b/reminder.ts new file mode 100644 index 0000000..89ce5d7 --- /dev/null +++ b/reminder.ts @@ -0,0 +1,92 @@ +// Third party dependencies +const moment = require("moment"); + +// Internal dependencies +let config = require("./classes/config.js"); +let google_sheets = require("./classes/google_sheets.js"); +let telegram = require("./classes/telegram.js"); + +google_sheets.setupGoogleSheets(runReminders); + +function runReminders(rawDataSheet, lastRunSheet) { + console.log("Checking if we need to send a reminder to the user..."); + + lastRunSheet.getRows( + { + offset: 1, + limit: 100 + }, + function(error, rows) { + for (let i = 0; i < rows.length; i++) { + let currentRow = rows[i]; + let command = currentRow.command; + let lastRun = moment(Number(currentRow.lastrun)); + + if (config.userConfig[command] == null) { + console.error( + "Error, command not found, means it's not on the last run sheet, probably due to renaming a command: " + + command + ); + break; + } + + let scheduleType = config.userConfig[command].schedule; + let timeDifferenceHours = moment().diff(moment(lastRun), "hours"); // hours + var shouldRemindUser = false; + + if (scheduleType == "fourTimesADay") { + if (timeDifferenceHours >= 24 / 4) { + shouldRemindUser = true; + } + } else if (scheduleType == "daily") { + if (timeDifferenceHours >= 24 * 1.1) { + shouldRemindUser = true; + } + } else if (scheduleType == "weekly") { + if (timeDifferenceHours >= 24 * 7 * 1.05) { + shouldRemindUser = true; + } + } else if (scheduleType == "monthly") { + if (timeDifferenceHours >= 24 * 30 * 1.05) { + shouldRemindUser = true; + } + } else if (scheduleType == "manual") { + // Never remind the user + } else { + console.error("Unknown schedule type " + scheduleType); + } + + console.log( + command + + ": " + + lastRun.format() + + " should remind? " + + shouldRemindUser + + " time diff in h " + + timeDifferenceHours + ); + + if (shouldRemindUser) { + console.log("Reminding user to run questions again..."); + let textToSend = + "Please run /" + + command + + " again, it's been " + + timeDifferenceHours + + " hours since you last filled it out"; + + if (process.env.TELEGRAM_CHAT_ID == null) { + console.error("Please set the `TELEGRAM_CHAT_ID` ENV variable"); + } + telegram.bot.telegram.sendMessage( + process.env.TELEGRAM_CHAT_ID, + textToSend + ); + } + } + console.log("Reminder check done"); + } + ); +} + +export {}; diff --git a/web.js b/web.js new file mode 100644 index 0000000..234d819 --- /dev/null +++ b/web.js @@ -0,0 +1,48 @@ +"use strict"; +exports.__esModule = true; +// Third party dependencies +var http = require("http"); +var moment = require("moment"); +// Internal dependencies +var google_sheets = require("./classes/google_sheets.js"); +// State +var lastMoodData = null; // used for the moods API +var currentMood = null; +var rawDataSheetRef = null; +google_sheets.setupGoogleSheets(initMoodAPI); +function initMoodAPI(rawDataSheet, lastRunSheet) { + rawDataSheetRef = rawDataSheet; + console.log("Launching up API web server..."); + // Periodically refresh the value + setInterval(loadCurrentMood, 5 * 60 * 1000); + loadCurrentMood(); + http + .createServer(function (req, res) { + res.writeHead(200, { "Content-Type": "application/json" }); + res.write(JSON.stringify(lastMoodData)); + return res.end(); + }) + .listen(process.env.PORT); +} +function loadCurrentMood() { + console.log("Refreshing latest moood entry..."); + currentMood = rawDataSheetRef.getRows({ + offset: 0, + limit: 1, + orderby: "timestamp", + reverse: true, + query: "key=mood" + }, function (error, rows) { + if (error) { + console.error(error); + } + var lastMoodRow = rows[0]; + // `lastMoodRow` is null if we haven't tracked a mood yet + if (lastMoodRow != null) { + lastMoodData = { + time: moment(Number(lastMoodRow.timestamp)).format(), + value: Number(lastMoodRow.value) + }; + } + }); +} diff --git a/web.ts b/web.ts new file mode 100644 index 0000000..2517968 --- /dev/null +++ b/web.ts @@ -0,0 +1,58 @@ +// Third party dependencies +var http = require("http"); +const moment = require("moment"); + +// Internal dependencies +let google_sheets = require("./classes/google_sheets.js"); + +// State +var lastMoodData = null; // used for the moods API +let currentMood = null; +let rawDataSheetRef = null; + +google_sheets.setupGoogleSheets(initMoodAPI); + +function initMoodAPI(rawDataSheet, lastRunSheet) { + rawDataSheetRef = rawDataSheet; + console.log("Launching up API web server..."); + + // Periodically refresh the value + setInterval(loadCurrentMood, 5 * 60 * 1000); + loadCurrentMood(); + + http + .createServer(function(req, res) { + res.writeHead(200, { "Content-Type": "application/json" }); + res.write(JSON.stringify(lastMoodData)); + return res.end(); + }) + .listen(process.env.PORT); +} + +function loadCurrentMood() { + console.log("Refreshing latest moood entry..."); + currentMood = rawDataSheetRef.getRows( + { + offset: 0, + limit: 1, + orderby: "timestamp", + reverse: true, + query: "key=mood" + }, + function(error, rows) { + if (error) { + console.error(error); + } + let lastMoodRow = rows[0]; + // `lastMoodRow` is null if we haven't tracked a mood yet + if (lastMoodRow != null) { + lastMoodData = { + time: moment(Number(lastMoodRow.timestamp)).format(), + value: Number(lastMoodRow.value) + }; + } + } + ); +} + +export {}; diff --git a/app.js b/worker.js similarity index 72% rename from app.js rename to worker.js index 377a723..86d86ef 100644 --- a/app.js +++ b/worker.js @@ -1,81 +1,24 @@ +"use strict"; +exports.__esModule = true; // Third party dependencies var moment = require("moment"); var needle = require("needle"); -var http = require("http"); -// Telegram setup -var Telegraf = require("telegraf"); -var Router = Telegraf.Router, Markup = Telegraf.Markup, Extra = Telegraf.Extra; -var bot = new Telegraf(process.env.TELEGRAM_BOT_TOKEN); -// Sheets setup -var GoogleSpreadsheet = require("google-spreadsheet"); -var async = require("async"); -// spreadsheet key is the long id in the sheets URL -console.log("Loading " + process.env.GOOGLE_SHEETS_DOC_ID); -var doc = new GoogleSpreadsheet(process.env.GOOGLE_SHEETS_DOC_ID); -var rawDataSheet; -var lastRunSheet; +var _a = require("telegraf"), Router = _a.Router, Markup = _a.Markup, Extra = _a.Extra; +// Internal dependencies +var config = require("./classes/config.js"); +var google_sheets = require("./classes/google_sheets.js"); +var telegram = require("./classes/telegram.js"); +var bot = telegram.bot; // State var currentlyAskedQuestionObject = null; var currentlyAskedQuestionMessageId = null; // The Telegram message ID reference var currentlyAskedQuestionQueue = []; // keep track of all the questions about to be asked -var cachedCtx = null; // TODO: this obviously hsa to be removed and replaced with something better -var lastCommandReminder = {}; // to not spam the user on each interval -var lastMoodData = null; // used for the moods API -var userConfig = require("./lifesheet.json"); -console.log("Successfully loaded user config"); -console.log("Starting Google Sheets Login, this might take a few seconds..."); -async.series([ - function setAuth(step) { - var creds = { - client_email: process.env.GOOGLE_SHEETS_CLIENT_EMAIL, - private_key: process.env.GOOGLE_SHEETS_PRIVATE_KEY.replace(/\\n/g, "\n") - }; - doc.useServiceAccountAuth(creds, step); - }, - function getInfoAndWorksheets(step) { - doc.getInfo(function (error, info) { - if (error) { - console.error(error); - } - console.log("Loaded doc: " + info.title + " by " + info.author.email); - for (var i = 0; i < info.worksheets.length; i++) { - // we iterate over those and check the name, as we don't want to use indexes to access a sheet directly - // to allow the user to order those sheets to however they want - var currentSheet = info.worksheets[i]; - if (currentSheet.title.toLowerCase() == "rawdata") { - rawDataSheet = currentSheet; - } - else if (currentSheet.title.toLowerCase() == "lastrun") { - lastRunSheet = currentSheet; - } - else { - console.log("Ignoring user's sheet named " + currentSheet.title); - } - } - if (rawDataSheet == null || lastRunSheet == null) { - console.error("Something is wrong with the Sheet, please make sure to create 2 sheets: RawData and LastRun, according to the project's README and try again"); - return; - } - console.log("Found the relevant sheets, already " + - rawDataSheet.rowCount + - " existing data entries"); - step(); - }); - } -], function (err) { - if (err) { - console.log("Error: " + err); - } - else { - console.log("Google login successful"); - console.log("Setting up Telegram bot..."); - initBot(); - console.log("Setting up background scheduler..."); - initScheduler(); - console.log("Setting up web API"); - initMoodAPI(); - console.log("Boot up complete"); - } +var rawDataSheet = null; +var lastRunSheet = null; +google_sheets.setupGoogleSheets(function (rawDataSheetRef, lastRunSheetRef) { + rawDataSheet = rawDataSheetRef; + lastRunSheet = lastRunSheetRef; + initBot(); }); function getButtonText(number) { var emojiNumber = { @@ -168,8 +111,153 @@ function triggerNextQuestionFromQueue(ctx) { currentlyAskedQuestionMessageId = message_id; }); } -// App logic +function insertNewValue(parsedUserValue, ctx, key, type) { + console.log("Inserting value '" + parsedUserValue + "' for key " + key); + var dateToAdd = moment(ctx.update.message.date * 1000); + var row = { + Timestamp: dateToAdd.valueOf(), + YearMonth: dateToAdd.format("YYYYMM"), + YearWeek: dateToAdd.format("YYYYWW"), + Year: dateToAdd.year(), + Quarter: dateToAdd.quarter(), + Month: dateToAdd.format("MM"), + Day: dateToAdd.date(), + Hour: dateToAdd.hours(), + Minute: dateToAdd.minutes(), + Week: dateToAdd.week(), + Key: key, + Question: currentlyAskedQuestionObject.question, + Type: type, + Value: parsedUserValue + }; + rawDataSheet.addRow(row, function (error, row) { + if (error) { + console.error(error); + if (ctx) { + ctx.reply("Error saving value: " + error); + } + } + if (ctx) { + // we don't use this for location sending as we have many values for that, so that's when `ctx` is nil + // Show that we saved the value + // Currently the Telegram API doens't support updating of messages that have a custom keyboard + // for no good reason, as mentioned here https://github.com/TelegramBots/telegram.bot/issues/176 + // + // Bad Request: Message can't be edited + // + // Please note, that it is currently only possible to edit messages without reply_markup or with inline keyboards + // ctx.telegram.editMessageText( + // ctx.update.message.chat.id, + // currentlyAskedQuestionMessageId, + // null, + // "✅ " + lastQuestionAskedDupe + " ✅" + // ); + } + }); +} +function parseUserInput(ctx, text) { + if (text === void 0) { text = null; } + if (ctx.update.message.from.username != process.env.TELEGRAM_USER_ID) { + console.error("Invalid user " + ctx.update.message.from.username); + return; + } + if (currentlyAskedQuestionMessageId == null) { + ctx + .reply("Sorry, I forgot the question I asked, this usually means it took too long for you to respond, please trigger the question again by running the `/` command") + .then(function (_a) { + var message_id = _a.message_id; + sendAvailableCommands(ctx); + }); + return; + } + // user replied with a value + var userValue; + if (text != null) { + userValue = text; + } + else { + userValue = ctx.match[1]; + } + var parsedUserValue = null; + if (currentlyAskedQuestionObject.type != "text") { + // First, see if it starts with emoji number, for which we have to do custom + // parsing instead + if (currentlyAskedQuestionObject.type == "range" || + currentlyAskedQuestionObject.type == "boolean") { + var tryToParseNumber = parseInt(userValue[0]); + if (!isNaN(tryToParseNumber)) { + parsedUserValue = tryToParseNumber; + } + else { + ctx.reply("Sorry, looks like your input is invalid, please enter a valid number from the selection", Extra.inReplyTo(ctx.update.message.message_id)); + } + } + if (parsedUserValue == null) { + // parse the int/float, support both ints and floats + userValue = userValue.match(/^(\d+(\.\d+)?)$/); + if (userValue == null) { + ctx.reply("Sorry, looks like you entered an invalid number, please try again", Extra.inReplyTo(ctx.update.message.message_id)); + return; + } + parsedUserValue = userValue[1]; + } + } + else { + parsedUserValue = userValue; // raw value is fine + } + if (currentlyAskedQuestionObject.type == "range") { + // ensure the input is 0-6 + if (parsedUserValue < 0 || parsedUserValue > 6) { + ctx.reply("Please enter a value from 0 to 6", Extra.inReplyTo(ctx.update.message.message_id)); + return; + } + } + console.log("Got a new value: " + + parsedUserValue + + " for question " + + currentlyAskedQuestionObject.key); + if (currentlyAskedQuestionObject.replies && + currentlyAskedQuestionObject.replies[parsedUserValue]) { + // Check if there is a custom reply, and if, use that + ctx.reply(currentlyAskedQuestionObject.replies[parsedUserValue], Extra.inReplyTo(ctx.update.message.message_id)); + } + insertNewValue(parsedUserValue, ctx, currentlyAskedQuestionObject.key, currentlyAskedQuestionObject.type); + triggerNextQuestionFromQueue(ctx); +} +function sendAvailableCommands(ctx) { + ctx.reply("Available commands:").then(function (_a) { + var message_id = _a.message_id; + ctx.reply("\n\n/skip\n/report\n\n/" + Object.keys(config.userConfig).join("\n/")); + }); +} +function saveLastRun(command) { + lastRunSheet.getRows({ + offset: 1, + limit: 100 + }, function (error, rows) { + var updatedExistingRow = false; + for (var i = 0; i < rows.length; i++) { + var currentRow = rows[i]; + var currentCommand = currentRow.command; + if (command == currentCommand) { + updatedExistingRow = true; + currentRow.lastrun = moment().valueOf(); // unix timestamp + currentRow.save(); + } + } + if (!updatedExistingRow) { + var row = { + Command: command, + LastRun: moment().valueOf() // unix timestamp + }; + lastRunSheet.addRow(row, function (error, row) { + console.log("Stored timestamp of last run for " + command); + }); + } + }); +} function initBot() { + console.log("Launching up Telegram bot..."); // parse numeric/text inputs // `^([^\/].*)$` matches everything that doens't start with / // This will enable us to get any user inputs, including longer texts @@ -218,8 +306,8 @@ function initBot() { console.log("User wants to track a specific value, without the whole survey: " + toTrack); var questionToAsk = null; - Object.keys(userConfig).forEach(function (key) { - var survey = userConfig[key]; + Object.keys(config.userConfig).forEach(function (key) { + var survey = config.userConfig[key]; for (var i = 0; i < survey.questions.length; i++) { var currentQuestion = survey.questions[i]; if (currentQuestion.key == toTrack) { @@ -317,10 +405,9 @@ function initBot() { if (ctx.update.message.from.username != process.env.TELEGRAM_USER_ID) { return; } - cachedCtx = ctx; // user entered a command to start the survey var command = ctx.match[1]; - var matchingCommandObject = userConfig[command]; + var matchingCommandObject = config.userConfig[command]; if (matchingCommandObject && matchingCommandObject.questions) { console.log("User wants to run:"); console.log(matchingCommandObject); @@ -384,249 +471,3 @@ function initBot() { // has to be last bot.launch(); } -function insertNewValue(parsedUserValue, ctx, key, type) { - console.log("Inserting value '" + parsedUserValue + "' for key " + key); - var dateToAdd = moment(ctx.update.message.date * 1000); - var row = { - Timestamp: dateToAdd.valueOf(), - YearMonth: dateToAdd.format("YYYYMM"), - YearWeek: dateToAdd.format("YYYYWW"), - Year: dateToAdd.year(), - Quarter: dateToAdd.quarter(), - Month: dateToAdd.format("MM"), - Day: dateToAdd.date(), - Hour: dateToAdd.hours(), - Minute: dateToAdd.minutes(), - Week: dateToAdd.week(), - Key: key, - Question: currentlyAskedQuestionObject.question, - Type: type, - Value: parsedUserValue - }; - rawDataSheet.addRow(row, function (error, row) { - if (error) { - console.error(error); - if (ctx) { - ctx.reply("Error saving value: " + error); - } - } - if (ctx) { - // we don't use this for location sending as we have many values for that, so that's when `ctx` is nil - // Show that we saved the value - // Currently the Telegram API doens't support updating of messages that have a custom keyboard - // for no good reason, as mentioned here https://github.com/TelegramBots/telegram.bot/issues/176 - // - // Bad Request: Message can't be edited - // - // Please note, that it is currently only possible to edit messages without reply_markup or with inline keyboards - // ctx.telegram.editMessageText( - // ctx.update.message.chat.id, - // currentlyAskedQuestionMessageId, - // null, - // "✅ " + lastQuestionAskedDupe + " ✅" - // ); - } - }); - if (key == "mood") { - // we only serve the current mood via an API - lastMoodData = { - time: dateToAdd, - value: Number(parsedUserValue) - }; - } -} -function parseUserInput(ctx, text) { - if (text === void 0) { text = null; } - if (ctx.update.message.from.username != process.env.TELEGRAM_USER_ID) { - console.error("Invalid user " + ctx.update.message.from.username); - return; - } - if (currentlyAskedQuestionMessageId == null) { - ctx - .reply("Sorry, I forgot the question I asked, this usually means it took too long for you to respond, please trigger the question again by running the `/` command") - .then(function (_a) { - var message_id = _a.message_id; - sendAvailableCommands(ctx); - }); - return; - } - // user replied with a value - var userValue; - if (text != null) { - userValue = text; - } - else { - userValue = ctx.match[1]; - } - var parsedUserValue = null; - if (currentlyAskedQuestionObject.type != "text") { - // First, see if it starts with emoji number, for which we have to do custom - // parsing instead - if (currentlyAskedQuestionObject.type == "range" || - currentlyAskedQuestionObject.type == "boolean") { - var tryToParseNumber = parseInt(userValue[0]); - if (!isNaN(tryToParseNumber)) { - parsedUserValue = tryToParseNumber; - } - else { - ctx.reply("Sorry, looks like your input is invalid, please enter a valid number from the selection", Extra.inReplyTo(ctx.update.message.message_id)); - } - } - if (parsedUserValue == null) { - // parse the int/float, support both ints and floats - userValue = userValue.match(/^(\d+(\.\d+)?)$/); - if (userValue == null) { - ctx.reply("Sorry, looks like you entered an invalid number, please try again", Extra.inReplyTo(ctx.update.message.message_id)); - return; - } - parsedUserValue = userValue[1]; - } - } - else { - parsedUserValue = userValue; // raw value is fine - } - if (currentlyAskedQuestionObject.type == "range") { - // ensure the input is 0-6 - if (parsedUserValue < 0 || parsedUserValue > 6) { - ctx.reply("Please enter a value from 0 to 6", Extra.inReplyTo(ctx.update.message.message_id)); - return; - } - } - console.log("Got a new value: " + - parsedUserValue + - " for question " + - currentlyAskedQuestionObject.key); - if (currentlyAskedQuestionObject.replies && - currentlyAskedQuestionObject.replies[parsedUserValue]) { - // Check if there is a custom reply, and if, use that - ctx.reply(currentlyAskedQuestionObject.replies[parsedUserValue], Extra.inReplyTo(ctx.update.message.message_id)); - } - insertNewValue(parsedUserValue, ctx, currentlyAskedQuestionObject.key, currentlyAskedQuestionObject.type); - triggerNextQuestionFromQueue(ctx); -} -function sendAvailableCommands(ctx) { - ctx.reply("Available commands:").then(function (_a) { - var message_id = _a.message_id; - ctx.reply("\n\n/skip\n/report\n\n/" + Object.keys(userConfig).join("\n/")); - }); -} -function saveLastRun(command) { - lastRunSheet.getRows({ - offset: 1, - limit: 100 - }, function (error, rows) { - var updatedExistingRow = false; - for (var i = 0; i < rows.length; i++) { - var currentRow = rows[i]; - var currentCommand = currentRow.command; - if (command == currentCommand) { - updatedExistingRow = true; - currentRow.lastrun = moment().valueOf(); // unix timestamp - currentRow.save(); - } - } - if (!updatedExistingRow) { - var row = { - Command: command, - LastRun: moment().valueOf() // unix timestamp - }; - lastRunSheet.addRow(row, function (error, row) { - console.log("Stored timestamp of last run for " + command); - }); - } - }); -} -function initScheduler() { - // Cron job to check if we need to run a given question again - setInterval(function () { - lastRunSheet.getRows({ - offset: 1, - limit: 100 - }, function (error, rows) { - for (var i = 0; i < rows.length; i++) { - var currentRow = rows[i]; - var command = currentRow.command; - var lastRun = moment(Number(currentRow.lastrun)); - if (userConfig[command] == null) { - console.error("Error, command not found, means it's not on the last run sheet, probably due to renaming a command: " + - command); - break; - } - var scheduleType = userConfig[command].schedule; - var timeDifferenceHours = moment().diff(moment(lastRun), "hours"); //hours - var shouldRemindUser = false; - if (scheduleType == "fourTimesADay") { - if (timeDifferenceHours >= 24 / 4) { - shouldRemindUser = true; - } - } - else if (scheduleType == "daily") { - if (timeDifferenceHours >= 24 * 1.1) { - shouldRemindUser = true; - } - } - else if (scheduleType == "weekly") { - if (timeDifferenceHours >= 24 * 7 * 1.05) { - shouldRemindUser = true; - } - } - else if (scheduleType == "monthly") { - if (timeDifferenceHours >= 24 * 30 * 1.05) { - shouldRemindUser = true; - } - } - else if (scheduleType == "manual") { - // Never remind the user - } - else { - console.error("Unknown schedule type " + scheduleType); - } - var lastReminderDiffInHours = 100; // not reminded yet by default - if (lastCommandReminder[command]) { - lastReminderDiffInHours = moment().diff(moment(Number(lastCommandReminder[command])), "hours"); - } - if (shouldRemindUser && - cachedCtx != null && - lastReminderDiffInHours > 1) { - cachedCtx.reply("Please run /" + - command + - " again, it's been " + - timeDifferenceHours + - " hours since you last filled it out"); - lastCommandReminder[command] = moment().valueOf(); // unix timestamp - } - } - }); - }, 30000); -} -function initMoodAPI() { - // needed for the API endpoint used by https://whereisfelix.today - // Fetch the last entry from the before the container was spawned - // From then on the cache is refreshed when the user enters the value - var currentMood = rawDataSheet.getRows({ - offset: 0, - limit: 1, - orderby: "timestamp", - reverse: true, - query: "key=mood" - }, function (error, rows) { - if (error) { - console.error(error); - } - var lastMoodRow = rows[0]; - // `lastMoodRow` is null if we haven't tracked a mood yet - if (lastMoodRow != null) { - lastMoodData = { - time: moment(Number(lastMoodRow.timestamp)).format(), - value: Number(lastMoodRow.value) - }; - } - }); - http - .createServer(function (req, res) { - res.writeHead(200, { "Content-Type": "application/json" }); - res.write(JSON.stringify(lastMoodData)); - return res.end(); - }) - .listen(process.env.PORT); -} diff --git a/app.ts b/worker.ts similarity index 71% rename from app.ts rename to worker.ts index a5cf8f4..99443ce 100644 --- a/app.ts +++ b/worker.ts @@ -1,110 +1,30 @@ // Third party dependencies const moment = require("moment"); var needle = require("needle"); -var http = require("http"); +const { Router, Markup, Extra } = require("telegraf"); -// Telegram setup -const Telegraf = require("telegraf"); -const { Router, Markup, Extra } = Telegraf; +// Internal dependencies +let config = require("./classes/config.js"); +let google_sheets = require("./classes/google_sheets.js"); +let telegram = require("./classes/telegram.js"); -const bot = new Telegraf(process.env.TELEGRAM_BOT_TOKEN); +import { Command, QuestionToAsk } from "./classes/config.js"; -// Sheets setup -var GoogleSpreadsheet = require("google-spreadsheet"); -var async = require("async"); - -// spreadsheet key is the long id in the sheets URL -console.log("Loading " + process.env.GOOGLE_SHEETS_DOC_ID); -var doc = new GoogleSpreadsheet(process.env.GOOGLE_SHEETS_DOC_ID); -var rawDataSheet; -var lastRunSheet; +let bot = telegram.bot; // State var currentlyAskedQuestionObject: QuestionToAsk = null; var currentlyAskedQuestionMessageId: String = null; // The Telegram message ID reference let currentlyAskedQuestionQueue: Array = []; // keep track of all the questions about to be asked -var cachedCtx = null; // TODO: this obviously hsa to be removed and replaced with something better -var lastCommandReminder: { [key: string]: Number } = {}; // to not spam the user on each interval -var lastMoodData = null; // used for the moods API +let rawDataSheet = null; +let lastRunSheet = null; -// Interfaces -interface Command { - description: String; - schedule: String; - questions: [QuestionToAsk]; -} +google_sheets.setupGoogleSheets(function(rawDataSheetRef, lastRunSheetRef) { + rawDataSheet = rawDataSheetRef; + lastRunSheet = lastRunSheetRef; -interface QuestionToAsk { - key: String; - question: String; - type: String; - buttons: { [key: string]: String }; - replies: { [key: string]: String }; -} - -let userConfig: { [key: string]: Command } = require("./lifesheet.json"); -console.log("Successfully loaded user config"); - -console.log("Starting Google Sheets Login, this might take a few seconds..."); -async.series( - [ - function setAuth(step) { - var creds = { - client_email: process.env.GOOGLE_SHEETS_CLIENT_EMAIL, - private_key: process.env.GOOGLE_SHEETS_PRIVATE_KEY.replace(/\\n/g, "\n") - }; - - doc.useServiceAccountAuth(creds, step); - }, - function getInfoAndWorksheets(step) { - doc.getInfo(function(error, info) { - if (error) { - console.error(error); - } - console.log("Loaded doc: " + info.title + " by " + info.author.email); - for (let i = 0; i < info.worksheets.length; i++) { - // we iterate over those and check the name, as we don't want to use indexes to access a sheet directly - // to allow the user to order those sheets to however they want - let currentSheet = info.worksheets[i]; - if (currentSheet.title.toLowerCase() == "rawdata") { - rawDataSheet = currentSheet; - } else if (currentSheet.title.toLowerCase() == "lastrun") { - lastRunSheet = currentSheet; - } else { - console.log("Ignoring user's sheet named " + currentSheet.title); - } - } - if (rawDataSheet == null || lastRunSheet == null) { - console.error( - "Something is wrong with the Sheet, please make sure to create 2 sheets: RawData and LastRun, according to the project's README and try again" - ); - return; - } - - console.log( - "Found the relevant sheets, already " + - rawDataSheet.rowCount + - " existing data entries" - ); - step(); - }); - } - ], - function(err) { - if (err) { - console.log("Error: " + err); - } else { - console.log("Google login successful"); - console.log("Setting up Telegram bot..."); - initBot(); - console.log("Setting up background scheduler..."); - initScheduler(); - console.log("Setting up web API"); - initMoodAPI(); - console.log("Boot up complete"); - } - } -); + initBot(); +}); function getButtonText(number) { let emojiNumber = { @@ -206,8 +126,197 @@ function triggerNextQuestionFromQueue(ctx) { }); } -// App logic +function insertNewValue(parsedUserValue, ctx, key, type) { + console.log("Inserting value '" + parsedUserValue + "' for key " + key); + + let dateToAdd = moment(ctx.update.message.date * 1000); + let row = { + Timestamp: dateToAdd.valueOf(), + YearMonth: dateToAdd.format("YYYYMM"), + YearWeek: dateToAdd.format("YYYYWW"), + Year: dateToAdd.year(), + Quarter: dateToAdd.quarter(), + Month: dateToAdd.format("MM"), // to get the leading 0 needed for Google Data Studio + Day: dateToAdd.date(), + Hour: dateToAdd.hours(), + Minute: dateToAdd.minutes(), + Week: dateToAdd.week(), + Key: key, + Question: currentlyAskedQuestionObject.question, + Type: type, + Value: parsedUserValue + }; + + rawDataSheet.addRow(row, function(error, row) { + if (error) { + console.error(error); + if (ctx) { + ctx.reply("Error saving value: " + error); + } + } + + if (ctx) { + // we don't use this for location sending as we have many values for that, so that's when `ctx` is nil + // Show that we saved the value + // Currently the Telegram API doens't support updating of messages that have a custom keyboard + // for no good reason, as mentioned here https://github.com/TelegramBots/telegram.bot/issues/176 + // + // Bad Request: Message can't be edited + // + // Please note, that it is currently only possible to edit messages without reply_markup or with inline keyboards + // ctx.telegram.editMessageText( + // ctx.update.message.chat.id, + // currentlyAskedQuestionMessageId, + // null, + // "✅ " + lastQuestionAskedDupe + " ✅" + // ); + } + }); +} + +function parseUserInput(ctx, text = null) { + if (ctx.update.message.from.username != process.env.TELEGRAM_USER_ID) { + console.error("Invalid user " + ctx.update.message.from.username); + return; + } + + if (currentlyAskedQuestionMessageId == null) { + ctx + .reply( + "Sorry, I forgot the question I asked, this usually means it took too long for you to respond, please trigger the question again by running the `/` command" + ) + .then(({ message_id }) => { + sendAvailableCommands(ctx); + }); + return; + } + + // user replied with a value + let userValue; + if (text != null) { + userValue = text; + } else { + userValue = ctx.match[1]; + } + + let parsedUserValue = null; + + if (currentlyAskedQuestionObject.type != "text") { + // First, see if it starts with emoji number, for which we have to do custom + // parsing instead + if ( + currentlyAskedQuestionObject.type == "range" || + currentlyAskedQuestionObject.type == "boolean" + ) { + let tryToParseNumber = parseInt(userValue[0]); + if (!isNaN(tryToParseNumber)) { + parsedUserValue = tryToParseNumber; + } else { + ctx.reply( + "Sorry, looks like your input is invalid, please enter a valid number from the selection", + Extra.inReplyTo(ctx.update.message.message_id) + ); + } + } + + if (parsedUserValue == null) { + // parse the int/float, support both ints and floats + userValue = userValue.match(/^(\d+(\.\d+)?)$/); + if (userValue == null) { + ctx.reply( + "Sorry, looks like you entered an invalid number, please try again", + Extra.inReplyTo(ctx.update.message.message_id) + ); + return; + } + parsedUserValue = userValue[1]; + } + } else { + parsedUserValue = userValue; // raw value is fine + } + + if (currentlyAskedQuestionObject.type == "range") { + // ensure the input is 0-6 + if (parsedUserValue < 0 || parsedUserValue > 6) { + ctx.reply( + "Please enter a value from 0 to 6", + Extra.inReplyTo(ctx.update.message.message_id) + ); + return; + } + } + + console.log( + "Got a new value: " + + parsedUserValue + + " for question " + + currentlyAskedQuestionObject.key + ); + + if ( + currentlyAskedQuestionObject.replies && + currentlyAskedQuestionObject.replies[parsedUserValue] + ) { + // Check if there is a custom reply, and if, use that + ctx.reply( + currentlyAskedQuestionObject.replies[parsedUserValue], + Extra.inReplyTo(ctx.update.message.message_id) + ); + } + + insertNewValue( + parsedUserValue, + ctx, + currentlyAskedQuestionObject.key, + currentlyAskedQuestionObject.type + ); + + triggerNextQuestionFromQueue(ctx); +} + +function sendAvailableCommands(ctx) { + ctx.reply("Available commands:").then(({ message_id }) => { + ctx.reply( + "\n\n/skip\n/report\n\n/" + Object.keys(config.userConfig).join("\n/") + ); + }); +} + +function saveLastRun(command) { + lastRunSheet.getRows( + { + offset: 1, + limit: 100 + }, + function(error, rows) { + var updatedExistingRow = false; + for (let i = 0; i < rows.length; i++) { + let currentRow = rows[i]; + let currentCommand = currentRow.command; + if (command == currentCommand) { + updatedExistingRow = true; + + currentRow.lastrun = moment().valueOf(); // unix timestamp + currentRow.save(); + } + } + + if (!updatedExistingRow) { + let row = { + Command: command, + LastRun: moment().valueOf() // unix timestamp + }; + lastRunSheet.addRow(row, function(error, row) { + console.log("Stored timestamp of last run for " + command); + }); + } + } + ); +} + function initBot() { + console.log("Launching up Telegram bot..."); + // parse numeric/text inputs // `^([^\/].*)$` matches everything that doens't start with / // This will enable us to get any user inputs, including longer texts @@ -268,8 +377,8 @@ function initBot() { let questionToAsk: QuestionToAsk = null; - Object.keys(userConfig).forEach(function(key) { - var survey = userConfig[key]; + Object.keys(config.userConfig).forEach(function(key) { + var survey = config.userConfig[key]; for (let i = 0; i < survey.questions.length; i++) { let currentQuestion = survey.questions[i]; if (currentQuestion.key == toTrack) { @@ -427,10 +536,9 @@ function initBot() { return; } - cachedCtx = ctx; // user entered a command to start the survey let command = ctx.match[1]; - let matchingCommandObject = userConfig[command]; + let matchingCommandObject = config.userConfig[command]; if (matchingCommandObject && matchingCommandObject.questions) { console.log("User wants to run:"); @@ -521,308 +629,4 @@ function initBot() { bot.launch(); } -function insertNewValue(parsedUserValue, ctx, key, type) { - console.log("Inserting value '" + parsedUserValue + "' for key " + key); - - let dateToAdd = moment(ctx.update.message.date * 1000); - let row = { - Timestamp: dateToAdd.valueOf(), - YearMonth: dateToAdd.format("YYYYMM"), - YearWeek: dateToAdd.format("YYYYWW"), - Year: dateToAdd.year(), - Quarter: dateToAdd.quarter(), - Month: dateToAdd.format("MM"), // to get the leading 0 needed for Google Data Studio - Day: dateToAdd.date(), - Hour: dateToAdd.hours(), - Minute: dateToAdd.minutes(), - Week: dateToAdd.week(), - Key: key, - Question: currentlyAskedQuestionObject.question, - Type: type, - Value: parsedUserValue - }; - - rawDataSheet.addRow(row, function(error, row) { - if (error) { - console.error(error); - if (ctx) { - ctx.reply("Error saving value: " + error); - } - } - - if (ctx) { - // we don't use this for location sending as we have many values for that, so that's when `ctx` is nil - // Show that we saved the value - // Currently the Telegram API doens't support updating of messages that have a custom keyboard - // for no good reason, as mentioned here https://github.com/TelegramBots/telegram.bot/issues/176 - // - // Bad Request: Message can't be edited - // - // Please note, that it is currently only possible to edit messages without reply_markup or with inline keyboards - // ctx.telegram.editMessageText( - // ctx.update.message.chat.id, - // currentlyAskedQuestionMessageId, - // null, - // "✅ " + lastQuestionAskedDupe + " ✅" - // ); - } - }); - - if (key == "mood") { - // we only serve the current mood via an API - lastMoodData = { - time: dateToAdd, - value: Number(parsedUserValue) - }; - } -} - -function parseUserInput(ctx, text = null) { - if (ctx.update.message.from.username != process.env.TELEGRAM_USER_ID) { - console.error("Invalid user " + ctx.update.message.from.username); - return; - } - - if (currentlyAskedQuestionMessageId == null) { - ctx - .reply( - "Sorry, I forgot the question I asked, this usually means it took too long for you to respond, please trigger the question again by running the `/` command" - ) - .then(({ message_id }) => { - sendAvailableCommands(ctx); - }); - return; - } - - // user replied with a value - let userValue; - if (text != null) { - userValue = text; - } else { - userValue = ctx.match[1]; - } - - let parsedUserValue = null; - - if (currentlyAskedQuestionObject.type != "text") { - // First, see if it starts with emoji number, for which we have to do custom - // parsing instead - if ( - currentlyAskedQuestionObject.type == "range" || - currentlyAskedQuestionObject.type == "boolean" - ) { - let tryToParseNumber = parseInt(userValue[0]); - if (!isNaN(tryToParseNumber)) { - parsedUserValue = tryToParseNumber; - } else { - ctx.reply( - "Sorry, looks like your input is invalid, please enter a valid number from the selection", - Extra.inReplyTo(ctx.update.message.message_id) - ); - } - } - - if (parsedUserValue == null) { - // parse the int/float, support both ints and floats - userValue = userValue.match(/^(\d+(\.\d+)?)$/); - if (userValue == null) { - ctx.reply( - "Sorry, looks like you entered an invalid number, please try again", - Extra.inReplyTo(ctx.update.message.message_id) - ); - return; - } - parsedUserValue = userValue[1]; - } - } else { - parsedUserValue = userValue; // raw value is fine - } - - if (currentlyAskedQuestionObject.type == "range") { - // ensure the input is 0-6 - if (parsedUserValue < 0 || parsedUserValue > 6) { - ctx.reply( - "Please enter a value from 0 to 6", - Extra.inReplyTo(ctx.update.message.message_id) - ); - return; - } - } - - console.log( - "Got a new value: " + - parsedUserValue + - " for question " + - currentlyAskedQuestionObject.key - ); - - if ( - currentlyAskedQuestionObject.replies && - currentlyAskedQuestionObject.replies[parsedUserValue] - ) { - // Check if there is a custom reply, and if, use that - ctx.reply( - currentlyAskedQuestionObject.replies[parsedUserValue], - Extra.inReplyTo(ctx.update.message.message_id) - ); - } - - insertNewValue( - parsedUserValue, - ctx, - currentlyAskedQuestionObject.key, - currentlyAskedQuestionObject.type - ); - - triggerNextQuestionFromQueue(ctx); -} - -function sendAvailableCommands(ctx) { - ctx.reply("Available commands:").then(({ message_id }) => { - ctx.reply("\n\n/skip\n/report\n\n/" + Object.keys(userConfig).join("\n/")); - }); -} - -function saveLastRun(command) { - lastRunSheet.getRows( - { - offset: 1, - limit: 100 - }, - function(error, rows) { - var updatedExistingRow = false; - for (let i = 0; i < rows.length; i++) { - let currentRow = rows[i]; - let currentCommand = currentRow.command; - if (command == currentCommand) { - updatedExistingRow = true; - - currentRow.lastrun = moment().valueOf(); // unix timestamp - currentRow.save(); - } - } - - if (!updatedExistingRow) { - let row = { - Command: command, - LastRun: moment().valueOf() // unix timestamp - }; - lastRunSheet.addRow(row, function(error, row) { - console.log("Stored timestamp of last run for " + command); - }); - } - } - ); -} - -function initScheduler() { - // Cron job to check if we need to run a given question again - setInterval(function() { - lastRunSheet.getRows( - { - offset: 1, - limit: 100 - }, - function(error, rows) { - for (let i = 0; i < rows.length; i++) { - let currentRow = rows[i]; - let command = currentRow.command; - let lastRun = moment(Number(currentRow.lastrun)); - - if (userConfig[command] == null) { - console.error( - "Error, command not found, means it's not on the last run sheet, probably due to renaming a command: " + - command - ); - break; - } - - let scheduleType = userConfig[command].schedule; - let timeDifferenceHours = moment().diff(moment(lastRun), "hours"); //hours - var shouldRemindUser = false; - - if (scheduleType == "fourTimesADay") { - if (timeDifferenceHours >= 24 / 4) { - shouldRemindUser = true; - } - } else if (scheduleType == "daily") { - if (timeDifferenceHours >= 24 * 1.1) { - shouldRemindUser = true; - } - } else if (scheduleType == "weekly") { - if (timeDifferenceHours >= 24 * 7 * 1.05) { - shouldRemindUser = true; - } - } else if (scheduleType == "monthly") { - if (timeDifferenceHours >= 24 * 30 * 1.05) { - shouldRemindUser = true; - } - } else if (scheduleType == "manual") { - // Never remind the user - } else { - console.error("Unknown schedule type " + scheduleType); - } - - let lastReminderDiffInHours = 100; // not reminded yet by default - if (lastCommandReminder[command]) { - lastReminderDiffInHours = moment().diff( - moment(Number(lastCommandReminder[command])), - "hours" - ); - } - - if ( - shouldRemindUser && - cachedCtx != null && - lastReminderDiffInHours > 1 - ) { - cachedCtx.reply( - "Please run /" + - command + - " again, it's been " + - timeDifferenceHours + - " hours since you last filled it out" - ); - lastCommandReminder[command] = moment().valueOf(); // unix timestamp - } - } - } - ); - }, 30000); -} - -function initMoodAPI() { - // needed for the API endpoint used by https://whereisfelix.today - // Fetch the last entry from the before the container was spawned - // From then on the cache is refreshed when the user enters the value - let currentMood = rawDataSheet.getRows( - { - offset: 0, - limit: 1, - orderby: "timestamp", - reverse: true, - query: "key=mood" - }, - function(error, rows) { - if (error) { - console.error(error); - } - let lastMoodRow = rows[0]; - // `lastMoodRow` is null if we haven't tracked a mood yet - if (lastMoodRow != null) { - lastMoodData = { - time: moment(Number(lastMoodRow.timestamp)).format(), - value: Number(lastMoodRow.value) - }; - } - } - ); - - http - .createServer(function(req, res) { - res.writeHead(200, { "Content-Type": "application/json" }); - res.write(JSON.stringify(lastMoodData)); - return res.end(); - }) - .listen(process.env.PORT); -} +export {};