Боты для Telegram

В настоящее время существует достаточно много разных популярных мессенджеров. Достоинством мессенджера Telegram является наличие богатого API, позволяющего взаимодействовать с мессенжером не людям, а программам, то есть писать боты.

Можете изучить полную документацию на Telegram Bot API, но непосредственно этим API мы пользоваться не будем. Но нужно понять, как это устроено внутри.

На самом простом уровне это API, использующее HTTP с JSON-ответом. Вы можете промоделировать работу бота, просто используя GET-запросы в браузере, то есть загружая страницы с определённым адресом используя браузер.

Самый простой пример тестовой страницы, которую можно загрузить при помощи API:

https://api.telegram.org/bot123456:ABC-DEF1234ghIkl-zyx57W2v1u123ew11/getMe

Вы можете скопировать этот адрес и вставить его в адресную строку, нажать Enter и получить JSON-документ с ответом. Ответ будет печальным: Unathorized. Это связано с тем, что для доступа к АPI нужен специальный ключ (токен), который мы заменили на последовательность цифр 123456:ABC-DEF1234ghIkl-zyx57W2v1u123ew11. Токен идентифицирует вашего бота и его необходимо “предъявлять” при каждом обращении вашего бота к API. Мы же использовали просто какую-то последовательность символов для примера. Поэтому прежде чем запустить бота, необходимо этот токен получить, для чего используется специальный бот, называемый @BotFather. Откройте диалог с этим ботом в телеграме, наберите команду /start и ответьте на два вопроса:

  1. Название бота (произвольное текстовое имя),
  2. Имя пользователя для бота (уникальное, из латинских букв, цифр, символа подчёркивания, должно заканчиваться на bot).

В ответ вы получите от @BotFather токен.

Дальнейшее описание Telegram Bot API не нужно для выполнения заданий (т.к. мы будем использовать упрощающую разработку ботов библиотеку), но полезно для понимания.

Пример простого запроса getMe для проверки токена, который можно просто ввести в адресную строку браузера, заменив <token> на токен вашего бота:

https://api.telegram.org/bot<token>/getMe

В ответ вы должны получить небольшой JSON с информацией о вашем боте.

Если написать боту сообщение (например, с телефона или используя web.telegram.org), то это сообщение будет храниться на серверах Telegram. Чтобы получить сообщения, адресованные боту, необходимо выполнить запрос getUpdates:

https://api.telegram.org/bot<token>/getUpdates

Вы получите JSON, содержащий все сообщения, написанные боту, за последние 24 часа. Повторно вызвав этот запрос, мы снова получим все сообщения с сервера. Чтобы не получать сообщения повторно, можно передать запросу параметр offset, например

https://api.telegram.org/bot<token>/getUpdates?offset=<update_id>

где <update_id> — минимальное значение параметра update_id, начиная с которого вы хотите получить сообщения. Оно должно быть на 1 больше значения параметра update_id последнего сообщения, которое вы получили. Все сообщения с меньшим значением update_id вам больше не будут отдаваться.

Для того, чтобы отправить сообщение, вам необходимо сделать запрос sendMessage с двумя обязательными параметрами в адресной строке: chat_id —идентификатор чата для отправки сообщения и text — сообщение, отправляемое пользователю. Например:

https://api.telegram.org/bot<token>/sendMessage?chat_id=123456789&text=Hello,%20world!

Таким образом, простейшая схема реализации бота следующая. Бот — это постоянно запущенное приложение, которое регулярно опрашивает сервер, посылая запросы getUpdates и “засыпая” на некоторое время после этого. Если ответ сервера содержит какие-то новые сообщения, то их нужно обработать и отправить запросы sendMessage для отправки ответных сообщений.

Библиотека pytelegrambotapi (telebot)

Как мы видим, реализация бота содержит много стандартной работы, типа запуск цикла обработки сообщений с учётом значения offset, разбор полученного JSON, ответ на все сообщения, отправка запросов, для ответа на сообщения. Для этого есть ряд библиотек, содержащих реализацию этой рутины, одна из наиболее популярных из таких библиотек — telebot.

Её необходимо установить при помощи pip, в pip она называется pytelegrambotapi. Соответствующая консольная команда может выглядеть, например, так:

pip install --user pytelegrambotapi

