WhatsApp бот с API Google Таблиц на NodeJS

Пошаговое руководство
Репозиторий с исходником бота
Опубликовано: 20 ноября 2020

В этом гайде мы покажем принципы работы и настроим API Google Sheets. Создадим Whatsapp бота на NodeJS, используя наш шлюз, который будет связан с Гугл Таблицами.

В данном примере бот будет реагировать на команды, поступающие ему в виде обычных сообщений в WhatsApp и отвечать на них либо выполнять команду. Не забудьте скачать готового бота с нашего репозитория и использовать его в своей работе!

Какой функционал у данного бота?

  1. Запись информации в таблицу;
  2. Считывание информации из таблицы;
  3. Рассылка сообщений по номерам из таблицы;
  4. Отправка файла из ячейки таблицы;

Подготовка и настройка сервисов Google

Официальная статья "Быстрый старт" на NodeJS от Google содержит в себе пример работы с API. Возьмем её за основу.

Настройка аккаунта на работу с API

Прежде всего необходимо зайти на сайт разработчиков и создать новый проект.

После создания проекта, необходимо выбрать вкладку "Библиотека".

И в списке выбрать Google Sheets API. После чего нужно нажать на кнопку "Включить" ("Enable")

Должно произойти перенаправление на страницу настроек данного API. На этой странице, необходимо будет создать доступы. Нажимаем на кнопку "Создать учетные данные" ("Create credentials").

В появившемся окне нужно выбрать настройки для сервисного аккаунта и нажать на кнопку "Выбрать тип учетных данных". Рекомендуем использовать такие, как на скриншоте:

Далее необходимо задать имя аккаунту и роль. Необходимо выбрать роль Редактор.

Тип ключа - JSON. Нажимаем продолжить. Будет предложено скачать JSON файл с данными. Сохраняем его в папку с проектом, переименовываем в keys.json для более удобного обращения к нему из проекта.

На этом настройка аккаунтов практически закончена. Осталось лишь назначить в качестве редактора только что созданный аккаунт в нашей Google таблице. Для этого откроем её и нажмем на кнопку "Настройки доступа".

Необходимо добавить аккаунт, который мы создали в качестве редактора данной таблицы. Для этого нужно посмотреть его email в консоли разработчика во вкладке "Учетные данные" и скопировать почту.

На этом настройку сервисов можно считать законченной. Перейдем к самому проекту.

Создадим модуль для работы с Google API

Для начала создадим файл config.js в котором мы будем хранить конфигурационные данные для бота. И запишем в него ID нашей Google таблицы. Посмотреть его можно в адресной строке.

config.js

module.exports = {
    spreadid:"1M6fyEhv64ug7zILRz86H1PBKEKHUgKV9pWSW2m_r4SI",  // ID Google таблицы
}

После чего создадим файл Googleapi.js. В нем у нас будут храниться все функции и данные по работе с API Google. Для начала необходимо установить модуль по работе с Google Api для NodeJS.
Введем команду npm install Googleapis@39 --save в терминале, для установки данного модуля. В самом файле импортируем зависимости.

const config = require("./config.js");
const {Google} = require('Googleapis');
const keys = require('./keys.json');

И создадим объект клиента, который будет авторизовывать нас в Google.

const client = new Google.auth.JWT(
    keys.client_email,
    null,
    keys.private_key,
    ['https://www.Googleapis.com/auth/spreadsheets']
) //Json Web Token

Параметры, которые принимает функция JWT:

  • Email из json файла с доступами;
  • Путь до файла с закрытым ключом (мы его не передаем, поэтому null);
  • Приватный ключ из json файла с доступами;
  • Список доступов. У нас из доступов только Google sheets. При необходимости передаются в этот список и другие API от Google.

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

client.authorize(function(err, tokens) {
    if (err){
        console.log(err);
        return;
    }

    console.log('Connected Google Sheets Api!');
    gsrun(client);
});

let gsapi;

async function gsrun(cl){
    gsapi = Google.sheets({version:'v4', auth:cl})
}

Если все успешно прошло, то выводим в консоль "Connected Google Sheets Api!" и записываем в объект gsapi класс Sheets, который в параметры принимает версию используемого API и объект Client, который мы создавали ранее. После этого нам остается описать функции, которые будут работать с данными.

Метод для получения данных

async function getValues(range)
{
    const opt = {
        spreadsheetId: config.spreadid,
        range : range
    }

    let data = await gsapi.spreadsheets.values.get(opt);
    let dataArray = data.data.values;

    return dataArray;
}

Для того, чтобы получать данные из таблицы, мы напишем функцию. В параметры она принимает диапазон ячеек в таком формате: "Лист1!A1:B2", где Лист1 - это имя вашего листа в таблице. Будьте, внимательны, когда указываете этот параметр.

opt - Это словарь параметров, которые мы передаем в запрос к Api Google.

  • spreadsheetId - id таблицы;
  • range - Диапазон значений, откуда извлекать информацию;

Для того, чтобы извлечь данные из таблицы, opt нужно передать в метод gsapi.spreadsheets.values.get(opt);

