Опросы v2.0
Пятиминутка ненависти к 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):
#!venv/bin/python import logging from aiogram import Bot, Dispatcher, executor, types logging.basicConfig(level=logging.INFO)
Теперь создадим объект бота. А за хэндлеры здесь отвечает специальный Диспетчер:
bot = Bot(token="12345678:AABcdeFGhIJkXyZ") dp = Dispatcher(bot)
Далее напишем простейший хэндлер, повторяющий текстовые сообщения:
@dp.message_handler() async def echo(message: types.Message): await message.reply(message.text)
Началась магия.
Во-первых, как я написал чуть выше, за хэндлеры отвечает диспетчер (dp).
Во-вторых, подхэндлерные функции в aiogram асинхронные (async def), вызовы Bot API тоже асинхронные, поэтому необходимо использовать ключевое слово await .
В-третьих, вместо bot.send_message можно для удобства использовать message.reply( ) без указания chat_id и message.id , чтобы бот сделал «ответ» (reply), либо аналог message.answer( ) , чтобы просто отправить в тот же чат, не создавая «ответ». Само выражение в хэндлере пустое, т.к. нас устроят любые текстовые сообщения.
if __name__ == "__main__": executor.start_polling(dp, skip_updates=True)
Параметр skip_updates=True позволяет пропустить накопившиеся входящие сообщения, если они нам не важны.
Запускаем код, убеждаемся в его работоспособности, после чего удаляем хэндлер вместе с функцией echo, нам они больше не понадобятся, в отличие от остального кода.
Запрашиваем викторину у пользователя
В BotAPI 4.6 появилась новая кнопка для обычной (не инлайн) клавиатуры с типом KeyboardButtonPollType. При нажатии на неё в приложении Telegram появляется окно для создания опроса. В самой кнопке можно выставить ограничение по типу создаваемого объекта: опрос, викторина или что угодно. Опросы нас пока не интересуют, поэтому напишем обработчик команды /start , выводящий приветственное сообщение и обычную клавиатуру с двумя кнопками: “Создать викторину” и “Отмена”, причём вторая отправляет ReplyKeyboardRemove, удаляя первую клавиатуру.
# Хэндлер на команду /start @dp.message_handler(commands=["start"]) async def cmd_start(message: types.Message): poll_keyboard = types.ReplyKeyboardMarkup(resize_keyboard=True) poll_keyboard.add(types.KeyboardButton(text="Создать викторину", request_poll=types.KeyboardButtonPollType(type=types.PollType.QUIZ))) poll_keyboard.add(types.KeyboardButton(text="Отмена")) await message.answer("Нажмите на кнопку ниже и создайте викторину!", reply_markup=poll_keyboard) # Хэндлер на текстовое сообщение с текстом “Отмена” @dp.message_handler(lambda message: message.text == "Отмена") async def action_cancel(message: types.Message): remove_keyboard = types.ReplyKeyboardRemove() await message.answer("Действие отменено. Введите /start, чтобы начать заново.", reply_markup=remove_keyboard)
Клавиатура с кнопками
Сохраняем и предлагаем
В 11-м уроке я использовал библиотеку Vedis для сохранения состояний в файле, чтобы те не сбрасывались после перезагрузки бота. В этот раз мы будем сохранять всё в памяти, а выбор постоянного хранилища останется за читателем, чтобы не навязывать то или иное решение. Разумеется, данные в памяти сотрутся при остановке бота, но для примера так даже лучше.
Наше хранилище будет основано на стандартных питоновских словарях (dict), причём их будет два: первый словарь содержит пары (“id пользователя”, “массив сохранённых викторин”), а второй — пары (“id викторины”, “id автора викторины”). Зачем два словаря? В дальнейшем нам нужно будет по идентификатору викторины получать некоторую информацию о ней. Необходимые нам сведения лежат в первом словаре, но в виде значений, а не ключей. Поэтому нам пришлось бы проходиться по всем возможным парам ключ-значение, чтобы найти нужную викторину.
Для ускорения поиска мы заведём второй словарь, чтобы по идентификатору викторины сразу же найти идентификатор её автора, который, в свою очередь, является ключом в первом словаре. А дальше проход по небольшому массиву и вуаля! Наши данные получены. На словах звучит сложно, но на практике реализуется довольно быстро и с минимальной избыточностью. Если придумаете решение лучше — пишите, буду рад исправить текст.
Помимо определения викторины, нам нужно хранить некоторую дополнительную информацию. Поэтому давайте создадим файл quizzer.py , опишем наш класс Quiz со всеми нужными полями в конструкторе класса (обратите внимание, в конструктор передаются не все поля, т.к. часть из них будет заполнена позднее):
from typing import List class Quiz: type: str = "quiz" def __init__(self, quiz_id, question, options, correct_option_id, owner_id): # Используем подсказки типов, чтобы было проще ориентироваться. self.quiz_id: str = quiz_id # ID викторины. Изменится после отправки от имени бота self.question: str = question # Текст вопроса self.options: List[str] = [*options] # "Распакованное" содержимое массива m_options в массив options self.correct_option_id: int = correct_option_id # ID правильного ответа self.owner: int = owner_id # Владелец опроса self.winners: List[int] = [] # Список победителей self.chat_id: int = 0 # Чат, в котором опубликована викторина self.message_id: int = 0 # Сообщение с викториной (для закрытия)
Если вы раньше не сталкивались с подсказками типов (type hints), код вида “chat_id: int = 0” может ввести в замешательство. Здесь chat_id — это имя переменной, далее через двоеточие int — её тип (число), а дальше инициализация числом 0. Python по-прежнему является языком с динамической типизацией, отсюда и название “подсказка типа”. В реальности это влияет только на восприятие кода и предупреждения в полноценных IDE типа PyCharm. Никто не мешает вам написать quiz_id: int = «чемодан» , но зачем так делать? Вернёмся в наш основной файл (я его далее буду называть bot.py ) и импортируем наш класс: from quizzer import Quiz . Также добавим в начале файла под определением бота два пустых словаря:
quizzes_database = <> # здесь хранится информация о викторинах quizzes_owners = <> # здесь хранятся пары "id викторины id её создателя"
Теперь будем отлавливать викторины, приходящие в бота. Как только прилетает что-то, похожее на неё, извлекаем информацию и создаём две записи. В первом словаре храним параметры викторины, чтобы потом её воспроизвести, а во втором просто создаём пару викторина-создатель. Идентификаторы, составляющие ключ словаря, конвертируем в строки методом 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 .
В случае, если второе слово есть, то считаем его идентификатором и пробуем отправить викторину в ту же группу. При этом мы, по сути, воспроизводим её [викторину] заново, просто от своего имени: повторяем вопрос, варианты ответов и отключаем анонимный режим, т.к. нам нужно знать, кто победитель.
Очень важный момент: при отправке викторины, в объекте 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 (ну, или всё так же в чате). Полный код урока можно найти здесь.
Работа с опросами в модуле python-telegram-bot
Внимание! Пакеты python-telegram-bot версии 13.x будут придерживаться многопоточной парадигмы программирования (*на данный момент актуальна версия 13.15). Пакеты версий 20.x и новее предоставляют чистый асинхронный Python интерфейс для Telegram Bot API. Дополнительно смотрите основные изменения в пакете python-telegram-bot версии 20.x.
Базовый пример бота, который работает с опросами и построен на библиотеке python-telegram-bot . В примере задается лимит на количество участников/ответов (принимается только 3 ответа от 3-х пользователей), которые могут взаимодействовать с каждым опросом/викториной.
Команда /preview генерирует пользовательский закрытый опрос/викторину, очень похожую на ту, которую пользователь отправляет боту.
Хранение промежуточных данных опроса происходит в bot_data для последующего использования в функции ответа на опрос/викторину.
Пример бота, работающего с опросами, снабжен подробными комментариями.
import logging # импорт API Telegramm from telegram import ( Poll, ParseMode, KeyboardButton, KeyboardButtonPollType, ReplyKeyboardMarkup, ReplyKeyboardRemove, Update, ) # импорт расширений библиотеки from telegram.ext import ( Updater, CommandHandler, PollAnswerHandler, PollHandler, MessageHandler, Filters, ) # подключение логирования logging.basicConfig( format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', level=logging.INFO ) logger = logging.getLogger(__name__) ### ОПРЕДЕЛЕНИЕ ФУНКЦИЙ ОБРАТНОГО ВЫЗОВА ### def start(update, _): """Информация о том, что может сделать этот бот""" update.message.reply_text( 'Введите `/poll` для участия в опросе, `/quiz` для участия в викторине или `/preview`' ' чтобы создать собственный опрос/викторину' ) def poll(update, context): """Отправка заранее подготовленного опроса""" # Вопрос опроса и его ответы. questions = "Как дела?" answer = ["Нормально", "Хорошо", "Отлично", "Супер!"] # Отправляем опрос в чат message = context.bot.send_poll( update.effective_chat.id, questions, answer, is_anonymous=False, allows_multiple_answers=True, ) # Сохраним информацию опроса в `bot_data` для последующего # использования в функции `receive_poll_answer` payload = # ключом словаря с данными будет `id` опроса message.poll.id: "questions": questions, "message_id": message.message_id, "chat_id": update.effective_chat.id, "answers": 0, > > # сохранение промежуточных результатов в `bot_data` context.bot_data.update(payload) def receive_poll_answer(update, context): """Итоги опроса пользователей""" answer = update.poll_answer poll_id = answer.poll_id try: questions = context.bot_data[poll_id]["questions"] except KeyError: # Это ответ на старый опрос return selected_options = answer.option_ids answer_string = "" # подсчет и оформление результатов for question_id in selected_options: if question_id != selected_options[-1]: answer_string += questions[question_id] + " и " else: answer_string += questions[question_id] context.bot.send_message( context.bot_data[poll_id]["chat_id"], f"update.effective_user.mention_html()> => answer_string>!", parse_mode=ParseMode.HTML, ) # изменение промежуточных результатов в `bot_data` context.bot_data[poll_id]["answers"] += 1 # Закрываем опрос после того, как проголосовали три участника if context.bot_data[poll_id]["answers"] == 3: context.bot.stop_poll( context.bot_data[poll_id]["chat_id"], context.bot_data[poll_id]["message_id"] ) def quiz(update, context): """Отправка заранее определенную викторину""" # Вопрос викторины и ответы questions = 'Сколько яиц нужно для торта?' answer = ['1', '2', '4', '20'] # посылаем сообщение с викториной, правильный ответ указывается # в `correct_option_id`, представляет собой индекс `answer` message = update.effective_message.reply_poll( questions, answer, type=Poll.QUIZ, correct_option_id=2 ) # Сохраним промежуточные данные викторины в `bot_data` для использования в `receive_quiz_answer` payload = # ключом словаря с данными будет `id` викторины message.poll.id: "chat_id": update.effective_chat.id, "message_id": message.message_id> > context.bot_data.update(payload) def receive_quiz_answer(update, context): """Закрываем викторину после того, как ее прошли три участника""" # бот может получать обновления уже закрытого опроса, которые уже не волнуют if update.poll.is_closed: return if update.poll.total_voter_count == 3: try: quiz_data = context.bot_data[update.poll.id] except KeyError: # Это означает, что это ответ из старой викторины return context.bot.stop_poll(quiz_data["chat_id"], quiz_data["message_id"]) def preview(update, _): """Позволяет создать викторину или опрос пользователям чата""" # При использовании, без указания типа, позволяет пользователю # выбрать то, что он хочет создать - викторину или опрос button = [[KeyboardButton("Нажми меня!", request_poll=KeyboardButtonPollType())]] message = "Нажмите кнопку, для предварительного просмотра вашего опроса" # использование `one_time_keyboard=True` скрывает клавиатуру update.effective_message.reply_text( message, reply_markup=ReplyKeyboardMarkup(button, one_time_keyboard=True) ) def receive_poll(update, _): """ При получении ответа на пользовательский опрос/викторину, отвечаем на него закрытым опросом, копируя полученный опрос """ actual_poll = update.effective_message.poll # Нужно только `question` и `options`, все остальные # параметры не имеют значения для закрытого опроса update.effective_message.reply_poll( question=actual_poll.question, options=[o.text for o in actual_poll.options], # с `is_closed=True` опрос/викторина немедленно закрывается is_closed=True, reply_markup=ReplyKeyboardRemove(), ) def help_handler(update, _): """Отображение справочного сообщения""" update.message.reply_text("Используйте /quiz, /poll или /preview для тестирования этого бота.") if __name__ == '__main__': updater = Updater("TOKEN") dispatcher = updater.dispatcher # определяем соответствующие обработчики dispatcher.add_handler(CommandHandler('start', start)) # команда `/pool` dispatcher.add_handler(CommandHandler('poll', poll)) # обработчик ответа на опрос dispatcher.add_handler(PollAnswerHandler(receive_poll_answer)) # команда `/quiz` dispatcher.add_handler(CommandHandler('quiz', quiz)) # обработчик ответа на викторину dispatcher.add_handler(PollHandler(receive_quiz_answer)) # команда `/preview` dispatcher.add_handler(CommandHandler('preview', preview)) # обработчик создания пользовательского опроса/викторины dispatcher.add_handler(MessageHandler(Filters.poll, receive_poll)) dispatcher.add_handler(CommandHandler('help', help_handler)) # Запуск бота updater.start_polling() updater.idle()
- КРАТКИЙ ОБЗОР МАТЕРИАЛА.
- Переход на асинхронный python-telegram-bot версии 20.x
- Чистый интерфейс Python для Telegram Bot API
- Команды и оповещения @BotFather в Telegram
- Обработка сообщений модулем python-telegram-bot
- Фильтры сообщений модуля python-telegram-bot
- Хранение временных данных модулем python-telegram-bot
- Настройки по умолчанию модуля python-telegram-bot
- Планировщик сообщений модуля python-telegram-bot
- Форматирование и отправка сообщений в python-telegram-bot
- Работа с файлами/media, модуль python-telegram-bot
- Меню из кнопок, модуль python-telegram-bot
- Объект CallbackContext модуля python-telegram-bot
- Подключения Telegram-бота через webhook
- Обработка исключений модуля python-telegram-bot
- Создание Inline-бота, модуль python-telegram-bot
- Работа с опросами в модуле python-telegram-bot
- Создание разговоров ConversationHandler в python-telegram-bot
- Перезапуск телеграмм-бота в случае ошибки
- Декоратор-обработчик сообщений в python-telegram-bot
- Авторизация на сайте через Telegram Passport
- Ведение публикаций в Telegram-канале с python-telegram-bot
- UTF коды emoji/эмодзи для отправки в Telegram из Python
Как добавить опрос в телеграмм боте?
Я написал код, в котором опрос отправляется всем пользователям, но после нажатия на один из ответов опрос не обновляется для всех пользователей. То есть не видно, что другой участник нажал. Как сделать так, чтобы опрос обновлялся у всех пользователей?
@bot.message_handler(commands=['poll']) def poll(message): con = sqlite3.connect('users.db') with con: cur = con.cursor() cur.execute("SELECT id FROM data_base") while True: row = cur.fetchone() if row is None: break bot.send_poll(row[0], 'Question', options=['1', '2', '3'])
Отслеживать
Elvin Gadirov
задан 22 окт 2021 в 19:04
Elvin Gadirov Elvin Gadirov
1 2 2 бронзовых знака
приведите пример кода с которым у вас трудности
18 ноя 2021 в 13:27
добавил код и написал развернутый вопрос.
19 ноя 2021 в 17:24
0
Сортировка: Сброс на вариант по умолчанию
Знаете кого-то, кто может ответить? Поделитесь ссылкой на этот вопрос по почте, через Твиттер или Facebook.
- python
- python-3.x
- telegram
- telebot
- python-telegram-bot
Пишем чат-бот для Telegram на Python, используя webhook и минимум внешних библиотек

