Основи Python и 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>

З тегів 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

    nname = 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'
     # знаходимо файл 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