From 2518174da9c8535e8464702d6708bd152a1daf09 Mon Sep 17 00:00:00 2001 From: "duc.tdm" Date: Fri, 2 Oct 2026 16:52:18 +0700 Subject: [PATCH 01/41] [FIX] mail_gateway: process webhooks as the webhook user The webhook route is public, so the transaction default environment is the public user. Using with_user() was not enough: when the ORM writes a relation to a model that forbids sudo commands (like res.partner), it checks the access with the transaction user. Incoming messages from a new contact then failed with an AccessError when webhook_user_id was a regular internal user. Use request.update_env() instead, so the transaction runs as the webhook user. --- mail_gateway/controllers/gateway.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/mail_gateway/controllers/gateway.py b/mail_gateway/controllers/gateway.py index a7acd954c..8e7909176 100644 --- a/mail_gateway/controllers/gateway.py +++ b/mail_gateway/controllers/gateway.py @@ -34,9 +34,11 @@ def post_update(self, usage, token, *args, **kwargs): ("Content-Type", "application/json"), ], ) + # Use update_env and not with_user: the ORM checks some accesses + # with the transaction user (e.g. res.partner forbids sudo commands) + request.update_env(user=bot_data["webhook_user_id"]) return ( request.env[f"mail.gateway.{usage}"] - .with_user(bot_data["webhook_user_id"]) .with_company(bot_data["company_id"]) ._receive_get_update(bot_data, request, **kwargs) ) @@ -61,10 +63,9 @@ def post_update(self, usage, token, *args, **kwargs): or "utf-8" ) jsonrequest = json.loads(request.httprequest.get_data().decode(charset)) - dispatcher = ( - request.env[f"mail.gateway.{usage}"] - .with_user(bot_data["webhook_user_id"]) - .with_context(no_gateway_notification=True) + request.update_env(user=bot_data["webhook_user_id"]) + dispatcher = request.env[f"mail.gateway.{usage}"].with_context( + no_gateway_notification=True ) if not dispatcher._verify_update(bot_data, jsonrequest): _logger.warning( From b04d7f8319342254b9517e13b0f3d17c5f987e8c Mon Sep 17 00:00:00 2001 From: "duc.tdm" Date: Fri, 2 Oct 2026 17:42:06 +0700 Subject: [PATCH 02/41] [IMP] mail_gateway: only allow gateway users as webhook user The webhook user processes the incoming messages, so it needs the access of a gateway user (e.g. to link the authors to their partner). Restrict the selection to users of the Gateway / User group and document it. --- mail_gateway/README.rst | 2 ++ mail_gateway/models/mail_gateway.py | 5 ++++- mail_gateway/readme/CONFIGURE.md | 2 ++ mail_gateway/static/description/index.html | 2 ++ 4 files changed, 10 insertions(+), 1 deletion(-) diff --git a/mail_gateway/README.rst b/mail_gateway/README.rst index 72f951211..094db0e34 100644 --- a/mail_gateway/README.rst +++ b/mail_gateway/README.rst @@ -53,6 +53,8 @@ Configuration - Go to ``Settings / Technical / Email / Gateway`` - Create a gateway. Follow the instruction of the specific tab in order to integrate it. +- The Webhook User processes the incoming messages. Keep the default + user (OdooBot) or choose a user of the ``Gateway / User`` group. - Start receiving notifications Usage diff --git a/mail_gateway/models/mail_gateway.py b/mail_gateway/models/mail_gateway.py index 58e3f307a..453268f90 100644 --- a/mail_gateway/models/mail_gateway.py +++ b/mail_gateway/models/mail_gateway.py @@ -26,7 +26,10 @@ class MailGateway(models.Model): webhook_user_id = fields.Many2one( "res.users", default=lambda self: self.env.ref("base.user_root"), - help="User that will create the messages", + domain=lambda self: [ + ("all_group_ids", "in", self.env.ref("mail_gateway.gateway_user").ids) + ], + help="User that will create the messages. It must be a gateway user.", ) member_ids = fields.Many2many("res.users") company_id = fields.Many2one( diff --git a/mail_gateway/readme/CONFIGURE.md b/mail_gateway/readme/CONFIGURE.md index e3bd3066d..da8e374b6 100644 --- a/mail_gateway/readme/CONFIGURE.md +++ b/mail_gateway/readme/CONFIGURE.md @@ -2,4 +2,6 @@ - Go to `Settings / Technical / Email / Gateway` - Create a gateway. Follow the instruction of the specific tab in order to integrate it. +- The Webhook User processes the incoming messages. Keep the default + user (OdooBot) or choose a user of the `Gateway / User` group. - Start receiving notifications diff --git a/mail_gateway/static/description/index.html b/mail_gateway/static/description/index.html index e660582ca..8aef89dd1 100644 --- a/mail_gateway/static/description/index.html +++ b/mail_gateway/static/description/index.html @@ -405,6 +405,8 @@

