odoo18/addons_extensions/whatsapp/models/whatsapp_account.py

244 lines
12 KiB
Python

# Part of Odoo. See LICENSE file for full copyright and licensing details.
import logging
import mimetypes
import secrets
import string
from markupsafe import Markup
from odoo import api, fields, models, _
from odoo.exceptions import UserError, ValidationError
from odoo.addons.whatsapp.tools.whatsapp_api import WhatsAppApi
from odoo.addons.whatsapp.tools.whatsapp_exception import WhatsAppError
from odoo.tools import plaintext2html
_logger = logging.getLogger(__name__)
class WhatsAppAccount(models.Model):
_name = 'whatsapp.account'
_inherit = ['mail.thread']
_description = 'WhatsApp Business Account'
name = fields.Char(string="Name", tracking=1)
active = fields.Boolean(default=True, tracking=6)
app_uid = fields.Char(string="App ID", required=True, tracking=2)
app_secret = fields.Char(string="App Secret", groups='whatsapp.group_whatsapp_admin', required=True)
account_uid = fields.Char(string="Account ID", required=True, tracking=3)
phone_uid = fields.Char(string="Phone Number ID", required=True, tracking=4)
token = fields.Char(string="Access Token", required=True, groups='whatsapp.group_whatsapp_admin')
webhook_verify_token = fields.Char(string="Webhook Verify Token", compute='_compute_verify_token',
groups='whatsapp.group_whatsapp_admin', store=True)
callback_url = fields.Char(string="Callback URL", compute='_compute_callback_url', readonly=True, copy=False)
allowed_company_ids = fields.Many2many(
comodel_name='res.company', string="Allowed Company",
default=lambda self: self.env.company)
notify_user_ids = fields.Many2many(
comodel_name='res.users', default=lambda self: self.env.user,
domain=[('share', '=', False)], required=True, tracking=5,
help="Users to notify when a message is received and there is no template send in last 15 days")
templates_count = fields.Integer(string="Message Count", compute='_compute_templates_count')
_sql_constraints = [
('phone_uid_unique', 'unique(phone_uid)', "The same phone number ID already exists")]
@api.constrains('notify_user_ids')
def _check_notify_user_ids(self):
for phone in self:
if len(phone.notify_user_ids) < 1:
raise ValidationError(_("Users to notify is required"))
def _compute_callback_url(self):
for account in self:
account.callback_url = self.get_base_url() + '/whatsapp/webhook'
@api.depends('account_uid')
def _compute_verify_token(self):
""" webhook_verify_token only set when record is created. Not update after that."""
for rec in self:
if rec.id and not rec.webhook_verify_token:
rec.webhook_verify_token = ''.join(secrets.choice(string.ascii_letters + string.digits) for _ in range(8))
def _compute_templates_count(self):
for tmpl in self:
tmpl.templates_count = self.env['whatsapp.template'].search_count([('wa_account_id', '=', tmpl.id)])
def button_sync_whatsapp_account_templates(self):
"""
This method will sync all the templates of the WhatsApp Business Account.
It will create new templates and update existing templates.
"""
self.ensure_one()
try:
response = WhatsAppApi(self)._get_all_template(fetch_all=True)
except WhatsAppError as err:
raise ValidationError(str(err)) from err
WhatsappTemplate = self.env['whatsapp.template']
existing_tmpls = WhatsappTemplate.with_context(active_test=False).search([('wa_account_id', '=', self.id)])
existing_tmpl_by_id = {t.wa_template_uid: t for t in existing_tmpls}
template_update_count = 0
template_create_count = 0
if response.get('data'):
create_vals = []
for template in response['data']:
existing_tmpl = existing_tmpl_by_id.get(template['id'])
if existing_tmpl:
template_update_count += 1
existing_tmpl._update_template_from_response(template)
else:
template_create_count += 1
create_vals.append(WhatsappTemplate._create_template_from_response(template, self))
WhatsappTemplate.create(create_vals)
return {
'type': 'ir.actions.client',
'tag': 'display_notification',
'params': {
'title': _("Templates synchronized!"),
'type': 'success',
'message': _("%(create_count)s were created, %(update_count)s were updated",
create_count=template_create_count, update_count=template_update_count),
'next': {'type': 'ir.actions.act_window_close'},
}
}
def button_test_connection(self):
""" Test connection of the WhatsApp Business Account. with the given credentials.
"""
self.ensure_one()
wa_api = WhatsAppApi(self)
try:
wa_api._test_connection()
except WhatsAppError as e:
raise UserError(str(e))
return {
'type': 'ir.actions.client',
'tag': 'display_notification',
'params': {
'type': 'success',
'message': _("Credentials look good!"),
}
}
def action_open_templates(self):
self.ensure_one()
return {
'name': _("Templates Of %(account_name)s", account_name=self.name),
'view_mode': 'list,form',
'res_model': 'whatsapp.template',
'domain': [('wa_account_id', '=', self.id)],
'type': 'ir.actions.act_window',
'context': {'default_wa_account_id': self.id},
}
def _find_active_channel(self, sender_mobile_formatted, sender_name=False, create_if_not_found=False):
"""This method will find the active channel for the given sender mobile number."""
self.ensure_one()
whatsapp_message = self.env['whatsapp.message'].sudo().search(
[
('mobile_number_formatted', '=', sender_mobile_formatted),
('wa_account_id', '=', self.id),
('wa_template_id', '!=', False),
('state', 'not in', ['outgoing', 'error', 'cancel']),
], limit=1, order='id desc')
return self.env['discuss.channel'].sudo()._get_whatsapp_channel(
whatsapp_number=sender_mobile_formatted,
wa_account_id=self,
sender_name=sender_name,
create_if_not_found=create_if_not_found,
related_message=whatsapp_message.mail_message_id,
)
def _process_messages(self, value):
"""
This method is used for processing messages with the values received via webhook.
If any whatsapp message template has been sent from this account then it will find the active channel or
create new channel with last template message sent to that number and post message in that channel.
And if channel is not found then it will create new channel with notify user set in account and post message.
Supported Messages
=> Text Message
=> Attachment Message with caption
=> Location Message
=> Contact Message
=> Message Reactions
"""
if 'messages' not in value and value.get('whatsapp_business_api_data', {}).get('messages'):
value = value['whatsapp_business_api_data']
wa_api = WhatsAppApi(self)
for messages in value.get('messages', []):
parent_msg_id = False
parent_id = False
channel = False
sender_name = value.get('contacts', [{}])[0].get('profile', {}).get('name')
sender_mobile = messages['from']
message_type = messages['type']
if 'context' in messages and messages['context'].get('id'):
parent_whatsapp_message = self.env['whatsapp.message'].sudo().search([('msg_uid', '=', messages['context']['id'])])
if parent_whatsapp_message:
parent_msg_id = parent_whatsapp_message.id
parent_id = parent_whatsapp_message.mail_message_id
if parent_id:
channel = self.env['discuss.channel'].sudo().search([('message_ids', 'in', parent_id.id)], limit=1)
if not channel:
channel = self._find_active_channel(sender_mobile, sender_name=sender_name, create_if_not_found=True)
kwargs = {
'message_type': 'whatsapp_message',
'author_id': channel.whatsapp_partner_id.id,
'parent_msg_id': parent_msg_id,
'subtype_xmlid': 'mail.mt_comment',
'parent_id': parent_id.id if parent_id else None
}
if message_type == 'text':
kwargs['body'] = plaintext2html(messages['text']['body'])
elif message_type == 'button':
kwargs['body'] = messages['button']['text']
elif message_type in ('document', 'image', 'audio', 'video', 'sticker'):
filename = messages[message_type].get('filename')
is_voice = messages[message_type].get('voice')
mime_type = messages[message_type].get('mime_type')
caption = messages[message_type].get('caption')
datas = wa_api._get_whatsapp_document(messages[message_type]['id'])
if not filename:
extension = mimetypes.guess_extension(mime_type) or ''
filename = message_type + extension
kwargs['attachments'] = [(filename, datas, {'voice': is_voice})]
if caption:
kwargs['body'] = plaintext2html(caption)
elif message_type == 'location':
url = Markup("https://maps.google.com/maps?q={latitude},{longitude}").format(
latitude=messages['location']['latitude'], longitude=messages['location']['longitude'])
body = Markup('<a target="_blank" href="{url}"> <i class="fa fa-map-marker"/> {location_string} </a>').format(
url=url, location_string=_("Location"))
if messages['location'].get('name'):
body += Markup("<br/>{location_name}").format(location_name=messages['location']['name'])
if messages['location'].get('address'):
body += Markup("<br/>{location_address}").format(location_name=messages['location']['address'])
kwargs['body'] = body
elif message_type == 'contacts':
body = ""
for contact in messages['contacts']:
body += Markup("<i class='fa fa-address-book'/> {contact_name} <br/>").format(
contact_name=contact.get('name', {}).get('formatted_name', ''))
for phone in contact.get('phones'):
body += Markup("{phone_type}: {phone_number}<br/>").format(
phone_type=phone.get('type'), phone_number=phone.get('phone'))
kwargs['body'] = body
elif message_type == 'reaction':
msg_uid = messages['reaction'].get('message_id')
whatsapp_message = self.env['whatsapp.message'].sudo().search([('msg_uid', '=', msg_uid)])
if whatsapp_message:
partner_id = channel.whatsapp_partner_id
emoji = messages['reaction'].get('emoji')
whatsapp_message.mail_message_id._post_whatsapp_reaction(reaction_content=emoji, partner_id=partner_id)
continue
else:
_logger.warning("Unsupported whatsapp message type: %s", messages)
continue
channel.message_post(whatsapp_inbound_msg_uid=messages['id'], **kwargs)