Однажды во время первой беременности моей жены мне пришло в голову, что многие люди захотят узнать, когда родится ребенок. Я начал пересчитывать всех родственников и друзей, которым понадобится уведомление о рождении малыша, и понял, что мне придется сделать больше звонков, чем хотелось бы. Конечно, я мог бы собрать всех в одну большую групповую смс, но тогда рискую пропустить кого-то, кому это важно, а также обречь себя на бесконечный поток звуковых уведомлений.
Чтобы решить проблему бесконечных сигналов, нам нужен способ быстро оповестить всех заинтересованных о рождении ребенка. Мы реализуем это с помощью Python, Flask и Twilio. С Twilio мы можем отправлять сообщения или звонить кому угодно, не утруждая себя этим: достаточно около 150 строк кода на Python.
Наша цель — взять список телефонных номеров и отправить каждому звонок с выбранным нами сообщением. Можно было бы угадывать, какие номера должны быть в этом списке, но лучше не вводить их вручную в базу данных. Поскольку цель — уведомить людей о рождении ребенка, это не может произойти в заранее установленное время, поэтому приложение должно иметь возможность запускаться в удобный для нас момент.
Приложение позволяет пользователям позвонить на определенный номер и зарегистрировать свои номера для получения звонка. Когда на этот номер отправляется специальное текстовое сообщение, оно использует Twilio, чтобы обзвонить все номера из списка и озвучить заранее записанное сообщение, уведомляя о рождении ребенка. Это функционально и просто, и отлично решает нашу задачу. Хорошо размещенный пост в социальных сетях с номером телефона приложения позволит собрать номера всех, кому небезразлично рождение вашего ребенка.
Проект написан на Python и использует Flask. Если вы хотите пропустить объяснение, можете посмотреть все на его странице на GitHub.
Базы данных SQLite достаточно для хранения данных. Flask-SQLAlchemy — отличное расширение, которое я настоятельно рекомендую, но для такого простого проекта это излишество, поэтому давайте использовать встроенный модуль Python `sqlite3`.
Flask — это микро-веб-фреймворк, и для запуска приложения нам не нужно много. Давайте настроим наш проект.
Хорошей практикой при разработке проектов на Python является использование виртуальной среды. Используя предпочитаемую вами версию Python (желательно 3), установите `virtualenv`, если вы еще этого не сделали, через pip (`pip install virtualenv`), затем выполните `virtualenv .venv` в директории вашего проекта. Активируйте виртуальную среду с помощью `source .venv/bin/activate` на Linux или `.venv\Scripts\activate` на Windows.
Далее необходимо установить зависимости в вашей виртуальной среде. В этом проекте будут использоваться библиотеки `flask`, `twilio` и `requests`. Установите их через pip: `pip install flask twilio requests`. Если вы хотите использовать те же версии библиотек, что и я, можете воспользоваться моим файлом requirements.txt. Сохраните его на диск и выполните `pip install -r requirements.txt`. Стоит отметить, что я использовал версию `twilio` 5.6.0, которая использует `TwilioRestClient`, как в коде, а не более новые версии (6+), использующие `Client`.
Следующим шагом будет покупка номера телефона у Twilio и получение вашего SID и токена аутентификации. После этого нам нужен простой способ передать эти данные в наше Flask-приложение. Создайте файл конфигурации `config.py` в корневой директории вашего проекта со следующей информацией.
Приложения Flask по умолчанию работают на порту 5000, но вы можете изменить его на любое значение в указанном выше файле конфигурации под параметром `PORT`. Параметр `PHRASE` — это, по сути, пароль, используемый для запуска процесса уведомления, поэтому установите его так, чтобы его не смогли угадать недоброжелательные родственники. Для работы телефонных звонков ваш TwiML должен содержать URL обратного вызова, по которому Twilio будет направлять запросы. Если вы размещаете сервис на сервере с доменным именем, используйте его здесь. В моем случае я просто использую публичный IP-адрес моего дроплета Digital Ocean, где запущено приложение (`http://192.168.1.1` в качестве примера).
Наше приложение небольшое и простое, поэтому все файлы могут находиться в корневой директории вместе с `config.py`. Если хотите, можно поместить весь код в один файл, но все же стоит разделить его логически для поддержания порядка.
Мы начали это логическое разделение с `config.py` и можем продолжить его с утилитарными функциями. Создайте файл `utils.py` для хранения функций взаимодействия с базой данных и другой простой функциональности, которая может понадобиться:
Три из четырех функций связаны с базой данных: одна для создания базы данных, если она не существует (`init_db()`), другая для добавления информации о пользователе (`insert_to_db()`), и еще одна для получения всех пользователей из базы данных (`get_all_numbers()`). Четвертая функция `make_recordings_directory()` создает директорию для хранения записей, о которой мы поговорим позже. Если у вас есть базовое понимание SQL, функциональность базы данных должна быть вам понятна. Если нет, просто поверьте, что это работает, или изучите вопрос, если доверие не ваша сильная сторона.
Теперь мы можем приступить к созданию нашего приложения. В этом посте я предполагаю, что у вас есть базовые знания Flask. Создайте файл `run.py` (или `app.py`, если предпочитаете) и добавьте следующий код.
Этот код инициализирует ваше Flask-приложение, настраивает его, инициализирует базу данных и создает фиктивный маршрут индекса. Вам не нужен фиктивный маршрут, но я добавил его, чтобы отбиваться от ботов, которые регулярно сканируют мой дроплет.
Первая функция, которую мы реализуем, — это возможность регистрации телефонного номера пользователями. Нам понадобится начальная точка входа для ввода номера, другая для подтверждения введенного номера, еще одна для обработки цифр подтверждения (перенаправление на начало в случае ошибки, иначе переход к следующему шагу), еще одна для вопроса, хочет ли пользователь получить звонок или сообщение, и, наконец, точка входа для сохранения информации о пользователе. Каждая точка входа активно использует `twilio.twiml.Response`, который позволяет нам указывать Twilio, что мы хотим сделать, и метод `gather()` этого класса.
Ниже приведена точка входа для первого звонка пользователей на наш номер. Мы хотим поприветствовать звонящего, затем попросить его ввести номер телефона для регистрации. Мы делаем это с помощью Response.gather(), чтобы собрать цифры и указать Twilio, к какой точке входа перейти дальше. Каждая точка входа должна возвращать валидный TwiML для взаимодействия с Twilio, и вызов str() на объекте Response() обеспечивает это.
Следующая точка входа — это куда Twilio перенаправляется после сбора 10-значного номера телефона от звонящего. Ее цель — попросить пользователя подтвердить правильность введенного номера. Вы можете пропустить этот шаг, если доверяете пользователям всегда вводить правильно, но я не очень доверяю дрожащим рукам старшего поколения, не говоря уже о своих собственных. Мы снова используем метод `gather()`, чтобы попросить звонящего подтвердить правильность введенного номера и перенаправить на другой маршрут для проверки результата этого звонка. Важно отметить, что «action», указанный в вызове `gather()`, включает передачу цифр, введенных на первом шаге, между точками входа. Приложение на данный момент не имеет состояния, поэтому нам нужно запомнить исходный номер телефона, введенный до его сохранения в базе данных.
В следующей точке входа, если звонящий нажал 1, мы можем продолжить и передать номер телефона, введенный на первом шаге. Если звонящий ввел что-то кроме 1, мы отправляем его обратно к точке входа `confirm()`.
На данный момент звонящий ввел свой номер телефона и подтвердил его правильность. Теперь нам нужно спросить, хочет ли он получить звонок или сообщение, когда придет время отправлять уведомление. Мы еще не дошли до сохранения номера в базе данных, поэтому продолжаем передавать номер между точками входа.
Теперь мы наконец можем сохранить этот номер в базе данных. Аргумент запроса ‘Digits’ содержит число, соответствующее тому, хочет ли звонящий получить звонок или сообщение. У нас все еще есть доступ к исходному номеру благодаря нашей тщательности в передаче его между каждой точкой входа в параметре «number». На этот раз нам нужно точно знать, хочет ли звонящий сообщение или звонок, поэтому мы перенаправляем звонящего обратно к предыдущей точке входа, если введенная цифра не 1 или 2. Затем мы вызываем наш метод `insert_to_db()` из `utils.py` и завершаем звонок.
Эти пять точек входа решают поставленные задачи. Одна из тонкостей — сохранение введенного пользователем номера телефона между запросами. Мы передаем его в параметре `number` каждой точке входа. Этот процесс начинается в точке `/api/confirm` с `action=url_for(‘confirm_route’, number=digits)`. Другая тонкость — заставить приятный голос робота правильно повторить номер звонящему для подтверждения. Если просто вставить `8675309` в TwiML, она послушно скажет «восемь миллионов шестьсот семьдесят пять тысяч триста девять» вместо «восемь шесть семь пять три ноль девять». Вставьте пробелы между цифрами и используйте это вместо этого (`digits_spaced = ‘ ‘.join(ch for ch in digits)`). Также обратите внимание, что SQLite не поддерживает логические значения. Вместо этого мы используем ноль или единицу для обозначения ложного или истинного значения (вы могли уже заметить это в `utils.insert_to_db()`).
Далее нам нужно обработать уведомление пользователей. Приложение должно проверять входящие текстовые сообщения на наличие `PHRASE`, которую мы установили в нашем файле конфигурации, и перебирать наших пользователей, отправляя сообщения или совершая звонки. Пользователи, получающие текстовые сообщения, не нуждаются в каких-либо дополнительных точках входа, так как мы не ожидаем ответов от них, но для телефонных звонков нам нужна точка входа для обработки перенаправления от Twilio.
Это наша точка входа для обработки текстовых сообщений, отправленных на наш номер. Если пользователь отправляет текст, который не начинается с нашей секретной фразы, мы игнорируем сообщение, а не отправляем что-то в ответ. Это экономит немного денег, хотя вы все равно несете расходы за полученное сообщение. Мы получаем всех наших пользователей из базы данных, затем используем их предпочтения относительно текста или звонка, чтобы либо отправить текстовое сообщение с помощью `client.messages.create()`, либо сделать телефонный звонок с помощью `client.calls.create()`. Текстовое сообщение, содержащее секретную фразу, также должно включать сообщение для отправки вашим пользователям. Этот код ожидает, что оно будет выглядеть так: `«секретнаяфразa Вот мое сообщение для отправки»`. Поскольку пользователи, получающие телефонные звонки, обрабатываются в другой точке входа, мы используем глобальную переменную `MESSAGE` для сохранения указанного сообщения.
Этот последний маршрут обрабатывает пользователей, получающих телефонный звонок. Он просто произносит наше указанное сообщение, а затем завершает звонок.
Обратите внимание на ужасное ключевое слово `global`? Это мерзость, которая будет удалена в будущей итерации и заменена передачей параметров, аналогично тому, как это было сделано с номером телефона пользователя в процессе регистрации. Когда мы отправляем приложению нашу специальную фразу, мы ставим пробел после фразы и вводим желаемое сообщение для передачи пользователям. Эта фраза может быть сразу использована в этой точке входа для пользователей, получающих текстовые сообщения, но ее нужно передать в точку входа для обработки телефонных звонков, чтобы эти пользователи могли ее услышать. Мы создаем `TwilioRestClient` с использованием нашего SID и токена аутентификации и используем его для отправки сообщений и совершения звонков.
Одной из функций, которую я добавил, была возможность для пользователей, получающих телефонный звонок, оставить сообщение для меня и моей жены. Это, конечно, не обязательно, но стало хорошим экспериментом с функцией записи голоса, которую предлагает Twilio. Мы не будем рассматривать ее здесь, но вы можете увидеть ее в `run.py` на GitHub.
Завершите свое приложение строкой `app.run(host=’0.0.0.0’, port=app.config[‘PORT’])` — и ваш код готов! Это запустит ваше Flask-приложение таким образом, чтобы оно могло слушать входящие соединения на указанном порту со всеми доступными IPv4-интерфейсами вашего сервера.
Последний шаг — предоставить Twilio ваши точки входа для обработки телефонных звонков и текстовых сообщений. Откройте консоль Twilio и добавьте `http://вашip:1234/api/register` для телефонных звонков и `http://вашip:1234/notify` для сообщений к вашему купленному номеру телефона.
Запуск вашего приложения так же прост, как `python run.py`. Если вы запускаете его на своем сервере, скорее всего, вы подключены к нему удаленно. Из-за этого, если вы отключитесь от удаленной сессии, ваше приложение перестанет работать. Я решаю эту проблему с помощью терминального эмулятора `tmux` и запускаю приложение в нем: сессия tmux продолжает работать даже после моего отключения. Если ваш сервер выйдет из строя, то же самое произойдет и с вашим приложением, поэтому более надежное решение было бы идеальным, но это выходит за рамки данного поста. У меня один и тот же дроплет на Digital Ocean работает уже 3 года без простоев, так что это пока не стало реальной проблемой.
Поздравляю! Вы только что избавили себя от головной боли звонить каждому родственнику, когда родится ваш ребенок. Приложение легкое, позволяет использовать пользовательские сообщения и перекладывает проблему уведомлений на других. Возможно, вам придется помочь бабушке пройти процесс регистрации, но в итоге простое текстовое сообщение позволит вам заменить симфонию сигналов на соло (или дуэт, или трио) плача.
Поскольку это приложение изначально было написано как небольшой побочный проект, в нем отсутствуют некоторые функции, которые могли бы сделать его более удобным.
— Возможность регистрации через текстовое сообщение
— Добавление административного интерфейса для легкого управления базой данных
— Избавление от неприятной глобальной переменной
— Настройка голоса бота
Использование Twilio для этого позволило мне и моей жене насладиться временем с нашим новорожденным в больнице, одновременно удовлетворяя массы, желающие узнать все подробности. Автоматизация упрощает и улучшает нашу жизнь, позволяя сосредоточиться на том, что действительно важно, а такие сервисы, как Twilio, делают это возможным.
Если у вас есть вопросы по приложению или идеи для улучшения, создайте тикет на GitHub.