Slide 1

Slide 1 text

Django внутри Django Фреймворк для чатботов Михаил Новиков Founder & Dev Lead, Fasttrack [email protected] [email protected] 1

Slide 2

Slide 2 text

Fasttrack Bot as a service fstrk.io [email protected] 2

Slide 3

Slide 3 text

История [email protected] 3

Slide 4

Slide 4 text

История [email protected] 4

Slide 5

Slide 5 text

История • 2016 год. В чатботов додумались запихнуть кнопки. Начинается хайп [email protected] 4

Slide 6

Slide 6 text

История • 2016 год. В чатботов додумались запихнуть кнопки. Начинается хайп • FB выкатывает Messenger API [email protected] 4

Slide 7

Slide 7 text

История • 2016 год. В чатботов додумались запихнуть кнопки. Начинается хайп • FB выкатывает Messenger API • Viber выкатывает Public Account API [email protected] 4

Slide 8

Slide 8 text

История • 2016 год. В чатботов додумались запихнуть кнопки. Начинается хайп • FB выкатывает Messenger API • Viber выкатывает Public Account API • ... и мы пилим свой конструктор [email protected] 4

Slide 9

Slide 9 text

Как устроены чатботы в туториалах [email protected] 5

Slide 10

Slide 10 text

Как устроены чатботы в реальной жизни [email protected] 6

Slide 11

Slide 11 text

2016: простой конструктор [email protected] 7

Slide 12

Slide 12 text

2017: сложный конструктор [email protected] 8

Slide 13

Slide 13 text

2018: очень сложный конструктор [email protected] 9

Slide 14

Slide 14 text

Как организовать выполнение команд? [email protected] 10

Slide 15

Slide 15 text

Как организовать выполнение команд? Договориться о формате команд • Что мы зашиваем в кнопки пользователю? Придумать, как их маршрутизировать • Когда эти команды прилетают к нам: как их распознавать? [email protected] 11

Slide 16

Slide 16 text

Как организовать выполнение команд? # вьюха DRF def receive_webhook(request, bot_uuid): chat_id = request.data['chat_id'] payload = request.data['payload'] # вход в логику def do_logic(bot_uuid, chat_id, payload): ... [email protected] 12

Slide 17

Slide 17 text

Как организовать выполнение команд? # Может быть, так? # высылаем пейлоады типа "get_node_10", # "answer_question_15", "choose_operator_3" if payload.startswith('get_node'): return get_node(payload) elif payload.startswith('answer_question'): return answer_question(payload) elif ... # еще 100 условий [email protected] 13

Slide 18

Slide 18 text

Что если... def do_logic(bot_uuid, chat_id, payload): ... payload логика /get_node/15/ get_node(15) /answer_question/12/ answer_question(12) [email protected] 14

Slide 19

Slide 19 text

Что если... urlpatterns = [ url(r'^get_node/(?P.+)/', utils.get_node), url(r'^answer_question/(?P.+)/', utils.answer_question), ] [email protected] 15

Slide 20

Slide 20 text

Что если... from django.urls import resolve # вьюха DRF def receive_webhook(request, bot_uuid): chat_id = request.data['chat_id'] payload = request.data['payload'] # /get_node/15/ match = resolve(payload) # превращаем строчку в функцию с параметрами if match: view = match.view # apps.routing.views.get_node args = match.args # () kwargs = match.kwargs # {"node_pk": 15} return view(*args, **kwargs) [email protected] 16

Slide 21

Slide 21 text

Вложенный URLconf [email protected] 17

Slide 22

Slide 22 text

Вложенный django [email protected] 18

Slide 23

Slide 23 text

Regular views vs. nested views • Обычные вьюхи: резолвятся обычным urlconf, принимают Request и возвращают Response. • Вложенные вьюхи: резолвятся вложенным urlconf, принимают BotRequest и возвращают BotResponse. def get_node(botrequest, node_id): node = Node.objects.get(pk=node_id) response = build_response(botrequest, node) return BotResponse(messages=response.messages) [email protected] 19

Slide 24

Slide 24 text

