Skip to content

Commit

Permalink
[docs] adding docs to FlowDetails methods and explain how to create…
Browse files Browse the repository at this point in the history
… a business public key
  • Loading branch information
david-lev committed Dec 22, 2023
1 parent 7a08888 commit dfe3735
Show file tree
Hide file tree
Showing 4 changed files with 153 additions and 29 deletions.
80 changes: 55 additions & 25 deletions docs/source/content/flows/overview.rst
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,6 @@

.. currentmodule:: pywa.types.flows

.. note::

WORK IN PROGRESS

The ``Flows`` in pywa are still in beta and not fully tested.
Install the RC version of pywa to use it:

>>> pip3 install "pywa[cryptography]==1.13.0rc6"

The ``cryptography`` extra is required for the default implementation of the decryption and encryption of the flow requests and responses.

If you find any bugs or have any suggestions, please open an issue on `GitHub <https://github.com/david-lev/pywa/issues>`_.

The WhatsApp Flows are now the most exciting part of the WhatsApp Cloud API.

From `developers.facebook.com <https://developers.facebook.com/docs/whatsapp/flows>`_:
Expand Down Expand Up @@ -63,10 +50,6 @@ You can create the flows using the `WhatsApp Flow Builder <https://business.face
Now you can start building the flow structure.

.. note::

WORK IN PROGRESS

A flow is collection of screens containing components. screens can exchange data with each other and with your server.

Flow can be static; all the components settings are predefined and no interaction is required from your server.
Expand Down Expand Up @@ -233,6 +216,7 @@ Here is example of dynamic flow:
.. code-block:: python
:caption: support_request.json
:linenos:
:emphasize-lines: 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 34, 40, 46
dynamic_flow = FlowJSON(
data_api_version=utils.Version.FLOW_DATA_API,
Expand Down Expand Up @@ -389,7 +373,7 @@ After you have the flow json, you can update the flow with :meth:`pywa.client.Wh
wa = WhatsApp(...)
wa.update_flow_json(flow_id, flow_json=customer_satisfaction_survey)
wa.update_flow_json(flow_id, flow_json=flow_json)
The ``flow_json`` argument can be :class:`FlowJSON`, a :class:`dict`, json :class:`str`, json file path or open(json_file) obj.

Expand Down Expand Up @@ -488,7 +472,57 @@ Handling Flow requests and responding to them

.. note::

WORK IN PROGRESS
Since the requests and responses can contain sensitive data, such as passwords and other personal information,
all the requests and responses are encrypted using the `WhatsApp Business Encryption <https://developers.facebook.com/docs/whatsapp/cloud-api/reference/whatsapp-business-encryption>`_.

Before you continue, you need to sign and upload the business public key.
First you need to generate a private key and a public key:

Generate a public and private RSA key pair by typing in the following command:

>>> openssl genrsa -des3 -out private.pem 2048


This generates 2048-bit RSA key pair encrypted with a password you provided and is written to a file.

Next, you need to export the RSA Public Key to a file.

>>> openssl rsa -in private.pem -outform PEM -pubout -out public.pem


This exports the RSA Public Key to a file.

Once you have the public key, you can upload it using the :meth:`pywa.client.WhatsApp.set_business_public_key` method.

.. code-block:: python
:linenos:
from pywa import WhatsApp
wa = WhatsApp(...)
wa.set_business_public_key(open("public.pem").read())
Every request need to be decrypted using the private key. so you need to provide it when you create the :class:`WhatsApp` object:

.. code-block:: python
:linenos:
from pywa import WhatsApp
wa = WhatsApp(..., business_private_key=open("private.pem").read())
Now you are ready to handle the requests.

Just one more thing, the default decryption & encryption implementation is using the `cryptography <https://cryptography.io/en/latest/>`_ library,
So you need to install it:

>>> pip3 install cryptography

Or when installing PyWa:

>>> pip3 install "pywa[cryptography]"


In dynamic flow, when the user perform an action with type of ``FlowActionType.DATA_EXCHANGE`` you will receive a request to your server with the payload
and you need to determine if you want to continue to the next screen or complete the flow.
Expand All @@ -497,7 +531,7 @@ So in our dynamic example (``dynamic_flow``) we have just one screen: ``SIGN_UP`

.. code-block:: python
:linenos:
:emphasize-lines: 3, 6, 10, 14
:emphasize-lines: 4, 6, 10, 14
Screen(
id="SIGN_UP",
Expand Down Expand Up @@ -585,7 +619,7 @@ Let's register a callback function to handle this request:
wa = WhatsApp(
...,
business_private_key="PRIVATE_KEY",
business_private_key=open("private.pem").read(), # provide your business private key
)
@wa.on_flow_request(endpoint="/flow") # The endpoint we set above
Expand All @@ -603,10 +637,6 @@ Let's register a callback function to handle this request:
We need to provide our business private key to decrypt the request and encrypt the response.

We need to setup WhatsApp Business Encryption in order to decrypt the request and encrypt the response.
You can read more about it in `WhatsApp Business Encryption <https://developers.facebook.com/docs/whatsapp/cloud-api/reference/whatsapp-business-encryption>`_.
The public key can be uploaded using the :meth:`pywa.client.WhatsApp.set_business_public_key` method.

After that. we are registering a callback function to handle the request.
The callback function will receive the :class:`FlowRequest` object and should return :class:`FlowResponse` object.

Expand Down
2 changes: 2 additions & 0 deletions docs/source/content/getting-started.rst
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,8 @@ Now you can continue to the next section and learn how to use the ``pywa`` packa

