Skip to content

Filters

A filter decides whether a handler sees a message. They compose:

from sunnygram import filters

@app.on_message(filters.private & filters.text & ~filters.bot)
async def handler(client, message): ...

& for both, | for either, ~ for not. The result is another filter, so there is nothing special about a combined one.

What there is

Content

Filter True when
text there is text and it is not a service message
media there is anything attached
photo the attachment is a photo
video, audio, voice, sticker the attachment is that kind of document
document there is a document of any kind
service Telegram wrote it, not a person: joins, pins, title changes

Direction and origin

Filter True when
incoming / outgoing someone else sent it / we did
reply it replies to another message
forwarded it came from somewhere else
me the sender is us
bot the sender is a bot

Where

Filter True when
private one person to another
group a group or supergroup
channel a broadcast channel

Everything and nothing

everything and nothing, which are duller than they sound: everything is the default when a handler names no filter, and nothing is a readable way to switch a handler off without deleting it.

The ones that take arguments

filters.command("start")                     # /start, and /start@somebot
filters.command(["start", "help"])           # either of them
filters.command("start", prefixes="/!")      # /start or !start
filters.command("start", to_me=True)         # only /start@somebot
filters.regex(r"^\d{4}$")
filters.user("durov", 777000)
filters.chat(-1001234567890, "somegroup")
filters.data("yes", "no")                    # a button press carrying one of these
filters.data(prefix="page:")                 # a press whose payload starts with it

to_me is for a command in a group with more than one bot in it, where the addressed form is the only one meant for you.

command and regex leave what they found on the message, so a handler reads it rather than parsing the text a second time:

@app.on_message(filters.command("say"))
async def say(client, message):
    await message.respond(" ".join(message.arguments))


@app.on_message(filters.regex(r"issue #(\d+)"))
async def issue(client, message):
    await message.reply(f"issue {message.match[1]}")

user and chat take ids or usernames, as many as you like.

Your own

Any function of a message will do:

long = filters.make(lambda client, message: len(message.text) > 500, "long")

@app.on_message(filters.text & long)
async def handler(client, message): ...

The function may be async, which you want if deciding means a call:

async def admin(client, message):
    return message.sender and message.sender.id in await admins_of(message.chat)

@app.on_message(filters.make(admin, "admin"))
async def handler(client, message): ...

Filters run for every message that reaches the dispatcher, so an async one that makes a call runs that call on every message. Cache it.

A filter that raises is reported as a handler error and the update carries on to the next handler. It does not end the update stream, which matters more than it sounds: a filter runs on updates its own handler never sees, so one bad filter would otherwise take down every other feature in the program.

Button presses

The other thing a handler can be given is a button press, and most of these work on one unchanged. A press says who pressed it and in which chat, so user, chat, private, group, channel, me and bot all read what they always read. So does regex, because a press has text: the payload the button was built with.

@app.on_callback_query(filters.data(prefix="page:") & filters.group)
async def turn(client, press): ...

The filters asking what kind of media a message carries have nothing to ask a press, and putting one on on_callback_query is reported as a handler error, not quietly matching nothing.

Signature by signature: Filters reference.