1
0
forked from Mapan/odoo17e
odoo17e-kedaikipas58/addons/mail_mobile/models/mail_thread.py
2024-12-10 09:04:09 +07:00

212 lines
9.1 KiB
Python

# -*- coding: utf-8 -*-
import copy
import logging as logger
import re
import urllib.parse
from odoo import models, api, tools
from odoo.addons.iap.tools import iap_tools
_logger = logger.getLogger(__name__)
MOBILE_APP_IDENTIFIER = 'com.odoo.mobile'
FIREBASE_DEFAULT_LINK = 'https://redirect-url.email/'
BLACK_LIST_PARAM = {
'access_token',
'auth_signup_token',
}
class MailThread(models.AbstractModel):
_inherit = 'mail.thread'
def _notify_thread(self, message, msg_vals=False, **kwargs):
scheduled_date = self._is_notification_scheduled(kwargs.get('scheduled_date'))
recipients_data = super(MailThread, self)._notify_thread(message, msg_vals=msg_vals, **kwargs)
# if scheduled for later: notification queue will call the notification method
if not scheduled_date:
self._notify_thread_by_ocn(message, recipients_data, msg_vals, **kwargs)
return recipients_data
def _notify_thread_by_ocn(self, message, recipients_data, msg_vals=False, **kwargs):
""" Method to send cloud notifications for every mentions of a partner
and every direct message. We have to take into account the risk of
duplicated notifications in case of a mention in a channel of `chat` type.
:param message: ``mail.message`` record to notify;
:param recipients_data: list of recipients information (based on res.partner
records), formatted like
[{'active': partner.active;
'id': id of the res.partner being recipient to notify;
'groups': res.group IDs if linked to a user;
'notif': 'inbox', 'email', 'sms' (SMS App);
'share': partner.partner_share;
'type': 'customer', 'portal', 'user;'
}, {...}].
See ``MailThread._notify_get_recipients``;
:param msg_vals: dictionary of values used to create the message. If given it
may be used to access values related to ``message`` without accessing it
directly. It lessens query count in some optimized use cases by avoiding
access message content in db;
"""
icp_sudo = self.env['ir.config_parameter'].sudo()
# Avoid to send notification if this feature is disabled or if no user use the mobile app.
if not icp_sudo.get_param('odoo_ocn.project_id') or not icp_sudo.get_param('mail_mobile.enable_ocn'):
return
msg_vals = dict(msg_vals or {})
pids = self._extract_partner_ids_for_notifications(message, msg_vals, recipients_data)
if not pids:
return
self._notify_by_ocn_send(message, pids, msg_vals=msg_vals)
def _notify_by_ocn_send(self, message, partner_ids, msg_vals=False):
"""
Send the notification to a list of partners
:param message: current mail.message record
:param partner_ids: list of partner IDs
:param msg_vals: see ``_notify_thread_by_ocn()``;
"""
if not partner_ids:
return
receiver_ids = self.env['res.partner'].sudo().search([
('id', 'in', partner_ids),
('ocn_token', '!=', False)
])
if receiver_ids:
endpoint = self.env['res.config.settings']._get_endpoint()
payload = self._notify_by_ocn_prepare_payload(message, receiver_ids, msg_vals=msg_vals)
# prepare chunks
chunks = []
at_mention_ocn_token_list = []
identities_ocn_token_list = []
at_mention_analyser_id_list = self._at_mention_analyser(msg_vals.get('body') if msg_vals else message.body)
for receiver_id in receiver_ids:
if receiver_id.id in at_mention_analyser_id_list:
at_mention_ocn_token_list.append(receiver_id.ocn_token)
else:
identities_ocn_token_list.append(receiver_id.ocn_token)
# first chunk
if identities_ocn_token_list:
chunks.append({
'ocn_tokens': identities_ocn_token_list,
'data': payload,
})
# second chunk for mentions with specific channel
if at_mention_ocn_token_list:
new_payload = copy.copy(payload)
new_payload['android_channel_id'] = 'AtMention'
chunks.append({
'ocn_tokens': at_mention_ocn_token_list,
'data': new_payload,
})
for chunk in chunks:
try:
iap_tools.iap_jsonrpc(endpoint + '/iap/ocn/send', params=chunk)
except Exception as e:
_logger.error('An error occurred while contacting the ocn server: %s', e)
def _notify_by_ocn_prepare_payload(self, message, receiver_ids, msg_vals=False):
"""Returns dictionary containing message information for mobile device.
This info will be delivered to mobile device via Google Firebase Cloud
Messaging (FCM). And it is having limit of 4000 bytes (4kb)
"""
author_id = [msg_vals.get('author_id')] if 'author_id' in msg_vals else message.author_id.ids
author_name = self.env['res.partner'].browse(author_id).name
model = msg_vals.get('model') if msg_vals else message.model
res_id = msg_vals.get('res_id') if msg_vals else message.res_id
record_name = msg_vals.get('record_name') if msg_vals else message.record_name
subject = msg_vals.get('subject') if msg_vals else message.subject
payload = {
"author_name": author_name,
"model": model,
"res_id": res_id,
"db_id": self.env['res.config.settings']._get_ocn_uuid()
}
if not payload['model'] and msg_vals and msg_vals.get('body'):
payload['model'], payload['res_id'] = self._extract_model_and_id(msg_vals)
payload['subject'] = record_name or subject
payload['android_channel_id'] = 'Following'
# Check payload limit of 4000 bytes (4kb) and if remain space add the body
payload_length = len(str(payload).encode('utf-8'))
body = msg_vals.get('body') if msg_vals else message.body
# FIXME: when msg_type is 'user_notification', the type value of msg_vals.get('body') is bytes
if isinstance(body, bytes):
body = body.decode("utf-8")
if payload_length < 4000:
payload_body = tools.html2plaintext(body)
payload_body += self._generate_tracking_message(message)
payload['body'] = payload_body[:4000 - payload_length]
return payload
@api.model
def _at_mention_analyser(self, body):
"""
Analyse the message to see if there is a @Mention in the notification
:param body: original body of current mail.message record
:return: a array with the list of ids for the @Mention partners
"""
if isinstance(body, bytes):
body = body.decode('utf-8')
at_mention_ids = []
regex = r"<a[^>]+data-oe-id=['\"](?P<id>\d+)['\"][^>]+data-oe-model=['\"](?P<model>[\w.]+)['\"][^>]+>@[^<]+<\/a>"
matches = re.finditer(regex, body)
for match in matches:
if match.group('model') == 'res.partner':
match_id = match.group('id')
try:
at_mention_ids.append(int(match_id))
except (ValueError, TypeError):
# We catch the exception because mail.message is mainly used by other app.
# So it's better to have no id instead of blocking the process.
_logger.error("Invalid conversion to int: %s" % match_id)
return at_mention_ids
# Firebase Dynamic Links
def _notify_get_action_link(self, link_type, **kwargs):
original_link = super(MailThread, self)._notify_get_action_link(link_type, **kwargs)
# BLACK_LIST_PARAM to avoid leak of token (3rd party: Firebase)
if link_type != 'view' or BLACK_LIST_PARAM.intersection(kwargs.keys()):
return original_link
# Check if feature is enable to avoid request and computation
disable_redirect_fdl = self.env['ir.config_parameter'].sudo().get_param(
'mail_mobile.disable_redirect_firebase_dynamic_link', default=False)
if disable_redirect_fdl:
return original_link
# Force to have absolute url and not relative url
# This is already done in the super function _notify_get_action_link
# but in some case "this" is not defined.
# The base url is not prepend it's why we do it manually.
if original_link.startswith('/'):
base_url = self.env['ir.config_parameter'].sudo().get_param('web.base.url')
original_link = base_url + original_link
# https://firebase.google.com/docs/dynamic-links/create-manually
url_params = urllib.parse.urlencode({
'link': original_link,
'apn': MOBILE_APP_IDENTIFIER,
'afl': original_link,
'ibi': MOBILE_APP_IDENTIFIER,
'ifl': original_link,
})
return "%s?%s" % (FIREBASE_DEFAULT_LINK, url_params)