Basics of Python and Django. / Практикум. Создаем телеграм бота. / Создаем телеграм бота, управляемого из файловой системы.

Пишем бота Telegram с управлением через файловую систему.

Задача.

Нам дают картинки и описание товаров и из них нужно сделать каталог.

Этот каталог будет использоваться телеграм-ботом, который бродит по каталогам и предлагать пользователю товары, отображая картинки и опции для навигации по каталогу.

Реализация.

Картинки и текстовые файлы будем складывать в каталоги.

У нас будет общий каталог, в котором каждый бот будет иметь свой собственный, названный его именем.

В каталоге бота будет много подкаталогов, в каждом из которых будет картинка товара и текстовый файл message.txt с инструкциями для бота по созданию сообщения при попадании в этот каталог.

В этом файле будет содержаться содержимое каждого сообщения бота оформленного в виде xml документа.

Например.

<message>Hello! Welcome</message>
<button value="path_to_one_step">
    Do you want to see the catalog?
</button>

<button value="path_to_second_step">
   No thank you
</button>

В теге message указывается текстовое сообщение бота.

Из тегов button будут созданы кнопки, по которым бот будет прыгать в другие директории, указанные в их атрибуте value.

Для работы нам понадобиться следующие библиотеки:

python-telegram-bot - для использования API Telegram;

bs4 - для парсинга xml текстового файла.

Создадим рабочий каталог.

mkdir tbot
cd tbot

В нем файл requirements.txt где перечислим необходимые зависимости.

python-telegram-bot==12.0.0b1
bs4

Установим виртуальное окружение и установим в него пакеты.

virtualenv -p python3 venv
source ./venv/bin/activate
pip install -r requirements.txt

Напишем скрипт инсталляции бота install.py.

В начале создадим новую пустую директорию storage, где будем хранить каталоги ботов, т. к. их может быть несколько.

Потом, зададим пользователю 2 вопроса с просьбой указать:

  1. Имя бота.

  2. Секретный ключ.

По имени бота будем создавать подкаталоги в папке storage.

Код инсталлятора.

#!/usr/bin/env python
import os
print('Устанавливаем бот!')
DATA_DIR = 'storage'

if __name__ == '__main__': # если запуск из консоли

    # Создаем каталог storage если его нет
    if not os.path.exists(DATA_DIR):
        print('Создаю директорию %s' % DATA_DIR)
        os.makedirs(DATA_DIR)

    name = input('Укажите имя бота: ')
    # Формируем путь к каталогу бота
    bot_path = '%s/%s' % (DATA_DIR, name)
    if not os.path.exists(bot_path):
        os.makedirs(bot_path)
        if not os.path.exists(bot_path+'/index'):
            print('Создаю директорию для бота %s' % name)
            os.makedirs(bot_path+'/index')
    else:
        print("Такой бот с именем %s уже есть!" % bot_path)

    key = input('Укажите ключ: ')
    # записываем секретный ключ в файл
    f = open('%s/key' % bot_path, 'w+')
    f.write(key)
    f.close()
    print("Все готово. Для активации бота запустите команду ./run.py %s" % name)

Результат работы с введенным не валидным ключом.

python requests

Как зарегистрировать бота и получить секретный ключ (токен).

Необходимо найти контакт BotFather.

Написать команду /newbot

Подобрать уникальное имя, после чего вам отправят сообщение такого вида.

Done! Congratulations on your new bot. You will find it at t.me/zdimon77_bot. You can now add a description, 
about section and profile picture for your bot, see /help for a list of commands. 
By the way, when you've finished creating your cool bot, ping our Bot Support if you want a better username for it. 
Just make sure the bot is fully operational before you do this.

Use this token to access the HTTP API:
829228816:AAGSjgoh0hfj-fwyMBo4UlGvGxHtnp6Z_сs
Keep your token secure and store it safely, it can be used by anyone to control your bot.

For a description of the Bot API, see this page: https://core.telegram.org/bots/api

Начнем писать бота в файле run.py.

Он будет запускаться с одним параметром - именем бота.

Сначала импортируем все, что нам необходимо.

import sys
import os
from telegram.ext import Updater
from telegram.ext import CommandHandler, CallbackContext, CallbackQueryHandler
from telegram import InlineKeyboardButton, InlineKeyboardMarkup
from telegram import Bot
from bs4 import BeautifulSoup    
from install import DATA_DIR

Забераем имя бота из первого аргумента командной строки, а если его нет то сообщим об этом и вывалимся из программы.

try:
    botname = sys.argv[1]
except:
    print("Введите в качестве аргумента имя бота например ./run.py my_bot")
    sys.exit()

Найдем секретный ключ и используя его, создадим объект бота.

