Рубрики
Без рубрики

Получение страны вашего пользователя и название в боте WhatsApp, используя fastapi и faunadb – часть 3

Мы вернулись! Таким образом, немного рекомендуется последней частью: теперь у нас есть наша база данных фауны, а также наша AP … Теги с Python, Showdev, WebDev.

Использование fastapi и faunadb с автопилотом WhatsApp Bot (3 часть серии)

Мы вернулись! Таким образом, немного рекомендуется последней частью: теперь у нас есть наша база данных FAUNA, а также наш API с нашей открытой конечной точкой и функциональностью для поиска пользователей по номеру их телефона. Но прежде чем мы продолжим, позвольте мне сказать вам, что я солгал вам: я сказал вам, что мы принесем страну пользователя с номера телефона, но мы этого не сделали. Итак, давайте сделаем это сразу!

Для этого мы будем использовать великий фоненовые игры Библиотека, которая является Python Work of Google либен . Установите это так:

pip install phonenumbers

Теперь вернемся к нашему приветствовать файл:

from fastapi import APIRouter, HTTPException, Form
import phonenumbers
from phonenumbers import geocoder # For geographical data

from src.models import User

user_greeting = APIRouter()

def phone_to_country(number: str) -> str:
    parsed_number = phonenumbers.parse(number)

    if not phonenumbers.is_possible_number(parsed_number):
        raise HTTPException(status_code=400, detail="Invalid phone number")

    return geocoder.country_name_for_number(parsed_number, "en")

@user_greeting.post("/greeting")
def greet_user(UserIdentifier: str = Form(...)):
    """
    User greet endpoint
    :param: UserIdentifier: user's phone number from Twilio
    """

    user = User.get_by_phone(phone_number=UserIdentifier)

# Greeting the user since already exists
    if user is not None:
        return {
            "actions": [
                {"remember": {"name": user.name, "country": user.country}},
                {"say": "Hi there {name}! Welcome back, how are things in {c}?".format(name=user.name, c=user.country)},
            ]
        }
    country = phone_to_country(UserIdentifier)

    return {
        "actions": [
            {
                "say": "Hello there! Looks like you are writing from {c}".format(c=country)
            },
            {"redirect": "task://can-have-name"},
        ]
    }

Увидеть это, помимо импорта фоненовые игры Мы также импортируем Geocoder , это добавляет возможность получить страну (среди другой географической информации) с номера телефона; Это разделенный импорт, потому что он добавляет большое количество метаданных.

Далее мы пишем небольшой помощник функции для анализа номера телефона и бросить Плохой запрос Ошибка в случае, если номер недействителен. Это произойдет только в том случае, если эта конечная точка он ударил за пределами Twilio, но давайте все равно добавим его. Тогда это просто вопрос использования функции помощника в случае, если пользователь его не в базе данных.

Cool, с этим с пути, давайте сделаем остальную часть API.

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

from typing import Optional, Any

from pydantic import BaseModel

from faunadb import query as q
from faunadb.objects import Ref

from src.core import session

class User(BaseModel):
  _collection_name: str = 'users'
  ref: Optional[Ref]
  ts: Optional[str]
  phone_number: str
  name: str
  country: str


  class Config:
    arbitrary_types_allowed = True

  def __init__(self, **data: Any):
    super().__init__(**data)

  def save(self):
    """
    Saves the document in the collection,
    the attributes are serialized utilizing Pydantic's dict method, which
    traverses trough the child class attributes
    :return: An instance of the newly saved object.
    """
    attributes = self.dict()
    result = session().query(
        q.create(q.collection(self._collection_name), {"data": attributes})
    )

    return self.__class__(ref=result["ref"], ts=result["ts"], **result["data"])
 # The code from the previous post

