Футбольный телеграм бот на Python (1/4): Подготовка и настройка бота

В этой серии статей мы напишем телеграм бота на python. Он работает с внешним API, запрашивает результаты футбольных матчей и выводить их в сообщении.

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

Вся разработка разбита на этапы:

  1. Локальная установка библиотек и Redis.
  2. Регистрация и получение токена.
  3. Настройка , подключение к базам данных.
  4. Написание основной функциональности бота.
  5. Регистрации, выбор и настройка внешнего апи футбольных матчей.
  6. Добавление сбора результатов матчей и интеграция в бота.
  7. Деплой, публикация на сервере:
    1. Регистрация дешевого или бесплатного VPS.
    2. Запуск Редис-клиента.
    3. Запуск и настройка бота на сервере.

Рабочая версия бота запущена в телеграме до конца февраля @FonlineBOT. Бот отключен.

Вводные данные

Материал рассчитан на уровень Начинающий+, нужно понимать как работают классы и функции, знать основы базы данных и async/await. Если знаний мало, крайне желательно писать код в Pycharm, бесплатная версия подходит.

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

Версия Python - 3.8+

aiogram==2.11.2
emoji==1.1.0
redis==3.5.3
ujson==4.0.1
uvloop==0.14.0  # не работает и не требуется на Windows

Репозиторий с кодом этой для этой части бота:
https://gitlab.com/PythonRu/fonlinebot/-/tree/master/first_step

Локальная установка библиотек для бота и Redis

Для начала нужно создать проект «fonlinebot» с виртуальным окружение. В Pycharm это делается так:

pycharm new project

Затем установить библиотеки в виртуальном окружении. Сразу понадобятся 4: для бота, работы с redis, ускорения и emoji в сообщениях.

pip install aiogram==2.11.2 redis==3.5.3 ujson==4.0.1 emoji==1.1.0

Установка Redis локально

Redis — это резидентная база данных (такая, которая хранит записи прямо в оперативной памяти) в виде пар ключ-значение. Чтение и запись в память происходит намного быстрее, чем в случае с дисками, поэтому такой подход отлично подходит для хранения второстепенных данных.

Из недавней статьи — Redis для приложений на Python

Для установки Redis на Linux/Mac следуйте этим инструкциям: https://redis.io/download#from-source-code. Для запуска достаточно ввести src/redis-server.

Что бы установить на Windows скачайте и распакуйте архив отсюда. Для запуска откройте «redis-server.exe».

Теперь нужно убедиться, что все работает. Создайте файл «main.py» в корне проекта и выполните этот код:

# fonlinebot/main.py
import redis


r = redis.StrictRedis()
print(r.ping())

Вывод будет True, в другом случае ошибка.

Регистрация бота и получение токена

Для регистрации напишем https://t.me/botfather команду /newbot. Далее он просит ввести имя и адрес бота. Если данные корректны, выдает токен. Учтите, что адрес должен быть уникальным, нельзя использовать «fonlinebot» снова.

Футбольный телеграм бот на Python (1/4): Подготовка и настройка бота

На время разработки сохраним токен в файл. Создайте «config.py» в папке проекта для хранения настроек и запишите токен TOKEN = "ВАШ ТОКЕН"

Настройка бота

Теперь нужно связать бота с redis и базой данных, проверить работоспособность.

Создадим необходимые модули и файлы. В папке «fonlinebot» к созданным ранее «main.py» и «config.py» добавим: «database.py», «requirements.txt» и папку «app». В папку «app» добавьте: «bot.py», «dialogs.py», «service.py». Вот такая структура получится:

Футбольный телеграм бот на Python (1/4): Подготовка и настройка бота

Разделив бот на модули, его удобнее поддерживать и дорабатывать.

  • «main.py» — для запуска бота.
  • «config.py» — хранит настройки, ключи доступов и другую статическую информацию.
  • «database.py» — для работы с базой данных и кешем(redis).
  • «requirements.txt» — хранит зависимости проекта, для запуска на сервере.
  • «app» — папка самого бота.
    • «bot.py» — для взаимодействия бота с юзерами, ответы на сообщения.
    • «dialogs.py» — все текстовые ответы бота.
    • «service.py» — бизнес логика, получение и обработка данных о матчах.

Пришло время перейти к программированию. Запишем в «requirements.txt» наши зависимости:

aiogram==2.11.2
emoji==1.1.0
redis==3.5.3
ujson==4.0.1
uvloop==0.14.0

Так как большая часть программирует на Windows, uvloop мы не устанавливали локально. Установим его на сервере.

В «config.py» к токену добавим данные бота и подключения к redis.

# fonlinebot/config.py
import ujson
import logging
logging.basicConfig(level=logging.INFO)


