Как работает long polling telegram python

от admin

Опросы v2.0

Добро пожаловать в 2020! В последний раз мы рассматривали нововведения Bot API аж в далёком 2017 году, когда появилось удаление сообщений и ограничения в чатах. С тех пор вышло много чего интересного и, возможно, о чём-то стоит выпустить отдельные уроки.

А сегодня мы познакомимся с опросами 2.0, точнее, с новой сущностью: викторинами (quiz). Викторина – это именно то, что вы и предположили; тест с одним правильными вариантом ответа и ещё N неправильными.

Поставим себе задачу сделать бота, который умеет:

  1. принимать от пользователя только викторины;
  2. запоминать их содержимое и записывать к себе в память;
  3. предлагать викторины в инлайн-запросе и отправляет их в группу;
  4. получать новые ответы и сохранять ID правильно ответивших;
  5. останавливать викторину после двух правильных ответов и поздравлять победителей.

Задач много, придётся вспомнить, что такое колбэки, инлайн-режим и классы. Но и это не самое главное…

Пятиминутка ненависти к telebot или Привет, aiogram!

Как вы знаете, во всех предыдущих уроках использовалась библиотека pyTelegramBotAPI, именуемая в коде telebot. В 2015-2017 годах, возможно, она ещё была актуальна, но прогресс не стоит на месте. А telebot, увы, стоит. Кривая реализация поллинга, проблемный next_step_handler, медленная поддержка новых версий Bot API и т.д.

В течение 2019 года я постепенно переносил своих ботов на другой фреймворк, который по многим пунктам превосходит pyTelegramBotAPI, и имя ему – aiogram. «Почему?», спросит меня уважаемый читатель. Что ж, приведу следующие аргументы:

  • это полноценный фреймворк, т.е. позволяет сделать больше полезных вещей;
  • асинхронный, что делает его быстрее в некоторых задачах;
  • поддерживается Python 3.7+ и выше, что сподвигнет обновить свой старенький интерпретатор и использовать новые возможности языка;
  • множество встроенных «помощников» (синтаксический «сахар»), улучшающих читабельность кода;
  • оперативные обновления (поддержка новых опросов появилась в тот же день, что и в самом Bot API);
  • русскоязычный чат поддержки и обсуждений, где сидит, в том числе, и сам разработчик фреймворка;
  • мой любимый пункт: нормально работающий поллинг.

Прокомментирую последний пункт: в настоящий момент почти все мои боты работают на aiogram-ном поллинге и не падают ежедневно, как в случае с pyTelegramBotAPI.

Введение получилось очень большим, поэтому давайте уже перейдём к делу.

Плацдарм для бота

Напишем элементарного эхо-бота на aiogram с поллингом, чтобы бегло ознакомиться с фреймворком. Прежде всего, добавим нужные импорты (предполагается, что мы используем Virtual Environment, подробнее о нём – в уроке №0):

Теперь создадим объект бота. А за хэндлеры здесь отвечает специальный Диспетчер:

Далее напишем простейший хэндлер, повторяющий текстовые сообщения:

Началась магия.
Во-первых, как я написал чуть выше, за хэндлеры отвечает диспетчер (dp).
Во-вторых, подхэндлерные функции в aiogram асинхронные (async def), вызовы Bot API тоже асинхронные, поэтому необходимо использовать ключевое слово await .
В-третьих, вместо bot.send_message можно для удобства использовать message.reply( ) без указания chat_id и message.id , чтобы бот сделал «ответ» (reply), либо аналог message.answer( ) , чтобы просто отправить в тот же чат, не создавая «ответ». Само выражение в хэндлере пустое, т.к. нас устроят любые текстовые сообщения.

Параметр skip_updates=True позволяет пропустить накопившиеся входящие сообщения, если они нам не важны.
Запускаем код, убеждаемся в его работоспособности, после чего удаляем хэндлер вместе с функцией echo, нам они больше не понадобятся, в отличие от остального кода.

Запрашиваем викторину у пользователя

В BotAPI 4.6 появилась новая кнопка для обычной (не инлайн) клавиатуры с типом KeyboardButtonPollType. При нажатии на неё в приложении Telegram появляется окно для создания опроса. В самой кнопке можно выставить ограничение по типу создаваемого объекта: опрос, викторина или что угодно. Опросы нас пока не интересуют, поэтому напишем обработчик команды /start , выводящий приветственное сообщение и обычную клавиатуру с двумя кнопками: “Создать викторину” и “Отмена”, причём вторая отправляет ReplyKeyboardRemove, удаляя первую клавиатуру.