Сразу же установите библиотеку requests, она пригодится для выполнения ряда заданий.

pip install --user requests

Простой обработчик сообщений

Пример простого бота, который на любое сообщение всегда отвечает одним словом “Привет!”:

import telebot

TOKEN = '123456:ABC-DEF1234ghIkl-zyx57W2v1u123ew11'

bot = telebot.TeleBot(TOKEN)

@bot.message_handler(content_types=['text'])
def get_text_messages(message):
    bot.send_message(message.from_user.id, 'Привет!')

bot.infinity_polling()

В этом примере создаётся объект с именем bot класса telebot.TeleBot, которому в конструктор передаётся значение токена в качестве параметра. Затем объявляется функция get_text_message, принимающая в качестве параметра объект message, в котором будет хранится информация о полученном сообщении.

Эта функция регистрируется в качестве обработчика для сообщений типа “text”, что делается при помощи так называемых “декораторов” — это строчка, начинающаяся со значка “@” перед нашей функцией.

Декоратор: это специальная функция (или метод), которая получает другую функцию в качестве параметра, и создаёт новую функцию, используя переданную ей функцию. Строка, начинающаяся с “@” — это на самом деле вызов метода bot.message_handler, который зарегистрирует функцию get_text_messages, как обработчик для сообщений указанного типа. Подробней про декораторы можно прочитать, например, здесь.

Теперь при поступлении сообщений объект bot будет вызывать функцию get_text_messages для каждого сообщения отдельно, тем самым принимая на себя работу по получению и парсингу полученных сообщений. Вам нужно всего лишь определить, что делает бот для ответа на полученное сообщение. Вы можете отправлять сообщения, вызывая метод send_message с параметрами: идентификатор чата и текст отправляемого сообщения.

Наконец, запускается метод polling, который представляет собой бесконечный (none_stop=True) цикл запроса обновлений с сервера, который и будет вызывать функцию get_text_messages при получении новых сообщений.

Немного более подробно ознакомиться с написанием простейших ботов можно, например, в следующих статьях:

Как сдавать задачи в этом листке

В этих заданиях необходимо разработать бота для Telegram с использованием модуля telebot.

Вам необходимо:

  1. Зарегистрировать своего бота в @BotFather, получить ключ для доступа к API.
  2. Написать программу, которая будет реализовывать ваш бот.
  3. В начале программы напишите в комментарии имя пользователя для вашего бота (которое было указано при получении ключа), например # @my_super_bot
  4. Отладьте вашу программу, убедитесь, что она работает.
  5. Запустите вашего бота локально и сдайте программу в тестирующую систему
  6. Не останавливайте вашего бота до окончания проверки. На каждый запрос ваш бот должен успевать отвечать за 2 секунды.

A: Бот, отвечающий односложно

Напишите бота, который на все предложения отвечает односложно. Например, пишет в ответ “Привет!”

Отладочные сообщения

Можно сделать так, чтобы telebot выводил кучу отладочных сообщений: всё сообщения, которые приходят и уходят. Для этого нужно поменять уровень логирования, вот так:

import logging
logging.getLogger('TeleBot').setLevel(logging.DEBUG)

B: Приветствие по имени

Напишите бота, который в ответ на сообщение приветствует пользователя по имени. Например, “Привет, Андрей!”. Для этого изучите объект message, который передаётся в обработчик сообщений. Этот объект содержит в себе много информации. Чтобы разобраться, что есть в этом объекте, можно воспользоваться пошаговым отладчиком с просмотром переменных или отладочным выводом.

C: Бот-калькулятор

Напишите бота, который вычисляет значение выражения, которое ему прислали. Например, если написать боту 2 + 2, он должен ответить 4.

Для этого нужно использовать функцию eval, вычиcляющую переданное ей выражение в виде строки. Например, eval('2 + 2') вернёт число 4.

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

try:
    # вызов функции eval
    # и какие-то другие действия
except:
    # что будет происходить, если возникло исключение

Подробней про обработку исключений в Python можно прочитать, например, здесь.

На самом деле ни в коем случае нельзя использовать функцию eval для вычисления выражений, полученных ненадёжным способом. Например, злоумышленник может отправить вашему боту зловредный код и сделать что-то нехорошее. Например, можете посмотреть эту статью или даже вот эту.

