Статьи

Как создать приложение напоминания о встрече с помощью Twilio

Эта статья была рецензирована Марком Таулером и Бруно Мота . Спасибо всем рецензентам SitePoint за то, что сделали контент SitePoint как можно лучше!

В этом уроке мы собираемся создать приложение для напоминания SMS с Node.js. Мы собираемся использовать календарь Google пользователя, чтобы получать встречи, а затем отправлять текстовые сообщения с помощью Twilio.

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

Настройка вещей

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

Вам не нужно беспокоиться о Twilio, вы можете попробовать бесплатно.

Консольный проект Google

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

Снимок экрана экрана консоли Google

Оттуда вы можете нажать на меню select project в правом верхнем углу и выбрать create a project . Это открывает модальное окно, которое позволяет вам ввести название проекта.

Скриншот формы New Project

Как только проект создан, панель инструментов отображается. Оттуда вы можете нажать на use Google APIs , найти API Календаря Google и включить его.

Снимок экрана с обзором API Календаря Google

Как только API будет включен, он попросит вас создать учетные данные. Нажмите « Go to Credentials данным», чтобы начать настройку. Это покажет вам следующее:

Снимок экрана: экран учетных данных API

Нажмите кнопку Add credentials , затем выберите OAuth 2.0 client ID .

Это попросит вас сначала настроить экран согласия. Нажмите на configure consent screen .

Введите значение в поле « Product name shown to users и нажмите « save .

Снимок экрана экрана согласия OAuth

После настройки вы можете создать идентификатор клиента. Выберите Web application для типа приложения, оставьте имя по умолчанию (если хотите), введите http://localhost:3000/login для Authorized redirect URIs затем нажмите кнопку « create .

Снимок экрана формы создания идентификатора клиента

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

Twilio

Создав учетную запись Twilio, перейдите на страницу настроек и запишите значения для AuthToken и AuthToken в Live API Credentials .

Далее перейдите на programmable voice dashboard . Здесь вы можете увидеть номер песочницы. Вы можете использовать этот номер для тестирования twilio. Но позже вам нужно будет купить номер телефона, чтобы к текстовым сообщениям, отправленным twilio, не добавлялись «отправленные из песочницы twilio» . Другим ограничением номера песочницы Twilio является то, что он может использоваться только с проверенными номерами. Это означает, что вы должны зарегистрировать номер телефона в twilio, чтобы отправить ему сообщение. Вы можете сделать это на manage caller IDs page .

Сборка приложения

Теперь мы готовы построить приложение. Прежде чем мы продолжим, я хотел бы дать краткий обзор того, как мы собираемся реализовать приложение. Всего будет три основных файла: один для сервера, один для кэширования событий из Календаря Google и один для напоминания пользователю. Сервер используется для разрешения входа пользователя и получения токена доступа. События будут сохранены в базе данных MySQL, а глобальная конфигурация приложения будет добавлена ​​в файл .json . Реализация Node cron будет использоваться для выполнения задачи для кэширования событий и напоминания пользователю.

Установка зависимостей

В вашем рабочем каталоге создайте файл package.json и добавьте следующее:

 { "name": "google-calendar-twilio", "version": "0.0.1", "dependencies": { "config": "^1.17.1", "cron": "^1.1.0", "express": "^4.13.3", "googleapis": "^2.1.6", "moment": "^2.10.6", "moment-timezone": "^0.4.1", "mysql": "felixge/node-mysql", "twilio": "^2.6.0" } } 

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

  • config — используется для хранения и извлечения глобальной конфигурации приложения.
  • cron — используется для выполнения определенной задачи в определенное время суток. В этом приложении мы используем его для запуска задачи кэширования событий из календаря пользователей Google и отправки текстовых напоминаний.
  • express — де-факто веб-фреймворк для Node.js. Мы используем его для обслуживания страницы входа.
  • googleapis — официальный клиент Node.js для API Google.
  • moment — библиотека даты и времени. Мы используем его для простого форматирования дат, которые мы получаем из API Календаря Google.
  • moment-timezone — плагин часового пояса для момента. Это устанавливает часовой пояс по умолчанию для приложения.
  • mysql — клиент MySQL для Node.js.
  • twilio — официальный клиент Twilio для Node.js. Это позволяет нам отправлять текстовые напоминания.

