From f0c42c376551e0bf830f4d59a1a32481f47c7565 Mon Sep 17 00:00:00 2001 From: WOUTER DURNEZ Date: Mon, 13 Jul 2026 11:44:45 +0200 Subject: [PATCH 1/7] [callback] treat missing message context as no reply_to_message MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit WhatsApp does not always include the context field on incoming template quick-reply button presses (observed in production on Cloud API webhooks that also carry the new user_id/from_user_id fields). CallbackButton.from_update and CallbackSelection.from_update index msg["context"] unconditionally, so such updates raise KeyError: 'context' and are dropped by the server's update constructor — the handler never fires and, since 200 is returned, Meta never redelivers. Parse context the same way Message.from_update already does: treat it as optional and set reply_to_message=None when absent. Co-Authored-By: Claude Fable 5 --- pywa/types/callback.py | 18 ++++++--- pywa_async/types/callback.py | 4 +- tests/data/updates/callback_button.json | 43 +++++++++++++++++++++ tests/data/updates/callback_selection.json | 45 ++++++++++++++++++++++ tests/test_updates.py | 23 +++++++++-- 5 files changed, 122 insertions(+), 11 deletions(-) diff --git a/pywa/types/callback.py b/pywa/types/callback.py index 3eee0953..d483c472 100644 --- a/pywa/types/callback.py +++ b/pywa/types/callback.py @@ -258,14 +258,14 @@ class CallbackButton(BaseUserUpdate, Generic[_CallbackDataT]): type: The message type (:class:`MessageType.INTERACTIVE` for :class:`~pywa.types.callback.Button` presses or :class:`MessageType.BUTTON` for :class:`~pywa.types.templates.QuickReplyButton` clicks). from_user: The user who sent the message. timestamp: The timestamp when the message was sent (in UTC). - reply_to_message: The message to which this callback button is a reply to. + reply_to_message: The message to which this callback button is a reply to (``None`` when WhatsApp omits the ``context`` field). data: The data of the button (the ``callback_data`` parameter you provided in :class:`~pywa.types.callback.Button` or :class:`~pywa.types.templates.QuickReplyButton`). title: The title of the button. shared_data: Shared data between handlers. """ type: MessageType - reply_to_message: ReplyToMessage + reply_to_message: ReplyToMessage | None data: _CallbackDataT title: str @@ -286,6 +286,7 @@ def from_update(cls, client: "WhatsApp", update: RawUpdate) -> "CallbackButton": data = msg["button"]["payload"] case _: raise ValueError(f"Invalid message type {msg_type}") + context = msg.get("context", {}) return cls( _client=client, raw=update, @@ -295,7 +296,9 @@ def from_update(cls, client: "WhatsApp", update: RawUpdate) -> "CallbackButton": type=MessageType(msg_type), from_user=client._usr_cls.from_contact(value["contacts"][0], client=client), timestamp=helpers.timestamp_to_datetime(msg["timestamp"]), - reply_to_message=ReplyToMessage.from_dict(msg["context"]), + reply_to_message=ReplyToMessage.from_dict(context) + if context.get("id") + else None, data=data, title=title, ) @@ -354,14 +357,14 @@ class CallbackSelection(BaseUserUpdate, Generic[_CallbackDataT]): type: The message type (always :class:`MessageType.INTERACTIVE`). from_user: The user who sent the message. timestamp: The timestamp when the message was sent (in UTC). - reply_to_message: The message to which this callback selection is a reply to. + reply_to_message: The message to which this callback selection is a reply to (``None`` when WhatsApp omits the ``context`` field). data: The data of the selection (the ``callback_data`` parameter you provided in :class:`~pywa.types.callback.SectionRow`). title: The title of the selection. description: The description of the selection (optional). """ type: MessageType - reply_to_message: ReplyToMessage + reply_to_message: ReplyToMessage | None data: _CallbackDataT title: str description: str | None @@ -374,6 +377,7 @@ def from_update(cls, client: "WhatsApp", update: RawUpdate) -> "CallbackSelectio msg = (value := (entry := update["entry"][0])["changes"][0]["value"])[ "messages" ][0] + context = msg.get("context", {}) return cls( _client=client, raw=update, @@ -383,7 +387,9 @@ def from_update(cls, client: "WhatsApp", update: RawUpdate) -> "CallbackSelectio type=MessageType(msg["type"]), from_user=client._usr_cls.from_contact(value["contacts"][0], client=client), timestamp=helpers.timestamp_to_datetime(msg["timestamp"]), - reply_to_message=ReplyToMessage.from_dict(msg["context"]), + reply_to_message=ReplyToMessage.from_dict(context) + if context.get("id") + else None, data=msg["interactive"]["list_reply"]["id"], title=msg["interactive"]["list_reply"]["title"], description=msg["interactive"]["list_reply"].get("description"), diff --git a/pywa_async/types/callback.py b/pywa_async/types/callback.py index bf5b3a74..0e6e5d8c 100644 --- a/pywa_async/types/callback.py +++ b/pywa_async/types/callback.py @@ -46,7 +46,7 @@ class CallbackButton(BaseUserUpdateAsync, _CallbackButton[_CallbackDataT]): type: The message type (:class:`MessageType.INTERACTIVE` for :class:`~pywa.types.callback.Button` presses or :class:`MessageType.BUTTON` for :class:`~pywa.types.templates.QuickReplyButton` clicks). from_user: The user who sent the message. timestamp: The timestamp when the message was sent (in UTC). - reply_to_message: The message to which this callback button is a reply to. + reply_to_message: The message to which this callback button is a reply to (``None`` when WhatsApp omits the ``context`` field). data: The data of the button (the ``callback_data`` parameter you provided in :class:`~pywa.types.callback.Button` or :class:`~pywa.types.templates.QuickReplyButton`). title: The title of the button. shared_data: Shared data between handlers. @@ -95,7 +95,7 @@ class CallbackSelection(BaseUserUpdateAsync, _CallbackSelection[_CallbackDataT]) type: The message type (always :class:`MessageType.INTERACTIVE`). from_user: The user who sent the message. timestamp: The timestamp when the message was sent (in UTC). - reply_to_message: The message to which this callback selection is a reply to. + reply_to_message: The message to which this callback selection is a reply to (``None`` when WhatsApp omits the ``context`` field). data: The data of the selection (the ``callback_data`` parameter you provided in :class:`~pywa.types.callback.SectionRow`). title: The title of the selection. description: The description of the selection (optional). diff --git a/tests/data/updates/callback_button.json b/tests/data/updates/callback_button.json index bc69fa45..6928a60c 100644 --- a/tests/data/updates/callback_button.json +++ b/tests/data/updates/callback_button.json @@ -93,5 +93,48 @@ ] } ] + }, + "quick_reply_without_context": { + "object": "whatsapp_business_account", + "entry": [ + { + "id": "5467539754836534", + "changes": [ + { + "value": { + "messaging_product": "whatsapp", + "metadata": { + "display_phone_number": "972123456789", + "phone_number_id": "1122334455667" + }, + "contacts": [ + { + "profile": { + "name": "Test Name" + }, + "wa_id": "972987654321", + "user_id": "US.13491208655302741918", + "parent_user_id": "US.ENT.11815799212886844830" + } + ], + "messages": [ + { + "from": "972987654321", + "from_user_id": "US.13491208655302741918", + "id": "wamid.xsidjx", + "timestamp": "1698269027", + "type": "button", + "button": { + "payload": "callback_data", + "text": "title" + } + } + ] + }, + "field": "messages" + } + ] + } + ] } } diff --git a/tests/data/updates/callback_selection.json b/tests/data/updates/callback_selection.json index 439a1e88..15ab6eb6 100644 --- a/tests/data/updates/callback_selection.json +++ b/tests/data/updates/callback_selection.json @@ -97,5 +97,50 @@ ] } ] + }, + "callback_without_context": { + "object": "whatsapp_business_account", + "entry": [ + { + "id": "1234567890987654321", + "changes": [ + { + "value": { + "messaging_product": "whatsapp", + "metadata": { + "display_phone_number": "972123456789", + "phone_number_id": "1122334455667" + }, + "contacts": [ + { + "profile": { + "name": "Test Name" + }, + "wa_id": "972987654321", + "user_id": "US.13491208655302741918", + "parent_user_id": "US.ENT.11815799212886844830" + } + ], + "messages": [ + { + "from": "972987654321", + "id": "wamid.zyzxyw", + "timestamp": "1698266905", + "type": "interactive", + "interactive": { + "type": "list_reply", + "list_reply": { + "id": "callback_data", + "title": "title" + } + } + } + ] + }, + "field": "messages" + } + ] + } + ] } } diff --git a/tests/test_updates.py b/tests/test_updates.py index 59edce54..9da62f26 100644 --- a/tests/test_updates.py +++ b/tests/test_updates.py @@ -66,12 +66,29 @@ "media_with_url": [lambda m: m.media.url is not None], }, "callback_button": { - "button": [lambda b: b.type == MessageType.INTERACTIVE], - "quick_reply": [lambda b: b.type == MessageType.BUTTON], + "button": [ + lambda b: b.type == MessageType.INTERACTIVE, + lambda b: b.reply_to_message is not None, + ], + "quick_reply": [ + lambda b: b.type == MessageType.BUTTON, + lambda b: b.reply_to_message is not None, + ], + "quick_reply_without_context": [ + lambda b: b.type == MessageType.BUTTON, + lambda b: b.reply_to_message is None, + ], }, "callback_selection": { - "callback": [lambda s: s.data is not None], + "callback": [ + lambda s: s.data is not None, + lambda s: s.reply_to_message is not None, + ], "description": [lambda s: s.description is not None], + "callback_without_context": [ + lambda s: s.data is not None, + lambda s: s.reply_to_message is None, + ], }, "message_status": { "sent": [lambda s: s.status == MessageStatusType.SENT], From 7ebc5e1ac882bb24ce19d94b044ca50d43e5e6f1 Mon Sep 17 00:00:00 2001 From: David Lev <42866208+david-lev@users.noreply.github.com> Date: Mon, 13 Jul 2026 15:32:40 +0300 Subject: [PATCH 2/7] [types] deprecate `caller` and `callee` properties as part of BSUID changes --- pywa/types/base_update.py | 8 ++++---- pywa/types/calls.py | 34 +++++++++++++++++++++++++--------- pywa/types/sent_update.py | 12 ++++++------ tests/test_types.py | 4 ++-- 4 files changed, 37 insertions(+), 21 deletions(-) diff --git a/pywa/types/base_update.py b/pywa/types/base_update.py index 8861b14c..66d26ce3 100644 --- a/pywa/types/base_update.py +++ b/pywa/types/base_update.py @@ -1315,9 +1315,9 @@ class BaseUserUpdate(BaseUpdate, _ClientShortcuts, abc.ABC): """The WhatsApp Business Account ID that the update was sent to.""" @property - def sender(self) -> str | None: + def sender(self) -> None: """ - Deprecated. Use :py:attr:`~pywa.types.others.User.wa_id` instead. + Deprecated. Use ``update.from_user.wa_id`` instead. """ warnings.warn( "Deprecated. Use `update.from_user.wa_id` instead.", @@ -1327,9 +1327,9 @@ def sender(self) -> str | None: return self.from_user.wa_id @property - def recipient(self) -> str: + def recipient(self) -> None: """ - Deprecated. Use :py:attr:`~pywa.types.others.Metadata.phone_number_id` instead. + Deprecated. Use ``update.metadata.phone_number_id`` instead. """ warnings.warn( "Deprecated. Use `update.metadata.phone_number_id` instead.", diff --git a/pywa/types/calls.py b/pywa/types/calls.py index fb926ef3..f06e9f91 100644 --- a/pywa/types/calls.py +++ b/pywa/types/calls.py @@ -42,10 +42,11 @@ import dataclasses import datetime +import warnings from typing import TYPE_CHECKING, Generic from .. import _helpers as helpers -from ..errors import WhatsAppError +from ..errors import PywaDeprecationWarning, WhatsAppError from .base_update import BaseUpdate, BaseUserUpdate, RawUpdate, _ClientShortcuts # noqa from .callback import CallbackData, _CallbackDataT from .others import ( @@ -67,8 +68,6 @@ class _CallShortcuts: id: str _client: WhatsApp - _internal_sender: str - _internal_recipient: str @property def message_id_to_reply(self) -> str: @@ -76,16 +75,33 @@ def message_id_to_reply(self) -> str: raise ValueError("You cannot use `message_id_to_reply` to quote a call update.") @property - def caller(self) -> str: + def caller(self) -> None: """ - The WhatsApp ID of the business phone number that initiated the call. + Deprecated. Use ``call.from_user.wa_id`` instead. """ - return self._internal_recipient + warnings.warn( + "Deprecated. Use `call.chat.id` instead.", + PywaDeprecationWarning, + stacklevel=2, + ) + return ( + self.from_user.wa_id if hasattr(self, "from_user") else self.from_phone_id # ty:ignore[unresolved-attribute] + ) @property - def callee(self) -> str: - """The WhatsApp ID of the user that received the call.""" - return self._internal_sender + def callee(self) -> None: + """ + Deprecated. Use ``call.metadata.phone_number_id`` instead. + """ + warnings.warn( + "Deprecated. Use `call.metadata.phone_number_id` instead.", + PywaDeprecationWarning, + stacklevel=2, + ) + + return ( + self.metadata.phone_number_id if hasattr(self, "metadata") else self.chat.id # ty:ignore[unresolved-attribute] + ) def pre_accept(self, *, sdp: SessionDescription) -> SuccessResult: """ diff --git a/pywa/types/sent_update.py b/pywa/types/sent_update.py index 06e42448..f927c08d 100644 --- a/pywa/types/sent_update.py +++ b/pywa/types/sent_update.py @@ -171,24 +171,24 @@ class SentMessage(_SentUpdate, _PinUnpinActions): ) @property - def recipient(self) -> str: - """Deprecated. Use :py:attr:`~pywa.types.SentMessage.chat.id` / :py:attr:`~pywa.types.SentMessage.to` instead.""" + def recipient(self) -> None: + """Deprecated. Use ``sent.chat.id`` or ``sent.to`` instead.""" warnings.warn( "Deprecated. Use `sent.chat.id` or `sent.to` instead.", PywaDeprecationWarning, stacklevel=2, ) - return self._internal_sender + return self.chat.id @property - def sender(self) -> str: - """Deprecated. Use :py:attr:`~pywa.types.SentMessage.from_phone_id` instead.""" + def sender(self) -> None: + """Deprecated. Use ``sent.from_phone_id`` instead.""" warnings.warn( "Deprecated. Use `sent.from_phone_id` instead.", PywaDeprecationWarning, stacklevel=2, ) - return self._internal_recipient + return self.from_phone_id @classmethod def from_sent_update( diff --git a/tests/test_types.py b/tests/test_types.py index 8e235d92..3c1b7576 100644 --- a/tests/test_types.py +++ b/tests/test_types.py @@ -312,10 +312,10 @@ def test_initiated_call(): recipient_type=RecipientType.WA_ID, callee="16506666666", ) - assert c.caller == wa.phone_id + assert c.from_phone_id == wa.phone_id assert c.chat.id == "16506666666" assert c.to == "16506666666" - assert c.callee == "16506666666" + assert c.chat.id == "16506666666" assert c == InitiatedCall( _client=wa, _recipient_type=RecipientType.WA_ID, From fbaadb69bdb9bddba5021f539ad2a3fd8898e6e0 Mon Sep 17 00:00:00 2001 From: WOUTER DURNEZ Date: Mon, 13 Jul 2026 16:07:04 +0200 Subject: [PATCH 3/7] [callback] narrow missing-context handling to CallbackButton MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CallbackSelection only ever parses interactive list_reply payloads, which always carry the context field — only the button payload type (template QuickReplyButton clicks) omits it. Revert the defensive context parsing and synthetic fixture for CallbackSelection, and document on CallbackButton.reply_to_message (sync and async) that it is None specifically for template quick-reply clicks. Co-Authored-By: Claude Fable 5 --- pywa/types/callback.py | 11 ++---- pywa_async/types/callback.py | 4 +- tests/data/updates/callback_selection.json | 45 ---------------------- tests/test_updates.py | 9 +---- 4 files changed, 7 insertions(+), 62 deletions(-) diff --git a/pywa/types/callback.py b/pywa/types/callback.py index d483c472..94bcf9d3 100644 --- a/pywa/types/callback.py +++ b/pywa/types/callback.py @@ -258,7 +258,7 @@ class CallbackButton(BaseUserUpdate, Generic[_CallbackDataT]): type: The message type (:class:`MessageType.INTERACTIVE` for :class:`~pywa.types.callback.Button` presses or :class:`MessageType.BUTTON` for :class:`~pywa.types.templates.QuickReplyButton` clicks). from_user: The user who sent the message. timestamp: The timestamp when the message was sent (in UTC). - reply_to_message: The message to which this callback button is a reply to (``None`` when WhatsApp omits the ``context`` field). + reply_to_message: The message to which this callback button is a reply to. ``None`` when a template :class:`~pywa.types.templates.QuickReplyButton` is clicked (WhatsApp omits the ``context`` field for template ``button`` messages). data: The data of the button (the ``callback_data`` parameter you provided in :class:`~pywa.types.callback.Button` or :class:`~pywa.types.templates.QuickReplyButton`). title: The title of the button. shared_data: Shared data between handlers. @@ -357,14 +357,14 @@ class CallbackSelection(BaseUserUpdate, Generic[_CallbackDataT]): type: The message type (always :class:`MessageType.INTERACTIVE`). from_user: The user who sent the message. timestamp: The timestamp when the message was sent (in UTC). - reply_to_message: The message to which this callback selection is a reply to (``None`` when WhatsApp omits the ``context`` field). + reply_to_message: The message to which this callback selection is a reply to. data: The data of the selection (the ``callback_data`` parameter you provided in :class:`~pywa.types.callback.SectionRow`). title: The title of the selection. description: The description of the selection (optional). """ type: MessageType - reply_to_message: ReplyToMessage | None + reply_to_message: ReplyToMessage data: _CallbackDataT title: str description: str | None @@ -377,7 +377,6 @@ def from_update(cls, client: "WhatsApp", update: RawUpdate) -> "CallbackSelectio msg = (value := (entry := update["entry"][0])["changes"][0]["value"])[ "messages" ][0] - context = msg.get("context", {}) return cls( _client=client, raw=update, @@ -387,9 +386,7 @@ def from_update(cls, client: "WhatsApp", update: RawUpdate) -> "CallbackSelectio type=MessageType(msg["type"]), from_user=client._usr_cls.from_contact(value["contacts"][0], client=client), timestamp=helpers.timestamp_to_datetime(msg["timestamp"]), - reply_to_message=ReplyToMessage.from_dict(context) - if context.get("id") - else None, + reply_to_message=ReplyToMessage.from_dict(msg["context"]), data=msg["interactive"]["list_reply"]["id"], title=msg["interactive"]["list_reply"]["title"], description=msg["interactive"]["list_reply"].get("description"), diff --git a/pywa_async/types/callback.py b/pywa_async/types/callback.py index 0e6e5d8c..cdfae938 100644 --- a/pywa_async/types/callback.py +++ b/pywa_async/types/callback.py @@ -46,7 +46,7 @@ class CallbackButton(BaseUserUpdateAsync, _CallbackButton[_CallbackDataT]): type: The message type (:class:`MessageType.INTERACTIVE` for :class:`~pywa.types.callback.Button` presses or :class:`MessageType.BUTTON` for :class:`~pywa.types.templates.QuickReplyButton` clicks). from_user: The user who sent the message. timestamp: The timestamp when the message was sent (in UTC). - reply_to_message: The message to which this callback button is a reply to (``None`` when WhatsApp omits the ``context`` field). + reply_to_message: The message to which this callback button is a reply to. ``None`` when a template :class:`~pywa.types.templates.QuickReplyButton` is clicked (WhatsApp omits the ``context`` field for template ``button`` messages). data: The data of the button (the ``callback_data`` parameter you provided in :class:`~pywa.types.callback.Button` or :class:`~pywa.types.templates.QuickReplyButton`). title: The title of the button. shared_data: Shared data between handlers. @@ -95,7 +95,7 @@ class CallbackSelection(BaseUserUpdateAsync, _CallbackSelection[_CallbackDataT]) type: The message type (always :class:`MessageType.INTERACTIVE`). from_user: The user who sent the message. timestamp: The timestamp when the message was sent (in UTC). - reply_to_message: The message to which this callback selection is a reply to (``None`` when WhatsApp omits the ``context`` field). + reply_to_message: The message to which this callback selection is a reply to. data: The data of the selection (the ``callback_data`` parameter you provided in :class:`~pywa.types.callback.SectionRow`). title: The title of the selection. description: The description of the selection (optional). diff --git a/tests/data/updates/callback_selection.json b/tests/data/updates/callback_selection.json index 15ab6eb6..439a1e88 100644 --- a/tests/data/updates/callback_selection.json +++ b/tests/data/updates/callback_selection.json @@ -97,50 +97,5 @@ ] } ] - }, - "callback_without_context": { - "object": "whatsapp_business_account", - "entry": [ - { - "id": "1234567890987654321", - "changes": [ - { - "value": { - "messaging_product": "whatsapp", - "metadata": { - "display_phone_number": "972123456789", - "phone_number_id": "1122334455667" - }, - "contacts": [ - { - "profile": { - "name": "Test Name" - }, - "wa_id": "972987654321", - "user_id": "US.13491208655302741918", - "parent_user_id": "US.ENT.11815799212886844830" - } - ], - "messages": [ - { - "from": "972987654321", - "id": "wamid.zyzxyw", - "timestamp": "1698266905", - "type": "interactive", - "interactive": { - "type": "list_reply", - "list_reply": { - "id": "callback_data", - "title": "title" - } - } - } - ] - }, - "field": "messages" - } - ] - } - ] } } diff --git a/tests/test_updates.py b/tests/test_updates.py index 9da62f26..f7f4f3fd 100644 --- a/tests/test_updates.py +++ b/tests/test_updates.py @@ -80,15 +80,8 @@ ], }, "callback_selection": { - "callback": [ - lambda s: s.data is not None, - lambda s: s.reply_to_message is not None, - ], + "callback": [lambda s: s.data is not None], "description": [lambda s: s.description is not None], - "callback_without_context": [ - lambda s: s.data is not None, - lambda s: s.reply_to_message is None, - ], }, "message_status": { "sent": [lambda s: s.status == MessageStatusType.SENT], From efb2d275183ee09c71263cea95b8f18dfc86dcff Mon Sep 17 00:00:00 2001 From: David Lev <42866208+david-lev@users.noreply.github.com> Date: Thu, 16 Jul 2026 14:53:03 +0300 Subject: [PATCH 4/7] [sent_update] better recipient type resolving --- pywa/types/sent_update.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/pywa/types/sent_update.py b/pywa/types/sent_update.py index f927c08d..3caceba3 100644 --- a/pywa/types/sent_update.py +++ b/pywa/types/sent_update.py @@ -69,12 +69,12 @@ def from_recipient(cls, recipient: str | int) -> RecipientType: if _BSUID_RE.fullmatch(s): return cls.BSUID - if re.sub(_PHONE_CLEAN_RE, "", s).isdigit(): - return RecipientType.PHONE_NUMBER - - if re.match(_PARENT_BSUID_RE, s): + if _PARENT_BSUID_RE.fullmatch(s): return cls.PARENT_BSUID + if _PHONE_CLEAN_RE.sub("", s).isdigit(): + return cls.PHONE_NUMBER + return cls.GROUP_ID def to_chat_type(self) -> ChatType: From 05281102be3711c32616e7d4f7fb911c67a25035 Mon Sep 17 00:00:00 2001 From: David Lev <42866208+david-lev@users.noreply.github.com> Date: Thu, 16 Jul 2026 14:53:34 +0300 Subject: [PATCH 5/7] [security] adding security policy --- SECURITY.md | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100644 SECURITY.md diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 00000000..8835556e --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,35 @@ +# Security Policy + +## Supported Versions + +Currently, only the **4.x** release line is supported with security updates. + +Versions prior to 4.0 are no longer supported due to the `BSUID` changes that introduced breaking changes to the older +codebase. We strongly encourage all users to upgrade to the latest 4.x release. + +| Version | Supported | +|---------|--------------------| +| 4.x.x | :white_check_mark: | +| < 4.0.0 | :x: | + +## Reporting a Vulnerability + +We take the security of **pywa** seriously. If you discover a security vulnerability, please do not disclose it +publicly. + +**How to report:** +Please use GitHub's Private Vulnerability Reporting feature to ensure the issue remains confidential until a fix is +released. + +1. Go to the **Security** tab of this repository and click **Report a vulnerability**. +2. Provide a clear description of the issue and steps to reproduce it. + +Alternatively, you can email reports directly to **david@davidlev.dev**. + +**What to expect:** + +* You will receive an acknowledgment of your report within 48 hours. +* We will use the private advisory to communicate, verify the issue, and work on a patch. +* Once the vulnerability is patched and published, you will be credited for the discovery. + +Please avoid opening public GitHub issues for sensitive security matters. From 9310a15b894f7e90f3c8ada674b4de53c70797dd Mon Sep 17 00:00:00 2001 From: David Lev <42866208+david-lev@users.noreply.github.com> Date: Thu, 16 Jul 2026 20:38:21 +0300 Subject: [PATCH 6/7] [enums] adding copy-paste-compatible repr for `UploadedBy` and `UserIdentifier` --- pywa/server.py | 6 +++--- pywa/types/media.py | 3 +++ pywa/utils.py | 3 +++ 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/pywa/server.py b/pywa/server.py index ddabaf18..1729b54b 100644 --- a/pywa/server.py +++ b/pywa/server.py @@ -287,13 +287,13 @@ def _register_routes(self: "WhatsApp") -> None: match self._server_type: case utils.CustomServerType.STARLETTE: - _logger.info("Using Starlette") + _logger.debug("Using Starlette") helpers.register_routes_starlette(wa=self) case utils.CustomServerType.FASTAPI: - _logger.info("Using FastAPI") + _logger.debug("Using FastAPI") helpers.register_routes_fastapi(wa=self) case utils.CustomServerType.FLASK: - _logger.info("Using Flask") + _logger.debug("Using Flask") helpers.register_routes_flask(wa=self) case _: raise ValueError( diff --git a/pywa/types/media.py b/pywa/types/media.py index ea7723b6..5be2ceb4 100644 --- a/pywa/types/media.py +++ b/pywa/types/media.py @@ -43,6 +43,9 @@ class UploadedBy(enum.Enum): BUSINESS = BUSINESS_UPLOADS_EXPIRATION_DAYS USER = USER_UPLOADS_EXPIRATION_DAYS + def __repr__(self) -> str: + return f"UploadedBy.{self.name}" + class _MediaActions: _client: WhatsApp diff --git a/pywa/utils.py b/pywa/utils.py index a04e07f9..213e05ac 100644 --- a/pywa/utils.py +++ b/pywa/utils.py @@ -416,6 +416,9 @@ class UserIdentifier(enum.Enum): def user_attr(self) -> str: return self.value + def __repr__(self) -> str: + return f"UserIdentifier.{self.name}" + def start_ngrok_tunnel( *, From fce5f310a6b18e2eed22ac9b2097a269d68e19e7 Mon Sep 17 00:00:00 2001 From: David Lev <42866208+david-lev@users.noreply.github.com> Date: Fri, 17 Jul 2026 00:03:50 +0300 Subject: [PATCH 7/7] [version] new version 4.3.1 --- CHANGELOG.md | 9 ++++++++- pywa/__init__.py | 2 +- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 46b29f0f..5802f5b8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,7 +3,14 @@ > NOTE: pywa follows the [semver](https://semver.org/) versioning standard. -#### 4.3.0 (2026-08-07) **Latest** +#### 4.3.1 (2026-08-17) **Latest** + +- [callback] treat missing message context as no `reply_to_message` +- [types] deprecate `caller` and `callee` properties as part of BSUID changes +- [sent_update] better recipient type resolving +- [enums] adding copy-paste-compatible repr for `UploadedBy` and `UserIdentifier` + +#### 4.3.0 (2026-08-07) - [filters] make `Filter` class generic - [signups] adding support for creating listing and updating signups diff --git a/pywa/__init__.py b/pywa/__init__.py index acb6685c..6a660cb4 100644 --- a/pywa/__init__.py +++ b/pywa/__init__.py @@ -9,6 +9,6 @@ from pywa.client import WhatsApp from pywa.utils import Version -__version__ = "4.3.0" +__version__ = "4.3.1" __author__ = "David Lev" __license__ = "MIT"