diff options
| author | XavierDo <xdo@odoo.com> | 2018-08-02 13:18:53 +0200 |
|---|---|---|
| committer | Thibault Delavallée <tde@odoo.com> | 2018-08-10 13:33:28 +0200 |
| commit | cf505c36985221819139ce30d7f03e6e51ec0298 (patch) | |
| tree | 39c7cd3e7cff094a8b4b231aa87eb3e08b62cbbf /addons/mail_bot/models | |
| parent | c446fb20015d489d9c38e97e10f25cf2e5e3efd7 (diff) | |
[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.
Diffstat (limited to 'addons/mail_bot/models')
| -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 |
7 files changed, 326 insertions, 0 deletions
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) |
