Как создать Telegram-бота на Python + Aiogram

abstract

Пользователь
Закрытый раздел
Сообщения
76
Всем приветик!
Сегодня я постараюсь объяснить Вам основные аспекты создания бота в Telegram на Python с поддержкой библиотеки Aiogram

На какие части будет делиться тема?

Подготовка:
  • Создаём необходимые файлы
  • Импорт модулей и компонентов
Разработка бота:
  • Первая команда, альтернативная замена БД и обычные кнопки
  • Оживляем обычные кнопки
  • Вывод статистики (Inline-кнопки, callback_data)
  • Рассылка пользователям бота (+ отправка фотографии)
  • FSM
Подводим итог:
  • Чему научились?

Создаём необходимые файлы
Создаём папку с названием нашего Бота, в ней три файла: main.py , keyboard.py , config.py
Открываем эти файлы в любом текстовом редакторе ( я использую Sublime, ибо удобно переключаться между файлами + дизайн ).


[IMG]


Импорт модулей и компонентов
Устанавливаем библиотеку с помощью pip в консоли

Код:
pip install -U aiogram

Импортируем необходимое

Код:
Код:
# -*- coding: utf8 -*-
################################################################################################################################
from aiogram import Bot, types
from aiogram.utils import executor
from aiogram.dispatcher import Dispatcher
from aiogram.types import ReplyKeyboardRemove, ReplyKeyboardMarkup, KeyboardButton, InlineKeyboardMarkup, InlineKeyboardButton
import asyncio
#################################################################################################################################
 
######################################################################
from aiogram.dispatcher import FSMContext                            ## ТО, ЧЕГО ВЫ ЖДАЛИ - FSM
from aiogram.dispatcher.filters import Command                        ## ТО, ЧЕГО ВЫ ЖДАЛИ - FSM
from aiogram.contrib.fsm_storage.memory import MemoryStorage        ## ТО, ЧЕГО ВЫ ЖДАЛИ - FSM
from aiogram.dispatcher.filters.state import StatesGroup, State        ## ТО, ЧЕГО ВЫ ЖДАЛИ - FSM
######################################################################
 
######################
import config        ## ИМПОРТИРУЕМ ДАННЫЕ ИЗ ФАЙЛОВ config.py
import keyboard        ## ИМПОРТИРУЕМ ДАННЫЕ ИЗ ФАЙЛОВ keyboard.py
######################
 
import logging # ПРОСТО ВЫВОДИТ В КОНСОЛЬ ИНФОРМАЦИЮ, КОГДА БОТ ЗАПУСТИТСЯ

Подключаем токен нашего Бота

Код:
Код:
storage = MemoryStorage() # FOR FSM
bot = Bot(token=config.botkey, parse_mode=types.ParseMode.HTML)
dp = Dispatcher(bot, storage=storage)
 
logging.basicConfig(format=u'%(filename)s [LINE:%(lineno)d] #%(levelname)-8s [%(asctime)s]  %(message)s',
                    level=logging.INFO,
                    )

Попутно с этим заходим в config.py и прописываем наш ключ с BotFather:

Код:
botkey = '1770592647:AAHrIpW5XW6jYKmB56Kg63r_2LcCK8gOKtg' # ТОКЕН С BOTFATHER

Спойлер: Важно
Не забывайте сохранять все действия в файле CTRL+S

Первая команда, альтернативная замена БД и обычные кнопки
Прелюдии закончились. Пора приступать к сладенькому.
Перед тем как начать с команды старт, надо добавить кнопки, которые будут появляться при вводе команды.
Открываем keyboard.py и пишем такие строки, в начале импортируя библиотеку.

Код:
Код:
from aiogram import Bot, types
from aiogram.types import ReplyKeyboardRemove, ReplyKeyboardMarkup, KeyboardButton, InlineKeyboardMarkup, InlineKeyboardButton
 
######################################################
start = types.ReplyKeyboardMarkup(resize_keyboard=True) # СОЗДАЕМ ВООБЩЕ ОСНОВУ ДЛЯ КНОПОК
 
info = types.KeyboardButton("Информация")            # ДОБАВЛЯЕМ КНОПКУ ИНФОРМАЦИИ
stats = types.KeyboardButton("Статистика")            # ДОБАВЛЯЕМ КНОПКУ СТАТИСТИКИ
 
start.add(stats, info) #ДОБАВЛЯЕМ ИХ В БОТА

Вернулись в main.py
Мы создаём бота, который может собирать статистику и делать рассылку. Значит, для начала, надо научить его записывать id пользователя, который отправил команду /start.


Для этого создаём текстовик user.txt в папке с ботом.

[IMG]
Начнём с команды /start