Выполните npm install с вашего терминала, чтобы установить все зависимости.

База данных

Как упоминалось ранее, мы собираемся использовать базу данных MySQL для этого приложения. Идите вперед и создайте новую базу данных, используя инструмент управления базой данных по вашему выбору. Затем используйте следующий файл дампа SQL для создания таблиц: appointment-notifier.sql .

В базе данных есть две таблицы: users и appointments . Таблица users используется для хранения данных пользователя. В случае этого приложения мы собираемся хранить только одного пользователя, и сохраняется только токен доступа.
Таблица appointments используется для хранения событий, которые мы получили из API Календаря Google. Обратите внимание, что в нем нет поля user_id потому что у нас только один пользователь. И мы собираемся извлечь все строки, которые имеют ноль в качестве значения для notified поля.

Конфигурация приложения

В вашем рабочем каталоге создайте папку config затем внутри нее создайте файл default.json . Здесь мы разместим глобальную конфигурацию приложения. Это включает часовой пояс, номер телефона, на который мы собираемся отправить напоминания, базу данных, приложение Google и настройки Twilio.

Вот шаблон, обязательно заполните все поля.

 { "app": { "timezone": "Asia/Manila" }, "me": { "phone_number": "" }, "db": { "host": "localhost", "user": "root", "password": "secret", "database": "calendar_notifier" }, "google":{ "client_id": "THE CLIENT ID OF YOUR GOOGLE APP", "client_secret": "THE CLIENT SECRET OF YOUR GOOGLE APP", "redirect_uri": "http://localhost:3000/login", "access_type": "offline", "scopes": [ "https://www.googleapis.com/auth/plus.me", "https://www.googleapis.com/auth/calendar" ] }, "twilio": { "sid": "YOUR TWILIO SID", "secret": "YOUR TWILIO SECRET", "phone_number": "+YOUR TWILIO PHONE NUMBER / SANDBOX NUMBER" } } 

Общие файлы

Как хорошие разработчики, мы должны избегать повторения кода как можно больше. Вот почему мы должны поместить код, необходимый для этих трех основных файлов (сервер, кеш, уведомление), которые я упоминал ранее, в отдельные файлы. Создайте common папку в вашем рабочем каталоге. Это где мы собираемся добавить общие файлы.

База данных

Создайте файл db.js в common каталоге и добавьте следующее:

 var config = require('config'); var db_config = config.get('db'); var mysql = require('mysql'); var connection = mysql.createConnection({ host: db_config.host, user: db_config.user, password: db_config.password, database: db_config.database }); exports.db = connection; 

При этом используется библиотека config, чтобы получить значения конфигурации, которые мы добавили ранее в файле config/default.json . В частности, мы получаем конфигурацию базы данных, чтобы мы могли подключиться к базе данных. Затем мы экспортируем этот модуль, чтобы потом использовать его из другого файла.

Время

Файл time.js используется для установки часового пояса по умолчанию с библиотекой moment-timezone . Мы также экспортируем значение для часового пояса, так как мы собираемся использовать его позже при запуске двух задач cron (кэширование событий и уведомление пользователей).

 var config = require('config'); var app_timezone = config.get('app.timezone'); var moment = require('moment-timezone'); moment.tz.setDefault(app_timezone); exports.config = { timezone: app_timezone }; exports.moment = moment; 

Google

Файл google.js используется для инициализации клиента Google и клиента OAuth2. Для инициализации клиента OAuth2 нам нужно передать идентификатор клиента, секрет клиента и URL-адрес перенаправления, который мы добавили в файл конфигурации ранее. Затем мы инициализируем службу Календаря Google. Наконец, мы экспортируем клиент OAuth2, календарь и конфигурацию Google.

 var config = require('config'); var google_config = config.get('google'); var google = require('googleapis'); var OAuth2 = google.auth.OAuth2; var oauth2Client = new OAuth2(google_config.client_id, google_config.client_secret, google_config.redirect_uri); var calendar = google.calendar('v3'); exports.oauth2Client = oauth2Client; exports.calendar = calendar; exports.config = google_config; 