Меня зовут Андрей Устьянцев, я ведущий аналитик направления Big Data Лиги Цифровой Экономики, и в этой статье я расскажу, как писал чат‑бот в Telegram на webhook. Если вы знаете, что это такое, и подготовка не вызывает интереса — можете сразу переходить к разделу «Очень кратко». С остальными поделюсь всеми необходимыми шагами.
Почему webhook
Чат‑бот в Telegram может работать в одном из двух режимов.
Один из них называется polling — это когда код, непосредственно реализующий механику бота, опрашивает сервера Telegram с определенной периодичностью («не появилось ли чего новенького»). А если обнаружена активность в чате — реализуется определенная механика взаимодействия (общения).
Большинство материалов в интернете посвящено описанию того, как создавать ботов именно на этой механике, но мне (частное мнение без претензии на истину в последней инстанции) такой подход не очень нравится. Вот почему:
- не вижу смысла постоянно «дергать» сервера Telegram почем зря;
- таймаут, установленный для опроса всех чатов, в которых «общается» бот — это суть задержка в коммуникациях с человеком, который «общается» с ботом.
Второй режим работы ботов, webhook, подразумевает, что Telegram сам вызывает обработчик события/сообщения, когда в боте происходит какая‑то активность. Другими словами, код, реализующий механику бота, срабатывает по инициативе человека, который «общается» с ботом. Самый главный плюс от такого режима работы — ответ бота на действие человека происходит мгновенно: человек написал что‑то боту, Telegram тут же вызвал webhook, написанный код сразу «ответил» человеку.
Лирическое отступление: на тему «что лучше — polling или webhook» спорить можно бесконечно долго. Критерий истины: все зависит от решаемой задачи и от user‑story.
Для задачи, которую решал я, лучше подходит режим webhook: заранее не известно, когда кто‑то напишет боту, но тот должен в любой момент быть готовым поддержать диалог.
При чем тут «минимум» внешних библиотек?
Мое личное убеждение — использовать «чужие» библиотеки по минимуму при написании кода. Особенно в процессе изучения языка программирования. Это отнюдь не означает, что надо впадать в крайности и писать вообще все с нуля — везде возможен разумный компромисс.
Ниже я покажу, что можно написать аккуратный, короткий и читаемый код на Python и без использования специализированных библиотек именно для чат‑ботов (особенно работающих в режиме webhook).
Пошаговая инструкция: как и с чего начать
Расскажу, как я писал код на Python, с какими трудностями столкнулся по пути и как их решал.
Ремарка: код написан в процессе изучения Python, поэтому некоторые моменты могут показаться неоптимальными — буду рад конструктивной критике и предложениям в комментариях к статье.
Итак, что необходимо для работы чат-бота в режиме webhook?
Ссылка на официальную документацию Telegam, где описана работа чат‑ботов в режиме webhook: https://core.telegram.org/bots/api#setwebhook
1. Бот. Я не буду здесь подробно расписывать, как зарегистрировать бота — подробных мануалов достаточно и в интернете, и на этом портале.
2. Сервер для размещения кода, обрабатывающего механику бота — тот самый webhook, которого будет вызывать Telegram при активностях в боте.
Важные моменты (требования Telegram к таким серверам):
- Сервер должен работать круглосуточно. Telegram периодически проверяет служебными запросами наличие и работоспособность webhook.
- Важно, чтобы на сервере был установлен SSL‑сертификат, обеспечивающий (подтверждающий) безопасное соединение с серверами Telegram.
Ну и где брать такой сервер?
Первое — создать свой сервер:
- настроить собственный компьютер под управлением Linux в режиме сервера,
- получить, купить «фирменный» SSL-сертификат или сформировать самостоятельно такой, который Telegram примет как достоверный.
Недостатки такого решения:
- Необходимы дополнительные компетенции по «поднятию» сервера (обычно это еще и на основе Linux) и созданию SSL-сертификата.
Хотя пошаговых инструкций и статей на эту тему достаточно.
- Нужен постоянный IP-адрес подключения к интернету.
Если компьютер подключен к интернету из домашней сети — скорее всего, у вас динамический IP-адрес, который точно не подойдет.
- Надо держать компьютер постоянно включенным в 220В.
Мне в рамках этой задачи круглосуточно гудящий под столом системный блок не нужен.
- Об этом редко пишут, но на поднятый на скорую руку сервер, на который может «постучаться» Telegram, точно будут «ломиться» разные люди, боты, сети в попытке использовать ваш сервер для своих (не всегда хороших) целей.
А это означает, что мало «поднять» сервер, его еще надо и защитить от потенциальных атак из внешних сетей — суровая дополнительная компетенция, скажу я вам.
С профессиональной точки зрения неплохо, конечно, прокачать дополнительные компетенции, но задача состоит в том, чтобы написать на Python код, реализующий механику бота. Причем в процессе изучения самого Python.
Поэтому я выбрал второе решение — облачное.
Идеальный вариант — виртуальный хостинг.
- Круглосуточно работающий сервер Apach, настроенный для запуска приложений на Python.
- Защита от ddos-атак и прочих «пакостей» извне.
- Автоматический выпуск качественного SSL-сертификата.
- Качественный url webhook, соответствующий требованиям Telegram.
- Удобный интерфейс для написания кода на Python прямо из браузера.
Итак, напишем простенького бота, который при любом взаимодействии с ним будет всегда писать в ответ «Ну привет, >»
Очень кратко
- Берем виртуальный хостинг. Подробная инструкция здесь.
- Регистрируем бота.
- Определяем URL-адрес, который сообщим Telegram — что это webhook, соответствующий нашему чат-боту.
- Пишем код на Python.
- «Сообщаем» Telegram адрес webhook нашего бота.
Важно! Рекомендую не менять местами последовательность действий из пунктов 5 и 4. В чем риск? Если Telegram решит проверить работоспособность адреса webhook, а там еще не отлажен код на Python, неправильный ответ Telegram на запрос может привести к блокировке webhook и бота. Снятие блокировки — отдельная неинтересная история.
Разберем код
import time import json import requests def MainProtokol(s,ts = 'Запись'): dt=time.strftime('%d.%m.%Y %H:%M:')+'00' f=open('log.txt','a') f.write(dt+';'+str(ts)+';'+str(s)+'\n') f.close def application(env, start_response): try: content='' token='5937929205:AAHW3_J3oQTSo1fCncpoK7tu6wD6iyH-kSo' if env['PATH_INFO'].lower() == '/tg_bot': # тут вся механика бота именно этот код будет исполняться, когда Telegram будет вызывать webhook wsgi_string=env['wsgi.input'].read() x=wsgi_string.decode('UTF-8') y=x.replace('\n',' ') try: json_string=json.loads(y) except: raise ValueError('Не удалось распарсить в JSON полученную строку') chat_id=json_string['message']['chat']['id'] from_first_name=json_string['message']['from']['first_name'] # имя отправителя # отправка сообщения в чат msg='Ну, привет, '+str(from_first_name) send_message=requests.get('https://api.telegram.org/bot'+token+'/sendMessage?&chat_id='+str(chat_id)+'&text='+str(msg)) if not send_message: raise ValueError('Не удалось отправить текст в бот') elif env['PATH_INFO'] == '/': # случай, когда кто-то просто из браузера зашел на сайт MainProtokol('кто-то просто зашел на сайт') else: # заглушка для обработки ситуации, когда кто-то методом перебора пытается обратиться к какой-то странице сайта raise ValueError('Вызов неизвестного URL :: '+env['PATH_INFO']) start_response('200 OK', [('Content-Type','text/html')]) return [content.encode('utf-8')] except Exception as S: content=str(S) MainProtokol(content,'Ошибка') start_response('200 OK', [('Content-Type','text/html')]) return [content.encode('utf-8')]
Нам понадобятся следующие библиотеки:
- time — для работы с функциями даты и времени;
- JSON — для распарсивания JSON-строки, получаемой от Telegram;
- requests — для отправки сообщений в бот.
Примечание: в зависимости от настроек виртуальных серверов по умолчанию и инсталляций Python перечисленные библиотеки могут быть уже предустановлены. Если нет — необходимо их установить командой pip install.
Строка 5: Функция MainProtokol написана для логирования происходящего в коде. Задача этой функции — записывать в текстовый файл то, что в обычных условиях выводится на экран компьютера. Поскольку код выполняется по сути на сервере в момент вызова webhook Telegram, на экран ничего вывести нельзя, потому что экрана как такового нет. Так что текстовый файл — это замена вывода на экран ошибок (если они возникнут при выполнении кода), а также всего, что было бы полезно залогировать.
Строка 12: Функция application — основная у Python, которую вызывает сервер Apach при любом обращении к сайту. И при вызове webhook тоже.
Все данные, которые поступают при обращении к сайту (не важно, открывают ли его в браузере при прямом заходе по имени сайта, или происходит вызов webhook), сохраняются в переменной env (тип — словарь) — суть переменные окружения.
Строка 14: Переменная content в текущем коде — это «заглушка», потому что на экран ничего выводиться не должно. Можно в ней разместить HTML-код, который будет отображается в случае захода на сайт из браузера.
Строка 15: Переменная token — в ней хранится токен бота, полученный при его регистрации.
Строка 17: Проверяем, что вызывается именно webhook. В env[‘PATH_INFO’] хранится имя страницы, к которой было обращение. Я решил, что у меня страница для коммуникаций с ботами будет называться ‘tg_bot’. Настоятельно рекомендую придумывать страницу (адрес) webhook, а не сообщать Telegram просто имя домена – это защитит дополнительно ваш сайт от внешних атак.
Примечание: приведение значения переменной окружения к нижнему регистру функцией lower() — это, можно сказать, мой «пунктик»: так я еще больше уверен, что сравнение строк в одинаковом регистре произойдет корректно.
Строка 19: Получаем данные, переданные из Telegram («от бота») — они хранятся в переменной окружения «wsgi.input» в формате WSGI. Для его дальнейшей обработки применяем метод read().
Строки 24–27: Распарсиваем JSON-строку, полученную от бота.
И вот тут меня ждал подводный камень размерами с булыжник. Метод load библиотеки JSON выдавал ошибку «нарушение структуры JSON». Потратив значительное время и логируя в текстовый файл с использованием функции MainProtokol все, что только можно, я обнаружил следующее:

В JSON-строке, передаваемой из Telegram, совершенно непонятно зачем добавляется спецсимвол «\n» (перевод строки). Причем только в одном месте.
Решение проблемы — перед распарсиванием JSON-строки принудительно заменить в ней «мешающий» спецсимвол на пробел: replace(‘\n’,’ ‘).
Ремарка: по-хорошему надо было бы написать кусок кода или функцию, которая «убирает» из JSON-строки все известные спецсимволы. Но я СТОЛЬКО времени потратил на поиск проблемы, что сил на ее полномасштабное устранение со всеми возможными вариантами последствий впредь у меня уже не осталось.
Итого мы получили словарь в переменной json_string, содержащий все, что передал Telegram от бота. В общем-то, блок строк 17-30 — это то место, где необходимо реализовывать логику «общения» бота. Суть — анализировать полученный JSON и в зависимости от полученных данных программировать поведение бота.
Как разбираться с полученным JSON? По сути это объект message, полное описание которого можно найти в официальной документации вместе с описанием всех вложенных объектов типа chat, user и т. д.
Я пошел другим путем: писал в лог получаемые JSON по разным сценариям взаимодействия с ботом (отправка команды боту, отправка текста боту, отправка картинки), потом смотрел их структуру и реализовывал нужную мне логику.
Ремарка: Telegram передает в webhook только те поля, которые имеют значение. Покажу на примере.
(Исходный код – тут log1 с примерами JSON.txt)
Отправка команды «/start»
Отправка простого текста