Сохраняем и предлагаем

В 11-м уроке я использовал библиотеку Vedis для сохранения состояний в файле, чтобы те не сбрасывались после перезагрузки бота. В этот раз мы будем сохранять всё в памяти, а выбор постоянного хранилища останется за читателем, чтобы не навязывать то или иное решение. Разумеется, данные в памяти сотрутся при остановке бота, но для примера так даже лучше.

Наше хранилище будет основано на стандартных питоновских словарях (dict), причём их будет два: первый словарь содержит пары (“id пользователя”, “массив сохранённых викторин”), а второй — пары (“id викторины”, “id автора викторины”). Зачем два словаря? В дальнейшем нам нужно будет по идентификатору викторины получать некоторую информацию о ней. Необходимые нам сведения лежат в первом словаре, но в виде значений, а не ключей. Поэтому нам пришлось бы проходиться по всем возможным парам ключ-значение, чтобы найти нужную викторину.

Для ускорения поиска мы заведём второй словарь, чтобы по идентификатору викторины сразу же найти идентификатор её автора, который, в свою очередь, является ключом в первом словаре. А дальше проход по небольшому массиву и вуаля! Наши данные получены. На словах звучит сложно, но на практике реализуется довольно быстро и с минимальной избыточностью. Если придумаете решение лучше — пишите, буду рад исправить текст.

Помимо определения викторины, нам нужно хранить некоторую дополнительную информацию. Поэтому давайте создадим файл quizzer.py , опишем наш класс Quiz со всеми нужными полями в конструкторе класса (обратите внимание, в конструктор передаются не все поля, т.к. часть из них будет заполнена позднее):

Если вы раньше не сталкивались с подсказками типов (type hints), код вида “chat_id: int = 0” может ввести в замешательство. Здесь chat_id — это имя переменной, далее через двоеточие int — её тип (число), а дальше инициализация числом 0. Python по-прежнему является языком с динамической типизацией, отсюда и название “подсказка типа”. В реальности это влияет только на восприятие кода и предупреждения в полноценных IDE типа PyCharm. Никто не мешает вам написать quiz_id: int = «чемодан» , но зачем так делать? Вернёмся в наш основной файл (я его далее буду называть bot.py ) и импортируем наш класс: from quizzer import Quiz . Также добавим в начале файла под определением бота два пустых словаря:

Теперь будем отлавливать викторины, приходящие в бота. Как только прилетает что-то, похожее на неё, извлекаем информацию и создаём две записи. В первом словаре храним параметры викторины, чтобы потом её воспроизвести, а во втором просто создаём пару викторина-создатель. Идентификаторы, составляющие ключ словаря, конвертируем в строки методом str() :

Раз уж мы сохраняем викторины, давайте теперь позволим пользователям их отправлять, причём через инлайн-режим. Есть одна загвоздка: в BotAPI через инлайн-режим нельзя напрямую отправлять опросы (нет объекта InlineQueryResultPoll), поэтому придётся доставать костыли. Будем возвращать обычное сообщение с URL-кнопкой вида https://t.me/нашбот?startgroup=id_викторины. Параметры startgroup и start — это т.н. “глубокие ссылки” (Deep Linking). Когда пользователь нажмёт на кнопку, он перейдёт по указанной выше ссылке, что, в свою очередь, благодаря параметру startgroup перекинет его к выбору группы, а затем, уже после подтверждения выбора, бот будет добавлен в группу с вызовом команды /start id_викторины .

Начнём разбираться с инлайн-режимом (не забудьте включить его у @BotFather). Когда пользователь вызывает нашего бота через инлайн, показываем все созданные им викторины, плюс кнопку “Создать новую”. Если ничего нет, то только кнопку.

Очень важно выставить флаг is_personal равным True (ответ на запрос будет уникален для каждого Telegram ID) и указать небольшое значение параметра cache_time , чтобы кэш инлайн-ответов оперативно обновлялся по мере появления новых викторин.
Теперь при вызове бота через инлайн мы увидим наши сохранённые викторины, а при выборе одной из них — сообщение с кнопкой, по нажатию на которую нам предложат выбрать группу для отправки сообщения. Как только группа будет выбрана, в неё будет автоматически добавлен бот с сообщением вида /start@имя_бота . Но ничего не происходит! Сейчас разберёмся.

Отправляем викторину и получаем ответы