Создание сервера

Теперь мы готовы работать на сервере. Сервер отвечает за получение токена доступа. Который можно использовать для общения с API Календаря Google без входа пользователя. Начните с создания файла server.js и добавления следующего:

 var google = require('./common/google'); var connection = require('./common/db'); var express = require('express'); var app = express(); var server = app.listen(3000, function () { var host = server.address().address; var port = server.address().port; console.log('Example app listening at http://%s:%s', host, port); }); function updateAccessToken(tokens, response){ connection.db.query( "UPDATE users SET access_token = ? WHERE id = 1", [JSON.stringify(tokens)], function(err, rows, fields){ if(!err){ console.log('updated!'); response.send('connected!'); }else{ console.log('error updating table'); console.log(err); response.send('error occured, please try again'); } } ); } app.get('/', function(req, res){ var url = google.oauth2Client.generateAuthUrl({ access_type: google.config.access_type, scope: google.config.scopes }); res.send('<a href="' + url + '">login to google</a>'); }); app.get('/login', function(req, res){ var code = req.query.code; console.log('login'); google.oauth2Client.getToken(code, function(err, tokens){ if(!err){ console.log('tokens'); console.log(tokens); updateAccessToken(tokens, res); }else{ res.send('error getting token'); console.log('error getting token'); } }); }); 

Разбивая это:

Сначала мы импортируем модуль google и db который мы создали ранее.

 var google = require('./common/google'); var connection = require('./common/db'); 

Создайте сервер Express, который работает на порту 3000 localhost. Вот почему мы добавили http://localhost:3000/login ранее в конфигурации приложения и в URI перенаправления для Google:

 var express = require('express'); var app = express(); var server = app.listen(3000, function () { var host = server.address().address; var port = server.address().port; console.log('Example app listening at http://%s:%s', host, port); }); 

Определите функцию updateAccessToken . Это принимает два аргумента: tokens и response . Токен — это токен доступа, который мы получаем от Google после того, как пользователь дал необходимые разрешения. И response — это объект ответа, полученный от Express. Мы передаем его этой функции, чтобы мы могли отправить ответ пользователю. Внутри функции мы обновляем access_token первой строки. Как упоминалось ранее, это приложение работает только для одного пользователя. После обновления access_token мы отправляем ответ.

 function updateAccessToken(tokens, response){ connection.db.query( "UPDATE users SET access_token = ? WHERE id = 1", [JSON.stringify(tokens)], function(err, rows, fields){ if(!err){ console.log('updated!'); response.send('connected!'); }else{ console.log('error updating table'); console.log(err); response.send('error occured, please try again'); } } ); } 

Добавьте маршрут для домашней страницы. Это выполняется при обращении к http://localhost:3000 . Отсюда мы генерируем URL аутентификации. При этом используется метод generateAuthUrl из oauth2Client . Он принимает объект, содержащий access_type и scope . Мы получаем эти значения из файла конфигурации приложения, который мы создали ранее. Наконец, мы отправляем фактическую ссылку, по которой пользователь может щелкнуть. Обратите внимание, что вы всегда должны делать это внутри представления, но для упрощения вещей мы просто собираемся напрямую вернуть ссылку.

 app.get('/', function(req, res){ var url = google.oauth2Client.generateAuthUrl({ access_type: google.config.access_type, scope: google.config.scopes }); res.send('<a href="' + url + '">login to google</a>'); }); 

Добавьте маршрут для входа в систему. Это маршрут, по которому пользователь перенаправляется после предоставления необходимых разрешений приложению. Google передает параметр запроса, называемый code . И мы получаем это через объект query в запросе. Затем мы вызываем метод getToken и передаем code в качестве аргумента. Это даст нам токен доступа. Поэтому мы вызываем функцию updateAccessToken чтобы сохранить ее в базе данных.

 app.get('/login', function(req, res){ var code = req.query.code; console.log('login'); google.oauth2Client.getToken(code, function(err, tokens){ if(!err){ console.log('tokens'); console.log(tokens); updateAccessToken(tokens, res); }else{ res.send('error getting token'); console.log('error getting token'); } }); }); 