Благодаря Pydantic это довольно просто:

  • Во-первых, я добавил дополнительный атрибут ( _Collection_name ), чтобы получить имя коллекции; Это соответствует названию, которое мы установили на панели инструментов Fauna
  • Мы получаем данные, которые мы хотим сэкономить как обдумывать мы используем Базомодель ‘s Дикт метод экземпляра; Что именно делает именно это.
  • Далее сам процесс сохранения:
    • Затем мы используем сессия объект Создать запрос
    • Мы передаем Коллекция Объект с использованием атрибута имени коллекции
    • Наконец, мы передаем атрибуты обдумывать как второй аргумент. Помните, что мы сказали, что нестандартные объекты фауны должны быть внутри данные Диктовать? Здесь мы имеем именно это.
    • Наконец, мы создаем новый Пользователь Объект, использующий класс __Class__ магический метод; Что это в основном нравится призывать конструктора. Результат Объект – это не что иное, как объект, возвращаемый операцией по сохранению фауны. Я знаю, что это не может быть лучшим подходом (возможно, мы должны просто установить новые атрибуты вместо того, чтобы возвращать новый экземпляр), но это то, что я сделал

Теперь давайте, наконец, пойдем по причине этой всей серии: прося пользователя их имя.

Спрашивать пользователя, если они хотят дать нам свое имя

Помните первый пост, когда мы определили нашу схему? Мы сказали, что маршрут нашей конечной точки будет Можно-имя Имя задача было бы то же самое, имя действия было бы Спросить-имя и ответ от действия будет Can_have_name Отказ Напомним, что действие и задачи – это два разных веща. В этом случае задавать вопрос.

Теперь код:

import json
# The other imports

# The code we added at the beginning

@user_greeting.post("/can-have-name")
def can_have_name(Memory: str = Form(...)):
    """
    Asks the user if he/she wants to give us their name
    :param: Memory: JSON Stringified object from Twilio
    """
    memory = json.loads(Memory)

    answer = memory["twilio"]["collected_data"]["ask-for-name"]["answers"][
        "can_have_name"
    ]["answer"]
    if answer == "Yes":
        return {"actions": [{"redirect": "task://store-user"}]}
    return {
        "actions": [
            {
                "say": "😭"
            }
        ]
    }

Напомним, что наше собирать Вопрос имел тип Twilio. Yes_no , Это означает, что механизм обработки природного языка автопилота сделал это магию, чтобы преобразовать все, что пользователь писал в Да или Нет Ответ (эти два являются буквальными). Поэтому нам просто нужно разбирать этот ответ.

Здесь вам нужно принять во внимание вещь, которая заставила меня ударить на клавиатуру с головой пару дней: автопилот отправляет все запросы с кодировкой Приложение/X-www- Форма-орленкадированный и Память Объект это в JSON, но из-за кодировки Этот JSON не проанализирован ; Это просто простой струна. Из-за этого мы установили Память Параметр, чтобы иметь тип ул И тогда мы используем стандартную библиотеку Python json.loads разбирать Память строка в обдумывать . Теперь, что такое сложный словарь прямо там? Ну, Память Объект содержит другие вещи, помимо проанализированных ответов, проанализированные ответы сохраняются в соответствии с Collected_data Отказ После этого ключа: формат action_name.answers.question_name.answer Как мы сказали выше, зовут нашего действия Спросить-имя И вопрос в том, что Can_have_name

Так что после того, как это запутало бурение, у нас есть наш ответ. Если пользователь сказал, что да, мы просто перенаправляем на задачу по заданию (без каламбура), чтобы получить имя; Если не хорошо, мы просто можем сделать все, что мы хотим. В этом случае давайте просто вернемся эмоджи, почему? Потому что мы используем WhatsApp, и мы можем!

Наконец, получение имени

Для последней конечной точки давайте получим имя пользователя. Напомним, что мы написал в нашей схеме, что маршрут конечной точки – магазин-пользователь :

# Rest of greet.py up here

