├── .github ├── ISSUE_TEMPLATE.md └── PULL_REQUEST_TEMPLATE.md ├── .gitignore ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── app ├── app.yaml ├── blueprints │ ├── __init__.py │ └── webhook │ │ ├── __init__.py │ │ └── blueprint.py ├── data_sources │ ├── __init__.py │ ├── bigquery │ │ └── bigquery_data_source.py │ └── factory.py ├── helpers │ ├── __init__.py │ └── webhook │ │ ├── __init__.py │ │ └── helpers.py ├── main.py ├── middlewares │ ├── __init__.py │ └── auth.py ├── partners │ ├── __init__.py │ ├── botmaker │ │ └── botmaker_partner.py │ ├── factory.py │ ├── infobip │ │ └── infobip_partner.py │ ├── take │ │ └── take_partner.py │ └── whatsapp │ │ └── whatsapp_partner.py └── requirements.txt ├── deployment ├── deploy.sh ├── docker │ └── Dockerfile └── samples │ └── app_engine │ ├── app_engine.yaml │ └── app_engine_flexible.yaml ├── docs ├── code-of-conduct.md ├── contributing.md ├── wci_bsp_guide.pdf └── wci_guide.pdf ├── run_local.sh ├── run_local_webhook.sh └── tutorial.md /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ## Expected Behavior 2 | 3 | 4 | ## Actual Behavior 5 | 6 | 7 | ## Steps to Reproduce the Problem 8 | 9 | 1. 10 | 1. 11 | 1. 12 | 13 | ## Specifications 14 | 15 | - Version: 16 | - Platform: -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | Fixes # 2 | 3 | > It's a good idea to open an issue first for discussion. 4 | 5 | - [ ] Tests pass 6 | - [ ] Appropriate changes to README are included in PR -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *__pycache__* 2 | *.egg-info 3 | temp 4 | .pytest_cache 5 | .idea 6 | .venv 7 | .vscode 8 | .DS_Store -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # How to become a contributor and submit your own code 2 | 3 | ## Contributor License Agreements 4 | 5 | We'd love to accept your patches! Before we can take them, we 6 | have to jump a couple of legal hurdles. 7 | Please fill out either the individual or corporate Contributor License Agreement 8 | (CLA). 9 | * If you are an individual writing original source code and you're sure you 10 | own the intellectual property, then you'll need to sign an [individual CLA](https://developers.google.com/open-source/cla/individual). 11 | * If you work for a company that wants to allow you to contribute your work, 12 | then you'll need to sign a [corporate CLA](https://developers.google.com/open-source/cla/corporate). 13 | Follow either of the two links above to access the appropriate CLA and 14 | instructions for how to sign and return it. Once we receive it, we'll be able to 15 | accept your pull requests. 16 | 17 | ## Contributing A Patch 18 | 19 | 1. Submit an issue describing your proposed change to the repo in question. 20 | 1. The repo owner will respond to your issue promptly. 21 | 1. If your proposed change is accepted, and you haven't already done so, sign a 22 | Contributor License Agreement (see details above). 23 | 1. Fork the desired repo, develop and test your code changes. 24 | 1. Ensure that your code adheres to the existing style in the sample to which 25 | you are contributing. Refer to the 26 | [Google Cloud Platform Samples Style Guide](https://github.com/GoogleCloudPlatform/Template/wiki/style.html) for the 27 | recommended coding standards for this organization. 28 | 1. Ensure that your code has an appropriate set of unit tests which all pass. 29 | 1. Submit a pull request. 30 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # WCI - Conversion Import for Whatsapp 2 | 3 | WCI is an open source solution that enables advertisers who offer a WhatsApp channel to measure, attribute, target and optimize their campaigns through signals received in-chat messages. The solution offers a way to integrate and track conversions that happen in business-account WhatsApp chats by linking clicked leads (click to chat) to final conversions (scheduled events, purchased through chat app, etc). As a result, WCI allows advertisers to bring visibility to the WhatsApp journey; measure in-WhatsApp chat interactions; attribute in-WhatsApp chat conversions and target audience-lists with Customer Match. 4 | 5 | **Disclaimer:** This is not an officially supported Google product 6 | 7 | ## How does it work? 8 | - User clicks on the Contact via WhatsApp link 9 | - A cloud function collects an identifier such as gclid and generates a unique protocol 10 | - The user is redirected to the WhatsApp with a pre-typed message with the generated protocol 11 | - Once the user sends the message, a cloud function / webhook associated with the WhatsApp Business Account - checks for the protocol and relates it to the gclid and phone number that the message was sent from 12 | 13 | ![image](https://github.com/google/wci/assets/6962758/7fe48295-cfc1-4a26-b8e7-05ca073232bc) 14 | 15 | ## Prerequisites 16 | - Whatsapp Business Account 17 | 18 | ## Deployment 19 | In the Cloud Shell, execute the following command: 20 | ``` shell 21 | git clone https://github.com/google/wci && cd wci && sh ./deployment/deploy.sh 22 | ``` 23 | 24 | ## Updating WCI to the latest version 25 | In the Cloud Shell, execute the following command: 26 | ``` shell 27 | git clone https://github.com/google/wci && cd wci && sh ./deployment/deploy.sh service=update 28 | ``` 29 | 30 | ## Guided Deployment 31 | If you want to do a guided deployment through Cloud Shell, click the link below:
32 | [![Open in Cloud Shell](https://gstatic.com/cloudssh/images/open-btn.svg)](https://shell.cloud.google.com/cloudshell/editor?cloudshell_git_repo=https%3A%2F%2Fgithub.com%2Fgoogle%2Fwci&cloudshell_git_branch=main&cloudshell_tutorial=tutorial.md) 33 | 34 | ## Video-guided Deployment 35 | [![Open on Youtube](https://www.gstatic.com/youtube/img/branding/favicon/favicon_48x48.png)](https://youtu.be/OVXIO5RMHX8) 36 | 37 | ## Resources 38 | Video-guided deployment
39 | https://youtu.be/OVXIO5RMHX8 40 | 41 | WCI’s source code
42 | https://github.com/google/wci 43 | 44 | 45 | WhatsApp Business Platform Cloud API
46 | https://developers.facebook.com/docs/whatsapp/cloud-api/guides/set-up-webhooks 47 | 48 | Google Cloud’s Artifacts Registry
49 | https://cloud.google.com/artifact-registry/docs 50 | 51 | Google Cloud’s Run
52 | https://cloud.google.com/run/docs 53 | 54 | Google Cloud’s BigQuery
55 | https://cloud.google.com/bigquery/docs 56 | 57 | Google Ads’ Conversion API
58 | https://developers.google.com/google-ads/api/docs/conversions/overview 59 | 60 | Google Ads’ Customer Match API
61 | https://developers.google.com/google-ads/api/docs/remarketing/audience-types/customer-match 62 | 63 | Google Ads’ Enhanced Conversion for Leads API
64 | https://developers.google.com/google-ads/api/docs/conversions/upload-identifiers 65 | -------------------------------------------------------------------------------- /app/app.yaml: -------------------------------------------------------------------------------- 1 | # Copyright 2022 Google LLC. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | apiVersion: serving.knative.dev/v1 15 | kind: Service 16 | metadata: 17 | name: wci 18 | labels: 19 | cloud.googleapis.com/location: {{REGION}} 20 | annotations: 21 | run.googleapis.com/description: WhatsApp Conversion Import 22 | run.googleapis.com/ingress: all 23 | spec: 24 | template: 25 | spec: 26 | serviceAccountName: {{SERVICE_ACCOUNT}} 27 | containers: 28 | - image: {{REGION}}-docker.pkg.dev/{{GOOGLE_CLOUD_PROJECT}}/wci/wci:latest 29 | env: 30 | - name: GOOGLE_CLOUD_PROJECT 31 | value: "{{GOOGLE_CLOUD_PROJECT}}" 32 | - name: DATA_SOURCE_TYPE 33 | value: "BIG_QUERY" 34 | - name: PARTNER_TYPE 35 | value: "{{PARTNER_TYPE}}" 36 | - name: BQ_DATASET_NAME 37 | value: "{{BQ_DATASET_NAME}}" 38 | - name: API_KEY 39 | value: "{{API_KEY}}" 40 | - name: PROTOCOL_MESSAGE 41 | value: "{{PROTOCOL_MESSAGE}}" 42 | - name: CTM_PROTOCOL_MESSAGE 43 | value: "Chat ID:" 44 | - name: WELCOME_MESSAGE 45 | value: "{{WELCOME_MESSAGE}}" 46 | - name: STATS_OPTIN 47 | value: "{{STATS_OPTIN}}" 48 | - name: ECL_ENABLED 49 | value: "{{ECL_ENABLED}}" 50 | - name: BQ_PENDING_LEAD_TABLE 51 | value: "{{GOOGLE_CLOUD_PROJECT}}.{{BQ_DATASET_NAME}}.pending_leads" 52 | - name: BQ_LEAD_TABLE 53 | value: "{{GOOGLE_CLOUD_PROJECT}}.{{BQ_DATASET_NAME}}.leads" 54 | - name: BQ_CHAT_TABLE 55 | value: "{{GOOGLE_CLOUD_PROJECT}}.{{BQ_DATASET_NAME}}.chat_leads" 56 | 57 | -------------------------------------------------------------------------------- /app/blueprints/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright 2022 Google LLC. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | 16 | from .webhook import webhook_page 17 | -------------------------------------------------------------------------------- /app/blueprints/webhook/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright 2022 Google LLC. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | 16 | from .blueprint import webhook_page 17 | -------------------------------------------------------------------------------- /app/blueprints/webhook/blueprint.py: -------------------------------------------------------------------------------- 1 | # Copyright 2022 Google LLC. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | import os 16 | from middlewares.auth import auth_required 17 | from helpers.webhook.helpers import ( 18 | generate_a_protocol, 19 | get_default_messages, 20 | get_domain_from_request, 21 | save_protocol 22 | ) 23 | from partners.factory import PartnerFactory 24 | from flask import Blueprint, request, jsonify 25 | 26 | webhook_page = Blueprint("webhook", __name__) 27 | 28 | 29 | @webhook_page.route("/webhook", methods=["GET", "POST"]) 30 | def process_protocol(): 31 | """ 32 | Generates a new protocol 33 | 34 | Parameters: 35 | None 36 | Output: 37 | Returns the newly generated protocol number for the received lead 38 | """ 39 | 40 | # Collects gclid, phone from the URL 41 | identifier = request.args.get("id") 42 | type = request.args.get("type") or "gclid" 43 | 44 | # Checks if this is a post with a payload to be associated with 45 | # the protocol number 46 | payload = None 47 | if request.is_json: 48 | payload = request.get_json(silent=True) 49 | 50 | # Always generate a protocol for every request 51 | has_protocol = generate_a_protocol() 52 | # Save protocol 53 | save_protocol(identifier, type, has_protocol, payload) 54 | 55 | # Stats of usage 56 | if os.environ.get("STATS_OPTIN") != "no": 57 | try: 58 | from tadau.measurement_protocol import Tadau 59 | 60 | Tadau().process( 61 | [ 62 | { 63 | "client_id": f"{has_protocol}", 64 | "name": "wci", 65 | "action": "lead", 66 | "context": get_domain_from_request(request), 67 | } 68 | ] 69 | ) 70 | except: 71 | pass 72 | 73 | # Gets url-safe messages 74 | messages = get_default_messages(has_protocol) 75 | 76 | # Returns the generated protocol + default messages 77 | return ( 78 | jsonify( 79 | protocol=has_protocol, 80 | message=messages.get("message"), 81 | protocol_message=messages.get("protocol_message"), 82 | welcome_message=messages.get("welcome_message"), 83 | ), 84 | 200, 85 | ) 86 | 87 | 88 | @webhook_page.route("/webhook-wci", methods=["POST"]) 89 | def process_message(): 90 | """ 91 | Process message received 92 | 93 | Parameters: 94 | None 95 | Output: 96 | Status code. 97 | """ 98 | 99 | # Collects the payload received 100 | partner = PartnerFactory(os.environ.get("PARTNER_TYPE")).get() 101 | partner.process_message(request.get_json()) 102 | 103 | # Always return success 104 | return "Success", 200 105 | 106 | 107 | @webhook_page.route("/webhook-wci", methods=["GET"]) 108 | @auth_required 109 | def validates_challenge(auth_context): 110 | """ 111 | Validates the webhook verification 112 | 113 | Parameters: 114 | None 115 | Output: 116 | Returns the challenge 117 | """ 118 | 119 | # Collects token and challenge 120 | challenge = request.args.get("hub.challenge") 121 | 122 | # Redirects the request 123 | return challenge, 200 124 | 125 | #Click-to-message webhook 126 | @webhook_page.route("/webhook-ctm", methods=["POST"]) 127 | #@auth_required 128 | def process_chat_id(): 129 | """ 130 | Saves protocol and gclid from Click-To-Message 131 | 132 | Parameters: 133 | None 134 | Output: 135 | Saves the CTM Protocol generated by Google Ads 136 | """ 137 | 138 | # Collects gclid, chatid from the URL 139 | identifier = request.args.get("gclid") 140 | protocol = request.args.get("chatid") 141 | type = "gclid" 142 | 143 | # Checks if this is a post with a payload to be associated with 144 | # the protocol number 145 | payload= {} 146 | if request.is_json: 147 | payload = request.get_json(silent=True) 148 | payload['ctm'] = True 149 | 150 | save_protocol(identifier, type, protocol, payload) 151 | 152 | # Stats of usage 153 | if os.environ.get("STATS_OPTIN") != "no": 154 | try: 155 | from tadau.measurement_protocol import Tadau 156 | 157 | Tadau().process( 158 | [ 159 | { 160 | "client_id": f"{protocol}", 161 | "name": "wci", 162 | "action": "lead", 163 | "context": get_domain_from_request(request), 164 | } 165 | ] 166 | ) 167 | except: 168 | pass 169 | 170 | return "Success", 200 171 | 172 | @webhook_page.route("/health_checker", methods=["GET"]) 173 | def health_checker(): 174 | return "alive", 200 175 | -------------------------------------------------------------------------------- /app/data_sources/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright 2024 Google LLC. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | """ 16 | A generic data source class to ease the process data 17 | """ 18 | 19 | from abc import ABC, abstractmethod 20 | from typing import Dict, Optional 21 | 22 | 23 | class DataSource(ABC): 24 | @abstractmethod 25 | def save_protocol( 26 | self, 27 | identifier: str, 28 | type: str, 29 | protocol: str, 30 | mapped: Optional[Dict[str, str]], 31 | ): 32 | pass 33 | 34 | def save_phone_protocol_match(self, phone: str, protocol: str): 35 | pass 36 | 37 | def save_message(self, message: str, sender: str, receiver: str): 38 | pass 39 | 40 | def get_protocol_match(self, protocol: str, sender: str): 41 | pass 42 | -------------------------------------------------------------------------------- /app/data_sources/bigquery/bigquery_data_source.py: -------------------------------------------------------------------------------- 1 | # Copyright 2022 Google LLC. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | """ 16 | A BigQuery extension for Data Sources 17 | """ 18 | 19 | import json 20 | import os 21 | import datetime 22 | from typing import Dict, Optional 23 | from google.cloud import bigquery 24 | from data_sources import DataSource 25 | 26 | BQ_PENDING_LEAD_TABLE = os.environ.get("BQ_PENDING_LEAD_TABLE") 27 | BQ_LEAD_TABLE = os.environ.get("BQ_LEAD_TABLE") 28 | BQ_CHAT_TABLE = os.environ.get("BQ_CHAT_TABLE") 29 | 30 | 31 | class BigQueryDataSource(DataSource): 32 | """BigQuery as datasource""" 33 | 34 | def __init__(self): 35 | # TODO(mr-lopes): adds client settings such as location 36 | self._bq_client = bigquery.Client() 37 | 38 | def save_protocol( 39 | self, 40 | identifier: str, 41 | type: str, 42 | protocol: str, 43 | mapped: Optional[Dict[str, str]], 44 | ): 45 | """ 46 | Saves a protocol number with identifier and mapped values 47 | 48 | Parameters: 49 | identifier: gclid, client_id, etc 50 | type: indicates the type of identifier (gclid, etc) 51 | protocol: a generated protocol 52 | mapped: any additional value[s] to be associated with the protocol 53 | """ 54 | 55 | rows_to_insert = [ 56 | { 57 | "identifier": identifier, 58 | "type": type, 59 | "protocol": protocol, 60 | "mapped": json.dumps(mapped) if mapped else None, 61 | "timestamp": datetime.datetime.now().timestamp(), 62 | } 63 | ] 64 | 65 | errors = self._bq_client.insert_rows_json(BQ_PENDING_LEAD_TABLE, rows_to_insert) 66 | 67 | if not errors == []: 68 | raise Exception("Error while creating pending-lead: {}".format(errors)) 69 | 70 | def save_phone_protocol_match(self, phone: str, protocol: str): 71 | """ 72 | Saves a protocol matched to a number (phone) 73 | 74 | Parameters: 75 | phone: phone number 76 | protocol: protocol sent by phone number 77 | """ 78 | rows_to_insert = [ 79 | { 80 | "phone": phone, 81 | "protocol": protocol, 82 | "timestamp": datetime.datetime.now().timestamp(), 83 | } 84 | ] 85 | 86 | errors = self._bq_client.insert_rows_json(BQ_LEAD_TABLE, rows_to_insert) 87 | 88 | if not errors == []: 89 | raise Exception("Error while creating lead: {}".format(errors)) 90 | 91 | def save_message(self, message: str, sender: str, receiver: str): 92 | """ 93 | Saves menssage sent by phone number (sender) 94 | 95 | Parameters: 96 | message: content of message 97 | sender: emitter 98 | receiver: recipient 99 | """ 100 | 101 | rows_to_insert = [ 102 | { 103 | "sender": sender, 104 | "receiver": receiver, 105 | "message": message, 106 | "timestamp": datetime.datetime.now().timestamp(), 107 | } 108 | ] 109 | 110 | errors = self._bq_client.insert_rows_json(BQ_CHAT_TABLE, rows_to_insert) 111 | 112 | if not errors == []: 113 | raise Exception("Error while creating chat-lead: {}".format(errors)) 114 | 115 | def get_protocol_match(self, protocol: str, sender: str): 116 | """ 117 | Gets the lead match for the protocol and sender 118 | 119 | Parameters: 120 | protocol: matched protocol 121 | sender: emitter 122 | 123 | """ 124 | 125 | query = f""" 126 | SELECT 127 | plead.identifier, plead.type, plead.protocol, plead.mapped 128 | FROM `{BQ_PENDING_LEAD_TABLE}` AS plead 129 | INNER JOIN `{BQ_LEAD_TABLE}` AS lead USING (protocol) 130 | WHERE plead.protocol = @protocol 131 | AND lead.phone = @sender 132 | LIMIT 1 133 | """ 134 | 135 | job_config = bigquery.QueryJobConfig( 136 | query_parameters=[ 137 | bigquery.ScalarQueryParameter("protocol", "STRING", protocol), 138 | bigquery.ScalarQueryParameter("sender", "STRING", sender), 139 | ] 140 | ) 141 | 142 | rows = self._bq_client.query(query, job_config=job_config).result() 143 | 144 | # Maps the query's schema for later use 145 | query_schema = {sch.name: sch for sch in rows.schema} 146 | for row in rows: 147 | return self._convert_row_to_dict(row, query_schema) 148 | 149 | def _convert_row_to_dict(self, row, schema: dict = {}): 150 | """ 151 | Converts a row into dict -- including json'd strings 152 | 153 | Parameters: 154 | row: a row from bq's query result 155 | schema: query's schema 156 | 157 | """ 158 | dict = {} 159 | for key, value in row.items(): 160 | # This is necessary because bq.client does not 161 | # automatically convert a stringify json into a dict 162 | if value and schema and schema[key].field_type.lower() == "json": 163 | # In case it's an array of json, apply the proper 164 | # transformation 165 | if schema[key].mode.lower() == "repeated": 166 | dict[key] = list(map(json.loads, value)) 167 | else: 168 | dict[key] = json.loads(value) 169 | else: 170 | dict[key] = value 171 | return dict 172 | -------------------------------------------------------------------------------- /app/data_sources/factory.py: -------------------------------------------------------------------------------- 1 | # Copyright 2022 Google LLC. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | """ 16 | A generic data source class to ease the process of integrating with other databases 17 | """ 18 | 19 | from enum import Enum 20 | from data_sources import DataSource 21 | from data_sources.bigquery.bigquery_data_source import BigQueryDataSource 22 | 23 | 24 | class SourceType(Enum): 25 | (BIG_QUERY, FILE) = range(2) 26 | 27 | 28 | AVAILABLE_DATA_SOURCES = {SourceType.BIG_QUERY: BigQueryDataSource()} 29 | 30 | 31 | class DataSourceFactory: 32 | def __init__(self, source_type: SourceType): 33 | self._source = SourceType[source_type] 34 | 35 | def get(self) -> DataSource: 36 | if self._source: 37 | return AVAILABLE_DATA_SOURCES.get(self._source) 38 | else: 39 | raise NotImplementedError( 40 | "Data Source not implemented. Please check your configuration." 41 | ) 42 | -------------------------------------------------------------------------------- /app/helpers/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright 2022 Google LLC. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | -------------------------------------------------------------------------------- /app/helpers/webhook/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright 2022 Google LLC. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | -------------------------------------------------------------------------------- /app/helpers/webhook/helpers.py: -------------------------------------------------------------------------------- 1 | # Copyright 2022 Google LLC. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | 16 | """ 17 | A collection of helper functions for webhook related operations. 18 | """ 19 | 20 | import base64 21 | import hashlib 22 | import requests 23 | import os 24 | import uuid 25 | import zlib 26 | import re 27 | import urllib.parse 28 | from flask import Request 29 | from typing import Dict, Optional 30 | from data_sources.factory import DataSourceFactory 31 | 32 | data_source = DataSourceFactory(os.environ.get("DATA_SOURCE_TYPE")).get() 33 | 34 | 35 | def generate_a_protocol() -> Optional[str]: 36 | """ 37 | Helper function for generating a new protocol for a given request. 38 | 39 | Output: 40 | a new Protocol. 41 | """ 42 | 43 | # Generates a protocol based on current timestamp 44 | protocol = zlib.crc32(f"{uuid.uuid1()}".encode()) 45 | 46 | # Returns the generated protocol 47 | return protocol 48 | 49 | def save_protocol( 50 | identifier: str, type: str, protocol: str, payload: Optional[any]) -> Optional[str]: 51 | """ 52 | Helper function for saving protocols and identifiers. 53 | 54 | Parameters: 55 | identifier (str): to be associated with the new protocol. 56 | type (str): type of the identifier. E.g. gclid, device_id, phone_number, etc. 57 | protocol (str): protocol number 58 | 59 | Output: 60 | a new Protocol. 61 | """ 62 | # Sends protocol to db 63 | data_source.save_protocol(identifier, type, protocol, payload) 64 | 65 | # Returns the generated protocol 66 | return 200 67 | 68 | 69 | def get_protocol_by_phone(message: str, sender: str, receiver: str) -> Optional[str]: 70 | """ 71 | Helper function for getting a generated protocol for a given sender. 72 | 73 | Parameters: 74 | message (str) 75 | sender (str) 76 | receiver (str) 77 | 78 | Output: 79 | found protocol or none 80 | """ 81 | # Checks if a protocol is within the given message 82 | # If not, returns None 83 | _protocol_message = os.environ.get("PROTOCOL_MESSAGE").strip() 84 | _ctm_message = os.environ.get("CTM_PROTOCOL_MESSAGE").strip() 85 | has_protocol = re.search(f"({_protocol_message}|{_ctm_message}) (\w+)", message) 86 | protocol = None 87 | 88 | # If a protocol was found, creates a match with sender 89 | if has_protocol: 90 | # Captures the second group matched 91 | protocol = has_protocol.group(2) 92 | 93 | # Updates the phone_number by protcol 94 | data_source.save_phone_protocol_match(sender, protocol) 95 | 96 | # Checks if ECL is enabled 97 | if os.environ.get("ECL_ENABLED").lower() == "true": 98 | set_protocol_ecl_for_phone(protocol, sender) 99 | 100 | # Saves a copy of the received message 101 | data_source.save_message(message, sender, receiver) 102 | 103 | # Returns the raw protocol 104 | return protocol 105 | 106 | 107 | def get_domain_from_request(request: Request) -> str: 108 | """ 109 | Helper function to extract domain from Request 110 | 111 | Parameters: 112 | url: full url that may contain paths, paramerters and achors 113 | 114 | Output: 115 | Extracted domain or "Not set" 116 | """ 117 | 118 | if request.origin: 119 | url = request.origin 120 | elif request.host_url: 121 | url = request.host_url 122 | else: 123 | return "Not set" 124 | 125 | domain = re.match("([^\n\?\=\&\# ]+)", url) 126 | 127 | if domain is None: 128 | return "Not set" 129 | 130 | return domain.group(1) 131 | 132 | 133 | def get_default_messages(protocol: str) -> Dict[str, str]: 134 | """ 135 | Gets defined, standard messages 136 | 137 | Parameters: 138 | protocol: generated protocol 139 | 140 | """ 141 | _protocol_message = os.environ.get("PROTOCOL_MESSAGE").strip() 142 | _welcome_message = os.environ.get("WELCOME_MESSAGE").strip() 143 | 144 | return { 145 | "message": urllib.parse.quote_plus( 146 | f"{_protocol_message} {protocol}. {_welcome_message}" 147 | ), 148 | "protocol_message": _protocol_message, 149 | "welcome_message": _welcome_message, 150 | } 151 | 152 | 153 | def get_safe_phone(phone: str) -> str: 154 | """ 155 | Gets phone number safely 156 | 157 | Parameters: 158 | sender: phone who sent the message 159 | 160 | Outputs: 161 | phone number only - may include + (e.g. +15555555555) 162 | 163 | """ 164 | 165 | # Checks if phone number is valid (without +) 166 | is_valid = re.match(r"\d{8,15}", phone) 167 | 168 | # Returns its raw value if it's not a valid phone number 169 | if is_valid is None: 170 | return phone 171 | 172 | # Otherwise, extracts only numbers and includes plus code 173 | return "+{}".format(re.sub(r"[^0-9]", "", phone)) 174 | 175 | 176 | def to_sha256(string: str) -> str: 177 | """ 178 | Receives a string and hashes into sha256 179 | 180 | Parameters: 181 | string: str 182 | 183 | """ 184 | return hashlib.sha256(string.strip().lower().encode("utf-8")).hexdigest() 185 | 186 | 187 | def to_bytes(hex_digest: str) -> bytes: 188 | """ 189 | Receives a digested hex from sha256 190 | 191 | Parameters: 192 | hex_digest: str 193 | 194 | """ 195 | return bytes.fromhex(hex_digest) 196 | 197 | 198 | def to_base64(from_hex: bytes) -> str: 199 | """ 200 | Receives a string and hashes into sha256 201 | 202 | Parameters: 203 | from_hex: str 204 | 205 | """ 206 | return base64.urlsafe_b64encode(from_hex).rstrip(b"=").decode("utf-8") 207 | 208 | 209 | def set_protocol_ecl_for_phone(protocol: str, sender: str) -> None: 210 | """ 211 | Fires ECL tag for a given protoco and phone number 212 | 213 | Parameters: 214 | protocol: matched protocol for processed message 215 | sender: phone who sent the message 216 | 217 | """ 218 | try: 219 | # hashes phone number and transfors into base 64 220 | sha256_phone = to_sha256(get_safe_phone(sender)) 221 | # mimis transformation for firing the tag 222 | hashed_phone = to_base64(to_bytes(sha256_phone)) 223 | 224 | # gets the data for the matched protocol 225 | matched_protocol = data_source.get_protocol_match(protocol, sender) 226 | 227 | if ( 228 | matched_protocol.get("mapped") 229 | and matched_protocol.get("mapped")["conversion_id"] 230 | ): 231 | if matched_protocol.get("type") == "gclid" and matched_protocol.get( 232 | "identifier" 233 | ): 234 | gclid = matched_protocol.get("identifier") 235 | conversion_id = matched_protocol.get("mapped")["conversion_id"] 236 | # Targets ECL beacon endpoint 237 | url = f"https://google.com/pagead/form-data/{conversion_id}?em=tv.1~pn.{hashed_phone}&gclaw={gclid}" 238 | response = requests.post(url) 239 | 240 | # Updates a pending lead into ecl after successfully triggering 241 | # the ECL beacon 242 | if response.ok: 243 | data_source.save_protocol( 244 | sha256_phone, "ecl", protocol, matched_protocol.get("mapped") 245 | ) 246 | else: 247 | print(f"ECL not fired for {protocol}", response) 248 | except: 249 | pass 250 | -------------------------------------------------------------------------------- /app/main.py: -------------------------------------------------------------------------------- 1 | # Copyright 2022 Google LLC. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | 16 | """ 17 | This module is the main flask application. 18 | """ 19 | 20 | from flask import Flask 21 | from flask_cors import CORS 22 | from blueprints import * 23 | 24 | import os 25 | 26 | app = Flask(__name__) 27 | # Enables CORS on the app 28 | # For safety, origins can be set into CORS 29 | # reference: https://flask-cors.readthedocs.io/en/latest/configuration.html 30 | CORS(app) 31 | 32 | # Generates a random, safe secret key 33 | app.secret_key = os.urandom(12).hex() 34 | app.register_blueprint(webhook_page) 35 | 36 | if __name__ == "__main__": 37 | app.run(debug=True, host="0.0.0.0", port=int(os.environ.get("PORT", 8080))) 38 | -------------------------------------------------------------------------------- /app/middlewares/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright 2022 Google LLC. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | -------------------------------------------------------------------------------- /app/middlewares/auth.py: -------------------------------------------------------------------------------- 1 | # Copyright 2022 Google LLC. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | 16 | """ 17 | This module includes decorators for authenticating requests. 18 | """ 19 | 20 | import os 21 | from functools import wraps 22 | from flask import request, abort 23 | 24 | API_KEY = os.environ.get('API_KEY') 25 | 26 | def auth_required(f): 27 | """ 28 | A decorator for view functions that require authentication. 29 | If signed in, pass the request to the decorated view function with 30 | authentication context; otherwise redirect the request. 31 | 32 | Parameters: 33 | f (func): The view function to decorate. 34 | 35 | Output: 36 | decorated (func): The decorated function. 37 | """ 38 | @wraps(f) 39 | def decorated(*args, **kwargs): 40 | token = request.args.get('hub.verify_token') 41 | 42 | # In case the request comes from the challenge route 43 | if API_KEY != token: 44 | return abort(403) 45 | 46 | return f(auth_context=True, *args, **kwargs) 47 | return decorated 48 | 49 | -------------------------------------------------------------------------------- /app/partners/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright 2024 Google LLC. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | """ 16 | A generic partner class to ease the process of integrating with bot managing companies 17 | """ 18 | 19 | from abc import ABC, abstractmethod 20 | 21 | 22 | class Partner(ABC): 23 | @abstractmethod 24 | def process_message(self, payload): 25 | pass 26 | -------------------------------------------------------------------------------- /app/partners/botmaker/botmaker_partner.py: -------------------------------------------------------------------------------- 1 | # Copyright 2022 Google LLC. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | """ 16 | A Botmaker extension for Partners 17 | """ 18 | 19 | import os 20 | from partners import Partner 21 | from helpers.webhook.helpers import get_protocol_by_phone 22 | 23 | ACCOUNT_NUMBER = os.environ.get("ACCOUNT_NUMBER") 24 | 25 | 26 | class BotmakerPartner(Partner): 27 | """ 28 | Process message received 29 | 30 | Parameters: 31 | payload: Botmaker's webhook payload 32 | Ref.: https://botmaker.com/en/developers/ 33 | 34 | """ 35 | 36 | def process_message(self, payload): 37 | if payload.get("contactId") is not None and payload.get("type") == "message": 38 | for message in payload.get("messages"): 39 | try: 40 | if message.get("fromCustomer") == True: 41 | get_protocol_by_phone( 42 | message.get("message"), payload.get("contactId"), ACCOUNT_NUMBER 43 | ) 44 | else: 45 | get_protocol_by_phone( 46 | message.get("message"), ACCOUNT_NUMBER, payload.get("contactId") 47 | ) 48 | except: 49 | continue 50 | -------------------------------------------------------------------------------- /app/partners/factory.py: -------------------------------------------------------------------------------- 1 | # Copyright 2022 Google LLC. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | """ 16 | A factory partner class to ease the process of integrating with bot managing companies 17 | """ 18 | 19 | from enum import Enum 20 | from partners import Partner 21 | from partners.whatsapp.whatsapp_partner import WhatsAppPartner 22 | from partners.botmaker.botmaker_partner import BotmakerPartner 23 | from partners.infobip.infobip_partner import InfobipPartner 24 | from partners.take.take_partner import TakePartner 25 | 26 | 27 | class PartnerType(Enum): 28 | (BOTMAKER, TAKE, WHATSAPP, INFOBIP) = range(4) 29 | 30 | 31 | AVAILABLE_PARTNERS = { 32 | PartnerType.BOTMAKER: BotmakerPartner(), 33 | PartnerType.TAKE: TakePartner(), 34 | PartnerType.WHATSAPP: WhatsAppPartner(), 35 | PartnerType.INFOBIP: InfobipPartner(), 36 | } 37 | 38 | 39 | class PartnerFactory: 40 | def __init__(self, partner_type: PartnerType): 41 | self._partner = PartnerType[partner_type] 42 | 43 | def get(self) -> Partner: 44 | if self._partner: 45 | return AVAILABLE_PARTNERS.get(self._partner) 46 | 47 | else: 48 | raise NotImplementedError( 49 | "Partner not implemented. Please check your configuration." 50 | ) 51 | -------------------------------------------------------------------------------- /app/partners/infobip/infobip_partner.py: -------------------------------------------------------------------------------- 1 | # Copyright 2024 Google LLC. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | """ 16 | A Infobip extension for Partners 17 | """ 18 | 19 | 20 | from partners import Partner 21 | from helpers.webhook.helpers import get_protocol_by_phone 22 | 23 | 24 | class InfobipPartner(Partner): 25 | """ 26 | Process message received 27 | 28 | Parameters: 29 | payload: Infobip's webhook payload 30 | Ref.: https://www.infobip.com/docs/api/channels/whatsapp/whatsapp-inbound-messages/receive-whatsapp-inbound-messages 31 | 32 | """ 33 | 34 | def process_message(self, payload): 35 | if payload.get("results") is not None: 36 | for message in payload.get("results"): 37 | try: 38 | if message.get("integrationType") == "WHATSAPP": 39 | # Only processes text messages 40 | _msg = message.get("message") 41 | if _msg and _msg.get("type") == "TEXT": 42 | get_protocol_by_phone( 43 | _msg.get("text"), message.get("from"), message.get("to") 44 | ) 45 | except: 46 | continue 47 | -------------------------------------------------------------------------------- /app/partners/take/take_partner.py: -------------------------------------------------------------------------------- 1 | # Copyright 2022 Google LLC. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | """ 16 | A Take extension for Partners 17 | """ 18 | 19 | from partners import Partner 20 | from helpers.webhook.helpers import get_protocol_by_phone 21 | 22 | 23 | class TakePartner(Partner): 24 | """ 25 | Process message received 26 | 27 | Parameters: 28 | payload: Take | Blip's webhook payload 29 | Ref.: https://help.blip.ai/hc/en-us/articles/4474381206423-Submitting-data-for-analysis-through-Webhooks 30 | 31 | """ 32 | 33 | def process_message(self, payload): 34 | if payload.get("type") == "text/plain": 35 | get_protocol_by_phone( 36 | payload.get("content"), payload.get("from"), payload.get("to") 37 | ) 38 | -------------------------------------------------------------------------------- /app/partners/whatsapp/whatsapp_partner.py: -------------------------------------------------------------------------------- 1 | # Copyright 2022 Google LLC. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | """ 16 | A Whatsapp extension for Partners 17 | """ 18 | 19 | from partners import Partner 20 | from helpers.webhook.helpers import get_protocol_by_phone 21 | 22 | 23 | class WhatsAppPartner(Partner): 24 | def process_message(self, payload): 25 | """ 26 | Process message received 27 | 28 | Parameters: 29 | payload: WhatsApp's webhook payload 30 | Ref.: https://developers.facebook.com/docs/whatsapp/cloud-api/webhooks/payload-examples 31 | 32 | 33 | """ 34 | if ( 35 | payload.get("object") == "whatsapp_business_account" 36 | and payload.get("entry") is not None 37 | ): 38 | for each in payload.get("entry"): 39 | for change in each["changes"]: 40 | if ( 41 | change["field"] == "messages" 42 | and change["value"].get("messages") is not None 43 | ): 44 | get_protocol_by_phone( 45 | change["value"]["messages"][0]["text"]["body"], 46 | change["value"]["contacts"][0]["wa_id"], 47 | change["value"]["metadata"]["display_phone_number"], 48 | ) 49 | -------------------------------------------------------------------------------- /app/requirements.txt: -------------------------------------------------------------------------------- 1 | flask==3.0.3 2 | gunicorn==23.0.0 3 | flask-cors==5.0.0 4 | Jinja2==3.1.5 5 | requests==2.32.3 6 | MarkupSafe==2.1.5 7 | itsdangerous==2.2.0 8 | google-auth==2.30.0 9 | google-cloud-bigquery==3.24.0 10 | google-cloud-core==2.4.1 11 | tadau==1.0.3 -------------------------------------------------------------------------------- /deployment/deploy.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Copyright 2022 Google LLC 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # https://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | 16 | reset="$(tput sgr 0)" 17 | bold="$(tput bold)" 18 | text_red="$(tput setaf 1)" 19 | text_yellow="$(tput setaf 3)" 20 | text_green="$(tput setaf 2)" 21 | 22 | # Sets defaults variables 23 | BQ_DATASET_NAME=${BQ_DATASET_NAME:="wci"} 24 | BQ_LOCATION=${BQ_DATASET_LOCATION:="US"} 25 | REGION=${REGION:="us-central1"} 26 | SERVICE_ACCOUNT_NAME=${SERVICE_ACCOUNT_NAME:="wci-runner"} 27 | SERVICE_ACCOUNT="${SERVICE_ACCOUNT_NAME}@${GOOGLE_CLOUD_PROJECT}.iam.gserviceaccount.com" 28 | 29 | function start_message() { 30 | echo "☑️ ${bold}${text_green}$1${reset}" 31 | } 32 | function ask() { 33 | while true; do 34 | read -r -p "${BOLD}${1:-Continue?} : ${NOFORMAT}" 35 | case ${REPLY:0:1} in 36 | [yY]) return 0 ;; 37 | [nN]) return 1 ;; 38 | *) echo "Please answer yes or no." 39 | esac 40 | done 41 | } 42 | function enable_services(){ 43 | start_message "Enabling required services (artifactregistry, cloud run)" 44 | gcloud services enable run.googleapis.com 45 | gcloud services enable artifactregistry.googleapis.com 46 | } 47 | function create_artifacts(){ 48 | start_message "Creating WCI repository" 49 | gcloud artifacts repositories create wci \ 50 | --repository-format=docker \ 51 | --location=$REGION \ 52 | --description="WCI Repo" 53 | echo 54 | 55 | # Uploads artifacts 56 | upload_artifacts 57 | } 58 | function upload_artifacts(){ 59 | # Ensures docker is logged-in 60 | # If not, there may be a permission denied when uploading 61 | # the artifcats 62 | start_message "Docker Authorization" 63 | gcloud auth print-access-token | docker login -u oauth2accesstoken --password-stdin https://$REGION-docker.pkg.dev 64 | echo 65 | 66 | # Builds WCI image 67 | start_message "Building WCI Image..." 68 | docker build --no-cache ./ -t wci -f ./deployment/docker/Dockerfile 69 | docker tag wci $REGION-docker.pkg.dev/$GOOGLE_CLOUD_PROJECT/wci/wci 70 | docker push $REGION-docker.pkg.dev/$GOOGLE_CLOUD_PROJECT/wci/wci 71 | echo 72 | } 73 | function create_service_account() { 74 | start_message "Creating the service account ${SERVICE_ACCOUNT_NAME}..." 75 | gcloud iam service-accounts create $SERVICE_ACCOUNT_NAME \ 76 | --display-name='Service Account for WCI use' 77 | # Add editor role to the service account 78 | gcloud projects add-iam-policy-binding $GOOGLE_CLOUD_PROJECT \ 79 | --member "serviceAccount:${SERVICE_ACCOUNT}" \ 80 | --role "roles/bigquery.admin" 81 | echo 82 | } 83 | function create_bq_dataset() { 84 | start_message "Creating dataset ${BQ_DATASET_NAME}..." 85 | bq --location=$BQ_LOCATION mk \ 86 | --dataset \ 87 | --description="BigQuery dataset for WCI" \ 88 | --label=name:$BQ_DATASET_NAME \ 89 | $GOOGLE_CLOUD_PROJECT:$BQ_DATASET_NAME 90 | echo 91 | } 92 | function create_bq_tables(){ 93 | start_message "Creating WCI schema:chat_leads..." 94 | 95 | bq mk \ 96 | --table \ 97 | --description="BigQuery table for WCI" \ 98 | $GOOGLE_CLOUD_PROJECT:$BQ_DATASET_NAME.chat_leads \ 99 | sender:STRING,receiver:STRING,message:STRING,timestamp:TIMESTAMP 100 | echo 101 | 102 | start_message "Creating WCI schema:pending_leads..." 103 | bq mk \ 104 | --table \ 105 | --description="BigQuery table for WCI" \ 106 | $GOOGLE_CLOUD_PROJECT:$BQ_DATASET_NAME.pending_leads \ 107 | identifier:STRING,type:STRING,protocol:STRING,mapped:JSON,timestamp:TIMESTAMP 108 | echo 109 | 110 | start_message "Creating WCI schema:leads..." 111 | bq mk \ 112 | --table \ 113 | --description="BigQuery table for WCI" \ 114 | $GOOGLE_CLOUD_PROJECT:$BQ_DATASET_NAME.leads \ 115 | protocol:STRING,phone:STRING,timestamp:TIMESTAMP 116 | echo 117 | } 118 | function show_endpoints(){ 119 | ENDPOINT="$(gcloud run services list | grep -oP '(http|https)://wci(.*)')" 120 | echo "${bold}${text_yellow}NEXT STEPS: To finalize, set your account's webhook to${reset}" 121 | echo "${bold}Callback URL: ${text_yellow}${ENDPOINT}/webhook-wci${reset}" 122 | echo "${bold}Verify token: ${text_yellow}${API_KEY}${reset}" 123 | echo "${bold}Lead URL: ${text_yellow}${ENDPOINT}/webhook${reset}" 124 | echo "${bold}Message Ads URL: ${text_yellow}${ENDPOINT}/webhook-ctm${reset}" 125 | echo 126 | } 127 | function deploy_app(){ 128 | echo "${bold}${text_green}To deploy, inform the following values:${reset}" 129 | 130 | echo -n "Type the API Key:" 131 | read -r API_KEY 132 | 133 | echo -n "Type the message to be sent with the protocol number (E.g. Your protocol is):" 134 | read -r PROTOCOL_MESSAGE 135 | 136 | echo -n "Type the message to be sent AFTER the protocol number (E.g. Your protocol is 98765432. Hello, Advertiser):" 137 | read -r WELCOME_MESSAGE 138 | 139 | echo -n "How would like to connect to your WhatsApp Business Account? Through BOTMAKER, TAKE, INFOBIP, WHATSAPP (Choose and type one):" 140 | read -r PARTNER_TYPE 141 | 142 | echo -n "Would you like to enable ECL - Enhanced Conversion for Leads? (E.g. true/false):" 143 | read -r ECL_ENABLED 144 | 145 | echo -n "Optin to collect usage stats to improve the solution. This helps us suporting the solution (E.g. yes/no):" 146 | read -r STATS_OPTIN 147 | 148 | 149 | cp ./app/app.yaml ./deployment/ 150 | 151 | sed -i "s/{{GOOGLE_CLOUD_PROJECT}}/$GOOGLE_CLOUD_PROJECT/g" ./deployment/app.yaml 152 | sed -i "s/{{REGION}}/$REGION/g" ./deployment/app.yaml 153 | sed -i "s/{{SERVICE_ACCOUNT}}/${SERVICE_ACCOUNT}/g" ./deployment/app.yaml 154 | sed -i "s/{{BQ_DATASET_NAME}}/$BQ_DATASET_NAME/g" ./deployment/app.yaml 155 | sed -i "s/{{API_KEY}}/${API_KEY}/g" ./deployment/app.yaml 156 | sed -i "s/{{PROTOCOL_MESSAGE}}/${PROTOCOL_MESSAGE}/g" ./deployment/app.yaml 157 | sed -i "s/{{WELCOME_MESSAGE}}/${WELCOME_MESSAGE}/g" ./deployment/app.yaml 158 | sed -i "s/{{STATS_OPTIN}}/${STATS_OPTIN}/g" ./deployment/app.yaml 159 | sed -i "s/{{PARTNER_TYPE}}/${PARTNER_TYPE}/g" ./deployment/app.yaml 160 | sed -i "s/{{ECL_ENABLED}}/${ECL_ENABLED}/g" ./deployment/app.yaml 161 | echo 162 | 163 | # Create repository for docker image 164 | create_artifacts 165 | echo 166 | 167 | # Forces to sleep in order to artifact be uploaded completely 168 | sleep 45s 169 | 170 | # Deploys the service 171 | start_message "Deploying WCI Service..." 172 | gcloud run services replace ./deployment/app.yaml 173 | echo 174 | 175 | echo "✅ ${bold}${text_green} Service deployed${reset}" 176 | echo 177 | 178 | start_message "Making WCI accessible..." 179 | gcloud run services add-iam-policy-binding wci --region $REGION \ 180 | --member="allUsers" \ 181 | --role="roles/run.invoker" 182 | echo 183 | 184 | show_endpoints 185 | } 186 | function init() { 187 | 188 | # Firstly checks for flags 189 | # In case the user is only interested in updating their service 190 | # to the lastest version, then skips the entire deployment 191 | # and builds artifacts and renews the cloud run revision to 192 | # the latest one 193 | if [ $flag_service = "update" ]; then 194 | start_message "Updating WCI service..." 195 | upload_artifacts 196 | # Forces to sleep in order to artifact be uploaded completely 197 | sleep 45s 198 | gcloud run services update wci --image=$REGION-docker.pkg.dev/$GOOGLE_CLOUD_PROJECT/wci/wci:latest --region=$REGION 199 | echo "✅ ${bold}${text_green} WCI updated!${reset}" 200 | echo 201 | show_endpoints 202 | echo 203 | return; 204 | fi 205 | 206 | # Initiates deployment 207 | echo 208 | echo "${bold}┌────────────────┐${reset}" 209 | echo "${bold}│ WCI Deployment │${reset}" 210 | echo "${bold}└────────────────┘${reset}" 211 | echo 212 | echo "${bold}${text_red}This is not an officially supported Google product.${reset}" 213 | echo "${bold}WCI will be deployed in the Google Cloud project ${text_green}${GOOGLE_CLOUD_PROJECT}${bold}${reset}" 214 | echo 215 | if [ -z "${CLOUD_SHELL}" ]; then 216 | echo "${bold}${text_yellow}WARNING! You are not running this script from the Google Cloud Shell environment.${reset}" 217 | echo 218 | fi 219 | 220 | # Confirm details 221 | echo 222 | echo "${bold}${text_green}Settings${reset}" 223 | echo "${bold}${text_green}──────────────────────────────────────────${reset}" 224 | echo "${bold}${text_green}Project ID: ${GOOGLE_CLOUD_PROJECT}${reset}" 225 | echo 226 | if ask "Continue?"; then 227 | 228 | echo 229 | enable_services 230 | 231 | echo 232 | EXISTING_SERVICE_ACCOUNT=$(gcloud iam service-accounts list --filter "email:${SERVICE_ACCOUNT_NAME}" --format="value(email)") 233 | if [ -z "${EXISTING_SERVICE_ACCOUNT}" ]; then 234 | create_service_account 235 | else 236 | echo 237 | echo "${text_yellow}INFO: Service account '${SERVICE_ACCOUNT_NAME}' already exists.${reset}" 238 | echo 239 | fi 240 | 241 | EXISTING_BQ_DATASET=$(bq ls --filter labels."name:${BQ_DATASET_NAME}") 242 | echo 'Dataset' $EXISTING_BQ_DATASET 243 | if [ -z "${EXISTING_BQ_DATASET}" ]; then 244 | create_bq_dataset 245 | else 246 | echo 247 | echo "${text_yellow}INFO: The dataset '${BQ_DATASET_NAME}' already exists."${reset} 248 | echo 249 | fi 250 | 251 | create_bq_tables 252 | deploy_app 253 | echo "✅ ${bold}${text_green} Deployment Done!${reset}" 254 | 255 | echo "🟡 ${bold}${text_yellow} Click: https://groups.google.com/g/wci-announce to stay up to date on updates, features and use cases (highly recommended)!" 256 | echo 257 | fi 258 | } 259 | 260 | for ARGUMENT in "$@" 261 | do 262 | KEY=$(echo $ARGUMENT | cut -f1 -d=) 263 | KEY_LENGTH=${#KEY} 264 | VALUE="${ARGUMENT:$KEY_LENGTH+1}" 265 | export "flag_$KEY"="$VALUE" 266 | done 267 | 268 | # Get parameters 269 | if [ -z "${GOOGLE_CLOUD_PROJECT}" ]; then 270 | GOOGLE_CLOUD_PROJECT="$(gcloud config get-value project)" 271 | fi 272 | 273 | init -------------------------------------------------------------------------------- /deployment/docker/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.10-slim 2 | 3 | # Allow statements and log messages to immediately appear in the Knative logs 4 | ENV PYTHONUNBUFFERED True 5 | 6 | # Copy local code to the container image. 7 | ENV APP_HOME /app 8 | WORKDIR $APP_HOME 9 | COPY app ./ 10 | 11 | # Install dependencies. 12 | RUN pip install --no-cache-dir -r requirements.txt 13 | 14 | # Run the web service on container startup. Here we use the gunicorn 15 | # webserver, with one worker process and 8 threads. 16 | # For environments with multiple CPU cores, increase the number of workers 17 | # to be equal to the cores available. 18 | # Timeout is set to 0 to disable the timeouts of the workers to allow Cloud Run to handle instance scaling. 19 | CMD exec gunicorn --bind :$PORT --workers 1 --threads 8 --timeout 0 main:app -------------------------------------------------------------------------------- /deployment/samples/app_engine/app_engine.yaml: -------------------------------------------------------------------------------- 1 | # Copyright 2024 Google LLC. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | # How to use this file? 16 | # Filled out all the missing pieces below (within {{}}), 17 | # move the file under /app folder and run on Cloud shell: 18 | # gcloud app deploy app_engine.yaml 19 | runtime: python311 20 | service: wci 21 | service_account: {{SERVICE_ACCOUNT}} 22 | 23 | env_variables: 24 | GOOGLE_CLOUD_PROJECT: "{{GOOGLE_CLOUD_PROJECT}}" 25 | DATA_SOURCE_TYPE: "BIG_QUERY" 26 | PARTNER_TYPE: "{{PARTNER_TYPE}}" 27 | BQ_PENDING_LEAD_TABLE: "{{GOOGLE_CLOUD_PROJECT}}.wci.pending_leads" 28 | BQ_LEAD_TABLE: "{{GOOGLE_CLOUD_PROJECT}}.wci.leads" 29 | BQ_CHAT_TABLE: "{{GOOGLE_CLOUD_PROJECT}}.wci.chat_leads" 30 | API_KEY: "{{API_KEY}}" 31 | PROTOCOL_MESSAGE: "{{PROTOCOL_MESSAGE}}" 32 | WELCOME_MESSAGE: "{{WELCOME_MESSAGE}}" 33 | CTM_PROTOCOL_MESSAGE: "Chat ID:" 34 | ECL_ENABLED: "False" 35 | STATS_OPTIN: "yes" 36 | 37 | handlers: 38 | - url: /.* 39 | script: auto 40 | secure: always 41 | redirect_http_response_code: 301 -------------------------------------------------------------------------------- /deployment/samples/app_engine/app_engine_flexible.yaml: -------------------------------------------------------------------------------- 1 | # Copyright 2024 Google LLC. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | # How to use this file? 16 | # Filled out all the missing pieces below (within {{}}), 17 | # move the file under /app folder and run on Cloud shell: 18 | # gcloud app deploy app_engine_flexible.yaml 19 | runtime: custom 20 | env: flex 21 | service: wci 22 | service_account: {{SERVICE_ACCOUNT}} 23 | 24 | env_variables: 25 | DATA_SOURCE_TYPE: "BIG_QUERY" 26 | PARTNER_TYPE: "{{PARTNER_TYPE}}" 27 | BQ_PENDING_LEAD_TABLE: "{{GOOGLE_CLOUD_PROJECT}}.wci.pending_leads" 28 | BQ_LEAD_TABLE: "{{GOOGLE_CLOUD_PROJECT}}.wci.leads" 29 | BQ_CHAT_TABLE: "{{GOOGLE_CLOUD_PROJECT}}.wci.chat_leads" 30 | API_KEY: "{{API_KEY}}" 31 | PROTOCOL_MESSAGE: "{{PROTOCOL_MESSAGE}}" 32 | WELCOME_MESSAGE: "{{WELCOME_MESSAGE}}" 33 | CTM_PROTOCOL_MESSAGE: "Chat ID:" 34 | ECL_ENABLED: "False" 35 | STATS_OPTIN: "Yes" 36 | 37 | liveness_check: 38 | path: '/health_checker' 39 | check_interval_sec: 300 40 | timeout_sec: 300 41 | failure_threshold: 4 42 | success_threshold: 2 43 | initial_delay_sec: 1800 44 | 45 | readiness_check: 46 | path: '/health_checker' 47 | check_interval_sec: 300 48 | timeout_sec: 300 49 | failure_threshold: 4 50 | success_threshold: 2 51 | app_start_timeout_sec: 3600 -------------------------------------------------------------------------------- /docs/code-of-conduct.md: -------------------------------------------------------------------------------- 1 | # Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, gender identity and expression, level of 9 | experience, education, socio-economic status, nationality, personal appearance, 10 | race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or reject 41 | comments, commits, code, wiki edits, issues, and other contributions that are 42 | not aligned to this Code of Conduct, or to ban temporarily or permanently any 43 | contributor for other behaviors that they deem inappropriate, threatening, 44 | offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | This Code of Conduct also applies outside the project spaces when the Project 56 | Steward has a reasonable belief that an individual's behavior may have a 57 | negative impact on the project or its community. 58 | 59 | ## Conflict Resolution 60 | 61 | We do not believe that all conflict is bad; healthy debate and disagreement 62 | often yield positive results. However, it is never okay to be disrespectful or 63 | to engage in behavior that violates the project’s code of conduct. 64 | 65 | If you see someone violating the code of conduct, you are encouraged to address 66 | the behavior directly with those involved. Many issues can be resolved quickly 67 | and easily, and this gives people more control over the outcome of their 68 | dispute. If you are unable to resolve the matter for any reason, or if the 69 | behavior is threatening or harassing, report it. We are dedicated to providing 70 | an environment where participants feel welcome and safe. 71 | 72 | Reports should be directed to *[PROJECT STEWARD NAME(s) AND EMAIL(s)]*, the 73 | Project Steward(s) for *[PROJECT NAME]*. It is the Project Steward’s duty to 74 | receive and address reported violations of the code of conduct. They will then 75 | work with a committee consisting of representatives from the Open Source 76 | Programs Office and the Google Open Source Strategy team. If for any reason you 77 | are uncomfortable reaching out to the Project Steward, please email 78 | opensource@google.com. 79 | 80 | We will investigate every complaint, but you may not receive a direct response. 81 | We will use our discretion in determining when and how to follow up on reported 82 | incidents, which may range from not taking action to permanent expulsion from 83 | the project and project-sponsored spaces. We will notify the accused of the 84 | report and provide them an opportunity to discuss it before any action is taken. 85 | The identity of the reporter will be omitted from the details of the report 86 | supplied to the accused. In potentially harmful situations, such as ongoing 87 | harassment or threats to anyone's safety, we may take action without notice. 88 | 89 | ## Attribution 90 | 91 | This Code of Conduct is adapted from the Contributor Covenant, version 1.4, 92 | available at 93 | https://www.contributor-covenant.org/version/1/4/code-of-conduct.html -------------------------------------------------------------------------------- /docs/contributing.md: -------------------------------------------------------------------------------- 1 | # How to Contribute 2 | 3 | We'd love to accept your patches and contributions to this project. There are 4 | just a few small guidelines you need to follow. 5 | 6 | ## Contributor License Agreement 7 | 8 | Contributions to this project must be accompanied by a Contributor License 9 | Agreement. You (or your employer) retain the copyright to your contribution; 10 | this simply gives us permission to use and redistribute your contributions as 11 | part of the project. Head over to to see 12 | your current agreements on file or to sign a new one. 13 | 14 | You generally only need to submit a CLA once, so if you've already submitted one 15 | (even if it was for a different project), you probably don't need to do it 16 | again. 17 | 18 | ## Code Reviews 19 | 20 | All submissions, including submissions by project members, require review. We 21 | use GitHub pull requests for this purpose. Consult 22 | [GitHub Help](https://help.github.com/articles/about-pull-requests/) for more 23 | information on using pull requests. 24 | 25 | ## Community Guidelines 26 | 27 | This project follows [Google's Open Source Community 28 | Guidelines](https://opensource.google/conduct/). -------------------------------------------------------------------------------- /docs/wci_bsp_guide.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/google/wci/853f580258fb207cfab83c79256e62c6f269dacc/docs/wci_bsp_guide.pdf -------------------------------------------------------------------------------- /docs/wci_guide.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/google/wci/853f580258fb207cfab83c79256e62c6f269dacc/docs/wci_guide.pdf -------------------------------------------------------------------------------- /run_local.sh: -------------------------------------------------------------------------------- 1 | # Sets required env vars 2 | export GOOGLE_CLOUD_PROJECT=$(gcloud config get-value project) 3 | export DATA_SOURCE_TYPE=BIG_QUERY 4 | export PARTNER_TYPE=WHATSAPP 5 | export BQ_DATASET_NAME=wci 6 | export API_KEY="My_Key" 7 | export PROTOCOL_MESSAGE="Chat ID" 8 | export WELCOME_MESSAGE="Hello, World" 9 | export STATS_OPTIN=yes 10 | export ECL_ENABLED=false 11 | export CTM_PROTOCOL_MESSAGE="Chat ID:" 12 | export BQ_PENDING_LEAD_TABLE="${GOOGLE_CLOUD_PROJECT}.${BQ_DATASET_NAME}.pending_leads" 13 | export BQ_LEAD_TABLE="${GOOGLE_CLOUD_PROJECT}.${BQ_DATASET_NAME}.leads" 14 | export BQ_CHAT_TABLE="${GOOGLE_CLOUD_PROJECT}.${BQ_DATASET_NAME}.chat_leads" 15 | 16 | # Runs the solution locally 17 | python3 app/main.py -------------------------------------------------------------------------------- /run_local_webhook.sh: -------------------------------------------------------------------------------- 1 | 2 | curl -X POST "http://127.0.0.1:${PORT}/webhook-wci" \ 3 | -H "Content-Type: application/json" \ 4 | -d '{ 5 | "object": "whatsapp_business_account", 6 | "entry": [ 7 | { 8 | "id": "1234", 9 | "changes": [ 10 | { 11 | "value": { 12 | "messaging_product": "whatsapp", 13 | "metadata": { 14 | "display_phone_number": "+1 234-5678", 15 | "phone_number_id": "12345678" 16 | }, 17 | "contacts": [ 18 | { 19 | "profile": { 20 | "name": "Test, Test" 21 | }, 22 | "wa_id": "123" 23 | } 24 | ], 25 | "messages": [ 26 | { 27 | "from": "12345678", 28 | "id": "test_1", 29 | "timestamp": "1723232055", 30 | "text": { 31 | "body": "This is a local test" 32 | }, 33 | "type": "text" 34 | } 35 | ] 36 | }, 37 | "field": "messages" 38 | } 39 | ] 40 | } 41 | ] 42 | }' -------------------------------------------------------------------------------- /tutorial.md: -------------------------------------------------------------------------------- 1 | # WCI - Conversion Import for Whatsapp 2 | 3 | ## Welcome 4 | Welcome to the guide deployment of the WhatsApp Conversion Import (WCI). We will deploy the solution on Google Cloud. 5 | 6 | WCI offers a sample on how to integrate and track conversions that happen in business-account chats by linking clicked leads (click to chat) to final conversions (purchased through chat app) 7 | 8 | **Disclaimer:** This is not an officially supported Google product 9 | 10 | ## Setup 11 | 12 | First of all, you will need to configure the Google Cloud project you're going to use. 13 | 14 | 15 | After selecting the project in the menu above, execute the following command: 16 | 17 | ``` bash 18 | gcloud config set project 19 | ``` 20 | 21 | ## WCI Deployment - Step 1 22 | In the Cloud Shell, execute the following command: 23 | 24 | ``` bash 25 | . ./deployment/deploy.sh 26 | ``` 27 | 28 | WCI will be deployed in the Google Cloud Project, and it will choose by default the current active project you're at. 29 | If you want to deploy to the Active project, type Y/y to Continue 30 | 31 | ```bash 32 | Y 33 | ``` 34 | 35 | ## WCI Deployment - Step 2 36 | 37 | During the App deployment, it will ask for a few specific information: 38 | - The Whatsapp Business Account number (e.g. 5511999888777) 39 | - The API Key (This is the WhatsApp API Key) 40 | - The message to be sent with the protocol number (E.g. *Your protocol is* XXXXXXXX) 41 | - Type the message to be sent AFTER the protocol number (E.g. Your protocol is 98765432. *Hello, Customer*) 42 | 43 | ## Confirmation 44 | 45 | The solution will then prompt you to accept the services that will be deployed. 46 | If they are all correct, please write Y/y to continue 47 | 48 | ``` bash 49 | Y 50 | ``` 51 | --------------------------------------------------------------------------------