Данный метод возвращает всю информацию о запросе, а конкретно данные хранит в data.values

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

async function getLastRow() // Получить номер последней строки в таблице
{
    const opt = {
        spreadsheetId: config.spreadid,
        range: 'Data!A1:A'
    }
    let response = await gsapi.spreadsheets.values.get(opt);
    return response.data.values.length;
}

Его суть в том, чтобы получить все данные из диапазона A1:1 - То есть до конца таблицы, а потом просто вернуть длину получившегося массива.

Метод по записи данных

async function updateSheet(name, phone) // Записать в последнюю строку таблицы данные.
{
    let lastRow = await getLastRow() + 1;
    const opt = {
            spreadsheetId : config.spreadid,
            range: 'Data!A' + lastRow,
            valueInputOption:'USER_ENTERED',
            resource: {values: [[name, phone]]}
    }
    await gsapi.spreadsheets.values.update(opt);
}

В параметры принимает имя и телефон (Их мы будем хранить в таблице). Также словарь opt теперь содержит дополнительные параметры в виде наших данных. Обратите внимание, что values - это массив массивов. Так, мы можем передавать диапазон данных, а не только одну строку. Для записи используется метод update.

На этом наша работа с GoogleApi закончена, осталось лишь экспортировать методы по работе с Api, чтобы мы могли их вызывать из другого класса.

module.exports.updateSheet = updateSheet;
module.exports.getValues = getValues;
module.exports.getLastRow = getLastRow;

Работа с WhatsApp API

Внимание!
Чтобы бот работал, телефон должен быть всегда подключен к интернету и не должен использоваться Whatsapp Web.

В самом начале, сразу свяжем whatsapp с нашим скриптом, чтобы по мере написания кода - проверять его работу. Для этого переходим в личный кабинет и получаем там QR-код. Далее открываем WhatsApp на мобильном телефоне, заходим в Настройки -> WhatsApp Web -> Сканируем QR-код.

Для работы с Whatsapp API вам потребуется token и Uri из вашего личного кабинета. Вы найдете их в "шапке" вашего инстанса.

Запишем их в конфигурационный файл:

module.exports = {
    apiUrl: "https://eu115.chat-api.com/instance12345/", // URL адрес для обращений к API
    token: "1hi0xwfzaxsews12345", // Токен для работы с API из личного кабинета
    spreadid:"1M6fyEhv64ug7zILRz86H1PBKEKHUgKV9pWSW2m_r4SI",  // ID Google таблицы
}

После чего создадим файл index.js. Он будет содержать всю логику работы бота и сервер по обработке запросов от Weebhook. Импортируем все зависимости.

const config = require("./config.js");
const Googleapi = require("./Googleapi.js");
const token = config.token, apiUrl = config.apiUrl;
const menu_text = config.menuText;
const app = require('express')();
const bodyParser = require('body-parser');
const fetch = require('node-fetch');
  • node-fetch позволит совершать запросы к API, config подгрузит наши данные с другого файла;
  • token и apiUrl наши данные из конфигурационного файла, которые позволяют обращаться к WA Api;
  • Модуль Express нужен для развертывания веб-сервера, который будет обрабатывать запросы;
  • body-parser позволит удобно извлекать поток входящих запросов;
  • Googleapi - наш модуль по работе с GoogleApi;

Далее говорим серверу, что будем парсить Json данные:

app.use(bodyParser.json());

Вешаем обработчик ошибок:

process.on('unhandledRejection', err => {
    console.log(err)
});

И описываем функцию, которая будет работать с Whatsapp Api.

async function apiChatApi(method, params){
    const options = {};
    options['method'] = "POST";
    options['body'] = JSON.stringify(params);
    options['headers'] = { 'Content-Type': 'application/json' };

    const url = `${apiUrl}/${method}?token=${token}`;

    const apiResponse = await fetch(url, options);
    const jsonResponse = await apiResponse.json();
    return jsonResponse;
}

В параметры данная функция принимает метод, который необходимо выполнить и объект с параметрами, которые мы будем передавать в запросе. Внутри функции создаём объект options, который сразу пополняем двумя ключами: json и method. В первом мы передаём параметры, необходимые для API, а во втором указываем метод, с котором обращаемся и в котором хотим получить ответ. Далее мы объявляем константу – наш url адрес для обращения к API. Он будет содержать в себе, собственно, сам url (из конфига), метод и токен. После этого посылаем запрос к Chat-Api.

Теперь, когда у нас функция готова, можем описать основную логику работы бота. Опишем обработчик, на который будут приходить данные от webhook.