@user_greeting.post("/store-user")
def store_user(UserIdentifier: str = Form(...), Memory: str = Form(...)):
    """
    Stores a user in the database, fields stored are: country, name, and phone number
    :param: UserIdentifier: Phone number from Twilio
    :param: Memory: JSON Stringified object from Twilio
    """
    memory = json.loads(Memory)
    name: str = memory["twilio"]["collected_data"]["collect-name"]["answers"][
        "first_name"
    ]["answer"]

    country = phone_to_country(UserIdentifier)
    # This needs error handling right here ;)
    new_user = User(
        name=name.capitalize(), phone_number=UserIdentifier, country=country
    ).save()

    return {
        "actions": [
            {"remember": {"name": new_user.name, "country": new_user.country}},
            {
                "say": "Hi, {name} from {c}!".format(
                    name=new_user.name, c=country
                )
            },
            {
                "say": "This is a WIP ok bye"
            }
        ]
    }

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

Следующие пару строк такие же, как и с последней конечной точкой, единственное, что изменяется, это ключи для памяти обдумывать : В этом случае имя действия – Собирать имя И название вопроса в том, что first_name Отказ Если вы проверяете нашу схему, вы поймете, что тип вопроса – Twilio. First_name ; Это значит это? Ты угадал! Двигатель обработки натурального языка Autopilot постарается анализировать ответ в качестве имени.

Теперь, Процесс экономии:

  • Во-первых, мы разбираем страну, как мы сделали в /Приветствие конечная точка
  • Затем мы проходим мгновенно A Пользователь объект с параметрами, которые у нас есть: имя (мы немногомузируем вход немного за счет капитала строки), страны и телефона; Мы также называем Сохранить метод прямо, чтобы иметь полный экземпляр
  • Затем, как на первой конечной точке, мы говорим Бот, чтобы запомнить имя и страну и вернуть любые действия мы хотим

Так что теперь приходит часть, которую мы ждали: тестирование этого плохого мальчика. Для этого нам сначала нужно развернуть нашу API, чтобы получить конечную точку, чтобы положить в нашу схему. Например, есть несколько способов сделать это, например, вы можете развернуть это на Heroku, цифровом океане или просто написать код непосредственно на Glitch, чтобы получить живой URL (который я сделал для этой серии); Что бы вы ни выбрали, зависит от вас! Просто не забудьте установить Отладка Флаг к ложе и заполнить Fauna_server_key env переменная с Ключ сервера Отказ

После того, как API будет жить, давайте перейдем в файл схемы и замените все вхождения our_super_yet_to_be_endpoint с живым URL нашего API. Сейчас самое время создать бот, GO Создать учетную запись Twilio, если у вас уже нет; После этого скопируйте SID и токен авторизации, вы найдете на приборной панели.

Существует два способа создания автопилота бота: первый – через консоль, а вторая использует CLI. Поскольку у нас уже есть схема для бота, давайте использовать CLI; тебе нужно либо пряжа или NPM Для этого я буду использовать первое:

yarn global add twilio-cli
twilio plugins:install @dabblelab/plugin-autopilot

После этого установлен, к Twilio вход В вашем терминале и вставьте ключ SID и AUTH, при появлении запроса.

Теперь установите терминал на путь, где хранятся готовая схема и делает:

twilio autopilot:create -s=./schema.json

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

Теперь давайте добавим свой бот в свой список контактов A Отправить сообщение!

Мы снова можем приветствовать бот:

Наконец, пойдем на панель инструментов Fauna и стереть нашего пользователя; Просто сказать нет и посмотреть, что произойдет:

Большой! Мы видим, что наш бот правильно реагирует на «Да» и «Нет».

«Но подожди минутку» Я слышу, ты говоришь «Разве вы не говорили мне, что сеанс длится 24 часа? Почему разговор сбрасывается во второй раз, когда я приветствовал бот “. Что ж, это была полвея правда на самом деле: сеанс длится 24 часа Если мы включаем Слушать действие в конце или если мы перенаправляем к другому действию . Если вы проверяете схему и действия, которые мы вернулись с наших конечных точек, вы поймете, что не вернули это действие: мы просто проехали пользователей к заранее определенному пути, так сказать. Итак, если возвращенное действие ни перенаправления, ни Слушать Действие, сессия закончится.

