Как создать тайм-трекер с помощью API Telegram Bot и веб хуков

В данной статье вы узнаете, как вести учет рабочего времени с помощью Telegram Bot API и механизма веб хуков (англ. Webhooks). Мы разберем этапы создания, настройки и развертывания тайм-трекера. Но сначала хотелось бы рассказать, как эта идея пришла мне в голову. Если же вас интересует только техническая часть повествования, переходите сразу к следующему разделу.

Введение 

Как-то в разговоре моя жена сказала, что ей нужно написать почасовой отчет о работе, проделанной за 3 последних месяца, и определить точное количество отработанных часов. Не имея в распоряжении никакого инструмента для регистрации рабочего времени, ей было довольно сложно рассчитать эти показатели по старым заметкам и чатам. Тогда она поддержала предложение своего руководителя создать группу в WhatsApp, куда будет отправлять сообщение в момент начала и окончания работы. Имеющиеся сообщения упростят составление подобных отчетов. 

Идея отличная: телефон практически всегда под рукой, так что ничего не стоит просто написать слова “начало”/“окончание” или что угодно. Я вдохновился этой идеей и задумался о способах ее реализации. Часами я искал хороший инструмент для учета рабочего времени, руководствуясь следующими обязательными условиями: 1) он должен легко вводить сообщение “Задание выполняется” или “Задание готово”; 2) он работает на всех имеющихся устройствах. 

Я опробовал несколько соответствующих приложений, которые запускались на компьютерах Mac, Linux и смартфонах, но ни одно из них меня не зацепило. Тогда я вернулся к способу учета времени, которым жена пользовалась на тот момент, и еще раз осмыслил классную задумку прописывать в мессенджере время начала и окончания работ. Изучив материал по теме ботов и веб хуков, я создал инструмент на основе Telegram.

Суть идеи состоит в создании Telegram-бота и реализации веб хуков для специально отформатированных сообщений, которые сохраняют временные метки и описания задач в формате CSV. Далее рассмотрим его принцип действия. 

Создание проекта

Прежде всего, я начал искать существующие библиотеки и примеры использования веб хуков Telegram. Я нашел на GitHub один модуль  —  node-telegram-bot-api. Решил им воспользоваться по двум причинам: из-за его простоты и моей любви к Node. Поскольку он предоставлял библиотеку, необходимо было создать приложение Node. Отдавая предпочтения TypeScript, я создал проект соответствующим образом.

Есть один классный инструмент под названием typescript-starter, который запускается как исполняемый файл npm через npx typescript-starter. Он задает вопросы, например уточняет намерение создать библиотеку или приложение, последнее как раз представляет наш случай. Создав проект и сохранив значения по умолчанию в диалоговом окне установки typescript-starter, я задал конфигурацию запуска для отладки в VS Code. В результате ряда экспериментов она приняла следующий вид: 

{
"type": "node",
"request": "launch",
"name": "Launch Program",
"program": "${workspaceFolder}/build/main/index.js",
"console": "integratedTerminal",
"preLaunchTask": "tsc: build - tsconfig.json",
"outFiles": [
"${workspaceFolder}/out/**/*.js"
]
}

Основная часть кода находится в src/main/index.ts. Чтобы настроить веб хуки для сообщений, отправляемых в Telegram-бот, node-telegram-bot-api предлагает отличный шаблон, который я немного адаптировал. Сначала установил нужные пакеты: 

  • npm i --save node-telegram-bot-api;
  • npm i --save @types/node-telegram-bot-api.

Код Telegram-бота

Далее я изменил импорты из кода примера с require на import с целью обеспечения безопасности типов для импортируемого кода и добавил свой токен. Код выглядел следующим образом и был готов к тестированию: 

// const TelegramBot = require('node-telegram-bot-api');
import TelegramBot from 'node-telegram-bot-api';

// замена нижеуказанного значения на токен Telegram, полученного из @BotFather
const token = 'YOUR_TELEGRAM_BOT_TOKEN';

// создание бота, использующего технологию опроса сервера (polling) для получения новых обновлений
const bot = new TelegramBot(token, { polling: true });

// Совпадения с "/echo [whatever]"
bot.onText(/\/echo (.+)/, (msg, match) => {
// 'msg' - это полученное сообщение из Telegram
// 'match' - результат выполнения вышеуказанного регулярного выражения для проверки текстового содержимого
// сообщения

const chatId = msg.chat.id;
const resp = match[1]; // перехваченный "whatever"

// отправка обратно в чат совпавшего "whatever"
bot.sendMessage(chatId, resp);
});

// Прослушивание сообщений любого вида. Существуют разные виды
// сообщений.
bot.on('message', (msg) => {
const chatId = msg.chat.id;

// отправка сообщения в чат и подтверждение его получения
bot.sendMessage(chatId, 'Received your message');
});

С помощью хуков on и onText вызываются функции для каждого входящего сообщения (on) или сообщений, совпадающих с определенным регулярным выражением (onText). В этих функциях код отвечает, отправляя сообщения обратно в соответствующий чат. На мой взгляд, библиотека предоставляет отличный уровень абстракции. Итак, все готово к тестированию. 

Сначала я создал бота: перешел в Telegram (или Web Telegram), открыл чат в BotFather (по ссылке t.me в документации), написал /newbot, дал ему имя и имя пользователя в соответствии с требованиями. BotFather предоставил токен, который присваивается const token в начале кода. Затем запустил бота Telegram API посредством ранее добавленной команды запуска в VS Code. 

Далее открыл новый чат в боте. Для этого прошел по ссылке, полученной из BotFather, и нажал на Start в правом верхнем углу. После этого я смог протестировать веб хуки, отправив боту /echo Hello. Он ответил Hello (через функцию onText) и прислал другое текстовое сообщение Received your message, т.е. сообщение получено. 