TOKEN = "здесь должен быть токен"
BOT_VERSION = 0.1
# База данных хранит выбранные юзером лиги
BOT_DB_NAME = "users_leagues"
# Тестовые данные поддерживаемых лиг
BOT_LEAGUES = {
    "1": "Бундеслига",
    "2": "Серия А",
    "3": "Ла Лига",
    "4": "Турецкая Суперлига",
    "5": "Чемпионат Нидерландов",
    "6": "Про-лига Бельгии",
    "7": "Английская Премьер-лига",
    "8": "Лига 1",
}
# Флаги для сообщений, emoji-код
BOT_LEAGUE_FLAGS = {
    "1": ":Germany:",
    "2": ":Italy:",
    "3": ":Spain:",
    "4": ":Turkey:",
    "5": ":Netherlands:",
    "6": ":Belgium:",
    "7": ":England:",
    "8": ":France:",
}

# Данные redis-клиента
REDIS_HOST = 'localhost'
REDIS_PORT = 6379
# По умолчанию пароля нет. Он будет на сервере
REDIS_PASSWORD = None

Информацию о лигах в будущем можно будет вынести в отдельный json файл. Эта версия бота будет поддерживать не более 10 вариантов, я явно их записал.

Добавление базы данных

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

Юзер будет выбирать 3 чемпионата для отслеживания, бот сохранит их в БД и использует для запроса результатов.

Кеш(redis) будет сохранять результаты матчей, что бы уменьшить количество запросов к API и ускорить время ответов. Как правило, бесплатные API лимитирует запросы.

# fonlinebot/database.py
import os
import logging
import sqlite3
import redis
import ujson

import config


# класс наследуется от redis.StrictRedis
class Cache(redis.StrictRedis):
    def __init__(self, host, port, password,
                 charset="utf-8",
                 decode_responses=True):
        super(Cache, self).__init__(host, port,
                                    password=password,
                                    charset=charset,
                                    decode_responses=decode_responses)
        logging.info("Redis start")

    def jset(self, name, value, ex=0):
        """функция конвертирует python-объект в Json и сохранит"""
        r = self.get(name)
        if r is None:
            return r
        return ujson.loads(r)

    def jget(self, name):
        """функция возвращает Json и конвертирует в python-объект"""
        return ujson.loads(self.get(name))

Класс Cache наследуется от StrictRedis. Мы добавляем 2 метода jset, jget для сохранения списков и словарей python в хранилище redis. Изначально он не работает с ними.

Теперь добавим класс, который будет создавать базы данных и выполнять функции CRUD.

# fonlinebot/database.py
#...


class Database:
    """ Класс работы с базой данных """
    def __init__(self, name):
        self.name = name
        self._conn = self.connection()
        logging.info("Database connection established")

    def create_db(self):
        connection = sqlite3.connect(f"{self.name}.db")
        logging.info("Database created")
        cursor = connection.cursor()
        cursor.execute('''CREATE TABLE users 
                          (id INTEGER PRIMARY KEY,
                           leagues VARCHAR NOT NULL);''')
        connection.commit()
        cursor.close()

    def connection(self):
        db_path = os.path.join(os.getcwd(), f"{self.name}.db")
        if not os.path.exists(db_path):
            self.create_db()
        return sqlite3.connect(f"{self.name}.db")

    def _execute_query(self, query, select=False):
        cursor = self._conn.cursor()
        cursor.execute(query)
        if select:
            records = cursor.fetchone()
            cursor.close()
            return records
        else:
            self._conn.commit()
        cursor.close()

    async def insert_users(self, user_id: int, leagues: str):
        insert_query = f"""INSERT INTO users (id, leagues)
                                       VALUES ({user_id}, "{leagues}")"""
        self._execute_query(insert_query)
        logging.info(f"Leagues for user {user_id} added")

    async def select_users(self, user_id: int):
        select_query = f"""SELECT leagues from leagues 
                           where id = {user_id}"""
        record = self._execute_query(select_query, select=True)
        return record

    async def update_users(self, user_id: int, leagues: str):
        update_query = f"""Update leagues 
                              set leagues = "{leagues}" where id = {user_id}"""
        self._execute_query(update_query)
        logging.info(f"Leagues for user {user_id} updated")

    async def delete_users(self, user_id: int):
        delete_query = f"""DELETE FROM users WHERE id = {user_id}"""
        self._execute_query(delete_query)
        logging.info(f"User {user_id} deleted")

Sqlite подходит для тестовых проектов. В будущем потребуется переход на внешнюю базу данных и асинхронная работа. Что бы не переписывать всю логику работы с базой, я сразу добавил асинхронный синтаксис.

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

# fonlinebot/database.py
#...


# создание объектов cache и database
cache = Cache(
    host=config.REDIS_HOST,
    port=config.REDIS_PORT,
    password=config.REDIS_PASSWORD
)
database = Database(config.BOT_DB_NAME)

