odoo-addon-mail-gateway-whatsapp 16.0.1.0.0.2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- odoo/addons/mail_gateway_whatsapp/README.rst +127 -0
- odoo/addons/mail_gateway_whatsapp/__init__.py +4 -0
- odoo/addons/mail_gateway_whatsapp/__manifest__.py +28 -0
- odoo/addons/mail_gateway_whatsapp/i18n/mail_gateway_whatsapp.pot +537 -0
- odoo/addons/mail_gateway_whatsapp/models/__init__.py +5 -0
- odoo/addons/mail_gateway_whatsapp/models/mail_channel.py +25 -0
- odoo/addons/mail_gateway_whatsapp/models/mail_gateway.py +15 -0
- odoo/addons/mail_gateway_whatsapp/models/mail_gateway_whatsapp.py +383 -0
- odoo/addons/mail_gateway_whatsapp/models/mail_thread.py +65 -0
- odoo/addons/mail_gateway_whatsapp/models/res_partner.py +20 -0
- odoo/addons/mail_gateway_whatsapp/readme/CONFIGURE.rst +30 -0
- odoo/addons/mail_gateway_whatsapp/readme/CONTRIBUTORS.rst +2 -0
- odoo/addons/mail_gateway_whatsapp/readme/CREDITS.rst +1 -0
- odoo/addons/mail_gateway_whatsapp/readme/DESCRIPTION.rst +4 -0
- odoo/addons/mail_gateway_whatsapp/readme/USAGE.rst +3 -0
- odoo/addons/mail_gateway_whatsapp/security/ir.model.access.csv +2 -0
- odoo/addons/mail_gateway_whatsapp/static/description/icon.png +0 -0
- odoo/addons/mail_gateway_whatsapp/static/description/icon.svg +48 -0
- odoo/addons/mail_gateway_whatsapp/static/description/index.html +478 -0
- odoo/addons/mail_gateway_whatsapp/static/src/components/message/message.xml +11 -0
- odoo/addons/mail_gateway_whatsapp/static/src/components/phone_field/phone_field.esm.js +25 -0
- odoo/addons/mail_gateway_whatsapp/static/src/components/phone_field/phone_field.xml +20 -0
- odoo/addons/mail_gateway_whatsapp/static/src/components/send_whatsapp_button/send_whatsapp_button.esm.js +44 -0
- odoo/addons/mail_gateway_whatsapp/static/src/components/send_whatsapp_button/send_whatsapp_button.xml +13 -0
- odoo/addons/mail_gateway_whatsapp/static/src/models/message.esm.js +21 -0
- odoo/addons/mail_gateway_whatsapp/static/src/models/message_view.esm.js +25 -0
- odoo/addons/mail_gateway_whatsapp/static/src/models/notification.esm.js +25 -0
- odoo/addons/mail_gateway_whatsapp/tests/__init__.py +1 -0
- odoo/addons/mail_gateway_whatsapp/tests/test_mail_gateway_whatsapp.py +308 -0
- odoo/addons/mail_gateway_whatsapp/views/mail_gateway.xml +103 -0
- odoo/addons/mail_gateway_whatsapp/wizards/__init__.py +1 -0
- odoo/addons/mail_gateway_whatsapp/wizards/whatsapp_composer.py +59 -0
- odoo/addons/mail_gateway_whatsapp/wizards/whatsapp_composer.xml +50 -0
- odoo_addon_mail_gateway_whatsapp-16.0.1.0.0.2.dist-info/METADATA +147 -0
- odoo_addon_mail_gateway_whatsapp-16.0.1.0.0.2.dist-info/RECORD +37 -0
- odoo_addon_mail_gateway_whatsapp-16.0.1.0.0.2.dist-info/WHEEL +5 -0
- odoo_addon_mail_gateway_whatsapp-16.0.1.0.0.2.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# Copyright 2024 Dixmit
|
|
2
|
+
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
|
3
|
+
|
|
4
|
+
from odoo import models
|
|
5
|
+
from odoo.modules.module import get_resource_path
|
|
6
|
+
|
|
7
|
+
from odoo.addons.base.models.avatar_mixin import get_hsl_from_seed
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class MailChannel(models.Model):
|
|
11
|
+
|
|
12
|
+
_inherit = "mail.channel"
|
|
13
|
+
|
|
14
|
+
def _generate_avatar_gateway(self):
|
|
15
|
+
if self.gateway_id.gateway_type == "whatsapp":
|
|
16
|
+
path = get_resource_path(
|
|
17
|
+
"mail_gateway_whatsapp", "static/description", "icon.svg"
|
|
18
|
+
)
|
|
19
|
+
with open(path, "r") as f:
|
|
20
|
+
avatar = f.read()
|
|
21
|
+
|
|
22
|
+
bgcolor = get_hsl_from_seed(self.uuid)
|
|
23
|
+
avatar = avatar.replace("fill:#875a7b", f"fill:{bgcolor}")
|
|
24
|
+
return avatar
|
|
25
|
+
return super()._generate_avatar_gateway()
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# Copyright 2022 Creu Blanca
|
|
2
|
+
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
|
3
|
+
|
|
4
|
+
from odoo import fields, models
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class MailGateway(models.Model):
|
|
8
|
+
_inherit = "mail.gateway"
|
|
9
|
+
|
|
10
|
+
whatsapp_security_key = fields.Char()
|
|
11
|
+
gateway_type = fields.Selection(
|
|
12
|
+
selection_add=[("whatsapp", "WhatsApp")], ondelete={"whatsapp": "cascade"}
|
|
13
|
+
)
|
|
14
|
+
whatsapp_from_phone = fields.Char()
|
|
15
|
+
whatsapp_version = fields.Char(default="15.0")
|
|
@@ -0,0 +1,383 @@
|
|
|
1
|
+
# Copyright 2024 Dixmit
|
|
2
|
+
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
|
3
|
+
import hashlib
|
|
4
|
+
import hmac
|
|
5
|
+
import logging
|
|
6
|
+
import mimetypes
|
|
7
|
+
import traceback
|
|
8
|
+
from datetime import datetime
|
|
9
|
+
from io import StringIO
|
|
10
|
+
|
|
11
|
+
import requests
|
|
12
|
+
import requests_toolbelt
|
|
13
|
+
|
|
14
|
+
from odoo import _, models
|
|
15
|
+
from odoo.exceptions import UserError
|
|
16
|
+
from odoo.http import request
|
|
17
|
+
from odoo.tools import html2plaintext
|
|
18
|
+
|
|
19
|
+
from odoo.addons.base.models.ir_mail_server import MailDeliveryException
|
|
20
|
+
|
|
21
|
+
_logger = logging.getLogger(__name__)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class MailGatewayWhatsappService(models.AbstractModel):
|
|
25
|
+
_inherit = "mail.gateway.abstract"
|
|
26
|
+
_name = "mail.gateway.whatsapp"
|
|
27
|
+
_description = "Whatsapp Gateway services"
|
|
28
|
+
|
|
29
|
+
def _receive_get_update(self, bot_data, req, **kwargs):
|
|
30
|
+
self._verify_update(bot_data, {})
|
|
31
|
+
gateway = self.env["mail.gateway"].browse(bot_data["id"])
|
|
32
|
+
if kwargs.get("hub.verify_token") != gateway.whatsapp_security_key:
|
|
33
|
+
return None
|
|
34
|
+
gateway.sudo().integrated_webhook_state = "integrated"
|
|
35
|
+
response = request.make_response(kwargs.get("hub.challenge"))
|
|
36
|
+
response.status_code = 200
|
|
37
|
+
return response
|
|
38
|
+
|
|
39
|
+
def _set_webhook(self, gateway):
|
|
40
|
+
gateway.integrated_webhook_state = "pending"
|
|
41
|
+
|
|
42
|
+
def _verify_update(self, bot_data, kwargs):
|
|
43
|
+
signature = request.httprequest.headers.get("x-hub-signature-256")
|
|
44
|
+
if not signature:
|
|
45
|
+
return False
|
|
46
|
+
if (
|
|
47
|
+
"sha256=%s"
|
|
48
|
+
% hmac.new(
|
|
49
|
+
bot_data["webhook_secret"].encode(),
|
|
50
|
+
request.httprequest.data,
|
|
51
|
+
hashlib.sha256,
|
|
52
|
+
).hexdigest()
|
|
53
|
+
!= signature
|
|
54
|
+
):
|
|
55
|
+
return False
|
|
56
|
+
return True
|
|
57
|
+
|
|
58
|
+
def _get_channel_vals(self, gateway, token, update):
|
|
59
|
+
result = super()._get_channel_vals(gateway, token, update)
|
|
60
|
+
for contact in update.get("contacts", []):
|
|
61
|
+
if contact["wa_id"] == token:
|
|
62
|
+
result["name"] = contact["profile"]["name"]
|
|
63
|
+
continue
|
|
64
|
+
return result
|
|
65
|
+
|
|
66
|
+
def _receive_update(self, gateway, update):
|
|
67
|
+
if update:
|
|
68
|
+
for entry in update["entry"]:
|
|
69
|
+
for change in entry["changes"]:
|
|
70
|
+
if change["field"] != "messages":
|
|
71
|
+
continue
|
|
72
|
+
for message in change["value"].get("messages", []):
|
|
73
|
+
chat = self._get_channel(
|
|
74
|
+
gateway, message["from"], change["value"], force_create=True
|
|
75
|
+
)
|
|
76
|
+
if not chat:
|
|
77
|
+
continue
|
|
78
|
+
self._process_update(chat, message, change["value"])
|
|
79
|
+
|
|
80
|
+
def _process_update(self, chat, message, value):
|
|
81
|
+
chat.ensure_one()
|
|
82
|
+
body = ""
|
|
83
|
+
attachments = []
|
|
84
|
+
if message.get("text"):
|
|
85
|
+
body = message.get("text").get("body")
|
|
86
|
+
for key in ["image", "audio", "video", "document", "sticker"]:
|
|
87
|
+
if message.get(key):
|
|
88
|
+
image_id = message.get(key).get("id")
|
|
89
|
+
if image_id:
|
|
90
|
+
image_info_request = requests.get(
|
|
91
|
+
"https://graph.facebook.com/v%s/%s"
|
|
92
|
+
% (
|
|
93
|
+
chat.gateway_id.whatsapp_version,
|
|
94
|
+
image_id,
|
|
95
|
+
),
|
|
96
|
+
headers={
|
|
97
|
+
"Authorization": "Bearer %s" % chat.gateway_id.token,
|
|
98
|
+
},
|
|
99
|
+
timeout=10,
|
|
100
|
+
proxies=self._get_proxies(),
|
|
101
|
+
)
|
|
102
|
+
image_info_request.raise_for_status()
|
|
103
|
+
image_info = image_info_request.json()
|
|
104
|
+
image_url = image_info["url"]
|
|
105
|
+
else:
|
|
106
|
+
image_url = message.get(key).get("url")
|
|
107
|
+
if not image_url:
|
|
108
|
+
continue
|
|
109
|
+
image_request = requests.get(
|
|
110
|
+
image_url,
|
|
111
|
+
headers={
|
|
112
|
+
"Authorization": "Bearer %s" % chat.gateway_id.token,
|
|
113
|
+
},
|
|
114
|
+
timeout=10,
|
|
115
|
+
proxies=self._get_proxies(),
|
|
116
|
+
)
|
|
117
|
+
image_request.raise_for_status()
|
|
118
|
+
attachments.append(
|
|
119
|
+
(
|
|
120
|
+
"{}{}".format(
|
|
121
|
+
image_id,
|
|
122
|
+
mimetypes.guess_extension(image_info["mime_type"]),
|
|
123
|
+
),
|
|
124
|
+
image_request.content,
|
|
125
|
+
)
|
|
126
|
+
)
|
|
127
|
+
if message.get("location"):
|
|
128
|
+
body += (
|
|
129
|
+
'<a target="_blank" href="https://www.google.com/'
|
|
130
|
+
'maps/search/?api=1&query=%s,%s">Location</a>'
|
|
131
|
+
% (
|
|
132
|
+
message["location"]["latitude"],
|
|
133
|
+
message["location"]["longitude"],
|
|
134
|
+
)
|
|
135
|
+
)
|
|
136
|
+
if message.get("contacts"):
|
|
137
|
+
pass
|
|
138
|
+
if len(body) > 0 or attachments:
|
|
139
|
+
author = self._get_author(chat.gateway_id, value)
|
|
140
|
+
new_message = chat.message_post(
|
|
141
|
+
body=body,
|
|
142
|
+
author_id=author and author._name == "res.partner" and author.id,
|
|
143
|
+
gateway_type="whatsapp",
|
|
144
|
+
date=datetime.fromtimestamp(int(message["timestamp"])),
|
|
145
|
+
# message_id=update.message.message_id,
|
|
146
|
+
subtype_xmlid="mail.mt_comment",
|
|
147
|
+
message_type="comment",
|
|
148
|
+
attachments=attachments,
|
|
149
|
+
)
|
|
150
|
+
self._post_process_message(new_message, chat)
|
|
151
|
+
related_message_id = message.get("context", {}).get("id", False)
|
|
152
|
+
if related_message_id:
|
|
153
|
+
related_message = (
|
|
154
|
+
self.env["mail.notification"]
|
|
155
|
+
.search(
|
|
156
|
+
[
|
|
157
|
+
("gateway_channel_id", "=", chat.id),
|
|
158
|
+
("gateway_message_id", "=", related_message_id),
|
|
159
|
+
]
|
|
160
|
+
)
|
|
161
|
+
.mail_message_id
|
|
162
|
+
)
|
|
163
|
+
if related_message and related_message.gateway_message_id:
|
|
164
|
+
new_related_message = (
|
|
165
|
+
self.env[related_message.gateway_message_id.model]
|
|
166
|
+
.browse(related_message.gateway_message_id.res_id)
|
|
167
|
+
.message_post(
|
|
168
|
+
body=body,
|
|
169
|
+
author_id=author
|
|
170
|
+
and author._name == "res.partner"
|
|
171
|
+
and author.id,
|
|
172
|
+
gateway_type="whatsapp",
|
|
173
|
+
date=datetime.fromtimestamp(int(message["timestamp"])),
|
|
174
|
+
# message_id=update.message.message_id,
|
|
175
|
+
subtype_xmlid="mail.mt_comment",
|
|
176
|
+
message_type="comment",
|
|
177
|
+
attachments=attachments,
|
|
178
|
+
)
|
|
179
|
+
)
|
|
180
|
+
self._post_process_reply(related_message)
|
|
181
|
+
new_message.gateway_message_id = new_related_message
|
|
182
|
+
|
|
183
|
+
def _send(
|
|
184
|
+
self,
|
|
185
|
+
gateway,
|
|
186
|
+
record,
|
|
187
|
+
auto_commit=False,
|
|
188
|
+
raise_exception=False,
|
|
189
|
+
parse_mode=False,
|
|
190
|
+
):
|
|
191
|
+
message = False
|
|
192
|
+
try:
|
|
193
|
+
attachment_mimetype_map = self._get_whatsapp_mimetype_kind()
|
|
194
|
+
for attachment in record.mail_message_id.attachment_ids:
|
|
195
|
+
if attachment.mimetype not in attachment_mimetype_map:
|
|
196
|
+
raise UserError(_("Mimetype is not valid"))
|
|
197
|
+
attachment_type = attachment_mimetype_map[attachment.mimetype]
|
|
198
|
+
m = requests_toolbelt.multipart.encoder.MultipartEncoder(
|
|
199
|
+
fields={
|
|
200
|
+
"file": (
|
|
201
|
+
attachment.name,
|
|
202
|
+
attachment.raw,
|
|
203
|
+
attachment.mimetype,
|
|
204
|
+
),
|
|
205
|
+
"messaging_product": "whatsapp",
|
|
206
|
+
# "type": attachment_type
|
|
207
|
+
},
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
response = requests.post(
|
|
211
|
+
"https://graph.facebook.com/v%s/%s/media"
|
|
212
|
+
% (
|
|
213
|
+
gateway.whatsapp_version,
|
|
214
|
+
gateway.whatsapp_from_phone,
|
|
215
|
+
),
|
|
216
|
+
headers={
|
|
217
|
+
"Authorization": "Bearer %s" % gateway.token,
|
|
218
|
+
"content-type": m.content_type,
|
|
219
|
+
},
|
|
220
|
+
data=m,
|
|
221
|
+
timeout=10,
|
|
222
|
+
proxies=self._get_proxies(),
|
|
223
|
+
)
|
|
224
|
+
response.raise_for_status()
|
|
225
|
+
response = requests.post(
|
|
226
|
+
"https://graph.facebook.com/v%s/%s/messages"
|
|
227
|
+
% (
|
|
228
|
+
gateway.whatsapp_version,
|
|
229
|
+
gateway.whatsapp_from_phone,
|
|
230
|
+
),
|
|
231
|
+
headers={"Authorization": "Bearer %s" % gateway.token},
|
|
232
|
+
json=self._send_payload(
|
|
233
|
+
record.gateway_channel_id,
|
|
234
|
+
media_id=response.json()["id"],
|
|
235
|
+
media_type=attachment_type,
|
|
236
|
+
media_name=attachment.name,
|
|
237
|
+
),
|
|
238
|
+
timeout=10,
|
|
239
|
+
proxies=self._get_proxies(),
|
|
240
|
+
)
|
|
241
|
+
response.raise_for_status()
|
|
242
|
+
message = response.json()
|
|
243
|
+
body = self._get_message_body(record)
|
|
244
|
+
if body:
|
|
245
|
+
response = requests.post(
|
|
246
|
+
"https://graph.facebook.com/v%s/%s/messages"
|
|
247
|
+
% (
|
|
248
|
+
gateway.whatsapp_version,
|
|
249
|
+
gateway.whatsapp_from_phone,
|
|
250
|
+
),
|
|
251
|
+
headers={"Authorization": "Bearer %s" % gateway.token},
|
|
252
|
+
json=self._send_payload(record.gateway_channel_id, body=body),
|
|
253
|
+
timeout=10,
|
|
254
|
+
proxies=self._get_proxies(),
|
|
255
|
+
)
|
|
256
|
+
response.raise_for_status()
|
|
257
|
+
message = response.json()
|
|
258
|
+
except Exception as exc:
|
|
259
|
+
buff = StringIO()
|
|
260
|
+
traceback.print_exc(file=buff)
|
|
261
|
+
_logger.error(buff.getvalue())
|
|
262
|
+
if raise_exception:
|
|
263
|
+
raise MailDeliveryException(
|
|
264
|
+
_("Unable to send the whatsapp message")
|
|
265
|
+
) from exc
|
|
266
|
+
else:
|
|
267
|
+
_logger.warning(
|
|
268
|
+
"Issue sending message with id {}: {}".format(record.id, exc)
|
|
269
|
+
)
|
|
270
|
+
record.sudo().write(
|
|
271
|
+
{"notification_status": "exception", "failure_reason": exc}
|
|
272
|
+
)
|
|
273
|
+
if message:
|
|
274
|
+
record.sudo().write(
|
|
275
|
+
{
|
|
276
|
+
"notification_status": "sent",
|
|
277
|
+
"failure_reason": False,
|
|
278
|
+
"gateway_message_id": message["messages"][0]["id"],
|
|
279
|
+
}
|
|
280
|
+
)
|
|
281
|
+
if auto_commit is True:
|
|
282
|
+
# pylint: disable=invalid-commit
|
|
283
|
+
self.env.cr.commit()
|
|
284
|
+
|
|
285
|
+
def _send_payload(
|
|
286
|
+
self, channel, body=False, media_id=False, media_type=False, media_name=False
|
|
287
|
+
):
|
|
288
|
+
if body:
|
|
289
|
+
return {
|
|
290
|
+
"messaging_product": "whatsapp",
|
|
291
|
+
"recipient_type": "individual",
|
|
292
|
+
"to": channel.gateway_channel_token,
|
|
293
|
+
"type": "text",
|
|
294
|
+
"text": {"preview_url": False, "body": html2plaintext(body)},
|
|
295
|
+
}
|
|
296
|
+
if media_id:
|
|
297
|
+
media_data = {"id": media_id}
|
|
298
|
+
if media_type == "document":
|
|
299
|
+
media_data["filename"] = media_name
|
|
300
|
+
return {
|
|
301
|
+
"messaging_product": "whatsapp",
|
|
302
|
+
"recipient_type": "individual",
|
|
303
|
+
"to": channel.gateway_channel_token,
|
|
304
|
+
"type": media_type,
|
|
305
|
+
media_type: media_data,
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
def _get_whatsapp_mimetype_kind(self):
|
|
309
|
+
return {
|
|
310
|
+
"text/plain": "document",
|
|
311
|
+
"application/pdf": "document",
|
|
312
|
+
"application/vnd.ms-powerpoint": "document",
|
|
313
|
+
"application/msword": "document",
|
|
314
|
+
"application/vnd.ms-excel": "document",
|
|
315
|
+
"application/vnd.openxmlformats-officedocument."
|
|
316
|
+
"wordprocessingml.document": "document",
|
|
317
|
+
"application/vnd.openxmlformats-officedocument."
|
|
318
|
+
"presentationml.presentation": "document",
|
|
319
|
+
"application/vnd.openxmlformats-officedocument."
|
|
320
|
+
"spreadsheetml.sheet": "document",
|
|
321
|
+
"audio/aac": "audio",
|
|
322
|
+
"audio/mp4": "audio",
|
|
323
|
+
"audio/mpeg": "audio",
|
|
324
|
+
"audio/amr": "audio",
|
|
325
|
+
"audio/ogg": "audio",
|
|
326
|
+
"image/jpeg": "image",
|
|
327
|
+
"image/png": "image",
|
|
328
|
+
"video/mp4": "video",
|
|
329
|
+
"video/3gp": "video",
|
|
330
|
+
"image/webp": "sticker",
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
def _get_author(self, gateway, update):
|
|
334
|
+
author_id = update.get("messages")[0].get("from")
|
|
335
|
+
if author_id:
|
|
336
|
+
gateway_partner = self.env["res.partner.gateway.channel"].search(
|
|
337
|
+
[
|
|
338
|
+
("gateway_id", "=", gateway.id),
|
|
339
|
+
("gateway_token", "=", str(author_id)),
|
|
340
|
+
]
|
|
341
|
+
)
|
|
342
|
+
if gateway_partner:
|
|
343
|
+
return gateway_partner.partner_id
|
|
344
|
+
partner = self.env["res.partner"].search(
|
|
345
|
+
[("phone_sanitized", "=", "+" + str(author_id))]
|
|
346
|
+
)
|
|
347
|
+
if partner:
|
|
348
|
+
self.env["res.partner.gateway.channel"].create(
|
|
349
|
+
{
|
|
350
|
+
"name": gateway.name,
|
|
351
|
+
"partner_id": partner.id,
|
|
352
|
+
"gateway_id": gateway.id,
|
|
353
|
+
"gateway_token": str(author_id),
|
|
354
|
+
}
|
|
355
|
+
)
|
|
356
|
+
return partner
|
|
357
|
+
guest = self.env["mail.guest"].search(
|
|
358
|
+
[
|
|
359
|
+
("gateway_id", "=", gateway.id),
|
|
360
|
+
("gateway_token", "=", str(author_id)),
|
|
361
|
+
]
|
|
362
|
+
)
|
|
363
|
+
if guest:
|
|
364
|
+
return guest
|
|
365
|
+
author_vals = self._get_author_vals(gateway, author_id, update)
|
|
366
|
+
if author_vals:
|
|
367
|
+
return self.env["mail.guest"].create(author_vals)
|
|
368
|
+
|
|
369
|
+
return False
|
|
370
|
+
|
|
371
|
+
def _get_author_vals(self, gateway, author_id, update):
|
|
372
|
+
for contact in update.get("contacts", []):
|
|
373
|
+
if contact["wa_id"] == author_id:
|
|
374
|
+
return {
|
|
375
|
+
"name": contact.get("profile", {}).get("name", "Anonymous"),
|
|
376
|
+
"gateway_id": gateway.id,
|
|
377
|
+
"gateway_token": str(author_id),
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
def _get_proxies(self):
|
|
381
|
+
# This hook has been created in order to add a proxy if needed.
|
|
382
|
+
# By default, it does nothing.
|
|
383
|
+
return {}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# Copyright 2022 CreuBlanca
|
|
2
|
+
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
|
|
3
|
+
|
|
4
|
+
from odoo import _, models
|
|
5
|
+
from odoo.exceptions import UserError
|
|
6
|
+
|
|
7
|
+
from odoo.addons.phone_validation.tools import phone_validation
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class MailThread(models.AbstractModel):
|
|
11
|
+
|
|
12
|
+
_inherit = "mail.thread"
|
|
13
|
+
|
|
14
|
+
def _get_whatsapp_channel_vals(self, token, gateway, partner):
|
|
15
|
+
result = {
|
|
16
|
+
"gateway_channel_token": token,
|
|
17
|
+
"gateway_id": gateway.id,
|
|
18
|
+
}
|
|
19
|
+
if partner:
|
|
20
|
+
result["partner_id"] = partner.id
|
|
21
|
+
result["name"] = partner.display_name
|
|
22
|
+
return result
|
|
23
|
+
|
|
24
|
+
def _whatsapp_get_channel(self, field_name, gateway):
|
|
25
|
+
phone = self[field_name]
|
|
26
|
+
sanitize_res = phone_validation.phone_sanitize_numbers_w_record([phone], self)
|
|
27
|
+
sanitized_number = sanitize_res[phone].get("sanitized")
|
|
28
|
+
if not sanitized_number:
|
|
29
|
+
raise UserError(_("Phone cannot be sanitized"))
|
|
30
|
+
sanitized_number = sanitized_number[1:]
|
|
31
|
+
partner = self._whatsapp_get_partner()
|
|
32
|
+
if not self.env["res.partner.gateway.channel"].search(
|
|
33
|
+
[
|
|
34
|
+
("partner_id", "=", partner.id),
|
|
35
|
+
("gateway_id", "=", gateway.id),
|
|
36
|
+
("gateway_token", "=", sanitized_number),
|
|
37
|
+
]
|
|
38
|
+
):
|
|
39
|
+
self.env["res.partner.gateway.channel"].create(
|
|
40
|
+
{
|
|
41
|
+
"name": gateway.name,
|
|
42
|
+
"partner_id": partner.id,
|
|
43
|
+
"gateway_id": gateway.id,
|
|
44
|
+
"gateway_token": sanitized_number,
|
|
45
|
+
}
|
|
46
|
+
)
|
|
47
|
+
return self.env["mail.gateway.whatsapp"]._get_channel(
|
|
48
|
+
gateway,
|
|
49
|
+
sanitized_number,
|
|
50
|
+
{
|
|
51
|
+
"contacts": [
|
|
52
|
+
{
|
|
53
|
+
"wa_id": sanitized_number,
|
|
54
|
+
"profile": {"name": partner.display_name},
|
|
55
|
+
}
|
|
56
|
+
],
|
|
57
|
+
"messages": [{"from": sanitized_number}],
|
|
58
|
+
},
|
|
59
|
+
force_create=True,
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
def _whatsapp_get_partner(self):
|
|
63
|
+
if "partner_id" in self._fields:
|
|
64
|
+
return self.partner_id
|
|
65
|
+
return None
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# Copyright 2024 Dixmit
|
|
2
|
+
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
|
|
3
|
+
|
|
4
|
+
from odoo import models
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class ResPartner(models.Model):
|
|
8
|
+
_name = "res.partner"
|
|
9
|
+
_inherit = ["mail.thread.phone", "res.partner"]
|
|
10
|
+
|
|
11
|
+
def _whatsapp_get_partner(self):
|
|
12
|
+
return self
|
|
13
|
+
|
|
14
|
+
def _phone_get_number_fields(self):
|
|
15
|
+
"""This method returns the fields to use to find the number to use to
|
|
16
|
+
send an SMS on a record."""
|
|
17
|
+
result = set(super()._phone_get_number_fields())
|
|
18
|
+
result.add("mobile")
|
|
19
|
+
result.add("phone")
|
|
20
|
+
return list(result)
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
First steps
|
|
2
|
+
~~~~~~~~~~~
|
|
3
|
+
|
|
4
|
+
You need to create a WhatsApp Business Account (WABA), a Meta App and define a phone number.
|
|
5
|
+
You can follow this `steps <https://developers.facebook.com/micro_site/url/?click_from_context_menu=true&country=ES&destination=https%3A%2F%2Fwww.facebook.com%2Fbusiness%2Fhelp%2F2087193751603668&event_type=click&last_nav_impression_id=0m3TRxrxOlly1eRmB&max_percent_page_viewed=22&max_viewport_height_px=1326&max_viewport_width_px=2560&orig_http_referrer=https%3A%2F%2Fdevelopers.facebook.com%2Fdocs%2Fwhatsapp%2Fcloud-api%2Fget-started-for-bsps%3Flocale%3Den_US&orig_request_uri=https%3A%2F%2Fdevelopers.facebook.com%2Fajax%2Fpagelet%2Fgeneric.php%2FDeveloperNotificationsPayloadPagelet%3Ffb_dtsg_ag%3D--sanitized--%26data%3D%257B%2522businessUserID%2522%253Anull%252C%2522cursor%2522%253Anull%252C%2522length%2522%253A15%252C%2522clientRequestID%2522%253A%2522js_k6%2522%257D%26__usid%3D6-Trd7hi4itpm%253APrd7ifiub2tvy%253A0-Ard7g9twdm0p1-RV%253D6%253AF%253D%26locale%3Den_US%26jazoest%3D24920®ion=emea&scrolled=false&session_id=1jLoVJNU6iVMaw3ml&site=developers>`_.
|
|
6
|
+
|
|
7
|
+
If you create a test Business Account, passwords will change every 24 hours.
|
|
8
|
+
|
|
9
|
+
In order to make the webhook accessible, the system must be public.
|
|
10
|
+
|
|
11
|
+
Configure the gateway
|
|
12
|
+
~~~~~~~~~~~~~~~~~~~~~
|
|
13
|
+
|
|
14
|
+
Once you have created the Meta App, you need to add the gateway and webhook.
|
|
15
|
+
In order to make it you must follow this steps:
|
|
16
|
+
|
|
17
|
+
* Access `Settings > Emails > Mail Gateway`
|
|
18
|
+
* Create a Gateway of type `WhatsApp`
|
|
19
|
+
|
|
20
|
+
* Use the Meta App authentication key as `Token` field
|
|
21
|
+
* Use the Meta App Phone Number ID as `Whatsapp from Phone` field
|
|
22
|
+
* Write your own `Webhook key`
|
|
23
|
+
* Use the Application Secret Key on `Whatsapp Security Key`. It will be used in order to validate the data
|
|
24
|
+
* Press the `Integrate Webhook Key`. In this case, it will not integrate it, we need to make it manually
|
|
25
|
+
* Copy the webhook URL
|
|
26
|
+
|
|
27
|
+
* Access `Facebook Apps website <https://developers.facebook.com/apps/>`_
|
|
28
|
+
* Access your App then `Whatsapp > Configuration`
|
|
29
|
+
* Create your webhook using your URL and put the Whatsapp Security Key as validation Key
|
|
30
|
+
* Administer the Webhook and activate the messages webhook
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
This work has been funded by AEOdoo (Asociación Española de Odoo - https://www.aeodoo.org)
|
|
Binary file
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
|
2
|
+
<svg
|
|
3
|
+
viewBox="0 0 240 240"
|
|
4
|
+
version="1.1"
|
|
5
|
+
id="svg4"
|
|
6
|
+
sodipodi:docname="whatsapp.svg"
|
|
7
|
+
inkscape:version="1.1.2 (0a00cf5339, 2022-02-04)"
|
|
8
|
+
width="240"
|
|
9
|
+
height="240"
|
|
10
|
+
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
|
11
|
+
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
|
12
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
13
|
+
xmlns:svg="http://www.w3.org/2000/svg">
|
|
14
|
+
<defs
|
|
15
|
+
id="defs8" />
|
|
16
|
+
<sodipodi:namedview
|
|
17
|
+
id="namedview6"
|
|
18
|
+
pagecolor="#ffffff"
|
|
19
|
+
bordercolor="#666666"
|
|
20
|
+
borderopacity="1.0"
|
|
21
|
+
inkscape:pageshadow="2"
|
|
22
|
+
inkscape:pageopacity="0.0"
|
|
23
|
+
inkscape:pagecheckerboard="0"
|
|
24
|
+
showgrid="false"
|
|
25
|
+
inkscape:zoom="2.8085937"
|
|
26
|
+
inkscape:cx="103.25452"
|
|
27
|
+
inkscape:cy="127.2879"
|
|
28
|
+
inkscape:window-width="1858"
|
|
29
|
+
inkscape:window-height="1016"
|
|
30
|
+
inkscape:window-x="0"
|
|
31
|
+
inkscape:window-y="0"
|
|
32
|
+
inkscape:window-maximized="1"
|
|
33
|
+
inkscape:current-layer="svg4"
|
|
34
|
+
units="px"
|
|
35
|
+
width="240px" />
|
|
36
|
+
<!--!Font Awesome Free 6.5.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2024 Fonticons, Inc.-->
|
|
37
|
+
<rect
|
|
38
|
+
style="fill:#875a7b"
|
|
39
|
+
id="rect965"
|
|
40
|
+
width="240"
|
|
41
|
+
height="240"
|
|
42
|
+
x="0"
|
|
43
|
+
y="0" />
|
|
44
|
+
<path
|
|
45
|
+
d="M 176.0428,63.25121 C 161.07774,48.25043 141.14813,40 119.96845,40 76.25189,40 40.6786,75.57328 40.6786,119.28985 c 0,13.96501 3.64306,27.60858 10.57199,39.64492 L 40,200.00834 82.0379,188.97205 c 11.572034,6.32176 24.60843,9.64336 37.89484,9.64336 h 0.0357 c 43.68085,0 80.03989,-35.57328 80.03989,-79.28985 0,-21.17967 -9.00047,-41.07357 -23.96554,-56.07435 z m -56.07435,122.00636 c -11.85776,0 -23.465496,-3.17873 -33.573176,-9.17905 l -2.392984,-1.42864 -24.92987,6.53605 6.64321,-24.3227 -1.57151,-2.50012 c -6.60749,-10.50055 -10.07196,-22.60832 -10.07196,-35.07326 0,-36.32332 29.57297,-65.89629 65.93201,-65.89629 17.60806,0 34.14464,6.8575 46.57385,19.32243 12.42923,12.46494 20.07248,29.00151 20.03677,46.60957 0,36.35904 -30.32301,65.93201 -66.64634,65.93201 z m 36.14474,-49.35971 c -1.96438,-1.00005 -11.71489,-5.78602 -13.53641,-6.42891 -1.82153,-0.67861 -3.14303,-1.00005 -4.46453,1.00005 -1.32149,2.00011 -5.1074,6.42891 -6.28603,7.78612 -1.14292,1.3215 -2.32155,1.50008 -4.28594,0.50003 -11.64347,-5.82173 -19.28673,-10.3934 -26.9657,-23.57266 -2.03582,-3.50018 2.03583,-3.25017 5.82174,-10.82199 0.64289,-1.3215 0.32144,-2.46441 -0.17858,-3.46447 -0.50003,-1.00005 -4.46452,-10.75056 -6.10747,-14.71505 -1.60722,-3.85734 -3.25016,-3.3216 -4.464506,-3.39304 -1.14292,-0.0714 -2.46442,-0.0714 -3.78592,-0.0714 -1.32149,0 -3.46446,0.50003 -5.28598,2.46442 -1.821534,2.0001 -6.928944,6.78607 -6.928944,16.53657 0,9.75051 7.107514,19.17957 8.071854,20.50107 1.00005,1.3215 13.965006,21.32254 33.858906,29.93014 12.57209,5.42885 17.50091,5.89316 23.78695,4.96454 3.82163,-0.57146 11.7149,-4.78597 13.35784,-9.42907 1.64294,-4.6431 1.64294,-8.60758 1.14292,-9.42906 -0.46431,-0.8929 -1.78581,-1.39293 -3.7502,-2.35726 z"
|
|
46
|
+
id="path2"
|
|
47
|
+
style="fill:#ffffff;fill-opacity:1;stroke-width:0.357162" />
|
|
48
|
+
</svg>
|