[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:
XavierDo
2018-08-10 13:33:28 +02:00
committed by Thibault Delavallée
parent c446fb2001
commit cf505c3698
30 changed files with 615 additions and 17 deletions
@@ -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) {
+4
View File
@@ -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)
+5 -1
View File
@@ -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>
+4
View File
@@ -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;
+4
View File
@@ -0,0 +1,4 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import models
+22
View File
@@ -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',
],
}
+12
View File
@@ -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>
+9
View File
@@ -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
+13
View File
@@ -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
+216
View File
@@ -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
+41
View File
@@ -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
+12
View File
@@ -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)
+26
View File
@@ -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]
+9
View File
@@ -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;
});
+10
View File
@@ -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>
+13
View File
@@ -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>
+1 -1
View File
@@ -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',
+2 -1
View File
@@ -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
+124
View File
@@ -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&amp;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
+12 -12
View File
@@ -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,