Commands and Handlers

Astra uses a decorator pattern to register message handlers. Each handler is an async function that runs when an incoming message matches a filter.

Registering a handler

Use the @client.on_message() decorator with a filter:

from astra import Client, Filters

async with Client(session_id="bot") as client:

 @client.on_message(Filters.command(".ping"))
 async def ping(msg):
  await msg.respond("Pong!")

 @client.on_message(Filters.command(".help"))
 async def help_cmd(msg):
  await msg.respond(
   "*Commands*\n"
   "`.ping` -- check if bot is alive\n"
   "`.echo <text>` -- echo your text back\n"
   "`.help` -- show this message"
  )

 await client.run_forever()

Automated Plugin Loading

For complex projects, you can organize your handlers into a directory (e.g., commands/) and load them all at once:

import os
from astra import Client

async with Client(session_id="bot") as client:
 # Load all plugin modules in the 'commands' folder
 client.load_plugins("commands")

 await client.run_forever()

In your plugin files, use the class-level @Client.on_message decorator:

from astra import Client, Filters

@Client.on_message(Filters.command(".hello"))
async def hello(client, msg):
 await msg.respond("Hello from a plugin!")

The msg parameter

Every handler receives a Message object with these useful attributes:

And these helper methods:

await msg.respond("text")  # Send in the same chat
await msg.reply("text")   # Quote-reply the original message
await msg.react("👍")   # React with an emoji
await msg.delete(for_everyone=True) # Delete the message

Building a command system

A practical pattern for bots with many commands:

@client.on_message(Filters.command(".echo"))
async def echo(msg):
 # Strip the command prefix to get the argument
 text = msg.text.replace(".echo", "", 1).strip()
 if not text:
  await msg.respond("Usage: `.echo <text>`")
  return
 await msg.reply(text)

@client.on_message(Filters.command(".react"))
async def react(msg):
 emoji = msg.text.replace(".react", "", 1).strip() or "👍"
 if msg.quoted:
  await msg.quoted.react(emoji)
 else:
  await msg.respond("Reply to a message with `.react <emoji>`")

@client.on_message(Filters.command(".delete"))
async def delete(msg):
 if msg.quoted:
  await msg.quoted.delete(for_everyone=True)
  await msg.delete(for_everyone=True)

Multiple filters

You can register multiple handlers – they are checked in registration order and the first matching handler runs:

@client.on_message(Filters.command(".admin"))
async def admin_only(msg):
 """Only fires if message starts with .admin"""
 ...

@client.on_message(Filters.text_contains("hello"))
async def greet(msg):
 """Fires for any message containing 'hello'"""
 await msg.respond("Hey!")

Error handling in handlers

If a handler raises an exception, Astra logs it but keeps running. For critical operations, wrap your logic in try/except:

from astra.errors import MessageSendError

@client.on_message(Filters.command(".send"))
async def send(msg):
 try:
  await client.chat.send_message("target@c.us", "Hello")
  await msg.respond("Sent!")
 except MessageSendError as e:
  await msg.respond(f"Failed: {e.hint}")