Статьи

Привет, Ларавел? Общение с PHP через телефонные звонки!

Вектор значок смартфона с наложением значок погоды

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 и запомните это.

Фотография страницы приборной панели консоли Twilio

Затем откройте файл .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 , и сохраните изменения.

Фото страницы настройки голосового веб-крючка Twilio

Использование приложения

Запустив свое приложение и программу Ngrok, позвоните по бесплатному номеру. Вас должны попросить ввести почтовый индекс. После ввода почтового индекса вы должны получить текущий прогноз и голосовое меню для навигации по приложению.

Производственные соображения

Одна вещь, которая не рассматривается в этой статье, но которую можно сделать для улучшения этого приложения, — это заставить Twilio использовать конечные точки HTTPS вместо HTTP. Если вы хотите увидеть, как это делается, пожалуйста, запросите это в комментариях ниже!

Вывод

В этой статье мы создали веб-приложение, которое пользователь может вызывать и взаимодействовать с помощью Twilio. Это приложение позволило пользователю получить прогноз погоды для определенного почтового индекса. Помните, что может быть несоответствие даты между тем, откуда пользователь звонит, и почтовым индексом, который он вводит, если они находятся в разных часовых поясах.

Вы можете найти код для приложения в этой серии статей на Github .

В следующей части этой серии из двух частей мы будем использовать то, что мы создали здесь, чтобы это приложение работало поверх SMS.