Commands

Anjani Command Handler returns 2 positional arguments:

  1. self The instance of the Sub Plugin Class
  2. ctx The instance of ~command.Context that constructed for every command the bot received.

If you want to use or interact with the ~pyrogram.Client you can use as example below here.

from anjani import command, plugin

class ExamplePlugin(plugin.Plugin):
    name = "Example Plugin"

    async def cmd_test(self, ctx: command.Context) -> None:
        await self.bot.client.send_message(ctx.chat.id, "Hii...")
        # self.bot == ~Anjani
        # self.bot.client == ~pyrogram.Client
        # This method is must instead of importing the client itself.

Commands are defined by attaching it to a regular Python function. The command is invoked by the user who send a message similar to the function name. For example, in the given command:

async def cmd_hi(self, ctx):
    await ctx.respond("Hello")

so that the command will invoked when a user send a /hi.

await ctx.respond() here is the same as message.reply_text() with a few modification from us. You can see it here

In addition, if the string is the end of the execution, you can just return the string from command handler and we will handle the ctx.respond() for you. E.G:

async def cmd_hi(self, ctx):
    return "Hello"

This will make the bot send the same message as await ctx.respond("Hello") above.

Parameters

Since we create a command with Python function name, we also have argument passing behaviour by the function parameters. Note that only command function that have this ability. An event listener will not have this feature.

Positional

The most basic parameters is a positional parameter. Like the name, positional parameter works by the positional text of the invoker message seperated by space.

async def cmd_test(self, ctx, arg):
    await ctx.respond(arg)

positional parameters

positional parameters 2

There are no limitation on this kind of parameter. You can have it as much as you want. The parameter is default to NoneType if the command message have no argument. You can also assign a default value to the parameters.

Keyword-Only Arguments

You can use this when you want to parse the rest of the message text to become one variable.

async def cmd_test(self, ctx, *, arg):
    await ctx.respond(arg)

Keyword-Only Arguments

By default, the Keyword-Only arguments are stripped from whitespace. Because of that the parameter is default to empty string ("") unlike the positional parameters. And take a note that you can only have one keyword-only argument for each command.

Converters

We try our best to make plugin creation easy. Sometimes a commands need an argument to make it properly. Hence, we're introducing a Converters. Converters comes in several types:

  • A callable object that takes an argument and returns with a different type.
    • This can be your own function or something like bool and int.
  • A custom converter class that inherits anjani.util.converter.Converter

We specify a converters by using something called a function annotation introduced on PEP 3107.

Basic Converter

A basic converter is a callable that takes in an argument and change it into something else.

For example, if we wanted to add two numbers together, we could request that they are turned into integers for us by specifying the converter:

async def cmd_add(self, ctx, num1: int, num2: int):
    await ctx.respond(num1 + num2)

Bool

To prevent ambiguities of bool E.G: when a non-empty string evaluated as True. We modify this a bit so that it evaluate based on the content of the argument as you can see below.

if arg in ("yes", "true", "enable", "on", "1"):
    return True
if arg in ("no", "false", "disable", "off", "0"):
    return False

So later you can have the code

async def cmd_test(self, ctx, is_true: bool):
    if is_true:
        # do something

If the bool converter failed to detect the args it will return an exception ~anjani.error.BadBoolArgument

Callable

Converter also works with any callable that takes a single parameter.

def to_lower(arg):
    return arg.lower()

async def cmd_test(self, ctx, text: to_lower):
    await ctx.respond(text)

You are free to modify an argument on that function. You also have a choice to use async (async def) or sync (def) function.

Advanced Converter

Sometimes the basic converter doesn't have enough information you need. For example you need the command.Context. In that case we provide a Converter class to do all that jobs. Defining a a custom converter of this require to have the Asynchronous Converter.__call__ method.

An example of advanced converter:

from anjani.utils.converter import Converter

class MediaGroup(Converter):

    async def __call__(self, ctx: command.Context):
        replied_id = ctx.message.reply_to_message.id
        chat_id = ctx.chat.id
        return await ctx.bot.client.get_media_group(chat_id, replied_id)


class Testing(plugin.Plugin):
    name = "Testing"

    async def cmd_test(self, ctx, arg: MediaGroup):
        print(arg)
        for i in arg:
            await ctx.respond(arg)

Pyrogram Converter

We provide a several pyrogram types converter. This converter might be have the most use case on the commands:

  1. User
  2. Chat
  3. ChatMember

Under the hood, those are implemented with Advanced Converter as above. Below are our converter of those pyrogram types:

Pyrogram TypesConverter
UserUserConverter
ChatChatConverter
ChatMemberChatMemberConverter

Use example of Pyrogram Converters:

from pyrogram.types import ChatMember

async def cmd_test(self, ctx, member: ChatMember):
    await ctx.respond(f"User joined on {member.joined_date}")

Note When our pyrogram converters failes, it will pass an exception ~anjani.error.ConversionError. If you don't want this to happen you can specifiy a default value so the converter will pass the default value instead of Exception. E.G: async def cmd_test(self, ctx, user: Optional[pyrogram.types.User] = None)

You can also use the converter we provide to use in your custom converter:

from anjani.util import converter

class UserFullname(converter.UserConverter):
    async def __call__(self, ctx):
        user = await super().__call__(ctx)
        user.fullname = user.first_name + (user.last_name or "")   # add fullname attribute to user
        return user


async def cmd_test(self, ctx, member: UserFullname):
    await ctx.respond(f"User full name: {member.fullname}")

Typing

If you're working with type hinting, we have two accepted type hinting typing.Union and typing.Optional. But currently we only convert to one of the types with left-to-right priority.

async def cmd_test(self, ctx, user: typing.Optional[pyrogram.types.User]):
    # Do something
async def cmd_test(self, ctx, user: typing.Union[pyrogram.types.User, None]):
    # Do something

async def cmd_test(self, ctx, user: typing.Union[None, pyrogram.types.User]):
    # Do something

async def cmd_test(self, ctx, user: typing.Union[pyrogram.types.User, pyrogram.types.ChatMember]):  # This will only convert to `User`
    # Do something

Default Value

If you want to have a default value apart from the default value we provide (None), you can give a default value by your own.

async def cmd_test(self, ctx, number: int = 0):
    # Do something

Important notes

By default, the Converter exception is not raised and passed to the args. If the args have the default value set it will overwrite the exception.

For example

# input command -> /test hi

async def cmd_test(self, ctx, val: bool):  # val here will be ~anjani.error.BadBoolArgument
    # do something

async def cmd_test(self, ctx, val: bool = false):  # val here will be false even if the conversion is failed
    # do something