Продолжаем писать своего крутого бота-модератора чатов на Python. Все части туториала:
Часть 1. Создание бота
Часть 2. Проверка админов
Часть 3. Команды для модерации
Полный код для этой части на GitHub
В этой части мы сделаем команды для модерации. Админы чата смогут банить участников, запрещать им писать в чате, давать предупреждения с помощью команд /ban, /mute, /warn.
Некоторые боты-администраторы используют не слэш-команды, а команды через, например, восклицательный знак: !ban, !mute. Но мы будем использовать слэш: это "нативные" команды в Телеграме. Они подсвечиваются в сообщениях, и их можно добавить в список команд для автодополнения.
Как обрабатывать команды
Каждую команду будем обрабатывать отдельной функцией.
Когда кто-то отправляет команду, бот должен:
Проверить, что этот пользователь админ.
Выполнить нужное действие.
Если сделать нужное действие не получилось из-за того, что у бота недостаточно прав администратора — сообщить об этом.
Понятно, что не стоит писать все эти проверки заново для каждой новой функции. Вместо этого мы сделаем декоратор. Использовать его можно будет так:
@admin_command('greet')
async def greet_command(event: Message):
await event.respond('Привет')
После применения декоратора admin_command(command)
к функции greet_command
, она будет обрабатывать команду /greet (и добавятся нужные проверки).
Давайте напишем функцию admin_command
в utils.py
. Тут может быть немного сложно, поэтому по шагам.
Функция
admin_command(command)
возвращает нужный декоратор:def admin_command(command: str): def decorator(func): # возвращает новую функцию return decorator
Внутри функции
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
Тут мы как раз проверяем, что пользователь — админ и что у бота есть нужные права.
Новую функцию
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
Команды модерации
Сейчас мы хотим реализовать команды для модерации: для бана, мута, предупреждений участников. Все эти команды будут отправляться в ответ на сообщение того пользователя, к которому применяется действие (как на первой картинке).
Кроме проверок, которые мы реализовали выше, для каждой такой команды нужно:
Проверить, что эта команда отправлена ответом на другое сообщение.
Проверить, что отправитель того сообщения не администратор.
Выполнить нужное действие с отправителем.
И да — для этого мы напишем ещё один декоратор. Вот пример его использования:
@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
:
Принимает айди чата, айди пользователя и упоминание пользователя.
Изменяет разрешения участника в чате (чтобы он не мог отправлять сообщения).
Возвращает текст сообщения о том, что команда выполнена: "Участник такой-то помещён в мут". Для этого и используется упоминание пользователя.
Если что, упоминание — это элемент разметки сообщений (ссылка вида 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
.
Функция возвращает декоратор:
def admin_moderate_command(command: str): def decorator(func): # возвращает новую функцию return decorator
Декоратор возвращает новую функцию:
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
с нужными аргументами. Потом отправляет сообщение с результатом.Команду 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)'
В следующей части
Мы сделаем более сложные команды для админов и сообщения с кнопками. Админы смогут удалять последние сообщения в чате, изменять приветствие в чате и другие настройки.