πŸ“₯ Listeners#

When handling updates, most of the time you ask the user for input (e.g. a reply, text, button press, etc.). This is where listeners come in. With listeners, you can create an inline handler that waits for a specific user input and returns the result.

Listening#

In this example, we will create a listener that waits for the user to send their age. The listener will wait for a text-digit message from the user and then reply with a message based on the age provided.

 1from pywa import WhatsApp, types, filters
 2
 3wa = WhatsApp(...)
 4
 5@wa.on_message(filters.command("start"))
 6def start(client: WhatsApp, msg: types.Message):
 7    sent = msg.reply("Hello! How old are you?")
 8    age_reply: Message = sent.wait_for_reply(
 9        filters=filters.text & filters.new(lambda _, m: m.text.isdigit())
10    )
11    age = int(age_reply.text)
12    if age < 18:
13        age_reply.reply("You are too young to use this service.")
14        # Handle the case when the user is too young
15    else:
16        age_reply.reply("Welcome! You can now use the service.")
17        # Handle the case when the user is old enough

In the example above, we storing the sent message in the variable sent. Then, we used the wait_for_reply() method to create a listener that waits for a reply from the user. The listener will wait for a message that matches the filter filters.text & filters.new(lambda _, m: m.text.isdigit()), which means it will wait for a text message that contains only digits. When the user sends a message that matches the filter, the listener will return the message as a Message object, which we store in the variable age_reply. We then convert the text of the message to an integer and check if the user is old enough to use the service.

Canceling#

Now, listeners are blocking. This means that the code execution will stop until the listener returns a result. However, you can cancel the listener if you want to stop waiting for a reply. For example, you can add a button to the message that the user can press to cancel the listener or you can set a timeout for the listener to stop waiting after a certain period of time.

 1from pywa import WhatsApp, types, filters
 2
 3wa = WhatsApp(...)
 4
 5@wa.on_message(filters.command("start"))
 6def start(_: WhatsApp, msg: types.Message):
 7    sent = msg.reply("Hello! How old are you?", buttons=[types.Button(title="Cancel", callback_data="cancel")])
 8    age_reply = sent.wait_for_reply(
 9        filters=filters.text & filters.new(lambda _, m: m.text.isdigit()),
10        cancelers=filters.callback_button & filters.matches("cancel"),
11        timeout=60,
12    )
13    ...

In the example above, we added a button to the message that the user can press to cancel the listener. We also set a timeout of 60 seconds for the listener. If the user presses the cancel button or if the listener times out, the listener will stop waiting for a reply and raise an exception.

Handling cancel and timeout#

When a listener is canceled or times out, it raises an exception. Most of the time, you will want to handle these exceptions to provide a better user experience. PyWa provides two exceptions for this purpose: ListenerCanceled and ListenerTimeout. You can use these exceptions to handle the cancel and timeout cases in your code. Let’s see an example:

 1from pywa import WhatsApp, types, filters
 2
 3wa = WhatsApp(...)
 4
 5
 6@wa.on_message(filters.command("start"))
 7def start(_: WhatsApp, msg: types.Message):
 8    try:
 9        age_reply = msg.reply(
10            text="Hello! How old are you?",
11            buttons=[types.Button(title="Cancel", callback_data="cancel")],
12        ).wait_for_reply(
13            filters=filters.text & filters.new(lambda _, m: m.text.isdigit()),
14            cancelers=filters.callback_button & filters.matches("cancel"),
15            timeout=60,
16        )
17    except types.ListenerCanceled:
18        msg.reply("You canceled the operation by clicking the cancel button.")
19        return
20    except types.ListenerTimeout:
21        msg.reply("You took too long to respond. Please try again later.")
22        return
23    ...

In the example above, we used a try-except block to handle the ListenerCanceled and ListenerTimeout exceptions. If the user cancels the listener by clicking the cancel button, we send a message to inform them that they canceled the operation. If the listener times out, we send a message to inform the user that they took too long to respond. If the listener returns a result, we can continue processing the user’s input as usual.

Custom listeners#

You can create custom listeners by using the raw WhatsApp.listen() method. This method allows you to create a listener that waits for a specific update and returns the result when the update is received.

For example, let’s create a listener that waits for another user to enter the bot and become an admin. We will create a simple database to store the users and admins, and then we will create a listener that waits for a user to enter the bot and adds them as an admin if they are not already registered.

 1from pywa import WhatsApp, types, filters, listeners
 2
 3wa = WhatsApp(...)
 4
 5class Database:
 6    def __init__(self):
 7        self._users = []
 8        self._admins = set()
 9
10    def add_user(self, user: str):
11        if user not in self._users:
12            self._users.append(user)
13
14    def is_user_exists(self, user: str) -> bool:
15        return user in self._users
16
17    def add_admin(self, admin: str):
18        if admin not in self._admins:
19            self._admins.add(admin)
20            self.add_user(admin)
21
22    def is_admin(self, user: str) -> bool:
23        return user in self._admins
24
25db = Database()
26db.add_admin("my_phone_number")  # Add your phone number as an default admin
27
28@wa.on_message(filters.command("add_admin") & filters.new(lambda _, msg: db.is_admin(msg.sender)))
29def add_admin(client: WhatsApp, msg: types.Message):
30    _, new_admin_phone = msg.text.split(maxsplit=1)  # command is /add_admin <phone_number>
31    if not db.is_user_exists(new_admin_phone):
32        msg.reply("This user is not registered with the bot. Please ask them to enter the bot first.")
33        new_chat: types.ChatOpened = client.listen(
34            to=listeners.UserUpdateListenerIdentifier(
35                sender=new_admin_phone, recipient=client.phone_id
36            ),
37            filters=filters.chat_opened,
38        )
39        new_chat.reply(f"Hi {new_chat.from_user.name}, you have been added as an admin.")
40        msg.reply(f"{new_chat.from_user.name} entered the bot, you are now an admin.")
41    else:
42        db.add_admin(new_admin_phone)
43        msg.reply(f"{new_admin_phone} is now an admin.")

Attention

If the listener did not use the update (the update not matched the filters or the cancelers), the update will be passed to the handlers. This means that the update can be processed by other handlers that are registered to handle the same update type. This behavior changed since version 3.0.0, before that - when update was not used by the listener - it was ignored and not passed to the handlers.

If you must prevent the update from being passed to the handlers, call the stop_handling() method on the update inside the filters or the cancelers (it will not affect the listener behavior, just prevent the update from being passed to the handlers).

Shortcuts#

PyWa provides a few shortcuts to create listeners when sending messages. Let’s see an example:

1from pywa import WhatsApp, types, filters
2
3wa = WhatsApp(...)
4
5@wa.on_message(filters.command("start"))
6def start(client: WhatsApp, msg: types.Message):
7    age: types.Message = m.reply("Hello! How old are you?").wait_for_reply(filters.text)
8    m.reply(f"You are {age.text} years old")

In the example above, we used the wait_for_reply() method to create a listener that waits for a text reply from the user.

1from pywa import WhatsApp, types, filters
2
3wa = WhatsApp(...)
4
5@wa.on_message(filters.command("start"))
6def start(client: WhatsApp, msg: types.Message):
7    msg.reply(f"Hello {msg.from_user.name}!").wait_until_delivered()
8    msg.reply("How can I help you?")

In the example above, we used the wait_until_delivered() method to create a listener that waits until the message is delivered to the user.

Other shortcuts are available, such as wait_for_click(), wait_for_selection(), wait_until_read(), and more.