Skip to content

Danstiv/cm-assistant

Repository files navigation

Community assistant

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 в текстовом редакторе и подставьте необходимые значения.

Следующие инструкции выполняйте из раздела, соответствующего вашей ОС.

Linux

Создайте и активируйте виртуальное окружение:

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, так как, вероятно, вы не сможете обеспечить круглосуточное функционирование бота.

Однако, в целях отладки / разработки, запускать бота можно и на собственной машине с 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.

Планы

  1. Реализовать возможность создавать массовые рассылки участникам группы.
  2. При повышении нагрузки переключиться с sqlite на postgresql.
  3. Отрефакторить код по 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

Ядро переопределяет или дополняет некоторые компоненты pyrogram, ниже описаны некоторые такие дополнения.

Обработчики

В tgbot реализован собственный набор декораторов, находящийся в tgbot.handler_decorators.

Наименования соответствуют декоратором вида on_* из класса pyrogram.Client.

Так как tgbot предназначен для создания class-based ботов, декораторы из tgbot.handler_decorators специально заточены под такое использование, и должны применяться исключительно к методам класса контроллера.

Инстанс клиента доступен в self.app, потому, в отличии от pyrogram, в обработчики инстанс клиента не передаётся.

Диспетчер

Для более удобного контроля обновлений был частично переписан диспетчер из pyrogram.dispatcher.

В диспетчер поверх групп была добавлена ещё одна абстракция: категории.

