Commands
Anjani Command Handler returns 2 positional arguments:
self
The instance of the Sub Plugin Class- 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 asmessage.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)
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)
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
andint
.
- This can be your own function or something like
- 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:
Under the hood, those are implemented with Advanced Converter as above. Below are our converter of those pyrogram types:
Pyrogram Types | Converter |
---|---|
User | UserConverter |
Chat | ChatConverter |
ChatMember | ChatMemberConverter |
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 ofException
. 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