Хардкорная разработка под Телеграм. Бот-модератор своими руками. Часть 3

Моя цель - предложение широкого ассортимента товаров и услуг на постоянно высоком качестве обслуживания по самым выгодным ценам.

Продолжаем писать своего крутого бота-модератора чатов на Python. Все части туториала:

  • Часть 1. Создание бота

  • Часть 2. Проверка админов

  • Часть 3. Команды для модерации

Полный код для этой части на GitHub


В этой части мы сделаем команды для модерации. Админы чата смогут банить участников, запрещать им писать в чате, давать предупреждения с помощью команд /ban, /mute, /warn.

Некоторые боты-администраторы используют не слэш-команды, а команды через, например, восклицательный знак: !ban, !mute. Но мы будем использовать слэш: это "нативные" команды в Телеграме. Они подсвечиваются в сообщениях, и их можно добавить в список команд для автодополнения.

Как обрабатывать команды

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

Когда кто-то отправляет команду, бот должен:

  1. Проверить, что этот пользователь админ.

  2. Выполнить нужное действие.

  3. Если сделать нужное действие не получилось из-за того, что у бота недостаточно прав администратора — сообщить об этом.

Понятно, что не стоит писать все эти проверки заново для каждой новой функции. Вместо этого мы сделаем декоратор. Использовать его можно будет так:

@admin_command('greet')
async def greet_command(event: Message):
    await event.respond('Привет')

После применения декоратора admin_command(command) к функции greet_command, она будет обрабатывать команду /greet (и добавятся нужные проверки).

Это простой пример. Команду /greet могут использовать только админы.
Это простой пример. Команду /greet могут использовать только админы.

Давайте напишем функцию admin_command в utils.py. Тут может быть немного сложно, поэтому по шагам.

  1. Функция admin_command(command) возвращает нужный декоратор:

    def admin_command(command: str):
        def decorator(func):
            # возвращает новую функцию
    
    return decorator
  2. Внутри функции decorator мы определяем новую функцию:

    from telethon.errors import ChatAdminRequiredError
    from telethon.tl.custom import Message
    async def handle(event: Message):
        if not await is_admin(event.chat.id, event.sender.id):
            await event.respond('Эта команда доступна только админам')
            return
    
    try:
        await func(event)
    except ChatAdminRequiredError:
        await event.respond('У меня нет нужных прав администратора')
    
    return handle

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

  3. Новую функцию handle мы сразу привязываем к событию NewMessage. Обрабатываем только сообщения из группы с текстом '/' + command:

    from telethon import events
    pattern = f'(?i)^/{command}$'  # Регулярное выражение — принимает команду в любом регистрах
    
    @bot.on(events.NewMessage(pattern=pattern, func=lambda event: event.is_group))

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

И получается такой код:

from telethon import events
from telethon.errors import ChatAdminRequiredError
from telethon.tl.custom import Message
...

def admin_command(command: str):
    def decorator(func):
        pattern = f'(?i)^/{command}$'
    @bot.on(events.NewMessage(pattern=pattern, func=lambda event: event.is_group))
    async def handle(event: Message):
        if not await is_admin(event.chat.id, event.sender.id):
            await event.respond('Эта команда доступна только админам')
            return

        try:
            await func(event)
        except ChatAdminRequiredError:
            await event.respond('У меня нет нужных прав администратора')

    return handle
return decorator

Команды модерации

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

Кроме проверок, которые мы реализовали выше, для каждой такой команды нужно:

  1. Проверить, что эта команда отправлена ответом на другое сообщение.

  2. Проверить, что отправитель того сообщения не администратор.

  3. Выполнить нужное действие с отправителем.

И да — для этого мы напишем ещё один декоратор. Вот пример его использования:

@admin_moderate_command('mute')
async def mute_command(chat_id, user_id, mention):
    await bot.edit_permissions(chat_id, user_id, send_messages=False)
    return f'Участник {mention} теперь в муте'

Здесь функция mute_command:

  1. Принимает айди чата, айди пользователя и упоминание пользователя.

  2. Изменяет разрешения участника в чате (чтобы он не мог отправлять сообщения).

  3. Возвращает текст сообщения о том, что команда выполнена: "Участник такой-то помещён в мут". Для этого и используется упоминание пользователя.