Модуль requests для запросов из интернета

Чтобы получить данные с удалённого сайта есть несколько способов, самый простой — библиотека requests. Смотрите документацию на библиотеку, простой пример использования, а также можно сразу же распарсить полученный JSON.

D: Бот-цитатник

Пора научить бота делать что-то интересное. У нас есть интернет с огромным объёмом информации. Ну и помимо сложного способа (самим добывать информацию, парсить её и т.д.), есть некоторые ресурсы, обладающие удобным API. Зачастую это API построено точно так же: отправляете GET-запрос через http, в ответ получаете JSON с нужной информацией.

Например, есть сайт favqs.com, выдающий цитаты. У этого сайта есть API, описанное на странице https://favqs.com/api/. Для большинства функций этого API требуется авторизация при помощи токена, но мы воспользуемся простым запросом Quote of the Day, не требующим токена. Для этого нужно просто сделать запрос по адресу https://favqs.com/api/qotd, и вы получите JSON с цитатой.

Задание. Напишите бота, который на любое сообщение отвечает случайно цитатой. Не забудьте также указать автора цитаты!

E: Конвертер валют

У Центрального банка РФ есть API, выдающее официальные курсы валют: https://www.cbr-xml-daily.ru/daily_json.js

Напишите бота, который мог бы переводить любую сумму любой поддерживаемой валюты в любую другую поддерживаемую валюту. Не забудьте про российский рубль, его курс в этом документе не указан!

Бот должен уметь распознавать запросы в каком-либо формате, например, "100 USD в EUR". Если бот не смог распознать запрос, то должно выводиться сообщение об ошибке, которое бывает двух видов: сообщение о том, как использовать бота и сообщение о том, что указанная валюта ботом не поддерживается (в этом случае бот должен вывести список всех поддерживаемых валют).

F: Цитаты на заданную тему

Вернёмся к API цитатника https://favqs.com/api/.

Необходимо написать бота, который выдаёт случайную цитату на заданную тему. Например, если боту отправить какое-либо довольное распространённое слово, например, “school”, “weather”, “love” и т.д., то API вернёт список цитат на эту тему.

Техническое задание: напишите бота, который по заданному слову выдаёт случайную цитату на данную тему. Необходимо вывести одну (случайную) цитату из всех цитат в базе, у которых проставлен тэг, соответствующий этому слову. Если такие цитаты не найдены, бот должен сообщить об этом. Для выбора случайной цитаты лучше всего подойдёт функция random.choice

Для этого у API есть метод quotes (List Quotes), который выдаёт список всех цитат. Этому методу можно передать параметр filter, со значением — слово, по которому осуществляется поиск, и параметр type=tag, чтобы осуществлять поиск по тегам, а не по всем текстам цитат.

Обратите внимание, что если цитат, удовлетворяющих условию, много, то API выдаёт только 25 из них. Чтобы получить больше цитат (они выдаются страницами по 25 штук), нужно задать параметр page, равный номеру страницы.

Всё, что здесь описано, передаётся при помощи параметров GET запроса. Примеры запросов можете посмотреть в документации на API.

Но использовать функцию quotes вы можете только при наличии токена. Для этого нужно зарегистрироваться на сайте и получить бесплатно токен (они называются API keys). Токен — это строка из 32 hex-символов. Токен нужно передавать (см. раздел Authorization в API) при помощи дополнительного параметра “Authorization” в параметрах GET-запроса. Это можно сделать при помощи той же библиотеки requests, посмотрите на пример Custom Headers в документации.

Наконец, вы должны реализовать кэширование запросов. То есть если бот один раз получил список всех цитат на какую-то тему, то можно ожидать, что пользователь захочет получить ещё одну цитату на эту же тему. Чтобы не получать снова цитаты через API, необходимо сохранять все цитаты по этой теме, а при повторном запросе — выдавать одну цитату из сохранённых.

Рекомендуемый план работы над заданием такой. Переходите к следующему этапу, реализовав предыдущий этап.

  1. Получить API key.
  2. Научиться делать запросы с использованием API key (любые, лишь бы работала авторизация).
  3. Научиться делать запросы с поиском по тэгу.
  4. Реализовать загрузку всех цитат с учётом пагинации. До сих пор вам на самом деле не нужен никакой бот, удобней написать простое приложение, которое будет делать необходимые запросы и выдавать результат на стандартный вывод.
  5. Теперь облачаем это в форму бота, кэшируя полученные цитаты в словаре.

