Skip to content

Commit

Permalink
Adding support to WhatsApp events on Business SDK
Browse files Browse the repository at this point in the history
Summary:
CAPI CTWA has been launched to 100% since November 2023, but is still not supported in our Meta Business SDK.

This diff adds support for WhatsApp events on the Business SDK. It includes changes to the Event class to add a new field for the messaging channel, as well as changes to the User Data class to include new constants for the CTWA_CLID and PAGE_ID parameters.

Reviewed By: stcheng

Differential Revision: D54592282

fbshipit-source-id: 95e2e43718ab45e9a51bc3a89d8ee38239da7ff9
  • Loading branch information
Rafael Batista authored and facebook-github-bot committed Mar 7, 2024
1 parent e43a205 commit 2c7166d
Show file tree
Hide file tree
Showing 4 changed files with 105 additions and 2 deletions.
5 changes: 5 additions & 0 deletions facebook_business/adobjects/serverside/action_source.py
Expand Up @@ -60,6 +60,11 @@ class ActionSource(Enum):
"""
SYSTEM_GENERATED = 'system_generated'

"""
Conversion happened through a business messaging channel, such as WhatsApp or Instagram Direct.
"""
BUSINESS_MESSAGING = 'business_messaging'

"""
Conversion happened in a way that is not listed.
"""
Expand Down
40 changes: 39 additions & 1 deletion facebook_business/adobjects/serverside/event.py
Expand Up @@ -25,6 +25,7 @@
from facebook_business.adobjects.serverside.custom_data import CustomData
from facebook_business.adobjects.serverside.user_data import UserData
from facebook_business.adobjects.serverside.app_data import AppData
from facebook_business.adobjects.serverside.messaging_channel import MessagingChannel


class Event(object):
Expand All @@ -42,12 +43,13 @@ class Event(object):
'data_processing_options_state': 'int',
'action_source': 'ActionSource',
'advanced_measurement_table': 'str',
'messaging_channel': 'MessagingChannel',
}

def __init__(self, event_name = None, event_time = None, event_source_url = None,
opt_out = None, event_id = None, user_data = None, custom_data = None,
app_data = None, data_processing_options = None, data_processing_options_country = None,
data_processing_options_state = None, action_source = None, advanced_measurement_table = None):
data_processing_options_state = None, action_source = None, advanced_measurement_table = None, messaging_channel = None):
# type: (str, int, str, bool, str, UserData, CustomData, AppData, list[str], int, int, ActionSource, str) -> None

"""Conversions API Event"""
Expand All @@ -66,6 +68,7 @@ def __init__(self, event_name = None, event_time = None, event_source_url = None
self._advanced_measurement_table = None
self.event_name = event_name
self.event_time = event_time
self._messaging_channel = None
if event_source_url is not None:
self.event_source_url = event_source_url
if opt_out is not None:
Expand All @@ -88,6 +91,8 @@ def __init__(self, event_name = None, event_time = None, event_source_url = None
self.action_source = action_source
if advanced_measurement_table is not None:
self.advanced_measurement_table = advanced_measurement_table
if messaging_channel is not None:
self.messaging_channel = messaging_channel

@property
def event_name(self):
Expand Down Expand Up @@ -402,6 +407,28 @@ def advanced_measurement_table(self, advanced_measurement_table):
"""
self._advanced_measurement_table = advanced_measurement_table

@property
def messaging_channel(self):
"""Gets the messaging_channel.
Return the messaging channel of the event.
:return: The messaging_channel.
:rtype: str
"""
return self._messaging_channel

@messaging_channel.setter
def messaging_channel(self, messaging_channel):
"""Sets the advanced_measurement_table.
Allow you to specify the messaging channel of the event.
:param messaging_channel: The messaging_channel.
:type: str
"""
self._messaging_channel = messaging_channel

def normalize(self):
normalized_payload = {'event_name': self.event_name, 'event_time': self.event_time,
'event_source_url': self.event_source_url, 'opt_out': self.opt_out,
Expand All @@ -423,6 +450,10 @@ def normalize(self):
self.validate_action_source(self.action_source)
normalized_payload['action_source'] = self.action_source.value

if self.messaging_channel is not None:
self.validate_messaging_channel(self.messaging_channel)
normalized_payload['messaging_channel'] = self.messaging_channel.value

normalized_payload = {k: v for k, v in normalized_payload.items() if v is not None}
return normalized_payload

Expand All @@ -432,6 +463,13 @@ def validate_action_source(self, action_source):
'action_source must be an ActionSource. TypeError on value: %s' % action_source
)

