Listeners¶
A wzgram extension
A listener waits for the next update inline, inside the code that asked for it. Instead of registering a handler, storing state keyed by user id and picking the conversation back up in a second function, you write the conversation as a conversation.
@app.on_message(filters.command("setup"))
async def setup(client, message):
answer = await message.chat.ask("What should I call you?")
await answer.reply(f"Nice to meet you, {answer.text}.")
listen and ask¶
ask() sends a question and waits for the reply.
listen() only waits:
from pyrogram import filters
reply = await app.ask(
chat_id=user_id,
text="Send me the file now.",
filters=filters.document,
timeout=120,
)
await app.download_media(reply)
Both accept the criteria that decide which update counts: chat_id, user_id,
message_id, inline_message_id — each taking a single value or a list — plus any
filters.Filter.
listener_type chooses what to wait for: ListenerTypes’s
MESSAGE (the default) or CALLBACK_QUERY.
query = await app.listen(
chat_id=chat_id,
user_id=user_id,
listener_type=enums.ListenerTypes.CALLBACK_QUERY,
timeout=60,
)
await query.answer("Got it")
wait_for_message() and
wait_for_callback_query() are thin shortcuts over the same thing, and
register_next_step_handler() runs a callback on the next matching
update instead of returning it.
Bound versions live on Chat.listen(),
Chat.ask() and
Chat.stop_listening().
Timeouts and cancellation¶
Two errors end a wait, and the difference matters:
ListenerTimeout— nobody answered in time. Retrying is reasonable.ListenerStopped— the wait was cancelled, bystop_listening()or by the client shutting down. Retrying is not.
from pyrogram.errors import ListenerTimeout
try:
answer = await message.chat.ask("Still there?", timeout=30)
except ListenerTimeout:
await message.reply("Never mind, I'll ask later.")
timeout defaults to the client’s listener_timeout, 300 seconds. Passing None
waits forever, which leaks one listener per person who wanders off — do that only when
something else is guaranteed to end the wait.
What it does to updates¶
A consumed update does not reach your handlers. That is the point: the message answering a question should not also run the command handler. Raw update handlers still see it, since they are a different contract.
The hook lives in the dispatcher rather than in the handlers, so an update probes at most three buckets keyed by peer id, and a client with no listeners pays one dictionary length check per update.
Conversations inside handlers¶
Handler callbacks are awaited inline in their dispatcher worker, so a handler parked in
await listen() is holding a worker. wzgram covers each parked worker with a relief
worker for as long as it is parked — without that, the worker-th concurrent conversation
would exhaust the pool, and the workers left would be exactly the ones needed to deliver the
updates everyone is waiting for.
You do not have to do anything for this. It is why nesting conversations works.
Limits¶
WZGRAM_MAX_LISTENERS (1000) is the process-wide ceiling on outstanding waiters, shared by
every client in the process — like every other budget in wzgram, see Performance and Resource Budgets. It
can also be set per client with max_listeners.
Expiry is one deadline heap and one reaper task per client rather than a timer per listener, which is what makes ten thousand waiters affordable.
Gotchas¶
A listener registered for a chat the client has never met takes an
intid as given — usernames are resolved at registration, ints are not. Pass a username if the peer may be unknown.stop_listeningwith no arguments stops every listener on that client. Narrow it withchat_id/user_idunless you mean all of them.Under
workers > 1two workers can race for the same listener. Only one wins, and the loser’s update falls through to the handlers rather than vanishing — so a handler may see an update you expected a listener to swallow.