Будучи евангелистом разработчиков, я могу работать из любой точки мира. Часто я нахожусь в дороге, посещая крутые мероприятия и веду отличные беседы с разработчиками со всего мира.
Однако обратная сторона заключается в том, что большинство моих коллег также работают удалённо и обычно занимаются тем же самым в сообществах, которым они служат. Организовать встречи с ними или с кем-либо ещё, кто физически не находится в том же городе, стране или континенте, может быть сложно.
Обычно мы решаем эту проблему, проводя встречи по телефону, но одна из проблем, которая меня затрагивает, заключается в том, что моё расписание напоминает мне за 15 минут до начала, и я откладываю напоминание до тех пор, пока не останется 5 минут до встречи.
Иногда я случайно нажимаю кнопку «Отклонить», и меня напоминают через мессенджер о том, что я должен был быть на встрече, которая началась около одиннадцати минут назад.
Однажды утром я думал об этой ситуации и о том, что должен быть лучший способ решить эту проблему, чем добавлять множество напоминаний в календарь или телефон.
Мое первое решение было простым: «*Мне нужен личный помощник!*»
Давайте сделаем шаг назад — Twilio создан для того, чтобы облегчить мою жизнь, но я не думаю, что у меня достаточно встреч, чтобы оправдать найм личного помощника.
Будучи находчивым разработчиком, я подумал о следующем лучшем решении: создать собственного программного личного помощника. Что, если бы я мог написать скрипт, который отслеживает мой Google Календарь и, когда обнаруживает, что мне нужно позвонить кому-то, автоматически соединяет меня с этим человеком, набирая номера нам обоим?
«*Это звучит как то, что я смогу сделать за один день*», — сказал я!
Чтобы создать собственного личного помощника, нам понадобятся следующие компоненты:
— Аккаунт Twilio и один номер телефона Twilio
— Установленные Node.js и NPM
— Установленная MongoDB
— Учётные данные Google Календаря
Если вы не хотите проходить весь процесс создания, можете ознакомиться с репозиторием кода здесь.
Получение учётных данных Google Календаря может быть сложным, если вы никогда не работали с ними раньше. В следующих шагах я проведу вас через процесс получения этих учётных данных.
Начните с перехода в Google Developer Console и нажмите *Создать проект*.
На следующем экране вас попросят дать проекту имя. Мы назовём его *Twilio PA* и оставим *Идентификатор проекта* со значением по умолчанию.
После создания проекта нажмите на *API и аутентификация* в меню слева, затем нажмите *API*. Вы увидите большой список всех доступных API для этого проекта. Нас интересует *Google Календарь*, поэтому давайте включим его.
В меню слева нажмите на *Учётные данные* в разделе *API и аутентификация* и нажмите *Создать новый идентификатор клиента* в разделе *OAuth*. Поскольку мы создаём *Веб-приложение*, выберем этот вариант.
Нажмите на *Настроить экран согласия*, и вас перенаправит на страницу, где требуется указать ваш адрес электронной почты и *Название продукта*. Заполните это поле названием *Twilio PA* и нажмите сохранить.
Затем вам будет представлен модальный экран, где можно заполнить несколько URL-адресов. Для целей этой статьи наш проект будет работать на локальном хосте на порту 2002. Используйте следующие значения перед нажатием *Создать идентификатор клиента*:
Эта страница настроит наше приложение и предоставит учётные данные. Если вы видите экран, похожий на приведённый ниже, значит, вы успешно прошли процесс. Запишите *ИДЕНТИФИКАТОР КЛИЕНТА* и *СЕКРЕТ КЛИЕНТА*.
У меня есть несколько разных календарей на моём аккаунте. Обычно я разделяю личные встречи и рабочие, чтобы уделять рабочим встречам более высокий приоритет.
Поскольку я хочу быть уверенным, что никогда не пропущу рабочие звонки, я запишу идентификатор моего «рабочего» календаря. Самый простой способ узнать этот идентификатор — зайти в Google Календарь и нажать на *Настройки*. Настройки обозначены значком шестерёнки в правой части экрана. Теперь вы должны оказаться на экране *Настройки календаря*. Нажав на значок *Календари* в верхнем меню, вы попадёте в список всех календарей, которыми владеете. Нажмите на выбранный календарь, и на следующем экране вы увидите раздел *Адрес календаря*.
Запишите идентификатор календаря. Это идентификатор календаря, который мы хотим использовать.
Перейдите на страницу номеров Twilio и нажмите *Купить номер*, если у вас ещё нет доступного номера.
Выберите номер с возможностями голосовой связи и SMS и нажмите *Купить*.
Нажмите *Купить этот номер*, и вы увидите экран подтверждения.
Теперь мы успешно собрали все необходимые компоненты для создания этого приложения. Каждый раз, когда приложение запускается, оно будет собирать все запланированные встречи на этот день и создавать запланированные задачи в нашем экземпляре MongoDB, как показано на диаграмме ниже.
Как видно на диаграмме, для каждого события создаются две запланированные задачи. Эти задачи включают отправку SMS-сообщения за 5 минут до начала встречи и ещё одну задачу, которая запускается ровно в начале встречи и состоит из звонка на наш номер телефона, который затем соединяется с номером человека, с которым у нас назначена встреча.
Давайте начнём с создания небольшого приложения на Node.js, использующего Express для маршрутизации. Вы можете сделать это в любом месте, но в моём случае я создал директорию *twilio-pa* в *~/Projects/JavaScript/*.
Создайте файл в этой директории под названием *package.json*. Этот файл будет содержать определения проекта и все следующие зависимости:
Чтобы установить эти зависимости в папке проекта, перейдите в терминал и выполните команду:
Обратите внимание, что в вашей папке проекта появилась новая директория *node_modules* и в ней множество других директорий для каждой из указанных в *package.json* зависимостей.
Следующее, что мы сделаем, — создадим два новых файла *config.js* и *app.js* в корне нашего проекта, который в нашем примере находится в *~/Projects/JavaScript/twilio-pa*. Теперь у нас есть три файла в этой директории: *config.js* и *app.js*, которые пока пусты, и *package.json*.
В файл *config.js* мы добавим конфигурации для нашего приложения. Это учётные данные Google, которые мы собрали ранее, наш номер телефона Twilio и учётные данные. Вы можете получить свои учётные данные Twilio из панели управления.
Здесь мы настроили две переменные, о которых ещё не говорили. Одна из них называется *ownNumber* — это ваш номер телефона. Это номер, на который наше приложение будет отправлять SMS и звонить каждый раз перед началом звонка.
Другая конфигурация — это *mongoConfig* с информацией о нашем экземпляре MongoDB.
Отредактируйте *app.js*, чтобы убедиться, что наша настройка корректна и все зависимости правильно установлены.
Сохраните это и убедитесь, что ваш экземпляр MongoDB запущен.
В окне терминала введите:
Как уже упоминалось, в этом приложении мы будем взаимодействовать с базой данных для хранения некоторой информации аутентификации. Подключение к MongoDB осуществляется просто, но открывать и закрывать соединения каждый раз неудобно, поэтому мы создадим новый файл *connection.js* прямо в корне нашего приложения.
Этот файл будет управлять нашим соединением и гарантировать, что у нас всегда открыто только одно соединение, сколько бы раз мы ни обращались к базе данных.
Теперь у нас есть менеджер соединений, так что давайте немного рефакторим *app.js* в части инициализации. Нам нужно пройти аутентификацию через OAuth2, чтобы получить доступ к нашему Google Календарю, а также добавить зависимость к нашему новому менеджеру соединений. Мы делаем это, изменяя код, как показано в выделенном разделе:
Вся информация в разделе *config* уже доступна нам с момента включения *config.js* в наш файл.
Давайте также создадим класс *Calendar Event* выше указанного кода. Этот класс будет полезен, когда нам нужно будет передавать информацию о событиях.
Наша следующая задача — изменить существующий маршрут так, чтобы вместо возврата «*hello world*» он делал что-то более значимое.
Здесь происходит несколько вещей. Мы начинаем с проверки, есть ли уже какие-либо токены в базе данных. При первом запуске этого скрипта после старта нашего Node-сервера их не будет, поэтому мы всегда будем попадать в категорию *не аутентифицирован*. Это вызовет функцию *requestToken*, которую мы ещё не реализовали.
В следующий раз, когда мы запустим этот же скрипт, логика в начале функции сообщит нам, что у нас уже есть токены в базе данных, поэтому нам нужно только обновить их, вызвав *refreshToken*, которую мы также ещё не реализовали.
Нам нужно обновлять токен, потому что Google выдаёт нам токены, действительные только в течение 60 минут по соображениям безопасности. Если вы попытаетесь использовать их после этого времени, ваша аутентификация не удастся, так как токен уже недействителен. Обновление токена сообщает Google, что нам нужен доступ к этому аккаунту ещё на некоторое время, и Google выдаёт нам новый токен доступа, действительный ещё 60 минут.
Давайте реализуем некоторые из этих вспомогательных функций в новом файле *~/Projects/JavaScript/twilio-pa/token-utils.js*. Добавьте в него следующее.
Выше приведены несколько вспомогательных функций, которые мы используем для хранения и обновления наших токенов. После первой аутентификации в нашей базе данных всегда будет одна запись, и она всегда должна содержать токен обновления.
Функция *updateToken* имеет одну особенность: она обновляет только те данные, которые мы хотим обновить, вместо обновления всего документа. Это гарантирует, что наш токен обновления всегда присутствует и никогда не перезаписывается.
Google возвращает нам токен обновления только при первой аутентификации, поэтому мы должны убедиться, что сохраняем его в этот момент. Если вы по какой-то причине не сохранили его, вы всегда можете перейти в раздел Разрешения аккаунта Google и отозвать доступ для Twilio PA. В следующий раз при аутентификации вы получите новый токен обновления.
Продолжая, мы создадим две функции аутентификации, которые будут взаимодействовать с серверами аутентификации Google. Эти функции могут использоваться как в ситуации, когда мы ещё не аутентифицировались и нам нужен полный набор токенов, так и когда мы уже аутентифицировались и просто должны получить токены из базы данных.
Функция *authenticateWithCode* используется только при первой аутентификации. Она принимает код, который мы получили от серверов аутентификации Google, и генерирует набор токенов, которые мы сохраняем в базе данных. Эта функция также сообщает вызывающим функциям о завершении через колбэк. Этот колбэк особенно полезен, когда мы вызываем эту функцию в первый раз в *app.js* и должны убедиться, что аутентификация произошла до перенаправления пользователя.
Когда мы аутентифицируемся из базы данных с помощью *authenticateWithDB*, мы должны проверить, действителен ли ещё токен доступа, чтобы мы могли его использовать. Если он уже истёк, мы используем функцию *refreshToken*, как вы можете видеть ниже:
Функции *refreshToken* и *requestToken* позаботятся о запросе нового токена у Google или его обновлении, если он уже истёк. Эти функции — единственные две, которые фактически взаимодействуют с серверами аутентификации Google для получения или обновления токена.
Наконец, нам нужно убедиться, что некоторые из этих функций доступны за пределами этого файла, поэтому мы экспортируем их следующим образом:
Откройте *app.js* и добавьте зависимость к нашему новому файлу сразу после инициализации *oAuthClient*. Нам нужно сделать это на этом этапе, так как мы передадим ссылку на *oAuthClient* в наш новый файл, чтобы он мог её повторно использовать.
Серверу аутентификации Google нужно знать, куда нас перенаправить после проверки нашей аутентификации. Мы уже указали серверам аутентификации, куда перенаправлять, установив *Авторизованные URI перенаправления* в консоли разработчика на http://localhost:2002/auth, поэтому всё, что нам нужно сделать сейчас, — это создать маршрут *auth* в *app.js* под маршрутом «/».
Мы получаем код обратно из запроса и затем генерируем токен аутентификации, передавая его в функцию *authenticateWithCode*. Мы вызываем функцию *authenticateWithCode* с колбэком, так как это позволяет нам узнать, когда цикл аутентификации завершён.
Если аутентификация успешна, пользователь перенаправляется на наш основной маршрут «/».
В случае, если аутентификация не удалась, мы выводим ошибку в консоль, описывающую причину неудачи. Это может произойти из-за того, что вы ввели неправильный пароль или не предоставили нужные разрешения для просмотра этого календаря. У Google есть обширная документация по их потоку OAuth, если вы хотите узнать больше.
Последнее, что нам нужно сделать здесь, — это изменить инициализацию сервера так, чтобы он пытался аутентифицировать пользователя при запуске. Этот поток инициализации гарантирует, что у нас всегда будут свежие токены доступа сразу после загрузки базы данных.
Если вы видите экран, который просит выбрать аккаунт, скорее всего, вы уже вошли в систему. Нажав на пользователя, которому принадлежит выбранный календарь, вас перенаправит на экран с надписью «аутентифицировано».
Поздравляем! Мы прошли самую сложную часть этой статьи, и всё, что будет дальше, будет намного интереснее.
Наши запланированные задачи будут создаваться с использованием модуля Agenda. Agenda — отличный модуль для управления задачами, так как он сохраняет ваши задачи в базе данных, поэтому даже если вы перезапустите своё приложение, ваши задачи всё равно будут выполнены при следующем запуске.
В терминале создайте новый файл *job-schedule.js* в корневой директории приложения. В моём случае это *~/Projects/JavaScript/twilio-pa*.
Откройте этот файл и добавьте в него следующий код.
Вся информация для инициализации модуля считывается из нашего файла конфигурации. Если позже мы решим изменить одно из настроек, например, на каком порту работает MongoDB, всё, что нам нужно будет сделать, — это изменить файл конфигурации.
Создайте новую директорию в корне нашего приложения под названием *jobs*. Эта директория будет содержать определения для двух запланированных задач, которые мы хотим создать — Отправка SMS и Начало звонка.
В терминале вы можете создать директорию следующим образом:
В этой директории создайте файл *send-sms.js*. Этот файл является одним из определений наших запланированных задач и содержит всю логику для отправки исходящих SMS.
Мы снова используем наш файл конфигурации, а также импортируем библиотеку Twilio, которая значительно облегчит взаимодействие с API Twilio.
Весь код, выделенный в этом блоке, — это то, что мы хотим выполнить в нашей запланированной задаче.
Метод *send* принимает четыре аргумента: объект *Agenda*, инициализированный *job-schedule.js*, объект *CalendarEvent*, имя задачи и номер телефона.
В последнем выделенном блоке мы убедимся, что наши задачи запланированы на то время, когда мы хотим их запустить, как определено в объекте *CalendarEvent*, и также гарантируем, что наши задачи создаются уникально. Этот шаг очень важен, так как он гарантирует, что задачи не создаются снова и снова каждый раз при запуске скрипта. Он также позаботится о любых обновлениях этого события, таких как изменение номера телефона или времени.
Создайте ещё одну задачу под названием *start-call.js*. Эта задача похожа на только что созданную нами, но, как следует из названия, она отвечает за начало телефонных звонков. Эта задача будет запускаться во время встречи и соединять нас с человеком, с которым мы должны быть на звонке.
На этом этапе Twilio нужно будет иметь возможность подключаться к нашему локальному приложению. Мы могли бы обработать соединение двумя способами: либо развернув его на публичном веб-сервере, либо используя ngrok для предоставления доступа к нашему локальному окружению через туннелирование. Мы выберем второй вариант, чтобы упростить задачу. Мой коллега Кевин Уайнери написал отличный пост о том, как начать работу с ngrok.
После установки Ngrok подключите его к вашему приложению, открыв ещё одно окно терминала и выполнив команду:
Когда он запустится, он получит уникальный URL для нашего приложения, и это то, что мы будем использовать в качестве атрибута URL для метода *makeCall*.
Запишите этот URL перенаправления, так как он понадобится вам в коде ниже.
Снова откройте *app.js* в корне проекта и включите определения запланированных задач, которые мы только что создали. Они должны находиться между нашей *инициализацией* и *Объектом события*:
При инициализации сервера мы добавим ещё одну повторяющуюся запланированную задачу, которая будет получать любые события календаря каждые 10 минут. Таким образом, вам не нужно будет вручную запускать скрипт каждый раз.
Выделенные части — это те, которые мы только что добавили для повторяющейся задачи.
Вы заметите, что мы вызываем функцию под названием *fetchAndSchedule*. Эта функция отвечает за получение всех наших событий календаря каждый раз, когда её вызывает запланированная задача. Под функцией CalendarEvent разместите следующий код:
Когда возвращаются сегодняшние события, мы проходим по ним в цикле, чтобы получить их информацию.
Затем мы фильтруем эти события с помощью регулярного выражения, чтобы получить только те, которые содержат номер телефона в поле местоположения, а также проверяем события, происходящие после текущего времени. Каждое совпадение затем заполняет новый объект *CalendarEvent* информацией из события.
Наконец, мы настраиваем запланированные задачи, передавая информацию о событии в каждое из наших определений задач. Мы делаем это один раз для SMS и один раз для звонка.
Сейчас самое время создать наш маршрут */call*. Всё ещё в *app.js* добавьте следующее сразу после функции *fetchAndSchedule*.
Приведённый выше код воспроизводит сообщение и набирает номер человека, с которым у вас должна быть встреча.
На данный момент наше приложение готово, и мы должны иметь возможность запустить его и пройти аутентификацию.
Убедитесь, что у вас есть запись в календаре на сегодня и что в записи указан номер телефона в поле местоположения.
Снова запустите ваш Node-сервер.
Откройте браузер по адресу http://127.0.0.1:2002, и вы должны увидеть на экране слово «Authenticated». Если у вас есть записи в календаре на сегодня, скрипт подберёт их и добавит в базу данных.
Если вы хотите подтвердить, были ли добавлены новые записи в качестве запланированных задач, вы можете запросить MongoDB через терминал в новом окне терминала:
Все записи с именами и временными метками теперь будут перечислены. Конечно, вы заметите, что SMS-сообщение приходит за 5 минут до начала встречи, а затем звонок с вашего номера Twilio в момент начала встречи.
Вы можете сделать то же самое, чтобы запросить информацию о токенах, выполнив команду:
И вот так, всего за несколько шагов мы создали собственного личного помощника Twilio, который будет управлять нашими исходящими звонками и гарантировать, что мы больше никогда не опоздаем на 11 минут на этот звонок.
В идеале я бы хотел, чтобы мои напоминания о встречах были более настойчивыми и также запускали сирену в момент отправки мне SMS. Это точно вытащит меня из комнаты и подготовит к звонку. Как насчёт добавления интерфейса, который позволит вам видеть все будущие запланированные задачи? Возможно, это идея для Twilio PA версии 2.0.
Мне бы очень хотелось услышать о замечательных способах, которыми вы можете заставить вашего личного помощника Twilio выполнять больше работы за вас и позволить вам сосредоточиться на другой работе, которую не так просто автоматизировать.
Жду с нетерпением ваших наработок. Свяжитесь со мной в Twitter @marcos_placona, по электронной почте marcos@twilio.com или MarcosPlacona в G+, чтобы рассказать об этом подробнее.