Telegram-бот+GoogleSheets: решение некоторых проблем

Тема Telegram-ботов только ширится. Если вы разрабатываете дополнения для Google Sheets, то пишете код на Google App Script (GAS). Поэтому наверное, и мозги для этих ботов пишете на GAS.

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

Как всегда, в процессе практической реализации задуманного всплывают подводные камни. О некоторых из них и способах их обхода я и расскажу ниже. Но сначала немного теории.

getUpdates VS setWebhook?

Неважно, на каком языке вы пишете мозги для своего бота - на GAS, PHP, Pyton или чём-то ещё. В любом случае вы используете Телеграм API.

API сейчас поддерживает два способа обработки обновлений ботов: getUpdates и setWebhook.

getUpdates — это механизм pull, setWebhook — push.

Например, вы 1 раз в час запускаете getUpdates и получаете все сообщения, написанные в чате за этот час. В GAS можно настроить временной триггер с таким интервалом. Но это значит, что для нашего примера с антиспам-ботом спам будет целый час безнаказанно висеть в чате. Нехорошо это, желательно подстрелить спам сразу на подлёте.

Поэтому надо использовать setWebhook. Как написано в “Чудесном путеводителе Марвина по всем вещам Webhook”:

Webhook по сравнению с getUpdates

  • избавляет вашего бота от необходимости часто запрашивать обновления.
  • избегает необходимости в каком-либо механизме опроса в вашем коде.

setWebhook() + fetch(url)

Теперь пара слов о механизме получения апдейтов, или, проще говоря, сообщений из Телеграм-чата.

  • Пишем в редакторе GAS простейший скрипт:
function doPost(e) { var contents = JSON.parse(e.postData.contents); var chat_id = contents.message.from.id; // код извлечения данных и записи на лист }

Всё, в переменной contents у нас апдейт. С помощью дальнейшей обработки из него можно извлечь всё, что нужно. Например, chat_id - идентификатор отправителя сообщения, text - текст сообщения.

Дальше можно эти данные записать на лист, переслать кому-то и т.д.

  • Разворачиваем этот скрипт как веб-приложение.

При этом указываем параметры “Запуск от имени: От моего имени, У кого есть доступ: Все” и копируем ссылку на веб-приложение (webAppUrl).

  • Формируем ссылку.

Для этого надо соединить токен бота и урл веб-приложения следующим образом:

var token = "1234567890:ABCDEFGHIJKLMNOPQRSTUVWXYZ"; var webAppUrl = "https://script.google.com/macros/s/XXXXXXXX/exec"; var telegramUrl = "https://api.telegram.org/bot" + token;
  • Фетчим урл и устанавливаем webhook:
function setWebhook() { var url = telegramUrl + "/setWebhook?url=" + webAppUrl; var response = UrlFetchApp.fetch(url); }

Можно и безо всякого скрипта перейти по этому url в браузере и увидеть

{"ok":true,"result":true,"description":"Webhook is already set"}

Это объект JSON, разобрав который, можно извлечь все 3 поля по отдельности - ok, result и description. Затем их можно использовать в условной логике, показывать юзеру и всё такое.

Подводные камни

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

Но представьте, что вы, обрадованный, выложили ссылку на этого своего бота в широкий доступ и ждёте, что довольные юзеры будут вас благодарить за полезный инструмент.

Не тут-то было! Всем удобны Google-сервисы. И таблицы они вам предоставляют, и среду разработки скриптов, и мощные сервера по всему свету, на которых эти скрипты работают, и многие другие вкусные плюшки. Однако есть подводные камни, и один них - квоты и ограничения Google. “Службы Apps Script имеют ежедневные квоты и ограничения на некоторые функции. Если вы превысите квоту или ограничение, ваш скрипт выдаст исключение и выполнение остановится”. Например, вызовы URL Fetch ограничены 20,000 в день, а ограничение на одновременное выполнение скрипта - 30 юзеров. Это означает, что если число пользователей бота станет достаточно большим, он перестанет работать.

И что же делать?

Например, можно купить хостинг, поднять на нём собственный сервер и обрабатывать всю логику на нём. Это стоит дополнительных денег и времени, поэтому логично подумать про что-то другое.

Выход из положения

Разработчику может прийти в голову следующее.

Поскольку веб-приложение выполняется от имени меня, то и расходуются мои квоты. А что, если разворачивать его не от моего имени, а от имени пользователя?

Заинтересованный пользователь может это сделать в 3 клика. Надо только дать ему шаблон таблицы с внедрённым в него скриптом. Он сделает себе копию таблицы, развернёт скрипт как веб-приложение и вставит webAppUrl в нужную ячейку таблицы.

Теперь будут расходоваться квоты и ограничения не разработчика, а пользователя.

И очень маловероятно, что у этого пользователя в его Телеграм-группе будет больше 20,000 сообщений в день.

Всё, вроде бы, должно заработать. Не тут-то было! :-)

Снова камни

Каждый разработчик хочет, чтобы плоды его творчества приносили пользу. И ему, и пользователям.

Как распространять наш скрипт, чтобы о нём узнали заинтересованные пользователи? Можно писать статьи на тематических площадках, давать рекламу в Google, Яндекс и т.п.

Это всё замечательно, но грех не использовать и бесплатные инструменты.

Говоря о плюшках Google, нельзя не упомянуть Google Workspace Marketplace. Там разработчики Google публикуют дополнения для продуктов Google - для таблиц, документов, почты и пр.

Таким образом, идея реализации нашего Телеграм-бота вырисовывается такая:

  • Делаем шаблон таблицы Google.

