Создание телеграм-бота с веб-интерфейсом при помощи Python и Replit

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

В данной статье мы создадим общедоступную доску объявлений. Особенностью этой доски будет то, что пользователи смогут посылать свои объявления Telegram-боту, не заходя на сам сайт.

Сообщения будут просто содержать текст, но мы предоставим дополнительную информацию о том, как использовать другие данные, например, имена пользователей. Реализацию этого мы оставим вам в качестве упражнения по расширению функциональности.

bot functionality

Необходимые приготовления

Для получения максимальной пользы от этого руководства читателю нужно:

  • знать язык программирования Python
  • иметь аккаунт в Телеграм и установить клиентскую часть на свой компьютер
  • иметь аккаунт в Replit или создать его прямо сейчас.

Также было бы полезно, если бы вы были знакомы с базой данных Replit, но это не критично.

Регистрация бота

Для начала нам нужно зарегистрировать нашего бота в Telegram, чтобы сгенерировать учетные данные, которые мы будем использовать для подключения к Telegram API. Каждый бот должен быть привязан к конкретной учетной записи пользователя. Это можно сделать с помощью официального управляющего бота Telegram под названием «BotFather».

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

bot father
BotFather

Для активации BotFather нажмите на кнопку Start:

bot father start

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

Бот у нас спросит следующие данные:

  • имя бота, которое будет отображаться в верхней части чата нового бота, например, «Replit Quick-start Tutorial».
  • имя пользователя, которое будет использоваться для уникальной ссылки на этого бота, например, «@replit_tutorialbot».

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

token
Токен

Как только мы ответим на все вопросы, BotFather отправит нам наш токен аутентификации, который будет выглядеть примерно так:

110201543:AAHdqTcvCH1vGWJxfSeofSAs0K5PALDsaw

Обратите внимание, что вся строка (до двоеточия и после) является токеном.

Создание интерфейса бота

Теперь мы можем приступить к написанию той части программы, которая обрабатывает запросы из Telegram. Создадим новую repl-среду, а в качестве языка программирования выберем Python.

new repl

Наш бот должен взаимодействовать с Telegram. Для этого нам понадобится доступ к Telegram REST API. Есть много способов сделать это, но в рамках данной статьи мы будем использовать удобную библиотеку, обернутую обернута вокруг API.

Прежде чем мы продолжим, нам нужно сделать наш токен доступным для использования нашим ботом. Создайте переменную среды под названием TOKEN, щелкнув значок замка на боковой панели, как показано ниже, и вставьте свой токен бота, который вы получили ранее, например 110201543:AAHdqTcvCH1vGWJxfSeofSAs0K5:

env variables
Создание переменной среды

Это гарантирует, что наш токен доступен как переменная среды и что к нему не смогут получить доступ люди, имеющие доступ к нашей среде repl (в бесплатной версии она открыта для всех, — прим. переводчика).

Создание скелета бота

Теперь, когда все настроено, мы можем приступить к программированию! Начнем с нашего файла main.py:

import os

from telegram import Update #upm package(python-telegram-bot)
from telegram.ext import Updater, CommandHandler, MessageHandler, Filters, CallbackContext  #upm package(python-telegram-bot)


def help_command(update: Update, context: CallbackContext) -> None:
    htext = '''
Welcome
Send a message to store it.
Send /fetch to retrieve the most recent message'''
    update.message.reply_text(htext)


def main():
    updater = Updater(os.getenv("TOKEN"))

    dispatcher = updater.dispatcher
    dispatcher.add_handler(CommandHandler("start", help_command))
    dispatcher.add_handler(CommandHandler("help", help_command))

    updater.start_polling()

    updater.idle()


if __name__ == '__main__':
    main() 

Сначала мы импортируем модуль os, чтобы получить доступ к токену как переменной окружения.

Затем мы импортируем некоторые классы из библиотеки Telegram.

Комментарии, начинающиеся с #upm, не являются обязательными. Они используются Replit для загрузки правильного пакета. В общем случае это не нужно, но здесь необходимо, потому что подобных библиотек Telegram очень много.

Функция help_command запускается всякий раз, когда пользователь отправляет нам команду /start или /help. Команда /start также выполняется автоматически, когда новый пользователь запускает ваш бот (как мы ранее запускали BotFather). Бот будет знать, как использовать эту функцию, потому что мы сообщим ему об этом позже в теле функции main.