Создание Кэчера

Кешер отвечает за сохранение пользовательских встреч в базе данных. Это избавляет нас от необходимости запрашивать каталог API Календаря Google каждый раз, когда мы отправляем напоминания. Создайте файл cache.js и добавьте следующее:

 var google = require('./common/google'); var connection = require('./common/db'); var time = require('./common/time'); var CronJob = require('cron').CronJob; function addAppointment(event_id, summary, start, end){ connection.db.query( "INSERT INTO appointments SET id = ?, summary = ?, datetime_start = ?, datetime_end = ?, notified = 0", [event_id, summary, start, end], function(err, rows, fields){ if(!err){ console.log('added!'); }else{ console.log('error adding to table'); } } ); } function getEvents(err, response){ console.log('response'); console.log(response); if(err){ console.log('The API returned an error: ' + err); } var events = response.items; if(events.length == 0){ console.log('No upcoming events found.'); }else{ console.log('Upcoming 10 events:'); for(var i = 0; i < events.length; i++){ var event = events[i]; var event_id = event.id; var summary = event.summary; var start = event.start.dateTime || event.start.date; var end = event.end.dateTime || event.end.date; addAppointment(event_id, summary, start, end); } } } function cache(){ var current_datetime = time.moment().toISOString(); google.calendar.events.list({ auth: google.oauth2Client, calendarId: 'primary', timeMin: current_datetime, maxResults: 10, singleEvents: true, orderBy: 'startTime' }, getEvents); } connection.db.query('SELECT access_token FROM users WHERE id = 1', function(error, results, fields){ if(!error){ var tokens = JSON.parse(results[0].access_token); google.oauth2Client.setCredentials({ 'access_token': tokens.access_token, 'refresh_token': tokens.refresh_token }); new CronJob('0 0 * * *', cache, null, true, time.config.timezone); //cache(); //for testing } }); 

Разбивая это:

Сначала мы импортируем все модули, которые нам нужны.

 var google = require('./common/google'); var connection = require('./common/db'); var time = require('./common/time'); var CronJob = require('cron').CronJob; 

Функция addAppointment отвечает за сохранение встреч в таблице appointments . Это принимает event_id , summary , event_id start и end встречи. event_id — это, в основном, идентификатор конкретной встречи в Календаре Google. Мы используем его в качестве значения для первичного ключа, что означает, что дубликаты не будут вставлены в таблицу appointments . Здесь не хватает средств для сравнения встреч, которые уже есть в базе данных, и тех, которые возвращены API. Если по какой-то причине расписание встреч изменится, база данных не будет обновляться, поскольку все, что мы здесь делаем, это вставляем в таблицу. Я оставлю это для вашего списка задач.

 function addAppointment(event_id, summary, start, end){ connection.db.query( "INSERT INTO appointments SET id = ?, summary = ?, datetime_start = ?, datetime_end = ?, notified = 0", [event_id, summary, start, end], function(err, rows, fields){ if(!err){ console.log('added!'); }else{ console.log('error adding to table'); } } ); } 

Функция getEvents отвечает за цикл всех встреч, возвращаемых API. При этом используется метод addAppointment чтобы сохранить назначение для каждой итерации цикла.

 function getEvents(err, response){ console.log('response'); console.log(response); if(err){ console.log('The API returned an error: ' + err); } var events = response.items; if(events.length == 0){ console.log('No upcoming events found.'); }else{ for(var i = 0; i < events.length; i++){ var event = events[i]; var event_id = event.id; var summary = event.summary; var start = event.start.dateTime || event.start.date; var end = event.end.dateTime || event.end.date; addAppointment(event_id, summary, start, end); } } } 

Метод cache — это тот, который выполняет фактический вызов API Календаря Google. Это с помощью клиента Google. Здесь мы вызываем метод list для объекта calendar.events . Это принимает два аргумента: первый — это объект, содержащий опции для запроса, а второй — функция, которая должна быть выполнена после возврата результата.

 function cache(){ var current_datetime = time.moment().toISOString(); google.calendar.events.list({ auth: google.oauth2Client, calendarId: 'primary', timeMin: current_datetime, maxResults: 10, singleEvents: true, orderBy: 'startTime' }, getEvents); } 

В объекте, содержащем параметры, мы имеем следующее:

  • auth — это oauth2Client . Это используется для аутентификации запроса.
  • calendarId — идентификатор календаря, в котором мы будем получать встречи. В этом случае мы используем основной календарь. Календарь Google на самом деле позволяет создавать много календарей. Другие могут также поделиться своими календарями с вами. И у каждого из этих календарей есть свой идентификатор. Это то, что мы указываем здесь. Если вы заинтересованы в доступе к другим календарям, обязательно ознакомьтесь с документацией API по календарям .
  • timeMin — базовая timeMin время, которые будут использоваться в запросе. В этом случае мы используем текущую дату и время. Потому что, кто хочет получить уведомление о событии, которое произошло в прошлом? Обратите внимание, что для представления времени используется стандарт ISO 8601. К счастью, есть метод toISOString который мы можем использовать для этого.
  • maxResults — общее количество результатов, которые вы хотите вернуть.
  • singleEvents — позволяет указать, следует ли возвращать только одноразовые события. Здесь мы использовали true что означает, что повторяющиеся события не будут возвращены.
  • orderBy — позволяет указать порядок, в котором будут возвращены результаты. В этом случае мы использовали startTime который упорядочивает результат в порядке возрастания в зависимости от времени их запуска. Это доступно, только если singleEvents параметра singleEvents установлено значение true .

Все эти опции и многие другие можно найти в Events: list документации

Получите access_token из базы данных и используйте его для установки учетных данных для клиента oauth2Client . После этого создайте новое задание cron, которое будет запускать метод cache каждый день в 12 часов ночи.

 connection.db.query('SELECT access_token FROM users WHERE id = 1', function(error, results, fields){ if(!error){ var tokens = JSON.parse(results[0].access_token); google.oauth2Client.setCredentials({ 'access_token': tokens.access_token, 'refresh_token': tokens.refresh_token }); new CronJob('0 0 * * *', cache, null, true, time.config.timezone); //cache(); //for testing } }); 

Создание уведомителя

И последнее, но не менее важное: у нас есть уведомитель ( notify.js ). Это отвечает за получение встреч из базы данных и определение того, готовы ли они к уведомлению. Если они есть, то мы их отправляем.

 var config = require('config'); var twilio_config = config.get('twilio'); var twilio = require('twilio')(twilio_config.sid, twilio_config.secret); var connection = require('./common/db'); var time = require('./common/time'); var CronJob = require('cron').CronJob; function updateAppointment(id){ //update appointment to notified=1 connection.db.query( "UPDATE appointments SET notified = 1 WHERE id = ?", [id], function(error, results, fields){ if(!error){ console.log('updated appointment with ID of ' + id); } } ); } function sendNotifications(error, results, fields){ var phone_number = config.get('me.phone_number'); console.log(phone_number); console.log('results'); console.log(results); if(!error){ for(var x in results){ var id = results[x].id; var datetime_start = results[x].datetime_start; var datetime_end = results[x].datetime_end; var appointment_start = time.moment(datetime_start); var summary = results[x].summary + " is fast approaching on " + appointment_start.format('MMM DD, YYYY hh:mm a'); var hour_diff = appointment_start.diff(time.moment(), 'hours'); console.log('hour diff:'); console.log(hour_diff); if(hour_diff <= 24){ twilio.sendMessage({ to: phone_number, from: twilio_config.phone_number, body: summary }, function(err, responseData){ if(!err){ console.log('message sent!'); console.log(responseData.from); console.log(responseData.body); }else{ console.log('error:'); console.log(err); } }); updateAppointment(id); } } } } function startTask(){ connection.db.query('SELECT * FROM appointments WHERE notified = 0', sendNotifications); } new CronJob('0 12 * * *', startTask, null, true, time.config.timezone); 

Разбивая это:

Импортируйте все необходимые модули.

 var config = require('config'); var twilio_config = config.get('twilio'); var twilio = require('twilio')(twilio_config.sid, twilio_config.secret); var connection = require('./common/db'); var time = require('./common/time'); var CronJob = require('cron').CronJob; 

Создайте функцию updateAppointment . Это принимает ID назначения в качестве аргумента. Все, что он делает, это устанавливает значение для поля notified равным 1, что означает, что уведомление для конкретной встречи уже было отправлено.

 function updateAppointment(id){ //update appointment to notified=1 connection.db.query( "UPDATE appointments SET notified = 1 WHERE id = ?", [id], function(error, results, fields){ if(!error){ console.log('updated appointment with ID of ' + id); } } ); } 

Далее у нас есть функция sendNotifications . Это отвечает за фактическую отправку текстовых напоминаний с помощью Twilio. Эта функция вызывается после извлечения встреч из базы данных. Вот почему в него передаются error , results и аргументы fields . error содержит любую ошибку из базы данных. results содержат строки, возвращенные из базы данных. И fields содержат информацию о возвращаемых полях результатов.

 function sendNotifications(error, results, fields){ var phone_number = config.get('me.phone_number'); console.log(phone_number); console.log('results'); console.log(results); if(!error){ for(var x in results){ var id = results[x].id; var datetime_start = results[x].datetime_start; var datetime_end = results[x].datetime_end; var appointment_start = time.moment(datetime_start); var summary = results[x].summary + " is fast approaching on " + appointment_start.format('MMM DD, YYYY hh:mm a'); var hour_diff = appointment_start.diff(time.moment(), 'hours'); console.log('hour diff:'); console.log(hour_diff); if(hour_diff <= 24){ twilio.sendMessage({ to: phone_number, from: twilio_config.phone_number, body: summary }, function(err, responseData){ if(!err){ console.log('message sent!'); console.log(responseData.from); console.log(responseData.body); updateAppointment(id); }else{ console.log('error:'); console.log(err); } }); } } } } 

Внутри функции мы получаем номер телефона пользователя из конфигурации приложения.

 var phone_number = config.get('me.phone_number'); console.log(phone_number); 

Проверьте, есть ли какие-либо ошибки, и если таковых нет, продолжайте цикл по всем возвращенным результатам.

 if(!error){ for(var x in results){ ... } } 

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

 var id = results[x].id; var datetime_start = results[x].datetime_start; var datetime_end = results[x].datetime_end; var appointment_start = time.moment(datetime_start); var summary = results[x].summary + " is fast approaching on " + appointment_start.format('MMM DD, YYYY hh:mm a'); var hour_diff = appointment_start.diff(time.moment(), 'hours'); console.log('hour diff:'); console.log(hour_diff); if(hour_diff <= 24){ ... } 

Если оно меньше или равно 24 часам, мы отправляем уведомление. Это благодаря использованию клиента Twilio. Мы вызываем sendMessage и передаем объект, содержащий to (номер телефона пользователя), from (номер песочницы Twilio или номер телефона, который вы купили у Twilio), и body которое содержит текстовое сообщение. Если ошибок нет, мы предполагаем, что уведомление отправлено. Поэтому мы вызываем функцию updateAppointment чтобы установить для поля notified значение 1, чтобы оно не было выбрано при следующем updateAppointment задачи.

 twilio.sendMessage({ to: phone_number, from: twilio_config.phone_number, body: summary }, function(err, responseData){ if(!err){ console.log('message sent!'); console.log(responseData.from); console.log(responseData.body); updateAppointment(id); }else{ console.log('error:'); console.log(err); } }); 

Наконец, у нас есть метод startTask . Все, что он делает, это выбирает все встречи из таблицы appointments чье уведомление еще не было отправлено. Эта функция выполняется каждые 12 часов и 6 часов вечера.

 function startTask(){ connection.db.query('SELECT * FROM appointments WHERE notified = 0', sendNotifications); } new CronJob('0 12,18 * * *', startTask, null, true, time.config.timezone); 

Вывод

Это оно! Из этого урока вы узнали, как создать приложение для напоминания SMS с Twilio. В частности, мы рассмотрели, как получать встречи пользователя через API Календаря Google. Мы сохранили их в базе данных и уведомили пользователя через Twilio. Вы можете найти код, используемый в этом руководстве, из репозитория github .