[ADD] mail_bot, im_livechat_mail_bot: add Odoobot, your new best friend
Purpose of this commit is to improve onboarding with a wow effect and a bot
to test the Discuss app. Otherwise new users have nobody to talk to. Retention
will be improved by both increasing interactions and onboarding of Discuss
features.
This commit adds a simple bot in discuss. It answers some questions, helps
users getting their hand on discuss and eases the onboarding.
In this version, the flow is quite simple, and only im_livechat adds some
logic in order to show canned response. The logic is contained in new
modules: mail_bot and im_livechat_mail_bot in order to keep everything
well separated. It also allows users to remove the mailbot if they do not
want to keep this functionality.
Odoobot will only answer if he is in the onboarding conversation (alone
with a user in a channel of type chat) or if a user pings odoobot.
Odoobot logic applies to both standard chatter / channel messages and
also transient messages (like help commands).
Specifications
* 2 minutes after first sign in, users will receive a direct chat from
Odoobot;
* make Odoobot an archived partner;
* scenario
* Odoobot: "Hello, I'm here to help you discover chat features. Try
answering me with an emoji :)";
* User: Send emoji
* Odoobot: "Great! :) Did you notice that you can also send attachments,
like a picture of your cute dog? Try it!"
* Odoobot: "Not a cute dog, but you get it :) To access special features,
start your sentence with "/" (I.E. /help)""
* User: /help
* Auto message then Odoobot: "Wow you're a natural! As a channel usually
contain a lot of users, you can grab the attention with a ping. Try to
ping me with @Odoobot!"
* User: @Odoobot lorem ipsum
* if Livechat installed
* add 2 demo canned response so it does not look weird (like "Hello, how
may I help you?" and "Have a nice day!")
* Odoobot: "Perfect! <br> Try to type ":" to use canned responses."
* User tries canned
* Odoobot: "Good, you can customize your canned responses in the live chat
application. <br><br> + réponse suivante"
* Odoobot: "There's 3 different ways in Odoo to interact with your
colleagues: via this chat window, [img of chat window] via the Discuss
application [img of Discuss app + icon on it] or via the chatter [img of
the chatter]. Aaaaand that's it! Enjoy discovering Odoo! :)"
* random answers to ping/bad answer
* "Mmmmh I'm not sure what you mean.. Can you try again?"
* "I'm afraid I don't understand. Sorry!"
* when someone pings @OdooBot with no reason: Odoobot: Yaaaay that's me!
[party emoji]
* fun stuff to add for some answer
* User: i love you / love
* Odoobot: 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 <3
* User: Fuck
* Odoobot: That's not a really nice thing to say, you know? I'm a bot but I
have feelings, ok?! </3
This commit is linked to task ID 1838588 and PR #25075.
This commit is contained in:
committed by
Thibault Delavallée
parent
c446fb2001
commit
cf505c3698
@@ -80,7 +80,7 @@ var BusService = CrossTab.extend(ServicesMixin, {
|
||||
* @param {function} [callback] if given callback will be called when user clicks on notification
|
||||
*/
|
||||
_sendNativeNotification: function (title, content, callback) {
|
||||
var notification = new Notification(title, {body: content, icon: "/mail/static/src/img/odoo_o.png"});
|
||||
var notification = new Notification(title, {body: content, icon: "/mail/static/src/img/odoobot.png"});
|
||||
notification.onclick = function () {
|
||||
window.focus();
|
||||
if (this.cancel) {
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from . import models
|
||||
@@ -0,0 +1,15 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
{
|
||||
'name': 'Odoobot for livechat',
|
||||
'version': '1.0',
|
||||
'category': 'Discuss',
|
||||
'summary': 'Add livechat support for odoobot',
|
||||
'description': "",
|
||||
'website': 'https://www.odoo.com/page/discuss',
|
||||
'depends': ['mail_bot', 'im_livechat'],
|
||||
'installable': True,
|
||||
'application': False,
|
||||
'auto_install': True,
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from . import mail_bot
|
||||
@@ -0,0 +1,25 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import models, _
|
||||
|
||||
|
||||
class MailBot(models.AbstractModel):
|
||||
_inherit = 'mail.bot'
|
||||
|
||||
def _get_answer(self, record, body, values, command):
|
||||
if self._is_bot_in_private_channel(record):
|
||||
if self._is_bot_pinged(values):
|
||||
return _("That's me! 🎉<br/>Try to type \":\" to use canned responses.")
|
||||
elif values.get("canned_response_ids"):
|
||||
discuss_href = 'href="/web#action=mail.mail_channel_action_client_chat&active_id=%s"' % record.id
|
||||
discuss_src = 'src="/mail_bot/static/img/odoobot_discuss.png"'
|
||||
chatter_src = 'src="/mail_bot/static/img/odoobot_chatter.png"'
|
||||
return _("Good, you can customize your canned responses in the live chat application. <br/><br/>") + \
|
||||
_("There are 3 different ways in Odoo to interact with your colleagues: <br/>\
|
||||
-via this chat window<br/>\
|
||||
-via the <a href=%s>Discuss</a> application:<br/><img %s/><br/><br/>\
|
||||
-or via the chatter:<br/><img %s/><br/><br/>\
|
||||
Aaaaand that's it! Enjoy discovering Odoo!") % (discuss_href, discuss_src, chatter_src)
|
||||
|
||||
return super(MailBot, self)._get_answer(record, body, values, command)
|
||||
@@ -828,6 +828,9 @@ class Channel(models.Model):
|
||||
WHERE C.uuid = %s""", (uuid,))
|
||||
return self._cr.dictfetchall()
|
||||
|
||||
def _channel_fetch_listeners_where_clause(self, uuid):
|
||||
return ("C.uuid = %s", (uuid,))
|
||||
|
||||
@api.multi
|
||||
def channel_fetch_preview(self):
|
||||
""" Return the last message of the given channels """
|
||||
@@ -888,7 +891,8 @@ class Channel(models.Model):
|
||||
if self.public == 'private':
|
||||
msg += _(" This channel is private. People must be invited to join it.")
|
||||
else:
|
||||
channel_partners = self.env['mail.channel.partner'].search([('partner_id', '!=', partner.id), ('channel_id', '=', self.id)])
|
||||
all_channel_partners = self.env['mail.channel.partner'].with_context(active_test=False)
|
||||
channel_partners = all_channel_partners.search([('partner_id', '!=', partner.id), ('channel_id', '=', self.id)])
|
||||
msg = _("You are in a private conversation with <b>@%s</b>.") % (channel_partners[0].partner_id.name if channel_partners else _('Anonymous'))
|
||||
msg += _("""<br><br>
|
||||
Type <b>@username</b> to mention someone, and grab his attention.<br>
|
||||
|
||||
@@ -2030,7 +2030,11 @@ class MailThread(models.AbstractModel):
|
||||
values.pop(x, None)
|
||||
|
||||
# Post the message
|
||||
# canned_response_ids are added by js to be used by other computations (odoobot)
|
||||
# we need to pop it from values since it is not stored on mail.message
|
||||
canned_response_ids = values.pop('canned_response_ids', False)
|
||||
new_message = MailMessage.create(values)
|
||||
values['canned_response_ids'] = canned_response_ids
|
||||
self._message_post_after_hook(new_message, values, model_description=model_description, mail_auto_delete=mail_auto_delete)
|
||||
return new_message
|
||||
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 2.2 KiB |
@@ -361,6 +361,7 @@ var BasicComposer = Widget.extend({
|
||||
content: this._mentionManager.generateLinks(value),
|
||||
attachment_ids: _.pluck(this.get('attachment_ids'), 'id'),
|
||||
partner_ids: _.uniq(_.pluck(this._mentionManager.getListenerSelection('@'), 'id')),
|
||||
canned_response_ids: _.uniq(_.pluck(this._mentionManager.getListenerSelections()[':'], 'id')),
|
||||
command: commands.length > 0 ? commands[0].name : undefined,
|
||||
});
|
||||
},
|
||||
|
||||
@@ -97,7 +97,7 @@ var Message = AbstractMessage.extend(Mixins.EventDispatcherMixin, ServicesMixin
|
||||
*/
|
||||
getAvatarSource: function () {
|
||||
if (this._isOdoobotAuthor()) {
|
||||
return '/mail/static/src/img/odoo_o.png';
|
||||
return '/mail/static/src/img/odoobot.png';
|
||||
} else if (this.hasAuthor()) {
|
||||
return '/web/image/res.partner/' + this.getAuthorID() + '/image_small';
|
||||
} else if (this.getType() === 'email') {
|
||||
|
||||
@@ -329,6 +329,7 @@ var Thread = AbstractThread.extend(Mixins.EventDispatcherMixin, ServicesMixin, {
|
||||
partner_ids: data.partner_ids,
|
||||
body: body,
|
||||
attachment_ids: data.attachment_ids,
|
||||
canned_response_ids: data.canned_response_ids,
|
||||
};
|
||||
if ('subject' in data) {
|
||||
messageData.subject = data.subject;
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from . import models
|
||||
@@ -0,0 +1,22 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
{
|
||||
'name': 'Odoobot',
|
||||
'version': '1.0',
|
||||
'category': 'Discuss',
|
||||
'summary': 'Add Odoobot in discussions',
|
||||
'description': "",
|
||||
'website': 'https://www.odoo.com/page/discuss',
|
||||
'depends': ['mail'],
|
||||
'installable': True,
|
||||
'application': False,
|
||||
'auto_install': True,
|
||||
'data': [
|
||||
'views/assets.xml',
|
||||
'data/mailbot_data.xml',
|
||||
],
|
||||
'qweb': [
|
||||
'views/discuss.xml',
|
||||
],
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<data>
|
||||
<record id="partner_odoobot" model="res.partner">
|
||||
<field name="name">OdooBot</field>
|
||||
<field name="customer" eval="False"/>
|
||||
<field name="email">OdooBot</field>
|
||||
<field name="active" eval="False"/>
|
||||
<field name="image" type="base64" file="mail/static/src/img/odoobot.png"/>
|
||||
</record>
|
||||
</data>
|
||||
</odoo>
|
||||
@@ -0,0 +1,9 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from . import ir_http
|
||||
from . import mail_bot
|
||||
from . import mail_channel
|
||||
from . import mail_thread
|
||||
from . import res_partner
|
||||
from . import res_users
|
||||
@@ -0,0 +1,13 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import models
|
||||
|
||||
|
||||
class Http(models.AbstractModel):
|
||||
_inherit = 'ir.http'
|
||||
|
||||
def session_info(self):
|
||||
res = super(Http, self).session_info()
|
||||
res['odoobot_initialized'] = self.env.user.odoobot_initialized
|
||||
return res
|
||||
@@ -0,0 +1,216 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
import itertools
|
||||
import random
|
||||
|
||||
from odoo import models, _
|
||||
|
||||
|
||||
class MailBot(models.AbstractModel):
|
||||
_name = '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("mail_bot.partner_odoobot")
|
||||
if len(record) != 1 or values.get("author_id") == odoobot_id:
|
||||
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 = values.get('message_type', 'comment')
|
||||
subtype_id = values.get('subtype_id', self.env['ir.model.data'].xmlid_to_res_id('mail.mt_comment'))
|
||||
record.with_context({"mail_create_nosubscribe": True}).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
|
||||
if self._is_bot_in_private_channel(record):
|
||||
# main flow
|
||||
if body in ('hi', 'hello'):
|
||||
return _("Hi, I will guide you through chat features. First, try to send me an emoji 😊")
|
||||
elif self._body_contains_emoji(body):
|
||||
return _("Great! :) Did you notice that you can also send attachments, like a picture of your cute dog? Try it!")
|
||||
elif values.get("attachment_ids"):
|
||||
return _("Not a cute dog, but you get it :) To access special features, start your sentence with '/' (e.g. /help).")
|
||||
elif command == 'help':
|
||||
return _("Wow you are a natural! Ping someone to grab its attention with @nameoftheuser. Try to ping me with <b>@OdooBot</b>.")
|
||||
elif self._is_bot_pinged(values):
|
||||
discuss_href = 'href="/web#action=mail.mail_channel_action_client_chat&active_id=%s"' % record.id
|
||||
discuss_src = 'src="/mail_bot/static/img/odoobot_discuss.png"'
|
||||
chatter_src = 'src="/mail_bot/static/img/odoobot_chatter.png"'
|
||||
return _("That's me! 🎉<br/>") + \
|
||||
_("There are 3 different ways in Odoo to interact with your colleagues: <br/>\
|
||||
-via this chat window<br/>\
|
||||
-via the <a href=%s>Discuss</a> application:<br/><img %s/><br/><br/>\
|
||||
-or via the chatter:<br/><img %s/><br/><br/>\
|
||||
Aaaaand that's it! Enjoy discovering Odoo!") % (discuss_href, discuss_src, chatter_src)
|
||||
# easter eggs
|
||||
elif 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 body in [_('help'), _('help me'), _('i need help')]:
|
||||
return _("I'm trying to help you, but I'm just a bot... :( You can also check <a href=\"https://www.odoo.com/page/docs\">our documentation</a>) for more information!")
|
||||
elif _('fuck') in body or "fuck" in body:
|
||||
return _("That's not a really nice thing to say, you know? I'm a bot but I have feelings, ok?! 💔")
|
||||
else:
|
||||
return random.choice([
|
||||
_("Mmmmh I'm not sure what you mean.. Can you try again?"),
|
||||
_("I'm afraid I don't understand. Sorry!")
|
||||
])
|
||||
elif self._is_bot_pinged(values):
|
||||
return random.choice([_("Yaaaay that's me!"), _("Odeyhoooo")])
|
||||
return False
|
||||
|
||||
def _body_contains_emoji(self, body):
|
||||
# coming from https://unicode.org/emoji/charts/full-emoji-list.html
|
||||
emoji_list = itertools.chain(
|
||||
range(0x0030, 0x3a),
|
||||
range(0x2194, 0x219a),
|
||||
range(0x21A9, 0x21ab),
|
||||
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),
|
||||
[0x23, 0x2a, 0xa9, 0xae, 0x203c, 0x2049, 0x2122, 0x2139, 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("mail_bot.partner_odoobot")
|
||||
return (4, 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("mail_bot.partner_odoobot")
|
||||
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
|
||||
@@ -0,0 +1,41 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import api, models, _
|
||||
|
||||
|
||||
class Channel(models.Model):
|
||||
_inherit = 'mail.channel'
|
||||
|
||||
def _execute_command_help(self, **kwargs):
|
||||
super(Channel, self)._execute_command_help(**kwargs)
|
||||
self.env['mail.bot']._apply_logic(self, kwargs, command="help") # kwargs are not usefull but...
|
||||
|
||||
@api.model
|
||||
def channel_fetch_listeners(self, uuid):
|
||||
""" Return the id, name and email of partners listening to the given channel """
|
||||
odoobot_id = self.env['ir.model.data'].xmlid_to_res_id("mail_bot.partner_odoobot")
|
||||
self._cr.execute("""
|
||||
SELECT P.id, P.name, P.email
|
||||
FROM mail_channel_partner CP
|
||||
INNER JOIN res_partner P ON CP.partner_id = P.id
|
||||
INNER JOIN mail_channel C ON CP.channel_id = C.id
|
||||
WHERE C.uuid = %s OR P.id = %s""", (uuid, odoobot_id,))
|
||||
return self._cr.dictfetchall()
|
||||
|
||||
@api.model
|
||||
def init_odoobot(self):
|
||||
if not self.env.user.odoobot_initialized:
|
||||
partner = self.env.user.partner_id
|
||||
odoobot_id = self.env['ir.model.data'].xmlid_to_res_id("mail_bot.partner_odoobot")
|
||||
channel = self.with_context({"mail_create_nosubscribe": True}).create({
|
||||
'channel_partner_ids': [(4, partner.id), (4, odoobot_id)],
|
||||
'public': 'private',
|
||||
'channel_type': 'chat',
|
||||
'email_send': False,
|
||||
'name': 'OdooBot'
|
||||
})
|
||||
message = _("Hello, I'm here to help you discover chat features. Try answering me with an emoji 😊")
|
||||
channel.message_post(body=message, author_id=odoobot_id, message_type="comment", subtype="mail.mt_comment")
|
||||
self.env.user.odoobot_initialized = True
|
||||
return channel
|
||||
@@ -0,0 +1,12 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import models
|
||||
|
||||
|
||||
class MailThread(models.AbstractModel):
|
||||
_inherit = 'mail.thread'
|
||||
|
||||
def _message_post_after_hook(self, message, values, model_description=False, mail_auto_delete=True):
|
||||
self.env['mail.bot']._apply_logic(self, values)
|
||||
return super(MailThread, self)._message_post_after_hook(message, values, model_description=model_description, mail_auto_delete=mail_auto_delete)
|
||||
@@ -0,0 +1,26 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
from odoo import api, models
|
||||
|
||||
class Partner(models.Model):
|
||||
_inherit = 'res.partner'
|
||||
|
||||
def _compute_im_status(self):
|
||||
#we asume that mail_bot _compute_im_status will be executed after bus _compute_im_status
|
||||
super(Partner, self)._compute_im_status()
|
||||
odoobot_id = self.env['ir.model.data'].xmlid_to_res_id("mail_bot.partner_odoobot")
|
||||
for partner in self:
|
||||
if partner.id == odoobot_id:
|
||||
partner.im_status = 'bot'
|
||||
|
||||
@api.model
|
||||
def get_mention_suggestions(self, search, limit=8):
|
||||
#add odoobot in mention suggestion when pinging in mail_thread
|
||||
[users, partners] = super(Partner, self).get_mention_suggestions(search, limit=limit)
|
||||
if len(partners) + len(users) < limit and "odoobot".startswith(search.lower()):
|
||||
odoobot = self.env.ref("mail_bot.partner_odoobot")
|
||||
if not any([elem['id'] == odoobot.id for elem in partners]):
|
||||
if odoobot:
|
||||
partners.append({'id': odoobot.id, 'name': odoobot.name, 'email': odoobot.email})
|
||||
return [users, partners]
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import models, fields
|
||||
|
||||
class Users(models.Model):
|
||||
_inherit = 'res.users'
|
||||
|
||||
odoobot_initialized = fields.Boolean(readonly=True, default=False)
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 40 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 30 KiB |
@@ -0,0 +1,28 @@
|
||||
odoo.define('mail_bot.MailBotService', function (require) {
|
||||
"use strict";
|
||||
|
||||
var AbstractService = require('web.AbstractService');
|
||||
var core = require('web.core');
|
||||
var session = require('web.session');
|
||||
|
||||
var MailBotService = AbstractService.extend({
|
||||
name: 'mailbot_service',
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
start: function () {
|
||||
var self = this;
|
||||
if ('odoobot_initialized' in session && ! session.odoobot_initialized) {
|
||||
setTimeout(function () {
|
||||
session.odoobot_initialized = true;
|
||||
self._rpc({
|
||||
model: 'mail.channel',
|
||||
method: 'init_odoobot',
|
||||
});
|
||||
}, 2*60*1000);
|
||||
}
|
||||
},
|
||||
});
|
||||
core.serviceRegistry.add('mailbot_service', MailBotService);
|
||||
return MailBotService;
|
||||
});
|
||||
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<data>
|
||||
<template id="assets_backend" name="mailbot assets" inherit_id="web.assets_backend">
|
||||
<xpath expr="." position="inside">
|
||||
<script type="text/javascript" src="/mail_bot/static/src/js/mailbot_service.js"></script>
|
||||
</xpath>
|
||||
</template>
|
||||
</data>
|
||||
</odoo>
|
||||
@@ -0,0 +1,13 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates xml:space="preserve">
|
||||
<t t-extend="mail.Preview">
|
||||
<t t-jquery="div[t-attf-class*='o_mail_preview_image']" t-operation="append">
|
||||
<i t-if="preview.status == 'bot'" class="o_mail_user_status o_user_online fa fa-heart" title="Bot" role="img" aria-label="User is a bot"/>
|
||||
</t>
|
||||
</t>
|
||||
<t t-extend="mail.UserStatus">
|
||||
<t t-jquery="i:last" t-operation="after">
|
||||
<i t-if="status == 'bot'" class="o_mail_user_status o_user_online fa fa-heart" title="Bot" role="img" aria-label="User is a bot"/>
|
||||
</t>
|
||||
</t>
|
||||
</templates>
|
||||
@@ -9,7 +9,7 @@
|
||||
'description': """This module contains tests related to mail. Those are
|
||||
contained in a separate module as it contains models used only to perform
|
||||
tests independently to functional aspects of other models. """,
|
||||
'depends': ['test_performance', 'mail'],
|
||||
'depends': ['test_performance', 'mail', 'mail_bot'],
|
||||
'data': [
|
||||
'security/ir.model.access.csv',
|
||||
'data/data.xml',
|
||||
|
||||
@@ -15,4 +15,5 @@ from . import test_ir_actions
|
||||
from . import test_update_notification
|
||||
from . import test_discuss
|
||||
from . import test_performance
|
||||
from . import test_res_users
|
||||
from . import test_res_users
|
||||
from . import test_odoobot
|
||||
|
||||
@@ -0,0 +1,124 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from unittest.mock import patch
|
||||
|
||||
from odoo.addons.test_mail.tests.common import BaseFunctionalTest, MockEmails, TestRecipients
|
||||
from odoo.tools import mute_logger
|
||||
from odoo.tests import tagged
|
||||
|
||||
|
||||
@tagged("odoobot")
|
||||
class TestOdoobot(BaseFunctionalTest, MockEmails, TestRecipients):
|
||||
|
||||
def setUp(self):
|
||||
super(TestOdoobot, self).setUp()
|
||||
self.odoobot = self.env.ref("mail_bot.partner_odoobot")
|
||||
self.message_post_default_kwargs = {
|
||||
'body': '',
|
||||
'attachment_ids': [],
|
||||
'message_type': 'comment',
|
||||
'partner_ids': [],
|
||||
'subtype': 'mail.mt_comment'
|
||||
}
|
||||
self.odoobot_ping_body = '<a href="http://odoo.com/web#model=res.partner&id=%s" class="o_mail_redirect" data-oe-id="%s" data-oe-model="res.partner" target="_blank">@OdooBot</a>' % (self.odoobot.id, self.odoobot.id)
|
||||
self.test_record_employe = self.test_record.sudo(self.user_employee)
|
||||
|
||||
@mute_logger('odoo.addons.mail.models.mail_mail')
|
||||
def test_odoobot_ping(self):
|
||||
kwargs = self.message_post_default_kwargs.copy()
|
||||
kwargs.update({'body': self.odoobot_ping_body, 'partner_ids': [self.odoobot.id, self.user_admin.partner_id.id]})
|
||||
|
||||
with patch('random.choice', lambda x: x[0]):
|
||||
self.assertNextMessage(
|
||||
self.test_record_employe.with_context({'mail_post_autofollow': True}).message_post(**kwargs),
|
||||
sender=self.odoobot,
|
||||
answer=["Yaaaay that's me!"] # no the perfect idea to base a test on a random but it should work for test phase, better mock random choice?
|
||||
)
|
||||
# Odoobot should not be a follower but user_employee and user_admin should
|
||||
follower = self.test_record.message_follower_ids.mapped('partner_id')
|
||||
self.assertNotIn(self.odoobot, follower)
|
||||
self.assertIn(self.user_employee.partner_id, follower)
|
||||
self.assertIn(self.user_admin.partner_id, follower)
|
||||
|
||||
@mute_logger('odoo.addons.mail.models.mail_mail')
|
||||
def test_onboarding_flow(self):
|
||||
kwargs = self.message_post_default_kwargs.copy()
|
||||
channel = self.env['mail.channel'].sudo(self.user_employee).init_odoobot()
|
||||
|
||||
kwargs['body'] = 'tagada 😊'
|
||||
self.assertNextMessage(
|
||||
channel.message_post(**kwargs),
|
||||
sender=self.odoobot,
|
||||
answer="Great! :) Did you notice that you can also send attachments, like a picture of your cute dog? Try it!"
|
||||
)
|
||||
kwargs['body'] = ''
|
||||
kwargs['attachment_ids'] = [1]
|
||||
last_message = self.assertNextMessage(
|
||||
channel.message_post(**kwargs),
|
||||
sender=self.odoobot,
|
||||
answer="Not a cute dog, but you get it :) To access special features, start your sentence with '/' (e.g. /help)."
|
||||
)
|
||||
kwargs['attachment_ids'] = []
|
||||
|
||||
channel.execute_command(command="help")
|
||||
self.assertNextMessage(
|
||||
last_message, # no message will be post with command help, use last odoobot message instead
|
||||
sender=self.odoobot,
|
||||
answer="Wow you are a natural! Ping someone to grab its attention with @nameoftheuser. Try to ping me with <b>@OdooBot</b>."
|
||||
)
|
||||
# we dont test the end of the flow since it will depends of the installed apps (livechat)
|
||||
|
||||
kwargs['partner_ids'] = []
|
||||
kwargs['body'] = "I love you"
|
||||
self.assertNextMessage(
|
||||
channel.message_post(**kwargs),
|
||||
sender=self.odoobot,
|
||||
answer="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 ❤️"
|
||||
)
|
||||
kwargs['body'] = "Go fuck yourself"
|
||||
self.assertNextMessage(
|
||||
channel.message_post(**kwargs),
|
||||
sender=self.odoobot,
|
||||
answer="That's not a really nice thing to say, you know? I'm a bot but I have feelings, ok?! 💔"
|
||||
)
|
||||
# we should have a default answer
|
||||
with patch('random.choice', lambda x: x[0]):
|
||||
kwargs['body'] = "I'm batman"
|
||||
self.assertNextMessage(
|
||||
channel.message_post(**kwargs),
|
||||
sender=self.odoobot,
|
||||
answer="Mmmmh I'm not sure what you mean.. Can you try again?"
|
||||
)
|
||||
|
||||
@mute_logger('odoo.addons.mail.models.mail_mail')
|
||||
def test_odoobot_no_default_answer(self):
|
||||
kwargs = self.message_post_default_kwargs.copy()
|
||||
kwargs.update({'body': "I'm not talking to @odoobot right now", 'partner_ids': []})
|
||||
self.assertNextMessage(
|
||||
self.test_record_employe.message_post(**kwargs),
|
||||
answer=False
|
||||
)
|
||||
|
||||
def assertNextMessage(self, message, answer=None, sender=None):
|
||||
last_message = self.env['mail.message'].search([('id', '=', message.id + 1)])
|
||||
if last_message:
|
||||
body = last_message.body.replace('<p>', '').replace('</p>', '')
|
||||
else:
|
||||
self.assertFalse(answer, "No last message found when an answer was expect")
|
||||
if answer is not None:
|
||||
if answer and not last_message:
|
||||
self.assertTrue(False, "No last message found")
|
||||
if isinstance(answer, list):
|
||||
self.assertIn(body, answer)
|
||||
elif isinstance(answer, tuple):
|
||||
for elem in answer:
|
||||
self.assertIn(elem, body)
|
||||
elif not answer:
|
||||
self.assertFalse(last_message, "No answer should have been post")
|
||||
return
|
||||
else:
|
||||
self.assertEqual(body, answer)
|
||||
if sender:
|
||||
self.assertEqual(sender, last_message.author_id)
|
||||
return last_message
|
||||
@@ -166,7 +166,7 @@ class TestAdvMailPerformance(TransactionCase):
|
||||
'default_res_model': 'mail.test.activity',
|
||||
})
|
||||
|
||||
with self.assertQueryCount(margin=1, admin=9, emp=13): # test_mail only: 9 - 13
|
||||
with self.assertQueryCount(margin=1, admin=10, emp=15): # test_mail only: 9 - 13
|
||||
activity = MailActivity.create({
|
||||
'summary': 'Test Activity',
|
||||
'res_id': record.id,
|
||||
@@ -182,7 +182,7 @@ class TestAdvMailPerformance(TransactionCase):
|
||||
def test_adv_activity_mixin(self):
|
||||
record = self.env['mail.test.activity'].create({'name': 'Test'})
|
||||
|
||||
with self.assertQueryCount(margin=1, admin=9, emp=13): # test_mail only: 9 - 13
|
||||
with self.assertQueryCount(margin=1, admin=10, emp=15): # test_mail only: 9 - 13
|
||||
record.action_start('Test Start')
|
||||
|
||||
record.write({'name': 'Dupe write'})
|
||||
@@ -199,7 +199,7 @@ class TestAdvMailPerformance(TransactionCase):
|
||||
self.user_test.write({'notification_type': 'email'})
|
||||
record = self.env['mail.test.track'].create({'name': 'Test'})
|
||||
|
||||
with self.assertQueryCount(margin=1, admin=56, emp=71): # com runbot: 56 - 71 // test_mail only: 56 - 71
|
||||
with self.assertQueryCount(margin=1, admin=56, emp=75): # com runbot: 56 - 75 // test_mail only: 56 - 75
|
||||
record.write({
|
||||
'user_id': self.user_test.id,
|
||||
})
|
||||
@@ -209,7 +209,7 @@ class TestAdvMailPerformance(TransactionCase):
|
||||
def test_message_assignation_inbox(self):
|
||||
record = self.env['mail.test.track'].create({'name': 'Test'})
|
||||
|
||||
with self.assertQueryCount(margin=1, admin=36, emp=46): # test_mail only: 36 - 46
|
||||
with self.assertQueryCount(margin=1, admin=44, emp=55): # test_mail only: 43 - 54
|
||||
record.write({
|
||||
'user_id': self.user_test.id,
|
||||
})
|
||||
@@ -253,7 +253,7 @@ class TestAdvMailPerformance(TransactionCase):
|
||||
def test_message_post_one_email_notification(self):
|
||||
record = self.env['mail.test.simple'].create({'name': 'Test'})
|
||||
|
||||
with self.assertQueryCount(margin=1, admin=47, emp=64): # com runbot: 45 - 62 // test_mail only: 47 - 64
|
||||
with self.assertQueryCount(margin=1, admin=47, emp=67): # com runbot: 45 - 65 // test_mail only: 47 - 67
|
||||
record.message_post(
|
||||
body='<p>Test Post Performances with an email ping</p>',
|
||||
partner_ids=self.customer.ids,
|
||||
@@ -389,7 +389,7 @@ class TestHeavyMailPerformance(TransactionCase):
|
||||
self.umbrella.message_subscribe(self.user_portal.partner_id.ids)
|
||||
record = self.umbrella.sudo(self.env.user)
|
||||
|
||||
with self.assertQueryCount(admin=79, emp=99): # com runbot 77 - 97 // test_mail only: 79 - 99
|
||||
with self.assertQueryCount(admin=97, emp=124): # com runbot 90 - 113 // test_mail only: 97 - 120
|
||||
record.message_post(
|
||||
body='<p>Test Post Performances</p>',
|
||||
message_type='comment',
|
||||
@@ -406,7 +406,7 @@ class TestHeavyMailPerformance(TransactionCase):
|
||||
record = self.umbrella.sudo(self.env.user)
|
||||
template_id = self.env.ref('test_mail.mail_test_tpl').id
|
||||
|
||||
with self.assertQueryCount(admin=98, emp=130): # com runbot 96 - 128 // test_mail only: 98 - 130
|
||||
with self.assertQueryCount(admin=116, emp=155): # com runbot 109 - 144 // test_mail only: 116 - 151
|
||||
record.message_post_with_template(template_id, message_type='comment', composition_mode='comment')
|
||||
|
||||
self.assertEqual(record.message_ids[0].body, '<p>Adding stuff on %s</p>' % record.name)
|
||||
@@ -476,7 +476,7 @@ class TestHeavyMailPerformance(TransactionCase):
|
||||
})
|
||||
self.assertEqual(rec.message_partner_ids, self.partners | self.env.user.partner_id)
|
||||
|
||||
with self.assertQueryCount(admin=58, emp=73): # com runbot: 58 - 73 // test_mail only: 58 - 73
|
||||
with self.assertQueryCount(admin=66, emp=88): # com runbot: 65 - 83 // test_mail only: 66 - 84
|
||||
rec.write({'user_id': self.user_portal.id})
|
||||
|
||||
self.assertEqual(rec.message_partner_ids, self.partners | self.env.user.partner_id | self.user_portal.partner_id)
|
||||
@@ -499,7 +499,7 @@ class TestHeavyMailPerformance(TransactionCase):
|
||||
customer_id = self.customer.id
|
||||
user_id = self.user_portal.id
|
||||
|
||||
with self.assertQueryCount(margin=1, admin=155, emp=184): # com runbot: 155 - 184 // test_mail only: 155 - 184
|
||||
with self.assertQueryCount(margin=1, admin=155, emp=190): # com runbot: 155 - 190 // test_mail only: 155 - 190
|
||||
rec = self.env['mail.test.full'].create({
|
||||
'name': 'Test',
|
||||
'umbrella_id': umbrella_id,
|
||||
@@ -528,7 +528,7 @@ class TestHeavyMailPerformance(TransactionCase):
|
||||
})
|
||||
self.assertEqual(rec.message_partner_ids, self.user_portal.partner_id | self.env.user.partner_id)
|
||||
|
||||
with self.assertQueryCount(margin=1, admin=94, emp=109): # com runbot: 94 - 109 // test_mail only: 94 - 109
|
||||
with self.assertQueryCount(margin=1, admin=105, emp=126): # com runbot: 104 - 123 // test_mail only: 105 - 124
|
||||
rec.write({
|
||||
'name': 'Test2',
|
||||
'umbrella_id': self.umbrella.id,
|
||||
@@ -566,7 +566,7 @@ class TestHeavyMailPerformance(TransactionCase):
|
||||
})
|
||||
self.assertEqual(rec.message_partner_ids, self.user_portal.partner_id | self.env.user.partner_id)
|
||||
|
||||
with self.assertQueryCount(margin=1, admin=100, emp=119): # test_mail only: 100 - 119
|
||||
with self.assertQueryCount(margin=1, admin=111, emp=137): # test_mail only: 111 - 134
|
||||
rec.write({
|
||||
'name': 'Test2',
|
||||
'umbrella_id': umbrella_id,
|
||||
@@ -600,7 +600,7 @@ class TestHeavyMailPerformance(TransactionCase):
|
||||
})
|
||||
self.assertEqual(rec.message_partner_ids, self.partners | self.env.user.partner_id | self.user_portal.partner_id)
|
||||
|
||||
with self.assertQueryCount(admin=54, emp=75): # test_mail only: 54 - 75
|
||||
with self.assertQueryCount(admin=54, emp=76): # test_mail only: 54 - 75
|
||||
rec.write({
|
||||
'name': 'Test2',
|
||||
'customer_id': customer_id,
|
||||
|
||||
Reference in New Issue
Block a user