Возникла идея простого бота, который будет отправлять переданные ему файлы в облако. Однако для аутентификации сейчас практически везде используется OAuth2. Если в web-приложениях пользоваться OAuth2 все уже научились, то с telegram у меня возникли вопросы.

В этой заметке будет минимальный пример telegram-бота, который работает с box.com через API с аутентификацией через OAuth2.

disclaimer #1: В статье не будет подробного описания работы OAuth2 — только необходимый минимум информации для понимания процесса.

disclaimer #2: Для примера использую облачное хранилище box.com, так как именно в нём лежит моя библиотека электронных книг. При реализации аналогичной функциональности для других облаков проблем возникнуть не должно.

Какой сценарий с точки зрения пользователя получим в конце текущей статьи:

  1. Пользователь добавляет бота в telegram
  2. По команде /login пользователь получает ссылку на аутентификацию
  3. Пользователь переходит по ссылке в браузере на box.com
  4. Пользователь аутентифицируется на box.com
  5. Открывается страница бота https://t.me/{BOT_NAME}?start={code} с предложением отправить ему сообщение по кнопке Send message
  6. Пользователь нажимает на кнопку Send message
  7. В telegram открывается диалог с ботом с активной кнопкой Start
  8. Пользователь нажимает на кнопку Start
  9. Бот отправляет пользователю его login в box.com

Добавить остальную функциональность — дело техники.

Необходимый минимум об OAuth2

Для реализация аутентификации через OAuth2 необходимо выполнить несколько пунктов:

  1. Получить client_id и client_secret в консоли разработчика box.com. Подробно процесс описан в приложении в конце статьи
  2. Сформировать ссылку для аутентификации пользователя https://account.box.com/api/oauth2/authorize?client_id={client_id}&redirect_uri={redirect_url}&response_type=code. client_id и redirect_url необходимо скопировать из настроек созданного ранее приложения в консоли box.com
  3. На адресе redirect_url (по-умолчанию в этой статье — это localhost:8000) поднять веб-сервер, который сможет получить код для аутентификации в box.com
  4. С полученным в п.3 кодом и параметрами client_id и client_secret из п.1 обратиться к endpoint box.com API для получения access_token и refresh_token. access_token в дальнейшем необходимо добавлять во все запросы к API.

Реализация

Шаг 1. Подготовка

Для работы понадобятся три внешних библиотеки:

  • python-telegram-bot — для работы с Telegram Bot API
  • requests — для выполнения запросов к API
  • boxsdk — для работы с API box.com

Библиотеки и их версии прописаны в requirements.txt. Установить всё через pip install -r requirements.txt — ничего необычного. Но лучше для этого создать отдельное виртуальное окружение.

Шаг 2. Продолжаем подготовку

Теперь надо получить токен для бота. Я уже писал об этом ранее.

Кратко повторю. Получить токен для нового бота возможно через служебный аккаунт @BotFatherBot. Для этого отправить ему команду /newbot и пройти небольшой визард. В процессе надо сохранить токен, который потребуется при создании бота, и имя бота.

Шаг 3. Веб-сервис для получения OAuth2-кода

Нужен простой веб-сервис. На URI этого веб-сервиса будет происходить перенаправление со страницы аутентификации box.com. Адрес перенаправления будет содержать параметр code в строке запроса. Этот код используется для получения access_token и refresh_token дальше.

Веб-сервис будет без использования внешних зависимостей. Реализован на встроенном simple http server. Код позаимоствован из gist, но немного модифицирован:

#!/usr/bin/env python3

"""
Very simple HTTP server in python for logging requests
Usage::
    ./server.py [<port>]
"""
from http.server import BaseHTTPRequestHandler, HTTPServer
import logging

# Имя созданного ранее в @BotFather бота

bot_name = 'BOT_NAME'  

class S(BaseHTTPRequestHandler):
    def do_GET(self):
        logging.info("GET request,\nPath: %s\nHeaders:\n%s\n", str(self.path), str(self.headers))
        # Необходимо перенаправить запрос на адрес твоего бота.

        # Полученный из box.com API код передаётся созданному боту через механизм deep linking (https://core.telegram.org/bots#deep-linking).

        self.send_response(301)
        self.send_header('Location',f'https://t.me/{bot_name}?start={self.path.split("=")[-1]}')
        self.end_headers()

def run(server_class=HTTPServer, handler_class=S, port=8000):
    logging.basicConfig(level=logging.INFO)
    server_address = ('', port)
    httpd = server_class(server_address, handler_class)
    logging.info('Starting httpd...\n')
    try:
        httpd.serve_forever()
    except KeyboardInterrupt:
        pass
    httpd.server_close()
    logging.info('Stopping httpd...\n')

if __name__ == '__main__':
    from sys import argv

    if len(argv) == 2:
        run(port=int(argv[1]))
    else:
        run()

Использовать код выше в настоящем боевом production я бы, конечно, не стал, но для демонстрации идеи подойдёт.

Шаг 4. Минимально работающий бот

Простейший бот с использованием библиотеки python-telegram-bot выглядит так:

from telegram.ext import Updater
from telegram.update import Update
from telegram.ext.callbackcontext import CallbackContext
from telegram.ext import CommandHandler

token = 'TG_BOT_TOKEN'
updater = Updater(token=token, use_context=True)
dispatcher = updater.dispatcher

def start(update: Update, context: CallbackContext):
    context.bot.send_message(chat_id=update.effective_chat.id, text="I'm a bot, please talk to me!")

start_handler = CommandHandler('start', start)

dispatcher.add_handler(start_handler)

updater.start_polling()

где TG_BOT_TOKEN — токен, который выдал @BotFather.

Этот бот не делает ничего полезного — только отправляет сообщение "I'm a bot, please talk to me!" по команде /start.

Теперь надо добавить самое важное — аутентификацию через OAuth2.

Шаг 4.1. OAuth2-аутентификация

Для аутентификации необходимо перейти по сформированной особым образом ссылке: https://account.box.com/api/oauth2/authorize?client_id={client_id}&redirect_uri={redirect_url}&response_type=code, где client_id и redirect_url параметры из настроек созданного ранее приложения в консоли разработчика box.com.

На этой странице будет стандартная форма логина в box.com login-page.

После ввода логина и пароля произойдёт перенаправление на redirect_url. На этом адресе сервис, получив GET-запрос, вычленит из строки запроса code. После чего перенаправит запрос на страницу старта работы с telegram-ботом. В адрес будет добавлен полученный код. Далее должен открыться диалог с ботом с активной кнопкой Start. По клику на кнопку в обработчике команды /start можно будет получить переданный код.

Простейший код как proof of concept:

from telegram.ext import Updater
from telegram.update import Update
from telegram.ext.callbackcontext import CallbackContext
from telegram.ext import CommandHandler
import requests
import json
import boxsdk


client_secret: str = 'CLIENT_SECRET'
client_id: str = 'CLIENT_ID'
redirect_url: str = 'REDIRECT_URL'
token = 'TG_BOT_TOKEN'

updater = Updater(token=token, use_context=True)
dispatcher = updater.dispatcher

# Обработчик команды /start

def start(update: Update, context: CallbackContext):
    message_text: str = update.message.text
    # В deep linking параметры из url передаются в сообщении в виде строки "/start code", 

    # где code - строка из url https://t.me/{BOT_NAME}?start=code.

    message_parts: list[str] = message_text.split(' ')
    if len(message_parts) == 2:
        # Во втором элементе будет содержаться переданный код, 

        # который необходим для получения access_token и refresh_token

        code: str = message_parts[1]

        # Формируем запрос к API box.com для получения access_token и refresh_token

        access_token_url = 'https://api.box.com/oauth2/token'
        headers = {'Content-Type': 'application/x-www-form-urlencoded'}
        params = {
            'client_id': client_id,
            'client_secret': client_secret,
            'code': code,
            'grant_type': 'authorization_code'
        }
        req = requests.post(access_token_url, data=params, headers=headers)
        data = json.loads(req.text)

        # Полученный JSON содержит необходимые данные.

        # Используя их можно инициализировать клиент boxsdk

        # и начать обращаться к его API.

        oauth: boxsdk.OAuth2 = boxsdk.OAuth2(
            client_id,
            client_secret,
            access_token=data['access_token'],
            refresh_token=data['refresh_token'])
        box_client = boxsdk.Client(oauth)

        # Например, можно получить информацию об аутентифицированном пользователе.

        user = box_client.user().get()
        context.bot.send_message(chat_id=update.effective_chat.id, text=user.login)
    else:
        show_welcome_message(update, context)

# Обработчик команды /login

def login(update: Update, context: CallbackContext):
    context.bot.send_message(chat_id=update.effective_chat.id, text=f'<a href="https://account.box.com/api/oauth2/authorize?client_id={client_id}&redirect_uri={redirect_url}&response_type=code">Connect to Box.com</a>', parse_mode='HTML')

def show_welcome_message(update: Update, context: CallbackContext):
    context.bot.send_message(chat_id=update.effective_chat.id, text="I'm a bot, please talk to me!")

start_handler = CommandHandler('start', start)
login_handler = CommandHandler('login', login)

dispatcher.add_handler(start_handler)
dispatcher.add_handler(login_handler)

updater.start_polling()

Что дальше

  1. Привести код бота в порядок: выделить классы, методы,
  2. Сделать конфигурирование бота более удобным,
  3. Реализовать загрузку файлов в облако.

О чём-то из этого постараюсь написать позже.

Исходники для этой заметки можно найти на github.

Приложение

Получить client_id и client_secret необходимо в консоли разработчика box.com:

  1. Cоздать новое приложение с типом Custom app create-app
  2. В настройках приложения выбрать User Authentication (OAuth2) и указывать App Name setup-app
  3. В разделе Configuration созданного приложения:
    1. в секции OAuth 2.0 Credentials забрать client_id и client_secret,
    2. в секции OAuth 2.0 Redirect URI указать localhost:8000 (или другой адрес, если есть понимание, что это и зачем)
    3. поставить галочку в чекбоксе Write all files and folders stored in Box credentials (в этой статье про это говорить не буду)