1
0
mirror of https://github.com/elisspace/FxLifeSheet.git synced 2026-08-29 15:44:00 +00:00

Refactor bot to use separate source files

This commit is contained in:
Felix Krause
2019-04-30 12:56:30 +02:00
parent 62aa021e39
commit 145f4b6749
15 changed files with 836 additions and 730 deletions

View File

@@ -1 +1,2 @@
web: npm run prod
web: npm run web
worker: npm run worker

View File

@@ -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:

5
classes/config.js Normal file
View File

@@ -0,0 +1,5 @@
"use strict";
exports.__esModule = true;
var userConfig = require("../lifesheet.json");
console.log("Successfully loaded user config");
module.exports.userConfig = userConfig;

20
classes/config.ts Normal file
View File

@@ -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 };

61
classes/google_sheets.js Normal file
View File

@@ -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;

73
classes/google_sheets.ts Normal file
View File

@@ -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 {};

6
classes/telegram.js Normal file
View File

@@ -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;

7
classes/telegram.ts Normal file
View File

@@ -0,0 +1,7 @@
// Telegram setup
const Telegraf = require("telegraf");
const bot = new Telegraf(process.env.TELEGRAM_BOT_TOKEN);
module.exports.bot = bot;
export {};

View File

@@ -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",

76
reminder.js Normal file
View File

@@ -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");
});
}

92
reminder.ts Normal file
View File

@@ -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 {};

48
web.js Normal file
View File

@@ -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)
};
}
});
}

58
web.ts Normal file
View File

@@ -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 {};

View File

@@ -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);
}

View File

@@ -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<QuestionToAsk> = []; // 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 {};