def validate_messaging_channel(self, messaging_channel):
if not type(messaging_channel) == MessagingChannel:
raise TypeError(
'messaging_channel must be an messaging_channel. TypeError on value: %s' % messaging_channel
)


def to_dict(self):
"""Returns the model properties as a dict"""
result = {}
Expand Down
32 changes: 32 additions & 0 deletions facebook_business/adobjects/serverside/messaging_channel.py
@@ -0,0 +1,32 @@
# Copyright 2014 Facebook, Inc.

# You are hereby granted a non-exclusive, worldwide, royalty-free license to
# use, copy, modify, and distribute this software in source code or binary
# form for use in connection with the web services and APIs provided by
# Facebook.

# As with any software that integrates with the Facebook platform, your use
# of this software is subject to the Facebook Developer Principles and
# Policies [http://developers.facebook.com/policy/]. This copyright notice
# shall be included in all copies or substantial portions of the software.

# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
# DEALINGS IN THE SOFTWARE.

from enum import Enum


# Used to specify which messaging channel was used.
# See https://developers.facebook.com/docs/marketing-api/conversions-api/business-messaging

class MessagingChannel(Enum):

"""
Conversion happened on WhatsApp.
"""
WHATSAPP = 'whatsapp'
30 changes: 29 additions & 1 deletion facebook_business/adobjects/serverside/user_data.py
Expand Up @@ -55,6 +55,8 @@ class UserData(object):
'doby': 'str',
'madid': 'str',
'anon_id': 'str',
'ctwa_clid': 'ctwa_clid',
'page_id': 'page_id',
}

def __init__(
Expand Down Expand Up @@ -96,8 +98,10 @@ def __init__(
external_ids=None,
madid=None,
anon_id=None,
ctwa_clid=None,
page_id=None,
):
# type: (str, str, Gender, str, str, str, str, str, str, str, str, str, str, str, str, str, str, str, str, str, str, str, str, str, list[str], list[str], list[Gender], list[str], list[str], list[str], list[str], list[str], list[str], list[str], list[str], str, str) -> None
# type: (str, str, Gender, str, str, str, str, str, str, str, str, str, str, str, str, str, str, str, str, str, str, str, str, str, list[str], list[str], list[Gender], list[str], list[str], list[str], list[str], list[str], list[str], list[str], list[str], str, str, str, str) -> None

"""UserData is a set of identifiers Facebook can use for targeted attribution"""
self._emails = None
Expand Down Expand Up @@ -126,6 +130,8 @@ def __init__(
self._doby = None
self._madid = None
self._anon_id = None
self._ctwa_clid = None
self._page_id = None

if email is not None and emails is not None:
raise ValueError(UserData.multi_value_constructor_err.format('email', 'emails'))
Expand Down Expand Up @@ -225,6 +231,10 @@ def __init__(
self.madid = madid
if anon_id is not None:
self.anon_id = anon_id
if ctwa_clid is not None:
self.ctwa_clid = ctwa_clid
if page_id is not None:
self.page_id = page_id

@property
def email(self):
Expand Down Expand Up @@ -1079,6 +1089,22 @@ def anon_id(self):
def anon_id(self, anon_id):
self._anon_id = anon_id

@property
def ctwa_clid(self):
return self._ctwa_clid

@ctwa_clid.setter
def ctwa_clid(self, ctwa_clid):
self._ctwa_clid = ctwa_clid

@property
def page_id(self):
return self._page_id

@page_id.setter
def page_id(self, page_id):
self._page_id = page_id

def normalize(self):
normalized_payload = {'em': self.__normalize_list('em', self.emails),
'ph': self.__normalize_list('ph', self.phones),
Expand All @@ -1105,6 +1131,8 @@ def normalize(self):
'doby': Normalize.normalize_field('doby', self.doby),
'madid': self.madid,
'anon_id': self.anon_id,
'ctwa_clid': self.ctwa_clid,
'page_id': self.page_id,
}
if self.genders:
normalized_payload['ge'] = self.__normalize_list('ge', list(map(lambda g: g.value, self.genders)))
Expand Down

0 comments on commit 2c7166d

Please sign in to comment.