Инсайт: скорость urlconf Если у вас очень много урлов, то resolve по подмножеству urlconf намного дешевле, чем по всему urlconf resolve('/get_node/15/') # 1000 runs: 2.3576704149367 s resolve('/get_node/15/', urlconf='apps.routing.urls') # 1000 runs: 0.0216069859853 s [email protected] 20

Slide 25

Slide 25 text

Инсайт: скорость urlconf То же самое с reverse и с template-тегом {% url %}. {% for obj in objects %} {% endfor %} Если объектов objects много - это замедлит вам рендер шаблона [email protected] 21

Slide 26

Slide 26 text

Что мы говорим клиентам [email protected] 22

Slide 27

Slide 27 text

Что мы говорим клиентам • "Чатбот за 5 минут!" [email protected] 22

Slide 28

Slide 28 text

Что мы говорим клиентам • "Чатбот за 5 минут!" • "Без навыков программирования!" [email protected] 22

Slide 29

Slide 29 text

Что мы говорим клиентам • "Чатбот за 5 минут!" • "Без навыков программирования!" • "Визуальный конструктор!" [email protected] 22

Slide 30

Slide 30 text

Что мы говорим клиентам • "Чатбот за 5 минут!" • "Без навыков программирования!" • "Визуальный конструктор!" • ...это не совсем так. [email protected] 23

Slide 31

Slide 31 text

Как отправить сообщение в ФБ { "payload": { "text": "Выберите свою пиццу", "buttons": [ { "type": "postback", "data": "pizza1", "title": "Маргарита" }, { "type": "postback", "data": "pizza2", "title": "Четыре сыра" } ] } } [email protected] 24

Slide 32

Slide 32 text

Шаблонизация! { "payload": { "text": "Выберите свою пиццу", "buttons": [ {% for item in api_response %}, { "type": "postback", "data": "{{ item.id }}", "title": "{{ item.name }}" }, {% endfor %} ] } } [email protected] 25

Slide 33

Slide 33 text

Теперь у нас еще и свой шаблонизатор [email protected] 26

Slide 34

Slide 34 text

Безопасность [email protected] 27

Slide 35

Slide 35 text

Безопасность • Что если юзер вызовет тег {% debug %}? [email protected] 27

Slide 36

Slide 36 text

Безопасность • Что если юзер вызовет тег {% debug %}? • Что если юзер сделает {% url %}? [email protected] 27

Slide 37

Slide 37 text

Безопасность • Что если юзер вызовет тег {% debug %}? • Что если юзер сделает {% url %}? • Запретить! [email protected] 27

Slide 38

Slide 38 text

Безопасность # Шаблон нормального человека template = Template(string) rendered = template.render(context) # Шаблон курильщика class SafeEngine(Engine): default_builtins = [ ... # переопределяем дефолтные теги ] template = Template(string, engine=engine) rendered = template.render(context) [email protected] 28

Slide 39

Slide 39 text

Безопасность # Шаблон нормального человека template = Template(string) rendered = template.render(context) # Шаблон курильщика class SafeEngine(Engine): default_builtins = [ ... # переопределяем дефолтные теги ] template = Template(string, engine=engine) rendered = template.render(context) [email protected] 29

Slide 40

Slide 40 text

Как настраивать бизнес-логику? [email protected] 30

Slide 41

Slide 41 text

Как настраивать бизнес-логику? [email protected] 31

Slide 42

Slide 42 text

Как настраивать бизнес-логику? • Нужен способ писать логику на простом мета-языке [email protected] 31

Slide 43

Slide 43 text

Как настраивать бизнес-логику? • Нужен способ писать логику на простом мета-языке • Язык должен быть безопасным (rm -rf) [email protected] 31

Slide 44

Slide 44 text

Как настраивать бизнес-логику? • Нужен способ писать логику на простом мета-языке • Язык должен быть безопасным (rm -rf) • Язык должен выполнять типовые команды [email protected] 31

Slide 45

Slide 45 text

Как настраивать бизнес-логику? • Нужен способ писать логику на простом мета-языке • Язык должен быть безопасным (rm -rf) • Язык должен выполнять типовые команды • Переключиться на др.блок [email protected] 31

Slide 46

Slide 46 text