Реализация клавиатуры

Команды, набираемые пользователем, сложно декодировать, да и пользователь может ошибиться в их написании. Поэтому в Телеграме у ботов есть возможность использования клавиатуры для выбора стандартных действий пользователя. Есть два типа клавиатуры: ReplyKeyboardMarkup (более древняя) и InlineKeyboardMarkup (более современная и богатая возможностями), на второй и остановимся.

В клавиатуру можно добавлять кнопки, у кнопки есть два параметра: text — это надпись, отображаемая на кнопке, и callback_data — это информация, которая будет передана в обработчик нажатия на кнопку. Сначала создадим все кнопки, потом добавим их в объект-клавиатуру. Метод row у клавиатуры добавляет к клавиатуре ряд из нескольких кнопок, если вызвать метод row ещё раз, то будет добавлен следущий ряд кнопок, и т.д. После этого при отправке сообщения укажем параметр reply_markup, указав созданную клавиатуру.

@bot.message_handler(content_types=['text'])
def get_text_messages(message):
    keyboard = telebot.types.InlineKeyboardMarkup()
    button1 = telebot.types.InlineKeyboardButton(text="Кнопка1", callback_data="button1")
    button2 = telebot.types.InlineKeyboardButton(text="Кнопка2", callback_data="button2")
    keyboard.row(button1, button2)
    bot.send_message(message.from_user.id, "Привет!",  reply_markup=keyboard)

Теперь нам необходимо сделать обработчики кнопок. Это отдельные функции, которые необходимо зарегистрировать при помощи декоратора callback_query_handler. Эта функция будет получать объект callback_obj, которых хранит информацию о нажатии кнопки. В частности, помимо обычных данных о пользователе, чате и т.д. у этого объекта есть поле data, в котором хранится та самая информация, которая была привязана к кнопке. Пример такого обработчика:

@bot.callback_query_handler(func=lambda call: True)
def callback_function1(callback_obj):
    if callback_obj.data == "button1":
        bot.send_message(callback_obj.from_user.id, "Вы нажали на кнопку 1")
    else:
        bot.send_message(callback_obj.from_user.id, "Вы нажали на кнопку 2")
    bot.answer_callback_query(callback_query_id=callback_obj.id)

Обратите внимание на вызов метода answer_callback_query. Он сообщает серверу телеграма, что обработка данного callback-запроса завершена. Если этот метод не вызвать, то на кнопке будут изображены часики, а сама кнопка будет недоступна для повторного нажатия.

Есть и другой способ определить, какая кнопка была нажата. Обратите внимание на параметр декоратора: func=lambda call: True. Это лямбда-функция, которая в данном случае всегда возвращает True. Это означает, что данный обработчик будет применяться ко всем callback-запросам. Но если эта функция будет возвращать не всегда True, то так можно установить обработчик для отдельных callback-запросов, а именно, будет вызван тот обработчик, для которого эта функция вернёт True.

Используем это для того, чтобы сделать разные обработчики для разных кнопок.

@bot.callback_query_handler(func=lambda call: call.data == "button1")
def callback_function1(callback_obj):
    bot.send_message(callback_obj.from_user.id, "Вы нажали на кнопку 1")
    bot.answer_callback_query(callback_query_id=callback_obj.id)

@bot.callback_query_handler(func=lambda call: call.data == "button2")
def callback_function2(callback_obj):
    bot.send_message(callback_obj.from_user.id, "Вы нажали на кнопку 2")
    bot.answer_callback_query(callback_query_id=callback_obj.id)

G: Расписание уроков

Напишите бота, который выдаёт расписание уроков на сегодняшний день (после запуска, ответ на любое сообщение), а также на любой другой день недели.

Выбор дня недели реализуйте при помощи клавиатуры.

Вот начало вашего расписание

[
    ["Информатика", "Информатика", "Обществознание", "Литература", "Матан", "Матан", ],
    ["Английский", "Английский", "Обществознание", "", "", "", ],
    [...],
    ...
]

H: Текущая погода