Configuration

  • Go to Settings / Technical / Email / Gateway
  • Create a gateway. Follow the instruction of the specific tab in order to integrate it.
  • +
  • The Webhook User processes the incoming messages. Keep the default +user (OdooBot) or choose a user of the Gateway / User group.
  • Start receiving notifications
  • From 525af48dd89aa3aeba89b30b986738c859ff2c0f Mon Sep 17 00:00:00 2001 From: Olga Marco Date: Wed, 25 May 2022 10:07:34 +0200 Subject: [PATCH 03/41] [ADD] mail_broker_whatsapp --- mail_gateway_whatsapp/README.rst | 1 + mail_gateway_whatsapp/__init__.py | 1 + mail_gateway_whatsapp/__manifest__.py | 15 + mail_gateway_whatsapp/models/__init__.py | 3 + mail_gateway_whatsapp/models/mail_broker.py | 54 +++ .../models/mail_broker_channel.py | 78 +++ .../models/mail_message_broker.py | 78 +++ mail_gateway_whatsapp/readme/CONFIGURE.rst | 29 ++ mail_gateway_whatsapp/readme/CONTRIBUTORS.rst | 2 + mail_gateway_whatsapp/readme/DESCRIPTION.rst | 4 + mail_gateway_whatsapp/readme/USAGE.rst | 3 + .../static/description/icon.png | Bin 0 -> 7839 bytes .../static/description/icon.svg | 160 +++++++ .../static/description/index.html | 452 ++++++++++++++++++ mail_gateway_whatsapp/views/mail_broker.xml | 22 + 15 files changed, 902 insertions(+) create mode 100644 mail_gateway_whatsapp/README.rst create mode 100644 mail_gateway_whatsapp/__init__.py create mode 100644 mail_gateway_whatsapp/__manifest__.py create mode 100644 mail_gateway_whatsapp/models/__init__.py create mode 100644 mail_gateway_whatsapp/models/mail_broker.py create mode 100644 mail_gateway_whatsapp/models/mail_broker_channel.py create mode 100644 mail_gateway_whatsapp/models/mail_message_broker.py create mode 100644 mail_gateway_whatsapp/readme/CONFIGURE.rst create mode 100644 mail_gateway_whatsapp/readme/CONTRIBUTORS.rst create mode 100644 mail_gateway_whatsapp/readme/DESCRIPTION.rst create mode 100644 mail_gateway_whatsapp/readme/USAGE.rst create mode 100644 mail_gateway_whatsapp/static/description/icon.png create mode 100644 mail_gateway_whatsapp/static/description/icon.svg create mode 100644 mail_gateway_whatsapp/static/description/index.html create mode 100644 mail_gateway_whatsapp/views/mail_broker.xml diff --git a/mail_gateway_whatsapp/README.rst b/mail_gateway_whatsapp/README.rst new file mode 100644 index 000000000..d79d0756d --- /dev/null +++ b/mail_gateway_whatsapp/README.rst @@ -0,0 +1 @@ +TO DO diff --git a/mail_gateway_whatsapp/__init__.py b/mail_gateway_whatsapp/__init__.py new file mode 100644 index 000000000..0650744f6 --- /dev/null +++ b/mail_gateway_whatsapp/__init__.py @@ -0,0 +1 @@ +from . import models diff --git a/mail_gateway_whatsapp/__manifest__.py b/mail_gateway_whatsapp/__manifest__.py new file mode 100644 index 000000000..055960077 --- /dev/null +++ b/mail_gateway_whatsapp/__manifest__.py @@ -0,0 +1,15 @@ +# Copyright 2022 Creu Blanca +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +{ + "name": "Mail Whatsapp Broker", + "summary": """ + Set a broker for whatsapp""", + "version": "13.0.1.0.0", + "license": "AGPL-3", + "author": "Creu Blanca, Odoo Community Association (OCA)", + "website": "https://github.com/OCA/social", + "depends": ["mail_broker"], + "data": ["views/mail_broker.xml"], + "external_dependencies": {}, +} diff --git a/mail_gateway_whatsapp/models/__init__.py b/mail_gateway_whatsapp/models/__init__.py new file mode 100644 index 000000000..cf04d46c0 --- /dev/null +++ b/mail_gateway_whatsapp/models/__init__.py @@ -0,0 +1,3 @@ +from . import mail_broker_channel +from . import mail_message_broker +from . import mail_broker diff --git a/mail_gateway_whatsapp/models/mail_broker.py b/mail_gateway_whatsapp/models/mail_broker.py new file mode 100644 index 000000000..c73f29e8b --- /dev/null +++ b/mail_gateway_whatsapp/models/mail_broker.py @@ -0,0 +1,54 @@ +# Copyright 2022 Creu Blanca +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). +import logging + +from odoo import fields, models +from odoo.http import request + +_logger = logging + + +class MailBroker(models.Model): + _inherit = "mail.broker" + + whatsapp_security_key = fields.Char() + broker_type = fields.Selection(selection_add=[("whatsapp", "WhatsApp")]) + whatsapp_from_phone = fields.Char() + + def _set_webhook(self): + super(MailBroker, self)._set_webhook() + + def _remove_webhook(self): + super(MailBroker, self)._remove_webhook() + + def _get_channel_vals(self, token, update): + result = super(MailBroker, self)._get_channel_vals(token, update) + if self.broker_type == "whatsapp" and not result.get("name"): + for contact in update["contacts"]: + if contact["wa_id"] == token: + result["name"] = contact["profile"]["name"] + continue + return result + + def _receive_update_whatsapp(self, update): + chat = {} + if update: + for entry in update["entry"]: + for change in entry["changes"]: + if change["field"] != "messages": + continue + for message in change["value"].get("messages", []): + chat = self._get_channel( + message["from"], change["value"], force_create=True + ) + if not chat: + return + return chat.whatsapp_update(update) + + def _verify_bot(self, **kwargs): + self.ensure_one() + if self.broker_type != "whatsapp": + return super()._verify_bot() + response = request.make_response(kwargs.get("hub").get("challenge")) + response.status_code = 200 + return response diff --git a/mail_gateway_whatsapp/models/mail_broker_channel.py b/mail_gateway_whatsapp/models/mail_broker_channel.py new file mode 100644 index 000000000..fd31b58aa --- /dev/null +++ b/mail_gateway_whatsapp/models/mail_broker_channel.py @@ -0,0 +1,78 @@ +# Copyright 2022 Creu Blanca +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +import base64 +import mimetypes +from datetime import datetime + +import requests + +from odoo import models + + +class MailBrokerChannel(models.Model): + _inherit = "mail.broker.channel" + + def whatsapp_update(self, updates): + self.ensure_one() + body = "" + attachments = [] + for entry in updates["entry"]: + for change in entry["changes"]: + if change["field"] != "messages": + continue + for message in change["value"]["messages"]: + if message.get("text"): + body = message.get("text").get("body") + for key in ["image", "audio", "video"]: + if message.get(key): + image_id = message.get(key).get("id") + image_info_request = requests.get( + "https://graph.facebook.com/v13.0/%s" % image_id, + headers={ + "Authorization": "Bearer %s" % self.broker_id.token, + }, + ) + image_info_request.raise_for_status() + image_info = image_info_request.json() + image_request = requests.get( + image_info["url"], + headers={ + "Authorization": "Bearer %s" % self.broker_id.token, + }, + ) + image_request.raise_for_status() + attachments.append( + ( + "{}{}".format( + image_id, + mimetypes.guess_extension( + image_info["mime_type"] + ), + ), + base64.b64encode(image_request.content).decode( + "utf-8" + ), + image_info["mime_type"], + ) + ) + if message.get("location"): + body += ( + 'Location' + % ( + message["location"]["latitude"], + message["location"]["longitude"], + ) + ) + if message.get("contacts"): + pass + if len(body) > 0 or attachments: + self.message_post_broker( + body=body, + broker_type="whatsapp", + date=datetime.fromtimestamp(int(message["timestamp"])), + message_id=message.get("id"), + subtype="mt_comment", + attachments=attachments, + ) diff --git a/mail_gateway_whatsapp/models/mail_message_broker.py b/mail_gateway_whatsapp/models/mail_message_broker.py new file mode 100644 index 000000000..eeb132826 --- /dev/null +++ b/mail_gateway_whatsapp/models/mail_message_broker.py @@ -0,0 +1,78 @@ +# Copyright 2022 Creu Blanca +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). +import logging +import traceback +from io import StringIO + +import requests + +from odoo import _, models +from odoo.tools import html2plaintext + +from odoo.addons.base.models.ir_mail_server import MailDeliveryException + +_logger = logging.getLogger(__name__) + + +class MailMessageBroker(models.Model): + _inherit = "mail.message.broker" + + def _send_whatsapp_payload(self, body=False, media_id=False): + if body: + return { + "messaging_product": "whatsapp", + "recipient_type": "individual", + "to": self.channel_id.token, + "type": "text", + "text": {"preview_url": False, "body": html2plaintext(body)}, + } + if media_id: + return { + "messaging_product": "whatsapp", + "recipient_type": "individual", + "to": self.channel_id.token, + "type": "image", + "image": {"id": media_id}, + } + + def _send_whatsapp( + self, auto_commit=False, raise_exception=False, parse_mode=False + ): + message = False + try: + # TODO: Now only works for text. improve it... + if self.body: + response = requests.post( + "https://graph.facebook.com/v13.0/%s/messages" + % self.channel_id.broker_id.whatsapp_from_phone, + headers={ + "Authorization": "Bearer %s" % self.channel_id.broker_id.token, + }, + json=self._send_whatsapp_payload(body=self.body), + ) + response.raise_for_status() + message = response.json() + except Exception as exc: + buff = StringIO() + traceback.print_exc(file=buff) + _logger.error(buff.getvalue()) + if raise_exception: + raise MailDeliveryException( + _("Unable to send the whatsapp message"), exc + ) + else: + _logger.warning( + "Issue sending message with id {}: {}".format(self.id, exc) + ) + self.write({"state": "exception", "failure_reason": exc}) + if message: + self.write( + { + "state": "sent", + "message_id": message["messages"][0]["id"], + "failure_reason": False, + } + ) + if auto_commit is True: + # pylint: disable=invalid-commit + self._cr.commit() diff --git a/mail_gateway_whatsapp/readme/CONFIGURE.rst b/mail_gateway_whatsapp/readme/CONFIGURE.rst new file mode 100644 index 000000000..f23168d69 --- /dev/null +++ b/mail_gateway_whatsapp/readme/CONFIGURE.rst @@ -0,0 +1,29 @@ +First steps +~~~~~~~~~~~ + +You need to create a WhatsApp Business Account (WABA), a Meta App and define a phone number. +You can follow this `steps `_. + +If you create a test Business Account, passwords will change every 24 hours. + +In order to make the webhook accessible, the system must be public. + +Configure the broker +~~~~~~~~~~~~~~~~~~~~ + +Once you have created the Meta App, you need to add the broker and webhook. +In order to make it you must follow this steps: + +* Access `Settings > Emails > Mail Broker` +* Create a Broker of type `WhatsApp` + + * Use the Meta App authentication key as `Token` field + * Use the Meta App Phone Number ID as `Whatsapp from Phone` field + * Write your own `Webhook key` and `Whatsapp Security Key` + * Press the `Integrate Webhook Key`. In this case, it will not integrate it, we need to make it manually + * Copy the webhook URL + +* Access `Facebook Apps website `_ +* Access your App then `Whatsapp > Configuration` +* Create your webhook using your URL and put the Whatsapp Security Key as validation Key +* Administer the Webhook and activate the messages webhook diff --git a/mail_gateway_whatsapp/readme/CONTRIBUTORS.rst b/mail_gateway_whatsapp/readme/CONTRIBUTORS.rst new file mode 100644 index 000000000..cd3b2e447 --- /dev/null +++ b/mail_gateway_whatsapp/readme/CONTRIBUTORS.rst @@ -0,0 +1,2 @@ +* Olga Marco +* Enric Tobella diff --git a/mail_gateway_whatsapp/readme/DESCRIPTION.rst b/mail_gateway_whatsapp/readme/DESCRIPTION.rst new file mode 100644 index 000000000..b8b23c141 --- /dev/null +++ b/mail_gateway_whatsapp/readme/DESCRIPTION.rst @@ -0,0 +1,4 @@ +This module allows to respond whatsapp chats. + +This way, a group of users can respond customers or any other set +of partners in an integrated way. diff --git a/mail_gateway_whatsapp/readme/USAGE.rst b/mail_gateway_whatsapp/readme/USAGE.rst new file mode 100644 index 000000000..fa884cb73 --- /dev/null +++ b/mail_gateway_whatsapp/readme/USAGE.rst @@ -0,0 +1,3 @@ +1. Access `Broker` +2. Wait until someone starts a conversation. +3. Now you will be able to respond and receive messages to this person. diff --git a/mail_gateway_whatsapp/static/description/icon.png b/mail_gateway_whatsapp/static/description/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..dea8a38eceaafc76407fba56709f00ca5ae4483e GIT binary patch literal 7839 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 literal 0 HcmV?d00001 diff --git a/mail_gateway_whatsapp/static/description/icon.svg b/mail_gateway_whatsapp/static/description/icon.svg new file mode 100644 index 000000000..fe2e6d4f1 --- /dev/null +++ b/mail_gateway_whatsapp/static/description/icon.svg @@ -0,0 +1,160 @@ + + + + + + image/svg+xml + + icon + + + + + + + + + + + + + + + + + icon + + + + + + + + + + diff --git a/mail_gateway_whatsapp/static/description/index.html b/mail_gateway_whatsapp/static/description/index.html new file mode 100644 index 000000000..745179b53 --- /dev/null +++ b/mail_gateway_whatsapp/static/description/index.html @@ -0,0 +1,452 @@ + + + + + + +Mail Telegram Broker + + + +
    +

    Mail Telegram Broker

    + + +

    Beta License: AGPL-3 tegin/cb-addons

    +

    This module allows to respond telegram chats as a telegram bot.

    +

    This way, a group of users can respond customers or any other set +of partners in an integrated way.

    +

    It is not intended to be integrated on default chatter as users don’t need +to review again when one has responded.

    +

    Table of contents

    + +
    +

    Configuration

    +
    +

    Create the bot

    +
      +
    1. Create a Bot on telegram https://core.telegram.org/bots
    2. +
    3. Create a broker following the examples on +https://github.com/tegin/telegram-broker with the TOKEN provided
    4. +
    +
    +
    +

    Configure Odoo

    +
      +
    1. Access on debug mode
    2. +
    3. Access Settings > Technical Settings > Email > Telegram Bot.
    4. +
    5. Create a bot and assign the token. Mark it as Show on App
    6. +
    +
    +
    +
    +

    Usage

    +
      +
    1. Access Telegram
    2. +
    3. Wait until someone starts a conversation with your bot.
    4. +
    5. Now you will be able to respond and receive messages to this person.
    6. +
    +
    +
    +

    Bug Tracker

    +

    Bugs are tracked on GitHub Issues. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us smashing it by providing a detailed and welcomed +feedback.

    +

    Do not contact contributors directly about support or help with technical issues.

    +
    +
    +

    Credits

    +
    +

    Authors

    +
      +
    • Creu Blanca
    • +
    +
    +
    +

    Contributors

    + +
    +
    +

    Maintainers

    +

    This module is part of the tegin/cb-addons project on GitHub.

    +

    You are welcome to contribute.

    +
    +
    +
    + + diff --git a/mail_gateway_whatsapp/views/mail_broker.xml b/mail_gateway_whatsapp/views/mail_broker.xml new file mode 100644 index 000000000..8fc6d9275 --- /dev/null +++ b/mail_gateway_whatsapp/views/mail_broker.xml @@ -0,0 +1,22 @@ + + + + + mail.broker.form (in mail_broker_telegram) + mail.broker + + + + + + + + + From ce3517d1bb3290580217978cef50b55b2674b98f Mon Sep 17 00:00:00 2001 From: Enric Tobella Date: Mon, 11 Jul 2022 16:56:30 +0200 Subject: [PATCH 04/41] [IMP] mail_broker_whatsapp: Improve phone widget Allow to send documents, images and so on add mark on message Improve security Pass logic to components Add tests Manage change of version of Facebook API --- mail_gateway_whatsapp/__init__.py | 2 + mail_gateway_whatsapp/__manifest__.py | 11 +- mail_gateway_whatsapp/models/__init__.py | 3 +- mail_gateway_whatsapp/models/mail_broker.py | 43 +-- .../models/mail_broker_channel.py | 78 ----- .../models/mail_message_broker.py | 78 ----- mail_gateway_whatsapp/models/mail_thread.py | 48 +++ mail_gateway_whatsapp/readme/CONFIGURE.rst | 3 +- mail_gateway_whatsapp/services/__init__.py | 1 + .../services/mail_broker_service.py | 293 ++++++++++++++++++ .../static/src/js/phone_widget.js | 63 ++++ .../static/src/xml/thread.xml | 11 + mail_gateway_whatsapp/templates/assets.xml | 21 ++ mail_gateway_whatsapp/tests/__init__.py | 1 + .../tests/test_mail_broker_whatsapp.py | 197 ++++++++++++ mail_gateway_whatsapp/views/mail_broker.xml | 4 + mail_gateway_whatsapp/wizards/__init__.py | 1 + .../wizards/whatsapp_composer.py | 57 ++++ .../wizards/whatsapp_composer.xml | 50 +++ 19 files changed, 761 insertions(+), 204 deletions(-) delete mode 100644 mail_gateway_whatsapp/models/mail_broker_channel.py delete mode 100644 mail_gateway_whatsapp/models/mail_message_broker.py create mode 100644 mail_gateway_whatsapp/models/mail_thread.py create mode 100644 mail_gateway_whatsapp/services/__init__.py create mode 100644 mail_gateway_whatsapp/services/mail_broker_service.py create mode 100644 mail_gateway_whatsapp/static/src/js/phone_widget.js create mode 100644 mail_gateway_whatsapp/static/src/xml/thread.xml create mode 100644 mail_gateway_whatsapp/templates/assets.xml create mode 100644 mail_gateway_whatsapp/tests/__init__.py create mode 100644 mail_gateway_whatsapp/tests/test_mail_broker_whatsapp.py create mode 100644 mail_gateway_whatsapp/wizards/__init__.py create mode 100644 mail_gateway_whatsapp/wizards/whatsapp_composer.py create mode 100644 mail_gateway_whatsapp/wizards/whatsapp_composer.xml diff --git a/mail_gateway_whatsapp/__init__.py b/mail_gateway_whatsapp/__init__.py index 0650744f6..903154406 100644 --- a/mail_gateway_whatsapp/__init__.py +++ b/mail_gateway_whatsapp/__init__.py @@ -1 +1,3 @@ from . import models +from . import services +from . import wizards diff --git a/mail_gateway_whatsapp/__manifest__.py b/mail_gateway_whatsapp/__manifest__.py index 055960077..41f686dcf 100644 --- a/mail_gateway_whatsapp/__manifest__.py +++ b/mail_gateway_whatsapp/__manifest__.py @@ -9,7 +9,12 @@ "license": "AGPL-3", "author": "Creu Blanca, Odoo Community Association (OCA)", "website": "https://github.com/OCA/social", - "depends": ["mail_broker"], - "data": ["views/mail_broker.xml"], - "external_dependencies": {}, + "depends": ["mail_broker", "phone_validation"], + "external_dependencies": {"python": ["requests_toolbelt"]}, + "data": [ + "wizards/whatsapp_composer.xml", + "views/mail_broker.xml", + "templates/assets.xml", + ], + "qweb": ["static/src/xml/thread.xml"], } diff --git a/mail_gateway_whatsapp/models/__init__.py b/mail_gateway_whatsapp/models/__init__.py index cf04d46c0..2f4ddbb09 100644 --- a/mail_gateway_whatsapp/models/__init__.py +++ b/mail_gateway_whatsapp/models/__init__.py @@ -1,3 +1,2 @@ -from . import mail_broker_channel -from . import mail_message_broker from . import mail_broker +from . import mail_thread diff --git a/mail_gateway_whatsapp/models/mail_broker.py b/mail_gateway_whatsapp/models/mail_broker.py index c73f29e8b..107b2f00b 100644 --- a/mail_gateway_whatsapp/models/mail_broker.py +++ b/mail_gateway_whatsapp/models/mail_broker.py @@ -1,11 +1,7 @@ # Copyright 2022 Creu Blanca # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). -import logging from odoo import fields, models -from odoo.http import request - -_logger = logging class MailBroker(models.Model): @@ -14,41 +10,4 @@ class MailBroker(models.Model): whatsapp_security_key = fields.Char() broker_type = fields.Selection(selection_add=[("whatsapp", "WhatsApp")]) whatsapp_from_phone = fields.Char() - - def _set_webhook(self): - super(MailBroker, self)._set_webhook() - - def _remove_webhook(self): - super(MailBroker, self)._remove_webhook() - - def _get_channel_vals(self, token, update): - result = super(MailBroker, self)._get_channel_vals(token, update) - if self.broker_type == "whatsapp" and not result.get("name"): - for contact in update["contacts"]: - if contact["wa_id"] == token: - result["name"] = contact["profile"]["name"] - continue - return result - - def _receive_update_whatsapp(self, update): - chat = {} - if update: - for entry in update["entry"]: - for change in entry["changes"]: - if change["field"] != "messages": - continue - for message in change["value"].get("messages", []): - chat = self._get_channel( - message["from"], change["value"], force_create=True - ) - if not chat: - return - return chat.whatsapp_update(update) - - def _verify_bot(self, **kwargs): - self.ensure_one() - if self.broker_type != "whatsapp": - return super()._verify_bot() - response = request.make_response(kwargs.get("hub").get("challenge")) - response.status_code = 200 - return response + whatsapp_version = fields.Char(default="15.0") diff --git a/mail_gateway_whatsapp/models/mail_broker_channel.py b/mail_gateway_whatsapp/models/mail_broker_channel.py deleted file mode 100644 index fd31b58aa..000000000 --- a/mail_gateway_whatsapp/models/mail_broker_channel.py +++ /dev/null @@ -1,78 +0,0 @@ -# Copyright 2022 Creu Blanca -# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). - -import base64 -import mimetypes -from datetime import datetime - -import requests - -from odoo import models - - -class MailBrokerChannel(models.Model): - _inherit = "mail.broker.channel" - - def whatsapp_update(self, updates): - self.ensure_one() - body = "" - attachments = [] - for entry in updates["entry"]: - for change in entry["changes"]: - if change["field"] != "messages": - continue - for message in change["value"]["messages"]: - if message.get("text"): - body = message.get("text").get("body") - for key in ["image", "audio", "video"]: - if message.get(key): - image_id = message.get(key).get("id") - image_info_request = requests.get( - "https://graph.facebook.com/v13.0/%s" % image_id, - headers={ - "Authorization": "Bearer %s" % self.broker_id.token, - }, - ) - image_info_request.raise_for_status() - image_info = image_info_request.json() - image_request = requests.get( - image_info["url"], - headers={ - "Authorization": "Bearer %s" % self.broker_id.token, - }, - ) - image_request.raise_for_status() - attachments.append( - ( - "{}{}".format( - image_id, - mimetypes.guess_extension( - image_info["mime_type"] - ), - ), - base64.b64encode(image_request.content).decode( - "utf-8" - ), - image_info["mime_type"], - ) - ) - if message.get("location"): - body += ( - 'Location' - % ( - message["location"]["latitude"], - message["location"]["longitude"], - ) - ) - if message.get("contacts"): - pass - if len(body) > 0 or attachments: - self.message_post_broker( - body=body, - broker_type="whatsapp", - date=datetime.fromtimestamp(int(message["timestamp"])), - message_id=message.get("id"), - subtype="mt_comment", - attachments=attachments, - ) diff --git a/mail_gateway_whatsapp/models/mail_message_broker.py b/mail_gateway_whatsapp/models/mail_message_broker.py deleted file mode 100644 index eeb132826..000000000 --- a/mail_gateway_whatsapp/models/mail_message_broker.py +++ /dev/null @@ -1,78 +0,0 @@ -# Copyright 2022 Creu Blanca -# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). -import logging -import traceback -from io import StringIO - -import requests - -from odoo import _, models -from odoo.tools import html2plaintext - -from odoo.addons.base.models.ir_mail_server import MailDeliveryException - -_logger = logging.getLogger(__name__) - - -class MailMessageBroker(models.Model): - _inherit = "mail.message.broker" - - def _send_whatsapp_payload(self, body=False, media_id=False): - if body: - return { - "messaging_product": "whatsapp", - "recipient_type": "individual", - "to": self.channel_id.token, - "type": "text", - "text": {"preview_url": False, "body": html2plaintext(body)}, - } - if media_id: - return { - "messaging_product": "whatsapp", - "recipient_type": "individual", - "to": self.channel_id.token, - "type": "image", - "image": {"id": media_id}, - } - - def _send_whatsapp( - self, auto_commit=False, raise_exception=False, parse_mode=False - ): - message = False - try: - # TODO: Now only works for text. improve it... - if self.body: - response = requests.post( - "https://graph.facebook.com/v13.0/%s/messages" - % self.channel_id.broker_id.whatsapp_from_phone, - headers={ - "Authorization": "Bearer %s" % self.channel_id.broker_id.token, - }, - json=self._send_whatsapp_payload(body=self.body), - ) - response.raise_for_status() - message = response.json() - except Exception as exc: - buff = StringIO() - traceback.print_exc(file=buff) - _logger.error(buff.getvalue()) - if raise_exception: - raise MailDeliveryException( - _("Unable to send the whatsapp message"), exc - ) - else: - _logger.warning( - "Issue sending message with id {}: {}".format(self.id, exc) - ) - self.write({"state": "exception", "failure_reason": exc}) - if message: - self.write( - { - "state": "sent", - "message_id": message["messages"][0]["id"], - "failure_reason": False, - } - ) - if auto_commit is True: - # pylint: disable=invalid-commit - self._cr.commit() diff --git a/mail_gateway_whatsapp/models/mail_thread.py b/mail_gateway_whatsapp/models/mail_thread.py new file mode 100644 index 000000000..10e56b560 --- /dev/null +++ b/mail_gateway_whatsapp/models/mail_thread.py @@ -0,0 +1,48 @@ +# Copyright 2022 CreuBlanca +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from odoo import _, models +from odoo.exceptions import UserError + +from odoo.addons.phone_validation.tools import phone_validation + + +class MailThread(models.AbstractModel): + + _inherit = "mail.thread" + + def _get_whatsapp_channel_vals(self, token, broker, partner): + result = { + "token": token, + "broker_id": broker.id, + "show_on_app": broker.show_on_app, + } + if partner: + result["partner_id"] = partner.id + result["name"] = partner.display_name + return result + + def _whatsapp_get_channel(self, field_name, broker): + phone = self[field_name] + sanitize_res = phone_validation.phone_sanitize_numbers_w_record([phone], self) + sanitized_number = sanitize_res[phone].get("sanitized") + if not sanitized_number: + raise UserError(_("Phone cannot be sanitized")) + sanitized_number = sanitized_number[1:] + channel = broker._get_channel_id(sanitized_number) + partner = self._whastapp_get_partner() + if not channel: + channel = self.env["mail.broker.channel"].create( + self._get_whatsapp_channel_vals(sanitized_number, broker, partner) + ) + else: + channel = self.env["mail.broker.channel"].browse(channel) + if not channel.partner_id and partner: + channel.partner_id = partner + return channel + + def _whastapp_get_partner(self): + if self._name == "res.partner": + return self + if "partner_id" in self._fields: + return self.partner_id diff --git a/mail_gateway_whatsapp/readme/CONFIGURE.rst b/mail_gateway_whatsapp/readme/CONFIGURE.rst index f23168d69..4a49950cd 100644 --- a/mail_gateway_whatsapp/readme/CONFIGURE.rst +++ b/mail_gateway_whatsapp/readme/CONFIGURE.rst @@ -19,7 +19,8 @@ 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 - * Write your own `Webhook key` and `Whatsapp Security Key` + * 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 * Copy the webhook URL diff --git a/mail_gateway_whatsapp/services/__init__.py b/mail_gateway_whatsapp/services/__init__.py new file mode 100644 index 000000000..08034a842 --- /dev/null +++ b/mail_gateway_whatsapp/services/__init__.py @@ -0,0 +1 @@ +from . import mail_broker_service diff --git a/mail_gateway_whatsapp/services/mail_broker_service.py b/mail_gateway_whatsapp/services/mail_broker_service.py new file mode 100644 index 000000000..57cd6d7f5 --- /dev/null +++ b/mail_gateway_whatsapp/services/mail_broker_service.py @@ -0,0 +1,293 @@ +# Copyright 2018 ACSONE SA/NV +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). +import base64 +import hashlib +import hmac +import logging +import mimetypes +import traceback +from datetime import datetime +from io import StringIO + +import requests +import requests_toolbelt + +from odoo import _ +from odoo.exceptions import UserError +from odoo.http import request +from odoo.tools import html2plaintext + +from odoo.addons.base.models.ir_mail_server import MailDeliveryException +from odoo.addons.base_rest import restapi +from odoo.addons.component.core import Component +from odoo.addons.mail_broker.services.mail_broker_service import BrokerMethodParams + +_logger = logging.getLogger(__name__) + + +class MailBrokerWhatsappService(Component): + _inherit = "mail.broker.base.service" + _name = "mail.broker.whastapp.service" + _usage = "whatsapp" + _description = "Whatsapp Broker services" + + @restapi.method( + [(["//update"], "GET")], + input_param=BrokerMethodParams(), + auth="none", + ) + def get_update(self, token, **kwargs): + """Verification of the service from an external service""" + bot_data = self.env["mail.broker"]._get_broker( + token, state="pending", broker_type=self._usage, **kwargs + ) + if not bot_data: + return None + self.collection.env = self.env(user=bot_data["webhook_user_id"]) + broker = self.env["mail.broker"].browse(bot_data["id"]) + if kwargs.get("hub").get("verify_token") != broker.whatsapp_security_key: + return None + broker.sudo().integrated_webhook_state = "integrated" + response = request.make_response(kwargs.get("hub").get("challenge")) + response.status_code = 200 + return response + + def _set_webhook(self): + self.collection.integrated_webhook_state = "pending" + + def _verify_update(self, bot_data, kwargs): + signature = request.httprequest.headers.get("x-hub-signature-256") + if not signature: + return False + if ( + "sha256=%s" + % hmac.new( + bot_data["webhook_secret"].encode(), + request.httprequest.data, + hashlib.sha256, + ).hexdigest() + != signature + ): + return False + return True + + def _receive_update(self, broker, update): + if update: + for entry in update["entry"]: + for change in entry["changes"]: + if change["field"] != "messages": + continue + for message in change["value"].get("messages", []): + chat = self._get_channel( + broker, message["from"], change["value"], force_create=True + ) + if not chat: + return + return self._process_update(chat, update) + + def _get_channel_vals(self, broker, token, update): + result = super()._get_channel_vals(broker, token, update) + for contact in update.get("contacts"): + if contact["wa_id"] == token: + result["name"] = contact["profile"]["name"] + continue + return result + + def _process_update(self, chat, updates): + chat.ensure_one() + body = "" + attachments = [] + for entry in updates["entry"]: + for change in entry["changes"]: + if change["field"] != "messages": + continue + for message in change["value"]["messages"]: + if message.get("text"): + body = message.get("text").get("body") + for key in ["image", "audio", "video"]: + if message.get(key): + image_id = message.get(key).get("id") + if image_id: + image_info_request = requests.get( + "https://graph.facebook.com/v13.0/%s" % image_id, + headers={ + "Authorization": "Bearer %s" + % self.broker_id.token, + }, + ) + image_info_request.raise_for_status() + image_info = image_info_request.json() + image_url = image_info["url"] + else: + image_url = message.get(key).get("url") + if not image_url: + continue + image_request = requests.get( + image_url, + headers={ + "Authorization": "Bearer %s" % self.broker_id.token, + }, + ) + image_request.raise_for_status() + attachments.append( + ( + "{}{}".format( + image_id, + mimetypes.guess_extension( + image_info["mime_type"] + ), + ), + base64.b64encode(image_request.content).decode( + "utf-8" + ), + image_info["mime_type"], + ) + ) + if message.get("location"): + body += ( + 'Location' + % ( + message["location"]["latitude"], + message["location"]["longitude"], + ) + ) + if message.get("contacts"): + pass + if len(body) > 0 or attachments: + chat.message_post_broker( + body=body, + broker_type=self._usage, + date=datetime.fromtimestamp(int(message["timestamp"])), + message_id=message.get("id"), + subtype="mt_comment", + attachments=attachments, + ) + + def _send_payload( + self, channel, body=False, media_id=False, media_type=False, media_name=False + ): + if body: + return { + "messaging_product": "whatsapp", + "recipient_type": "individual", + "to": channel.token, + "type": "text", + "text": {"preview_url": False, "body": html2plaintext(body)}, + } + if media_id: + media_data = {"id": media_id} + if media_type == "document": + media_data["filename"] = media_name + return { + "messaging_product": "whatsapp", + "recipient_type": "individual", + "to": channel.token, + "type": media_type, + media_type: media_data, + } + + def _get_whatsapp_mimetype_kind(self): + return { + "text/plain": "document", + "application/pdf": "document", + "application/vnd.ms-powerpoint": "document", + "application/msword": "document", + "application/vnd.ms-excel": "document", + "application/vnd.openxmlformats-officedocument." + "wordprocessingml.document": "document", + "application/vnd.openxmlformats-officedocument." + "presentationml.presentation": "document", + "application/vnd.openxmlformats-officedocument." + "spreadsheetml.sheet": "document", + "audio/aac": "audio", + "audio/mp4": "audio", + "audio/mpeg": "audio", + "audio/amr": "audio", + "audio/ogg": "audio", + "image/jpeg": "image", + "image/png": "image", + "video/mp4": "video", + "video/3gp": "video", + "image/webp": "sticker", + } + + def _send(self, record, auto_commit=False, raise_exception=False, parse_mode=False): + message = False + try: + if record.body: + response = requests.post( + "https://graph.facebook.com/v%s/%s/messages" + % ( + self.collection.whatsapp_version, + self.collection.whatsapp_from_phone, + ), + headers={"Authorization": "Bearer %s" % self.collection.token}, + json=self._send_payload(record.channel_id, body=record.body), + ) + response.raise_for_status() + message = response.json() + attachment_mimetype_map = self._get_whatsapp_mimetype_kind() + for attachment in record.attachment_ids: + if attachment.mimetype not in attachment_mimetype_map: + raise UserError(_("Mimetype is not valid")) + attachment_type = attachment_mimetype_map[attachment.mimetype] + m = requests_toolbelt.multipart.encoder.MultipartEncoder( + fields={ + "file": ( + attachment.name, + base64.b64decode(attachment.datas), + attachment.mimetype, + ), + "messaging_product": "whatsapp", + # "type": attachment_type + }, + ) + + response = requests.post( + "https://graph.facebook.com/v13.0/%s/media" + % self.collection.whatsapp_from_phone, + headers={ + "Authorization": "Bearer %s" % self.collection.token, + "content-type": m.content_type, + }, + data=m, + ) + response.raise_for_status() + response = requests.post( + "https://graph.facebook.com/v13.0/%s/messages" + % self.collection.whatsapp_from_phone, + headers={"Authorization": "Bearer %s" % self.collection.token}, + json=self._send_payload( + record.channel_id, + media_id=response.json()["id"], + media_type=attachment_type, + media_name=attachment.name, + ), + ) + response.raise_for_status() + message = response.json() + except Exception as exc: + buff = StringIO() + traceback.print_exc(file=buff) + _logger.error(buff.getvalue()) + if raise_exception: + raise MailDeliveryException( + _("Unable to send the whatsapp message"), exc + ) + else: + _logger.warning( + "Issue sending message with id {}: {}".format(record.id, exc) + ) + record.write({"state": "exception", "failure_reason": exc}) + if message: + record.write( + { + "state": "sent", + "message_id": message["messages"][0]["id"], + "failure_reason": False, + } + ) + if auto_commit is True: + # pylint: disable=invalid-commit + self.env.cr.commit() diff --git a/mail_gateway_whatsapp/static/src/js/phone_widget.js b/mail_gateway_whatsapp/static/src/js/phone_widget.js new file mode 100644 index 000000000..df8f5681f --- /dev/null +++ b/mail_gateway_whatsapp/static/src/js/phone_widget.js @@ -0,0 +1,63 @@ +/** ******************************************************************************** + Copyright 2022 Creu Blanca + License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). + **********************************************************************************/ + +odoo.define("mail_broker_whatsapp.phone_widget", function(require) { + "use strict"; + var basic_fields = require("web.basic_fields"); + var core = require("web.core"); + var session = require("web.session"); + + var _t = core._t; + basic_fields.FieldPhone.include({ + /** + * Add a button to call the composer wizard + * + * @override + * @private + */ + _renderReadonly: function() { + var def = this._super.apply(this, arguments); + if (this.nodeOptions.enable_sms) { + var $composerButton = $("", { + title: _t("Send Whatsapp Message"), + href: "", + class: "ml-3 d-inline-flex align-items-center o_field_phone_sms", + }); + $composerButton.prepend($("", {class: "fa fa-whatsapp"})); + $composerButton.on("click", this._onClickWhatsapp.bind(this)); + this.$el.append($composerButton); + } + + return def; + }, + _onClickWhatsapp: function(ev) { + ev.preventDefault(); + + var context = session.user_context; + context = _.extend({}, context, { + default_res_model: this.model, + default_res_id: parseInt(this.res_id, 10), + default_number_field_name: this.name, + default_composition_mode: "comment", + }); + var self = this; + return this.do_action( + { + title: _t("Send Whatsapp Message"), + type: "ir.actions.act_window", + res_model: "whatsapp.composer", + target: "new", + views: [[false, "form"]], + context: context, + }, + { + on_close: function() { + self.trigger_up("reload"); + }, + } + ); + }, + }); +}); diff --git a/mail_gateway_whatsapp/static/src/xml/thread.xml b/mail_gateway_whatsapp/static/src/xml/thread.xml new file mode 100644 index 000000000..f4a85ee56 --- /dev/null +++ b/mail_gateway_whatsapp/static/src/xml/thread.xml @@ -0,0 +1,11 @@ + + + + + + + + diff --git a/mail_gateway_whatsapp/templates/assets.xml b/mail_gateway_whatsapp/templates/assets.xml new file mode 100644 index 000000000..3043238aa --- /dev/null +++ b/mail_gateway_whatsapp/templates/assets.xml @@ -0,0 +1,21 @@ + + + +