Команды

Один из самых привлекательных аспектов библиотеки - простота создания команд и богатые настройки для них

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

К примеру, в данной команде:

@bot.command()
async def foo(ctx, arg):
    await ctx.send(arg)

С данным префиксом ($), команда будет вызвана пользователем с помощью:

$foo abc

Команда всегда должна иметь как минимум один параметр, ctx, который является экземпляром Context как свой первый параметр.

Есть 2 способа зарегистрировать команду. Первый - используя декоратор Bot.command() как в примере выше. Второй - с помощью декоратора command() и метода Bot.add_command()

По сути, они аналогичны:

import vk_botting

bot = vk_botting.Bot(command_prefix='$')

@bot.command()
async def test(ctx):
    pass

# or:

@vk_botting.command()
async def test(ctx):
    pass

bot.add_command(test)

Так как декоратор Bot.command() короче и удобнее, он будет использоваться во всей документации

Любой параметр, принимаемый конструктором класса Command, может быть передан в декоратор. К примеру, для смены названия команды на что-то отличное от имени функции надо просто объявить ее так:

@bot.command(name='list')
async def _list(ctx, arg):
    pass

Параметры

Так как команды являются, по сути, функциями, мы можем в том числе объявить характер парсинга аргументов функцией

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

Позиционные

Самый базовый вид параметра - позиционный. Это когда параметр передается как есть:

@bot.command()
async def test(ctx, arg):
    await ctx.send(arg)

На стороне пользователя, передать параметр можно просто передавая обычную строку

Так как позиционные аргументы - это просто обычные аргументы, их может быть сколь угодно много

@bot.command()
async def test(ctx, arg1, arg2):
    await ctx.send('You passed {} and {}'.format(arg1, arg2))

Переменные

Иногда необходимо, чтобы пользователь мог отправить неопределенное число параметров. Библиотека делает это аналогично тому, как списки параметров делаются в Python

@bot.command()
async def test(ctx, *args):
    await ctx.send('{} arguments: {}'.format(len(args), ', '.join(args)))

Это позволяет пользователю передавать один или множество аргументов по желанию

Учтите, что так как команды работают аналогично функциям, пользователь может вообще не передавать аргументы

Так как переменная args - типа tuple, с ней можно делать все, что и с ним

Аргументы с ключевыми словами

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

@bot.command()
async def test(ctx, *, arg):
    await ctx.send(arg)

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

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

По умолчанию такие аргументы автоматически очищаются от пробелов на краях для удобства, но это можно поменять с помощью передачи атрибута Command.rest_is_raw в декоратор

Контекст Вызова

Как было показано ранее, все команды должны иметь как минимум один параметр, называемый контекстом (context.Context)

Этот параметр дает вам доступ к так называемому «контексту вызова». По сути он содержит всю информацию, необходимую для понимания, как и где была вызвана команда. Например:

  • Context.from_id - id автора сообщения

  • Context.peer_id - id диалога, в котором пришло сообщение

  • Context.get_user() возвращает автора как экземпляр класса User

  • Context.send() можно использовать для отправки сообщений в диалог, в котором была использована команда

Контекст является реализацией класса abstract.Messageable, так что все, что можно делать с ним, можно делать и с context.Context

Преобразователи

Аргументы бота как параметры для функции - только первый шаг на пути построения командного интерфейса бота. Для удобства использования аргументов, их обычно стоит преобразовать в нужный тип. Это можно сделать с помощью преобразователей.

Преобразователи бывают разными:

  • Обычная функция, которая принимает как аргумент один параметр и возвращает его другого типа.

    • Они могут быть как вашими собственными функциями, так и, к примеру, bool или int.

  • Собственный класс должен быть наследником conversions.Converter

Базовые преобразователи

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

К примеру, если надо добавить два числа вместе, то можно указать, что они должны быть целыми числами:

@bot.command()
async def add(ctx, a: int, b: int):
    await ctx.send(a + b)

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

Это работает со всеми функциями. Например, для преобразования строки в верхний регистр:

def to_upper(argument):
    return argument.upper()

@bot.command()
async def up(ctx, *, content: to_upper):
    await ctx.send(content)

Логические переменные

В отличии от других базовых преобразователей, преобразование в bool работает немного по другому. Вместо использования типа bool напрямую, он преобразовывает аргумент в True или False в зависимости от его значения:

if lowered in ('yes', 'y', 'true', 't', '1', 'enable', 'on', 'да', 'включить', 'правда'):
    return True
elif lowered in ('no', 'n', 'false', 'f', '0', 'disable', 'off', 'нет', 'выключить', 'ложь'):
    return False

Продвинутые Преобразователи

Обычно простого преобразователя может не хватить для наших нужд. Например, если требуется получить дополнительную информацию от сообщения (Message), вызвавшего команду или требуется какая-то асинхронная обработка.