Код:
Код:
@dp.message_handler(Command("start"), state=None)
 
async def welcome(message):
    joinedFile = open("user.txt","r")
    joinedUsers = set ()
    for line in joinedFile:
        joinedUsers.add(line.strip())
 
    if not str(message.chat.id) in joinedUsers:
        joinedFile = open("user.txt","a")
        joinedFile.write(str(message.chat.id)+ "\n")
        joinedUsers.add(message.chat.id)
 
    await bot.send_message(message.chat.id, f"ПРИВЕТ, *{message.from_user.first_name},* БОТ РАБОТАЕТ", reply_markup=keyboard.start, parse_mode='Markdown')

Что мы сделали?


Чтобы бот работал без остановок после выполнения команд, прописываем в самом конце main.py

Код:
Код:
##############################################################
if __name__ == '__main__':
    print('Монстр пчелы запущен!')                                    # ЧТОБЫ БОТ РАБОТАЛ ВСЕГДА с выводом в начале вашего любого текста
executor.start_polling(dp)
##############################################################

Запускаем бота, через файл main.py.


Если у вас так-же - Вы всё сделали правильно, можно идти к самому боту.

[IMG]

Отправляем команду /start


[IMG]


Всё работает. Смотрим в файлик user.txt. Наш ID записался.

Спойлер: Важно
Кнопки не нажимаются, потому что в них ничего не добавили. Если нажмете на кнопку - выдаст ошибку в консоли ( кнопка никуда не ведёт ), но бот будет работать.
Оживляем обычные кнопки

Попробуем оживить кнопку "Информация" ?
Простенькие строчки, чтобы бот ответил на кнопку.

Код:
Код:
@dp.message_handler(content_types=['text'])
async def get_message(message):
    if message.text == "Информация":
        await bot.send_message(message.chat.id, text = "Информация\nБот создан специально для моих любимых девочек и мальчиков с lzt ", parse_mode='Markdown')

Вывод статистики (Inline-кнопки, callback_data)

Отлично. Кнопку оживили, но запускать пока что не будем. Добавим просмотр статистики, при помощи Inline-кнопки, callback_data, да еще и админку прикрутим (Сразу трёх зайчиков
:stitchsleep:

)
Заходим в keyboard.py и пишем следующее

Код:
Код:
stats = InlineKeyboardMarkup()    # СОЗДАЁМ ОСНОВУ ДЛЯ ИНЛАЙН КНОПКИ
stats.add(InlineKeyboardButton(f'Да', callback_data = 'join')) # СОЗДАЁМ КНОПКУ И КАЛБЭК К НЕЙ
stats.add(InlineKeyboardButton(f'Нет', callback_data = 'cancle')) # СОЗДАЁМ КНОПКУ И КАЛБЭК К НЕЙ

Так же, как мы делали обычные кнопки, но оформлено по эстетики Inline-кнопки

Теперь заходим в config.py
Пишем наш ID для админки

Код:
Код:
botkey = '1770592647:AAHrIpW5XW6jYKmB56Kg63r_2LcCK8gOKtg' # ТОКЕН С BOTFATHER
admin = 1212341234

Возвращаемся в main.py и дополняем код.
Оживляем кнопку статистика.

Код:
Код:
@dp.message_handler(content_types=['text'])
async def get_message(message):
    if message.text == "Информация":
        await bot.send_message(message.chat.id, text = "Информация\nБот создан специально для моих любимых девочек и мальчиков с lzt ", parse_mode='Markdown')
 
 
    if message.text == "Статистика":
        await bot.send_message(message.chat.id, text = "Хочешь просмотреть статистику бота?", reply_markup=keyboard.stats, parse_mode='Markdown')

Далее прописываем "Ловлю callback" и что должно при этом выполняться.
Сделаем фичу, чтобы при нажатии на "да" Бот присылал не новое сообщение, а редактировал старое (выглядит более эстетично), выводя при этом количество пользователей бота.
Нам надо посчитать все строчки ( ID ) с текстовика user.txt и вывести их количество.

Всё это выглядит вот так

Код:
Код:
@dp.callback_query_handler(text_contains='join') # МЫ ПРОПИСЫВАЛИ В КНОПКАХ КАЛЛБЭК "JOIN" ЗНАЧИТ И ТУТ МЫ ЛОВИМ "JOIN"
async def join(call: types.CallbackQuery):
    if call.message.chat.id == config.admin:
        d = sum(1 for line in open('user.txt'))
        await bot.edit_message_text(chat_id=call.message.chat.id, message_id=call.message.message_id, text=f'Вот статистика бота: *{d}* человек', parse_mode='Markdown')
    else:
        await bot.edit_message_text(chat_id=call.message.chat.id, message_id=call.message.message_id, text = "У тебя нет админки\n Куда ты полез", parse_mode='Markdown')
 
 
 