Добавление текстовых сообщений

Для шаблонов сообщений создадим неизменяемый dataclass. Здесь будут все текстовые ответы. А dataclass удобно использовать при вызове аргументов.

# fonlinebot/app/dialogs.py
from dataclasses import dataclass


@dataclass(frozen=True)
class Messages:
    test: str = "Привет {name}. Работаю..."


msg = Messages()

Создание бота

В файле «bot.py» создадим бота. Импортируем зависимости, создадим объект бота и первое сообщение.

# fonlinebot/app/bot.py
from aiogram import Bot, types
from aiogram.contrib.middlewares.logging import LoggingMiddleware
from aiogram.dispatcher import Dispatcher

from config import TOKEN
from app.dialogs import msg
from database import database as db


# стандартный код создания бота
bot = Bot(token=TOKEN)
dp = Dispatcher(bot)
dp.middleware.setup(LoggingMiddleware())


@dp.message_handler()
async def test_message(message: types.Message):
    # имя юзера из настроек Телеграма
    user_name = message.from_user.first_name
    await message.answer(msg.test.format(name=user_name))

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

# fonlinebot/app/bot.py
# ...

async def on_shutdown(dp):
    logging.warning('Shutting down..')
    # закрытие соединения с БД
    db._conn.close()
    logging.warning("DB Connection closed")

Первый запуск бота

Для запуска используем файл «main.py». Импортируем бота и настроим пулинг:

# fonlinebot/main.py
from aiogram import executor

from app import bot


executor.start_polling(bot.dp, 
                       skip_updates=True, 
                       on_shutdown=bot.on_shutdown)

Теперь запустим проект. Если вы в Pycharm, откройте вкладку «Terminal» и введите python main.py. Что бы запустить бота без Pycharm:

  • перейдите в папку проекта,
  • активируйте виртуальное окружение,
  • запустите скрипт (python main.py).
Первый запуск бота

Теперь откроем телеграм и проверим.

Первое сообщение боту

Отлично, работает! Теперь самое время написать несколько базовых тестов, что бы избежать проблем в будущем.

Тестирование бота

В процессе написания проекта код будем изменяться. Что бы избежать появления новых ошибок после размещения на сервере, нужно сразу написать несколько тестов.

Добавьте модуль «test.py» в папку fonlinebot. На этом этапе достаточно 4 теста, вы можете добавить свои.

# fonlinebot/test.py
import unittest
import aiohttp
from unittest import IsolatedAsyncioTestCase

from database import cache, database
from app import bot


class TestDatabase(IsolatedAsyncioTestCase):
    async def test_crud(self):
        await database.insert_users(1111, "1 2 3")
        self.assertEqual(await database.select_users(1111), ('1 2 3',))
        await database.delete_users(1111)
        self.assertEqual(await database.select_users(1111), None)


class TestCache(unittest.TestCase):
    def test_connection(self):
        self.assertTrue(cache.ping())

    def test_response_type(self):
        cache.setex("test_type", 10, "Hello")
        response = cache.get("test_type")
        self.assertEqual(type(response), str)


class TestBot(IsolatedAsyncioTestCase):
    async def test_bot_auth(self):
        bot.bot._session = aiohttp.ClientSession()
        bot_info = await bot.bot.get_me()
        await bot.bot._session.close()

        self.assertEqual(bot_info["username"], "FonlineBOT")


if __name__ == '__main__':
    unittest.main()

Мы проверим CRUD функции базы данных, запишем тестовые данные и удалим. Проверим соединение с redis и ботом.

Запуск теста такой же как и бота, изменяется только имя файла python main.py.

Футбольный телеграм бот на Python (1/4): Подготовка и настройка бота

На скриншоте видно уведомление об ошибке после завершения тестирования. Это известная проблема aiohttp на windows, можно игнорировать.

Ошибки, которые можно встретить

  1. aiogram.utils.exceptions.Unauthorized: Unauthorized — неверный токен бота. Токен нужно сохранить как строку, его структура «цифры:буквы-и-цифры», проверьте.
  2. redis.exceptions.ConnectionError: Error 10061 connecting to ... — redis-server не запущен.
  3. sqlite3.IntegrityError: UNIQUE constraint failed: ... — вы пытаетесь добавить значение в базу данных, которое уже существует.

На этом подготовка проекта окончена. Переходите ко второй части: Написание ядра бота.

Максим
Я создал этот блог в 2018 году, чтобы распространять полезные учебные материалы, документации и уроки на русском. На сайте опубликовано множество статей по основам python и библиотекам, уроков для начинающих и примеров написания программ.
Мои контакты: Почта
admin@pythonru.comAlex Zabrodin2018-10-26OnlinePython, Programming, HTML, CSS, JavaScript