В таком случае стоит использовать собственный преобразователь как наследника conversions.Converter. Это позволяет получить доступ к Context и объявлять функцию как асинхронную. Это требует замены одного метода - Converter.convert()

Пример преобразователя:

import random

class Slapper(commands.Converter):
    async def convert(self, ctx, argument):
        to_slap = random.choice(['foo', 'bar'])
        return '{0.from_id} slapped {1} because *{2}*'.format(ctx, to_slap, argument)

@bot.command()
async def slap(ctx, *, reason: Slapper):
    await ctx.send(reason)

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

@bot.command()
async def slap(ctx, *, reason: Slapper):
    await ctx.send(reason)

# is the same as...

@bot.command()
async def slap(ctx, *, reason: Slapper()):
    await ctx.send(reason)

Возможность передавать экземпляр класса требуется для возможного использования __init__ для более детальной настройки преобразователя.

Если преобразователю не удается преобразовать аргумент, то вызывается ошибка BadArgument.

Обработка Ошибок

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

Чтобы обрабатывать ошибки, необходимо использовать так называемый обработчик ошибок. Глобальный обработчик ошибок (on_command_error()) работает как и любое другое событие в Справка по событиям и вызывается при каждой ошибке.

Большую часть времени, однако, требуется обрабатывать ошибку, которая возникает в конкретной команде. В таком случае можно объявить команде локальный обработчик ошибок с помощью Command.error().

@bot.command()
async def info(ctx):
    """Tells you some info about the author."""
    user = await ctx.get_user()
    fmt = '{0.first_name} was last seen on {0.last_seen}.'
    await ctx.send(fmt.format(user))

@info.error
async def info_error(ctx, error):
    if isinstance(error, commands.BadArgument):
        await ctx.send('Why is it here. It takes no arguments :thonk:')

Первым параметром этого обработчика всегда должен быть контекст (Context), в то время как второй - ошибка, являющаяся наследником CommandError. Список ошибок можно найти на странице Ошибки.

Проверки

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

Проверка - функция, принимающая на вход лишь контекст (Context). Внутри нее можно сделать следующее:

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

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

  • Вызвать CommandError или ее наследника, чтобы показать, что пользователь не может использовать команду.

    • Это позволяет иметь пользовательские сообщения об ошибках для обработки их с помощью обработчиков ошибок.

Привязать проверку к команде можно двумя способами. Первый - используя декоратор limiters.check(). К примеру:

async def is_owner(ctx):
    return ctx.author == 1234567890

@bot.command(name='eval')
@limiters.check(is_owner)
async def _eval(ctx, *, code):
    """A bad example of an eval command"""
    await ctx.send(eval(code))

Этот пример вызовет команду только если функция is_owner вернет True. Иногда, в случае частого использования проверки, ее можно превратить в декоратор. Для этого просто надо добавить еще один уровень вложенности:

def is_owner():
    async def predicate(ctx):
        return ctx.author == 1234567890
    return limiters.check(predicate)

@bot.command(name='eval')
@is_owner()
async def _eval(ctx, *, code):
    """A bad example of an eval command"""
    await ctx.send(eval(code))

Так как он часто используется, библиотека предоставляет заранее готовый декоратор для проверки наличия пользователя в списке (limitest.in_user_list()):

@bot.command(name='eval')
@limiters.in_user.list(1234567890)
async def _eval(ctx, *, code):
    """A bad example of an eval command"""
    await ctx.send(eval(code))

Когда объявлено несколько проверок, все они должны вернуть True:

def in_conversation(guild_id):
    async def predicate(ctx):
        return ctx.peer_id != ctx.from_id
    return commands.check(predicate)

@bot.command()
@limiters.in_user.list(1234567890)
@in_conversation()
async def secretdata(ctx):
    """super secret stuff"""
    await ctx.send('secret stuff')

В примере выше, если любая из проверок провалится, команда не будет работать

Когда происходит ошибка, она передается обработчикам ошибок. Если вы не вызвали пользовательскую ошибку, являющуюся CommandError или ее наследником, то ошибка будет обернута в CheckFailure и ее можно обработать так:

@bot.command()
@limiters.in_user.list(1234567890)
@in_conversation()
async def secretdata(ctx):
    """super secret stuff"""
    await ctx.send('secret stuff')

@secretdata.error
async def secretdata_error(ctx, error):
    if isinstance(error, exceptions.CheckFailure):
        await ctx.send('nothing to see here comrade.')

Глобальные Проверки

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

Глобальные проверки работают аналогично обычным, но регистрируются с помощью декоратора Bot.check()

К примеру, чтобы заблокировать все личные сообщения, можно сделать следующее:

@bot.check
async def globally_block_dms(ctx):
    return ctx.from_id != ctx.peer_id

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

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