Зачастую хорошие данные бесплатно не раздают... Но на сайте openweathermap.org есть бесплатное API, предоставляющее погодные данные. Вернее, там есть бесплатные запросы “Current Weather Data”, “One Call API”, “5 Day / 3 Hour Forecast”.

Зарегистрируйтесь на сайте и получите ключ для доступа к API (в меню найдите “My API keys”). Сделайте это заранее, т.к. активация ключа произойдёт через некоторое время. Если при доступе к API вы получаете сообщение о том, что ключ некорректный (а он точно корректный), то это означает, что ключ ещё не был активирован и нужно ещё подождать.

Напишите бота, который выдаёт текущую погоду для одного из городов. Приложение должно предлагать выбор города при помощи кнопки (должно быть минимум 4 города, минимум из трёх разных часовых поясов, обязательно включая Москву). Также должна быть возможность ввести название города вручную (при этом не гарантируется, что погоду для этого города удастся определить).

Необходимо вывести текущую погоду для выбранного города. Минимальный набор данных, который нужно вывести:

I: Конвертер валют-2

Бота, конвертирующего валюты, реализуйте при помощи кнопок. Например, должны быть кнопки “Введите сумму”, “Выберите исходную валюту” “, “Выберите валюту, в которую нужно перевести”. Для выбора валюты отображается меню из кнопок, соответствующих валютам. При вводе суммы нужно обрабатывать любые неправильные введённые данные.

J: Бот, который считает

Напишите бота с двумя кнопками: «+1» и «Сброс». По нажатию на первую кнопку выводится последнее выведенное число, увеличенное на 1, а по нажатию на «Сброс» выводится 0.

Для каждого пользователя ряд чисел должен быть собственным.

K: Камень, ножницы, бумага

Напишите бота, который реализует игру «Камень, ножницы, бумага» для двух игроков (двух пользователей). Пользователю отображаются три кнопки. Бот получает результат нажатия кнопок. Когда два пользователя нажали кнопки, бот сообщает каждому из них имя его противника, что выбрал противник и результат игры (выиграл, проиграл, ничья). То есть при получении ответа от одного пользователя бот запоминает его ход, после получения ответа от второго пользователя, бот сообщает обоим пользователям результат игры и всё начинается заново.

Если второй ответ пришёл от того же игрока, то это считается изменением его предыдущего ответа.

L: Игра ним

В игре ним на столе лежит несколько кучек камней (например, содержащих 3, 4 и 5 камней). Двое игроков по очереди берут по одному или несколько камней из одной кучки. Выигрывает тот, кто взял последний камень.

Стратегия при игре в ним: рассмотрим побитовый XOR количества камней в кучках (например, 3 ^ 4 ^ 5 = 2). Если эта величина ненулевая, то позиция выигрышная для ходящего игрока. Если же эта величина нулевая, то позиция проигрышная. Выигрышный ход должен быть таким, чтобы XOR размеров кучек стал нулевым.

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

Библиотека sympy для символьных вычислений

Есть отличная библиотека sympy для символьных вычислений. Вот так в ней можно считать производные:

import sympy
expr = '2*x*sin(2*x) - x**3 + e**x + x**(1/2)'
parsed = sympy.parsing.sympy_parser.parse_expr(expr)
dfdx = sympy.diff(parsed, 'x')
print(dfdx)

# e**x*log(e) - 3*x**2 + 4*x*cos(2*x) + 2*sin(2*x) + 1/(2*sqrt(x))

Если добавить кода, то можно сделать ввод и вывод более удобными:

import sympy
from sympy.parsing.sympy_parser import *
transformations = standard_transformations + (implicit_multiplication_application, function_exponentiation, convert_xor, factorial_notation)
expr = '2x sin 2x - x^3 + e^x + x^0.5'
parsed = parse_expr(expr, transformations=transformations)
dfdx = sympy.diff(parsed, 'x')
print(sympy.printing.pretty(dfdx))

#  x               -0.5      2
# e ⋅log(e) + 0.5⋅x     - 3⋅x  + 4⋅x⋅cos(2⋅x) + 2⋅sin(2⋅x)

M: Бот, считающий производную

Сделайте бота, который будет считать производную.

N: Бот, считающий интеграл

По аналогии с производной можно и интегралы считать. Подробности письмом.