Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[16.0][IMP] mail_gateway_whatsapp: Add support for WhatsApp templates #1497

Draft
wants to merge 2 commits into
base: 16.0
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 3 additions & 5 deletions mail_gateway/models/mail_gateway.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Copyright 2024 Dixmit
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from odoo import Command, api, fields, models, tools
from odoo import api, fields, models, tools


class MailGateway(models.Model):
Expand All @@ -25,12 +25,10 @@ class MailGateway(models.Model):
)
webhook_user_id = fields.Many2one(
"res.users",
default=lambda self: self.env.user.id,
default=lambda self: self.env.ref("base.user_root"),
help="User that will create the messages",
)
member_ids = fields.Many2many(
"res.users", default=lambda self: [Command.link(self.env.user.id)]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this changes are the reason behind the test issue

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this changes are the reason behind the test issue

The CI has been failing for several days on all PRs/commits in V16.

The last commit is unrelated to WhatsApp templates. If you prefer, I can create a new PR for it separately. However, this commit is not linked to the tests but addresses a different case, please refer to the commit description for more details.

Let me know what you think.

)
member_ids = fields.Many2many("res.users")
company_id = fields.Many2one(
"res.company", default=lambda self: self.env.company.id
)
Expand Down
12 changes: 12 additions & 0 deletions mail_gateway_whatsapp/README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ In order to make it you must follow this steps:

* Use the Meta App authentication key as `Token` field
* Use the Meta App Phone Number ID as `Whatsapp from Phone` field
* Use the Meta Account Business ID as `Whatsapp account` field (only if you need sync templates)
* Write your own `Webhook key`
* Use the Application Secret Key on `Whatsapp Security Key`. It will be used in order to validate the data
* Press the `Integrate Webhook Key`. In this case, it will not integrate it, we need to make it manually
Expand All @@ -79,6 +80,14 @@ Usage
2. Wait until someone starts a conversation.
3. Now you will be able to respond and receive messages to this person.

Known issues / Roadmap
======================

**WhatsApp templates**

* Add support for `Variables`
* Add support for `Buttons`

Bug Tracker
===========

Expand All @@ -103,6 +112,9 @@ Contributors

* Olga Marco <olga.marco@creublanca.es>
* Enric Tobella <etobella@creublanca.es>
* `Tecnativa <https://www.tecnativa.com>`_:

* Carlos Lopez

Other credits
~~~~~~~~~~~~~
Expand Down
1 change: 1 addition & 0 deletions mail_gateway_whatsapp/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from . import models
from . import tools

# from . import services
from . import wizards
3 changes: 3 additions & 0 deletions mail_gateway_whatsapp/__manifest__.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,11 @@
"depends": ["mail_gateway", "phone_validation"],
"external_dependencies": {"python": ["requests_toolbelt"]},
"data": [
"security/security.xml",
"security/ir.model.access.csv",
"wizards/whatsapp_composer.xml",
"wizards/mail_compose_gateway_message.xml",
"views/mail_whatsapp_template_views.xml",
"views/mail_gateway.xml",
],
"assets": {
Expand Down
1 change: 1 addition & 0 deletions mail_gateway_whatsapp/models/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@
from . import mail_gateway_whatsapp
from . import mail_channel
from . import res_partner
from . import mail_whatsapp_template
62 changes: 61 additions & 1 deletion mail_gateway_whatsapp/models/mail_gateway.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
# Copyright 2022 Creu Blanca
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
import requests
from werkzeug.urls import url_join

from odoo import fields, models
from odoo import _, api, fields, models
from odoo.exceptions import UserError

BASE_URL = "https://graph.facebook.com/"


class MailGateway(models.Model):
Expand All @@ -13,3 +18,58 @@ class MailGateway(models.Model):
)
whatsapp_from_phone = fields.Char()
whatsapp_version = fields.Char(default="15.0")
whatsapp_account_id = fields.Char()
whatsapp_template_ids = fields.One2many("mail.whatsapp.template", "gateway_id")
whatsapp_template_count = fields.Integer(compute="_compute_whatsapp_template_count")

@api.depends("whatsapp_template_ids")
def _compute_whatsapp_template_count(self):
for gateway in self:
gateway.whatsapp_template_count = len(gateway.whatsapp_template_ids)

