Інструкція: Як створювати ботів в Telegram

24 червня розробники Telegram відкрили платформу для створення ботів. Новина когось обійшла стороною Хабр, проте багато вже почали розробляти вікторини. При цьому мало де вказані хоч якісь приклади працюючих ботів.

Насамперед, бот для 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(сигнал.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['from']['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['from']['id'], response))
send_reply(response)
except as Exception 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['from']['id']}
result = ["Hey, %s!" % message["з"].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 користувача, так і його ім'я.

Команда /start без параметрів призначена для виведення інформації про боте, а з параметрами — для ідентифікації. Корисно використовувати для дій, які вимагають авторизації.

Після цього можна додати якусь свою команду, наприклад, /base64:

def base64_decode(arguments, message):
response = {'chat_id': message['from']['id']}
try:
response['text'] = b64decode(" ".join(arguments).encode("utf8"))
except:
response['text'] = "can't decode it"
finally:
return response


Для користувачів мобільного Telegra, буде корисно сказати @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 надсилає повідомлення цілком, а не розбите, і обмеження на те, що команди починаються з слеша — тільки для зручності мобільних користувачів. Завдяки цьому можна навчити бота трохи говорити по-людськи.

Для початку в Handler додаємо обробник:

if text[0] == '/':
...
else:
response = CMD["<speech>"](message)
logging.info("REPLY\t%s\t%s" % (message['from']['id'], response))
send_reply(response)

А потім у список команд додаємо псевдо-мова:

RESPONSES = {
"Привіт": ["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['from']['id']}
if leven[1] < 75:
response['text'] = "I can not understand you"
else:
response['text'] = random.choice(RESPONSES.get(льовен[0])).format_map(
{'name': message["з"].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 захищені власним сертифікатом.

Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.