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:
@@ -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
5
classes/config.js
Normal 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
20
classes/config.ts
Normal 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
61
classes/google_sheets.js
Normal 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
73
classes/google_sheets.ts
Normal 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
6
classes/telegram.js
Normal 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
7
classes/telegram.ts
Normal 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 {};
|
||||
11
package.json
11
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",
|
||||
|
||||
76
reminder.js
Normal file
76
reminder.js
Normal 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
92
reminder.ts
Normal 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
48
web.js
Normal 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
58
web.ts
Normal 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 {};
|
||||
@@ -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...");
|
||||
var rawDataSheet = null;
|
||||
var lastRunSheet = null;
|
||||
google_sheets.setupGoogleSheets(function (rawDataSheetRef, lastRunSheetRef) {
|
||||
rawDataSheet = rawDataSheetRef;
|
||||
lastRunSheet = lastRunSheetRef;
|
||||
initBot();
|
||||
console.log("Setting up background scheduler...");
|
||||
initScheduler();
|
||||
console.log("Setting up web API");
|
||||
initMoodAPI();
|
||||
console.log("Boot up complete");
|
||||
}
|
||||
});
|
||||
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);
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
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 {};
|
||||
Reference in New Issue
Block a user