diff options
| author | Thibault Delavallée <tde@odoo.com> | 2018-08-10 13:43:47 +0200 |
|---|---|---|
| committer | Thibault Delavallée <tde@odoo.com> | 2018-08-10 13:43:47 +0200 |
| commit | 1e7eb76c30c97390a3f71d79ce6bcfef49cceabc (patch) | |
| tree | e3f9a9b98ada989c2d82fb89fb91e5edc79c6bbb /addons/mail_bot | |
| parent | 7499b47ffab0712e8a03655365b11450716e5717 (diff) | |
| parent | cf505c36985221819139ce30d7f03e6e51ec0298 (diff) | |
[MERGE][IMP] mail, mail_bot: improve Discuss onboarding and add your new best friend
Purpose of this merge is to improve onboarding of Discuss. This is done via
a wow effect and a bot to test the Discuss app; otherwise you have nobody to
talk to. Purpose is also to improve retention as "the best way to increase
retention is this: when the user invites someone, when this guy activates its
account, the user gets a push notification from the invited user that just
logged in; that way he will come back to Odoo and start discussing with its
colleagues.
Concerning your new best friend: it is not a dog but Odoobot.
See sub commits for more details. This merge is linked to task ID 1838588 and
closes PR #25075.
Diffstat (limited to 'addons/mail_bot')
| -rw-r--r-- | addons/mail_bot/__init__.py | 4 | ||||
| -rw-r--r-- | addons/mail_bot/__manifest__.py | 22 | ||||
| -rw-r--r-- | addons/mail_bot/data/mailbot_data.xml | 12 | ||||
| -rw-r--r-- | addons/mail_bot/models/__init__.py | 9 | ||||
| -rw-r--r-- | addons/mail_bot/models/ir_http.py | 13 | ||||
| -rw-r--r-- | addons/mail_bot/models/mail_bot.py | 216 | ||||
| -rw-r--r-- | addons/mail_bot/models/mail_channel.py | 41 | ||||
| -rw-r--r-- | addons/mail_bot/models/mail_thread.py | 12 | ||||
| -rw-r--r-- | addons/mail_bot/models/res_partner.py | 26 | ||||
| -rw-r--r-- | addons/mail_bot/models/res_users.py | 9 | ||||
| -rw-r--r-- | addons/mail_bot/static/img/odoobot_chatter.png | bin | 0 -> 40576 bytes | |||
| -rw-r--r-- | addons/mail_bot/static/img/odoobot_discuss.png | bin | 0 -> 30404 bytes | |||
| -rw-r--r-- | addons/mail_bot/static/src/js/mailbot_service.js | 28 | ||||
| -rw-r--r-- | addons/mail_bot/views/assets.xml | 10 | ||||
| -rw-r--r-- | addons/mail_bot/views/discuss.xml | 13 |
15 files changed, 415 insertions, 0 deletions
diff --git a/addons/mail_bot/__init__.py b/addons/mail_bot/__init__.py new file mode 100644 index 00000000000..dc5e6b693d1 --- /dev/null +++ b/addons/mail_bot/__init__.py @@ -0,0 +1,4 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from . import models diff --git a/addons/mail_bot/__manifest__.py b/addons/mail_bot/__manifest__.py new file mode 100644 index 00000000000..01885576d16 --- /dev/null +++ b/addons/mail_bot/__manifest__.py @@ -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', + ], +} diff --git a/addons/mail_bot/data/mailbot_data.xml b/addons/mail_bot/data/mailbot_data.xml new file mode 100644 index 00000000000..96fe760ca1f --- /dev/null +++ b/addons/mail_bot/data/mailbot_data.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> diff --git a/addons/mail_bot/models/__init__.py b/addons/mail_bot/models/__init__.py new file mode 100644 index 00000000000..82aa165ecaf --- /dev/null +++ b/addons/mail_bot/models/__init__.py @@ -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 diff --git a/addons/mail_bot/models/ir_http.py b/addons/mail_bot/models/ir_http.py new file mode 100644 index 00000000000..ae01cb3e76f --- /dev/null +++ b/addons/mail_bot/models/ir_http.py @@ -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 diff --git a/addons/mail_bot/models/mail_bot.py b/addons/mail_bot/models/mail_bot.py new file mode 100644 index 00000000000..477e6aab7ee --- /dev/null +++ b/addons/mail_bot/models/mail_bot.py @@ -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 diff --git a/addons/mail_bot/models/mail_channel.py b/addons/mail_bot/models/mail_channel.py new file mode 100644 index 00000000000..23978eff1e6 --- /dev/null +++ b/addons/mail_bot/models/mail_channel.py @@ -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 diff --git a/addons/mail_bot/models/mail_thread.py b/addons/mail_bot/models/mail_thread.py new file mode 100644 index 00000000000..81e9fa44711 --- /dev/null +++ b/addons/mail_bot/models/mail_thread.py @@ -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) diff --git a/addons/mail_bot/models/res_partner.py b/addons/mail_bot/models/res_partner.py new file mode 100644 index 00000000000..941972a7184 --- /dev/null +++ b/addons/mail_bot/models/res_partner.py @@ -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] + diff --git a/addons/mail_bot/models/res_users.py b/addons/mail_bot/models/res_users.py new file mode 100644 index 00000000000..8c880b11a6e --- /dev/null +++ b/addons/mail_bot/models/res_users.py @@ -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) diff --git a/addons/mail_bot/static/img/odoobot_chatter.png b/addons/mail_bot/static/img/odoobot_chatter.png Binary files differnew file mode 100644 index 00000000000..e9147652083 --- /dev/null +++ b/addons/mail_bot/static/img/odoobot_chatter.png diff --git a/addons/mail_bot/static/img/odoobot_discuss.png b/addons/mail_bot/static/img/odoobot_discuss.png Binary files differnew file mode 100644 index 00000000000..18949ce9f9e --- /dev/null +++ b/addons/mail_bot/static/img/odoobot_discuss.png diff --git a/addons/mail_bot/static/src/js/mailbot_service.js b/addons/mail_bot/static/src/js/mailbot_service.js new file mode 100644 index 00000000000..300e95d06db --- /dev/null +++ b/addons/mail_bot/static/src/js/mailbot_service.js @@ -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; +}); diff --git a/addons/mail_bot/views/assets.xml b/addons/mail_bot/views/assets.xml new file mode 100644 index 00000000000..134b54e931e --- /dev/null +++ b/addons/mail_bot/views/assets.xml @@ -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> diff --git a/addons/mail_bot/views/discuss.xml b/addons/mail_bot/views/discuss.xml new file mode 100644 index 00000000000..e7f0650c867 --- /dev/null +++ b/addons/mail_bot/views/discuss.xml @@ -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> |