В функции main мы инициализируем экземпляр класса updater, который использует наш токен.

updater = Updater(os.getenv("TOKEN"))

Updater — это класс, который будет постоянно проверять Telegram на наличие новых сообщений в нашем боте.

Когда updater получает новое сообщение, он передает его в класс dispatcher. Тот проверяет, есть ли у нас подходящий обработчик для данного сообщения. Как уже упоминалось выше, нужно задать обработчик для команд /start и /help. Мы cделаем это при помощи функции add_handler, например:

dispatcher.add_handler(CommandHandler("start", help_command))

и

dispatcher.add_handler(CommandHandler("help", help_command))

Чтобы не усложнять задачу, команда /start и команда /help имеют здесь один и тот же обработчик, но при желании для обработки каждой из них вы можете использовать разные функции.

Затем нам нужно сказать классу updater, чтобы он начал проверку новых сообщений. Мы это сделаем при помощи следующей строки.

updater.start_polling()

Важно знать, что start_polling — это неблокирующая функция. Это означает, что код в ней не прекращает выполнение. Он будет исполняться, пока программа не завершится.

Другими словами, если мы оставим это в качестве последней строки функции main, код будет выполнен, а затем немедленно завершится, потому что больше ничего не блокирует его. Поэтому, чтобы наш бот не прекращал прослушку, мы используем строку updater.idle(), чтобы заблокировать скрипт, пока мы слушаем.

[python_ad_block]

Функционал для логирования

Согласно тексту справки, бот должен уметь делать две вещи:

  1. Если вы отправляете сообщение боту, он должен где-то его сохранить.
  2. При отправке боту команды /fetch он должен отправить вам последнее сообщение.

Для этого мы будем использовать встроенную в Replit базу данных ключ-значение. Начнем с импорта API:

from replit import db

Модуль db — это объект, который ведет себя как словарь, но сохраняет свое содержимое между запусками. Он также сериализует свои ключи в виде строк.

Мы хотим хранить зарегистрированные сообщения в определенном порядке, но объект db по своей сути не упорядочен (будучи словарем). Поэтому мы создадим вспомогательную функцию, которая может получать самый большой ключ (при условии, что мы будем использовать только числовые индексы). Добавим эту функцию перед определением функции help_command:

def latest_key():
    ks = db.keys()
    if len(ks):
        return max(map(int, ks))
    else:
        return -1 

Функция latest_key получает все ключи из нашей базы данных (модуль db). Если в ней есть ключи, они преобразуются в целые числа и возвращается максимальное из них. Если ключей нет, то возвращается -1.

Теперь мы можем создать обработчик, который записывает сообщения пользователей в базу данных. Добавим эту функцию после задания функции help_command:

def log(update: Update, context: CallbackContext) -> None:
    db[str(latest_key() + 1)] = update.message.text 

Этот обработчик получает последний ключ из базы данных, увеличивает его на единицу и создает новую пару ключ — сообщение.

Однако это не может быть выполнено, пока мы не зарегистрируем обработчик. Поэтому добавьте следующую строку после других строк dispatcher.add_handler (...):

dispatcher.add_handler(MessageHandler(Filters.text & ~Filters.command, log))

Можно заметить, что вместо метода CommandHandler используется MessageHandler. Это более общий обработчик, который выбирает сообщения на основе предоставленных вами флагов. В данном случае он обрабатывает сообщения, содержащие только текст, но не команды.

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

def fetch(update: Update, context: CallbackContext) -> None:
    update.message.reply_text(db.get(str(latest_key()), 'No Messages yet.'))

Мы можем зарегистрировать его вместе с обработчиками остальных команд. Добавьте данную строку после уже существующих строк dispatcher.add_handler (...):

dispatcher.add_handler(CommandHandler("fetch", fetch))

Создаем веб-интерфейс

Теперь, когда у нас есть вполне функциональный бот, нам нужно добавить для него веб-интерфейс. Мы будем использовать фреймворк Flask. Мы должны включить следующий код после других операций импорта и перед определением функции latest_key.

from math import ceil
from flask import render_template
from flask import Flask
app = Flask(__name__)

