diff --git a/.connector-store/meta.json b/.connector-store/meta.json index c0b9aa8..70a83aa 100644 --- a/.connector-store/meta.json +++ b/.connector-store/meta.json @@ -11,6 +11,131 @@ "rank": 51, "type": "Connector", "releases": [ + { + "tagName": "v1.1.0", + "products": [ + "MI 4.3.0" + ], + "operations": [ + { + "name":"init", + "description":"Config operation", + "isHidden":true + }, + { + "name":"createAd", + "description":"Create an ad.", + "isHidden":false + }, + { + "name":"createAdSet", + "description":"Creates an ad set.", + "isHidden":false + }, + { + "name":"createCampaign", + "description":"Create a campaign.", + "isHidden":false + }, + { + "name":"deleteAd", + "description":"Deletes an ad.", + "isHidden":false + }, + { + "name":"deleteAdSet", + "description":"Deletes an ad set.", + "isHidden":false + }, + { + "name":"deleteCampaign", + "description":"Deletes a campaign.", + "isHidden":false + }, + { + "name":"dissociateCampaign", + "description":"Dissociate a campaign from an AdAccount.", + "isHidden":false + }, + { + "name":"getAd", + "description":"Returns data of an ad.", + "isHidden":false + }, + { + "name":"getAdSet", + "description":"Return data related to an ad set.", + "isHidden":false + }, + { + "name":"getAdSets", + "description":"Returns all ad sets from one ad account.", + "isHidden":false + }, + { + "name":"getAds", + "description":"Returns ads under this ad account.", + "isHidden":false + }, + { + "name":"getCampaigns", + "description":"Returns campaigns under this ad account.", + "isHidden":false + }, + { + "name":"updateAd", + "description":"Updates an ad.", + "isHidden":false + }, + { + "name":"updateAdSet", + "description":"Updates an ad set.", + "isHidden":false + }, + { + "name":"updateCampaign", + "description":"Updates a campaign.", + "isHidden":false + }, + { + "name":"createAdCreative", + "description":"Creates an ad creative.", + "isHidden":false + }, + { + "name":"createCustomAudience", + "description":"Creates a custom audience.", + "isHidden":false + }, + { + "name":"updateCustomAudience", + "description":"Updates a custom audience.", + "isHidden":false + }, + { + "name":"addUsersToAudience", + "description":"Add users to your custom audience.", + "isHidden":false + }, + { + "name":"removeUsersFromAudience", + "description":"Remove users from your custom audience.", + "isHidden":false + }, + { + "name":"getCustomAudiences", + "description":"Returns all the custom audiences.", + "isHidden":false + } + ], + "connections": [ + { + "name": "facebookAds", + "description": "Connection for interacting with Facebook Ads." + } + ], + "isHidden": false + }, { "tagName": "v1.0.3", "products": [ diff --git a/README.md b/README.md index 7e461f5..06645a8 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ | Connector Version | Supported WSO2 MI Version | |-------------------|---------------------------| -| 1.0.2 | MI 4.3.0 | +| 1.1.0 | MI 4.3.0 | ## Documentation diff --git a/pom.xml b/pom.xml index 9c6962f..f91aaca 100644 --- a/pom.xml +++ b/pom.xml @@ -22,7 +22,7 @@ org.wso2.integration.connector mi-connector-facebookads - 1.0.4-SNAPSHOT + 1.1.0-SNAPSHOT WSO2 Carbon - Facebook Ads Connector http://wso2.org diff --git a/src/main/java/org/wso2/carbon/facebook/ads/connector/Constants.java b/src/main/java/org/wso2/carbon/facebook/ads/connector/Constants.java index f9e894a..f30b0aa 100644 --- a/src/main/java/org/wso2/carbon/facebook/ads/connector/Constants.java +++ b/src/main/java/org/wso2/carbon/facebook/ads/connector/Constants.java @@ -21,6 +21,7 @@ public class Constants { public static final String PROPERTY_ACCESS_TOKEN = "_FB_ACCESS_TOKEN_"; + public static final String HASHED_PAYLOAD = "HASHED_PAYLOAD"; public static final String ACCESS_TOKEN = "access_token"; public static final String PROPERTY_ERROR_CODE = "ERROR_CODE"; public static final String PROPERTY_ERROR_MESSAGE = "ERROR_MESSAGE"; diff --git a/src/main/java/org/wso2/carbon/facebook/ads/connector/FacebookDataClassMediator.java b/src/main/java/org/wso2/carbon/facebook/ads/connector/FacebookDataClassMediator.java new file mode 100644 index 0000000..5d4c38c --- /dev/null +++ b/src/main/java/org/wso2/carbon/facebook/ads/connector/FacebookDataClassMediator.java @@ -0,0 +1,54 @@ +/* + * Copyright (c) 2024, WSO2 LLC. (https://www.wso2.com). + * + * WSO2 LLC. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.wso2.carbon.facebook.ads.connector; + +import org.apache.synapse.MessageContext; +import org.apache.synapse.mediators.AbstractMediator; +import org.wso2.carbon.connector.core.util.ConnectorUtils; +import org.json.JSONArray; +import org.json.JSONObject; + +public class FacebookDataClassMediator extends AbstractMediator { + public static final String PROPERTIES = "properties"; + public static final String INPUT_STRUCTURE = "inputStructure"; + public static final String FACEBOOK_API_COMPATIBLE = "FACEBOOK_API_COMPATIBLE"; + + @Override + public boolean mediate(MessageContext synCtx) { + String inputStructure = (String) ConnectorUtils.lookupTemplateParamater(synCtx, INPUT_STRUCTURE); + String jsonString = (String) ConnectorUtils.lookupTemplateParamater(synCtx, PROPERTIES); + + // set the payload as it is for FACEBOOK_API_COMPATIBLE input structure + // executes if the inputStructure is not given or is empty too + if (FACEBOOK_API_COMPATIBLE.equals(inputStructure) + || inputStructure == null + || inputStructure.trim().isEmpty()) { + synCtx.setProperty(Constants.HASHED_PAYLOAD, jsonString); + return true; + } + + if (jsonString == null || jsonString.isEmpty()) { + return true; + } + JSONArray inputArray = new JSONArray(jsonString); + JSONObject finalObj = FacebookDataProcessor.processData(inputArray); + synCtx.setProperty(Constants.HASHED_PAYLOAD, finalObj.toString()); + return true; + } +} diff --git a/src/main/java/org/wso2/carbon/facebook/ads/connector/FacebookDataProcessor.java b/src/main/java/org/wso2/carbon/facebook/ads/connector/FacebookDataProcessor.java new file mode 100644 index 0000000..599ab84 --- /dev/null +++ b/src/main/java/org/wso2/carbon/facebook/ads/connector/FacebookDataProcessor.java @@ -0,0 +1,595 @@ +/* + * Copyright (c) 2024, WSO2 LLC. (https://www.wso2.com). + * + * WSO2 LLC. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.wso2.carbon.facebook.ads.connector; + +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.nio.charset.StandardCharsets; +import java.util.*; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import org.apache.synapse.SynapseException; +import org.json.JSONArray; +import org.json.JSONObject; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +class FacebookDataProcessor { + + private static final Log log = LogFactory.getLog(FacebookDataProcessor.class); + + private static final Set ALLOWED_PII_TYPES = new HashSet<>(Arrays.asList( + "EXTERN_ID", "EMAIL", "PHONE", "GEN", "DOBY", "DOBM", "DOBD", + "LN", "FN", "FI", "CT", "ST", "ZIP", "MADID", "COUNTRY", "DOB" + )); + + private static final Map FIELD_NAME_MAPPING = new HashMap<>(); + private static final Map US_STATE_MAP = new HashMap<>(); + private static final Map COUNTRY_MAP = new HashMap<>(); + + static { + FIELD_NAME_MAPPING.put("extern_id", "EXTERN_ID"); + FIELD_NAME_MAPPING.put("email", "EMAIL"); + FIELD_NAME_MAPPING.put("phone", "PHONE"); + FIELD_NAME_MAPPING.put("gen", "GEN"); + FIELD_NAME_MAPPING.put("doby", "DOBY"); + FIELD_NAME_MAPPING.put("dobm", "DOBM"); + FIELD_NAME_MAPPING.put("dobd", "DOBD"); + FIELD_NAME_MAPPING.put("ln", "LN"); + FIELD_NAME_MAPPING.put("fn", "FN"); + FIELD_NAME_MAPPING.put("fi", "FI"); + FIELD_NAME_MAPPING.put("ct", "CT"); + FIELD_NAME_MAPPING.put("st", "ST"); + FIELD_NAME_MAPPING.put("zip", "ZIP"); + FIELD_NAME_MAPPING.put("madid", "MADID"); + FIELD_NAME_MAPPING.put("country", "COUNTRY"); + FIELD_NAME_MAPPING.put("dob", "DOB"); + + // All US states and DC + US_STATE_MAP.put("alabama", "al"); + US_STATE_MAP.put("alaska", "ak"); + US_STATE_MAP.put("arizona", "az"); + US_STATE_MAP.put("arkansas", "ar"); + US_STATE_MAP.put("california", "ca"); + US_STATE_MAP.put("colorado", "co"); + US_STATE_MAP.put("connecticut", "ct"); + US_STATE_MAP.put("delaware", "de"); + US_STATE_MAP.put("florida", "fl"); + US_STATE_MAP.put("georgia", "ga"); + US_STATE_MAP.put("hawaii", "hi"); + US_STATE_MAP.put("idaho", "id"); + US_STATE_MAP.put("illinois", "il"); + US_STATE_MAP.put("indiana", "in"); + US_STATE_MAP.put("iowa", "ia"); + US_STATE_MAP.put("kansas", "ks"); + US_STATE_MAP.put("kentucky", "ky"); + US_STATE_MAP.put("louisiana", "la"); + US_STATE_MAP.put("maine", "me"); + US_STATE_MAP.put("maryland", "md"); + US_STATE_MAP.put("massachusetts", "ma"); + US_STATE_MAP.put("michigan", "mi"); + US_STATE_MAP.put("minnesota", "mn"); + US_STATE_MAP.put("mississippi", "ms"); + US_STATE_MAP.put("missouri", "mo"); + US_STATE_MAP.put("montana", "mt"); + US_STATE_MAP.put("nebraska", "ne"); + US_STATE_MAP.put("nevada", "nv"); + US_STATE_MAP.put("newhampshire", "nh"); + US_STATE_MAP.put("newjersey", "nj"); + US_STATE_MAP.put("newmexico", "nm"); + US_STATE_MAP.put("newyork", "ny"); + US_STATE_MAP.put("northcarolina", "nc"); + US_STATE_MAP.put("northdakota", "nd"); + US_STATE_MAP.put("ohio", "oh"); + US_STATE_MAP.put("oklahoma", "ok"); + US_STATE_MAP.put("oregon", "or"); + US_STATE_MAP.put("pennsylvania", "pa"); + US_STATE_MAP.put("rhodeisland", "ri"); + US_STATE_MAP.put("southcarolina", "sc"); + US_STATE_MAP.put("southdakota", "sd"); + US_STATE_MAP.put("tennessee", "tn"); + US_STATE_MAP.put("texas", "tx"); + US_STATE_MAP.put("utah", "ut"); + US_STATE_MAP.put("vermont", "vt"); + US_STATE_MAP.put("virginia", "va"); + US_STATE_MAP.put("washington", "wa"); + US_STATE_MAP.put("westvirginia", "wv"); + US_STATE_MAP.put("wisconsin", "wi"); + US_STATE_MAP.put("wyoming", "wy"); + US_STATE_MAP.put("district of columbia", "dc"); + + // If the country is not present here, default to lowercase + COUNTRY_MAP.put("unitedstates", "us"); + COUNTRY_MAP.put("us", "us"); + COUNTRY_MAP.put("unitedkingdom", "gb"); + COUNTRY_MAP.put("greatbritain", "gb"); + COUNTRY_MAP.put("china", "cn"); + COUNTRY_MAP.put("canada", "ca"); + COUNTRY_MAP.put("france", "fr"); + COUNTRY_MAP.put("germany", "de"); + } + + static JSONObject processData(JSONArray inputArray) { + Map fieldCounts = new HashMap<>(); + + // Determine maximum occurrences of each field + for (int i = 0; i < inputArray.length(); i++) { + JSONObject entry = inputArray.getJSONObject(i); + Iterator keys = entry.keys(); + while (keys.hasNext()) { + String key = keys.next(); + String mappedKey = FIELD_NAME_MAPPING.getOrDefault(key.toLowerCase().replaceAll("\\.\\d+", ""), + key.toUpperCase().replaceAll("\\.\\d+", "")); + if (key.toLowerCase().contains("uid") || key.toLowerCase().contains("externid")) { + mappedKey = "EXTERN_ID"; + } + if (ALLOWED_PII_TYPES.contains(mappedKey)) { + int currentMax = fieldCounts.getOrDefault(mappedKey, 0); + int keyCount = countOccurrences(entry, key); + fieldCounts.put(mappedKey, Math.max(currentMax, keyCount)); + } else { + log.warn("Ignoring unsupported PII type: " + key); + } + } + } + + // Build schema + List schemaList = new ArrayList<>(); + for (Map.Entry entry : fieldCounts.entrySet()) { + String field = entry.getKey(); + int count = entry.getValue(); + for (int i = 0; i < count; i++) { + schemaList.add(field); + } + } + + Collections.sort(schemaList); + JSONArray schemaArray = new JSONArray(schemaList); + JSONArray dataArray = new JSONArray(); + + // Build data rows + for (int i = 0; i < inputArray.length(); i++) { + JSONObject entry = inputArray.getJSONObject(i); + String country = entry.optString("country", null); + Map fieldOccurrenceCounter = new HashMap<>(); + JSONArray dataRow = new JSONArray(); + + for (String field : schemaList) { + int occurrence = fieldOccurrenceCounter.getOrDefault(field, 0); + fieldOccurrenceCounter.put(field, occurrence + 1); + + String originalField = getOriginalFieldName(field); + String foundValue = getOccurrenceValue(entry, originalField, occurrence, field); + String normalized = normalizeField(field, foundValue, country); + String finalValue = normalized.isEmpty() ? "" : (requiresHashing(field) ? hashIfNotAlreadyHashed(normalized) : normalized); + dataRow.put(finalValue); + } + dataArray.put(dataRow); + } + + JSONObject payloadObj = new JSONObject(); + payloadObj.put("schema", schemaArray); + payloadObj.put("data", dataArray); + + JSONObject finalObj = new JSONObject(); + finalObj.put("payload", payloadObj); + return finalObj; + } + + private static int countOccurrences(JSONObject entry, String key) { + int keyCount = 0; + String baseKey = key.replaceAll("\\.\\d+", ""); + while (true) { + String fieldKey = baseKey + (keyCount == 0 ? "" : "." + keyCount); + if (entry.has(fieldKey)) { + keyCount++; + } else { + break; + } + } + return keyCount; + } + + private static String getOccurrenceValue(JSONObject entry, String originalField, int occurrence, String piiType) { + if ("EXTERN_ID".equals(piiType)) { + // Gather all fields that could represent EXTERN_ID: extern_id, uid, externid + List externIdFields = new ArrayList<>(); + for (Object objKey : entry.keySet()) { + String key = (String) objKey; + String lower = key.toLowerCase(); + String baseKey = key.replaceAll("\\.\\d+", ""); + if (lower.contains("uid") || lower.contains("externid") || lower.contains("extern_id")) { + externIdFields.add(key); + } + } + // Sort them + externIdFields.sort((a, b) -> { + String baseA = a.replaceAll("\\.\\d+", ""); + String baseB = b.replaceAll("\\.\\d+", ""); + int cmp = baseA.compareTo(baseB); + if (cmp == 0) { + // Compare occurrence suffixes if any + int idxA = getSuffixIndex(a); + int idxB = getSuffixIndex(b); + return Integer.compare(idxA, idxB); + } + return cmp; + }); + + int foundIndex = 0; + for (String f : externIdFields) { + // Count occurrences of each matched field + // If a field base name matches multiple occurrences, we handle them too + String baseF = f.replaceAll("\\.\\d+", ""); + int maxCount = countOccurrences(entry, baseF); + for (int i = 0; i < maxCount; i++) { + String candidate = baseF + (i == 0 ? "" : "." + i); + if (!entry.has(candidate)) break; + if (foundIndex == occurrence) { + return entry.optString(candidate, ""); + } + foundIndex++; + } + } + + return ""; + } else { + int foundIndex = 0; + for (int attempt = 0; ; attempt++) { + String candidate = originalField + (attempt == 0 ? "" : "." + attempt); + if (!entry.has(candidate)) break; + if (foundIndex == occurrence) { + return entry.optString(candidate, ""); + } + foundIndex++; + } + return ""; + } + } + + private static int getSuffixIndex(String key) { + Matcher m = Pattern.compile("\\.(\\d+)$").matcher(key); + if (m.find()) { + try { + return Integer.parseInt(m.group(1)); + } catch (NumberFormatException e) { + log.error("Error occurred while getting suffix index: ", e); + throw new SynapseException("Error occurred while getting suffix index: " + e.getMessage(), e); + } + } + return 0; + } + + private static String getOccurrenceValue(JSONObject entry, String originalField, int occurrence) { + return getOccurrenceValue(entry, originalField, occurrence, ""); + } + + private static String getOriginalFieldName(String piiType) { + for (Map.Entry entry : FIELD_NAME_MAPPING.entrySet()) { + if (entry.getValue().equals(piiType)) { + return entry.getKey(); + } + } + return piiType.toLowerCase(); + } + + private static boolean requiresHashing(String fieldName) { + return !fieldName.equalsIgnoreCase("MADID") && + !fieldName.equalsIgnoreCase("FI") && + !fieldName.equalsIgnoreCase("PAGE_SCOPED_USER_ID"); + } + + private static Map parsePhoneMeta(String val) { + Map meta = new HashMap<>(); + Pattern p = Pattern.compile("(alreadyHasPhoneCode|countryCode|phoneCode|phoneNumber)\\s*:\\s*([^;]+)"); + Matcher m = p.matcher(val); + while (m.find()) { + String key = m.group(1).trim(); + String rawValue = m.group(2).trim(); + if (rawValue.startsWith("'") && rawValue.endsWith("'")) { + rawValue = rawValue.substring(1, rawValue.length() - 1); + } else if (rawValue.startsWith("\"") && rawValue.endsWith("\"")) { + rawValue = rawValue.substring(1, rawValue.length() - 1); + } + meta.put(key, rawValue); + } + return meta; + } + + // Normalize phone numbers + private static String normalizePhone(String val, String country) { + Map meta = parsePhoneMeta(val); + if (meta.isEmpty()) { + val = val.replaceAll("\\D+", ""); + if ("us".equalsIgnoreCase(country)) { + val = val.replaceFirst("^0+", ""); + if (!val.isEmpty() && !val.startsWith("1")) { + val = "1" + val; + } + } + return val; + } else { + String alreadyHasPhoneCodeStr = meta.get("alreadyHasPhoneCode"); + String countryCode = meta.get("countryCode"); + String phoneCode = meta.get("phoneCode"); + String phoneNumber = meta.get("phoneNumber"); + + if (phoneNumber == null) { + val = val.replaceAll("\\D+", ""); + if ("us".equalsIgnoreCase(country)) { + val = val.replaceFirst("^0+", ""); + if (!val.isEmpty() && !val.startsWith("1")) { + val = "1" + val; + } + } + return val; + } + + phoneNumber = phoneNumber.replaceAll("\\D+", ""); + boolean alreadyHasPhoneCode = alreadyHasPhoneCodeStr != null && alreadyHasPhoneCodeStr.equalsIgnoreCase("true"); + + if (!alreadyHasPhoneCode) { + if (phoneCode != null && !phoneCode.isEmpty()) { + phoneCode = phoneCode.replaceAll("\\D+", ""); + phoneNumber = phoneCode + phoneNumber; + } else if ("us".equalsIgnoreCase(countryCode)) { + phoneNumber = phoneNumber.replaceFirst("^0+", ""); + if (!phoneNumber.isEmpty() && !phoneNumber.startsWith("1")) { + phoneNumber = "1" + phoneNumber; + } + } + } + return phoneNumber; + } + } + + // Normalize the cities + private static String normalizeCity(String val) { + return val.replaceAll("[^a-z]", ""); + } + + // Normalize the states + private static String normalizeState(String val, String country) { + String clean = val.replaceAll("[^a-z]", ""); + if ("us".equalsIgnoreCase(country)) { + if (clean.length() == 2 && US_STATE_MAP.containsValue(clean)) { + return clean; + } + String stateCode = US_STATE_MAP.get(clean); + if (stateCode != null) { + return stateCode; + } + } + return clean; + } + + // Normalize the gender + private static String normalizeGender(String val) { + Set maleSynonyms = new HashSet<>(Arrays.asList("m", "male", "man", "boy")); + Set femaleSynonyms = new HashSet<>(Arrays.asList("f", "female", "woman", "girl")); + + if (maleSynonyms.contains(val)) { + return "m"; + } else if (femaleSynonyms.contains(val)) { + return "f"; + } + return "m"; + } + + // Normalize the countries + private static String normalizeCountry(String val) { + val = val.replaceAll("[^a-z]", ""); + String countryCode = COUNTRY_MAP.get(val); + if (countryCode != null) { + return countryCode; + } + return val; + } + + // Normalize the dob + private static String parseDOBToYYYYMMDD(String val) { + String[] parts = val.split("[/\\-–]+"); + if (parts.length < 3) { + return val; + } + + int[] nums = new int[3]; + for (int i = 0; i < 3; i++) { + try { + nums[i] = Integer.parseInt(parts[i].replaceAll("\\D", "")); + } catch (NumberFormatException e) { + return val; + } + } + + int a = nums[0], b = nums[1], c = nums[2]; + int year, month, day; + + if (String.valueOf(c).length() == 4) { + year = c; + if (a <= 12 && b <= 31) { + month = a; day = b; + } else if (b <= 12 && a <=31) { + month = b; day = a; + } else { + return val; + } + } else if (String.valueOf(a).length() == 4) { + year = a; + if (b <= 12 && c <=31) { + month = b; day = c; + } else if (c <=12 && b <=31) { + month = c; day = b; + } else { + return val; + } + } else if (String.valueOf(b).length() == 4) { + year = b; + if (a <= 12 && c <=31) { + month = a; day = c; + } else if (c <=12 && a <=31) { + month = c; day = a; + } else { + return val; + } + } else { + return val; + } + + if (month < 1 || month > 12 || day < 1 || day > 31) { + return val; + } + return String.format("%04d%02d%02d", year, month, day); + } + + private static String normalizeDOB(String val) { + return parseDOBToYYYYMMDD(val); + } + + private static String normalizeDOBY(String val) { + if (val.matches("\\d{2}")) { + int year = Integer.parseInt(val); + if (year < 50) { + year += 2000; + } else { + year += 1900; + } + val = String.valueOf(year); + } + return val; + } + + private static String normalizeDOBM(String val) { + if (val.matches("\\d+")) { + int month = Integer.parseInt(val); + if (month < 1 || month > 12) { + return ""; + } + return String.format("%02d", month); + } + return ""; + } + + private static String normalizeDOBD(String val) { + if (val.matches("\\d+")) { + int day = Integer.parseInt(val); + if (day < 1 || day > 31) { + return ""; + } + return String.format("%02d", day); + } + return ""; + } + + private static String normalizeField(String fieldName, String value, String country) { + if (value == null) { + return ""; + } + String val = value.trim().toLowerCase(); + + switch (fieldName) { + case "EMAIL": + return val; + + case "PHONE": + val = normalizePhone(val, country); + return val; + + case "FN": + case "LN": + val = val.replaceAll("[^\\p{L}]", ""); + return val; + + case "ZIP": + val = val.replaceAll("\\s+", ""); + if ("us".equalsIgnoreCase(country)) { + val = val.replaceAll("[^0-9]", ""); + if (val.length() > 5) { + val = val.substring(0, 5); + } + } + return val; + + case "CT": + val = normalizeCity(val); + return val; + + case "ST": + val = normalizeState(val, country); + return val; + + case "COUNTRY": + val = normalizeCountry(val); + return val; + + case "DOB": + val = normalizeDOB(val); + return val; + + case "DOBY": + val = normalizeDOBY(val); + return val; + + case "DOBM": + val = normalizeDOBM(val); + return val; + + case "DOBD": + val = normalizeDOBD(val); + return val; + + case "GEN": + val = normalizeGender(val); + return val; + + default: + return val; + } + } + + private static String hashString(String input) { + if (input == null || input.isEmpty()) return ""; + try { + MessageDigest digest = MessageDigest.getInstance("SHA-256"); + byte[] encodedhash = digest.digest(input.getBytes(StandardCharsets.UTF_8)); + return bytesToHex(encodedhash); + } catch (NoSuchAlgorithmException e) { + log.error("Error hashing string: " + input, e); + return ""; + } + } + + private static String bytesToHex(byte[] hash) { + StringBuilder hexString = new StringBuilder(); + for (byte b : hash) { + String hex = Integer.toHexString(0xff & b); + if (hex.length() == 1) hexString.append('0'); + hexString.append(hex); + } + return hexString.toString(); + } + + private static String hashIfNotAlreadyHashed(String input) { + if (input.matches("^[0-9a-f]{64}$")) { + return input; + } + return hashString(input); + } +} diff --git a/src/main/java/org/wso2/carbon/facebook/ads/connector/FilterAudiencesByNameMediator.java b/src/main/java/org/wso2/carbon/facebook/ads/connector/FilterAudiencesByNameMediator.java new file mode 100644 index 0000000..fca67c8 --- /dev/null +++ b/src/main/java/org/wso2/carbon/facebook/ads/connector/FilterAudiencesByNameMediator.java @@ -0,0 +1,95 @@ +/* + * Copyright (c) 2024, WSO2 LLC. (https://www.wso2.com). + * + * WSO2 LLC. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.wso2.carbon.facebook.ads.connector; + +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; +import org.apache.synapse.MessageContext; +import org.apache.synapse.commons.json.JsonUtil; +import org.apache.synapse.mediators.AbstractMediator; +import org.apache.synapse.core.axis2.Axis2MessageContext; +import org.wso2.carbon.connector.core.util.ConnectorUtils; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +public class FilterAudiencesByNameMediator extends AbstractMediator { + + private static final Log log = LogFactory.getLog(FilterAudiencesByNameMediator.class); + public static final String FILTER_BY_NAME = "filterByName"; + public static final String DATA = "data"; + public static final String PAGING = "paging"; + public static final String NAME = "name"; + + @Override + public boolean mediate(MessageContext synCtx) { + String filterByName = (String) ConnectorUtils.lookupTemplateParamater(synCtx, FILTER_BY_NAME); + if (filterByName == null || filterByName.trim().isEmpty()) { + log.warn("filterByName parameter is null or empty. Skipping filtering."); + return true; + } + try { + Axis2MessageContext axis2MessageContext = (Axis2MessageContext) synCtx; + org.apache.axis2.context.MessageContext axis2MC = axis2MessageContext.getAxis2MessageContext(); + + if (JsonUtil.hasAJsonPayload(axis2MC)) { + String jsonString = JsonUtil.jsonPayloadToString(axis2MC); + JsonObject jsonObject = JsonParser.parseString(jsonString).getAsJsonObject(); + JsonArray dataArray = jsonObject.getAsJsonArray(DATA); + if (dataArray == null) { + log.warn("No audiences are found."); + return true; + } + + JsonArray filteredData = new JsonArray(); + for (JsonElement element : dataArray) { + if (element.isJsonObject()) { + JsonObject dataObj = element.getAsJsonObject(); + JsonElement nameElement = dataObj.get(NAME); + if (nameElement != null && nameElement.isJsonPrimitive()) { + String name = nameElement.getAsString(); + if (name.contains(filterByName)) { + filteredData.add(dataObj); + } + } + } + } + + jsonObject.add(DATA, filteredData); + + // Remove the "paging" section from the JSON object + if (jsonObject.has(PAGING)) { + jsonObject.remove(PAGING); + } + + String filteredJsonString = jsonObject.toString(); + + // Set the filtered JSON back as the payload + JsonUtil.getNewJsonPayload(axis2MC, filteredJsonString, true, true); + } else { + log.warn("No JSON payload found in the message context."); + } + } catch (Exception e) { + log.error("Error while filtering JSON payload by name: " + filterByName, e); + return false; + } + return true; + } +} diff --git a/src/main/resources/functions/addUsersToAudience.xml b/src/main/resources/functions/addUsersToAudience.xml index 045d6d5..b8491e4 100644 --- a/src/main/resources/functions/addUsersToAudience.xml +++ b/src/main/resources/functions/addUsersToAudience.xml @@ -23,7 +23,10 @@ + + + @@ -33,18 +36,21 @@ ${args.arg1} ]]> - + +
+ +
diff --git a/src/main/resources/functions/component.xml b/src/main/resources/functions/component.xml index 50e7476..895888c 100644 --- a/src/main/resources/functions/component.xml +++ b/src/main/resources/functions/component.xml @@ -101,5 +101,9 @@ removeUsersFromAudience.xml Remove users from your Custom Audience. + + getCustomAudiences.xml + Returns all custom audiences. + diff --git a/src/main/resources/functions/getCustomAudiences.xml b/src/main/resources/functions/getCustomAudiences.xml new file mode 100644 index 0000000..e952730 --- /dev/null +++ b/src/main/resources/functions/getCustomAudiences.xml @@ -0,0 +1,51 @@ + + + + + diff --git a/src/main/resources/functions/removeUsersFromAudience.xml b/src/main/resources/functions/removeUsersFromAudience.xml index 8ec06a7..ea4403e 100644 --- a/src/main/resources/functions/removeUsersFromAudience.xml +++ b/src/main/resources/functions/removeUsersFromAudience.xml @@ -23,28 +23,35 @@ + + + + - + +
+ +
diff --git a/src/main/resources/uischema/addUsersToAudience.json b/src/main/resources/uischema/addUsersToAudience.json index 3190dd6..1975612 100644 --- a/src/main/resources/uischema/addUsersToAudience.json +++ b/src/main/resources/uischema/addUsersToAudience.json @@ -50,6 +50,18 @@ "required": "true", "helpTip": "Custom audience users properties." } + }, + { + "type": "attribute", + "value": { + "name": "inputStructure", + "displayName": "Input Data Structure", + "inputType": "comboOrExpression", + "comboValues": ["FACEBOOK_API_COMPATIBLE", "JSON_ARRAY"], + "defaultValue": "JSON_ARRAY", + "required": "false", + "helpTip": "Structure of the user data to be added." + } } ] } diff --git a/src/main/resources/uischema/getCustomAudiences.json b/src/main/resources/uischema/getCustomAudiences.json new file mode 100644 index 0000000..19b2a8d --- /dev/null +++ b/src/main/resources/uischema/getCustomAudiences.json @@ -0,0 +1,61 @@ +{ + "connectorName": "facebookAds", + "operationName": "getCustomAudiences", + "title": "Get Custom Audiences", + "help": "Returns all the custom audiences.", + "elements": [ + { + "type": "attributeGroup", + "value": { + "groupName": "General", + "elements": [ + { + "type": "attribute", + "value": { + "name": "configRef", + "displayName": "Connection", + "inputType": "connection", + "allowedConnectionTypes": [ + "facebookAds" + ], + "defaultType": "connection.facebookAds", + "defaultValue": "", + "required": "true", + "helpTip": "Connection to be used." + } + }, + { + "type": "attributeGroup", + "value": { + "groupName": "Parameters", + "elements": [ + { + "type": "attribute", + "value": { + "name": "adAccountId", + "displayName": "Ad Account Id", + "inputType": "stringOrExpression", + "defaultValue": "", + "required": "true", + "helpTip": "ID of the ad account." + } + }, + { + "type": "attribute", + "value": { + "name": "filterByName", + "displayName": "Filter By Name", + "inputType": "stringOrExpression", + "defaultValue": "", + "required": "false", + "helpTip": "Name of the audience to filter by name." + } + } + ] + } + } + ] + } + } + ] +} diff --git a/src/main/resources/uischema/removeUsersFromAudience.json b/src/main/resources/uischema/removeUsersFromAudience.json index 185967d..18df18b 100644 --- a/src/main/resources/uischema/removeUsersFromAudience.json +++ b/src/main/resources/uischema/removeUsersFromAudience.json @@ -50,6 +50,18 @@ "required": "true", "helpTip": "Custom audience users update properties." } + }, + { + "type": "attribute", + "value": { + "name": "inputStructure", + "displayName": "Input Data Structure", + "inputType": "comboOrExpression", + "comboValues": ["FACEBOOK_API_COMPATIBLE", "JSON_ARRAY"], + "defaultValue": "JSON_ARRAY", + "required": "false", + "helpTip": "Structure of the user data to be removed." + } } ] }