def button_import_whatsapp_template(self):
self.ensure_one()
WhatsappTemplate = self.env["mail.whatsapp.template"]
if not self.whatsapp_account_id:
raise UserError(_("WhatsApp Account is required to import templates."))
meta_info = {}
template_url = url_join(
BASE_URL,
f"v{self.whatsapp_version}/{self.whatsapp_account_id}/message_templates",
)
try:
meta_request = requests.get(
template_url,
headers={"Authorization": f"Bearer {self.token}"},
timeout=10,
)
meta_request.raise_for_status()
meta_info = meta_request.json()
except Exception as err:
raise UserError(str(err)) from err
current_templates = WhatsappTemplate.with_context(active_test=False).search(
[("gateway_id", "=", self.id)]
)
templates_by_id = {t.template_uid: t for t in current_templates}
create_vals = []
for template_data in meta_info.get("data", []):
ws_template = templates_by_id.get(template_data["id"])
if ws_template:
ws_template.write(
WhatsappTemplate._prepare_values_to_import(self, template_data)
)
else:
create_vals.append(
WhatsappTemplate._prepare_values_to_import(self, template_data)
)
WhatsappTemplate.create(create_vals)
return {
"type": "ir.actions.client",
"tag": "display_notification",
"params": {
"title": _("WathsApp Templates"),
"type": "success",
"message": _("Synchronization successfully."),
"next": {"type": "ir.actions.act_window_close"},
},
}
27 changes: 24 additions & 3 deletions mail_gateway_whatsapp/models/mail_gateway_whatsapp.py
Original file line number Diff line number Diff line change
Expand Up @@ -285,14 +285,35 @@ def _send(
def _send_payload(
self, channel, body=False, media_id=False, media_type=False, media_name=False
):
whatsapp_template = self.env["mail.whatsapp.template"]
if self.env.context.get("whatsapp_template_id"):
whatsapp_template = self.env["mail.whatsapp.template"].browse(
self.env.context.get("whatsapp_template_id")
)
if body:
return {
payload = {
"messaging_product": "whatsapp",
"recipient_type": "individual",
"to": channel.gateway_channel_token,
"type": "text",
"text": {"preview_url": False, "body": html2plaintext(body)},
}
if whatsapp_template:
payload.update(
{
"type": "template",
"template": {
"name": whatsapp_template.template_name,
"language": {"code": whatsapp_template.language},
},
}
)
else:
payload.update(
{
"type": "text",
"text": {"preview_url": False, "body": html2plaintext(body)},
}
)
return payload
if media_id:
media_data = {"id": media_id}
if media_type == "document":
Expand Down
193 changes: 193 additions & 0 deletions mail_gateway_whatsapp/models/mail_whatsapp_template.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,193 @@
# Copyright 2024 Tecnativa - Carlos López
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
import re

import requests
from werkzeug.urls import url_join

from odoo import api, fields, models
from odoo.exceptions import UserError
from odoo.tools import ustr

from odoo.addons.http_routing.models.ir_http import slugify

from ..tools.const import supported_languages
from .mail_gateway import BASE_URL


class MailWhatsAppTemplate(models.Model):
_name = "mail.whatsapp.template"
_description = "Mail WhatsApp template"

name = fields.Char(required=True)
body = fields.Text(required=True)
header = fields.Char()
footer = fields.Char()
template_name = fields.Char(
compute="_compute_template_name", store=True, copy=False
)
is_supported = fields.Boolean(copy=False)
template_uid = fields.Char(readonly=True, copy=False)
category = fields.Selection(
[
("authentication", "Authentication"),
("marketing", "Marketing"),
("utility", "Utility"),
],
required=True,
)
state = fields.Selection(
[
("draft", "Draft"),
("pending", "Pending"),
("approved", "Approved"),
("in_appeal", "In Appeal"),
("rejected", "Rejected"),
("pending_deletion", "Pending Deletion"),
("deleted", "Deleted"),
("disabled", "Disabled"),
("paused", "Paused"),
("limit_exceeded", "Limit Exceeded"),
("archived", "Archived"),
],
default="draft",
required=True,
)
language = fields.Selection(supported_languages, required=True)
gateway_id = fields.Many2one(
"mail.gateway",
domain=[("gateway_type", "=", "whatsapp")],
required=True,
ondelete="cascade",
)
company_id = fields.Many2one(
"res.company", related="gateway_id.company_id", store=True
)

_sql_constraints = [
(
"unique_name_gateway_id",
"unique(name, language, gateway_id)",
"Duplicate name is not allowed for Gateway.",
)
]

@api.depends("name", "state", "template_uid")
def _compute_template_name(self):
for template in self:
if not template.template_name or (
template.state == "draft" and not template.template_uid
):
template.template_name = re.sub(
r"\W+", "_", slugify(template.name or "")
)

def button_back2draft(self):
self.write({"state": "draft"})

def button_export_template(self):
self.ensure_one()
gateway = self.gateway_id
template_url = url_join(
BASE_URL,
f"v{gateway.whatsapp_version}/{gateway.whatsapp_account_id}/message_templates",
)
try:
payload = self._prepare_values_to_export()
response = requests.post(
template_url,
headers={"Authorization": "Bearer %s" % gateway.token},
json=payload,
timeout=10,
)
response.raise_for_status()
json_data = response.json()
self.write(
{
"template_uid": json_data.get("id"),
"state": json_data.get("status").lower(),
"is_supported": True,
}
)
except requests.exceptions.HTTPError as ex:
msj = f"{ustr(ex)} \n{ex.response.text}"
raise UserError(msj) from ex
except Exception as err:
raise UserError(ustr(err)) from err

def _prepare_values_to_export(self):
components = self._prepare_components_to_export()
return {
"name": self.template_name,
"category": self.category.upper(),
"language": self.language,
"components": components,
}

def _prepare_components_to_export(self):
components = [{"type": "BODY", "text": self.body}]
if self.header:
components.append(
{
"type": "HEADER",
"format": "text",
"text": self.header,
}
)
if self.footer:
components.append(
{
"type": "FOOTER",
"text": self.footer,
}
)
# TODO: add more components(buttons, location, etc)
return components

def button_sync_template(self):
self.ensure_one()
gateway = self.gateway_id
template_url = url_join(
BASE_URL,
f"{self.template_uid}",
)
try:
response = requests.get(
template_url,
headers={"Authorization": "Bearer %s" % gateway.token},
timeout=10,
)
response.raise_for_status()
json_data = response.json()
vals = self._prepare_values_to_import(gateway, json_data)
self.write(vals)
except Exception as err:
raise UserError(str(err)) from err
return {
"type": "ir.actions.client",
"tag": "reload",
}

@api.model
def _prepare_values_to_import(self, gateway, json_data):
vals = {
"name": json_data.get("name").replace("_", " ").title(),
"template_name": json_data.get("name"),
"category": json_data.get("category").lower(),
"language": json_data.get("language"),
"state": json_data.get("status").lower(),
"template_uid": json_data.get("id"),
"gateway_id": gateway.id,
}
is_supported = True
for component in json_data.get("components", []):
if component["type"] == "HEADER" and component["format"] == "TEXT":
vals["header"] = component["text"]
elif component["type"] == "BODY":
vals["body"] = component["text"]
elif component["type"] == "FOOTER":
vals["footer"] = component["text"]
else:
is_supported = False
vals["is_supported"] = is_supported
return vals
1 change: 1 addition & 0 deletions mail_gateway_whatsapp/readme/CONFIGURE.rst
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ In order to make it you must follow this steps:

* Use the Meta App authentication key as `Token` field
* Use the Meta App Phone Number ID as `Whatsapp from Phone` field
* Use the Meta Account Business ID as `Whatsapp account` field (only if you need sync templates)
* Write your own `Webhook key`
* Use the Application Secret Key on `Whatsapp Security Key`. It will be used in order to validate the data
* Press the `Integrate Webhook Key`. In this case, it will not integrate it, we need to make it manually
Expand Down
3 changes: 3 additions & 0 deletions mail_gateway_whatsapp/readme/CONTRIBUTORS.rst
Original file line number Diff line number Diff line change
@@ -1,2 +1,5 @@
* Olga Marco <olga.marco@creublanca.es>
* Enric Tobella <etobella@creublanca.es>
* `Tecnativa <https://www.tecnativa.com>`_:

* Carlos Lopez
Loading
Loading