Если что, упоминание — это элемент разметки сообщений (ссылка вида tg://user?id=123).

Для удобства напишем функцию get_mention, которая будет возвращать такую ссылку с именем пользователя:

from telethon.tl.types import User
...

def get_mention(user: User):
    name = user.first_name 
    if user.last_name:
        user += ' ' + user.last_name
    return f'<a href="tg://user?id={user.id}">{name}</a>'

Ну и приступаем к функции admin_moderate_command. Она будет похожа на admin_command.

  1. Функция возвращает декоратор:

    def admin_moderate_command(command: str):
        def decorator(func):
            # возвращает новую функцию
    
    return decorator
  2. Декоратор возвращает новую функцию:

    async def handle(event: Message):
        if not event.is_reply:
            await event.respond('Пожалуйста, напишите эту команду в ответ на сообщение пользователя')
            return
    
    reply_to = await event.get_reply_message()
    
    if await is_admin(event.chat.id, reply_to.sender.id):
        await event.respond('Эту команду нельзя применять к админам')
        return
    
    result = await func(event.chat.id, reply_to.sender_id, get_mention(reply_to.sender))
    await event.respond(result)
    
    return handle

    Функция выполняет две проверки и вызывает func с нужными аргументами. Потом отправляет сообщение с результатом.

  3. Команду command нужно обрабатывать как команду админов функцией handle:

    @admin_command(command)

И в итоге:

def admin_moderate_command(command: str):
    def decorator(func):
        @admin_command(command)
        async def handle(event: Message):
            if not event.is_reply:
                await event.respond('Пожалуйста, напишите эту команду в ответ на сообщение пользователя')
                return

        reply_to = await event.get_reply_message()

        if reply_to.sender.bot:
            await event.respond('Эту команду нельзя применять к ботам')
            return

        if await is_admin(event.chat.id, reply_to.sender.id):
            await event.respond('Эту команду нельзя применять к админам')
            return

        result = await func(event.chat.id, reply_to.sender_id, get_mention(reply_to.sender))
        await event.respond(result)

    return handle
return decorator

Готово. Теперь можно быстренько написать нужные команды.

Простые команды

Итак, команда /mute и противоположная ей /unmute:

@admin_moderate_command('mute')
async def mute_command(chat_id: int, user_id: int, mention: str):
    await bot.edit_permissions(chat_id, user_id, send_messages=False)
    return f'Участник {mention} теперь в муте'

@admin_moderate_command('unmute')
async def unmute_command(chat_id: int, user_id: int, mention: str):
    await bot.edit_permissions(chat_id, user_id, send_messages=True)
    return f'Участник {mention} больше не в муте'

Команда /ban будет удалять пользователя из группы. Он не сможет войти снова, пока не будет разбанен — для этого есть команда /unban.

(Для этого нужно просто изменить разрешение участника view_messages.)

@admin_moderate_command('ban')
async def ban_command(chat_id: int, user_id: int, mention: str):
    await bot.edit_permissions(chat_id, user_id, view_messages=False)
    return f'Участник {mention} забанен'

@admin_moderate_command('unban')
async def unban_command(chat_id: int, user_id: int, mention: str):
    await bot.edit_permissions(chat_id, user_id, view_messages=True)
    return f'Участник {mention} разбанен'

Можно сделать команду /kick, которая будет просто кикать участника из группы (он сможет зайти снова). По сути это просто бан+разбан, но в Telethon есть отдельная функция для этого:

@admin_moderate_command('kick')
async def kick_command(chat_id: int, user_id: int, mention: str):
    await bot.kick_participant(chat_id, user_id)
    return f'Участник {mention} исключен из группы'

Можете добавить другие команды. Например, запретить отправлять медиа в чат: bot.edit_permissions(chat_id, user_id, send_media=False).

Предупреждения

Давайте добавим команду /warn, которая будет давать участнику предупреждение. Как только участник будет набирать три предупреждения, он будет получать мут.

Для каждого участника будем хранить количество набранных предупреждений:

class ChatMember(Model):
    ...
    warns = fields.IntField(default=0)

Делаем миграцию БД с названием add_warns:

$ aerich migrate --name add_warns

Команда /warn получает увеличивает число предупреждений участника (максимум 3).

Если предупреждений стало 3, бот пробует дать мут участнику. Если недостаточно прав — сообщает об этом.

В handlers.py:

@admin_moderate_command('warn')
async def warn_command(chat_id: int, user_id: int, mention: str):
    member = await ChatMember.get_or_none(chat_id=chat_id, user_id=user_id)
    warns = member.warns if member else 0
    warns = min(warns + 1, 3)
    await update_chat_member(chat_id, user_id, warns=warns)

if warns == 3:
    try:
        await bot.edit_permissions(chat_id, user_id, send_messages=False)
    except ChatAdminRequiredError:
        return f'Участник {mention} получил 3 предупреждения. У меня недостаточно прав администратора, ' \\
               f'чтобы дать ему мут'
    else:
        return f'Участник {mention} получил 3 предупреждения. Теперь он в муте'

return f'Участнику {mention} выдано предупреждение ({warns}/3)'

Команда /unwarn похожа на /warn. Если у участника уже 0 предупреждений, то бот сообщает об этом. Иначе уменьшает предупреждения на один. Если теперь предупреждений стало 2 — бот пробует размутить участника.

@admin_moderate_command('unwarn')
async def unwarn_command(chat_id: int, user_id: int, mention: str):
    member = await ChatMember.get_or_none(chat_id=chat_id, user_id=user_id)
    warns = member.warns if member else 0
    if warns == 0:
        return f'У участника {mention} уже 0/3 предупреждений'
    warns -= 1
    await update_chat_member(chat_id, user_id, warns=warns)

if warns == 2:
    try:
        await bot.edit_permissions(chat_id, user_id, send_messages=True)
    except ChatAdminRequiredError:
        return f'Предупреждение участнику {mention} отменено ({warns}/3). Он больше не должен быть в муте'
    else:
        return f'Предупреждение участнику {mention} отменено ({warns}/3). Он больше не в муте'

return f'Предупреждение участнику {mention} отменено ({warns}/3)'

В следующей части

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

Источник: https://habr.com/ru/post/583560/


Интересные статьи

Интересные статьи

Продолжаем знакомиться с инструментом pg_probackup. В первой части мы установили pg_probackup, создали и настроили экземпляр, сняли два бэкапа — полный и инкрементный в режиме...
В предыдущей статье мы научились получать описатели волнового фронта(красных пикселей).В этой статье мы разберем такую вещь, как волновая память. Для того, чтобы с минимальными по...
Сегодня поговорим о конкретной работе в области sizecoding. Дело в том, что некоторые релизы не только имеют культовый статус в узких кругах — они прямо и явно воздействовали на умы людей, застав...
Это третья, заключительная часть из цикла. В предыдущей статье мы подробно рассказали об УСН, патенте и налоге для самозанятых. В этой части рассчитаем налоговую нагрузку для ИП с доходом 100, ...
Эта картинка, за авторством Артура Кузина (n01z3), достаточно точно суммирует содержание блог поста. Как следствие, дальнейшее повествование должно восприниматься скорее как пятничная история, ...