print("Запускаю бота %s" % botname)
bot_path = '%s/%s' % (DATA_DIR,botname)
print("Читаю настройки из %s" % bot_path)
f = open(bot_path+'/key','r')
key = f.read()
f.close()
print("Ключ: %s" % key)
bot = Bot(token=key)

Определим функцию start, которая будет срабатывать когда пользователь нажмет на кнопку Start в программе Telergam.

def start(update: Updater, context: CallbackContext):
    print("Start command!")

В эту функцию будет передано два объекта update и context.

Мы будем использовать update для получения информации о пришедшем пользователе.

Например, получить его логин и идентификатор комнаты можно так:

username = update.message.from_user['username']
room_id = update.message.chat_id

Мы будем использовать этот идентификатор дальше для отправки сообщений в канал пользователя.

Для того, чтобы привязать эту функцию к обработчику события нажатия на кнопку “Старт”, нужно создать объект-обработчик класса CommandHandler и «скормить» ему нашу функцию start.

start_handler = CommandHandler('start', start)

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

Контейнером для обработчиков будет объект updater.

updater = Updater(token=key, use_context=True)

Добавляем обработчик в контейнер и запускаем бесконечный процесс опроса telegram комнаты бота.

updater.dispatcher.add_handler(start_handler)
updater.start_polling()

Полный код программы.

#!/usr/bin/env python
import sys
import os
from telegram.ext import Updater
from telegram.ext import CommandHandler, CallbackContext, CallbackQueryHandler
from telegram import InlineKeyboardButton, InlineKeyboardMarkup
from telegram import Bot
from bs4 import BeautifulSoup

from install import DATA_DIR
try:
    botname = sys.argv[1]
except:
    print("Введите в качестве аргумента имя бота например ./run.py my_bot")
    sys.exit()

print("Запускаю бота %s" % botname)
bot_path = '%s/%s' % (DATA_DIR,botname)
print("Читаю настройки из %s" % bot_path)
f = open(bot_path+'/key','r')
key = f.read()
f.close()
bot = Bot(token=key)

def start(update: Updater, context: CallbackContext):
    print("Start command!")
    username = update.message.from_user['username']
    room_id = update.message.chat_id 
    print("Новый пользователь %s room_id: %s" % (username, room_id))

start_handler = CommandHandler('start', start)
updater = Updater(token=key, use_context=True)
updater.dispatcher.add_handler(start_handler)
updater.start_polling()

Результат работы.

python requests

Пробуем послать ботом сообщение приветствия.

Делается это функцией bot.send_message(), в которую кроме сообщения, передается идентификатор комнаты.

def start(update: Updater, context: CallbackContext):
    print("Start command!")
    username = update.message.from_user['username']
    room_id = update.message.chat_id 
    print("Новый пользователь %s room_id: %s" % (username, room_id))
    bot.send_message(room_id, 'Привет!')

python requests

Если надо послать картинку, то сделать это можно другой функцией.

bot.send_photo(chat_id=room_id, photo=open(img_path, 'rb'))

Теперь мы знаем как посылать сообщения и картинки.

Давайте разбираться с кнопками и тем как их посылать?

В начале необходимо определиться с их компоновкой.

Допустим, мы закинули кнопки в список, например:

lst = ['btn1', 'btn2', 'btn3', 'btn4']

Чтобы расположить их в два ряда по две, необходимо из этого списка сформировать такую структуру:

[['btn1', 'btn2'], ['btn3', 'btn4']]

Вот небольшая функция которая это делает.

def build_menu(buttons,n_cols):
    menu = [buttons[i:i + n_cols] for i in range(0, len(buttons), n_cols)]
    return menu

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

Сама кнопка создается такой конструкцией.

from telegram import InlineKeyboardButton
btn = InlineKeyboardButton('Нажми меня',callback_data='button_pressed')

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

Отправим 4 тестовые кнопки по 2 в ряд.

# список с кнопками
btn_list = []
# забрасываю 4 штучки
for cnt in range(0,4):
    btn_list.append(InlineKeyboardButton('Нажми меня %s' % cnt,callback_data='button_%s_pressed' % cnt))
# формирую объект разметки из класса InlineKeyboardMarkup
markup_list = InlineKeyboardMarkup(build_menu(btn_list,n_cols=2))
# посылаю кнопки
bot.send_message(room_id, 'Кнопки', reply_markup=markup_list)

python requests

Сигнатура функции-обработчика для кнопок выглядит по аналогии со start.

def press_button(update: Updater, context: CallbackContext):
    print("Pressing button %s" % update.callback_query.data)

Только связывание обработчика отличается использованием класса CallbackQueryHandler, который примет нашу функцию.

updater.dispatcher.add_handler(CallbackQueryHandler(press_button))