@app.route('/')
@app.route('/<int:page>')
def home(page=None):
    ks = sorted(map(int, db.keys()))
    pages = ceil(len(ks) / 10)
    if page is None: #Default to latest page
        page = pages

    if page < pages:
        next_page = page + 1
    else:
        next_page = None
    if page > 1:
        prev_page = page - 1
    else:
        prev_page = None

    messages = tuple(db[str(key)] for key in ks[(page-1)*10:page*10])

    return render_template('home.html', messages=messages, next_page=next_page, page=page, prev_page=prev_page)

Этот код создает небольшое Flask-приложение. Импорт модуля Flask обеспечивает среда программирования Replit. В этом уроке мы сделаем только одну страницу.

Мы сообщаем Flask, каким образом должна быть доступна страница при помощи специальных декораторов. Декоратор @app.route ('/') говорит, что когда пользователь обращается к https://example.com, его будет обслуживать этот обработчик. В этом случае переменная page будет иметь значение по умолчанию None.

Декоратор @app.route('/<int:page>')  говорит, что когда пользователь обращается к чему-то вроде https://example.com/4, он откроет страницу 4 записанных сообщений. В этом случае переменной page будет присвоено значение 4.

Но пока это работать не будет, потому что шаблон home.html еще не существует. Давайте создадим его прямо сейчас в папке с названием templates (т.е. templates/home.html):

<!doctype html>
<h1>Messages - Page {{ page }}</h1>
<ul>
 {% for msg in messages %}
 <li>{{ msg | escape }}</li>
 {% endfor %}
</ul>

{% if prev_page %}<a href='/{{ prev_page }}'>Previous Page</a>{% endif %}
{% if prev_page and next_page %}|{% endif%}
{% if next_page %}<a href='/{{ next_page }}'>Next Page</a>{% endif %}

Этот шаблон будет выводить страницу записанных сообщений и ссылки на следующую или предыдущую страницу. Он требует наличия переменной page и массива messages, который будет отображаться в виде списка. Шаблон также принимает переменные «prev_page» и «next_page», которые мы используем для создания ссылок на предыдущую и следующую страницу, если они существуют, конечно. Все они присутствуют в нашей функции маршрутизации, когда мы запускаем render_template.

Как нам рассчитать максимальное количество страниц?

pages = ceil(len(ks) / 10)

То есть мы делим количество ключей в нашей базе данных Replit на десять и округляем его в большую сторону. Мы также можем использовать это число по умолчанию. Таким образом, если кто-то вызовет простой маршрут «/», мы просто отобразим последнюю страницу.

if page is None:
    page = pages

Мы знаем, что последние сообщения всегда будут «самыми последними», потому что мы отсортировали их в порядке возрастания в предыдущей строке.

Переменные prev_page и next_page всегда содержат в себе значение текущей страницы, уменьшенное либо соответственно увеличенное на единицу, если они являются действительными номерами страниц. В противном случае им присваивается значение None, чтобы шаблон их не отображал.

Собираем все воедино

Если мы запустим нашу программу сейчас, веб-приложение Flask еще не будет работать. Flask должен прослушивать запросы аналогично библиотеке Telegram. Чтобы запустить сервер Flask, мы вполне можем в конце нашей программы использовать app.run() .

Проблема в том, что эта строка кода при нормальных обстоятельствах никогда не будет достигнута, потому что у нас есть строка updater.idle(), блокирующая наш код до нее. Чтобы решить эту проблему, мы можем заменить эту строку строкой, которая запускает наш сервер Flask на первом уровне. Строка updater.idle() у нас присутствовала лишь для того, чтобы предотвратить преждевременное завершение программы. Теперь то же самое будет делать сервер Flask. Итак, давайте произведем замену:

#updater.idle()
app.run(host='0.0.0.0', port=8080)

Параметры host и port, установленные в эти значения, позволяют среде Replit получить доступ к серверу и обычно будут отображать окно с содержимым нашей страницы. Теперь мы можем просматривать сообщения, отправленные пользователями через бот.

Сделайте все это сами

Если вы точно следовали нашим инструкциям, у вас уже есть собственная версия repl с работающим кодом. В противном случае попробуйте еще раз.

Что дальше?

Попробуйте использовать команду /setcommands в BotFather, чтобы добавить быстрое меню для команд в вашем боте. Здесь описаны все команды по работе с этим ботом.

Если бы мы хотели получить доступ к имени пользователя — отправителя сообщения, мы могли бы получить к нему доступ так же, как и к тексту сообщения:

username = update.message.from_user.username

Для изучения возможностей по получению дополнительных данных изучите, пожалуйста, документацию.