@dp.callback_query_handler(text_contains='cancle') # МЫ ПРОПИСЫВАЛИ В КНОПКАХ КАЛЛБЭК "cancle" ЗНАЧИТ И ТУТ МЫ ЛОВИМ "cancle"
async def cancle(call: types.CallbackQuery):
    await bot.edit_message_text(chat_id=call.message.chat.id, message_id=call.message.message_id, text= "Ты вернулся В главное меню. Жми опять кнопки", parse_mode='Markdown')

Спойлер: Зачем "F" перед кавычками?
У нас есть значение d которое содержит в себе количество строк. f-строки вступают в дело, чтобы помочь с форматированием.
Грубо говоря без f нашу несчастную d посчитают за текст.


[IMG]


[IMG]


[IMG]


[IMG]


Рассылка пользователям бота (+ отправка фотографии)

Делаем рассылку пользователям. Решил сделать так, чтобы с текстом отправлялась еще и фотка.
Скидываем фотку в нашу папку с ботом. Переименовываем её во что-то красивое (у меня это lzt.png)

Код:
Код:
@dp.message_handler(commands=['rassilka'])
async def rassilka(message):
    if message.chat.id == config.admin:
        await bot.send_message(message.chat.id, f"*Рассылка началась \nБот оповестит когда рассылку закончит*", parse_mode='Markdown')
        receive_users, block_users = 0, 0
        joinedFile = open ("user.txt", "r")
        jionedUsers = set ()
        for line in joinedFile:
            jionedUsers.add(line.strip())
        joinedFile.close()
        for user in jionedUsers:
            try:
                await bot.send_photo(user, open('lzt.jpg', 'rb'), message.text[message.text.find(' '):])
                receive_users += 1
            except:
                block_users += 1
            await asyncio.sleep(0.4)
        await bot.send_message(message.chat.id, f"*Рассылка была завершена *\n"
                                                              f"получили сообщение: *{receive_users}*\n"
                                                              f"заблокировали бота: *{block_users}*", parse_mode='Markdown')

Спойлер: Важно
Вставляем этот код сразу же после хэндлера со стартом.
Проверяйте какого фотка типа. У меня это jpg, поэтому я и пишу в коде .jpg

Коротко про код :



Запускаем бота и проверяем.


[IMG]
Отлично. Всё чики-пуки
:kitty:



FSM

Спойлер
FSM. О дааа. Самое вкусное сейчас будет. Сколько нервов было потрачено, чтобы понять как это работает
:stitchsad:



Приступим.
Создаём наши "БД" два файла в папку с ботом: link.txt и text.txt в них будут храниться наши ответы.
Переходим в main.py Я поместил код для этого выше старта, ибо всё, что связанно с FMS и State лучше писать в начале, дабы посреди основного кода он не отвлекал)

Прописываем State-группу. Называем её meinfo.