Как настраивать бизнес-логику? • Нужен способ писать логику на простом мета-языке • Язык должен быть безопасным (rm -rf) • Язык должен выполнять типовые команды • Переключиться на др.блок • Отправить сообщение [email protected] 31

Slide 47

Slide 47 text

Как настраивать бизнес-логику? • Нужен способ писать логику на простом мета-языке • Язык должен быть безопасным (rm -rf) • Язык должен выполнять типовые команды • Переключиться на др.блок • Отправить сообщение • Отправить email [email protected] 31

Slide 48

Slide 48 text

Как настраивать бизнес-логику? • Нужен способ писать логику на простом мета-языке • Язык должен быть безопасным (rm -rf) • Язык должен выполнять типовые команды • Переключиться на др.блок • Отправить сообщение • Отправить email • ... [email protected] 31

Slide 49

Slide 49 text

Как настраивать бизнес-логику? • Нужен способ писать логику на простом мета-языке • Язык должен быть безопасным (rm -rf) • Язык должен выполнять типовые команды • Переключиться на др.блок • Отправить сообщение • Отправить email • ... • Язык должен быть легко тестируемым и расширяемым [email protected] 31

Slide 50

Slide 50 text

Что если... {% for item in objects %} {% if item.expired %} Срок истек {% else %} Срок: {% now %} {% endif %} {% endfor %} [email protected] 32

Slide 51

Slide 51 text

Template tags 1. Собирается дерево нодов {% for item in objects %} {% if item.expired %} Срок истек {% else %} Срок: {% now %} {% endif %} {% endfor %} [email protected] 33

Slide 52

Slide 52 text

Template tags 2. Выполняется рекурсивный render() {% for item in objects %} {% if item.expired %} Срок истек {% else %} Срок: {% now %} {% endif %} {% endfor %} [email protected] 34

Slide 53

Slide 53 text

Что если... Если мы засунем действия бота в render(), то мы дадим возможность юзерам писать логику на DTL! [email protected] 35

Slide 54

Slide 54 text

Бизнес-логика на DTL {% if request.content == 'Супер пароль' %} {% send_message "Пароль верен!" %} {% send_email user.email subject="Сабж" body="Важный контент" %} {% else %} {% send_message "Пароль неверен!" %} {% switch_to "Ввод пароля" %} {% endif %} [email protected] 36

Slide 55

Slide 55 text

Получилась low- code система • Юзеры настраивают граф переходов со статикой • Сложная логика пишется на DTL [email protected] 37

Slide 56

Slide 56 text

+1 Template Engine [email protected] 38

Slide 57

Slide 57 text

Каждый цикл "вопрос- ответ" требует препроцессинга [email protected] 39

Slide 58

Slide 58 text

Каждый цикл "вопрос- ответ" требует препроцессинга • Укорачиваем ссылки в сообщениях [email protected] 39

Slide 59

Slide 59 text

Каждый цикл "вопрос- ответ" требует препроцессинга • Укорачиваем ссылки в сообщениях • Все PNG преобразуем в JPG [email protected] 39

Slide 60

Slide 60 text

Каждый цикл "вопрос- ответ" требует препроцессинга • Укорачиваем ссылки в сообщениях • Все PNG преобразуем в JPG • Ограничиваем время ответа бота [email protected] 39

Slide 61

Slide 61 text

Каждый цикл "вопрос- ответ" требует препроцессинга • Укорачиваем ссылки в сообщениях • Все PNG преобразуем в JPG • Ограничиваем время ответа бота • Логируем вопросы и ответы [email protected] 39

Slide 62

Slide 62 text

Каждый цикл "вопрос- ответ" требует препроцессинга • Укорачиваем ссылки в сообщениях • Все PNG преобразуем в JPG • Ограничиваем время ответа бота • Логируем вопросы и ответы • ... [email protected] 39

Slide 63

Slide 63 text

Что если... NESTED_MIDDLEWARE = [ 'routing.LinkShortenMiddleware', 'routing.PngToJpgMiddleware', 'routing.LimitResponseTimeMiddleware', 'routing.LogMessagesMiddleware', ] [email protected] 40

Slide 64

