Дмитрий
Дмитрий
Developer Python, PHP, Javascript
18.06.2019

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

Дмитрий
Дмитрий
Developer Python, PHP, Javascript
18.06.2019
18.06.2019
3.8
14075
0

Задача.

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

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

Реализация.

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

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

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

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

Например.

В теге 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)

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

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

Необходимо найти контакт 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 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()

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

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

Делается это функцией 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, 'Привет!')

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

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)

Сигнатура функции-обработчика для кнопок выглядит по аналогии со 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. В его основу положен принцип навигации по каталогам и отправку сообщений, содержащих текст, картинки и кнопки для навигации в следующий каталог. Освещены механизмы управления ботом и реакции на пользовательские события.

Перспективы

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

Как вам статья?
3.8
Проголосовало: 12
Давайте обсудим Ваш проект
Нажимая на кнопку “Отправить”, вы даете согласие на обработку личных данных. Подробнее
Комментарии
(0)
Будьте первыми, кто оставит комментарий
wezom logo
Остались вопросы?
Оставьте ваши контактные данные. Наш менеджер свяжется и проконсультирует вас.
Подписывайтесь на рассылку Айтыжблог
blog subscriber decor image
Хотите получать интересные статьи?
Нажимая на кнопку “Отправить”, вы даете согласие на обработку личных данных. Подробнее
Следите за нами в социальных сетях
Этот сайт использует cookie-файлы для более комфортной работы пользователя. Продолжая просматривать сайт, Вы соглашаетесь на использование cookie.