Прежде всего, бот для Telegram — это по-прежнему приложение, запущенное на вашей стороне и осуществляющее запросы к Telegram Bot API. Причем API довольное простое — бот обращается на определенный URL с параметрами, а Telegram отвечает JSON объектом.
Рассмотрим API на примере создания тривиального бота:
1. Регистрация
Прежде чем начинать разработку, бота необходимо зарегистрировать и получить его уникальный id, являющийся одновременно и токеном. Для этого в Telegram существует специальный бот — @BotFather.
Пишем ему /start и получаем список всех его команд.
Первая и главная — /newbot — отправляем ему и бот просит придумать имя нашему новому боту. Единственное ограничение на имя — в конце оно должно оканчиваться на «bot». В случае успеха BotFather возвращает токен бота и ссылку для быстрого добавления бота в контакты, иначе придется поломать голову над именем.
Для начала работы этого уже достаточно. Особо педантичные могут уже здесь присвоить боту аватар, описание и приветственное сообщение.
Не забудьте проверить полученный токен с помощью ссылки api.telegram.org/bot<TOKEN>/getMe, говорят, не всегда работает с первого раза.
2. Программирование
Создавать бота буду на Python3, однако благодаря адекватности этого языка алгоритмы легко переносятся на любой другой.
Telegram позволяет не делать выгрузку сообщений вручную, а поставить webHook, и тогда они сами будут присылать каждое сообщение. Для Python, чтобы не заморачиваться с cgi и потоками, удобно использовать какой-нибудь реактор, поэтому я для реализации выбрал tornado.web.
Каркас бота:
URL = "https://api.telegram.org/bot%s/" % BOT_TOKEN
MyURL = "https://example.com/hook"
api = requests.Session()
application = tornado.web.Application([
(r"/", Handler),
])
if __name__ == '__main__':
signal.signal(signal.SIGTERM, signal_term_handler)
try:
set_hook = api.get(URL + "setWebhook?url=%s" % MyURL)
if set_hook.status_code != 200:
logging.error("Can't set hook: %s. Quit." % set_hook.text)
exit(1)
application.listen(8888)
tornado.ioloop.IOLoop.current().start()
except KeyboardInterrupt:
signal_term_handler(signal.SIGTERM, None)
Здесь мы при запуске бота устанавливаем вебхук на наш адрес и отлавливаем сигнал выхода, чтобы вернуть поведение с ручной выгрузкой событий.
Приложение торнадо для обработки запросов принимает класс tornado.web.RequestHandler, в котором и будет логика бота.
class Handler(tornado.web.RequestHandler):
def post(self):
try:
logging.debug("Got request: %s" % self.request.body)
update = tornado.escape.json_decode(self.request.body)
message = update['message']
text = message.get('text')
if text:
logging.info("MESSAGE\t%s\t%s" % (message['chat']['id'], text))
if text[0] == '/':
command, *arguments = text.split(" ", 1)
response = CMD.get(command, not_found)(arguments, message)
logging.info("REPLY\t%s\t%s" % (message['chat']['id'], response))
send_reply(response)
except Exception as e:
logging.warning(str(e))
Здесь CMD — словарь доступных команд, а send_reply — функция отправки ответа, которая на вход принимает уже сформированный объект Message.
Собственно, её код довольно прост:
def send_reply(response):
if 'text' in response:
api.post(URL + "sendMessage", data=response)
Теперь, когда вся логика бота описана можно начать придумывать ему команды.
3. Команды
Перво-наперво, необходимо соблюсти соглашение Telegram и научить бота двум командам: /start и /help:
def help_message(arguments, message):
response = {'chat_id': message['chat']['id']}
result = ["Hey, %s!" % message["from"].get("first_name"),
"\rI can accept only these commands:"]
for command in CMD:
result.append(command)
response['text'] = "\n\t".join(result)
return response
Структура message['from'] — это объект типа User, она предоставляет боту информацию как id пользователя, так и его имя. Для ответов же полезнее использовать message['chat']['id'] — в случае личного общения там будет User, а в случае чата — id чата. В противном случае можно получить ситуацию, когда пользователь пишет в чат, а бот отвечает в личку.
Команда /start без параметров предназначена для вывода информации о боте, а с параметрами — для идентификации. Полезно её использовать для действий, требующих авторизации.
После этого можно добавить какую-нибудь свою команду, например, /base64:
def base64_decode(arguments, message):
response = {'chat_id': message['chat']['id']}
try:
response['text'] = b64decode(" ".join(arguments).encode("utf8"))
except:
response['text'] = "Can't decode it"
finally:
return response
Для пользователей мобильного Telegram, будет полезно сказать @BotFather, какие команды принимает наш бот:
I: /setcommands
BotFather : Choose a bot to change the list of commands.
I: @******_bot
BotFather: OK. Send me a list of commands for your bot. Please use this format:
command1 - Description
command2 - Another description
I:
whoisyourdaddy - Information about author
base64 - Base64 decode
BotFather: Success! Command list updated. /help
C таким описанием, если пользователь наберет /, Telegram услужливо покажет список всех доступных команд.
4. Свобода
Как можно было заметить, Telegram присылает сообщение целиком, а не разбитое, и ограничение на то, что команды начинаются со слеша — только для удобства мобильных пользователей. Благодаря этому можно научить бота немного говорить по-человечески.
UPD: Как верно подсказали, такое пройдет только при личном общении. В чатах боту доставляются только сообщения, начинающиеся с команды (/<command>) (https://core.telegram.org/bots#privacy-mode)
- All messages that start with a slash ‘/’ (see Commands above)
- Messages that mention the bot by username
- Replies to the bot's own messages
- Service messages (people added or removed from the group, etc.)
Чтобы бот получал все сообщения в группах пишем @BotFather команду /setprivacy и выключаем приватность.
Для начала в Handler добавляем обработчик:
if text[0] == '/':
...
else:
response = CMD["<speech>"](message)
logging.info("REPLY\t%s\t%s" % (message['chat']['id'], response))
send_reply(response)
А потом в список команд добавляем псевдо-речь:
RESPONSES = {
"Hello": ["Hi there!", "Hi!", "Welcome!", "Hello, {name}!"],
"Hi there": ["Hello!", "Hello, {name}!", "Hi!", "Welcome!"],
"Hi!": ["Hi there!", "Hello, {name}!", "Welcome!", "Hello!"],
"Welcome": ["Hi there!", "Hi!", "Hello!", "Hello, {name}!",],
}
def human_response(message):
leven = fuzzywuzzy.process.extract(message.get("text", ""), RESPONSES.keys(), limit=1)[0]
response = {'chat_id': message['chat']['id']}
if leven[1] < 75:
response['text'] = "I can not understand you"
else:
response['text'] = random.choice(RESPONSES.get(leven[0])).format_map(
{'name': message["from"].get("first_name", "")}
)
return response
Здесь эмпирическая константа 75 относительно неплохо отражает вероятность того, что пользователь всё-таки хотел сказать. А format_map — удобна для одинакового описания строк как требующих подстановки, так и без нее. Теперь бот будет отвечать на приветствия и иногда даже обращаться по имени.
5. Не текст.
Боты, как и любой нормальный пользователь Telegram, могут не только писать сообщения, но и делиться картинками, музыкой, стикерами.
Для примера расширим словарь RESPONSES:
RESPONSES["What time is it?"] = ["<at_sticker>", "{date} UTC"]
И будем отлавливать текст <at_sticker>:
if response['text'] == "<at_sticker>":
response['sticker'] = "BQADAgADeAcAAlOx9wOjY2jpAAHq9DUC"
del response['text']
Видно, что теперь структура Message уже не содержит текст, поэтому необходимо модифицировать send_reply:
def send_reply(response):
if 'sticker' in response:
api.post(URL + "sendSticker", data=response)
elif 'text' in response:
api.post(URL + "sendMessage", data=response)
И все, теперь бот будет время от времени присылать стикер вместо времени:
6. Возможности
Благодаря удобству API и быстрому старту боты Telegram могут стать хорошей платформой для автоматизации своих действий, настройки уведомлений, создания викторин и task-based соревнований (CTF, DozoR и прочие).
Вспоминая статью про умный дом, могу сказать, что теперь извращений меньше, а работа прозрачнее.
7. Ограничения
К сожалению, на данный момент существует ограничение на использование webHook — он работает только по https и только с валидным сертификатом, что, например для меня пока критично за счет отсутствия поддержки сертифицирующими центрами динамических днс.
К счастью, Telegram также умеет работать и по ручному обновлению, поэтому не меняя кода можно создать еще одну службу Puller, которая будет выкачивать их и слать на локальный адрес:
while True:
r = requests.get(URL + "?offset=%s" % (last + 1))
if r.status_code == 200:
for message in r.json()["result"]:
last = int(message["update_id"])
requests.post("http://localhost:8888/",
data=json.dumps(message),
headers={'Content-type': 'application/json',
'Accept': 'text/plain'}
)
else:
logging.warning("FAIL " + r.text)
time.sleep(3)
P.S. По пункту 7 нашел удобное решение — размещение бота не у себя, а на heroku, благо все имена вида *.herokuapp.com защищены их собственным сертификатом.
Комментарии (16)
Ogoun
10.07.2015 12:19Статья не про то как писать ботов, а как написать примитивный текстовый бот. Нет указаний по целому ряду вещей для полноценного бота, например что бот пока не сможет забрать фото от пользователя, что аудио принимается только в одном формате. Как правильно заполнить словарь чтобы отправить фото, аудио, видео и документы от бота. Как создать кастомную клавиатуру.
Ingener74
10.07.2015 13:04-1Тут есть пример отправления изображения, аудио, видео, и стикеры посути отправляются также, осторожно там есть мат))
github.com/Ingener74/Telegram-Bot/blob/master/TelegramBox.py
kapuletti
10.07.2015 17:05+1Если тут бывают разработчики Телеграма, сообщение для них:
Собственно хотелось бы увидеть в сл. обновлении BOT API, так это получение статуса пользователя, точнее, онлайн ли он, если нет, когда он был в сети последний раз.
dolphin4ik
10.07.2015 18:44+2Ждём первую текстовую РПГ. Помню в ICQ ботов было море, на любой вкус: анекдоты, новости, погода, анонимные чаты… эх Пашка «вернул 2007»
fingoldo
11.07.2015 17:34-2Допустим, я хочу слать сообщения одному из своих друзей из списка контактов в Телеграме.
Бота я зарегистрировал. Как теперь выцепить параметр chat_id друга для метода sendMessage?
Хочу отметить, что у Telegram документация ужасного качества, из неё совершенно не понятно, как что-то полезное сделать.zelenin
11.07.2015 21:23+2абсолютно ясная документация.
chat_id выцепить из объекта Update, полученного с помощью getUpdates(), после того, как ваш друг напишет вашему боту.
hormold
13.07.2015 02:05Боты могут писать только тем, кто первым им написал. Что бы не спамили с ботов
SonkoDmitry
14.07.2015 13:31+1Да еще есть такой нюанс, бот не может писать боту. У нас была идея, реализовать на ботах техсаппорт. Человек пишет главному боту, тот пересылает сообщение промежуточному, промежуточный пишет саппорта. Наоборот аналогично. Оказалось что нельзя, получаем ошибку «Error: Bad Request: user not found». Как ответила ТП телеграма на это:
Using the current bot API, it is not possible.
The bot API is still in its infancy right now. There are many potential features to consider and implement. We'll be studying what people do with their bots for a while to see which directions will be most important for the platform.
afanasyevsanya
03.08.2015 23:07Ты говоришь, что тебе удалось использовать heroku для хостинга бота + ты используешь webHook. Когда я попробовал хостить бота на heroku, приложение падает через 60 секунд с ошибкой: Error R10 (Boot timeout) -> Web process failed to bind to $PORT within 60 seconds of launch. Погуглив, я получил ответ, что heroku не может слушать определенный порт и надо использовать переменную process.env.PORT в нее heroku поставит тот порт, который посчитает нужным. Соответсвенно порт мне присваевается не правильный и telegram выдает соответствующую ошибку.
Скажи сталкивался ли ты с этим? Как решал?M_Muzafarov Автор
04.08.2015 10:26Ну собственно, как Хероку пишет — так и решал:
from os import environ def main(): ... application.listen(environ["PORT"])
Проблем не возникало.
Правда мне бесплатный хероку надоел своими фризами и я впоследствии весь код переписал на Flask для GAE. До 10 ботов нахаляву можно спокойно запустить и они не спят.
TyVik
На днях тоже писал своего бота, но только на flask. Столкнулся с проблемой, что запросы ко мне просто не приходят, ошибки было 2: попытка сделать бота на поддомене, в то время как сертификат был только на домен, и нужно указывать все промежуточные сертификаты. Нашёл ещё интересную библиотечку для работы с ботом — github.com/datamachine/twx.botapi (кто-нибудь ей пользовался?). Но вот так и не понял как же удобней этого бота разрабатывать локально, на заглушках?
M_Muzafarov Автор
Разрабатывать локально — я просто генерировал json, который должен приходить в хук и слал его локально себе curl'om. Вполне рабочий вариант.