Конечно, нет! Вы, вероятно, уже думали об этом: это не кажется слишком безопасным, делает это? Откуда я знаю, что запросы действительно приходят из Twilio? Если вы сделали, вы правы, это совершенно небезопасно: каждый может понять нашу API и сделать это запросы. Итак, давайте изменим это.

У Twilio есть пакет для Python, который, помимо прочего, класс для проверки того, что входящие запросы поступают из Twilio; Давайте установим это:

pip install twilio

Теперь, как мы реализуем это за каждый запрос? Ну, если вы похожи на меня и уже работали с Python в прошлом, вы, вероятно, думаете о декоратере. И, если вы похожи на меня, вы пошли напрямую реализовать указанный декоратор, чтобы он не прошел неудачу, когда развернут живой.

Вы видите, Starlette (и Fastapi, по расширению), не совсем работают так. Как я уже упоминал миллион раз, они – асинхронный ; Таким образом, различные свойства объекта запроса (например, тело или форма) реализуются как асинхронные потоки: если вы употребляете их, скажем, декоратор, они будут завершены и не будут доступны для следующего обработчика в цепочке. Итак, в смертных словах: доступ к доступу к частям асинхронизации запроса в любой другой части, что это не его конечный пункт назначения, сделает эти данные, недоступные в этом конечным пунктам назначения; Поэтому произойдет ошибка.

Итак, если не декоратор, как мы это реализуем? Ну, нам нужно сделать пользовательский маршрутизатор. Что это? Маршрутизатор – это класс, методы которого вызываются до запроса (или ответа), попадают в функцию обработчика. Из-за этого у нас есть, в роутере, полный доступ к запросу (или ответ). Давайте напишем код:

from typing import Callable

from fastapi import Request, Response, HTTPException
from fastapi.routing import APIRoute
from starlette.datastructures import FormData
from twilio.request_validator import RequestValidator

from src.core import config

class AutopilotRequest(Request):
    """
    This class serves two purposes. First one, know that Starlette (framework on which Fastapi is built upon)
    is an ASGI framework. That means that parts of the request (like the body) are async. So, if we await those streams in middleware they will be consumed and will not be available to the final route.
    For that, this class consumes the steam (in this case the form) does what it needs to do with the data,
    and creates a new FormData object to pass to the final route.
    """

    async def form(self):
        if not config.DEBUG:
            return await self.__handle_non_dev_env()
    return super().form()

    async def __handle_non_dev_env(self):
        """
        In production or staging, validate that the request comes from Twilio
        """
        validator = RequestValidator(config.TWILIO_AUTH_TOKEN)

        params = await super().form()
        x_twilio_signature = super().headers.get("X-Twilio-Signature", "no-header")
        is_valid = validator.validate(str(super().url), params, x_twilio_signature)
        if not is_valid:
            raise HTTPException(status_code=403)
        return FormData(dict(params.items()))

class AutopilotRoute(APIRoute):
    """
    The custom route to route requests through our AutopilotRequest object
    """

    def get_route_handler(self) -> Callable:
        original_route_handler = super().get_route_handler()

        async def custom_route_handler(request: Request) -> Response:
            new_request = AutopilotRequest(request.scope, request.receive)
            return await original_route_handler(new_request)

        return custom_route_handler

Хорошо, что здесь происходит? Во-первых, для пользовательского маршрутизатора нам нужны две вещи: пользовательский маршрутизатор и пользовательский объект запроса. Для этого мы подкласс APIROUTE и Запрос (из Fastapi) соответственно.

