<jupyter_start><jupyter_text>Домашнее задание №4 (курс "Практикум по программированию на языке Python")  Тема: Web-сервер для Telegram-бота. Преподаватели: <NAME> (<EMAIL>) и <NAME> (<EMAIL>)**Выдана**:   29 апреля 2021**Дедлайн**:   21:00 13 мая 2021**Среда выполнения**: Jupyter Notebook (Python 3.7) Правила:Результаты выполнения задания:- архив со скриптами и файлами Dockerfile, Pipfile и Pipfile.lock, который 1-2 команды позволяет развернуть сервер, решающий поставленные в задании задачи- Jupyter Notebook, где __весь код__ из скриптов дублируется (1 ячейка - 1 скрипт) с комментарием, содержащим информацию о том, из какого файла взят код и что верхнеуровнево этот код делает- ссылка на чат-бота в Telegram, который автоматически начинает обработку пользовательских запросов после разворачивания сервера__Максимальное число баллов за задание - 25__.Работа делится на базовую и дополнительную части, __полное выполнение базовой является обязательным__! Без этого вся работа автоматически оцениваниется 0 баллов. Дополнительную часть можно решать фрагментарно, оценка будет выставляться пропорционально.Готовое задание отправляется на почту преподавателя.Задание выполняется самостоятельно. Если какие-то студенты будут уличены в списывании, все они автоматически получат за эту работу 0 баллов. Если вы нашли в Интернете какой-то специфичный код, который собираетесь заимствовать, обязательно укажите это в задании - наверняка вы не единственный, кто найдёт и использует эту информацию.Удалять фрагменты формулировок заданий запрещается.  Постановка задачи:- В данной работе нужно написать веб-сервер для обслуживания чат-бота в мессенджере Telegram- Чат-бот предназначен для обработки изображений, передаваемых пользователем- Сервер должен быть реализован на Flask + waitress- Проект разворачивается с помощью библиотеки управления виртуальными окружениями Pipenv и технологии контейнеризации Docker[Пример полностью решенного задания](https://t.me/mipt_python_server_bot)  Базовая часть (12 баллов)- Требуется реализовать чат-бота для мессенджера Telegram, который возвращает пользователю на любое входное текстовое сообщение это сообщение. Предполагается, что никакие другие данные (картинки, стикеры) бот получать не будет- Бэкенд проекта должен быть реализован на Flask + waitress- Для прокидывания сервера наружу требуется использовать сервис и утилиту ngrok- Проект должен разворачиваться с помощью Docker, внутри которого создается виртуальное окружение с помощью Pipenv, решение должно разворачиваться командами:```docker build -t tg-bot .docker run -d tg-bot```- __Обязательно (!!!)__ нужно вставить копии содержимого всех файлов проекта в этот ноутбук (1 файл - 1 ячейка) и подписать, к какому файлу относится какая ячейка и что код в ней в общих чертах делает __Рекомендации:__1. [Ссылка](https://core.telegram.org/bots) на инструкцию по созданию ботов в Telegram2. Для работы с Telegram лучше всего использовать библиотеку python-telegram-bot3. Рекомендуемая сигнатура для обработчика POST-запросов от бота:<jupyter_code># app = Flask(__name__)

@app.route(f'/{BOT_TOKEN}', methods=['POST'])<jupyter_output><empty_output><jupyter_text>4. Рекомендуемая структура проекта (плоская):    - Dockerfile    - Pipfile    - Pipfile.lock    - settings.py (для хранения токена, локального адреса сервера \[localhost + port\] и любых иных общих статических данных)    - server.py (для старта локального сервера получения/отправки запросов)    - configure_and_run.py (для настройки и старта всего сервиса)    Опционально можно завести отдельные файлы для логики обработчика запросов, хранения текстов сообщений и любых иных целей5. Dockerfile может иметь следующую структуру:```FROM python:3.7.9-stretch установка нужных утилит в систему, например, curl и unzip установка pipenvWORKDIR /app копирование всех важных файлов в рабочую директорию запуск pipenvENTRYPOINT ["pipenv", "run", "python", "configure_and_run.py", "&"]```Последняя команда будет запускаться при вызове `docker run`6. Вывод информации для отладки и логгирования в stdout можно делать с помощью `os.system(f'echo {message}')`7. Поскольку локально поднятый сервер не виден снаружи, нужно как-то прокинуть его в Интернет, это можно сделать с помощью облачного хостинга или сервиса ngrok, который можно использовать через одноименную консольную утилиту, что и требуется в задании8. Выкачивание и запуск ngrok можно выполнить в configure_and_run.py, в этом помогут утилиты curl и unzip. Для этих и других задач, требующих параллельного вызова сторонних приложений и скриптов, полезен модуль subprocess9. После того, как ngrok будет запущен на указанном локальном адресе, нужно будет передать внешний адрес боту (выставить webhook). Достать этот адрес можно, запустив следующую команду и распарсив выходной файл (там будет json):<jupyter_code>os.system(f'curl http://localhost:4040/api/tunnels > {filename}')<jupyter_output><empty_output><jupyter_text>10. После этого выставить webhook можно в server.py после создания и перед запуском Flask-сервера командой<jupyter_code># bot = telegram.Bot(BOT_TOKEN)

bot.setWebhook('{URL}/{HOOK}'.format(URL=EXTERNAL_URL, HOOK=BOT_TOKEN))<jupyter_output><empty_output><jupyter_text>11. Обработку запросов стоит оборачивать в try-except-блок и отправлять пользователю валидное сообщение об ошибке или просьбу повторить какое-то действие, саму ошибку при этому надо логгировать12. Все вспомогательные файлы, возникающие в процессе работы, должны гарантированно удаляться, как только надобность в них отпадает13. Запуск сервера имеет смысл производить из configure_and_run.py с помощью subprocess (полезен будет метод communicate)  Дополнительная часть__Задание 1 (2 балла):__ Доработать базовое решение до бота, который предлагает пользователю отправить изображение в формате JPG или PNG (в виде аттача), реагирует адекватно на попытки послать ему что-то иное, и в случае получения валидного изображения возвращает его в чат ответным сообщением__Рекомендации:__1. Нужно аккуратно обрабатывать крайние случаи (пришел файл, не являющийся изображением, или имеющий не тот формат)2. Полезна функция, которая определяет, что сообщение содержит файл или фото:<jupyter_code># content = telegram.Update...

@staticmethod
def is_file(content):
    return (content.message is not None and
            ((content.message.document is not None and content.message.document.file_id is not None) or
             (content.message.photo is not None and len(content.message.photo) and content.message.photo[0].file_id is not None)))<jupyter_output><empty_output><jupyter_text>__Задание 2 (5 баллов):__ Модернизировать решение из предыдущего пункта - при получении изображения от пользователя нужно вернуть не его, а текстовое сообщение с кнопкой, в котором написано, что при нажатии кнопки полученное ранее изображение будет отправлено пользователю обратно. Это и должно происходить при нажатии на кнопку. При этом пользователь может отправить в ответ еще одно изображение, и тогда он снова получит сообщение с кнопкой, но сохранено будет уже новое изображение.__Рекомендации:__1. В отличие от предыдущих пунктов, здесь сессия пользователя начинает содержать состояния: пользователь еще не отправил картинку, и пользователь уже сделал это. Это означает, что стоит завести профиль пользователя, который будет идентифицироваться по `chat_id` (который используется при отправке сообщений) и содержать текущие состояние, картинку или любые иные полезные и вспомогательные данные пользователя2. Хранить нужно не один общий профиль, а по профилю на ользователя, иначе сервис нельзя будет использоваться нескольким пользователям. Обычно профили хранятся в базах, для простоты реализации в задании достаточно ограничиться хранением в `dict` в оперативной памяти, который заводится при старте сервера3. Нужно следить за корректностью изменения состояния сессии и профиля пользователя, в том числе и при возникновении различных ошибок4. Полезна функция, которая определяет, что сообщение отправлено нажатием кнопки:<jupyter_code>@staticmethod
def is_button(content):
    return content.callback_query is not None and content.callback_query.data is not None<jupyter_output><empty_output>
{'fraction_non_alphanumeric': 0.056338028169014086, 'fraction_numerical': 0.008083282302510717, 'mean_word_length': 6.021496130696475, 'pattern_counts': {'":': 0, '<': 25, '<?xml version=': 0, '>': 26, 'https://': 2, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 1, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'chain_length': '5', 'dir': 'jupyter-structured-clean-dedup', 'id': '359502', 'license': 'MIT', 'n_tokens_mistral': 3104, 'n_tokens_neox': 3686, 'n_words': 938, 'path': 'tasks/04-server.ipynb', 'repo_name': 'sir-rois/mipt-python', 'size': '8205'}