Отлично! Теперь сделаем код более функциональным. 

Добавление обработчиков и кода для учета рабочего времени 

Как ранее упоминалось, цель проекта  —  сохранить результаты учета времени в формате CSV для последующего редактирования в Excel. В Node работать с файлами CSV очень просто. Вместо того, чтобы воспользоваться одной из многочисленных библиотек, предназначенных для решения этой задачи, я решил ради интереса написать все с нуля. Для своего прототипа выбрал следующие команды бота: 

  • /work $message -> начать работу над задачей $message;
  • /done -> завершить последнюю начатую задачу; 
  • /state -> показать состояние последней задачи (начато или завершено); 
  • /print-> записать весь CSV в чат. 

Для чтения и записи CSV были реализованы 2 функции: 

const getEntries = async () => {
if (!fs.existsSync(csvFilePath)) {
fs.writeFileSync(csvFilePath, "");
}

const val = fs.readFileSync(csvFilePath).toString();
const entries = [];
if (val == "") {
return entries;
}
val.split("\n").forEach((row, index) => {
if (row === "" || index == 0) {
return;
}
const [work, startDate, endDate, duration] = row.split(delimiter);
const entry = {
work,
startDate,
endDate,
duration
};
entries.push(entry);
});
return entries;
}

const writeFile = (entries) => {
let str = "work;startDate;endDate;duration;\n";
entries.forEach(e => {
str += Object.keys(e).map(k => e[k]).join(delimiter);
str += '\n';
});
fs.unlinkSync(csvFilePath);
fs.writeFileSync(csvFilePath, str);
};

Функция getEntries возвращает содержимое CSV в виде объектов JS, а функция writeFile сохраняет объекты JS в файл CSV. Таким образом, нет никакой реальной необходимости использовать библиотеку. С помощью этих 2-х функций были реализованы 4 ранее упомянутых обработчика: 

bot.onText(/\/work (.+)/, async (msg, match) => {

const chatId = msg.chat.id;
const work = match[1]; // перехваченный "whatever"
const startDate = new Date();
const entries = await getEntries();
entries.push({
work,
startDate: startDate.toJSON(),
endDate: null,
duration: null,
});
writeFile(entries);

bot.sendMessage(chatId, `You started ${work} at ${startDate.toLocaleString()}. Go ahead!`);
});


bot.onText(/\/done(.*)/, async (msg, _match) => {
const chatId = msg.chat.id;
const endDate = new Date();

const entries = await getEntries();
if (entries.length < 1) {
bot.sendMessage(chatId, `You didn't start any work that can be ended. Uff.`);
return;
}
const targetEntry = entries[entries.length - 1];
targetEntry.endDate = endDate.toJSON();
const startDate = new Date(targetEntry.startDate);
const diff = format(endDate.valueOf() - startDate.valueOf(), { leading: true, });
targetEntry.duration = diff;

writeFile(entries);

bot.sendMessage(chatId, `You finished ${targetEntry.work} at ${endDate.toLocaleString()}. It took ${diff}. Congrats!`);
});

bot.onText(/\/state(.*)/, async (msg, _match) => {
const chatId = msg.chat.id;
const endDate = new Date();

const entries = await getEntries();
if (entries.length == 0) {
bot.sendMessage(chatId, `You don't have any work saved yet. Please start by using /work $myTodo`);
return;
}
const targetEntry = entries[entries.length - 1];
if (!targetEntry.endDate) {
bot.sendMessage(chatId, `Your current task is ${targetEntry.work} from ${endDate.toLocaleString()}.`);
return;
}
bot.sendMessage(chatId, `You recently finished ${targetEntry.work} at ${endDate.toLocaleString()}. It took ${targetEntry.duration}.!`);
});

bot.onText(/\/print(.*)/, async (msg, _match) => {
const chatId = msg.chat.id;

const entries = await getEntries();
let str = "work;startDate;endDate;duration;\n";
entries.forEach(e => {
str += Object.keys(e).map(k => e[k]).join(delimiter);
str += '\n';
});
bot.sendMessage(chatId, str);

});

Команда work добавляет запись с начальной датой startDate и соответствующий текст, внесенный в /work. Команда /done проверяет, есть ли в файле хотя бы одна запись, и устанавливает дату окончания endDate и продолжительность. Аналогично функционируют обработчики /state и /print, выполняя вышеописанные действия. Вот и все! 

Кода не так много, но зато какое классное чувство испытываешь при работе с этим инструментом! Я запустил его в режиме отладки и убедился, что все функционирует как надо. Теперь переходим к последнему этапу: развертывание.  

Развертывание инструмента для учета времени в Telegram 

Я добавил Dockerfile с многоэтапной сборкой для запуска на любом хосте. Также для развертывания можно передать токен как переменную env или что-то подобное. Самый простой способ запустить его на сервере (с настройкой Docker)  —  отправить свой репозиторий на сервер, а затем создать и запустить контейнер (после изменения токена на токен бота): 

  • docker build -t telegram-timetracker:latest;
  • docker run -ti --rm telegram-timetracker:latest;

Можно установить токен через переменную env во избежание редактирования кода, но этим мы добьемся лишь незначительной оптимизации. Весь репозиторий находится по ссылке. Как по мне, так это превосходный способ вести учет выполнения задач.

Читайте также:

Читайте нас в TelegramVK и Яндекс.Дзен


Перевод статьи Marten Gartner: Time-Tracking via the Telegram Bot API and Webhooks

Предыдущая статья10 лайфхаков для Linux, которые повысят продуктивность
Следующая статьяКак легко управлять зависимостями в монорепозитории JS