app.post('/', async function (req, res) {
    const data = req.body;
}

Данная функция - это и есть обработчик, который обрабатывает POST запросы по основному адресу сервера (За это отвечает '/' - путь). data - это пришедший json файл.

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

app.post('/', async function (req, res) {
    const data = req.body;
    for (var i in data.messages) {
        const body = String(data.messages[i].body.toLowerCase());
        const chatId = data.messages[i].chatId;
        splitBody = body.split(' ');
        command = splitBody[0];

        if(data.messages[i].fromMe)
            return;

        if(command == 'помощь')
        {
            await apiChatApi('sendMessage', {chatId:chatId, body: menu_text});
        }
        else if (command == 'запись')
        {
            name = splitBody[1];
            phone = splitBody[2];
            await Googleapi.updateSheet(name, phone)
            await apiChatApi('sendMessage', {chatId:chatId, body: 'Успешно записано'})
        }

        else if (command == 'инфо')
        {
            let result;
            if (splitBody.length == 1){
                result = await getInfoDataFromSheet('A2:D2');
            }
            else{
                result = await getInfoDataFromSheet(splitBody[1]);
            }
            x = await apiChatApi('sendMessage', {chatId:chatId, body: result})
            console.log(x);
        }

        else if (command == 'файл')
        {
            linkFile = (await Googleapi.getValues('Data!D2'))[0][0];
            x = await apiChatApi('sendFile', {chatId:chatId, body: linkFile, 'filename':'testfile'})
        }

        else if (command == 'рассылка'){
            lastRow = await Googleapi.getLastRow() + 1;
            dataAll = await Googleapi.getValues('Data!A2:D' + lastRow);
            dataAll.forEach(async function(entry){
                await apiChatApi('sendMessage', {phone:entry[1], body: `Привет, ${entry[0]}, это тестовая рассылка.`});
            });
        }

        else
        {
            await apiChatApi('sendMessage', {chatId:chatId, body: menu_text})
        }
    }
    res.send('Ok');
});

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

Заполнение ячейки

Для записи данных в таблицу мы берем сообщение, разбиваем его с помощью метода split и передаем имя и телефон в функцию, которую мы написали для работы с Google API.

 else if (command == 'запись'){
    name = splitBody[1];
    phone = splitBody[2];
    await Googleapi.updateSheet(name, phone)
    await apiChatApi('sendMessage', {chatid:chatId, body: 'Успешно записано'})
}

Прочитать из ячейки, получить информацию

Для получения данных мы либо передаем входящий диапазон данных из сообщения, либо если пользователь не отправил диапазон, то отправляем стандартный A2:D2

 else if (command == 'инфо'){
    let result;
    if (splitBody.length == 1){
        result = await getInfoDataFromSheet('A2:D2');
    }
    else{
        result = await getInfoDataFromSheet(splitBody[1]);
    }
    await apiChatApi('sendMessage', {chatId:chatId, body: result})
}

Функция GetInfoDataFromSheet просто формирует строку из массивов данных, которые вернул нам GoogleApi.

async function getInfoDataFromSheet(range){
    data = await Googleapi.getValues('Data!' + range);
    result = "";
    data.forEach(function(entry) {
        result += entry.join(' ') + "\n"
    });
    return result;
}

Отправить файл в WhatsApp

Для отправки файла мы берем прямую ссылку на файл из ячейки таблицы и отправляем с помощью метода sendFile.

 else if (command == 'файл'){
    linkFile = (await Googleapi.getValues('Data!D2'))[0][0];
    x = await apiChatApi('sendFile', {chatId:chatId, body: linkFile, 'filename':'testfile'})
}

Рассылка в WhatsApp

Для рассылки мы просто проходимся по всей таблице и отправляем сообщения на указанные номера. Для тестирования рассылки в таблицу добавили наш номер в две строки.

Внимание!
Мы призываем наших клиентов не отправлять нежелательные сообщения, делать массовые маркетинговые рассылки. Иначе ваш аккаунт может быть заблокирован антиспам-системой WhatsApp! Свяжитесь с нашей тех.поддержкой для уточнения рекомендаций.
else if (command == 'рассылка'){
    lastRow = await Googleapi.getLastRow() + 1;
    dataAll = await Googleapi.getValues('Data!A2:D' + lastRow);
    dataAll.forEach(async function(entry){
        await apiChatApi('sendMessage', {phone:entry[1], body: `Привет, ${entry[0]}, это тестовая рассылка.`});
    });
}

Все тесты закончены удачно. Теперь мы можем загрузить нашего бота на сервер и установить Webhook.

Weebhook

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

Но, если мы укажем адрес Webhook сервера, то данная необходимость перестанет быть актуальной. Сервера сами будут присылать уведомления о входящих изменениях, как только они появятся. А задача Webhook сервера их принять и правильно обработать, реализовывая логику бота. Указывать можно как домен, так и ip адрес.

Поэтому сейчас нам необходимо загрузить бота на хостинг (выделенный сервер) и запустить его. Когда мы это сделаем, то укажем домен или IP адрес в личном кабинете в качестве вебхука и протестируем работу бота.

При запуске должно появляться сообщение с успешным подключением:

Whatsapp бот в связке с Google Sheets готов

Итак, мы описали работу простого ватсап чатбота и выложили исходник с готовым функционалом на github.

Весь код и гайд будет доступен по ссылке: https://github.com/chatapi/whatsapp-google-sheets-bot-ru

Подготовили мануал о том, как написать вацап бота на nodejs в связке с Гугл Таблицами

Вам необходимо только подставить в коде свой токен из личного кабинета и номер инстанса.

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