Twilio — это SaaS-приложение, которое позволяет разработчикам создавать телефонные приложения с использованием веб-технологий. В этой серии из двух частей мы будем использовать Twilio для создания приложения для прогнозирования погоды, доступ к которому осуществляется с помощью телефонной системы. Серверная часть будет написана с использованием среды Laravel (ознакомительный видеокурс можно приобрести здесь или в виде письменных руководств здесь ).
В этой части мы создадим простую программу, которая позволит пользователю позвонить по номеру телефона, который мы покупаем у Twilio, ввести почтовый индекс и получить текущий прогноз погоды. Пользователь также может узнать погоду на любой день недели с помощью голосового меню. Во второй части этой серии мы будем использовать то, что было построено в этой статье, чтобы позволить пользователю взаимодействовать с приложением через SMS (текстовое сообщение).
Предпосылки
Среда разработки
Эта статья предполагает, что Homestead Improved установлен. Не обязательно использовать его, но команды могут немного отличаться, если вы используете другую среду. Если вы не знакомы с Homestead и хотите получить аналогичные результаты, на которые рассчитана эта статья, посетите эту статью SitePoint, в которой показано, как настроить Homestead, и, если вам нужен ускоренный курс в Vagrant, см. Этот пост . Кроме того, если это подстегивает ваш аппетит и вам хочется глубже изучить среды разработки PHP, у нас есть книга об этом, доступная для покупки .
зависимости
Мы создадим новый проект Laravel и затем добавим в проект клиентскую библиотеку Twilio PHP SDK и Guzzle HTTP:
cd ~/Code composer create-project --prefer-dist laravel/laravel Laravel 5.4.* cd Laravel composer require "twilio/sdk:^5.7" composer require "guzzlehttp/guzzle:~6.0"
развитие
Давайте пройдемся по всем шагам, один за другим.
Маршруты
Откройте файл routes/web.php
и добавьте следующие:
Route::group(['prefix' => 'voice', 'middleware' => 'twilio'], function () { Route::post('enterZipcode', 'VoiceController@showEnterZipcode')->name('enter-zip'); Route::post('zipcodeWeather', 'VoiceController@showZipcodeWeather')->name('zip-weather'); Route::post('dayWeather', 'VoiceController@showDayWeather')->name('day-weather'); Route::post('credits', 'VoiceController@showCredits')->name('credits'); });
В этом приложении все запросы будут проходить по пути /voice
. Когда Twilio впервые подключается к приложению, оно переходит в /voice/enterZipcode
через HTTP POST
. В зависимости от того, что происходит во время телефонного звонка, Twilio отправляет запросы на другие конечные точки. Это включает /voice/zipcodeWeather
для предоставления прогноза на сегодня, /voice/dayWeather
для предоставления прогноза на конкретный день и /voice/credits
для предоставления информации о том, откуда поступили данные.
Сервисный уровень
Мы собираемся добавить класс обслуживания. Этот класс будет содержать большую часть бизнес-логики, которая будет разделена между приложением голосового телефона и приложением SMS.
Создайте новую подпапку « Services
внутри папки app
. Затем создайте файл с именем WeatherService.php
и поместите в него следующее содержимое:
<?php namespace App\Services; use Illuminate\Support\Facades\Cache; use Twilio\Twiml; class WeatherService { }
Это большой файл в проекте, поэтому мы будем строить его по частям. Поместите следующие части кода в этом разделе в наш новый класс обслуживания:
public $daysOfWeek = [ 'Today', 'Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday' ];
Мы будем использовать этот массив для отображения дня недели на число; Воскресенье = 1, понедельник = 2 и т. Д.
public function getWeather($zip, $dayName) { $point = $this->getPoint($zip); $tz = $this->getTimeZone($point); $forecast = $this->retrieveNwsData($zip); $ts = $this->getTimestamp($dayName, $zip); $tzObj = new \DateTimeZone($tz->timezoneId); $tsObj = new \DateTime(null, $tzObj); $tsObj->setTimestamp($ts); foreach ($forecast->properties->periods as $k => $period) { $startTs = strtotime($period->startTime); $endTs = strtotime($period->endTime); if ($ts > $startTs and $ts < $endTs) { $day = $period; break; } } $response = new Twiml(); $weather = $day->name; $weather .= ' the ' . $tsObj->format('jS') . ': '; $weather .= $day->detailedForecast; $gather = $response->gather( [ 'numDigits' => 1, 'action' => route('day-weather', [], false) ] ); $menuText = ' '; $menuText .= "Press 1 for Sunday, 2 for Monday, 3 for Tuesday, "; $menuText .= "4 for Wednesday, 5 for Thursday, 6 for Friday, "; $menuText .= "7 for Saturday. Press 8 for the credits. "; $menuText .= "Press 9 to enter in a new zipcode. "; $menuText .= "Press 0 to hang up."; $gather->say($weather . $menuText); return $response; }
Метод getWeather
берет почтовый индекс с днем недели и создает текст прогноза погоды. Сначала он вычисляет базовое время для запрошенного дня, а затем просматривает прогноз погоды, выполняя предварительный анализ массива данных прогноза. После этого он возвращает ответ Voice TwiML . Ниже приведен пример того, что возвращается:
<?xml version="1.0" encoding="UTF-8"?> <Response> <Gather numDigits="1" action="/voice/dayWeather"> <Say> This Afternoon the 31st: Sunny, with a high near 72. South southwest wind around 8 mph. Press 1 for Sunday, 2 for Monday, 3 for Tuesday, 4 for Wednesday, 5 for Thursday, 6 for Friday, 7 for Saturday. Press 8 for the credits. Press 9 to enter in a new zipcode. Press 0 to hang up. </Say> </Gather> </Response>
Тег <Gather>
указывает Twilio ожидать ввода с клавиатуры пользователя. numDigits
говорит, сколько цифр ожидать. Атрибут action
сообщает, с какой конечной точкой следует связаться дальше.
protected function retrieveNwsData($zip) { return Cache::remember('weather:' . $zip, 60, function () use ($zip) { $point = $this->getPoint($zip); $point = $point->lat . ',' . $point->lng; $url = 'https://api.weather.gov/points/' . $point . '/forecast'; $client = new \GuzzleHttp\Client(); $response = $client->request('GET', $url, [ 'headers' => [ 'Accept' => 'application/geo+json', ] ]); return json_decode((string)$response->getBody()); }); }
Метод retrieveNwsData
получает данные прогноза погоды. Во-первых, метод проверяет, находится ли в кеше копия прогноза погоды по почтовому индексу. Если нет, то HTTP-клиент Guzzle используется для отправки HTTP-запроса GET
к конечной точке API Национальной метеорологической службы (NWS) https://api.weather.gov/points/{point}/forecast
enjpointcasts/forecast. Чтобы получить географическую точку getPoint
перед getPoint
API погоды getPoint
метод getPoint
. Ответ от конечной точки API — прогноз погоды в формате GeoJSON . Прогноз на каждый день и ночь на неделю (за некоторыми исключениями мы обсудим позже); Всего 14 записей. Мы кешируем ответ API в течение часа, потому что выполнение запроса медленное, плюс мы не хотим слишком часто заходить на государственные серверы и получать бан.
protected function getPoint($zip) { return Cache::remember('latLng:' . $zip, 1440, function () use ($zip) { $client = new \GuzzleHttp\Client(); $url = 'http://api.geonames.org/postalCodeSearchJSON'; $response = $client->request('GET', $url, [ 'query' => [ 'postalcode' => $zip, 'countryBias' => 'US', 'username' => env('GEONAMES_USERNAME') ] ]); $json = json_decode((string)$response->getBody()); return $json->postalCodes[0]; }); }
Метод getPoint
сопоставляет почтовый индекс с географической точкой. Это делается с помощью API GeoNames. Результаты кэшируются на один день, потому что использование API идет медленно.
protected function getTimeZone($point) { $key = 'timezone:' . $point->lat . ',' . $point->lng; return Cache::remember($key, 1440, function () use ($point) { $client = new \GuzzleHttp\Client(); $url = 'http://api.geonames.org/timezoneJSON'; $response = $client->request('GET', $url, [ 'query' => [ 'lat' => $point->lat, 'lng' => $point->lng, 'username' => env('GEONAMES_USERNAME') ] ]); return json_decode((string) $response->getBody()); }); }
Метод getTimeZone
используется для получения часового пояса, в котором находится географическая точка. Также используется API GeoNames, и результаты кешируются в течение дня по тем же причинам.
protected function getTimestamp($day, $zip) { $point = $this->getPoint($zip); $tz = $this->getTimeZone($point); $tzObj = new \DateTimeZone($tz->timezoneId); $now = new \DateTime(null, $tzObj); $hourNow = $now->format('G'); $dayNow = $now->format('l'); if ($day == $dayNow and $hourNow >= 18) { $time = new \DateTime('next ' . $day . ' noon', $tzObj); $ts = $time->getTimestamp(); } elseif (($day == 'Today' or $day == $dayNow) and $hourNow >= 6) { $ts = $now->getTimestamp(); } else { $time = new \DateTime($day . ' noon', $tzObj); $ts = $time->getTimestamp(); } return $ts; }
Метод getTimestamp
возвращает контрольное время, которое используется для поиска прогноза на конкретную дату. В большинстве случаев прогнозные данные содержат прогноз на день и ночь, но иногда на ночь (до 6 часов утра) и дневной прогноз (во второй половине дня, до 18 часов) для текущего дня. Из-за этого мы должны сделать некоторые вычисления, чтобы получить хорошую контрольную отметку времени. В большинстве случаев он возвращает время полдня почтового индекса в запрошенный день.
public function getCredits() { $credits = "Weather data provided by the National Weather Service. "; $credits .= "Zipcode data provided by GeoNames."; return $credits; } }
Метод getCredits
просто возвращает некоторый стандартный текст о том, откуда поступили данные.
контроллер
Создайте файл VoiceController.php
в VoiceController.php
app/Http/Controllers
и вставьте в него следующий код:
<?php namespace App\Http\Controllers; use App\Services\WeatherService; use Illuminate\Http\Request; use Twilio\Twiml; class VoiceController extends Controller { protected $weather; public function __construct(WeatherService $weatherService) { $this->weather = $weatherService; } public function showEnterZipcode() { $response = new Twiml(); $gather = $response->gather( [ 'numDigits' => 5, 'action' => route('zip-weather', [], false) ] ); $gather->say('Enter the zipcode for the weather you want'); return $response; } public function showZipcodeWeather(Request $request) { $zip = $request->input('Digits'); $request->session()->put('zipcode', $zip); return $this->weather->getWeather($zip, 'Today'); } public function showDayWeather(Request $request) { $digit = $request->input('Digits', '0'); switch ($digit) { case '8': $response = new Twiml(); $response->redirect(route('credits', [], false)); break; case '9': $response = new Twiml(); $response->redirect(route('enter-zip', [], false)); break; case '0': $response = new Twiml(); $response->hangup(); break; default: $zip = $request->session()->get('zipcode'); $day = $this->weather->daysOfWeek[$digit]; $response = $this->weather->getWeather($zip, $day); break; } return $response; } public function showCredits() { $response = new Twiml(); $credits = $this->weather->getCredits(); $response->say($credits); $response->hangup(); return $response; } }
Метод showEnterZipcode
выполняется, когда делается запрос к конечной точке /voice/enterZipcode
. Этот метод возвращает TwiML, который запрашивает у вызывающего абонента ввести почтовый индекс. TwiML также говорит, что запрос к /voice/zipcodeWeather
должен быть сделан, когда вызывающий абонент ввел 5 цифр. Вот пример ответа:
<?xml version="1.0" encoding="UTF-8"?> <Response> <Gather numDigits="5" action="/voice/zipcodeWeather"> <Say> Enter the zipcode for the weather you want </Say> </Gather> </Response>
Метод showZipcodeWeather
выполняется, когда делается запрос к конечной точке /voice/zipcodeWeather
. Этот метод возвращает текст прогноза на сегодня и голосовое меню для навигации по приложению в формате TwiML. Вот как выглядит ответ:
<?xml version="1.0" encoding="UTF-8"?> <Response> <Gather numDigits="1" action="/voice/dayWeather"> <Say> This Afternoon the 31st: Sunny, with a high near 72. South southwest wind around 8 mph. Press 1 for Sunday, 2 for Monday, 3 for Tuesday, 4 for Wednesday, 5 for Thursday, 6 for Friday, 7 for Saturday. Press 8 for the credits. Press 9 to enter in a new zipcode. Press 0 to hang up. </Say> </Gather> </Response>
Когда /voice/dayWeather
конечная точка /voice/dayWeather
, showDayWeather
метод showDayWeather
. Это возвращает прогноз на запрошенный день и голосовое меню для навигации по приложению в формате TwiML. Ответ на понедельник может выглядеть так:
<?xml version="1.0" encoding="UTF-8"?> <Response> <Gather numDigits="1" action="/voice/dayWeather"> <Say> Monday the 3rd: Sunny, with a high near 70. Press 1 for Sunday, 2 for Monday, 3 for Tuesday, 4 for Wednesday, 5 for Thursday, 6 for Friday, 7 for Saturday. Press 8 for the credits. Press 9 to enter in a new zipcode. Press 0 to hang up. </Say> </Gather> </Response>
Последний метод, showCredits
, выполняется, когда запрашивается конечная точка /voice/credits
. Ответ TwiML имеет кредиты и инструкцию повесить трубку. Ответ будет выглядеть так:
<?xml version="1.0" encoding="UTF-8"?> <Response> <Say> Weather data provided by the National Weather Service. Zipcode data provided by GeoNames. </Say> <Hangup/> </Response>
Промежуточное
По умолчанию Twilio отправляет запросы веб-пользователям, используя HTTP POST
. Из-за этого Laravel требует, чтобы в представлении POST
был токен CSRF. В нашем случае мы не будем использовать токен CSRF, поэтому мы должны отключить промежуточное ПО, которое проверяет его. В файле app/Http/Kernel.php
удалите или закомментируйте строку \App\Http\Middleware\VerifyCsrfToken::class,
В другом разделе мы настроим Ngrok — приложение, которое позволит Интернету подключаться к нашей локальной среде. Поскольку приложение больше не имеет защиты от CSRF, любой в Интернете сможет поразить наши конечные точки. Чтобы убедиться, что запросы поступают либо от Twilio, либо от модульных тестов, мы должны создать специальное промежуточное программное обеспечение. Мы будем использовать промежуточное программное обеспечение, предложенное в документации Twilio , но измененное для работы с нашей установкой.
В файле app/Http/Kernel.php
добавьте следующую строку в конец массива $routeMiddleware
:
'twilio' => \App\Http\Middleware\TwilioRequestValidator::class,
Создайте файл с именем TwilioRequestValidator.php
в TwilioRequestValidator.php
app/Http/Middleware
и вставьте в него следующий код:
<?php namespace App\Http\Middleware; use Closure; use Illuminate\Http\Response; use Twilio\Security\RequestValidator; class TwilioRequestValidator { /** * Handle an incoming request. * * @param \Illuminate\Http\Request $request * @param \Closure $next * @return mixed */ public function handle($request, Closure $next) { if (env('APP_ENV') === 'test') { return $next($request); } $host = $request->header('host'); $originalHost = $request->header('X-Original-Host', $host); $fullUrl = $request->fullUrl(); $fullUrl = str_replace($host, $originalHost, $fullUrl); $requestValidator = new RequestValidator(env('TWILIO_APP_TOKEN')); $isValid = $requestValidator->validate( $request->header('X-Twilio-Signature'), $fullUrl, $request->toArray() ); if ($isValid) { return $next($request); } else { return new Response('You are not Twilio :(', 403); } } }
С каждым запросом, который делает Twilio, он отправляет HMAC, вычисленный с URL-адресом и переменными запроса в качестве данных, и токен аутентификации Twilio в качестве секретного ключа. Этот HMAC отправляется в заголовке запроса X-Twilio-Signature
. Мы можем сравнить X-Twilio-Signature
отправленную в запросе, с HMAC, который мы генерируем на веб-сервере. Это делается с помощью метода validate()
объекта $requestValidator
.
Перейдите на панель инструментов консоли Twilio по адресу https://www.twilio.com/console . Оказавшись там, покажите AUTH TOKEN
и запомните это.
Затем откройте файл .env
и добавьте следующий код в конец файла.
TWILIO_APP_TOKEN=YOUR_AUTH_TOKEN_HERE
Не забудьте заменить YOUR_AUTH_TOKEN_HERE
значением, которое вы отметили на панели инструментов.
Из-за того, как работает Ngrok, он изменяет заголовок запроса host
с того, что определил исходный клиент (что-то вроде abc123.ngrok.io
) на хост, который мы указали. В этом случае это будет homestead.app
. Нам нужно изменить URL-адрес, чтобы учесть это для вычисления правильного HMAC. Промежуточное программное обеспечение пропускается, если определено, что выполняются модульные тесты.
GeoNames
Чтобы использовать API GeoNames для выполнения запросов почтового индекса и часового пояса, мы должны создать бесплатную учетную запись . После этого мы должны включить возможность использовать API, посетив страницу управления учетной записью .
После регистрации откройте файл .env
и добавьте переменную GEONAMES_USERNAME
значение которой будет вашим именем пользователя GeoNames.
Ngrok
Twilio требует, чтобы ваше приложение было доступно из Интернета, потому что Twilio называет веб-крючки, которые вы внедряете. При стандартном дизайне Homestead веб-сервер доступен только для вашего локального компьютера. Чтобы обойти это ограничение, мы используем Ngrok . Эта программа позволяет вам получить полное доменное имя, которое работает в Интернете и перенаправляет трафик для этого адреса в ваш экземпляр Homestead, используя туннель.
Чтобы использовать Ngrok, вы должны сначала зарегистрировать бесплатный аккаунт . После этого скачайте и установите Ngrok. После того, как приложение установлено, не забудьте установить authtoken
согласно документации по authtoken
работы . Далее мы запустим туннель:
./ngrok http 192.168.10.10:80 -host-header=homestead.app
Этот туннель позволит нашим локальным серверам получать входящие запросы из Интернета. Homestead ожидает, что HTTP-заголовок host
будет homestead.app
. Команда Ngrok, которую мы использовали, скопирует исходный заголовок host
HTTP-запроса в заголовок X-Original-Host
а затем перезапишет host
значением homestead.app
.
Запишите URL-адрес HTTP, предоставленный программой, поскольку нам потребуется имя хоста, которое будет указано позже при настройке телефонного номера Twilio для звонков.
Twilio
Создайте учетную запись в Twilio и добавьте деньги на свой счет. Для реализации и тестирования приложения более чем достаточно десяти долларов. Деньги нужны для приобретения телефонного номера и оплаты входящих звонков. Перейдите на страницу « Найти номер» и приобретите номер, поддерживающий голосовые и SMS-сообщения. Я выбрал бесплатный номер.
На странице поиска нажмите ссылку «Расширенный поиск» и убедитесь, что установлены флажки «Голос» и «SMS». Затем нажмите кнопку «Поиск». После появления списка результатов выберите номер телефона.
После того, как вы приобрели свой номер, перейдите на страницу настроек для номера телефона и замените голосовой веб-крючок «входящий вызов» https://demo.twilio.com/welcome/voice/
веб-крючком, который перейдет на ваш веб-крючок. приложение, например http://YOUR_NGROK_HOSTNAME/voice/enterZipcode
, и сохраните изменения.
Использование приложения
Запустив свое приложение и программу Ngrok, позвоните по бесплатному номеру. Вас должны попросить ввести почтовый индекс. После ввода почтового индекса вы должны получить текущий прогноз и голосовое меню для навигации по приложению.
Производственные соображения
Одна вещь, которая не рассматривается в этой статье, но которую можно сделать для улучшения этого приложения, — это заставить Twilio использовать конечные точки HTTPS вместо HTTP. Если вы хотите увидеть, как это делается, пожалуйста, запросите это в комментариях ниже!
Вывод
В этой статье мы создали веб-приложение, которое пользователь может вызывать и взаимодействовать с помощью Twilio. Это приложение позволило пользователю получить прогноз погоды для определенного почтового индекса. Помните, что может быть несоответствие даты между тем, откуда пользователь звонит, и почтовым индексом, который он вводит, если они находятся в разных часовых поясах.
Вы можете найти код для приложения в этой серии статей на Github .
В следующей части этой серии из двух частей мы будем использовать то, что мы создали здесь, чтобы это приложение работало поверх SMS.