Теперь, по запросу:

  • Мы заинтересованы только в Форма Свойство, поскольку именно здесь данные Twilio
  • Мы проверяем, если мы находимся в режиме отладки, если мы тогда, мы просто вернем оригинальную форму
  • Теперь в __handle_non_dev_env где волшебство происходит

    • Мы создаем RequestValidator объект с Twilio_auth_token (Помните что?) Свойство, которое уже в нашем объекте конфигурации
    • Мы потребляем Форма поток, увидим, что Wee надо Ждите Это. Нам нужно сделать это, потому что он содержит данные нуждается в валидаторе
    • Затем мы извлекаем заголовок по запросу. Подпись в X-Twilio-подпись заголовок
    • Теперь мы используем RequestValidator ‘s проверить метод выполнения проверки
    • Далее нам нужно проверить, вернул ли этот метод Правда , если не мы поднимаем 403 Запрещено ошибка
    • Наконец, если запрос действительно пришел для Twilio, то нам нужно построить новую форму, поскольку старая была потреблена; Для этого мы просто делаем: Formdata (dict (params.items ())) который вернет новый Formdata с исходными данными по запросу

Теперь маршрутизатор проще: просто нужно преобразовать входящий запрос на Автопилотрепестр и передайте его оригинальному обработчику. Таким образом, когда обработчик называет Форма Метод под капотом, проверка будет происходить.

Как мы это используем? Легко, пойдем к нашему приветствовать файл:

# routes/greet.py

# Original imports
from .custom_router import AutopilotRoute

user_greeting = APIRouter()
user_greeting.route_class = AutopilotRoute # <- here

Вот и все, теперь просто помните, чтобы привлечь ваш токен Twilio Authon и установить Twilio_auth_token с этим.

Теперь вы думаете: «Подожди минутку, мне нужно использовать свой телефон, чтобы отладить это? Разве это не так, стоит денег? “ Да! Вы также правы, текстовые сообщения на номер, предоставленный Twilio стоит денег. Для автопилота они предоставляют симулятор; который отлично подходит для отладки. Но, к сожалению, симулятор не отправляет Usentifientifier свойство; Так как не телефон.

Но не бойся! Я тоже тебя покрыл. Помните, что Fake_number свойство? Давайте будем использовать это сейчас, обратно наш Автопилотрууту :

# The imports

class AutopilotRequest(Request):
    # The huge docstring

    async def form(self):
        if not config.DEBUG:
            return await self.__handle_non_dev_env()
        return await self.__handle_dev_env()

    async def __handle_dev_env(self):
        """
        Here we just inject a fake number for testing, this so we can test from
        the Twilio Autopilot Simulator through an SSH tunnel.
        """
        form = await super().form()
        new_form = FormData(dict(form.items()), UserIdentifier=config.FAKE_NUMBER)
        return new_form

    # __handle_non_dev_env goes here
# Our router goes here

Теперь мы добавили __handle_dev_env Метод, который выполняется, когда Отладка установлен на Истинный . И что это делает? Ну, требуется все, что запрос был и впрыскивает Usentifientifier Имущество как какое бы число мы указали в Fake_number Отказ Сейчас вот!

Итак, для отладки, просто установить Отладка к Истинный и установить Fake_number на действительный номер телефона. Проушина: Вы можете использовать это с чем-то вроде Ngrok, чтобы отладить свой API локально.

Это все люди! Я надеюсь, вам понравилась серия. Я, конечно, многому узнал, делая это. Прежде чем заканчиваться, позвольте мне указать, что это все очень простое, он может быть улучшен во многих отношениях:

  • Нуждается в обработке ошибок
  • Хотя типы по умолчанию Twilio выполняют хорошую работу в распознавании общего языка, бот все еще должен быть обучен с использованием образцов из реальных разговоров
  • Нуждается в лучшей автоматизации для развертывания
  • Намного больше!

Если вы хотите проверить, как я решил это (а также проверить немного более сложный бот), вы можете проверить оригинал бот Я развивался, прежде чем написать этот пост.

PS: О, а также, вот код для этого поста, вы думаете, что забуду? 😉

Использование fastapi и faunadb с автопилотом WhatsApp Bot (3 часть серии)

Оригинал: “https://dev.to/alessandrojcm/getting-your-user-s-country-and-name-in-a-whatsapp-bot-using-fastapi-and-faunadb-part-3-1o6e”