- The `Updates <updates/overview.html>`_: Learn about the different types of updates that the client can receive, their attributes and properties and how to use them.

- The `Flows <flows/overview.html>`_: Learn how to create, update and send flows.

- The `errors <errors/overview.html>`_: Learn about the different types of errors in the package and how to handle them.

- The `Examples <examples/overview.html>`_: See some examples of how to use the package.
2 changes: 1 addition & 1 deletion pywa/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -1606,7 +1606,7 @@ def update_flow_metadata(
Whether the flow was updated.
Raises:
ValueError: If neither ``name`` nor ``categories`` are provided.
ValueError: If neither ``name``, ``categories`` or ``endpoint_uri`` are provided.
"""
if name is None and categories is None and endpoint_uri is None:
raise ValueError("At least one argument must be provided")
Expand Down
98 changes: 95 additions & 3 deletions pywa/types/flows.py
Original file line number Diff line number Diff line change
Expand Up @@ -469,24 +469,69 @@ def from_dict(cls, data: dict, client: WhatsApp) -> FlowDetails:
)

def publish(self) -> bool:
"""
Update the status of this flow to ``FlowStatus.PUBLISHED``.
- A shortcut for :meth:`pywa.client.WhatsApp.publish_flow`.
- This action is not reversible.
- The Flow and its assets become immutable once published.
- To update the Flow after that, you must create a new Flow. You specify the existing Flow ID as the clone_flow_id parameter while creating to copy the existing flow.
You can publish your Flow once you have ensured that:
- All validation errors and publishing checks have been resolved.
- The Flow meets the design principles of WhatsApp Flows
- The Flow complies with WhatsApp Terms of Service, the WhatsApp Business Messaging Policy and, if applicable, the WhatsApp Commerce Policy
Returns:
Whether the flow was published.
Raises:
FlowPublishingError: If this flow has validation errors or not all publishing checks have been resolved.
"""
if self._client.publish_flow(self.id):
self.status = FlowStatus.PUBLISHED
return True
return False

def delete(self) -> bool:
"""
When the flow is in ``FlowStatus.DRAFT`` status, you can delete it.
- A shortcut for :meth:`pywa.client.WhatsApp.delete_flow`.
Returns:
Whether the flow was deleted.
Raises:
FlowDeletingError: If this flow is already published.
"""
if self._client.delete_flow(self.id):
self.status = FlowStatus.DEPRECATED
self.status = FlowStatus.DEPRECATED # there is no `DELETED` status
return True
return False

def deprecate(self) -> bool:
"""
When the flow is in ``FlowStatus.PUBLISHED`` status, you can only deprecate it.
Returns:
Whether the flow was deprecated.
Raises:
FlowDeprecatingError: If this flow is not published or already deprecated.
"""
if self._client.deprecate_flow(self.id):
self.status = FlowStatus.DEPRECATED
return True
return False

def get_assets(self) -> tuple[FlowAsset, ...]:
"""
Get all assets attached to this flow.
Returns:
The assets of the flow.
"""
return self._client.get_flow_assets(self.id)

def update_metadata(
Expand All @@ -495,6 +540,31 @@ def update_metadata(
categories: Iterable[FlowCategory | str] | None = None,
endpoint_uri: str | None = None,
) -> bool:
"""
Args:
flow_id: The flow ID.
name: The name of the flow (optional).
categories: The new categories of the flow (optional).
endpoint_uri: The URL of the FlowJSON Endpoint. Starting from FlowJSON 3.0 this property should be
specified only gere. Do not provide this field if you are cloning a FlowJSON with version below 3.0.
Example:
>>> from pywa.types.flows import FlowCategory
>>> wa = WhatsApp(business_account_id='1234567890', ...)
>>> my_flows = wa.get_flows()
>>> my_flows[0].update_metadata(
... name='Feedback',
... categories=[FlowCategory.SURVEY, FlowCategory.OTHER],
... endpoint_uri='https://my-api-server/feedback_flow'
... )
Returns:
Whether the flow was updated.
Raises:
ValueError: If neither ``name``, ``categories`` or ``endpoint_uri`` is provided.
"""
success = self._client.update_flow_metadata(
flow_id=self.id,
name=name,
Expand All @@ -513,17 +583,39 @@ def update_metadata(
def update_json(
self, flow_json: FlowJSON | dict | str | pathlib.Path | bytes | BinaryIO
) -> bool:
"""
Update the json of this flow.
Args:
flow_json: The new json of the flow. Can be a :class:`FlowJSON` object, :class:`dict`, json :class:`str`,
json file path or json bytes.
Returns:
Whether the flow was updated.
Raises:
FlowUpdatingError: If the flow json is invalid or this flow is already published.
"""
is_success, errors = self._client.update_flow_json(
flow_id=self.id,
flow_json=flow_json,
)
self.validation_errors = errors
self.validation_errors = errors or None
return is_success


@dataclasses.dataclass(slots=True, kw_only=True, frozen=True)
class FlowAsset:
"""Represents an asset in a flow."""
"""
Represents an asset in a flow.
- Read more at `developers.facebook.com <https://developers.facebook.com/docs/whatsapp/flows/reference/flowsapi#asset-list>`_.
Attributes:
name: The name of the asset (e.g. ``"flow.json"``).
type: The type of the asset (e.g. ``"FLOW_JSON"``).
url: The URL to the asset.
"""

name: str
type: str
Expand Down

0 comments on commit dfe3735

Please sign in to comment.