Спойлер
Q1 и Q2 что это?
Объяснить сейчас я это не могу, но по ходу процесса всё станет ясно (прописываем их столько, сколько у нас будет вопросов. В данном случае 2 ( бот просит ссылку и текст )

Код:
Код:
class meinfo(StatesGroup):
    Q1 = State()
    Q2 = State()

Теперь пишем, когда же бот начнет слушать наши ответы.

Код:
Код:
class meinfo(StatesGroup):
    Q1 = State()
    Q2 = State()
 
@dp.message_handler(Command("me"), state=None)        # Создаем команду /me для админа.
async def enter_meinfo(message: types.Message):
    if message.chat.id == config.admin:               
        await message.answer("начинаем настройку.\n"        # Бот спрашивает ссылку
                         "№1 Введите линк на ваш профиль")
 
        await meinfo.Q1.set()                                    # и начинает ждать наш ответ.
 
@dp.message_handler(state=meinfo.Q1)                                # Как только бот получит ответ, вот это выполнится
async def answer_q1(message: types.Message, state: FSMContext):
    answer = message.text
    await state.update_data(answer1=answer)                            # тут же он записывает наш ответ (наш линк)
 
    await message.answer("Линк сохранён. \n"
                         "№2 Введите текст.")
    await meinfo.Q2.set()                                    # дальше ждёт пока мы введем текст
 
 
@dp.message_handler(state=meinfo.Q2)                    # Текст пришел а значит переходим к этому шагу
async def answer_q1(message: types.Message, state: FSMContext):
    answer = message.text
    await state.update_data(answer2=answer)                # опять же он записывает второй ответ
 
    await message.answer("Текст сохранён.")
 
    data = await state.get_data()                #
    answer1 = data.get("answer1")                # тут он сует ответы в переменную, чтобы сохранить их в "БД" и вывести в след. сообщении
    answer2 = data.get("answer2")                #
 
    joinedFile = open("link.txt","w", encoding="utf-8")        # Вносим в "БД" encoding="utf-8" НУЖЕН ДЛЯ ТОГО, ЧТОБЫ ЗАПИСЫВАЛИСЬ СМАЙЛИКИ
    joinedFile.write(str(answer1))
    joinedFile = open("text.txt","w", encoding="utf-8")        # Вносим в "БД" encoding="utf-8" НУЖЕН ДЛЯ ТОГО, ЧТОБЫ ЗАПИСЫВАЛИСЬ СМАЙЛИКИ
    joinedFile.write(str(answer2))
 
    await message.answer(f'Ваша ссылка на профиль : {answer1}\nВаш текст:\n{answer2}')    # Ну и выводим линк с текстом который бот записал
 
    await state.finish()

Код:
Код:
Уф... Как сложно...
Запускаем проверяем.
Вводим команду /me

[IMG]
Всё работает и сохранилось в нашу "БД" текстовики link.txt и text.txt
Теперь создаём кнопку "Разработчик"
В keyboard.py добавляем к основным кнопкам еще одну.
Я ее не записал через запятую, а опустил ниже, чтобы она была одна снизу двух других кнопок.

Код:
Код:
from aiogram import Bot, types
from aiogram.types import ReplyKeyboardRemove, ReplyKeyboardMarkup, KeyboardButton, InlineKeyboardMarkup, InlineKeyboardButton
 
######################################################
start = types.ReplyKeyboardMarkup(resize_keyboard=True) # СОЗДАЕМ ВООБЩЕ ОСНОВУ ДЛЯ КНОПОК
 
info = types.KeyboardButton("Информация")            # ДОБАВЛЯЕМ КНОПКУ ИНФОРМАЦИИ
stats = types.KeyboardButton("Статистика")            # ДОБАВЛЯЕМ КНОПКУ СТАТИСТИКИ
razrab = types.KeyboardButton("Разработчик")            # ДОБАВЛЯЕМ КНОПКУ РАЗРАБОТЧИК
 
start.add(stats, info) #ДОБАВЛЯЕМ ИХ В БОТА
start.add(razrab)
######################################################
######################################################
 
stats = InlineKeyboardMarkup()    # СОЗДАЁМ ОСНОВУ ДЛЯ ИНЛАЙН КНОПКИ
stats.add(InlineKeyboardButton(f'Да', callback_data = 'join')) # СОЗДАЁМ КНОПКУ И КАЛБЭК К НЕЙ
stats.add(InlineKeyboardButton(f'Нет', callback_data = 'cancle')) # СОЗДАЁМ КНОПКУ И КАЛБЭК К НЕЙ
 
######################################################

В main.py, где @dp.message_handler(content_types=['text'])
Прописываем следующее

Код:
Код:
@dp.message_handler(content_types=['text'])
async def get_message(message):
    if message.text == "Информация":
        await bot.send_message(message.chat.id, text = "Информация\nБот создан специально для моих любимых девочек и мальчиков с lzt ", parse_mode='Markdown')
 
 
    if message.text == "Статистика":
        await bot.send_message(message.chat.id, text = "Хочешь просмотреть статистику бота?", reply_markup=keyboard.stats, parse_mode='Markdown')
 
    if message.text == "Разработчик":
        link1 = open('link.txt', encoding="utf-8") # Вытаскиваем с нашей "БД" инфу, помещаем в переменную и выводим её
        link = link1.read()
 
        text1 = open('text.txt', encoding="utf-8") # Вытаскиваем с нашей "БД" инфу, помещаем в переменную и выводим её
        text = text1.read()
 
        await bot.send_message(message.chat.id, text = f"Создатель: {link}\n{text}", parse_mode='Markdown')

Запускаем, проверяем)

Спойлер: Важно
Прописываем /start заново, чтобы новая кнопка появилась

Отлично. Всё работает.
Если сменить опять данные через /me, то при нажатии на "Разработчик" будут уже другие ваши текст и ссылка.


Чему научились?

Подводим итог :
  • Мы научились создавать как обычные, так и inline кнопки.
  • Собирать статистику ( Подсчитывать пользователей в боте )
  • Подключать админку
  • Разобрались худо-бедно в callback-data
  • Бот теперь умеет редактировать свои сообщения
  • Победили FSM

Всем спасибо за просмотр моей статьи. Я надеюсь, что эта тема стала, пускай хоть и немного, но полезной для кого нибудь.
Всех люблю

:stitch:

 
Назад
Сверху