Slide 64 text

Фунциональный Middleware в Django def some_middleware(next_handler): # функция создает и возвращает мидлвар def handler(request): # код до след. мидлваров response = next_handler(request) # код после след.мидлваров return response return handler # как использовать: формируем цепочку мидлваров middleware_chain = middleware1(middleware2(middleware3)) # оборачиваем конечную вьюху в мидлвар wrapped_get_node = middleware_chain(get_node) # вызываем ее response = wrapped_get_node(node) [email protected] 41

Slide 65

Slide 65 text

Фунциональный Middleware в Django def some_middleware(next_handler): # функция создает и возвращает мидлвар def handler(request): # код до след. мидлваров response = next_handler(request) # код после след.мидлваров return response return handler # как использовать: формируем цепочку мидлваров middleware_chain = middleware1(middleware2(middleware3)) # оборачиваем конечную вьюху в мидлвар wrapped_get_node = middleware_chain(get_node) # вызываем ее response = wrapped_get_node(node) [email protected] 42

Slide 66

Slide 66 text

Примеры мидлваров 1. Логируем запросы и ответы def logging_middleware(next_handler): def handler(request): log(request) response = next_handler(request) log(response) return response return handler [email protected] 43

Slide 67

Slide 67 text

Примеры мидлваров 2. Не пускаем неавторизованных юзеров def restrict_access_middleware(next_handler): def handler(request): if request.user.is_authenticated: # цепочка мидлваров продолжится, # только если юзер аутентифицирован response = next_handler(request) else: # иначе цепочка оборвется response = 'Access Denied' return response return handler [email protected] 44

Slide 68

Slide 68 text

Теперь у нас есть вложенный Middleware [email protected] 45

Slide 69

Slide 69 text

Slide 70

Slide 70 text

Ещё Django Достаем глубоко вложенный элемент словаря some_dict = { "a": { "b": [ {"c": 15}, {"d": 20}, ] } } path = 'a.b.1.d' [email protected] 47

Slide 71

Slide 71 text

Ещё Django Достаем глубоко вложенный элемент словаря from django.template import Variable some_dict = { "a": { "b": [ {"c": 15}, {"d": 20}, ] } } path = 'a.b.1.d' result = Variable(path).resolve(some_dict) [email protected] 48

Slide 72

Slide 72 text

Ещё Django Сервисные объекты class CreateBooking(Service): name = forms.CharField() email = forms.EmailField() def process(self): name = self.cleaned_data['name'] email = self.cleaned_data['email'] # ... логика CreateBooking.execute({'name': 'Mike', 'email': '[email protected]'}) [email protected] 49

Slide 73

Slide 73 text

Ещё Django Сервисные объекты class CreateBooking(Service): name = forms.CharField() email = forms.EmailField() def process(self): name = self.cleaned_data['name'] email = self.cleaned_data['email'] # ... логика CreateBooking.execute({'name': 'Mike', 'email': '[email protected]'}) [email protected] 50

Slide 74

Slide 74 text

Теперь поиздеваемся над Celery Ловим таски, созданные другими приложениями # custom_consumer.py from celery.worker import consumer class Consumer(consumer.Consumer): def on_unknown_message(self, body, message): try: data = json.loads(body) do_stuff(data) finally: message.ack() # ------------------------ # app.py app = Celery('my_app') app.conf.worker_consumer = 'custom_consumer.Consumer' [email protected] 51

Slide 75

Slide 75 text

Теперь поиздеваемся над Celery Ловим таски, созданные другими приложениями # custom_consumer.py from celery.worker import consumer class Consumer(consumer.Consumer): def on_unknown_message(self, body, message): try: data = json.loads(body) do_stuff(data) finally: message.ack() # ------------------------ # app.py app = Celery('my_app') app.conf.worker_consumer = 'custom_consumer.Consumer' [email protected] 52

Slide 76

Slide 76 text

Резюме • Django полон интересных паттернов • Они не только для веб-разработки • Иногда полезно заглядывать под капот [email protected] 53

Slide 77

Slide 77 text

Спасибо! Михаил Новиков Founder & Dev Lead, Fasttrack [email protected] [email protected] 54