Помните наш простой обработчик команды /start , возвращающий сообщение с кнопкой? Настало время переписать этот хэндлер. Первым делом, будем проверять, куда отправлено сообщение – в диалог с ботом или нет. Если в диалог, то всё остаётся по-прежнему: приветствие (на этот раз укажем, что викторина принудительно будет сделана неанонимной) и кнопка для создания викторины.

А вот если сообщение отправлено в группу, то применяем следующую логику: проверяем количество “слов” в сообщении. Одно всегда есть (команда /start ), но может быть и второе, невидимое в интерфейсе приложения Telegram – параметр, переданный в качестве параметра startgroup , в нашем случае это ID викторины. Если второго слова нет (количество слов = 1), то показываем сообщение с предложением перейти в личку к боту с принудительным показом кнопки /start .

В случае, если второе слово есть, то считаем его идентификатором и пробуем отправить викторину в ту же группу. При этом мы, по сути, воспроизводим её [викторину] заново, просто от своего имени: повторяем вопрос, варианты ответов и отключаем анонимный режим, т.к. нам нужно знать, кто победитель.

Читать:
Как отправить сообщение в telegram

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

Далее необходимо научиться как-то обрабатывать новые ответы. В свежем обновлении API добавилось два новых типа обновлений (updates, т.е. входящие события): PollAnswer и просто Poll . Первый срабатывает при получении новых ответов в викторинах и опросах, в последнем случае ещё и при отзыве голоса (массив голосов от пользователя будет пустой). Второй срабатывает при изменении состояния опроса в целом, т.е. не только при получении новых ответов/голосов, но и при смене состояния “открыт/закрыт” и др. Опять-таки, в обучающих целях мы задействуем хэндлеры на оба типа событий.

Начнём с PollAnswer . Когда прилетает событие с новым ответом на викторину, прежде всего достаём её ID, по ней ищем автора во втором словаре. Если находим, то гуляем по всем викторинам этого пользователя и ищем совпадение по ID самой викторины, т.е. в точности обратное действие, только уже в первом словаре. Когда обнаружится нужная викторина, то проверяем, верный ответ или нет (сравниваем с correct_option_id ), и если да, то записываем ID пользователя в список победителей. Если количество победителей при этом достигает двух, то останавливаем викторину.

Остановка викторины (метод stop_poll( )) вызовет срабатывание хэндлера на тип обновлений Poll с условием is_closed is True . Снова извлекаем нужный нам экземпляр класса Quiz, вытаскиваем ID победителей и вызываем метод get_chat_member, после чего, используя aiogram-ный вспомогательный метод get_mention , формируем ссылку на каждого из победителей в HTML-разметке и создаём поздравительное сообщение. Викторины у нас одноразовые, поэтому подчищаем за собой словари, дабы не раздувать объекты в памяти.

Код готов. Закинем викторину в группу и попросим друзей правильно ответить, а сами ответим неправильно. После первого правильного ответа:

2 ответа, только один правильный

После второго правильного ответа:

3 ответа, 2 правильных, опрос закрыт

На этом всё! Если у вас возникли вопросы, не стесняйтесь задавать их в нашем чатике, а если вы нашли ошибку/опечатку, либо есть чем дополнить материал, то добро пожаловать на GitHub (ну, или всё так же в чате). Полный код урока можно найти здесь.

Простой Telegram-бот на Python за 30 минут

На Хабре, да и не только, про ботов рассказано уже так много, что даже слишком. Но заинтересовавшись пару недель назад данной темой, найти нормальный материал у меня так и не вышло: все статьи были либо для совсем чайников и ограничивались отправкой сообщения в ответ на сообщение пользователя, либо были неактуальны. Это и подтолкнуло меня на написание статьи, которая бы объяснила такому же новичку, как я, как написать и запустить более-менее осмысленного бота (с возможностью расширения функциональности).

Часть 1: Регистрация бота

Самая простая и описанная часть. Очень коротко: нужно найти бота @BotFather, написать ему /start, или /newbot, заполнить поля, которые он спросит (название бота и его короткое имя), и получить сообщение с токеном бота и ссылкой на документацию. Токен нужно сохранить, желательно надёжно, так как это единственный ключ для авторизации бота и взаимодействия с ним.

Часть 2: Подготовка к написанию кода

Как уже было сказано в заголовке, писать бота мы будем на Python’е. В данной статье будет описана работа с библиотекой PyTelegramBotAPI (Telebot). Если у вас не установлен Python, то сперва нужно сделать это: в терминале Linux нужно ввести

Если же вы пользуетесь Windows, то нужно скачать Python с официального сайта .

