Прежде всего, бот для 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. (для GAE удобно использовать связку Python2+Flask)
Каркас бота:
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: выберите бота, чтобы изменить список команд.
I: @******_bot
BotFather: OK. Отправьте мне список команд для вашего бота. Пожалуйста, используйте этот формат:
command1 - Description
command2 - Another description
I:
whoisyourdaddy - Информация об авторе
base64 - Base64 decode
BotFather: Успех! Список команд обновлен./help
C таким описанием, если пользователь наберет /, Telegram услужливо покажет список всех доступных команд.
4. Свобода
Как можно было заметить, Telegram присылает сообщение целиком, а не разбитое, и ограничение на то, что команды начинаются со слеша — только для удобства мобильных пользователей. Благодаря этому можно научить бота немного говорить по-человечески.
UPD: Как верно подсказали, такое пройдет только при личном общении. В чатах боту доставляются только сообщения, начинающиеся с команды (/<command>) (https://core.telegram.org/bots#privacy-mode)
Все сообщения, начинающиеся с косой черты ‘/’ (см. Команды выше)
Сообщения с упоминанием бота по имени пользователя
Ответы на собственные сообщения бота
Служебные сообщения (люди, добавленные или удаленные из группы и т. Д.)
Чтобы бот получал все сообщения в группах пишем @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 защищены их собственным сертификатом.
UPD: Telegram улучшили Бот Апи, из-за чего, теперь не обязательно иметь отдельную функцию для отправки сообщений при установленном вебхуке, а в ответ на POST запрос можно отвечать тем же сформированным JSON с ответным сообщением, где одно из полей устанавливается как ч 'method': 'sendMessage' (или любой другой метод, используемый ботом).