Репутація українського ІТ. Пройти опитування Асоціації IT Ukraine
×Закрыть

Telegram бот на Google Apps Script

Привіт! Ми з друзями працюємо в компанії Infopulse та у вільний час ведемо свій канал в Telegram про тестування ПЗ. І наразі в нас виникла необхідність створити бота для зворотнього зв’язку з читачами. Як всі ліниві люди, перше, що ми зробили — почали шукати готові рішення. На наш подив — вибір виявився не дуже багатим, і ми не знайшли жодного, що б задовольнив наші вимоги. Ми на деякий час закинули ідею, але я постійно думав над реалізацією власного боту. І згадав, що вже давно хотів написати щось про Google Apps Script.

Хто не в курсі — ця фіча з’явилась в Google Drive ще 10 років тому. Дозволяє писати JS скрипти у вбудованому редакторі і взаємодіяти майже з усіма сервісами Google: Drive, Docs, Gmail, Calendar. І виконувати на стороні серверу! Але незважаючи на всю крутизну ідеї — я не міг придумати їй серйозного застосунку. Я знаю, що є багато плагінів для Google Docs, створених в цьому сервісі і сам мав кілька сценаріїв використання:

  • В часи до Google Classroom ми створили свій навчальний сайт на Google Sites з простим бекендом на Apps Script. Він вмів приймати і роздавати домашки, керував правами доступу до сторінок.
  • Ми з колегами колись ходили в кафе неподалік від роботи де кожного дня було різне меню на обід — я написав скрипт, що кожного дня дізнавався меню, робив Google Form’у та відправляв її всім коллегам; і за 15 хв до обіду я телефонував і робив замовлення за результатами опитування, що отримував на пошту. Був же час та натхнення...

Але я відволікся від суті — нарешті в мене є задача, яку може виконувати Apps Script. Я погуглив і знайшов кілька готових рішень, почав вивчати їх та API телеграму, дебажити (сюрприз, чужий код в Інтернеті не завжди працює). А ще я записав відео процесу створення бота:

Отже, є 2 режими роботи з телеграм-ботом: періодично запитувати в нього, чи є нові повідомлення або налаштувати webhook, щоб бот сам повідомляв за вказаною адресою, коли йому пишуть.

Звісно, 2-й спосіб швидше та гарніший, тому будемо використовувати саме його. На щастя, Apps Script вміє працювати як веб сервіс.

Відкриваємо Google Drive та створюємо новий файл Apps Script. Якщо у вас немає такого типу на вибір, натисніть кнопку «підключити інші додатки».

Далі напишемо 2 функції, що будуть відповідати за обробку GET та POST запитів. Нам не важливо, що вони відповідають, головне, щоб код відповіді завжди був 200 ОК:

function doGet(e) {
  return HtmlService.createHtmlOutput('hello');
}

function doPost(e) {
  return HtmlService.createHtmlOutput('hello');
}

Тепер розгорнемо веб додаток і зробимо його доступним усім в Інтернеті, навіть анонімам (там є така опція). І надамо скрипту дозвіл на користування вашими Google сервісами (чим більше сервісів, тим більше прав, трохи пізніше побачите). В результаті отримаємо унікальний URL нашого сервісу. До речі, після кожної зміни коду треба робити публікацію нової версії, інакше ваш веб сервіс буде працювати на старій версії коду.

Важливо! Тримати цей URL в секреті, щоб вам не слали спам в бота.

Тепер створимо бота в телеграм та отримаємо його токен (теж секретна інфа).

Власне, цього вже достатньо для налаштування webhook. Я напишу 2 функції — першу, щоб встановити хук, іншу — щоб перевірити:

const token = 'TELEGRAM BOT TOKEN';
const tgBotUrl = 'https://api.telegram.org/bot' + token;
const hookUrl = 'GOOGLE APPS SCRIPT WEB SERVICE URL';

function setWebHook() {
  let response = UrlFetchApp.fetch(tgBotUrl + "/setWebhook?url=" + hookUrl);
  Logger.log('telegram response status is ' + response.getResponseCode());
}