В нём нарисован интерфейс с нужными заголовками, к таблице прикреплён скрипт веб-приложения.

  • Переносим всю логику обработки сообщений из Телеграм-чата в дополнение и публикуем его на Google Workspace Marketplace.
  • Пользователь устанавливает дополнение, делает себе копию шаблона, делает начальные установки (вводит в нужные ячейки токен бота, урл веб-приложения, свои ключевые слова) и пользуется себе на радость.

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

Тем более, что опять не тут-то было, здесь есть ещё подводные камни :-)

И опять

Проблема в том, что вы НИКАК не можете передать данные непосредственно в дополнение. Т.е. вам по вебхуку пришло сообщение, но вы в обработчик, который, напомню, вынесен в дополнение, это сообщение передать не можете.

Вы не можете запустить функцию, находящуюся внутри дополнения.

Вы даже не можете передать в дополнение какой-то тег или сигнал типа “Эй, адд-он! Пришло сообщение! Запускай fetch(url) и читай!”.

Низзя. Не знаю почему, то ли из соображений безопасности, то ли по ещё каким-то причинам. Об этом можно почитать тут: How to transfer data from webapp to addon.

Может прийти в голову мысль: “Мы же пишем сообщения на лист. Ставь триггер onEdit() или onChange() и, как только сообщение запишется в таблицу, триггер сработает, считает данные и всё обработает, как надо”.

Тут есть 2 закавыки.

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

В отличие от ситуации, если данные передавать минуя листы, непосредственно из fetch() в код дополнения. Сервера у Google шустрые, и такие операции занимают миллисекунды.

Вторая закавыка заключается в том, что триггеры onEdit() или onChange() НЕ срабатывают при изменении данных на листе программным путём.

Эти триггеры срабатывают, если данные на листе изменил пользователь. Например, ввёл с клавиатуры, или выбрал из меню.

Подробнее об этом тут: Event Objects.

Выход есть!

На вопрос в посте на Стеке всё же нашёлся ответ. Автор даже назначил баунти за правильный ответ, но самое лучшее, что тогда ему подсказали, это использовать облачный сервис Google. Да, среди вкусных плюшек есть и такая. И это наверняка мощная штука.

Вот только она платная при превышении определённых показателей.

Да и хороших мануалов по нему я не видел.

По GAS таких ресурсов полно. Это и Google Apps Script, и mdn web docs, и ещё куча.

По Google Cloud же таких ресурсов намного меньше.

Поэтому, когда через полгода в пост пришёл Alan Wells и написал про Sheets API и USER_ENTERED, то сразу всё прояснилось.

Схема такая:

  • Включаем Sheets API в сервисах веб-приложения. Вы можете убедиться, что всё правильно, если посмотрите код файла appsscript.json. Если кто забыл, то для этого в настройках скрипта чекните флажок “Показывать файл манифеста appsscript.json в редакторе”.

В нём должен присутствовать следующий код:

"dependencies": { "enabledAdvancedServices": [{ "userSymbol": "Sheets", "serviceId": "sheets", "version": "v4" }] }

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

  • Обычно мы как пишем данные в таблицу?

Используем setValue(value).

Типа такого:

var ss = SpreadsheetApp.getActiveSpreadsheet(); var sheet = ss.getSheets()[0]; var cell = sheet.getRange("B2"); cell.setValue(100);

В топку! Теперь используем Google Sheets API.

Код должен выглядеть примерно так:

var SHEETID = 'XXXXXXX'; function doPost(e) { var data = e.postData.contents; var rowValues = [ [data, ""], ]; var request = { 'valueInputOption': 'USER_ENTERED', 'insertDataOption': 'INSERT_ROWS', 'data': [ { "range": "TEMP!A2:B2", "majorDimension": "ROWS", "values": rowValues, }, ], }; var response = Sheets.Spreadsheets.Values.batchUpdate(request, SHEETID); }

Ключевыми здесь являются два момента:

А. Запись на лист производим с помощью batchUpdate().

Б. Параметр 'USER_ENTERED как бы говорит триггеру, что данные на листе изменены не программным путём, а пользователем.

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

Подробнее обо всём об этом можно почитать тут: Телеграм-Помощник: Описание и руководство. Там же есть ссылка на дополнение, и на таблицу со скриптом. Можете всё это свободно использовать.

И буду признателен за конструктивную критику и советы!

2727
11 комментариев

Спасибо.
Весь stackoverflow завален ответами на тему неработающих триггеров в стиле "onEdit() не работает при изменении данных на листе программно", и вот однако ж. Работает.

2
Ответить

onEdit() - простой триггер. у него упрощённые права.
как, в общем, и описано по ссылке указанной в статье:
https://developers.google.com/apps-script/guides/triggers

Если сделать такой же обработчик событий но с другим названием - будет работать.
function onEdit2(e) { и т.д.

Ответить

Интересно и актуально

1
Ответить

Зачем в здравом уме писать логику на GAS?
Триггеры и сериализация - да, окей, вынужденная мера.
Всё остальное - на нормальном языке и полноценном бэкенде, который можно нормально деплоить, дебажить и тестировать.
(Не говоря уж о том, зачем делать всё это через гуглотаблицы, если можно нет)

2
Ответить

Нормально не интересно.
Не уверен, что хотел бы разводить холивар на тему "Что есть полноценный язык, а что УГ".
И в GAS нет проблемы нормально деплоить, дебажить и тестировать, там вполне приличная GCP и Logs Explorer.

1
Ответить