When using message_post, the body format must be explicitly specified. If html is expected, a Markup object should be used. If text is given, the content will be escaped. Before this PR: message_post was unaware if the content of a message was HTML or text. This lead to multiple situation where the content was incorrectly considered as HTML and led to display errors. In self.message_post(body="Hello %s!" % self.name) if the name contained HTML, it would be evaluated. In self.message_post(body="Contact Raoul <raoul@caramail.be>") the email would not be displayed as considered as unknown HTML and discarded by the sanitizer Now each call must explict the type of content. Use the escape() helper to properly combine Markup and translations. It would also be acceptable to use Markup() to wrap a static translation but escape is better as one can not guarantee the content of a translation. closes odoo/odoo#111850 Related: odoo/documentation#3612 Related: odoo/enterprise#36728 Signed-off-by: Thibault Delavallee (tde) <tde@openerp.com>
239 lines
12 KiB
Python
239 lines
12 KiB
Python
# -*- coding: utf-8 -*-
|
|
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
|
|
|
import itertools
|
|
import random
|
|
|
|
from markupsafe import Markup
|
|
from odoo import models, _
|
|
|
|
class MailBot(models.AbstractModel):
|
|
_name = 'mail.bot'
|
|
_description = 'Mail Bot'
|
|
|
|
def _apply_logic(self, record, values, command=None):
|
|
""" Apply bot logic to generate an answer (or not) for the user
|
|
The logic will only be applied if odoobot is in a chat with a user or
|
|
if someone pinged odoobot.
|
|
|
|
:param record: the mail_thread (or mail_channel) where the user
|
|
message was posted/odoobot will answer.
|
|
:param values: msg_values of the message_post or other values needed by logic
|
|
:param command: the name of the called command if the logic is not triggered by a message_post
|
|
"""
|
|
odoobot_id = self.env['ir.model.data']._xmlid_to_res_id("base.partner_root")
|
|
if len(record) != 1 or values.get("author_id") == odoobot_id or values.get("message_type") != "comment" and not command:
|
|
return
|
|
if self._is_bot_pinged(values) or self._is_bot_in_private_channel(record):
|
|
body = values.get("body", "").replace(u'\xa0', u' ').strip().lower().strip(".!")
|
|
answer = self._get_answer(record, body, values, command)
|
|
if answer:
|
|
message_type = 'comment'
|
|
subtype_id = self.env['ir.model.data']._xmlid_to_res_id('mail.mt_comment')
|
|
record.with_context(mail_create_nosubscribe=True).sudo().message_post(body=answer, author_id=odoobot_id, message_type=message_type, subtype_id=subtype_id)
|
|
|
|
def _get_answer(self, record, body, values, command=False):
|
|
# onboarding
|
|
odoobot_state = self.env.user.odoobot_state
|
|
if self._is_bot_in_private_channel(record):
|
|
# main flow
|
|
if odoobot_state == 'onboarding_emoji' and self._body_contains_emoji(body):
|
|
self.env.user.odoobot_state = "onboarding_command"
|
|
self.env.user.odoobot_failed = False
|
|
return Markup(_("Great! 👍<br/>To access special commands, <b>start your sentence with</b> <span class=\"o_odoobot_command\">/</span>. Try getting help."))
|
|
elif odoobot_state == 'onboarding_command' and command == 'help':
|
|
self.env.user.odoobot_state = "onboarding_ping"
|
|
self.env.user.odoobot_failed = False
|
|
return Markup(_("Wow you are a natural!<br/>Ping someone with @username to grab their attention. <b>Try to ping me using</b> <span class=\"o_odoobot_command\">@OdooBot</span> in a sentence."))
|
|
elif odoobot_state == 'onboarding_ping' and self._is_bot_pinged(values):
|
|
self.env.user.odoobot_state = "onboarding_attachement"
|
|
self.env.user.odoobot_failed = False
|
|
return Markup(_("Yep, I am here! 🎉 <br/>Now, try <b>sending an attachment</b>, like a picture of your cute dog..."))
|
|
elif odoobot_state == 'onboarding_attachement' and values.get("attachment_ids"):
|
|
self.env.user.odoobot_state = "idle"
|
|
self.env.user.odoobot_failed = False
|
|
return Markup(_("I am a simple bot, but if that's a dog, he is the cutest 😊 <br/>Congratulations, you finished this tour. You can now <b>close this chat window</b>. Enjoy discovering Odoo."))
|
|
elif odoobot_state in (False, "idle", "not_initialized") and (_('start the tour') in body.lower()):
|
|
self.env.user.odoobot_state = "onboarding_emoji"
|
|
return _("To start, try to send me an emoji :)")
|
|
# easter eggs
|
|
elif odoobot_state == "idle" and body in ['❤️', _('i love you'), _('love')]:
|
|
return _("Aaaaaw that's really cute but, you know, bots don't work that way. You're too human for me! Let's keep it professional ❤️")
|
|
elif _('fuck') in body or "fuck" in body:
|
|
return _("That's not nice! I'm a bot but I have feelings... 💔")
|
|
# help message
|
|
elif self._is_help_requested(body) or odoobot_state == 'idle':
|
|
return Markup(_("Unfortunately, I'm just a bot 😞 I don't understand! If you need help discovering our product, please check "
|
|
"<a href=\"https://www.odoo.com/documentation\" target=\"_blank\">our documentation</a> or "
|
|
"<a href=\"https://www.odoo.com/slides\" target=\"_blank\">our videos</a>."))
|
|
else:
|
|
# repeat question
|
|
if odoobot_state == 'onboarding_emoji':
|
|
self.env.user.odoobot_failed = True
|
|
return Markup(_("Not exactly. To continue the tour, send an emoji: <b>type</b> <span class=\"o_odoobot_command\">:)</span> and press enter."))
|
|
elif odoobot_state == 'onboarding_attachement':
|
|
self.env.user.odoobot_failed = True
|
|
return Markup(_("To <b>send an attachment</b>, click on the <i class=\"fa fa-paperclip\" aria-hidden=\"true\"></i> icon and select a file."))
|
|
elif odoobot_state == 'onboarding_command':
|
|
self.env.user.odoobot_failed = True
|
|
return Markup(_("Not sure what you are doing. Please, type <span class=\"o_odoobot_command\">/</span> and wait for the propositions. Select <span class=\"o_odoobot_command\">help</span> and press enter"))
|
|
elif odoobot_state == 'onboarding_ping':
|
|
self.env.user.odoobot_failed = True
|
|
return Markup(_("Sorry, I am not listening. To get someone's attention, <b>ping him</b>. Write <span class=\"o_odoobot_command\">@OdooBot</span> and select me."))
|
|
return random.choice([
|
|
Markup(_("I'm not smart enough to answer your question.<br/>To follow my guide, ask: <span class=\"o_odoobot_command\">start the tour</span>.")),
|
|
_("Hmmm..."),
|
|
_("I'm afraid I don't understand. Sorry!"),
|
|
Markup(_("Sorry I'm sleepy. Or not! Maybe I'm just trying to hide my unawareness of human language...<br/>I can show you features if you write: <span class=\"o_odoobot_command\">start the tour</span>."))
|
|
])
|
|
return False
|
|
|
|
def _body_contains_emoji(self, body):
|
|
# coming from https://unicode.org/emoji/charts/full-emoji-list.html
|
|
emoji_list = itertools.chain(
|
|
range(0x231A, 0x231c),
|
|
range(0x23E9, 0x23f4),
|
|
range(0x23F8, 0x23fb),
|
|
range(0x25AA, 0x25ac),
|
|
range(0x25FB, 0x25ff),
|
|
range(0x2600, 0x2605),
|
|
range(0x2614, 0x2616),
|
|
range(0x2622, 0x2624),
|
|
range(0x262E, 0x2630),
|
|
range(0x2638, 0x263b),
|
|
range(0x2648, 0x2654),
|
|
range(0x265F, 0x2661),
|
|
range(0x2665, 0x2667),
|
|
range(0x267E, 0x2680),
|
|
range(0x2692, 0x2698),
|
|
range(0x269B, 0x269d),
|
|
range(0x26A0, 0x26a2),
|
|
range(0x26AA, 0x26ac),
|
|
range(0x26B0, 0x26b2),
|
|
range(0x26BD, 0x26bf),
|
|
range(0x26C4, 0x26c6),
|
|
range(0x26D3, 0x26d5),
|
|
range(0x26E9, 0x26eb),
|
|
range(0x26F0, 0x26f6),
|
|
range(0x26F7, 0x26fb),
|
|
range(0x2708, 0x270a),
|
|
range(0x270A, 0x270c),
|
|
range(0x270C, 0x270e),
|
|
range(0x2733, 0x2735),
|
|
range(0x2753, 0x2756),
|
|
range(0x2763, 0x2765),
|
|
range(0x2795, 0x2798),
|
|
range(0x2934, 0x2936),
|
|
range(0x2B05, 0x2b08),
|
|
range(0x2B1B, 0x2b1d),
|
|
range(0x1F170, 0x1f172),
|
|
range(0x1F191, 0x1f19b),
|
|
range(0x1F1E6, 0x1f200),
|
|
range(0x1F201, 0x1f203),
|
|
range(0x1F232, 0x1f23b),
|
|
range(0x1F250, 0x1f252),
|
|
range(0x1F300, 0x1f321),
|
|
range(0x1F324, 0x1f32d),
|
|
range(0x1F32D, 0x1f330),
|
|
range(0x1F330, 0x1f336),
|
|
range(0x1F337, 0x1f37d),
|
|
range(0x1F37E, 0x1f380),
|
|
range(0x1F380, 0x1f394),
|
|
range(0x1F396, 0x1f398),
|
|
range(0x1F399, 0x1f39c),
|
|
range(0x1F39E, 0x1f3a0),
|
|
range(0x1F3A0, 0x1f3c5),
|
|
range(0x1F3C6, 0x1f3cb),
|
|
range(0x1F3CB, 0x1f3cf),
|
|
range(0x1F3CF, 0x1f3d4),
|
|
range(0x1F3D4, 0x1f3e0),
|
|
range(0x1F3E0, 0x1f3f1),
|
|
range(0x1F3F3, 0x1f3f6),
|
|
range(0x1F3F8, 0x1f400),
|
|
range(0x1F400, 0x1f43f),
|
|
range(0x1F442, 0x1f4f8),
|
|
range(0x1F4F9, 0x1f4fd),
|
|
range(0x1F500, 0x1f53e),
|
|
range(0x1F549, 0x1f54b),
|
|
range(0x1F54B, 0x1f54f),
|
|
range(0x1F550, 0x1f568),
|
|
range(0x1F56F, 0x1f571),
|
|
range(0x1F573, 0x1f57a),
|
|
range(0x1F58A, 0x1f58e),
|
|
range(0x1F595, 0x1f597),
|
|
range(0x1F5B1, 0x1f5b3),
|
|
range(0x1F5C2, 0x1f5c5),
|
|
range(0x1F5D1, 0x1f5d4),
|
|
range(0x1F5DC, 0x1f5df),
|
|
range(0x1F5FB, 0x1f600),
|
|
range(0x1F601, 0x1f611),
|
|
range(0x1F612, 0x1f615),
|
|
range(0x1F61C, 0x1f61f),
|
|
range(0x1F620, 0x1f626),
|
|
range(0x1F626, 0x1f628),
|
|
range(0x1F628, 0x1f62c),
|
|
range(0x1F62E, 0x1f630),
|
|
range(0x1F630, 0x1f634),
|
|
range(0x1F635, 0x1f641),
|
|
range(0x1F641, 0x1f643),
|
|
range(0x1F643, 0x1f645),
|
|
range(0x1F645, 0x1f650),
|
|
range(0x1F680, 0x1f6c6),
|
|
range(0x1F6CB, 0x1f6d0),
|
|
range(0x1F6D1, 0x1f6d3),
|
|
range(0x1F6E0, 0x1f6e6),
|
|
range(0x1F6EB, 0x1f6ed),
|
|
range(0x1F6F4, 0x1f6f7),
|
|
range(0x1F6F7, 0x1f6f9),
|
|
range(0x1F910, 0x1f919),
|
|
range(0x1F919, 0x1f91f),
|
|
range(0x1F920, 0x1f928),
|
|
range(0x1F928, 0x1f930),
|
|
range(0x1F931, 0x1f933),
|
|
range(0x1F933, 0x1f93b),
|
|
range(0x1F93C, 0x1f93f),
|
|
range(0x1F940, 0x1f946),
|
|
range(0x1F947, 0x1f94c),
|
|
range(0x1F94D, 0x1f950),
|
|
range(0x1F950, 0x1f95f),
|
|
range(0x1F95F, 0x1f96c),
|
|
range(0x1F96C, 0x1f971),
|
|
range(0x1F973, 0x1f977),
|
|
range(0x1F97C, 0x1f980),
|
|
range(0x1F980, 0x1f985),
|
|
range(0x1F985, 0x1f992),
|
|
range(0x1F992, 0x1f998),
|
|
range(0x1F998, 0x1f9a3),
|
|
range(0x1F9B0, 0x1f9ba),
|
|
range(0x1F9C1, 0x1f9c3),
|
|
range(0x1F9D0, 0x1f9e7),
|
|
range(0x1F9E7, 0x1fa00),
|
|
[0x2328, 0x23cf, 0x24c2, 0x25b6, 0x25c0, 0x260e, 0x2611, 0x2618, 0x261d, 0x2620, 0x2626,
|
|
0x262a, 0x2640, 0x2642, 0x2663, 0x2668, 0x267b, 0x2699, 0x26c8, 0x26ce, 0x26cf,
|
|
0x26d1, 0x26fd, 0x2702, 0x2705, 0x270f, 0x2712, 0x2714, 0x2716, 0x271d, 0x2721, 0x2728, 0x2744, 0x2747, 0x274c,
|
|
0x274e, 0x2757, 0x27a1, 0x27b0, 0x27bf, 0x2b50, 0x2b55, 0x3030, 0x303d, 0x3297, 0x3299, 0x1f004, 0x1f0cf, 0x1f17e,
|
|
0x1f17f, 0x1f18e, 0x1f21a, 0x1f22f, 0x1f321, 0x1f336, 0x1f37d, 0x1f3c5, 0x1f3f7, 0x1f43f, 0x1f440, 0x1f441, 0x1f4f8,
|
|
0x1f4fd, 0x1f4ff, 0x1f57a, 0x1f587, 0x1f590, 0x1f5a4, 0x1f5a5, 0x1f5a8, 0x1f5bc, 0x1f5e1, 0x1f5e3, 0x1f5e8, 0x1f5ef,
|
|
0x1f5f3, 0x1f5fa, 0x1f600, 0x1f611, 0x1f615, 0x1f616, 0x1f617, 0x1f618, 0x1f619, 0x1f61a, 0x1f61b, 0x1f61f, 0x1f62c,
|
|
0x1f62d, 0x1f634, 0x1f6d0, 0x1f6e9, 0x1f6f0, 0x1f6f3, 0x1f6f9, 0x1f91f, 0x1f930, 0x1f94c, 0x1f97a, 0x1f9c0]
|
|
)
|
|
if any(chr(emoji) in body for emoji in emoji_list):
|
|
return True
|
|
return False
|
|
|
|
def _is_bot_pinged(self, values):
|
|
odoobot_id = self.env['ir.model.data']._xmlid_to_res_id("base.partner_root")
|
|
return odoobot_id in values.get('partner_ids', [])
|
|
|
|
def _is_bot_in_private_channel(self, record):
|
|
odoobot_id = self.env['ir.model.data']._xmlid_to_res_id("base.partner_root")
|
|
if record._name == 'mail.channel' and record.channel_type == 'chat':
|
|
return odoobot_id in record.with_context(active_test=False).channel_partner_ids.ids
|
|
return False
|
|
|
|
def _is_help_requested(self, body):
|
|
"""Returns whether a message linking to the documentation and videos
|
|
should be sent back to the user.
|
|
"""
|
|
return any(token in body for token in ['help', _('help'), '?']) or self.env.user.odoobot_failed
|