Количество категорий фиксировано, на данный момент их 5.

  1. INITIALIZE, обработчики этой категории выполняются в первую очередь.
  2. MAIN, обработчики этой категории выполняются после успешного выполнения всех обработчиков из INITIALIZE.
  3. RESTORE, обработчики этой категории запускаются, если произошло исключение в одном из обработчиков MAIN.
  4. FINISH, обработчики этой категории выполняются, если все обработчики из MAIN были успешно выполнены.
  5. 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 - наименее важные).

  1. Сообщения разработчикам бота (тем, id которых перечислены в переменной окружения DEV_IDS).
  2. Сообщения пользователям и группам (стандартный приоритет.
  3. Массовые рассылочные сообщения (возможность их отправки пока не реализована).

Отправка этим методом по умолчанию не блокирующая, однако, передав аргумент blocking=True, можно получить результат, похожий на результат вызова pyrogram.Client.send_message.

Вызовы методов pyrogram.client и обработка исключений

Почти во все методы pyrogram.Client можно передавать дополнительные аргументы, ограничивающие количество попыток выполнения и позволяющие задавать очерёдность вызовов (для подробностей смотрите описание в разделе "Вызовы методов клиента pyrogram")

Также в tgbot улучшена обработка исключений.

  1. Логируются все FloodWait-исключения (с дублированием в telegram чаты, перечисленные в DEV_IDS), что позволяет быстро отреагировать на превышенные лимиты и исправить проблему.

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

Использование базы данных

Для работы с базой реализован следующий механизм.

  1. При начале обработке обновления в категории INITIALIZE создаётся sqlalchemy session, который устанавливается в контекстную переменную.
  2. В категории restore выполняется rollback.
  3. В категории finish выполняется commit.
  4. В категории 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

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

Основные концепции:

  1. Интерфейс находится внутри окна.
  2. Одно окно - одно сообщение.
  3. Внутри окна может находиться неограниченное количество вкладок.
  4. Вкладки можно переключать в любом порядке.
  5. У каждой вкладки есть собственный текст и набор кнопок.

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

Описание пакета

db

Этот модуль содержит миксин с методами для работы с базой данных и некоторые функции.

db.db

Контекстное хранилище sqlalchemy сессии в базе данных.

Используйте его для работы с базой в обработчиках.

db.TGBotDBMixin

Все методы миксина не предназначены для вызова из внешнего кода.

db.with_db

Декоратор методов, устанавливающий сессию в db.db, вызывающий обёрнутый метод и закрывающий сессию после завершения выполнения.

При успехе делает commit, при исключении вызывает rollback.

Параметры:

  • wait_for_commit=False - если True, будет ждать коммита в session из db, прежде чем заменить его на новый.

При wait_for_commit=True также отслеживается и rollback, и, если он происходит, вызов обёрнутого метода не выполняется.

Так как, если для выполнения обёрнутого метода требуется коммит в активной сессии, вероятно, продолжение после rollback-а может навредить ещё больше.

db.tables

Содержит Base для создания sqlalchemy классов-таблиц, а также таблицы, используемые внутри tgbot.

enums

Содержит импорты перечислений.

enums.Category

Импортируется из wrappers.dispatcher, содержит следующие члены.

  1. INITIALIZE
  2. MAIN
  3. RESTORE
  4. FINISH
  5. FINALIZE

gui

Содержит различные компоненты для создания и управления интерфейсами.

gui.Window

Класс для создания окон, его нужно переопределять, для создания собственных окон.

В переопределённом классе нужно создать список tabs, в котором перечислить классы вкладок.

Параметры:

  • controller - инстанс контроллера
  • chat_id - id чата, в котором будет создано окно
  • user_id=None - id пользователя, который может управлять окном (если не задано, окном смогут управлять все пользователи в чате).

async gui.Window.build

Позволяет создать окно

Параметры:

  • *args - позиционные аргументы, которые будут переданы в build вкладки
  • tab=None - вкладка, которую нужно показать (по умолчанию это самая первая вкладка из tabs).
  • **kwargs - keyword-аргументы, которые будут переданы в build вкладки

rebind

Перепривязывает окно к сессии из db.db

Метод полезен, если окно было передано в фоновую задачу, а обработка обновления уже завершилась и сессия была закрыта.

Конечно, сессию можно продолжать использовать после закрытия, но в таком случае нужно самостоятельно делать коммит / rollback после завершения всех манипуляций, что не соответствует рекомендациям из документации sqlalchemy.

Поэтому, если внутри фоновой задачи необходимо использовать окно (например для периодических обновлений), то метод, который будет выполняться фоном (внутри асинхронной задачи) следует обернуть в with_db с wait_for_commit=True, а внутри метода выполнить window.rebind, чтобы добавить все необходимые инстансы sqlalchemy-таблиц в текущую сессию.

gui.Window.schedule_swap

Можно использовать при получении и обработке пользовательского ввода.

Если перед рендером окна был вызван этот метод, то сообщение с окном будет удалено и отправлено заново вместо редактирования, из-за чего сообщение от пользователя окажется выше в истории.

async gui.Window.render

Рендерит сообщение.

Для новых окон сообщение отправляется, для уже созданных редактируется.

async gui.Window.switch_tab

Переключает вкладку.

Параметры:

  • new_tab - класс вкладки, на которую нужно переключиться.
  • *args - позиционные аргументы, которые будут переданы в build вкладки
  • save_current_tab=False - если True, текущая вкладка будет сохранена в базу, иначе текущая вкладка будет уничтожена.
  • **kwargs - keyword-аргументы, которые будут переданы в build вкладки

Если выполняется switch_tab на вкладку, которая ранее была сохранена (save_current_tab=True), то *args и **kwargs не будут переданы в build, так как build вызывается только у создаваемых вкладок.

gui.BaseKeyboard

Класс для управления клавиатурами.

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

gui.BaseKeyboard.add_row

Добавляет строку кнопок.

Параметры:

  • *buttons - позиционные аргументы-кнопки

gui.BaseKeyboard.add_button

Добавляет кнопку.

Параметры:

  • button - добавляемая кнопка

Если в клавиатуре нет ни одной строки, она будет создана, иначе кнопка будет добавлена в последнюю строку.

gui.BaseKeyboard.remove_buttons_by_name

Позволяет удалить кнопки по имени.

Параметры:

  • name - имя кнопки, все кнопки с таким именем будут удалены.

gui.SimpleKeyboard(BaseKeyboard)

Класс стандартной клавиатуры, собственных атрибутов и методов не имеет.

gui.BaseButton

Базовый класс для создания кнопок, не должен использоваться внешним кодом напрямую.

Допускается наследование и реализация собственных кнопок.

Параметры:

  • text=None - текст кнопки
  • *args - позиционные аргументы, которые будут переданы в класс-таблицу кнопки
  • row=None - инстанс класса-таблицы sqlalchemy
  • **kwargs - keyword-аргументы, которые будут переданы в класс-таблицу кнопки

Если был передан row, то он будет присвоен в self.row, иначе в self.row будет присвоен инстанс таблицы, находящийся в self.table.

Во втором случае *args и **kwargs будут переданы в конструктор таблицы.

Для создания собственных кнопок нужно унаследоваться от этого класса и задать в нём атрибут table, значением которого должна быть sqlalchemy-таблица.

Таблицу будет удобно создать используя миксин (см. ниже).

gui.BaseButton.set_text

Устанавливает текст кнопки

Параметры:

  • text - новый текст

async gui.BaseButton.handle_button_activation

Вызывается при получении callback_query, этот метод необходимо переопределить при реализации собственной кнопки.

Параметры:

  • row_index - индекс строки, в которой находится кнопка
  • column_index - индекс столбца, в котором находится кнопка (индекс кнопки в строке)

Индексы строки и столбца могут быть полезны, когда при нажатии кнопки должны быть изменены другие кнопки в клавиатуре (в tgbot пока таких кнопок не реализовано).

async gui.BaseButton.db_render

Рендерит кнопку, добавляя row в db.

Вызывается из render, вам нужно будет вызвать этот метод, если будете переопределять render.

async gui.BaseButton.render

Возвращает pyrogram.types.InlineKeyboardButton.

gui.InputField

Используется для описания полей ввода.

Инстансы перечисляются в списке input_fields в классе вкладки.

Параметры:

  • name - имя поля, используется для переключения между полями
  • text=None - текст сообщения, который будет установлен при активации этого поля.
  • method_name=None - имя метода вкладки, которое будет использовано для обработки введённых данных (по умолчанию 'process_' + name).

Например, для поля с именем phone, при обработке ввода tgbot попытается вызвать метод process_phone.

gui.BaseText

Абстракция для работы с текстом сообщений, не должна использоваться внешним кодом (используйте классы-наследники).

gui.BaseText.set_header

Устанавливает заголовок сообщения.

Он будет отображаться в самом начале, после него будет следовать линия из дефисов.

Параметры:

  • header - текст заголовка
  • one_time=True - показывать заголовок только 1 раз (если True, заголовок исчезнет при следующем рендере окна)

gui.BaseText.set_body

Устанавливает тело сообщения.

Параметры:

  • body - тело сообщения

gui.Text(BaseText)

Класс для работы с текстом

gui.BaseTab

Базовый класс для создания вкладок.

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

При наследовании нужно задать атрибут table с sqlalchemy-таблицей.

Также как и для кнопок, для вкладок существует миксин.

В классе вкладки используются следующие атрибуты:

  • text_class задаёт класс для обработки текста, по умолчанию gui.Text.
  • keyboard_class задаёт класс для обработки клавиатуры, по умолчанию gui.SimpleKeyboard.
  • input_fields может содержать список инстансов gui.InputField для обработки пользовательского ввода.
  • rerender_text нужно ли повторно рендерить текст, если True (по умолчанию), текст будет рендериться всегда, если False, текст будет рендериться только в первый раз, при повторных рендерах будет переиспользоваться текст сообщения.

gui.BaseTab.get_text

Должен вернуть инстанс текста, по умолчанию возвращается self.text_class(self)

async gui.BaseTab.get_text_data

Должен вернуть словарь, которым будет отформатирован текст сообщения, по умолчанию возвращает {}.

gui.BaseTab.get_keyboard

Должен вернуть инстанс клавиатуры, по умолчанию возвращается self.keyboard_class(self)

async gui.BaseTab.build

Выполняет создание вкладки.

Этот метод нужно переопределять во вкладках, вызывать super().build и реализовывать далее логику для создания интерфейса (устанавливать текст, добавлять кнопки и т.п.).

Параметры:

  • *args - позиционные аргументы, которые будут переданы в класс-таблицу вкладки
  • **kwargs - keyword-аргументы, которые будут переданы в класс-таблицу вкладки

gui.BaseTab.switch_input_field

Переключает поле ввода

Параметры:

  • field_name - имя поля, на которое нужно переключиться
  • previous=False - если True и имя поля не задано, переключится на предыдущее поле
  • next=True - если True и имя поля не задано, переключится на следующее поле (поведение метода по умолчанию, без аргументов)

gui.Tab(BaseTab)

Класс вкладки, готовый для использования

gui.mixins

Миксины для sqlalchemy-таблиц.

gui.mixins.TabMixin

Миксин для создания таблиц-вкладок.

Наследуйте класс-таблицу от Base и от данного миксина, чтобы создавать собственные таблицы для вкладок.

gui.mixins.BaseButtonMixin

Миксин для создания кнопок, наследуйте класс-таблицу от Base и от данного миксина, чтобы создавать собственные таблицы для кнопок.

Один из полезных столбцов - это name, который вы можете передать в конструктор кнопки.

Позже вы сможете удалить кнопку с нужным именем, используя метод клавиатуры remove_buttons_by_name.

gui.mixins.ButtonMixin(BaseButtonMixin)

Миксин для кнопок с callback-ами. Добавляет к базовому миксину столбец callback_name.

Скорее всего вам не нужно использовать его явно, так как имеющиеся классы кнопок могут обрабатывать callable-объекты и сами получать из них значение для callback_name.

gui.mixins.CheckBoxButtonMixin(ButtonMixin)

Миксин для создания собственных кнопок-флажков.

Имеет следующие полезные столбцы:

  • is_checked - отмечен ли флажок, по умолчанию False
  • is_unchecked_prefix - префикс для снятого флажка, по умолчанию пустая строка
  • is_checked_prefix - префикс для отмеченного флажка, по умолчанию '☑ '.

Эти параметры можно задавать, передавая их в конструктор соответствующей кнопки.

gui.buttons

Пакет с кнопками, импортируйте все необходимые кнопки из этого пакета.

gui.buttons.CheckBoxButton(ButtonWithCallback)

Кнопка-флажок.

Стандартная таблица этой кнопки (db.tables.CheckBoxButton) имеет дополнительный строковый столбец arg, который вы можете использовать для хранения произвольных данных.

callback кнопки получает 2 позиционных аргумента:

  1. Флажок отмечен / снят True / False соответственно.
  2. Содержимое столбца arg.

gui.buttons.SimpleButton(ButtonWithCallback)

Обычная кнопка.

Стандартная таблица этой кнопки (db.tables.SimpleButton) имеет дополнительный строковый столбец arg, который вы можете использовать для хранения произвольных данных.

callback кнопки получает 1 позиционный аргумент - содержимое столбца arg.

gui.buttons.mixins

Содержит некоторые вспомогательные миксины для кнопок.

gui.buttons.mixins.ButtonWithCallback(BaseButton)

Конструктор обрабатывает аргумент callback.

Подразумевается, что этим аргументом должен быть метод вкладки.

При создании кнопки в callback_name таблицы будет передан __name__ этого метода.

gui.buttons.mixins.ButtonWithCallback.callback

Свойство, пытается получить callback по callback_name из вкладке, к которой привязана кнопка, и вернуть его.

gui.keyboards

Пакет с клавиатурами, импортируйте все необходимые клавиатуры из этого пакета.

gui.keyboards.SimpleKeyboard

Обычная клавиатура, была описана выше.

Она находится в пакете gui, но для единообразия кода рекомендуется импортировать её из пакета gui.keyboards.

gui.keyboards.GridKeyboard

Ограниченная по ширине клавиатура.

Конструктор принимает обязательный keyword-аргумент width - ширину клавиатуры.

Это не позволяет использовать клавиатуру переопределив атрибут класса вкладки keyboard_class, вместо этого переопределяйте метод get_keyboard и инициализируйте клавиатуру в нём.

Метод add_button клавиатуры также как и у SimpleKeyboard принимает и добавляет кнопки, но, помимо этого, автоматически создаёт новую строку, когда последняя строка достигает длины width.

Метод add_row работает без ограничений и позволяет добавлять строки любой длины, используйте с осторожностью.

gui.tabs

Пакет со вкладками, импортируйте все необходимые вкладки из этого пакета.

gui.tabs.Tab

Обычная вкладка.

gui.tabs.mixins

Вспомогательные миксины для создания собственных вкладок.

gui.tabs.mixins.DateTimeSelectionTabMixin(BaseTab)

Миксин вкладки для выбора даты и времени.

Миксин позволяет добавлять пользовательские кнопки (например "Далее", "Назад" и т.п.) ниже кнопок миксина.

Не добавляйте дополнительные кнопки сверху, слева или справа от имеющихся кнопок, их наличие приведёт к некорректному поведению вкладки.

Для использования миксина создайте собственную вкладку, унаследовавшись от него.

При необходимости создайте для вкладки таблицу.

Определите два асинхронных метода.

  1. get_date_time, должен возвращать datetime-объект, из которого будет взята информация для наполнения вкладки.
  2. set_date_time, должен принимать datetime-объект, который содержит дату и время, выбранные пользователем.

gui.texts

Пакет с классами для работы с текстом, импортируйте необходимые классы текстов из этого пакета.

gui.texts.Text

Класс текста, был описан выше.

helpers

Вспомогательные компоненты.

helpers.ContextVarWrapper

Обёртка на основе contextvars.

Конструктор принимает аргумент context_var_name, который передаётся в контекстную переменную, связанную с сосдаваемым инстансом.

Избегайте динамического создания этих объектов, так как внутри них содержится стандартный ContextVar, который не может быть корректно уничтожен сборщиком мусора.

После инициализации объекта и задания значения (см. ниже) его можно использовать так, будто это и есть требуемый объект (получать атрибуты, обращаться по индексу / ключу и т.п.).

helpers.ContextVarWrapper.set_context_var_value

Устанавливает значение контекстной переменной.

Параметры:

  • *args - позиционные аргументы, передаются в ContextVar.set
  • **kwargs - keyword-аргументы, передаются в ContextVar.set

helpers.ContextVarWrapper.get_context_var_value

Возвращает значение, хранимое во внутренней ContextVar.

Если значение отсутствует, рейзится EmptyContextVarException.

Скорее всего, вам не нужно использовать этот метод напрямую.

Параметры:

  • *args - позиционные аргументы, передаются в ContextVar.get
  • **kwargs - keyword-аргументы, передаются в ContextVar.get

helpers.ContextVarWrapper.reset_context_var

Сбрасывает значение контекстной переменной.

helpers.ContextVar.is_set

Свойство, True, если в контекстной переменной имеется значение, False, если значение отсутствует.

helpers.split_text

Модуль содержащий функции для разбивки текста на части

helpers.split_text.split_text

Разбивает текст на части

Параметры:

  • header - заголовок для каждой части
  • body - текст, который нужно разделить
  • max_part_length - максимально допустимая длина части.
  • unit='char' - единица текста, по которой нужно выполнить разделение, доступные варианты char - символ, word - слово, line - строка.
  • header_separator='\n' - разделитель между заголовком и частью тела
  • include_part_numbers=True - включать ли номера частей (текущая / всего) после заголовка (если заголовок не задан, он будет состоять только из номеров частей)

Функция возвращает список - части текста.

При невозможности выполнить разделение с заданными параметрами возникает исключение ValueError.

helpers.split_text.split_text_by_units

Выполняет вызовы split_text с различными единицами, пока не получит результат.

Параметры:

  • header - заголовок для каждой части
  • body - текст, который нужно разделить
  • max_part_length - максимально допустимая длина части.
  • units=['line', 'word', 'char'] - единицы текста, которые нужно использовать при вызове split_text в качестве значения параметра unit (функция перебирает единицы в том порядке, в котором они следуют в переданном списке).
  • header_separator='\n' - разделитель между заголовком и частью тела
  • include_part_numbers=True - включать ли номера частей (текущая / всего) после заголовка (если заголовок не задан, он будет состоять только из номеров частей)

Как только очередной вызов становится успешным, возвращается его результат.

Если разделение не удалось ни с одной единицей, возникает исключение ValueError.

wrappers

Пакет с некоторыми обёртками для pyrogram

wrappers.Dispatcher

Диспетчер, был описан выше.

wrappers.User

Обёртка, унаследованная от pyrogram.types.User, добавляет некоторые свойства.

  • full_name - полное имя пользователя (Если нет фамилии и username, значением будет имя, если нет username, значением будет имя и фамилия, если нет фамилии, значением будет имя и username, иначе значением будет имя, фамилия и username).
  • log_name - имя пользователя для использования в логах, состоит из username (если есть) и идентификатора пользователя.

limiter.Limiter

Класс -лимитер.

Позволяет лимитировать количество каких-либо действий за единицу времени.

Параметры:

  • controller - инстанс контроллера
  • amount - количество действий
  • period - временной период
  • static=True - если False, лимитер может быть удалён при определённых условиях (данный механизм пока не реализован)
  • name=None - имя лимитера (используется в отладочных сообщениях)

async limiter.Limiter.__call__

Вызвать этот метод (т.е. вызвать инстанс) перед выполнением ограничиваемого действия (отправки сообщения, api-вызова и т.п.).

Лимитер сам будет вычислять требуемую задержку, её может не быть, если количество действий за период не исчерпано, тогда метод сразу завершится.

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

BotController

Для создания бота нужно наследоваться от этого класса.

Конструктор класса принимает параметры:

  • 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.

BotController.get_global_filter

Должен вернуть pyrogram фильтр или None.

Может быть переопределён.

Возвращаемый фильтр будет применён к фильтрам, использованным в on_message-обработчиках.

async BotController.initialize

Метод инициализации, может быть дополнен (переопределён с вызовом super().initialize).

Помещайте вашу логику инициализации в этом методе.

async BotController.start

Точка входа.

Переопределяйте только если понимаете, что делаете.

BotController.stop

Используется для остановки, вызывается автоматически при обнаружении SIGINT.

BotController.add_task

Удобная обёртка над 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.

BotController.get_default_chat_id

Должен вернуть id telegram-чата или None.

Может быть переопределён.

Возвращаемый id будет использоваться в качестве id чата для отправки сообщения, если он не был задан при вызове BotController.send_message.

BotController.get_message_texts

Позволяет получить из текста сообщения его части, готовые для отправки в telegram.

Для разбивки метод использует helpers.split_text.split_text_by_units

Параметры:

  • text - Текст, который нужно разделить
  • title='' - Заголовок для каждой части.
  • **kwargs - keyword-аргументы, которые будут переданы в split_text_by_units

BotController.send_message_sync

Удобная обёртка для отправки сообщений

Не должна использоваться внешним кодом напрямую, вместо этого, используйте асинхронный метод 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.

async BotController.send_message

Обёртка над send_message_sync.

Параметры:

  • *args - позиционные аргументы, которые передаются в send_message_sync
  • **kwargs - keyword-аргументы, которые передаются в send_message_sync

Если send_message_sync возвращает event, метод дожидается его установки и возвращает отправленное сообщение, присвоенное в атрибут event-а message.

async BotController.get_or_create_user

Создаёт или возвращает пользователя из базы данных.

Необходимо, чтобы в db была установлена действительная сессия sqlalchemy.

Метод используется внутри ядра, но может быть задействован внешним кодом.

Параметры:

  • user_id - telegram id пользователя

Инстанс пользователя будет получен из базы, если он ранее был добавлен, если пользователь не будет обнаружен, инстанс будет создан и добавлен в базу.

Затем полученный или созданный инстанс будет возвращён из метода.

constants

Некоторые константы

constants.DEFAULT_USER_ID

id пользователя по умолчанию (используется ядром, не должен применяться во внешнем коде).

constants.ANONYMOUS_USER_ID

Используйте эту константу в качестве user_id при создании окон, которые должны быть доступны только для анонимных пользователей (администраторов групп).

Вызовы методов клиента pyrogram

Клиент находится в атрибуте app инстанса контроллера, и некоторые его методы обёрнуты вспомогательным декоратором.

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

Список обёрнутых методов можно посмотреть в файле tgbot/exception_handler.py.

Параметры обёртки:

  • *args - позиционные аргументы, которые должны быть переданы в оригинальный метод
  • ignore_errors=False - если True, ошибки логируются с уровнем info, иначе с уровнем error
  • max_attempts=10 - максимальное количество попыток, которое будет сделано при вызове метода
  • limiters=None - список инстансов лимитеров, которые будут вызваны перед вызовом обёрнутого метода.
  • previous_invoke_event=None - event, установки которого нужно дождаться перед вызовом обёрнутого метода.
  • current_invoke_event=None - event, который нужно установить после успешного вызова обёрнутого метода
  • **kwargs - keyword-аргументы, которые должны быть переданы в оригинальный метод

Алгоритм работы обёртки

  1. Вызывается обёрнутый метод.
  2. Если вызов успешен, возвращается значение, возвращённое обёрнутым методом.
  3. Если возникло исключение pyrogram.errors.FloodWait оно перехватывается, а таймаутом становится value.
  4. Если возникло исключение pyrogram.errors.InternalServerError, оно перехватывается, а таймаутом становится номер попытки в четвёртой степени.
  5. Если исчерпано количество попыток и ignore_errors=True, это событие логируется с info и возвращается None.
  6. Если исчерпано количество попыток и ignore_errors=False, возникает исключение AttemptLimitReached (содержится в модуле exception_handler).
  7. В случае, если попытка была не последняя, обёртка ожидает timeout секунд и повторяет вызов.

Не 500-е исключения НЕ перехватываются, также не перехватываются иные исключения, кроме описанных выше.

Планы

  1. Отделить ядро от Cm assistant
  2. Реализовать механизм для выполнения рассылок
  3. Отформатировать код по PEP8
  4. Реализовать монитор, очищающий устаревшие лимитеры и event_chain-ы
  5. Решить проблему с send_* методами (сейчас средствами BotController.send_message возможно отправлять только текстовые сообщения, нужно реализовать механизм, который позволит использовать таким же образом send_audio, send_document и т.п.).
  6. Сделать log.notification, отправляющий уведомление пользователям из списка DEV_IDS (может быть полезно при отладке).

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published