Теперь осталось связать это все вместе, и заставить бота перемещаться по каталогам и подбирать их содержимое.

В новой функции navigate, которая будет вызываться при нажатии на кнопку, я буду делать следующее:

  1. Переходить в нужный каталог.

  2. Посылать картинку image.png если она там есть.

  3. Считывать файл message.txt.

  4. Посылать содержимое тега message и кнопки в тегах button.

Код функции.

def navigate(command, chat_id):
    path = '%s/%s' % (bot_path,command)
    img_path = '%s/image.png' % path
    print(img_path)
    # шлю картинку если нашел
    if os.path.isfile(img_path):
        bot.send_photo(chat_id=chat_id, photo=open(img_path, 'rb'))
    message_path = path+'/message.txt'
    # находим файл message.txt
    if os.path.isfile(message_path):
        with open(message_path) as f:
            but_txt = f.read()
        # парсим xml 
        soup = BeautifulSoup(but_txt, 'html.parser')
        msg = soup.find('message')
        btns = soup.findAll('button')
        btn_lst = []
        # формируем кнопки
        for bt in btns:
            btn_lst.append(InlineKeyboardButton(bt.text,callback_data=bt['value']))
        button_list = InlineKeyboardMarkup(build_menu(btn_lst,n_cols=1))
        # посылаем кнопки и сообщение
        bot.send_message(chat_id, msg.text, reply_markup=button_list)

Последний пункт использует библиотеку BeautifulSoup и реализован следующим образом.

    with open(message_path) as f:
        but_txt = f.read()
    soup = BeautifulSoup(but_txt, 'html.parser')
    msg = soup.find('message') # один элемент
    btns = soup.findAll('button') # несколько

Полный код бота:

#!/usr/bin/env python3
import sys
import os
from telegram.ext import Updater
from telegram.ext import CommandHandler, CallbackContext, CallbackQueryHandler
from telegram import InlineKeyboardButton, InlineKeyboardMarkup
from telegram import Bot
from bs4 import BeautifulSoup

from install import DATA_DIR
try:
    botname = sys.argv[1]
except:
    print("Введите в качестве аргумента имя бота например ./run.py my_bot")
    sys.exit()

print("Запускаю бота %s" % botname)
bot_path = '%s/%s' % (DATA_DIR,botname)
print("Читаю настройки из %s" % bot_path)
f = open(bot_path+'/key','r')
key = f.read()
f.close()
bot = Bot(token=key)

def navigate(command, chat_id):
    path = '%s/%s' % (bot_path,command)
    img_path = '%s/image.png' % path
    print(img_path)
    # шлю картинку если нашел
    if os.path.isfile(img_path):
        bot.send_photo(chat_id=chat_id, photo=open(img_path, 'rb'))
    message_path = path+'/message.txt'
    # находим файл message.txt
    if os.path.isfile(message_path):
        with open(message_path) as f:
            but_txt = f.read()
        # парсим xml 
        soup = BeautifulSoup(but_txt, 'html.parser')
        msg = soup.find('message')
        btns = soup.findAll('button')
        btn_lst = []
        # формируем кнопки
        for bt in btns:
            btn_lst.append(InlineKeyboardButton(bt.text,callback_data=bt['value']))
        button_list = InlineKeyboardMarkup(build_menu(btn_lst,n_cols=1))
        # посылаем кнопки и сообщение
        bot.send_message(chat_id, msg.text, reply_markup=button_list)


def build_menu(buttons,n_cols):
    menu = [buttons[i:i + n_cols] for i in range(0, len(buttons), n_cols)]
    return menu

def press_button(update: Updater, context: CallbackContext):
    print("Pressing button %s" % update.callback_query.data)
    navigate(update.callback_query.data,update.callback_query.message.chat_id)

def start(update: Updater, context: CallbackContext):
    print("Start command!")
    username = update.message.from_user['username']
    room_id = update.message.chat_id 
    navigate('index',update.message.chat_id)

start_handler = CommandHandler('start', start)

updater = Updater(token=key, use_context=True)
updater.dispatcher.add_handler(start_handler)
updater.dispatcher.add_handler(CallbackQueryHandler(press_button))
updater.start_polling()

Результат работы.

python requests

Выводы

В статье рассмотрен процесс создания телеграм-бота на языке Python. В его основу положен принцип навигации по каталогам и отправку сообщений, содержащих текст, картинки и кнопки для навигации в следующий каталог. Освещены механизмы управления ботом и реакции на пользовательские события.

Перспективы

Нехватает функционала по принятию контактной информации от пользователя с сохранением заказов в базе данных и уведомлениях администатору. Об этом постараюсь рассказать в следующей статье.

Ссылка на репозиторий GIT