from ast import literal_eval from math import ceil from facebook_business.adobjects.customaudience import CustomAudience from facebook_business.api import FacebookAdsApi from service.conf.secrets import get_secret from service.tasks.api_service_handler.utils import chunks from .service_fb import FB_SECRET, FbServiceCore class FbServiceHandler(FbServiceCore): """Main class for Handling communication with FaceBook API""" fields = [ "id", "account_id", "approximate_count_lower_bound", "approximate_count_upper_bound", "description", "is_value_based", "name", "operation_status", ] def __init__(self, account_id=None): super().__init__(account_id) # access_token, app_id, app_secret self.fansifter_fb_credentials = literal_eval( get_secret(FB_SECRET[self.fansifter_adaccount_id]) ) self.fabsifter_business_id = "382678159060556" try: FacebookAdsApi.init( access_token=self.fansifter_fb_credentials["access_token"] ) except Exception as e: raise RuntimeError(f"Could not initiate FacebookAdsApi - {e}") def get_long_lived_token(self, user_short_lived_token): """Exchaning the obtained short_lived_token through SDK to a long lived one""" method = "GET" endpoint = "oauth/access_token" params = { "grant_type": "fb_exchange_token", "client_id": self.fansifter_fb_credentials["app_id"], "client_secret": self.fansifter_fb_credentials["app_secret"], "fb_exchange_token": user_short_lived_token, } response = self.make_request(method=method, endpoint=endpoint, params=params) return response def create_audience( self, audience_name, public_id, description="This is basic test description", subtype="CUSTOM", customer_file_source="PARTNER_PROVIDED_ONLY", ): params = { "name": audience_name, "subtype": subtype, "description": description, "customer_file_source": customer_file_source, "opt_out_link": f"https://optout.fansifter.com/facebook?audienceId={public_id}", "fields": self.fields, } method = "POST" endpoint = f"{self.fansifter_adaccount_id}/customaudiences" response = self.make_request(method=method, endpoint=endpoint, params=params) return response def _upload_users(self, audience_id, payload, session): params = {"payload": payload, "session": session} method = "POST" endpoint = audience_id + "/users" response = self.make_request(method=method, endpoint=endpoint, params=params) return response def upload_all_users_to_audience(self, audience_id, data_obj): """Uploading the data to selected audience""" # TODO! Handle 10k Rows per call limitation that comes from FB # https://stackoverflow.com/questions/64445577/adding-users-with-multiple-keys-to-a-custom-audience-using-python-facebook-marke # Example of data_obj: # data_obj = {'schema': ['EMAIL', 'DOB', 'GEN', 'FN', 'CT', 'COUNTRY'], # 'data': [['HASH1', 'HASH2', 'HASH3', 'HASH4', 'HASH5', 'HASH6'],...]} # RATE LIMIT DATA : https://developers.facebook.com/docs/marketing-apis/rate-limiting/ batch_size = 9900 num_total = len(data_obj["data"]) if len(data_obj["data"]) <= batch_size: session = { "session_id": int(audience_id), "batch_seq": 1, "last_batch_flag": True, "estimated_num_total": num_total, } payload = data_obj response = self._upload_users( audience_id=audience_id, payload=payload, session=session ) return response else: num_of_chunks = ceil((len(data_obj["data"]) / batch_size)) response_list = [] for index, chunk in enumerate(chunks(data_obj["data"], batch_size)): """Checking if the current chunk is the last one""" if index + 1 == num_of_chunks: last_batch_flag = True else: last_batch_flag = False session = { "session_id": int(audience_id), "batch_seq": index + 1, "last_batch_flag": last_batch_flag, "estimated_num_total": num_total, } payload = {"schema": data_obj["schema"], "data": chunk} response = self._upload_users( audience_id=audience_id, payload=payload, session=session ) response_list.append(response) return response_list @staticmethod def generate_list_of_users(df): """Generating users_obj formatted for upload to fb audience""" # TODO! Handle 10k Rows per call limitation that comes from FB users_obj = {} users_obj["schema"] = list(df.columns) users_obj["data"] = df.values.tolist() return users_obj def get_session_id(self, audience_id): method = "GET" endpoint = str(audience_id) + "/sessions" response = self.make_request(method=method, endpoint=endpoint) return response def delete_audience(self, audience_id): method = "DELETE" endpoint = audience_id response = self.make_request(method=method, endpoint=endpoint) return response def delete_multiple_audiences(self, audience_id_list): """This works only if: - only lookalike audiences sent - All present audiences sent - Custom audiences sent have all of their Lookalikes sent as well We can't delete custom audiences if lookalikes are left """ delete_last = [] for a_id in audience_id_list: if a_id.get("parent_id") is not None: self.delete_audience(a_id["external_id"]) else: delete_last.append(a_id["external_id"]) for a_id in delete_last: self.delete_audience(a_id) def read_audience(self, audience_id): """ Getting information about audience_id Usefull info: https://developers.facebook.com/docs/marketing-api/reference/custom-audience/ """ params = {"fields": self.fields} method = "GET" endpoint = audience_id response = self.make_request(method=method, endpoint=endpoint, params=params) return response def list_audiences(self): params = {"fields": self.fields} method = "GET" endpoint = f"{self.fansifter_adaccount_id}/customaudiences" response = self.make_request(method=method, endpoint=endpoint, params=params) return response def get_audience_adaccounts(self, audience_id): """Get list of adaccounts that audience_id is shared with""" method = "GET" endpoint = f"{audience_id}/adaccounts" response = self.make_request(method=method, endpoint=endpoint) return response def initiate_or_share_audience(self, audience_id, partner_ad_account_id): """https://developers.facebook.com/docs/marketing-api/business-asset-management/guides/share-custom-audiences""" method = "POST" endpoint = f"{audience_id}/adaccounts" params = { "adaccounts": [str(partner_ad_account_id)], "relationship_type": ["Audience Info Provider", "Information Manager"], } response = self.make_request(method=method, endpoint=endpoint, params=params) return response def revoke_audience_sharing_from_adaccount( self, external_audience_id, partner_ad_account_id ): """Unsharing particular audinece from rovided adaccount""" method = "DELETE" endpoint = f"{external_audience_id}/adaccounts" params = {"adaccounts": partner_ad_account_id} response = self.make_request(method=method, endpoint=endpoint, params=params) return response def create_lookalike_audience( self, audience_name, audience_description, source_audience_id, audience_ratio, audience_country_codes: list, ): """Creating a lookalike audience""" field = CustomAudience.Field lookalike_spec = field.lookalike_spec params = { "fields": self.fields, "description": audience_description, field.name: audience_name, field.subtype: CustomAudience.Subtype.lookalike, field.origin_audience_id: int(source_audience_id), lookalike_spec: { "ratio": audience_ratio, "allow_international_seeds": True, }, } if len(audience_country_codes) > 1: params[lookalike_spec]["location_spec"] = { "geo_locations": {"countries": audience_country_codes} } elif len(audience_country_codes) == 1: params[lookalike_spec]["country"] = audience_country_codes[0] else: raise RuntimeError("No Countries were provided.") method = "POST" endpoint = f"{self.fansifter_adaccount_id}/customaudiences" response = self.make_request(method=method, endpoint=endpoint, params=params) return response def check_audience_sharing_requests(self): method = "GET" endpoint = f"{self.fabsifter_business_id}/initiated_audience_sharing_requests" print(endpoint) response = self.make_request(method=method, endpoint=endpoint) return response def get_country_suggestions(self): endpoint = "search" params = {"location_types": ["country"], "type": "adgeolocation", "limit": 1000} method = "GET" response = self.make_request(method=method, endpoint=endpoint, params=params) final_response = [] for country in response["data"]["data"]: final_response.append( {"id": country["country_code"], "name": country["name"]} ) final_response = sorted(final_response, key=lambda i: i["name"]) return final_response def get_audience_shared_adaccount_info(self, audience_id): method = "GET" endpoint = f"{audience_id}/shared_account_info" response = self.make_request(method=method, endpoint=endpoint) # response example: # {'data': [{'account_id': '128568314489533', # 'account_name': 'Festivality Facebook Ads', # 'business_id': '128567621156269', # 'business_name': 'Festivality', # 'sharing_status': 'ACCEPTED_RELATIONSHIP'}]} # Need to add sharing_status if not exists (if checking for lookalike audience) # for consistency of responses for i, adaccount in enumerate(response["data"]["data"]): response["data"]["data"][i]["sharing_status"] = adaccount.get( "sharing_status", "shared" ) return response