function getWebHook() {
  let response = UrlFetchApp.fetch(tgBotUrl + "/getWebhookInfo");
  if (response.getResponseCode() == 200) {
    let data = JSON.parse(response.getContentText())
    Logger.log('current webhook url is ' + data.result.url);
  } else {
    Logger.log('telegram response status is ' + response.getResponseCode());
  }
}

Готово! Тепер все, що напишуть вашому боту, буде надіслано за вказаною адресою та оброблено функцією doPost(e). А перевірити статус хуку можна не тільки в Apps Script, а і просто в браузері, чи в Postman — оскільки це просто GET-запит.

Наступний крок — перевірити, що наш хук працює. Змусимо його на кожне повідомлення відповідати hello world. Ми перетворимо вхідне повідомлення на json, візьмемо з нього id чату, та сформуємо пейлоад відповіді. Далі я створив допоміжну функцію sendMessage(payload), що вказує метод передачі, тип даних, додає пейлоад та надсилає в телеграм. Зверніть увагу, функція doPost(e) має завжди повертати 200 ОК. Тепер можно опублікувати нову версію нашого скрипта і перевірити бота!

const token = 'TELEGRAM BOT TOKEN';
const tgBotUrl = 'https://api.telegram.org/bot' + token;

function doPost(e) {
  let content = JSON.parse(e.postData.contents);
  let payload = {
      chat_id: content.message.chat.id,
      text: 'Hello world'
    }
  
    sendMessage(payload);
    return HtmlService.createHtmlOutput();
 }

function sendMessage(payload){
  let options = {
    'method' : 'post',
    'contentType': 'application/json',
    'payload': JSON.stringify(payload)
  }
  return UrlFetchApp.fetch(tgBotUrl + "/sendMessage", options);
}

Настав час підвищити нашого бота на Feedback бота. Мої вимоги до нього такі:

КОЛИ користувач пише боту /start
ТОДІ бот відповідає вітальною фразою

КОЛИ користувач пише боту повідомлення
ТОДІ його повідомлення зберігається у файл
ТОДІ його повідомлення пересилається в адмінський чат

КОЛИ учасник адмінського чату відповідає на повідомлення бота (робить reply)
ТОДІ відповідь адміна пересилається тому, хто написав відгук

Зберігати повідомлення ми будемо в Google Spreadsheets. А пересилати повідомлення функцією телеграма sendMessage, а не forwardMessage. Так трохи не логічно, але на те є причина — налаштування приватності! Якщо користувач «закрився», то йому не можна написати за форвардом його повідомлення. А оскільки я не хочу ускладнювати бота, зберігаючи id в файл та шукаючи його потім по номеру повідомлення, я вибрав лінивий варіант і буду просто передавати номер чата в повідомленні бота. Готувати повідомлення буду в окремій функції prepareNotification(content). А у відповіді буду перевіряти, чи є там об’єкт reply_to_message та за допомоги регулярного виразу брати з повідомлення id чату. Для того, що відрізняти наш адмінський чат від інших чатів, куди можуть додати бота, заздалегідь напишемо функцію збереження повідомлення в файл:

const sheetLogId = 'GOOGLE SHEETS FILE ID';

function saveMessage(message) {
  let file = SpreadsheetApp.openById(sheetLogId);
  // first tab of the file
  let sheet = file.getSheets()[0];
  // get last row
  let lastRow = sheet.getLastRow() + 1;
  
  sheet.setActiveSelection('A' + lastRow).setValue(Date(message.message.date)); // date
  sheet.setActiveSelection('B' + lastRow).setValue(message.message.chat.id); // chat id
  sheet.setActiveSelection('C' + lastRow).setValue(message.message.from.username); // username
  sheet.setActiveSelection('D' + lastRow).setValue(message.message.text); // message
  sheet.setActiveSelection('E' + lastRow).setValue(JSON.stringify(message)); // json for debug
}

Після публікацію нової версії сервісу і надання йому прав на роботу з таблицями Google, додаємо бота в наш чат, пишемо йому там і відповідаємо. В json повідомлення знаходимо id бота та адмінського чату. Все готово, оновлюємо логіку основної функції:

function doPost(e) {
  let content = JSON.parse(e.postData.contents);
  
  // handle admin chat
  if (content.message.chat.id == adminChatId) {
    // handle replies
    if(content.message.reply_to_message != undefined) {
      // any replies
      if(content.message.reply_to_message.from.id != botId) {
        // do nothing
        return HtmlService.createHtmlOutput();
      } 
      // replies to bot
      else {
        let re = /chatId:\S*/;
        let chatId = content.message.reply_to_message.text.match(re)[0].split(':')[1];
        
        let payload = {
          chat_id: chatId,
          text: content.message.text
        }
  
        sendMessage(payload);
      }
    }
    return HtmlService.createHtmlOutput();
  }
  
  // handle /start
  if (content.message.text == '/start') {
    // send response 
    let payload = {
      chat_id: content.message.chat.id,
      text: 'Дякуємо, що написали нам! Ми прочитаємо ваш відгук і відповімо повідомленням в цьому боті️'
    }
  
    sendMessage(payload);
    return HtmlService.createHtmlOutput();
  }
  
  saveMessage(content);
  
  // notify admins
  payload = {
    'chat_id': adminChatId,
    'text': prepareNotification(content)
  }
  sendMessage(payload); 
  
  //return http 200 OK
  return HtmlService.createHtml
}

function prepareNotification(content) {
  return "користувач @" + content.message.from.username + "\n" +
         "chatId:" + content.message.chat.id + "\n" +
         "написав:\n" + content.message.text;
}

Ну от і майже все. Для зручності, в App Script я розподілив код по окремим файлам, в залежності від його функцій. Імпорти робити не потрібно, у всіх файлів спільна область видимості. Можна публікувати останню версію сервісу і поділитися посиланням на бота.

Хоча свою функцію бот вже виконує, в планах додати обробку стікерів та файлів а також кнопку з посиланням на себе, що буде автоматично з’являтись під кожним постом каналу.

А ось власне і наш канал, де ми користуємось ботом. Заходьте потестити ;)

Корисні посилання:

LinkedIn
Допустимые теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter
Допустимые теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter

А можно ли в пределах google api script сделать так, чтобы с бота в админский чат пересылались фото и файлы отправленные пользователями, и чтобы админы также могли ответить фото в бот?

Робив собі подібне для вайберу. Дружина пише денний дохід боту, той склада його до таблиці, таблиця рахує податки... Зекономив 300 грн на послугах бухгалтера.

OFFTOP. Бачу ви розумієтесь на Google Apps Script =). Якийсь час назад я шукав можливість скопіювати файли з рошарених папкок GDrive на свій GDrive (щоб у випадку, коли доступ відмінять або аккаунт видалять — не втратити файли). Навіть знайшов якийсь скрипт але він не працював.

(сюрприз, чужий код в Інтернеті не завжди працює).

То ж може у вас є схожий скрипт або знаєте плагін якийсь, який виконує такі функції. Та і взагалі функції копіювання фалів впринципі (не тільки з розшарених папок). Звісно, що переміщення файлів не вирішує проблему а то і взагалі папки можуть бути доступні тільки для читання.

Соррі, так руки і не дійшли :(

function start() {
  var sourceFolder = "id";//ID папки которую копируем
  var targetFolder = "folder";
  var source = DriveApp.getFolderById(sourceFolder);
  var target = DriveApp.createFolder(targetFolder)
  
  if (source.hasNext()) { 
    copyFolder(source.next(), target);
  }
}

function copyFolder(source, target) {
  var folders = source.getFolders();
  var files   = source.getFiles();
  
  while(files.hasNext()) {
    var file = files.next();
    file.makeCopy(file.getName(), target);
  }
  
  while(folders.hasNext()) {
    var subFolder = folders.next();
    var folderName = subFolder.getName();
    var targetFolder = target.createFolder(folderName);
    copyFolder(subFolder, targetFolder);
  }
}

Статья прикольная, но этот код :/ ...

if (content.message.chat.id == adminChatId) {
// handle replies
if(content.message.reply_to_message != undefined) {
// any replies
if(content.message.reply_to_message.from.id != botId) {

для начала не писать функции простыни, разбейте на более мелкие

так, трошки полінувався,думав, наочніше вийде і розбивати не став. Більш того, в мне doPost як раз на 1 скрін. Я думав, комент вище був про логіку...

Лінк на Apps Script docs веде на апі телеграму, а вцілому стаття кул!

Подписаться на комментарии