После, в терминале Linux, или командной строке Windows вводим

Теперь все готово для написания кода.

Часть 3: Получаем сообщения и говорим «Привет»

Небольшое отступление. Телеграмм умеет сообщать боту о действиях пользователя двумя способами: через ответ на запрос сервера (Long Poll), и через Webhook, когда сервер Телеграмма сам присылает сообщение о том, что кто-то написал боту. Второй способ явно выглядит лучше, но требует выделенного IP-адреса, и установленного SSL на сервере. В этой статье я хочу рассказать о написании бота, а не настройке сервера, поэтому пользоваться мы будем Long Poll’ом.

Открывайте ваш любимый текстовый редактор, и давайте писать код бота!

Первое, что нужно сделать это импортировать нашу библиотеку и подключить токен бота:

Теперь объявим метод для получения текстовых сообщений:

В этом участке кода мы объявили слушателя для текстовых сообщений и метод их обработки. Поле content_types может принимать разные значения, и не только одно, например

Будет реагировать на текстовые сообщения, документы и аудио. Более подробно можно почитать в официальной документации

Теперь добавим в наш метод немного функционала: если пользователь напишет нам «Привет», то скажем ему «Привет, чем я могу помочь?», а если нам напишут команду «/help», то скажем пользователю написать «Привет»:

Данный участок кода не требует комментариев, как мне кажется. Теперь нужно добавить в наш код только одну строчку (вне всех методов).

Теперь наш бот будет постоянно спрашивать у сервера Телеграмма «Мне кто-нибудь написал?», и если мы напишем нашему боту, то Телеграмм передаст ему наше сообщение. Сохраняем весь файл, и пишем в консоли

Где bot.py – имя нашего файла.

Теперь можно написать боту и посмотреть на результат:

Часть 4: Кнопки и ветки сообщений

Отправлять сообщения это несомненно весело, но ещё веселее вести с пользователем диалог: задавать ему вопросы и получать на них ответы. Допустим, теперь наш бот будет спрашивать у пользователя по очереди его имя, фамилию и возраст. Для этого мы будем использовать метод register_next_step_handler бота:

И так, данные пользователя мы записали. В этом примере показан очень упрощённый пример, по хорошему, хранить промежуточные данные и состояния пользователя нужно в БД, но мы сегодня работаем с ботом, а не с базами данных. Последний штрих – запросим у пользователей подтверждение того, что все введено верно, да не просто так, а с кнопками! Для этого немного отредактируем код метода get_age

И теперь наш бот отправляет клавиатуру, но если на нее нажать, то ничего не произойдёт. Потому что мы не написали метод-обработчик. Давайте напишем:

Why does telegram.Bot.getUpdates() long polling returns immediately?

According to telegram api, the getUpdates function receives incoming updates using long polling (wiki). From wiki:

If the server does not have any information available for the client when the poll is received, instead of sending an empty response, the server holds the request open and waits for response information to become available. Once it does have new information, the server immediately sends an HTTP/S response to the client, completing the open HTTP/S Request.

Then, why does the function getUpdates() returns immediately, even though the timeout parameter is set?

Polling и long polling

Чат-боты должны получать уведомления от соцсети моментально. Они не могут проверять обновления каждую секунду, это неэффективно. Большинство ответов от сервера будут неинформативны, вроде «У вас пока нет новых сообщений».

Такой подход, когда раз в n секунд опрашивается сторонний сервис, называется polling.

Чтобы сэкономить на ресурсах, можно использовать long polling. Устроен он так же, как и polling, с одним отличием: сервер дольше отвечает. Вообще, при лонг-поллинге сервер отвечает в двух случаях: или потому, что пришло новое сообщение, или потому, что соединение пора разрывать.

У каждого запроса есть timeout — время, в течении которого нужно ответить. Если на запрос не ответили за это время, считается, что сервер не ответит вообще. Поэтому сервер смотрит на timeout и решает так:

  1. Если за это время у меня не появится обновлений для клиента, я отвечу ему, что их нет.
  2. Если появятся, я отправлю ему обновления сразу, не дожидаясь таймаута.

Чтобы реализовать long polling на стороне клиента, нужно выставить большой timeout: 30 или 60 секунд.

Вот так выглядит polling со стороны клиента на Python:

А вот так выглядит long polling:

Попробуйте бесплатные уроки по Python

Получите крутое код-ревью от практикующих программистов с разбором ошибок и рекомендациями, на что обратить внимание — бесплатно.

Похожие статьи