Community assistant - это Telegram-бот для упрощения администрирования групп.
Ссылка на запущенный инстанс: https://t.me/cm_assistant_bot
В больших группах (да и в не очень больших) сообщения о присоединяющихся участниках иногда начинают надоедать.
Cm assistant, добавленный в группу, может автоматически удалять такие сообщения, и они не будут засорять историю переписки.
Также бот может удалять сообщения о выходящих участниках.
Данный функционал управляется независимыми флажками, вы можете включить только удаление джойнов (сообщения о вошедших участниках), только удаление ливов (сообщения о вышедших участников) или обе эти функции.
Бот обладает собственной системой ролей пользователей.
Он может находиться в нескольких группах одновременно, и в каждой группе у одного и того же пользователя может быть собственная роль
Обычный пользователь, имеет доступ только к настройкам группы как обычный участник.
Может тоже самое, что и обычный пользователь, а также обладает доступом к админке группы.
Название роли умышленно сокращено, чтобы минимизировать путаницу с администратором группы на уровне telegram.
Т.е. если используется термин "админ", речь идёт о роли пользователя в группе на уровне бота, а термин "администратор" о роли пользователя в группе на уровне telegram.
Может тоже самое, что и модератор, а также может назначать / смещать админов и модераторов.
Админ группы (тот, кто добавил бота в группу следуя его инструкциям), как сказано выше, может назначать других админов / модераторов.
Причём выдать роль в группе можно даже пользователю, который не является её участником.
То есть, пользователю достаточно иметь аккаунт в telegram, и обладать ролью админа или модератора, чтобы администрировать группу средствами Cm assistant.
Это может быть полезно, например, если не хочется давать пользователю доступ к переписке, но позволять просматривать и анализировать статистику или делать рассылки (см. ниже).
Бот собирает некоторую статистику.
Сейчас в неё входят
- Сообщения (id пользователя и время отправки)
- Джойны (id пользователя и время входа)
- Ливы (id пользователя и время выхода)
У модераторов имеется возможность просматривать статистику за желаемый период, начало и конец которого настраивается с точностью до секунды.
- python 3.10
- alembic
- asyncio
- pyrogram
- sqlalchemy
Склонируйте репозиторий.
Скопируйте .env.template в .env.
Откройте .env в текстовом редакторе и подставьте необходимые значения.
Следующие инструкции выполняйте из раздела, соответствующего вашей ОС.
Создайте и активируйте виртуальное окружение:
python3 -m venv env
source env/bin/activate
Установите зависимости:
pip install --upgrade pip
pip install -r requirements.txt
Примените миграции: alembic upgrade head
Запустите проект (рекомендуется сделать это в фоне, для чего можно использовать, например, сервис для systemd):
python3 cm_assistant.py
Запуск на windows возможен, но не рекомендован, если, конечно, вы не запускаете его на сервере с Windows, так как, вероятно, вы не сможете обеспечить круглосуточное функционирование бота.
Однако, в целях отладки / разработки, запускать бота можно и на собственной машине с Windows, он должен функционировать корректно.
Создайте и активируйте виртуальное окружение:
python -m venv env
env\scripts\activate
pip install --upgrade pip
pip install -r requirements.txt
Примените миграции: alembic upgrade head
Запустите проект:
python cm_assistant.py
Бот обрабатывает сигнал SIGINT (ctrl+c), корректно закрывает соединения с telegram и базой данных и завершается.
Я проходил курс Python-разработчик от Яндекс.Практикум, и мне предложили сделать небольшой проект.
Нужно было разработать telegram-бота для кураторов Практикума, который мог бы собирать статистику в группах выпускников.
А у меня как раз практически не было опыта в разработке ботов, и мне не нравились существующие решения.
По сути, существует куча фреймворков / библиотек для работы с telegram, но нет чего-то более высокоуровнего, именно для создания ботов.
Существует надстройка над aiogram, aiogram-dialog, в которой реализовано примерно то, чего мне хотелось, но во-первых для разработки изначально был выбран pyrogram, а во-вторых было необходимо работать с реляционной бд, поэтому aiogram-dialog точно не подходил (он хранит данные в хранилище aiogram, которое не умеет записывать в реляционные базы).
Поэтому мной была сделана попытка написать своё ядро-надстройку, но уже над pyrogram и sqlalchemy.
Красиво записать в реляционную базу информацию о различных кнопочках, вкладках и окнах - не очень, как оказалось, простая задача, ну или моего опыта не хватило на что-нибудь более изящное.
Поэтому прошу не судить строго за происходящее в tgbot/gui, и, если есть идеи того, как сделать лучше, не стесняйтесь ими делиться!
В процессе разработки стало ясно, что бот вполне может использоваться не только для помощи кураторам Практикума, поэтому было решено сделать его публичным и открыть доступ к нему для всех пользователей Telegram.
- Реализовать возможность создавать массовые рассылки участникам группы.
- При повышении нагрузки переключиться с sqlite на postgresql.
- Отрефакторить код по PEP8.
Перед началом разработки было принято решение написать потенциально переиспользуемое ядро поверх pyrogram, которое будет развиваться параллельно с Cm assistant.
Позже, вероятно, ядро будет отделено и размещено в отдельном репозитории.
На данный момент оно находится в текущем репозитории в пакете tgbot, ниже следует его описание.
- Создание class-based ботов
- Имеются удобные абстракции для создания интерфейсов на основе инлайн-клавиатур.
- Задание дефолтного
chat_id
для отправки сообщений - Задание глобального фильтра (например только текстовые сообщения; только голосовые и команда /start и т.п.)
- Использование SQLAlchemy для каких либо нужд
- Обработка и логирование ошибок вызовов методов pyrogram.Client, обработчиков событий и обработчиков инлайн-кнопок, а также возможность перенаправления логов в чат(ы) telegram
- Опциональная неблокирующая отправка сообщений.
- Удобный метод для создания задач (аналог
asyncio.create_task
) с последующим мониторингом задачи и логированием исключений
import asyncio
from pyrogram import filters
from tgbot import BotController
from tgbot.handler_decorators import on_message
CHAT_ID = ...
class Controller(BotController):
def __init__(self):
super().__init__(bot_name='test_bot')
def get_global_filter(self):
return filters.text & filters.chat(CHAT_ID)
def get_default_chat_id(self):
return CHAT_ID
@on_message()
async def test_handler(self, message):
await message.reply('working')
if __name__ == '__main__':
controller = Controller()
asyncio.run(controller.start())
Ядро построено по тому же принципу, что и pyrogram.
Имеется один главный класс, у pyrogram это Client, а в tgbot это Controller.
Главный класс наследуется от нескольких классов-миксинов.
Каждый миксин содержит свой не пересекающийся набор методов, и, в итоге, из них собирается основной класс, имеющий все эти методы.
Плюс такой архитектуры в том, что можно удобно хранить различные компоненты в отдельных модулях.
А минус - трудности с переопределением конструктора, потому что приходится держать в голове MRO, и не забывать вызывать super().__init__
в нужных местах.
Для создания собственного бота необходимо унаследоваться от главного класса tgbot.BotController, и реализовывать всю логику внутри этого класса.
Если кодовая база в процессе разработки станет слишком большой, возможно прибегнуть к подходу, описанному выше, и разделить логику на несколько миксинов, разнесённых по разным модулям.
Для запуска следует создать единственный инстанс контроллера и выполнить его асинхронный метод start.
Ядро переопределяет или дополняет некоторые компоненты pyrogram, ниже описаны некоторые такие дополнения.
В tgbot реализован собственный набор декораторов, находящийся в tgbot.handler_decorators
.
Наименования соответствуют декоратором вида on_*
из класса pyrogram.Client
.
Так как tgbot предназначен для создания class-based ботов, декораторы из tgbot.handler_decorators
специально заточены под такое использование, и должны применяться исключительно к методам класса контроллера.
Инстанс клиента доступен в self.app, потому, в отличии от pyrogram, в обработчики инстанс клиента не передаётся.
Для более удобного контроля обновлений был частично переписан диспетчер из pyrogram.dispatcher.
В диспетчер поверх групп была добавлена ещё одна абстракция: категории.
Количество категорий фиксировано, на данный момент их 5.
- INITIALIZE, обработчики этой категории выполняются в первую очередь.
- MAIN, обработчики этой категории выполняются после успешного выполнения всех обработчиков из INITIALIZE.
- RESTORE, обработчики этой категории запускаются, если произошло исключение в одном из обработчиков MAIN.
- FINISH, обработчики этой категории выполняются, если все обработчики из MAIN были успешно выполнены.
- FINALIZE, обработчики этой категории выполняются после FINISH / RESTORE, или после исключения в одном из обработчиков в INITIALIZE.
При добавлении обработчиков по умолчанию используется категория main, при необходимости можно задать требуемую категорию параметром category.
В качестве значения для category следует использовать одно из значений перечисления tgbot.wrappers.dispatcher.Category.
Параметр group действует также, как и в чистом pyrogram, но не глобально, а внутри своей категории.
Помимо этого, в отличии от pyrogram, где исключение в одном из обработчиков не прекращает распространение обновления, диспетчер из tgbot останавливает обработку как в текущей, так и в других группах активной категории, и переключается на другую категорию как описано выше.
Для работы с сообщениями в tgbot добавлен обработчик, который позволяет удобно отправлять сообщения не превышая лимиты telegram.
Чтобы его использовать, нужно вызывать не self.app.send_message
, а self.send_message
(т.е. метод контроллера).
Этот метод позволяет отправлять сообщения с приоритетом от 1 до 3 (1 - самые важные, 3 - наименее важные).
- Сообщения разработчикам бота (тем, id которых перечислены в переменной окружения
DEV_IDS
). - Сообщения пользователям и группам (стандартный приоритет.
- Массовые рассылочные сообщения (возможность их отправки пока не реализована).
Отправка этим методом по умолчанию не блокирующая, однако, передав аргумент blocking=True, можно получить результат, похожий на результат вызова pyrogram.Client.send_message
.
Почти во все методы pyrogram.Client можно передавать дополнительные аргументы, ограничивающие количество попыток выполнения и позволяющие задавать очерёдность вызовов (для подробностей смотрите описание в разделе "Вызовы методов клиента pyrogram")
Также в tgbot улучшена обработка исключений.
-
Логируются все
FloodWait
-исключения (с дублированием в telegram чаты, перечисленные вDEV_IDS
), что позволяет быстро отреагировать на превышенные лимиты и исправить проблему. -
При возникновении исключений в обработчиках логируется не только исключение, но и traceback.
Пока этот раздел написан с учётом того, что ядро вы будете использовать из текущего репозитория, позже, когда оно будет отделено, данный раздел будет переписан.
Скопируйте alembic, tgbot, .env.template, .gitignore, alembic.ini и requirements.txt в ваш проект.
Затем удалите содержимое alembic/versions.
Создайте файл tables.py и импортируйте в него Base из tgbot.db.tables
from tgbot.db.tables import Base
Это обязательно, даже если вы не планируете создавать собственные таблицы, так как alembic будет пытаться импортировать Base из tables.
Скопируйте / переименуйте .env.template в .env и заполните его.
API_ID
и API_HASH
возьмите на my.telegram.org.
BOT_TOKEN
получите у BotFather.
DB_URL
будет передан в engine sqlalchemy
, его нужно указать в соответствии с документацией данной библиотеке.
По умолчанию в шаблоне указан url для sqlite, вероятно, для разработки вам будет этого достаточно.
В DEV_IDS
вы можете задать идентификаторы чатов / пользователей telegram, которым бот будет отправлять отладочные сообщения.
Внутри tgbot для загрузки переменных окружения используется dotenv, поэтому вы можете дописать в .env требуемые вам переменные (api-ключ погодного сервиса, токен от чего-нибудь и т.п.).
А затем получить содержимое этой переменной средствами модуля os.
Для управления миграциями используется alembic
После первоначальной подготовки проекта создайте и примените миграции:
alembic revision --autogenerate
alembic upgrade head
В дальнейшем, при внесении изменений в метаданные (добавления / изменения таблиц, столбцов и т.п.) используйте alembic как обычно, основываясь на его документации.
Создайте главный файл приложения, дайте ему желаемое название.
Импортируйте BotController, унаследуйтесь от него, и реализуйте логику бота в этом классе (пример см. выше).
В конце файла пропишите стандартную для Python проверку if __name__ == '__main__'
, и запустите приложение.
Импортируйте декораторы из tgbot.handler_decorators
, и используйте также, как и в чистом pyrogram
.
Помните, что tgbot
не передаёт client первым аргументом в обработчик, так как он имеется в BotController.app
.
Также при использовании обработчиков вы можете передавать в декоратор категорию, подробнее эта абстракция описано выше в разделе "Диспетчер".
Для on_message
обработчиков можно применить глобальный фильтр.
Для этого переопределите метод контроллера get_global_filter
.
Он должен вернуть pyrogram фильтр, который позже будет объединён с фильтрами, переданными в on_message
.
Например, если ваш бот должен принимать только текстовые сообщения, из get_global_filter
вы можете вернуть pyrogram.filters.text
.
Если у вас много обработчиков, для которых нужны разные группы, вы можете задействовать group_manager
.
from tgbot.group_manager import group_manager
Теперь нужно добавить группы, для этого используются методы add_left_group
и add_right_group
.
Они принимают название группы, оно должно соответствовать правилам наименования переменных в Python, чтобы позже не возникло проблем с получением атрибутов.
Внутри group_manager
имеется два счётчика, для левых групп он начинается с -1000 и увеличивается при добавлении группы, а для правых счётчик начинается с 1000 и уменьшается при добавлении группы.
Таким образом, при добавлении двух левых групп для них будут сгенерированы значения -1000 и -999, а для правых 1000 и 999.
После добавления названия приводятся к верхнему регистру и становятся атрибутами group_manager
.
Допустим, если вы добавите левую группу init_a
, правую группу close_a
, левую группу init_b
, и правую группу close_b
, то обработчики с этими группами будут выполняться в следующем порядке.
init_a (-1000), init_b (-999), close_b (999), close_a (1000).
На самом деле, вы не получите именно таких значений, потому что некоторые группы добавляются tgbot для внутренних нужд, но смысл от этого не меняется.
Чтобы использовать добавленные группы, просто присвойте значения параметру group нужного декоратора
@on_message(filters.text, group=group_manager.INIT_A)
def your_handler(self, message):
...
Названия групп приводятся к верхнему регистру, чтобы использование group_manager
было похоже на использование перечисления, которым, в какой-то степени, group_manager
и является.
Возможно, позже это поведение будет пересмотрено и изменено.
- app - инстанс pyrogram.Client.
- log - стандартный логер из библиотеки logging.
- session - sqlalchemy sessionmaker, используйте с осторожностью, старайтесь соблюдать рекомендации, даваемые в документации.
Для работы с базой реализован следующий механизм.
- При начале обработке обновления в категории INITIALIZE создаётся sqlalchemy session, который устанавливается в контекстную переменную.
- В категории restore выполняется rollback.
- В категории finish выполняется commit.
- В категории finalize сессия закрывается.
Контекстная переменная находится в tgbot.db, вам нужно импортировать её, чтобы работать с базой.
На самом деле, это обёртка с контекстной переменной, поэтому вам не нужно использовать get, а обращаться к ней напрямую, будто это обычный объект.
from tgbot.db import db
...
@on_message(filters.command('create'))
def on_create(self, message):
# Необходимые вам вычисления / преобразования
data = message.text[:42]
length = len(message.text)
item = Item(data=data, length=length)
db.add(item)
При разработке ядра была реализована абстракция, позволяющая удобно создавать интерфейсы на основе инлайн клавиатур и взаимодействовать с ними.
Основные концепции:
- Интерфейс находится внутри окна.
- Одно окно - одно сообщение.
- Внутри окна может находиться неограниченное количество вкладок.
- Вкладки можно переключать в любом порядке.
- У каждой вкладки есть собственный текст и набор кнопок.
Примеры использования можете посмотреть в данном репозитории в директории gui.
Некоторая информация также имеется в разделе Описание пакета -> gui.
Позже этот раздел может быть дополнен.
Для более удобной отправки сообщений вы можете использовать метод send_message
.
Однако, если вам это не подходит, вы всё ещё можете задействовать send_message
из pyrogram (он находится в app).
Этот метод позволяет отправлять сообщения в фоне, не блокируя код, отправляющий сообщения.
При отправки нескольких сообщений в 1 чат они отправляются в верном порядке (race condition-ы не происходят).
Также, если бот предназначен для использования только в одном чате, можно переопределить метод get_default_chat_id
.
Этот метод должен вернуть id чата, который следует использовать по умолчанию.
Из-за этого подхода порядок аргументов send_message
был изменён, первым аргументом нужно передавать текст, и только вторым, опционально, id чата.
Описание метода можно найти ниже в разделе "Описание пакета".
tgbot автоматически сохраняет в базу пользователей, с которыми встречается.
Если очередь дошла для вашего обработчика из категории main, и если пользователь не анонимен, значит у вас уже есть к нему доступ.
Пользователь записывается в контекстную переменную current_user
, находящуюся в tgbot.users
.
Также как и с сессией базы данных, можно напрямую обращаться к атрибутам этого объекта.
Он представляет собой sqlalchemy строку инстанс таблицы (tgbot.db.tables.User).
Если эта таблица не была переопределена при инициализации контроллера, иначе, это будет инстанс переопределяющей таблицы.
К ней добавляется атрибут pyrogram_user
, значением которого становится исходный объект пользователя от pyrogram.
Если нужен id пользователя в telegram, можете использовать current_user.user_id
(это короче, чем current_user.pyrogram_user.id
).
Однако, если нужно, например, получить имя пользователя, используйте конструкцию current_user.pyrogram_user.username
(эта информация не заносится в базу).
Помимо user_id
в таблице пользователя есть столбец id - это локальный идентификатор в базе данных, также являющийся первичным ключом.
Этот модуль содержит миксин с методами для работы с базой данных и некоторые функции.
Контекстное хранилище sqlalchemy сессии в базе данных.
Используйте его для работы с базой в обработчиках.
Все методы миксина не предназначены для вызова из внешнего кода.
Декоратор методов, устанавливающий сессию в db.db, вызывающий обёрнутый метод и закрывающий сессию после завершения выполнения.
При успехе делает commit, при исключении вызывает rollback.
Параметры:
wait_for_commit=False
- если True, будет ждать коммита в session из db, прежде чем заменить его на новый.
При wait_for_commit=True
также отслеживается и rollback, и, если он происходит, вызов обёрнутого метода не выполняется.
Так как, если для выполнения обёрнутого метода требуется коммит в активной сессии, вероятно, продолжение после rollback-а может навредить ещё больше.
Содержит Base для создания sqlalchemy классов-таблиц, а также таблицы, используемые внутри tgbot.
Содержит импорты перечислений.
Импортируется из wrappers.dispatcher, содержит следующие члены.
- INITIALIZE
- MAIN
- RESTORE
- FINISH
- FINALIZE
Содержит различные компоненты для создания и управления интерфейсами.
Класс для создания окон, его нужно переопределять, для создания собственных окон.
В переопределённом классе нужно создать список tabs, в котором перечислить классы вкладок.
Параметры:
- controller - инстанс контроллера
chat_id
- id чата, в котором будет создано окноuser_id=None
- id пользователя, который может управлять окном (если не задано, окном смогут управлять все пользователи в чате).
Позволяет создать окно
Параметры:
- *args - позиционные аргументы, которые будут переданы в build вкладки
- tab=None - вкладка, которую нужно показать (по умолчанию это самая первая вкладка из tabs).
- **kwargs - keyword-аргументы, которые будут переданы в build вкладки
Перепривязывает окно к сессии из db.db
Метод полезен, если окно было передано в фоновую задачу, а обработка обновления уже завершилась и сессия была закрыта.
Конечно, сессию можно продолжать использовать после закрытия, но в таком случае нужно самостоятельно делать коммит / rollback после завершения всех манипуляций, что не соответствует рекомендациям из документации sqlalchemy.
Поэтому, если внутри фоновой задачи необходимо использовать окно (например для периодических обновлений), то метод, который будет выполняться фоном (внутри асинхронной задачи) следует обернуть в with_db
с wait_for_commit=True
, а внутри метода выполнить window.rebind
, чтобы добавить все необходимые инстансы sqlalchemy-таблиц в текущую сессию.
Можно использовать при получении и обработке пользовательского ввода.
Если перед рендером окна был вызван этот метод, то сообщение с окном будет удалено и отправлено заново вместо редактирования, из-за чего сообщение от пользователя окажется выше в истории.
Рендерит сообщение.
Для новых окон сообщение отправляется, для уже созданных редактируется.
Переключает вкладку.
Параметры:
new_tab
- класс вкладки, на которую нужно переключиться.- *args - позиционные аргументы, которые будут переданы в build вкладки
save_current_tab=False
- если True, текущая вкладка будет сохранена в базу, иначе текущая вкладка будет уничтожена.- **kwargs - keyword-аргументы, которые будут переданы в build вкладки
Если выполняется switch_tab
на вкладку, которая ранее была сохранена (save_current_tab=True
), то *args и **kwargs не будут переданы в build, так как build вызывается только у создаваемых вкладок.
Класс для управления клавиатурами.
Не должен создаваться внешним кодом, инстанс будет доступен в атрибуте keyboard вкладки.
Добавляет строку кнопок.
Параметры:
- *buttons - позиционные аргументы-кнопки
Добавляет кнопку.
Параметры:
- button - добавляемая кнопка
Если в клавиатуре нет ни одной строки, она будет создана, иначе кнопка будет добавлена в последнюю строку.
Позволяет удалить кнопки по имени.
Параметры:
- name - имя кнопки, все кнопки с таким именем будут удалены.
Класс стандартной клавиатуры, собственных атрибутов и методов не имеет.
Базовый класс для создания кнопок, не должен использоваться внешним кодом напрямую.
Допускается наследование и реализация собственных кнопок.
Параметры:
- text=None - текст кнопки
- *args - позиционные аргументы, которые будут переданы в класс-таблицу кнопки
- row=None - инстанс класса-таблицы sqlalchemy
- **kwargs - keyword-аргументы, которые будут переданы в класс-таблицу кнопки
Если был передан row, то он будет присвоен в self.row, иначе в self.row будет присвоен инстанс таблицы, находящийся в self.table.
Во втором случае *args и **kwargs будут переданы в конструктор таблицы.
Для создания собственных кнопок нужно унаследоваться от этого класса и задать в нём атрибут table, значением которого должна быть sqlalchemy-таблица.
Таблицу будет удобно создать используя миксин (см. ниже).
Устанавливает текст кнопки
Параметры:
- text - новый текст
Вызывается при получении callback_query
, этот метод необходимо переопределить при реализации собственной кнопки.
Параметры:
row_index
- индекс строки, в которой находится кнопкаcolumn_index
- индекс столбца, в котором находится кнопка (индекс кнопки в строке)
Индексы строки и столбца могут быть полезны, когда при нажатии кнопки должны быть изменены другие кнопки в клавиатуре (в tgbot пока таких кнопок не реализовано).
Рендерит кнопку, добавляя row в db.
Вызывается из render, вам нужно будет вызвать этот метод, если будете переопределять render.
Возвращает pyrogram.types.InlineKeyboardButton.
Используется для описания полей ввода.
Инстансы перечисляются в списке input_fields
в классе вкладки.
Параметры:
- name - имя поля, используется для переключения между полями
- text=None - текст сообщения, который будет установлен при активации этого поля.
method_name=None
- имя метода вкладки, которое будет использовано для обработки введённых данных (по умолчанию'process_' + name
).
Например, для поля с именем phone, при обработке ввода tgbot попытается вызвать метод process_phone
.
Абстракция для работы с текстом сообщений, не должна использоваться внешним кодом (используйте классы-наследники).
Устанавливает заголовок сообщения.
Он будет отображаться в самом начале, после него будет следовать линия из дефисов.
Параметры:
- header - текст заголовка
one_time=True
- показывать заголовок только 1 раз (если True, заголовок исчезнет при следующем рендере окна)
Устанавливает тело сообщения.
Параметры:
- body - тело сообщения
Класс для работы с текстом
Базовый класс для создания вкладок.
Не должен напрямую использоваться внешним кодом, возможно наследование.
При наследовании нужно задать атрибут table с sqlalchemy-таблицей.
Также как и для кнопок, для вкладок существует миксин.
В классе вкладки используются следующие атрибуты:
text_class
задаёт класс для обработки текста, по умолчаниюgui.Text
.keyboard_class
задаёт класс для обработки клавиатуры, по умолчаниюgui.SimpleKeyboard
.input_fields
может содержать список инстансов gui.InputField для обработки пользовательского ввода.rerender_text
нужно ли повторно рендерить текст, если True (по умолчанию), текст будет рендериться всегда, если False, текст будет рендериться только в первый раз, при повторных рендерах будет переиспользоваться текст сообщения.
Должен вернуть инстанс текста, по умолчанию возвращается self.text_class(self)
Должен вернуть словарь, которым будет отформатирован текст сообщения, по умолчанию возвращает {}.
Должен вернуть инстанс клавиатуры, по умолчанию возвращается self.keyboard_class(self)
Выполняет создание вкладки.
Этот метод нужно переопределять во вкладках, вызывать super().build и реализовывать далее логику для создания интерфейса (устанавливать текст, добавлять кнопки и т.п.).
Параметры:
- *args - позиционные аргументы, которые будут переданы в класс-таблицу вкладки
- **kwargs - keyword-аргументы, которые будут переданы в класс-таблицу вкладки
Переключает поле ввода
Параметры:
field_name
- имя поля, на которое нужно переключиться- previous=False - если True и имя поля не задано, переключится на предыдущее поле
- next=True - если True и имя поля не задано, переключится на следующее поле (поведение метода по умолчанию, без аргументов)
Класс вкладки, готовый для использования
Миксины для sqlalchemy-таблиц.
Миксин для создания таблиц-вкладок.
Наследуйте класс-таблицу от Base и от данного миксина, чтобы создавать собственные таблицы для вкладок.
Миксин для создания кнопок, наследуйте класс-таблицу от Base и от данного миксина, чтобы создавать собственные таблицы для кнопок.
Один из полезных столбцов - это name, который вы можете передать в конструктор кнопки.
Позже вы сможете удалить кнопку с нужным именем, используя метод клавиатуры remove_buttons_by_name
.
Миксин для кнопок с callback-ами. Добавляет к базовому миксину столбец callback_name
.
Скорее всего вам не нужно использовать его явно, так как имеющиеся классы кнопок могут обрабатывать callable-объекты и сами получать из них значение для callback_name
.
Миксин для создания собственных кнопок-флажков.
Имеет следующие полезные столбцы:
is_checked
- отмечен ли флажок, по умолчанию Falseis_unchecked_prefix
- префикс для снятого флажка, по умолчанию пустая строкаis_checked_prefix
- префикс для отмеченного флажка, по умолчанию '☑ '.
Эти параметры можно задавать, передавая их в конструктор соответствующей кнопки.
Пакет с кнопками, импортируйте все необходимые кнопки из этого пакета.
Кнопка-флажок.
Стандартная таблица этой кнопки (db.tables.CheckBoxButton) имеет дополнительный строковый столбец arg, который вы можете использовать для хранения произвольных данных.
callback кнопки получает 2 позиционных аргумента:
- Флажок отмечен / снят True / False соответственно.
- Содержимое столбца arg.
Обычная кнопка.
Стандартная таблица этой кнопки (db.tables.SimpleButton) имеет дополнительный строковый столбец arg, который вы можете использовать для хранения произвольных данных.
callback кнопки получает 1 позиционный аргумент - содержимое столбца arg.
Содержит некоторые вспомогательные миксины для кнопок.
Конструктор обрабатывает аргумент callback.
Подразумевается, что этим аргументом должен быть метод вкладки.
При создании кнопки в callback_name
таблицы будет передан __name__
этого метода.
Свойство, пытается получить callback по callback_name
из вкладке, к которой привязана кнопка, и вернуть его.
Пакет с клавиатурами, импортируйте все необходимые клавиатуры из этого пакета.
Обычная клавиатура, была описана выше.
Она находится в пакете gui, но для единообразия кода рекомендуется импортировать её из пакета gui.keyboards.
Ограниченная по ширине клавиатура.
Конструктор принимает обязательный keyword-аргумент width - ширину клавиатуры.
Это не позволяет использовать клавиатуру переопределив атрибут класса вкладки keyboard_class
, вместо этого переопределяйте метод get_keyboard
и инициализируйте клавиатуру в нём.
Метод add_button
клавиатуры также как и у SimpleKeyboard
принимает и добавляет кнопки, но, помимо этого, автоматически создаёт новую строку, когда последняя строка достигает длины width.
Метод add_row
работает без ограничений и позволяет добавлять строки любой длины, используйте с осторожностью.
Пакет со вкладками, импортируйте все необходимые вкладки из этого пакета.
Обычная вкладка.
Вспомогательные миксины для создания собственных вкладок.
Миксин вкладки для выбора даты и времени.
Миксин позволяет добавлять пользовательские кнопки (например "Далее", "Назад" и т.п.) ниже кнопок миксина.
Не добавляйте дополнительные кнопки сверху, слева или справа от имеющихся кнопок, их наличие приведёт к некорректному поведению вкладки.
Для использования миксина создайте собственную вкладку, унаследовавшись от него.
При необходимости создайте для вкладки таблицу.
Определите два асинхронных метода.
get_date_time
, должен возвращать datetime-объект, из которого будет взята информация для наполнения вкладки.set_date_time
, должен принимать datetime-объект, который содержит дату и время, выбранные пользователем.
Пакет с классами для работы с текстом, импортируйте необходимые классы текстов из этого пакета.
Класс текста, был описан выше.
Вспомогательные компоненты.
Обёртка на основе contextvars.
Конструктор принимает аргумент context_var_name
, который передаётся в контекстную переменную, связанную с сосдаваемым инстансом.
Избегайте динамического создания этих объектов, так как внутри них содержится стандартный ContextVar, который не может быть корректно уничтожен сборщиком мусора.
После инициализации объекта и задания значения (см. ниже) его можно использовать так, будто это и есть требуемый объект (получать атрибуты, обращаться по индексу / ключу и т.п.).
Устанавливает значение контекстной переменной.
Параметры:
- *args - позиционные аргументы, передаются в ContextVar.set
- **kwargs - keyword-аргументы, передаются в ContextVar.set
Возвращает значение, хранимое во внутренней ContextVar.
Если значение отсутствует, рейзится EmptyContextVarException.
Скорее всего, вам не нужно использовать этот метод напрямую.
Параметры:
- *args - позиционные аргументы, передаются в ContextVar.get
- **kwargs - keyword-аргументы, передаются в ContextVar.get
Сбрасывает значение контекстной переменной.
Свойство, True, если в контекстной переменной имеется значение, False, если значение отсутствует.
Модуль содержащий функции для разбивки текста на части
Разбивает текст на части
Параметры:
- header - заголовок для каждой части
- body - текст, который нужно разделить
max_part_length
- максимально допустимая длина части.- unit='char' - единица текста, по которой нужно выполнить разделение, доступные варианты char - символ, word - слово, line - строка.
header_separator='\n'
- разделитель между заголовком и частью телаinclude_part_numbers=True
- включать ли номера частей (текущая / всего) после заголовка (если заголовок не задан, он будет состоять только из номеров частей)
Функция возвращает список - части текста.
При невозможности выполнить разделение с заданными параметрами возникает исключение ValueError.
Выполняет вызовы split_text
с различными единицами, пока не получит результат.
Параметры:
- header - заголовок для каждой части
- body - текст, который нужно разделить
max_part_length
- максимально допустимая длина части.- units=['line', 'word', 'char'] - единицы текста, которые нужно использовать при вызове
split_text
в качестве значения параметра unit (функция перебирает единицы в том порядке, в котором они следуют в переданном списке). header_separator='\n'
- разделитель между заголовком и частью телаinclude_part_numbers=True
- включать ли номера частей (текущая / всего) после заголовка (если заголовок не задан, он будет состоять только из номеров частей)
Как только очередной вызов становится успешным, возвращается его результат.
Если разделение не удалось ни с одной единицей, возникает исключение ValueError.
Пакет с некоторыми обёртками для pyrogram
Диспетчер, был описан выше.
Обёртка, унаследованная от pyrogram.types.User, добавляет некоторые свойства.
full_name
- полное имя пользователя (Если нет фамилии и username, значением будет имя, если нет username, значением будет имя и фамилия, если нет фамилии, значением будет имя и username, иначе значением будет имя, фамилия и username).log_name
- имя пользователя для использования в логах, состоит из username (если есть) и идентификатора пользователя.
Класс -лимитер.
Позволяет лимитировать количество каких-либо действий за единицу времени.
Параметры:
- controller - инстанс контроллера
- amount - количество действий
- period - временной период
- static=True - если False, лимитер может быть удалён при определённых условиях (данный механизм пока не реализован)
- name=None - имя лимитера (используется в отладочных сообщениях)
Вызвать этот метод (т.е. вызвать инстанс) перед выполнением ограничиваемого действия (отправки сообщения, api-вызова и т.п.).
Лимитер сам будет вычислять требуемую задержку, её может не быть, если количество действий за период не исчерпано, тогда метод сразу завершится.
Иначе метод подождёт минимально необходимое время, после чего также завершится, и позволит выполнить нужное действие.
Для создания бота нужно наследоваться от этого класса.
Конструктор класса принимает параметры:
bot_name
- используется при создании лога и работы с бдuse_uvloop=False
- пытается использовать uvloop, если True, при отсутствии uvloop логирует предупреждениеuser_table=None
- таблица пользователя, если не задана, по умолчанию будет использоваться db.tables.User
В конструкторе помимо вспомогательных атрибутов создаётся log.
Он получает ConsoleHandler, выводящий в консоль INFO и выше, а также FileHandler с файлом log.log, в который логируются все события.
Помимо этого создаётся FileHandler для error.log, в который логируются события с ERROR и выше.
И WarningErrorHandler, отправляющий предупреждения и ошибки в telegram.
Должен вернуть pyrogram фильтр или None.
Может быть переопределён.
Возвращаемый фильтр будет применён к фильтрам, использованным в on_message
-обработчиках.
Метод инициализации, может быть дополнен (переопределён с вызовом super().initialize).
Помещайте вашу логику инициализации в этом методе.
Точка входа.
Переопределяйте только если понимаете, что делаете.
Используется для остановки, вызывается автоматически при обнаружении SIGINT.
Удобная обёртка над asyncio.create_task
.
Параметры:
- callable - асинхронный callable
- *args - позиционные аргументы, которые будут переданы в callable
- name=None - название задачи
- **kwargs - keyword-аргументы, которые будут переданы в callable
Создаёт coroutine передавая *args и **kwargs в callable.
Если name не был передан, в качестве имени используется callable.__name__
.
Далее coroutine передаётся в asyncio.create_task
, задача добавляется в список, который отслеживается служебной задачей-монитором.
Он логирует успешное выполнение задач, добавленных при помощи add_task
с уровнем debug, а упавшие задачи логируются с уровнем error.
Должен вернуть id telegram-чата или None.
Может быть переопределён.
Возвращаемый id будет использоваться в качестве id чата для отправки сообщения, если он не был задан при вызове BotController.send_message
.
Позволяет получить из текста сообщения его части, готовые для отправки в telegram.
Для разбивки метод использует helpers.split_text.split_text_by_units
Параметры:
- text - Текст, который нужно разделить
- title='' - Заголовок для каждой части.
- **kwargs - keyword-аргументы, которые будут переданы в
split_text_by_units
Удобная обёртка для отправки сообщений
Не должна использоваться внешним кодом напрямую, вместо этого, используйте асинхронный метод send_message
.
Параметры:
- text (обязательно позиционный)- текст сообщения, может превышать лимит telegram в 4096 символов (будет разбит автоматически).
chat_id=None
- id чата, в который нужно отправить сообщение, если не задан, будет сделана попытка получить id изget_default_chat_id
- *args - позиционные аргументы, которые будут переданы в
send_message
pyrogram - priority=2 - приоритет отправки от 1 (самый высокий) до 3 (самый низкий).
- blocking=False - если True, отправка будет блокирующей.
- **kwargs - keyword-аргументы, которые будут переданы в
send_message
pyrogram
Если blocking=True, из метода вернётся инстанс asyncio.Event, который будет установлен после фактической отправки сообщения.
Т.е. даже с blocking=True метод на самом деле не блокирует выполнение, но подразумевается, что какой-то код позже будет ждать установки возвращённого event-а.
Если была запрошена блокирующая отправка, после того, как сообщение отправлено, оно помещается в атрибут event-а message.
Обёртка над send_message_sync
.
Параметры:
- *args - позиционные аргументы, которые передаются в
send_message_sync
- **kwargs - keyword-аргументы, которые передаются в
send_message_sync
Если send_message_sync
возвращает event, метод дожидается его установки и возвращает отправленное сообщение, присвоенное в атрибут event-а message.
Создаёт или возвращает пользователя из базы данных.
Необходимо, чтобы в db была установлена действительная сессия sqlalchemy.
Метод используется внутри ядра, но может быть задействован внешним кодом.
Параметры:
user_id
- telegram id пользователя
Инстанс пользователя будет получен из базы, если он ранее был добавлен, если пользователь не будет обнаружен, инстанс будет создан и добавлен в базу.
Затем полученный или созданный инстанс будет возвращён из метода.
Некоторые константы
id пользователя по умолчанию (используется ядром, не должен применяться во внешнем коде).
Используйте эту константу в качестве user_id
при создании окон, которые должны быть доступны только для анонимных пользователей (администраторов групп).
Клиент находится в атрибуте app инстанса контроллера, и некоторые его методы обёрнуты вспомогательным декоратором.
Он позволяет гибко обрабатывать исключения и управлять порядком вызовов.
Список обёрнутых методов можно посмотреть в файле tgbot/exception_handler.py
.
Параметры обёртки:
- *args - позиционные аргументы, которые должны быть переданы в оригинальный метод
ignore_errors=False
- если True, ошибки логируются с уровнем info, иначе с уровнем errormax_attempts=10
- максимальное количество попыток, которое будет сделано при вызове метода- limiters=None - список инстансов лимитеров, которые будут вызваны перед вызовом обёрнутого метода.
previous_invoke_event=None
- event, установки которого нужно дождаться перед вызовом обёрнутого метода.current_invoke_event=None
- event, который нужно установить после успешного вызова обёрнутого метода- **kwargs - keyword-аргументы, которые должны быть переданы в оригинальный метод
Алгоритм работы обёртки
- Вызывается обёрнутый метод.
- Если вызов успешен, возвращается значение, возвращённое обёрнутым методом.
- Если возникло исключение pyrogram.errors.FloodWait оно перехватывается, а таймаутом становится value.
- Если возникло исключение pyrogram.errors.InternalServerError, оно перехватывается, а таймаутом становится номер попытки в четвёртой степени.
- Если исчерпано количество попыток и
ignore_errors=True
, это событие логируется с info и возвращается None. - Если исчерпано количество попыток и
ignore_errors=False
, возникает исключениеAttemptLimitReached
(содержится в модулеexception_handler
). - В случае, если попытка была не последняя, обёртка ожидает timeout секунд и повторяет вызов.
Не 500-е исключения НЕ перехватываются, также не перехватываются иные исключения, кроме описанных выше.
- Отделить ядро от Cm assistant
- Реализовать механизм для выполнения рассылок
- Отформатировать код по PEP8
- Реализовать монитор, очищающий устаревшие лимитеры и event_chain-ы
- Решить проблему с
send_*
методами (сейчас средствамиBotController.send_message
возможно отправлять только текстовые сообщения, нужно реализовать механизм, который позволит использовать таким же образомsend_audio
,send_document
и т.п.). - Сделать log.notification, отправляющий уведомление пользователям из списка
DEV_IDS
(может быть полезно при отладке).